openzeppelin_relayer/services/provider/stellar/
mod.rs

1//! Stellar Provider implementation for interacting with Stellar blockchain networks.
2//!
3//! This module provides functionality to interact with Stellar networks through RPC calls.
4//! It implements common operations like getting accounts, sending transactions, and querying
5//! blockchain state and events.
6
7use async_trait::async_trait;
8use eyre::Result;
9use soroban_rs::stellar_rpc_client::Client;
10use soroban_rs::stellar_rpc_client::{
11    Error as StellarClientError, EventStart, EventType, GetEventsResponse, GetLatestLedgerResponse,
12    GetLedgerEntriesResponse, GetNetworkResponse, GetTransactionResponse, GetTransactionsRequest,
13    GetTransactionsResponse, SendTransactionResponse, SimulateTransactionResponse,
14};
15use soroban_rs::xdr::{
16    AccountEntry, ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp,
17    LedgerKey, Limits, MuxedAccount, Operation, OperationBody, ReadXdr, ScAddress, ScSymbol, ScVal,
18    SequenceNumber, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM,
19    WriteXdr,
20};
21#[cfg(test)]
22use soroban_rs::xdr::{AccountId, LedgerKeyAccount, PublicKey};
23use soroban_rs::SorobanTransactionResponse;
24use std::sync::atomic::{AtomicU64, Ordering};
25
26#[cfg(test)]
27use mockall::automock;
28
29use once_cell::sync::Lazy;
30
31use crate::models::{JsonRpcId, RpcConfig};
32use crate::services::client_cache::SyncClientCache;
33use crate::services::provider::is_retriable_error;
34use crate::services::provider::retry::retry_rpc_call;
35use crate::services::provider::rpc_selector::RpcSelector;
36use crate::services::provider::should_mark_provider_failed;
37use crate::services::provider::RetryConfig;
38use crate::services::provider::{ProviderConfig, ProviderError};
39// Reqwest client is used for raw JSON-RPC HTTP requests. Alias to avoid name clash with the
40// soroban `Client` type imported above.
41use crate::utils::validate_safe_url;
42use reqwest::Client as ReqwestClient;
43use std::sync::Arc;
44use std::time::Duration;
45
46/// Generates a unique JSON-RPC request ID.
47///
48/// This function returns a monotonically increasing ID for JSON-RPC requests.
49/// It's thread-safe and guarantees unique IDs across concurrent requests.
50///
51/// # Returns
52///
53/// A unique u64 ID that can be used for JSON-RPC requests
54fn generate_unique_rpc_id() -> u64 {
55    static NEXT_ID: AtomicU64 = AtomicU64::new(1);
56    NEXT_ID.fetch_add(1, Ordering::Relaxed)
57}
58
59/// Cache for soroban_rs Stellar RPC clients, keyed by URL.
60/// Avoids recreating jsonrpsee HTTP clients on every retry attempt.
61static STELLAR_RPC_CLIENT_CACHE: Lazy<SyncClientCache<String, Client>> =
62    Lazy::new(SyncClientCache::new);
63
64/// Categorizes a Stellar client error into an appropriate `ProviderError` variant.
65///
66/// This function analyzes the given error and maps it to a specific `ProviderError` variant:
67/// - Handles StellarClientError variants directly (timeouts, JSON-RPC errors, etc.)
68/// - Extracts reqwest::Error from jsonrpsee Transport errors
69/// - Maps JSON-RPC error codes appropriately
70/// - Distinguishes between retriable network errors and non-retriable validation errors
71/// - Falls back to ProviderError::Other for unknown error types
72/// - Optionally prepends a context message to the error for better debugging
73///
74/// # Arguments
75///
76/// * `err` - The StellarClientError to categorize (takes ownership)
77/// * `context` - Optional context message to prepend (e.g., "Failed to get account")
78///
79/// # Returns
80///
81/// The appropriate `ProviderError` variant based on the error type
82fn categorize_stellar_error_with_context(
83    err: StellarClientError,
84    context: Option<&str>,
85) -> ProviderError {
86    let add_context = |msg: String| -> String {
87        match context {
88            Some(ctx) => format!("{ctx}: {msg}"),
89            None => msg,
90        }
91    };
92    match err {
93        // === Timeout Errors (Retriable) ===
94        StellarClientError::TransactionSubmissionTimeout => ProviderError::Timeout,
95
96        // === Address/Encoding Errors (Non-retriable, Client-side) ===
97        StellarClientError::InvalidAddress(decode_err) => ProviderError::InvalidAddress(
98            add_context(format!("Invalid Stellar address: {decode_err}")),
99        ),
100
101        // === XDR/Serialization Errors (Non-retriable, Client-side) ===
102        StellarClientError::Xdr(xdr_err) => {
103            ProviderError::Other(add_context(format!("XDR processing error: {xdr_err}")))
104        }
105
106        // === JSON Parsing Errors (Non-retriable, may indicate RPC response issue) ===
107        StellarClientError::Serde(serde_err) => {
108            ProviderError::Other(add_context(format!("JSON parsing error: {serde_err}")))
109        }
110
111        // === URL Configuration Errors (Non-retriable, Configuration issue) ===
112        StellarClientError::InvalidRpcUrl(uri_err) => {
113            ProviderError::NetworkConfiguration(add_context(format!("Invalid RPC URL: {uri_err}")))
114        }
115        StellarClientError::InvalidRpcUrlFromUriParts(uri_err) => {
116            ProviderError::NetworkConfiguration(add_context(format!(
117                "Invalid RPC URL parts: {uri_err}"
118            )))
119        }
120        StellarClientError::InvalidUrl(url) => {
121            ProviderError::NetworkConfiguration(add_context(format!("Invalid URL: {url}")))
122        }
123
124        // === Network Passphrase Mismatch (Non-retriable, Configuration issue) ===
125        StellarClientError::InvalidNetworkPassphrase { expected, server } => {
126            ProviderError::NetworkConfiguration(add_context(format!(
127                "Network passphrase mismatch: expected {expected:?}, server returned {server:?}"
128            )))
129        }
130
131        // === JSON-RPC Errors (May be retriable depending on the specific error) ===
132        StellarClientError::JsonRpc(jsonrpsee_err) => {
133            match jsonrpsee_err {
134                // Handle Call errors with error codes
135                jsonrpsee_core::error::Error::Call(err_obj) => {
136                    let code = err_obj.code() as i64;
137                    let message = add_context(err_obj.message().to_string());
138                    ProviderError::RpcErrorCode { code, message }
139                }
140
141                // Handle request timeouts
142                jsonrpsee_core::error::Error::RequestTimeout => ProviderError::Timeout,
143
144                // Handle transport errors (network-level issues)
145                jsonrpsee_core::error::Error::Transport(transport_err) => {
146                    // Check source chain for reqwest errors
147                    let mut source = transport_err.source();
148                    while let Some(s) = source {
149                        if let Some(reqwest_err) = s.downcast_ref::<reqwest::Error>() {
150                            return ProviderError::from(reqwest_err);
151                        }
152                        source = s.source();
153                    }
154
155                    ProviderError::TransportError(add_context(format!(
156                        "Transport error: {transport_err}"
157                    )))
158                }
159                // Catch-all for other jsonrpsee errors
160                other => ProviderError::Other(add_context(format!("JSON-RPC error: {other}"))),
161            }
162        }
163        // === Response Parsing/Validation Errors (May indicate RPC node issue) ===
164        StellarClientError::InvalidResponse => {
165            // This could be a temporary RPC node issue or malformed response
166            ProviderError::Other(add_context(
167                "Invalid response from Stellar RPC server".to_string(),
168            ))
169        }
170        StellarClientError::MissingResult => {
171            ProviderError::Other(add_context("Missing result in RPC response".to_string()))
172        }
173        StellarClientError::MissingError => ProviderError::Other(add_context(
174            "Failed to read error from RPC response".to_string(),
175        )),
176
177        // === Transaction Errors (Non-retriable, Transaction-specific issues) ===
178        StellarClientError::TransactionFailed(msg) => {
179            ProviderError::Other(add_context(format!("Transaction failed: {msg}")))
180        }
181        StellarClientError::TransactionSubmissionFailed(msg) => {
182            ProviderError::Other(add_context(format!("Transaction submission failed: {msg}")))
183        }
184        StellarClientError::TransactionSimulationFailed(msg) => {
185            ProviderError::Other(add_context(format!("Transaction simulation failed: {msg}")))
186        }
187        StellarClientError::UnexpectedTransactionStatus(status) => ProviderError::Other(
188            add_context(format!("Unexpected transaction status: {status}")),
189        ),
190
191        // === Resource Not Found Errors (Non-retriable) ===
192        StellarClientError::NotFound(resource, id) => {
193            ProviderError::Other(add_context(format!("{resource} not found: {id}")))
194        }
195
196        // === Client-side Validation Errors (Non-retriable) ===
197        StellarClientError::InvalidCursor => {
198            ProviderError::Other(add_context("Invalid cursor".to_string()))
199        }
200        StellarClientError::UnexpectedSimulateTransactionResultSize { length } => {
201            ProviderError::Other(add_context(format!(
202                "Unexpected simulate transaction result size: {length}"
203            )))
204        }
205        StellarClientError::UnexpectedOperationCount { count } => {
206            ProviderError::Other(add_context(format!("Unexpected operation count: {count}")))
207        }
208        StellarClientError::UnsupportedOperationType => {
209            ProviderError::Other(add_context("Unsupported operation type".to_string()))
210        }
211        StellarClientError::UnexpectedContractCodeDataType(data) => ProviderError::Other(
212            add_context(format!("Unexpected contract code data type: {data:?}")),
213        ),
214        StellarClientError::UnexpectedContractInstance(val) => ProviderError::Other(add_context(
215            format!("Unexpected contract instance: {val:?}"),
216        )),
217        StellarClientError::LargeFee(fee) => {
218            ProviderError::Other(add_context(format!("Fee too large: {fee}")))
219        }
220        StellarClientError::CannotAuthorizeRawTransaction => {
221            ProviderError::Other(add_context("Cannot authorize raw transaction".to_string()))
222        }
223        StellarClientError::MissingOp => {
224            ProviderError::Other(add_context("Missing operation in transaction".to_string()))
225        }
226        StellarClientError::MissingSignerForAddress { address } => ProviderError::Other(
227            add_context(format!("Missing signer for address: {address}")),
228        ),
229
230        // === Deprecated/Other Errors ===
231        #[allow(deprecated)]
232        StellarClientError::UnexpectedToken(entry) => {
233            ProviderError::Other(add_context(format!("Unexpected token: {entry:?}")))
234        }
235    }
236}
237
238/// Normalize a URL for logging by removing query strings, fragments and redacting userinfo.
239///
240/// Examples:
241/// - https://user:secret@api.example.com/path?api_key=XXX -> https://<redacted>@api.example.com/path
242/// - https://api.example.com/path?api_key=XXX -> https://api.example.com/path
243fn normalize_url_for_log(url: &str) -> String {
244    // Remove query and fragment first
245    let mut s = url.to_string();
246    if let Some(q) = s.find('?') {
247        s.truncate(q);
248    }
249    if let Some(h) = s.find('#') {
250        s.truncate(h);
251    }
252
253    // Redact userinfo if present (scheme://userinfo@host...)
254    if let Some(scheme_pos) = s.find("://") {
255        let start = scheme_pos + 3;
256        if let Some(at_pos) = s[start..].find('@') {
257            let after = &s[start + at_pos + 1..];
258            let prefix = &s[..start];
259            s = format!("{prefix}<redacted>@{after}");
260        }
261    }
262
263    s
264}
265#[derive(Debug, Clone)]
266pub struct GetEventsRequest {
267    pub start: EventStart,
268    pub event_type: Option<EventType>,
269    pub contract_ids: Vec<String>,
270    pub topics: Vec<Vec<String>>,
271    pub limit: Option<usize>,
272}
273
274#[derive(Clone, Debug)]
275pub struct StellarProvider {
276    /// RPC selector for managing and selecting providers
277    selector: RpcSelector,
278    /// Timeout in seconds for RPC calls
279    timeout_seconds: Duration,
280    /// Configuration for retry behavior
281    retry_config: RetryConfig,
282}
283
284#[async_trait]
285#[cfg_attr(test, automock)]
286#[allow(dead_code)]
287pub trait StellarProviderTrait: Send + Sync {
288    fn get_configs(&self) -> Vec<RpcConfig>;
289    async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError>;
290    async fn simulate_transaction_envelope(
291        &self,
292        tx_envelope: &TransactionEnvelope,
293    ) -> Result<SimulateTransactionResponse, ProviderError>;
294    async fn send_transaction_polling(
295        &self,
296        tx_envelope: &TransactionEnvelope,
297    ) -> Result<SorobanTransactionResponse, ProviderError>;
298    async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError>;
299    async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError>;
300    async fn send_transaction(
301        &self,
302        tx_envelope: &TransactionEnvelope,
303    ) -> Result<Hash, ProviderError>;
304    /// Sends a transaction and returns the full response including the status field.
305    ///
306    /// # Why this method exists
307    ///
308    /// The `stellar-rpc-client` crate's `send_transaction` method only returns
309    /// `Result<Hash, Error>` and discards the status field for non-ERROR responses.
310    /// This means TRY_AGAIN_LATER is silently treated as success, which is problematic
311    /// for relayers that need to track transaction states precisely.
312    ///
313    /// This method calls the `sendTransaction` RPC directly to get the full
314    /// `SendTransactionResponse` including the status field:
315    /// - "PENDING": Transaction accepted for processing
316    /// - "DUPLICATE": Transaction already submitted
317    /// - "TRY_AGAIN_LATER": Transaction NOT queued (e.g., another tx from same account
318    ///   in mempool, fee too low and resubmitted too soon, or resource limits exceeded)
319    /// - "ERROR": Transaction validation failed
320    async fn send_transaction_with_status(
321        &self,
322        tx_envelope: &TransactionEnvelope,
323    ) -> Result<SendTransactionResponse, ProviderError>;
324    async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError>;
325    async fn get_transactions(
326        &self,
327        request: GetTransactionsRequest,
328    ) -> Result<GetTransactionsResponse, ProviderError>;
329    async fn get_ledger_entries(
330        &self,
331        keys: &[LedgerKey],
332    ) -> Result<GetLedgerEntriesResponse, ProviderError>;
333    async fn get_events(
334        &self,
335        request: GetEventsRequest,
336    ) -> Result<GetEventsResponse, ProviderError>;
337    async fn raw_request_dyn(
338        &self,
339        method: &str,
340        params: serde_json::Value,
341        id: Option<JsonRpcId>,
342    ) -> Result<serde_json::Value, ProviderError>;
343    /// Calls a contract function (read-only, via simulation).
344    ///
345    /// This method invokes a Soroban contract function without submitting a transaction.
346    /// It uses simulation to execute the function and return the result.
347    ///
348    /// # Arguments
349    /// * `contract_address` - The contract address in StrKey format
350    /// * `function_name` - The function name as an ScSymbol
351    /// * `args` - Function arguments as ScVal vector
352    ///
353    /// # Returns
354    /// The function result as an ScVal, or an error if the call fails
355    async fn call_contract(
356        &self,
357        contract_address: &str,
358        function_name: &ScSymbol,
359        args: Vec<ScVal>,
360    ) -> Result<ScVal, ProviderError>;
361}
362
363impl StellarProvider {
364    // Create new StellarProvider instance
365    pub fn new(config: ProviderConfig) -> Result<Self, ProviderError> {
366        if config.rpc_configs.is_empty() {
367            return Err(ProviderError::NetworkConfiguration(
368                "No RPC configurations provided for StellarProvider".to_string(),
369            ));
370        }
371
372        RpcConfig::validate_list(&config.rpc_configs)
373            .map_err(|e| ProviderError::NetworkConfiguration(e.to_string()))?;
374
375        let mut rpc_configs = config.rpc_configs;
376        rpc_configs.retain(|config| config.get_weight() > 0);
377
378        if rpc_configs.is_empty() {
379            return Err(ProviderError::NetworkConfiguration(
380                "No active RPC configurations provided (all weights are 0 or list was empty after filtering)".to_string(),
381            ));
382        }
383
384        let selector = RpcSelector::new(
385            rpc_configs,
386            config.failure_threshold,
387            config.pause_duration_secs,
388            config.failure_expiration_secs,
389        )
390        .map_err(|e| {
391            ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
392        })?;
393
394        let retry_config = RetryConfig::from_env();
395
396        Ok(Self {
397            selector,
398            timeout_seconds: Duration::from_secs(config.timeout_seconds),
399            retry_config,
400        })
401    }
402
403    /// Gets the current RPC configurations.
404    ///
405    /// # Returns
406    /// * `Vec<RpcConfig>` - The current configurations
407    pub fn get_configs(&self) -> Vec<RpcConfig> {
408        self.selector.get_configs()
409    }
410
411    /// Get or create a cached Stellar RPC client for a given URL.
412    /// Reuses clients across retry attempts and provider instances.
413    fn initialize_provider(&self, url: &str) -> Result<Arc<Client>, ProviderError> {
414        let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
415        let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
416        validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
417            ProviderError::NetworkConfiguration(format!("RPC URL security validation failed: {e}"))
418        })?;
419
420        STELLAR_RPC_CLIENT_CACHE.get_or_try_init(url.to_string(), || {
421            Client::new(url).map_err(|e| {
422                let safe_url = crate::utils::mask_url(url);
423                ProviderError::NetworkConfiguration(format!(
424                    "Failed to create Stellar RPC client: {e} - URL: '{safe_url}'"
425                ))
426            })
427        })
428    }
429
430    /// Get the shared reqwest client for raw HTTP JSON-RPC calls, after
431    /// validating the URL as an SSRF safety net.
432    fn initialize_raw_provider(&self, url: &str) -> Result<ReqwestClient, ProviderError> {
433        let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
434        let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
435        validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
436            ProviderError::NetworkConfiguration(format!("RPC URL security validation failed: {e}"))
437        })?;
438
439        super::get_shared_rpc_http_client()
440    }
441
442    /// Helper method to retry RPC calls with exponential backoff
443    async fn retry_rpc_call<T, F, Fut>(
444        &self,
445        operation_name: &str,
446        operation: F,
447    ) -> Result<T, ProviderError>
448    where
449        F: Fn(Arc<Client>) -> Fut,
450        Fut: std::future::Future<Output = Result<T, ProviderError>>,
451    {
452        let provider_url_raw = match self.selector.get_current_url() {
453            Ok(url) => url,
454            Err(e) => {
455                return Err(ProviderError::NetworkConfiguration(format!(
456                    "No RPC URL available for StellarProvider: {e}"
457                )));
458            }
459        };
460        let provider_url = normalize_url_for_log(&provider_url_raw);
461
462        tracing::debug!(
463            "Starting Stellar RPC operation '{}' with timeout: {}s, provider_url: {}",
464            operation_name,
465            self.timeout_seconds.as_secs(),
466            provider_url
467        );
468
469        retry_rpc_call(
470            &self.selector,
471            operation_name,
472            is_retriable_error,
473            should_mark_provider_failed,
474            |url| self.initialize_provider(url),
475            operation,
476            Some(self.retry_config.clone()),
477        )
478        .await
479    }
480
481    /// Retry helper for raw JSON-RPC requests
482    async fn retry_raw_request(
483        &self,
484        operation_name: &str,
485        request: serde_json::Value,
486    ) -> Result<serde_json::Value, ProviderError> {
487        let provider_url_raw = match self.selector.get_current_url() {
488            Ok(url) => url,
489            Err(e) => {
490                return Err(ProviderError::NetworkConfiguration(format!(
491                    "No RPC URL available for StellarProvider: {e}"
492                )));
493            }
494        };
495        let provider_url = normalize_url_for_log(&provider_url_raw);
496
497        tracing::debug!(
498            "Starting raw RPC operation '{}' with timeout: {}s, provider_url: {}",
499            operation_name,
500            self.timeout_seconds.as_secs(),
501            provider_url
502        );
503
504        let request_clone = request.clone();
505        retry_rpc_call(
506            &self.selector,
507            operation_name,
508            is_retriable_error,
509            should_mark_provider_failed,
510            |url| {
511                // Initialize an HTTP client for this URL and return it together with the URL string
512                self.initialize_raw_provider(url)
513                    .map(|client| (url.to_string(), client))
514            },
515            |(url, client): (String, ReqwestClient)| {
516                let request_for_call = request_clone.clone();
517                async move {
518                    let response = client
519                        .post(&url)
520                        .json(&request_for_call)
521                        // Keep a per-request timeout as a safeguard (client also has a default timeout)
522                        .timeout(self.timeout_seconds)
523                        .send()
524                        .await
525                        .map_err(ProviderError::from)?;
526
527                    let json_response: serde_json::Value =
528                        response.json().await.map_err(ProviderError::from)?;
529
530                    Ok(json_response)
531                }
532            },
533            Some(self.retry_config.clone()),
534        )
535        .await
536    }
537}
538
539#[async_trait]
540impl StellarProviderTrait for StellarProvider {
541    fn get_configs(&self) -> Vec<RpcConfig> {
542        self.get_configs()
543    }
544
545    async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError> {
546        let account_id = Arc::new(account_id.to_string());
547
548        self.retry_rpc_call("get_account", move |client| {
549            let account_id = Arc::clone(&account_id);
550            async move {
551                client.get_account(&account_id).await.map_err(|e| {
552                    categorize_stellar_error_with_context(e, Some("Failed to get account"))
553                })
554            }
555        })
556        .await
557    }
558
559    async fn simulate_transaction_envelope(
560        &self,
561        tx_envelope: &TransactionEnvelope,
562    ) -> Result<SimulateTransactionResponse, ProviderError> {
563        let tx_envelope = Arc::new(tx_envelope.clone());
564
565        self.retry_rpc_call("simulate_transaction_envelope", move |client| {
566            let tx_envelope = Arc::clone(&tx_envelope);
567            async move {
568                client
569                    .simulate_transaction_envelope(&tx_envelope, None)
570                    .await
571                    .map_err(|e| {
572                        categorize_stellar_error_with_context(
573                            e,
574                            Some("Failed to simulate transaction"),
575                        )
576                    })
577            }
578        })
579        .await
580    }
581
582    async fn send_transaction_polling(
583        &self,
584        tx_envelope: &TransactionEnvelope,
585    ) -> Result<SorobanTransactionResponse, ProviderError> {
586        let tx_envelope = Arc::new(tx_envelope.clone());
587
588        self.retry_rpc_call("send_transaction_polling", move |client| {
589            let tx_envelope = Arc::clone(&tx_envelope);
590            async move {
591                client
592                    .send_transaction_polling(&tx_envelope)
593                    .await
594                    .map(SorobanTransactionResponse::from)
595                    .map_err(|e| {
596                        categorize_stellar_error_with_context(
597                            e,
598                            Some("Failed to send transaction (polling)"),
599                        )
600                    })
601            }
602        })
603        .await
604    }
605
606    async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError> {
607        self.retry_rpc_call("get_network", |client| async move {
608            client.get_network().await.map_err(|e| {
609                categorize_stellar_error_with_context(e, Some("Failed to get network"))
610            })
611        })
612        .await
613    }
614
615    async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError> {
616        self.retry_rpc_call("get_latest_ledger", |client| async move {
617            client.get_latest_ledger().await.map_err(|e| {
618                categorize_stellar_error_with_context(e, Some("Failed to get latest ledger"))
619            })
620        })
621        .await
622    }
623
624    async fn send_transaction(
625        &self,
626        tx_envelope: &TransactionEnvelope,
627    ) -> Result<Hash, ProviderError> {
628        let tx_envelope = Arc::new(tx_envelope.clone());
629
630        self.retry_rpc_call("send_transaction", move |client| {
631            let tx_envelope = Arc::clone(&tx_envelope);
632            async move {
633                client.send_transaction(&tx_envelope).await.map_err(|e| {
634                    categorize_stellar_error_with_context(e, Some("Failed to send transaction"))
635                })
636            }
637        })
638        .await
639    }
640
641    async fn send_transaction_with_status(
642        &self,
643        tx_envelope: &TransactionEnvelope,
644    ) -> Result<SendTransactionResponse, ProviderError> {
645        // Encode the transaction envelope to XDR base64
646        let tx_xdr = tx_envelope
647            .to_xdr_base64(Limits::none())
648            .map_err(|e| ProviderError::Other(format!("Failed to encode transaction XDR: {e}")))?;
649
650        // Call sendTransaction RPC method directly to get the full response
651        let params = serde_json::json!({
652            "transaction": tx_xdr
653        });
654
655        let result = self
656            .raw_request_dyn("sendTransaction", params, None)
657            .await?;
658
659        // Deserialize the response
660        serde_json::from_value(result).map_err(|e| {
661            ProviderError::Other(format!(
662                "Failed to deserialize SendTransactionResponse: {e}"
663            ))
664        })
665    }
666
667    async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError> {
668        let tx_id = Arc::new(tx_id.clone());
669
670        self.retry_rpc_call("get_transaction", move |client| {
671            let tx_id = Arc::clone(&tx_id);
672            async move {
673                client.get_transaction(&tx_id).await.map_err(|e| {
674                    categorize_stellar_error_with_context(e, Some("Failed to get transaction"))
675                })
676            }
677        })
678        .await
679    }
680
681    async fn get_transactions(
682        &self,
683        request: GetTransactionsRequest,
684    ) -> Result<GetTransactionsResponse, ProviderError> {
685        let request = Arc::new(request);
686
687        self.retry_rpc_call("get_transactions", move |client| {
688            let request = Arc::clone(&request);
689            async move {
690                client
691                    .get_transactions((*request).clone())
692                    .await
693                    .map_err(|e| {
694                        categorize_stellar_error_with_context(e, Some("Failed to get transactions"))
695                    })
696            }
697        })
698        .await
699    }
700
701    async fn get_ledger_entries(
702        &self,
703        keys: &[LedgerKey],
704    ) -> Result<GetLedgerEntriesResponse, ProviderError> {
705        let keys = Arc::new(keys.to_vec());
706
707        self.retry_rpc_call("get_ledger_entries", move |client| {
708            let keys = Arc::clone(&keys);
709            async move {
710                client.get_ledger_entries(&keys).await.map_err(|e| {
711                    categorize_stellar_error_with_context(e, Some("Failed to get ledger entries"))
712                })
713            }
714        })
715        .await
716    }
717
718    async fn get_events(
719        &self,
720        request: GetEventsRequest,
721    ) -> Result<GetEventsResponse, ProviderError> {
722        let request = Arc::new(request);
723
724        self.retry_rpc_call("get_events", move |client| {
725            let request = Arc::clone(&request);
726            async move {
727                client
728                    .get_events(
729                        request.start.clone(),
730                        request.event_type,
731                        &request.contract_ids,
732                        &request.topics,
733                        request.limit,
734                    )
735                    .await
736                    .map_err(|e| {
737                        categorize_stellar_error_with_context(e, Some("Failed to get events"))
738                    })
739            }
740        })
741        .await
742    }
743
744    async fn raw_request_dyn(
745        &self,
746        method: &str,
747        params: serde_json::Value,
748        id: Option<JsonRpcId>,
749    ) -> Result<serde_json::Value, ProviderError> {
750        let id_value = match id {
751            Some(id) => serde_json::to_value(id)
752                .map_err(|e| ProviderError::Other(format!("Failed to serialize id: {e}")))?,
753            None => serde_json::json!(generate_unique_rpc_id()),
754        };
755
756        let request = serde_json::json!({
757            "jsonrpc": "2.0",
758            "id": id_value,
759            "method": method,
760            "params": params,
761        });
762
763        let response = self.retry_raw_request("raw_request_dyn", request).await?;
764
765        // Check for JSON-RPC error
766        if let Some(error) = response.get("error") {
767            if let Some(code) = error.get("code").and_then(|c| c.as_i64()) {
768                return Err(ProviderError::RpcErrorCode {
769                    code,
770                    message: error
771                        .get("message")
772                        .and_then(|m| m.as_str())
773                        .unwrap_or("Unknown error")
774                        .to_string(),
775                });
776            }
777            return Err(ProviderError::Other(format!("JSON-RPC error: {error}")));
778        }
779
780        // Extract result
781        response
782            .get("result")
783            .cloned()
784            .ok_or_else(|| ProviderError::Other("No result field in JSON-RPC response".to_string()))
785    }
786
787    async fn call_contract(
788        &self,
789        contract_address: &str,
790        function_name: &ScSymbol,
791        args: Vec<ScVal>,
792    ) -> Result<ScVal, ProviderError> {
793        // Parse contract address
794        let contract = stellar_strkey::Contract::from_string(contract_address)
795            .map_err(|e| ProviderError::Other(format!("Invalid contract address: {e}")))?;
796        let contract_addr = ScAddress::Contract(ContractId(Hash(contract.0)));
797
798        // Convert args to VecM
799        let args_vec = VecM::try_from(args)
800            .map_err(|e| ProviderError::Other(format!("Failed to convert arguments: {e:?}")))?;
801
802        // Build InvokeHostFunction operation
803        let host_function = HostFunction::InvokeContract(InvokeContractArgs {
804            contract_address: contract_addr,
805            function_name: function_name.clone(),
806            args: args_vec,
807        });
808
809        let operation = Operation {
810            source_account: None,
811            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
812                host_function,
813                auth: VecM::try_from(vec![]).unwrap(),
814            }),
815        };
816
817        // Build a minimal transaction envelope for simulation
818        //
819        // Why simulation instead of direct reads?
820        // In Soroban, contract functions (even read-only ones like decimals()) must be invoked
821        // through the transaction system. Simulation is the standard way to call read-only
822        // functions because it:
823        // 1. Executes the contract function without submitting to the ledger (no fees, no state changes)
824        // 2. Returns the computed result immediately
825        // 3. Works for functions that compute values (not just storage reads)
826        //
827        // Direct storage reads (get_ledger_entries) only work if the value is stored in contract
828        // data storage. For functions that compute values, simulation is required.
829        //
830        // Use a dummy account - simulation doesn't require a real account or signature
831        let dummy_account = MuxedAccount::Ed25519(Uint256([0u8; 32]));
832        let operations: VecM<Operation, 100> = vec![operation].try_into().map_err(|e| {
833            ProviderError::Other(format!("Failed to create operations vector: {e:?}"))
834        })?;
835
836        let tx = Transaction {
837            source_account: dummy_account,
838            fee: 100,
839            seq_num: SequenceNumber(0),
840            cond: soroban_rs::xdr::Preconditions::None,
841            memo: soroban_rs::xdr::Memo::None,
842            operations,
843            ext: soroban_rs::xdr::TransactionExt::V0,
844        };
845
846        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
847            tx,
848            signatures: VecM::try_from(vec![]).unwrap(),
849        });
850
851        // Simulate the transaction to get the result (read-only execution, no ledger submission)
852        let sim_response = self.simulate_transaction_envelope(&envelope).await?;
853
854        // Check for simulation errors
855        if let Some(error) = sim_response.error {
856            return Err(ProviderError::Other(format!(
857                "Contract invocation simulation failed: {error}",
858            )));
859        }
860
861        // Extract result from simulation response
862        if sim_response.results.is_empty() {
863            return Err(ProviderError::Other(
864                "Simulation returned no results".to_string(),
865            ));
866        }
867
868        // Parse the XDR result as ScVal
869        let result_xdr = &sim_response.results[0].xdr;
870        ScVal::from_xdr_base64(result_xdr, Limits::none()).map_err(|e| {
871            ProviderError::Other(format!("Failed to parse simulation result XDR: {e}"))
872        })
873    }
874}
875
876#[cfg(test)]
877mod stellar_rpc_tests {
878    use super::*;
879    use crate::services::provider::stellar::{
880        GetEventsRequest, StellarProvider, StellarProviderTrait,
881    };
882    use futures::FutureExt;
883    use lazy_static::lazy_static;
884    use mockall::predicate as p;
885    use soroban_rs::stellar_rpc_client::{
886        EventStart, GetEventsResponse, GetLatestLedgerResponse, GetLedgerEntriesResponse,
887        GetNetworkResponse, GetTransactionEvents, GetTransactionResponse, GetTransactionsRequest,
888        GetTransactionsResponse, SimulateTransactionResponse,
889    };
890    use soroban_rs::xdr::{
891        AccountEntryExt, Hash, LedgerKey, OperationResult, String32, Thresholds,
892        TransactionEnvelope, TransactionResult, TransactionResultExt, TransactionResultResult,
893        VecM,
894    };
895    use soroban_rs::{create_mock_set_options_tx_envelope, SorobanTransactionResponse};
896    use std::str::FromStr;
897    use std::sync::Mutex;
898
899    lazy_static! {
900        static ref STELLAR_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
901    }
902
903    struct StellarTestEnvGuard {
904        _mutex_guard: std::sync::MutexGuard<'static, ()>,
905    }
906
907    impl StellarTestEnvGuard {
908        fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
909            std::env::set_var(
910                "API_KEY",
911                "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
912            );
913            std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
914            // Set minimal retry config to avoid excessive retries and TCP exhaustion in concurrent tests
915            std::env::set_var("PROVIDER_MAX_RETRIES", "1");
916            std::env::set_var("PROVIDER_MAX_FAILOVERS", "0");
917            std::env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "0");
918            std::env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "0");
919
920            Self {
921                _mutex_guard: mutex_guard,
922            }
923        }
924    }
925
926    impl Drop for StellarTestEnvGuard {
927        fn drop(&mut self) {
928            std::env::remove_var("API_KEY");
929            std::env::remove_var("REDIS_URL");
930            std::env::remove_var("PROVIDER_MAX_RETRIES");
931            std::env::remove_var("PROVIDER_MAX_FAILOVERS");
932            std::env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
933            std::env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
934        }
935    }
936
937    // Helper function to set up the test environment
938    fn setup_test_env() -> StellarTestEnvGuard {
939        let guard = STELLAR_TEST_ENV_MUTEX
940            .lock()
941            .unwrap_or_else(|e| e.into_inner());
942        StellarTestEnvGuard::new(guard)
943    }
944
945    fn dummy_hash() -> Hash {
946        Hash([0u8; 32])
947    }
948
949    fn dummy_get_network_response() -> GetNetworkResponse {
950        GetNetworkResponse {
951            friendbot_url: Some("https://friendbot.testnet.stellar.org/".into()),
952            passphrase: "Test SDF Network ; September 2015".into(),
953            protocol_version: 20,
954        }
955    }
956
957    fn dummy_get_latest_ledger_response() -> GetLatestLedgerResponse {
958        GetLatestLedgerResponse {
959            id: "c73c5eac58a441d4eb733c35253ae85f783e018f7be5ef974258fed067aabb36".into(),
960            protocol_version: 20,
961            sequence: 2_539_605,
962        }
963    }
964
965    fn dummy_simulate() -> SimulateTransactionResponse {
966        SimulateTransactionResponse {
967            min_resource_fee: 100,
968            transaction_data: "test".to_string(),
969            ..Default::default()
970        }
971    }
972
973    fn create_success_tx_result() -> TransactionResult {
974        // Create empty operation results
975        let empty_vec: Vec<OperationResult> = Vec::new();
976        let op_results = empty_vec.try_into().unwrap_or_default();
977
978        TransactionResult {
979            fee_charged: 100,
980            result: TransactionResultResult::TxSuccess(op_results),
981            ext: TransactionResultExt::V0,
982        }
983    }
984
985    fn dummy_get_transaction_response() -> GetTransactionResponse {
986        GetTransactionResponse {
987            status: "SUCCESS".to_string(),
988            envelope: None,
989            result: Some(create_success_tx_result()),
990            result_meta: None,
991            events: GetTransactionEvents {
992                contract_events: vec![],
993                diagnostic_events: vec![],
994                transaction_events: vec![],
995            },
996            ledger: None,
997        }
998    }
999
1000    fn dummy_soroban_tx() -> SorobanTransactionResponse {
1001        SorobanTransactionResponse {
1002            response: dummy_get_transaction_response(),
1003        }
1004    }
1005
1006    fn dummy_get_transactions_response() -> GetTransactionsResponse {
1007        GetTransactionsResponse {
1008            transactions: vec![],
1009            latest_ledger: 0,
1010            latest_ledger_close_time: 0,
1011            oldest_ledger: 0,
1012            oldest_ledger_close_time: 0,
1013            cursor: 0,
1014        }
1015    }
1016
1017    fn dummy_get_ledger_entries_response() -> GetLedgerEntriesResponse {
1018        GetLedgerEntriesResponse {
1019            entries: None,
1020            latest_ledger: 0,
1021        }
1022    }
1023
1024    fn dummy_get_events_response() -> GetEventsResponse {
1025        GetEventsResponse {
1026            events: vec![],
1027            latest_ledger: 0,
1028            latest_ledger_close_time: "0".to_string(),
1029            oldest_ledger: 0,
1030            oldest_ledger_close_time: "0".to_string(),
1031            cursor: "0".to_string(),
1032        }
1033    }
1034
1035    fn dummy_transaction_envelope() -> TransactionEnvelope {
1036        create_mock_set_options_tx_envelope()
1037    }
1038
1039    fn dummy_ledger_key() -> LedgerKey {
1040        LedgerKey::Account(LedgerKeyAccount {
1041            account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1042        })
1043    }
1044
1045    pub fn mock_account_entry(account_id: &str) -> AccountEntry {
1046        AccountEntry {
1047            account_id: AccountId(PublicKey::from_str(account_id).unwrap()),
1048            balance: 0,
1049            ext: AccountEntryExt::V0,
1050            flags: 0,
1051            home_domain: String32::default(),
1052            inflation_dest: None,
1053            seq_num: 0.into(),
1054            num_sub_entries: 0,
1055            signers: VecM::default(),
1056            thresholds: Thresholds([0, 0, 0, 0]),
1057        }
1058    }
1059
1060    fn dummy_account_entry() -> AccountEntry {
1061        mock_account_entry("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1062    }
1063
1064    // ---------------------------------------------------------------------
1065    // Tests
1066    // ---------------------------------------------------------------------
1067
1068    fn create_test_provider_config(configs: Vec<RpcConfig>, timeout: u64) -> ProviderConfig {
1069        ProviderConfig::new(configs, timeout, 3, 60, 60)
1070    }
1071
1072    #[test]
1073    fn test_new_provider() {
1074        let _env_guard = setup_test_env();
1075
1076        let provider = StellarProvider::new(create_test_provider_config(
1077            vec![RpcConfig::new("http://localhost:8000".to_string())],
1078            0,
1079        ));
1080        assert!(provider.is_ok());
1081
1082        let provider_err = StellarProvider::new(create_test_provider_config(vec![], 0));
1083        assert!(provider_err.is_err());
1084        match provider_err.unwrap_err() {
1085            ProviderError::NetworkConfiguration(msg) => {
1086                assert!(msg.contains("No RPC configurations provided"));
1087            }
1088            _ => panic!("Unexpected error type"),
1089        }
1090    }
1091
1092    #[test]
1093    fn test_new_provider_selects_highest_weight() {
1094        let _env_guard = setup_test_env();
1095
1096        let configs = vec![
1097            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 10).unwrap(),
1098            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), // Highest weight
1099            RpcConfig::with_weight("http://rpc3.example.com".to_string(), 50).unwrap(),
1100        ];
1101        let provider = StellarProvider::new(create_test_provider_config(configs, 0));
1102        assert!(provider.is_ok());
1103        // We can't directly inspect the client's URL easily without more complex mocking or changes.
1104        // For now, we trust the sorting logic and that Client::new would fail for a truly bad URL if selection was wrong.
1105        // A more robust test would involve a mock client or a way to inspect the chosen URL.
1106    }
1107
1108    #[test]
1109    fn test_new_provider_ignores_weight_zero() {
1110        let _env_guard = setup_test_env();
1111
1112        let configs = vec![
1113            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(), // Weight 0
1114            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), // Should be selected
1115        ];
1116        let provider = StellarProvider::new(create_test_provider_config(configs, 0));
1117        assert!(provider.is_ok());
1118
1119        let configs_only_zero =
1120            vec![RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap()];
1121        let provider_err = StellarProvider::new(create_test_provider_config(configs_only_zero, 0));
1122        assert!(provider_err.is_err());
1123        match provider_err.unwrap_err() {
1124            ProviderError::NetworkConfiguration(msg) => {
1125                assert!(msg.contains("No active RPC configurations provided"));
1126            }
1127            _ => panic!("Unexpected error type"),
1128        }
1129    }
1130
1131    #[test]
1132    fn test_new_provider_invalid_url_scheme() {
1133        let configs = vec![RpcConfig::new("ftp://invalid.example.com".to_string())];
1134        let provider_err = StellarProvider::new(create_test_provider_config(configs, 0));
1135        assert!(provider_err.is_err());
1136        match provider_err.unwrap_err() {
1137            ProviderError::NetworkConfiguration(msg) => {
1138                assert!(msg.contains("Invalid URL scheme"));
1139            }
1140            _ => panic!("Unexpected error type"),
1141        }
1142    }
1143
1144    #[test]
1145    fn test_new_provider_all_zero_weight_configs() {
1146        let _env_guard = setup_test_env();
1147
1148        let configs = vec![
1149            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(),
1150            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 0).unwrap(),
1151        ];
1152        let provider_err = StellarProvider::new(create_test_provider_config(configs, 0));
1153        assert!(provider_err.is_err());
1154        match provider_err.unwrap_err() {
1155            ProviderError::NetworkConfiguration(msg) => {
1156                assert!(msg.contains("No active RPC configurations provided"));
1157            }
1158            _ => panic!("Unexpected error type"),
1159        }
1160    }
1161
1162    #[tokio::test]
1163    async fn test_mock_basic_methods() {
1164        let mut mock = MockStellarProviderTrait::new();
1165
1166        mock.expect_get_network()
1167            .times(1)
1168            .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
1169
1170        mock.expect_get_latest_ledger()
1171            .times(1)
1172            .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
1173
1174        assert!(mock.get_network().await.is_ok());
1175        assert!(mock.get_latest_ledger().await.is_ok());
1176    }
1177
1178    #[tokio::test]
1179    async fn test_mock_transaction_flow() {
1180        let mut mock = MockStellarProviderTrait::new();
1181
1182        let envelope: TransactionEnvelope = dummy_transaction_envelope();
1183        let hash = dummy_hash();
1184
1185        mock.expect_simulate_transaction_envelope()
1186            .withf(|_| true)
1187            .times(1)
1188            .returning(|_| async { Ok(dummy_simulate()) }.boxed());
1189
1190        mock.expect_send_transaction()
1191            .withf(|_| true)
1192            .times(1)
1193            .returning(|_| async { Ok(dummy_hash()) }.boxed());
1194
1195        mock.expect_send_transaction_polling()
1196            .withf(|_| true)
1197            .times(1)
1198            .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1199
1200        mock.expect_get_transaction()
1201            .withf(|_| true)
1202            .times(1)
1203            .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1204
1205        mock.simulate_transaction_envelope(&envelope).await.unwrap();
1206        mock.send_transaction(&envelope).await.unwrap();
1207        mock.send_transaction_polling(&envelope).await.unwrap();
1208        mock.get_transaction(&hash).await.unwrap();
1209    }
1210
1211    #[tokio::test]
1212    async fn test_mock_events_and_entries() {
1213        let mut mock = MockStellarProviderTrait::new();
1214
1215        mock.expect_get_events()
1216            .times(1)
1217            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1218
1219        mock.expect_get_ledger_entries()
1220            .times(1)
1221            .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1222
1223        let events_request = GetEventsRequest {
1224            start: EventStart::Ledger(1),
1225            event_type: None,
1226            contract_ids: vec![],
1227            topics: vec![],
1228            limit: Some(10),
1229        };
1230
1231        let dummy_key: LedgerKey = dummy_ledger_key();
1232        mock.get_events(events_request).await.unwrap();
1233        mock.get_ledger_entries(&[dummy_key]).await.unwrap();
1234    }
1235
1236    #[tokio::test]
1237    async fn test_mock_all_methods_ok() {
1238        let mut mock = MockStellarProviderTrait::new();
1239
1240        mock.expect_get_account()
1241            .with(p::eq("GTESTACCOUNTID"))
1242            .times(1)
1243            .returning(|_| async { Ok(dummy_account_entry()) }.boxed());
1244
1245        mock.expect_simulate_transaction_envelope()
1246            .times(1)
1247            .returning(|_| async { Ok(dummy_simulate()) }.boxed());
1248
1249        mock.expect_send_transaction_polling()
1250            .times(1)
1251            .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1252
1253        mock.expect_get_network()
1254            .times(1)
1255            .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
1256
1257        mock.expect_get_latest_ledger()
1258            .times(1)
1259            .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
1260
1261        mock.expect_send_transaction()
1262            .times(1)
1263            .returning(|_| async { Ok(dummy_hash()) }.boxed());
1264
1265        mock.expect_get_transaction()
1266            .times(1)
1267            .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1268
1269        mock.expect_get_transactions()
1270            .times(1)
1271            .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
1272
1273        mock.expect_get_ledger_entries()
1274            .times(1)
1275            .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1276
1277        mock.expect_get_events()
1278            .times(1)
1279            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1280
1281        let _ = mock.get_account("GTESTACCOUNTID").await.unwrap();
1282        let env: TransactionEnvelope = dummy_transaction_envelope();
1283        mock.simulate_transaction_envelope(&env).await.unwrap();
1284        mock.send_transaction_polling(&env).await.unwrap();
1285        mock.get_network().await.unwrap();
1286        mock.get_latest_ledger().await.unwrap();
1287        mock.send_transaction(&env).await.unwrap();
1288
1289        let h = dummy_hash();
1290        mock.get_transaction(&h).await.unwrap();
1291
1292        let req: GetTransactionsRequest = GetTransactionsRequest {
1293            start_ledger: None,
1294            pagination: None,
1295        };
1296        mock.get_transactions(req).await.unwrap();
1297
1298        let key: LedgerKey = dummy_ledger_key();
1299        mock.get_ledger_entries(&[key]).await.unwrap();
1300
1301        let ev_req = GetEventsRequest {
1302            start: EventStart::Ledger(0),
1303            event_type: None,
1304            contract_ids: vec![],
1305            topics: vec![],
1306            limit: None,
1307        };
1308        mock.get_events(ev_req).await.unwrap();
1309    }
1310
1311    #[tokio::test]
1312    async fn test_error_propagation() {
1313        let mut mock = MockStellarProviderTrait::new();
1314
1315        mock.expect_get_account()
1316            .returning(|_| async { Err(ProviderError::Other("boom".to_string())) }.boxed());
1317
1318        let res = mock.get_account("BAD").await;
1319        assert!(res.is_err());
1320        assert!(res.unwrap_err().to_string().contains("boom"));
1321    }
1322
1323    #[tokio::test]
1324    async fn test_get_events_edge_cases() {
1325        let mut mock = MockStellarProviderTrait::new();
1326
1327        mock.expect_get_events()
1328            .withf(|req| {
1329                req.contract_ids.is_empty() && req.topics.is_empty() && req.limit.is_none()
1330            })
1331            .times(1)
1332            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1333
1334        let ev_req = GetEventsRequest {
1335            start: EventStart::Ledger(0),
1336            event_type: None,
1337            contract_ids: vec![],
1338            topics: vec![],
1339            limit: None,
1340        };
1341
1342        mock.get_events(ev_req).await.unwrap();
1343    }
1344
1345    #[test]
1346    fn test_provider_send_sync_bounds() {
1347        fn assert_send_sync<T: Send + Sync>() {}
1348        assert_send_sync::<StellarProvider>();
1349    }
1350
1351    #[cfg(test)]
1352    mod concrete_tests {
1353        use super::*;
1354
1355        const NON_EXISTENT_URL: &str = "http://127.0.0.1:9998";
1356
1357        fn create_test_provider_config(configs: Vec<RpcConfig>, timeout: u64) -> ProviderConfig {
1358            ProviderConfig::new(configs, timeout, 3, 60, 60)
1359        }
1360
1361        fn setup_provider() -> StellarProvider {
1362            StellarProvider::new(create_test_provider_config(
1363                vec![RpcConfig::new(NON_EXISTENT_URL.to_string())],
1364                0,
1365            ))
1366            .expect("Provider creation should succeed even with bad URL")
1367        }
1368
1369        #[tokio::test]
1370        async fn test_concrete_get_account_error() {
1371            let _env_guard = setup_test_env();
1372            let provider = setup_provider();
1373            let result = provider.get_account("SOME_ACCOUNT_ID").await;
1374            assert!(result.is_err());
1375            let err_str = result.unwrap_err().to_string();
1376            // Should contain the "Failed to..." context message
1377            assert!(
1378                err_str.contains("Failed to get account"),
1379                "Unexpected error message: {err_str}"
1380            );
1381        }
1382
1383        #[tokio::test]
1384        async fn test_concrete_simulate_transaction_envelope_error() {
1385            let _env_guard = setup_test_env();
1386
1387            let provider = setup_provider();
1388            let envelope: TransactionEnvelope = dummy_transaction_envelope();
1389            let result = provider.simulate_transaction_envelope(&envelope).await;
1390            assert!(result.is_err());
1391            let err_str = result.unwrap_err().to_string();
1392            // Should contain the "Failed to..." context message
1393            assert!(
1394                err_str.contains("Failed to simulate transaction"),
1395                "Unexpected error message: {err_str}"
1396            );
1397        }
1398
1399        #[tokio::test]
1400        async fn test_concrete_send_transaction_polling_error() {
1401            let _env_guard = setup_test_env();
1402
1403            let provider = setup_provider();
1404            let envelope: TransactionEnvelope = dummy_transaction_envelope();
1405            let result = provider.send_transaction_polling(&envelope).await;
1406            assert!(result.is_err());
1407            let err_str = result.unwrap_err().to_string();
1408            // Should contain the "Failed to..." context message
1409            assert!(
1410                err_str.contains("Failed to send transaction (polling)"),
1411                "Unexpected error message: {err_str}"
1412            );
1413        }
1414
1415        #[tokio::test]
1416        async fn test_concrete_get_network_error() {
1417            let _env_guard = setup_test_env();
1418
1419            let provider = setup_provider();
1420            let result = provider.get_network().await;
1421            assert!(result.is_err());
1422            let err_str = result.unwrap_err().to_string();
1423            // Should contain the "Failed to..." context message
1424            assert!(
1425                err_str.contains("Failed to get network"),
1426                "Unexpected error message: {err_str}"
1427            );
1428        }
1429
1430        #[tokio::test]
1431        async fn test_concrete_get_latest_ledger_error() {
1432            let _env_guard = setup_test_env();
1433
1434            let provider = setup_provider();
1435            let result = provider.get_latest_ledger().await;
1436            assert!(result.is_err());
1437            let err_str = result.unwrap_err().to_string();
1438            // Should contain the "Failed to..." context message
1439            assert!(
1440                err_str.contains("Failed to get latest ledger"),
1441                "Unexpected error message: {err_str}"
1442            );
1443        }
1444
1445        #[tokio::test]
1446        async fn test_concrete_send_transaction_error() {
1447            let _env_guard = setup_test_env();
1448
1449            let provider = setup_provider();
1450            let envelope: TransactionEnvelope = dummy_transaction_envelope();
1451            let result = provider.send_transaction(&envelope).await;
1452            assert!(result.is_err());
1453            let err_str = result.unwrap_err().to_string();
1454            // Should contain the "Failed to..." context message
1455            assert!(
1456                err_str.contains("Failed to send transaction"),
1457                "Unexpected error message: {err_str}"
1458            );
1459        }
1460
1461        #[tokio::test]
1462        async fn test_concrete_get_transaction_error() {
1463            let _env_guard = setup_test_env();
1464
1465            let provider = setup_provider();
1466            let hash: Hash = dummy_hash();
1467            let result = provider.get_transaction(&hash).await;
1468            assert!(result.is_err());
1469            let err_str = result.unwrap_err().to_string();
1470            // Should contain the "Failed to..." context message
1471            assert!(
1472                err_str.contains("Failed to get transaction"),
1473                "Unexpected error message: {err_str}"
1474            );
1475        }
1476
1477        #[tokio::test]
1478        async fn test_concrete_get_transactions_error() {
1479            let _env_guard = setup_test_env();
1480
1481            let provider = setup_provider();
1482            let req = GetTransactionsRequest {
1483                start_ledger: None,
1484                pagination: None,
1485            };
1486            let result = provider.get_transactions(req).await;
1487            assert!(result.is_err());
1488            let err_str = result.unwrap_err().to_string();
1489            // Should contain the "Failed to..." context message
1490            assert!(
1491                err_str.contains("Failed to get transactions"),
1492                "Unexpected error message: {err_str}"
1493            );
1494        }
1495
1496        #[tokio::test]
1497        async fn test_concrete_get_ledger_entries_error() {
1498            let _env_guard = setup_test_env();
1499
1500            let provider = setup_provider();
1501            let key: LedgerKey = dummy_ledger_key();
1502            let result = provider.get_ledger_entries(&[key]).await;
1503            assert!(result.is_err());
1504            let err_str = result.unwrap_err().to_string();
1505            // Should contain the "Failed to..." context message
1506            assert!(
1507                err_str.contains("Failed to get ledger entries"),
1508                "Unexpected error message: {err_str}"
1509            );
1510        }
1511
1512        #[tokio::test]
1513        async fn test_concrete_get_events_error() {
1514            let _env_guard = setup_test_env();
1515            let provider = setup_provider();
1516            let req = GetEventsRequest {
1517                start: EventStart::Ledger(1),
1518                event_type: None,
1519                contract_ids: vec![],
1520                topics: vec![],
1521                limit: None,
1522            };
1523            let result = provider.get_events(req).await;
1524            assert!(result.is_err());
1525            let err_str = result.unwrap_err().to_string();
1526            // Should contain the "Failed to..." context message
1527            assert!(
1528                err_str.contains("Failed to get events"),
1529                "Unexpected error message: {err_str}"
1530            );
1531        }
1532    }
1533
1534    #[test]
1535    fn test_generate_unique_rpc_id() {
1536        let id1 = generate_unique_rpc_id();
1537        let id2 = generate_unique_rpc_id();
1538        assert_ne!(id1, id2, "Generated IDs should be unique");
1539        assert!(id1 > 0, "ID should be positive");
1540        assert!(id2 > 0, "ID should be positive");
1541        assert!(id2 > id1, "IDs should be monotonically increasing");
1542    }
1543
1544    #[test]
1545    fn test_normalize_url_for_log() {
1546        // Test basic URL without query/fragment
1547        assert_eq!(
1548            normalize_url_for_log("https://api.example.com/path"),
1549            "https://api.example.com/path"
1550        );
1551
1552        // Test URL with query string removal
1553        assert_eq!(
1554            normalize_url_for_log("https://api.example.com/path?api_key=secret&other=value"),
1555            "https://api.example.com/path"
1556        );
1557
1558        // Test URL with fragment removal
1559        assert_eq!(
1560            normalize_url_for_log("https://api.example.com/path#section"),
1561            "https://api.example.com/path"
1562        );
1563
1564        // Test URL with both query and fragment
1565        assert_eq!(
1566            normalize_url_for_log("https://api.example.com/path?key=value#fragment"),
1567            "https://api.example.com/path"
1568        );
1569
1570        // Test URL with userinfo redaction
1571        assert_eq!(
1572            normalize_url_for_log("https://user:password@api.example.com/path"),
1573            "https://<redacted>@api.example.com/path"
1574        );
1575
1576        // Test URL with userinfo and query/fragment removal
1577        assert_eq!(
1578            normalize_url_for_log("https://user:pass@api.example.com/path?token=abc#frag"),
1579            "https://<redacted>@api.example.com/path"
1580        );
1581
1582        // Test URL without userinfo (should remain unchanged)
1583        assert_eq!(
1584            normalize_url_for_log("https://api.example.com/path?token=abc"),
1585            "https://api.example.com/path"
1586        );
1587
1588        // Test malformed URL (should handle gracefully)
1589        assert_eq!(normalize_url_for_log("not-a-url"), "not-a-url");
1590    }
1591
1592    #[test]
1593    fn test_categorize_stellar_error_with_context_timeout() {
1594        let err = StellarClientError::TransactionSubmissionTimeout;
1595        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1596        assert!(matches!(result, ProviderError::Timeout));
1597    }
1598
1599    #[test]
1600    fn test_categorize_stellar_error_with_context_xdr_error() {
1601        use soroban_rs::xdr::Error as XdrError;
1602        let err = StellarClientError::Xdr(XdrError::Invalid);
1603        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1604        match result {
1605            ProviderError::Other(msg) => {
1606                assert!(msg.contains("Test operation"));
1607            }
1608            _ => panic!("Expected Other error"),
1609        }
1610    }
1611
1612    #[test]
1613    fn test_categorize_stellar_error_with_context_serde_error() {
1614        // Create a serde error by attempting to deserialize invalid JSON
1615        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
1616        let err = StellarClientError::Serde(json_err);
1617        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1618        match result {
1619            ProviderError::Other(msg) => {
1620                assert!(msg.contains("Test operation"));
1621            }
1622            _ => panic!("Expected Other error"),
1623        }
1624    }
1625
1626    #[test]
1627    fn test_categorize_stellar_error_with_context_url_errors() {
1628        // Test InvalidRpcUrl
1629        let invalid_uri_err: http::uri::InvalidUri =
1630            ":::invalid url".parse::<http::Uri>().unwrap_err();
1631        let err = StellarClientError::InvalidRpcUrl(invalid_uri_err);
1632        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1633        match result {
1634            ProviderError::NetworkConfiguration(msg) => {
1635                assert!(msg.contains("Test operation"));
1636                assert!(msg.contains("Invalid RPC URL"));
1637            }
1638            _ => panic!("Expected NetworkConfiguration error"),
1639        }
1640
1641        // Test InvalidUrl
1642        let err = StellarClientError::InvalidUrl("not a url".to_string());
1643        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1644        match result {
1645            ProviderError::NetworkConfiguration(msg) => {
1646                assert!(msg.contains("Test operation"));
1647                assert!(msg.contains("Invalid URL"));
1648            }
1649            _ => panic!("Expected NetworkConfiguration error"),
1650        }
1651    }
1652
1653    #[test]
1654    fn test_categorize_stellar_error_with_context_network_passphrase() {
1655        let err = StellarClientError::InvalidNetworkPassphrase {
1656            expected: "Expected".to_string(),
1657            server: "Server".to_string(),
1658        };
1659        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1660        match result {
1661            ProviderError::NetworkConfiguration(msg) => {
1662                assert!(msg.contains("Test operation"));
1663                assert!(msg.contains("Expected"));
1664                assert!(msg.contains("Server"));
1665            }
1666            _ => panic!("Expected NetworkConfiguration error"),
1667        }
1668    }
1669
1670    #[test]
1671    fn test_categorize_stellar_error_with_context_json_rpc_call_error() {
1672        // Test that RPC Call errors are properly categorized as RpcErrorCode
1673        // We'll test this indirectly through other error types since creating Call errors
1674        // requires jsonrpsee internals that aren't easily accessible in tests
1675        let err = StellarClientError::TransactionSubmissionTimeout;
1676        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1677        // Verify timeout is properly categorized
1678        assert!(matches!(result, ProviderError::Timeout));
1679    }
1680
1681    #[test]
1682    fn test_categorize_stellar_error_with_context_json_rpc_timeout() {
1683        // Test timeout through TransactionSubmissionTimeout which is simpler to construct
1684        let err = StellarClientError::TransactionSubmissionTimeout;
1685        let result = categorize_stellar_error_with_context(err, None);
1686        assert!(matches!(result, ProviderError::Timeout));
1687    }
1688
1689    #[test]
1690    fn test_categorize_stellar_error_with_context_transport_errors() {
1691        // Test network-related errors through InvalidResponse which is simpler to construct
1692        let err = StellarClientError::InvalidResponse;
1693        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1694        match result {
1695            ProviderError::Other(msg) => {
1696                assert!(msg.contains("Test operation"));
1697                assert!(msg.contains("Invalid response"));
1698            }
1699            _ => panic!("Expected Other error for response issues"),
1700        }
1701    }
1702
1703    #[test]
1704    fn test_categorize_stellar_error_with_context_response_errors() {
1705        // Test InvalidResponse
1706        let err = StellarClientError::InvalidResponse;
1707        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1708        match result {
1709            ProviderError::Other(msg) => {
1710                assert!(msg.contains("Test operation"));
1711                assert!(msg.contains("Invalid response"));
1712            }
1713            _ => panic!("Expected Other error"),
1714        }
1715
1716        // Test MissingResult
1717        let err = StellarClientError::MissingResult;
1718        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1719        match result {
1720            ProviderError::Other(msg) => {
1721                assert!(msg.contains("Test operation"));
1722                assert!(msg.contains("Missing result"));
1723            }
1724            _ => panic!("Expected Other error"),
1725        }
1726    }
1727
1728    #[test]
1729    fn test_categorize_stellar_error_with_context_transaction_errors() {
1730        // Test TransactionFailed
1731        let err = StellarClientError::TransactionFailed("tx failed".to_string());
1732        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1733        match result {
1734            ProviderError::Other(msg) => {
1735                assert!(msg.contains("Test operation"));
1736                assert!(msg.contains("tx failed"));
1737            }
1738            _ => panic!("Expected Other error"),
1739        }
1740
1741        // Test NotFound
1742        let err = StellarClientError::NotFound("Account".to_string(), "123".to_string());
1743        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1744        match result {
1745            ProviderError::Other(msg) => {
1746                assert!(msg.contains("Test operation"));
1747                assert!(msg.contains("Account not found"));
1748                assert!(msg.contains("123"));
1749            }
1750            _ => panic!("Expected Other error"),
1751        }
1752    }
1753
1754    #[test]
1755    fn test_categorize_stellar_error_with_context_validation_errors() {
1756        // Test InvalidCursor
1757        let err = StellarClientError::InvalidCursor;
1758        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1759        match result {
1760            ProviderError::Other(msg) => {
1761                assert!(msg.contains("Test operation"));
1762                assert!(msg.contains("Invalid cursor"));
1763            }
1764            _ => panic!("Expected Other error"),
1765        }
1766
1767        // Test LargeFee
1768        let err = StellarClientError::LargeFee(1000000);
1769        let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1770        match result {
1771            ProviderError::Other(msg) => {
1772                assert!(msg.contains("Test operation"));
1773                assert!(msg.contains("1000000"));
1774            }
1775            _ => panic!("Expected Other error"),
1776        }
1777    }
1778
1779    #[test]
1780    fn test_categorize_stellar_error_with_context_no_context() {
1781        // Test with a simpler error type that doesn't have version conflicts
1782        let err = StellarClientError::InvalidResponse;
1783        let result = categorize_stellar_error_with_context(err, None);
1784        match result {
1785            ProviderError::Other(msg) => {
1786                assert!(!msg.contains(":")); // No context prefix
1787                assert!(msg.contains("Invalid response"));
1788            }
1789            _ => panic!("Expected Other error"),
1790        }
1791    }
1792
1793    #[test]
1794    fn test_initialize_provider_invalid_url() {
1795        let _env_guard = setup_test_env();
1796        let provider = StellarProvider::new(create_test_provider_config(
1797            vec![RpcConfig::new("http://localhost:8000".to_string())],
1798            30,
1799        ))
1800        .unwrap();
1801
1802        // Test with invalid URL that should fail client creation
1803        let result = provider.initialize_provider("invalid-url");
1804        assert!(result.is_err());
1805        match result.unwrap_err() {
1806            ProviderError::NetworkConfiguration(msg) => {
1807                // Error message can be either from URL validation or client creation
1808                assert!(
1809                    msg.contains("Failed to create Stellar RPC client")
1810                        || msg.contains("RPC URL security validation failed")
1811                );
1812            }
1813            _ => panic!("Expected NetworkConfiguration error"),
1814        }
1815    }
1816
1817    #[test]
1818    fn test_initialize_raw_provider_timeout_config() {
1819        let _env_guard = setup_test_env();
1820        let provider = StellarProvider::new(create_test_provider_config(
1821            vec![RpcConfig::new("http://localhost:8000".to_string())],
1822            30,
1823        ))
1824        .unwrap();
1825
1826        // Test with valid URL - should succeed
1827        let result = provider.initialize_raw_provider("http://localhost:8000");
1828        assert!(result.is_ok());
1829
1830        // Test with invalid URL for reqwest client - this might not fail immediately
1831        // but we can test that the function doesn't panic
1832        let result = provider.initialize_raw_provider("not-a-url");
1833        // reqwest::Client::builder() may not fail immediately for malformed URLs
1834        // but the function should return a Result
1835        assert!(result.is_ok() || result.is_err());
1836    }
1837
1838    #[tokio::test]
1839    async fn test_raw_request_dyn_success() {
1840        let _env_guard = setup_test_env();
1841
1842        // Create a provider with a mock server URL that won't actually connect
1843        let provider = StellarProvider::new(create_test_provider_config(
1844            vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1845            1,
1846        ))
1847        .unwrap();
1848
1849        let params = serde_json::json!({"test": "value"});
1850        let result = provider
1851            .raw_request_dyn("test_method", params, Some(JsonRpcId::Number(1)))
1852            .await;
1853
1854        // Should fail due to connection, but should go through the retry logic
1855        assert!(result.is_err());
1856        let err = result.unwrap_err();
1857        // Should be a network-related error, not a panic
1858        assert!(matches!(
1859            err,
1860            ProviderError::Other(_)
1861                | ProviderError::Timeout
1862                | ProviderError::NetworkConfiguration(_)
1863        ));
1864    }
1865
1866    #[tokio::test]
1867    async fn test_raw_request_dyn_with_auto_generated_id() {
1868        let _env_guard = setup_test_env();
1869
1870        let provider = StellarProvider::new(create_test_provider_config(
1871            vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1872            1,
1873        ))
1874        .unwrap();
1875
1876        let params = serde_json::json!({"test": "value"});
1877        let result = provider.raw_request_dyn("test_method", params, None).await;
1878
1879        // Should fail due to connection, but the ID generation should work
1880        assert!(result.is_err());
1881    }
1882
1883    #[tokio::test]
1884    async fn test_retry_raw_request_connection_failure() {
1885        let _env_guard = setup_test_env();
1886
1887        let provider = StellarProvider::new(create_test_provider_config(
1888            vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1889            1,
1890        ))
1891        .unwrap();
1892
1893        let request = serde_json::json!({
1894            "jsonrpc": "2.0",
1895            "id": 1,
1896            "method": "test",
1897            "params": {}
1898        });
1899
1900        let result = provider.retry_raw_request("test_operation", request).await;
1901
1902        // Should fail due to connection issues
1903        assert!(result.is_err());
1904        let err = result.unwrap_err();
1905        // Should be categorized as network error
1906        assert!(matches!(
1907            err,
1908            ProviderError::Other(_) | ProviderError::Timeout
1909        ));
1910    }
1911
1912    #[tokio::test]
1913    async fn test_raw_request_dyn_json_rpc_error_response() {
1914        let _env_guard = setup_test_env();
1915
1916        // This test would require mocking the HTTP response, which is complex
1917        // For now, we test that the function exists and can be called
1918        let provider = StellarProvider::new(create_test_provider_config(
1919            vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1920            1,
1921        ))
1922        .unwrap();
1923
1924        let params = serde_json::json!({"test": "value"});
1925        let result = provider
1926            .raw_request_dyn(
1927                "test_method",
1928                params,
1929                Some(JsonRpcId::String("test-id".to_string())),
1930            )
1931            .await;
1932
1933        // Should fail due to connection, but should handle the request properly
1934        assert!(result.is_err());
1935    }
1936
1937    #[test]
1938    fn test_provider_creation_edge_cases() {
1939        let _env_guard = setup_test_env();
1940
1941        // Test with empty configs
1942        let result = StellarProvider::new(create_test_provider_config(vec![], 30));
1943        assert!(result.is_err());
1944        match result.unwrap_err() {
1945            ProviderError::NetworkConfiguration(msg) => {
1946                assert!(msg.contains("No RPC configurations provided"));
1947            }
1948            _ => panic!("Expected NetworkConfiguration error"),
1949        }
1950
1951        // Test with configs that have zero weights after filtering
1952        let mut config1 = RpcConfig::new("http://localhost:8000".to_string());
1953        config1.weight = 0;
1954        let mut config2 = RpcConfig::new("http://localhost:8001".to_string());
1955        config2.weight = 0;
1956        let configs = vec![config1, config2];
1957        let result = StellarProvider::new(create_test_provider_config(configs, 30));
1958        assert!(result.is_err());
1959        match result.unwrap_err() {
1960            ProviderError::NetworkConfiguration(msg) => {
1961                assert!(msg.contains("No active RPC configurations"));
1962            }
1963            _ => panic!("Expected NetworkConfiguration error"),
1964        }
1965    }
1966
1967    #[tokio::test]
1968    async fn test_get_events_empty_request() {
1969        let _env_guard = setup_test_env();
1970
1971        let mut mock = MockStellarProviderTrait::new();
1972        mock.expect_get_events()
1973            .withf(|req| req.contract_ids.is_empty() && req.topics.is_empty())
1974            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1975
1976        let req = GetEventsRequest {
1977            start: EventStart::Ledger(1),
1978            event_type: Some(EventType::Contract),
1979            contract_ids: vec![],
1980            topics: vec![],
1981            limit: Some(10),
1982        };
1983
1984        let result = mock.get_events(req).await;
1985        assert!(result.is_ok());
1986    }
1987
1988    #[tokio::test]
1989    async fn test_get_ledger_entries_empty_keys() {
1990        let _env_guard = setup_test_env();
1991
1992        let mut mock = MockStellarProviderTrait::new();
1993        mock.expect_get_ledger_entries()
1994            .withf(|keys| keys.is_empty())
1995            .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1996
1997        let result = mock.get_ledger_entries(&[]).await;
1998        assert!(result.is_ok());
1999    }
2000
2001    #[tokio::test]
2002    async fn test_send_transaction_polling_success() {
2003        let _env_guard = setup_test_env();
2004
2005        let mut mock = MockStellarProviderTrait::new();
2006        mock.expect_send_transaction_polling()
2007            .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
2008
2009        let envelope = dummy_transaction_envelope();
2010        let result = mock.send_transaction_polling(&envelope).await;
2011        assert!(result.is_ok());
2012    }
2013
2014    #[tokio::test]
2015    async fn test_get_transactions_with_pagination() {
2016        let _env_guard = setup_test_env();
2017
2018        let mut mock = MockStellarProviderTrait::new();
2019        mock.expect_get_transactions()
2020            .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
2021
2022        let req = GetTransactionsRequest {
2023            start_ledger: Some(1000),
2024            pagination: None, // Pagination struct may not be available in this version
2025        };
2026
2027        let result = mock.get_transactions(req).await;
2028        assert!(result.is_ok());
2029    }
2030}