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