openzeppelin_relayer/services/provider/
mod.rs

1use std::num::ParseIntError;
2
3use crate::config::ServerConfig;
4use crate::models::{EvmNetwork, RpcConfig, SolanaNetwork, StellarNetwork};
5use serde::Serialize;
6use thiserror::Error;
7
8use alloy::transports::RpcError;
9
10pub mod evm;
11pub use evm::*;
12
13mod solana;
14pub use solana::*;
15
16mod stellar;
17pub use stellar::*;
18
19mod retry;
20pub use retry::*;
21
22pub mod rpc_health_store;
23pub mod rpc_selector;
24
25pub use rpc_health_store::{RpcConfigMetadata, RpcHealthStore};
26
27/// Configuration for creating a provider instance.
28///
29/// This struct encapsulates all the parameters needed to create a provider,
30/// making the API cleaner and easier to maintain.
31#[derive(Debug, Clone)]
32pub struct ProviderConfig {
33    /// RPC endpoint configurations (URLs and weights)
34    pub rpc_configs: Vec<RpcConfig>,
35    /// Timeout duration in seconds for RPC requests
36    pub timeout_seconds: u64,
37    /// Number of consecutive failures before pausing a provider
38    pub failure_threshold: u32,
39    /// Duration in seconds to pause a provider after reaching failure threshold
40    pub pause_duration_secs: u64,
41    /// Duration in seconds after which failures are considered stale and reset
42    pub failure_expiration_secs: u64,
43}
44
45impl ProviderConfig {
46    /// Creates a new `ProviderConfig` from individual parameters.
47    ///
48    /// # Arguments
49    /// * `rpc_configs` - RPC endpoint configurations
50    /// * `timeout_seconds` - Timeout duration in seconds
51    /// * `failure_threshold` - Number of consecutive failures before pausing
52    /// * `pause_duration_secs` - Duration in seconds to pause after threshold
53    /// * `failure_expiration_secs` - Duration in seconds after which failures are considered stale
54    pub fn new(
55        rpc_configs: Vec<RpcConfig>,
56        timeout_seconds: u64,
57        failure_threshold: u32,
58        pause_duration_secs: u64,
59        failure_expiration_secs: u64,
60    ) -> Self {
61        Self {
62            rpc_configs,
63            timeout_seconds,
64            failure_threshold,
65            pause_duration_secs,
66            failure_expiration_secs,
67        }
68    }
69
70    /// Creates a `ProviderConfig` from `ServerConfig` with the given RPC configs.
71    ///
72    /// This is a convenience method that extracts provider-related configuration
73    /// from the server configuration.
74    ///
75    /// # Arguments
76    /// * `server_config` - The server configuration
77    /// * `rpc_configs` - RPC endpoint configurations
78    pub fn from_server_config(server_config: &ServerConfig, rpc_configs: Vec<RpcConfig>) -> Self {
79        let timeout_seconds = server_config.rpc_timeout_ms / 1000; // Convert ms to s
80        Self {
81            rpc_configs,
82            timeout_seconds,
83            failure_threshold: server_config.provider_failure_threshold,
84            pause_duration_secs: server_config.provider_pause_duration_secs,
85            failure_expiration_secs: server_config.provider_failure_expiration_secs,
86        }
87    }
88
89    /// Creates a `ProviderConfig` from environment variables with the given RPC configs.
90    ///
91    /// This loads configuration from `ServerConfig::from_env()`.
92    ///
93    /// # Arguments
94    /// * `rpc_configs` - RPC endpoint configurations
95    pub fn from_env(rpc_configs: Vec<RpcConfig>) -> Self {
96        let server_config = ServerConfig::from_env();
97        Self::from_server_config(&server_config, rpc_configs)
98    }
99}
100
101#[derive(Error, Debug, Serialize)]
102pub enum ProviderError {
103    #[error("RPC client error: {0}")]
104    SolanaRpcError(#[from] SolanaProviderError),
105    #[error("Invalid address: {0}")]
106    InvalidAddress(String),
107    #[error("Network configuration error: {0}")]
108    NetworkConfiguration(String),
109    #[error("Request timeout")]
110    Timeout,
111    #[error("Rate limited (HTTP 429)")]
112    RateLimited,
113    #[error("Bad gateway (HTTP 502)")]
114    BadGateway,
115    #[error("Request error (HTTP {status_code}): {error}")]
116    RequestError { error: String, status_code: u16 },
117    #[error("JSON-RPC error (code {code}): {message}")]
118    RpcErrorCode { code: i64, message: String },
119    #[error("Transport error: {0}")]
120    TransportError(String),
121    #[error("Other provider error: {0}")]
122    Other(String),
123}
124
125impl ProviderError {
126    /// Determines if this error is transient (can retry) or permanent (should fail).
127    pub fn is_transient(&self) -> bool {
128        is_retriable_error(self)
129    }
130}
131
132impl From<hex::FromHexError> for ProviderError {
133    fn from(err: hex::FromHexError) -> Self {
134        ProviderError::InvalidAddress(err.to_string())
135    }
136}
137
138impl From<std::net::AddrParseError> for ProviderError {
139    fn from(err: std::net::AddrParseError) -> Self {
140        ProviderError::NetworkConfiguration(format!("Invalid network address: {err}"))
141    }
142}
143
144impl From<ParseIntError> for ProviderError {
145    fn from(err: ParseIntError) -> Self {
146        ProviderError::Other(format!("Number parsing error: {err}"))
147    }
148}
149
150/// Categorizes a reqwest error into an appropriate `ProviderError` variant.
151///
152/// This function analyzes the given reqwest error and maps it to a specific
153/// `ProviderError` variant based on the error's properties:
154/// - Timeout errors become `ProviderError::Timeout`
155/// - HTTP 429 responses become `ProviderError::RateLimited`
156/// - HTTP 502 responses become `ProviderError::BadGateway`
157/// - All other errors become `ProviderError::Other` with the error message
158///
159/// # Arguments
160///
161/// * `err` - A reference to the reqwest error to categorize
162///
163/// # Returns
164///
165/// The appropriate `ProviderError` variant based on the error type
166fn categorize_reqwest_error(err: &reqwest::Error) -> ProviderError {
167    if err.is_timeout() {
168        return ProviderError::Timeout;
169    }
170
171    if let Some(status) = err.status() {
172        match status.as_u16() {
173            429 => return ProviderError::RateLimited,
174            502 => return ProviderError::BadGateway,
175            _ => {
176                return ProviderError::RequestError {
177                    error: err.to_string(),
178                    status_code: status.as_u16(),
179                }
180            }
181        }
182    }
183
184    ProviderError::Other(err.to_string())
185}
186
187impl From<reqwest::Error> for ProviderError {
188    fn from(err: reqwest::Error) -> Self {
189        categorize_reqwest_error(&err)
190    }
191}
192
193impl From<&reqwest::Error> for ProviderError {
194    fn from(err: &reqwest::Error) -> Self {
195        categorize_reqwest_error(err)
196    }
197}
198
199impl From<eyre::Report> for ProviderError {
200    fn from(err: eyre::Report) -> Self {
201        // Downcast to known error types first
202        if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
203            return ProviderError::from(reqwest_err);
204        }
205
206        // Default to Other for unknown error types
207        ProviderError::Other(err.to_string())
208    }
209}
210
211// Add conversion from String to ProviderError
212impl From<String> for ProviderError {
213    fn from(error: String) -> Self {
214        ProviderError::Other(error)
215    }
216}
217
218// Generic implementation for all RpcError types
219impl<E> From<RpcError<E>> for ProviderError
220where
221    E: std::fmt::Display + std::any::Any + 'static,
222{
223    fn from(err: RpcError<E>) -> Self {
224        match err {
225            RpcError::Transport(transport_err) => {
226                // First check if it's a reqwest::Error using downcasting
227                if let Some(reqwest_err) =
228                    (&transport_err as &dyn std::any::Any).downcast_ref::<reqwest::Error>()
229                {
230                    return categorize_reqwest_error(reqwest_err);
231                }
232
233                ProviderError::TransportError(transport_err.to_string())
234            }
235            RpcError::ErrorResp(json_rpc_err) => ProviderError::RpcErrorCode {
236                code: json_rpc_err.code,
237                message: json_rpc_err.message.to_string(),
238            },
239            _ => ProviderError::Other(format!("Other RPC error: {err}")),
240        }
241    }
242}
243
244// Implement From for RpcSelectorError
245impl From<rpc_selector::RpcSelectorError> for ProviderError {
246    fn from(err: rpc_selector::RpcSelectorError) -> Self {
247        ProviderError::NetworkConfiguration(format!("RPC selector error: {err}"))
248    }
249}
250
251pub trait NetworkConfiguration: Sized {
252    type Provider;
253
254    fn public_rpc_urls(&self) -> Vec<RpcConfig>;
255
256    /// Creates a new provider instance using the provided configuration.
257    ///
258    /// # Arguments
259    /// * `config` - Provider configuration containing RPC configs and settings
260    fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError>;
261}
262
263impl NetworkConfiguration for EvmNetwork {
264    type Provider = EvmProvider;
265
266    fn public_rpc_urls(&self) -> Vec<RpcConfig> {
267        self.rpc_urls.clone()
268    }
269
270    fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
271        EvmProvider::new(config)
272    }
273}
274
275impl NetworkConfiguration for SolanaNetwork {
276    type Provider = SolanaProvider;
277
278    fn public_rpc_urls(&self) -> Vec<RpcConfig> {
279        self.rpc_urls.clone()
280    }
281
282    fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
283        SolanaProvider::new(config)
284    }
285}
286
287impl NetworkConfiguration for StellarNetwork {
288    type Provider = StellarProvider;
289
290    fn public_rpc_urls(&self) -> Vec<RpcConfig> {
291        self.rpc_urls.clone()
292    }
293
294    fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
295        StellarProvider::new(config)
296    }
297}
298
299/// Creates a network-specific provider instance based on the provided configuration.
300///
301/// # Type Parameters
302///
303/// * `N`: The type of the network, which must implement the `NetworkConfiguration` trait.
304///   This determines the specific provider type (`N::Provider`) and how to obtain
305///   public RPC URLs.
306///
307/// # Arguments
308///
309/// * `network`: A reference to the network configuration object (`&N`).
310/// * `custom_rpc_urls`: An `Option<Vec<RpcConfig>>`. If `Some` and not empty, these URLs
311///   are used to configure the provider. If `None` or `Some` but empty, the function
312///   falls back to using the public RPC URLs defined by the `network`'s
313///   `NetworkConfiguration` implementation.
314///
315/// # Returns
316///
317/// * `Ok(N::Provider)`: An instance of the network-specific provider on success.
318/// * `Err(ProviderError)`: An error if configuration fails, such as when no custom URLs
319///   are provided and the network has no public RPC URLs defined
320///   (`ProviderError::NetworkConfiguration`).
321pub fn get_network_provider<N: NetworkConfiguration>(
322    network: &N,
323    custom_rpc_urls: Option<Vec<RpcConfig>>,
324) -> Result<N::Provider, ProviderError> {
325    let rpc_urls = match custom_rpc_urls {
326        Some(configs) if !configs.is_empty() => configs,
327        _ => {
328            let configs = network.public_rpc_urls();
329            if configs.is_empty() {
330                return Err(ProviderError::NetworkConfiguration(
331                    "No public RPC URLs available for this network".to_string(),
332                ));
333            }
334            configs
335        }
336    };
337
338    let provider_config = ProviderConfig::from_env(rpc_urls);
339    N::new_provider(provider_config)
340}
341
342/// Determines if an HTTP status code indicates the provider should be marked as failed.
343///
344/// This is a low-level function that can be reused across different error types.
345///
346/// Returns `true` for:
347/// - 5xx Server Errors (500-599) - RPC node is having issues
348/// - Specific 4xx Client Errors that indicate provider issues:
349///   - 401 (Unauthorized) - auth required but not provided
350///   - 403 (Forbidden) - node is blocking requests or auth issues
351///   - 404 (Not Found) - endpoint doesn't exist or misconfigured
352///   - 410 (Gone) - endpoint permanently removed
353pub fn should_mark_provider_failed_by_status_code(status_code: u16) -> bool {
354    match status_code {
355        // 5xx Server Errors - RPC node is having issues
356        500..=599 => true,
357
358        // 4xx Client Errors that indicate we can't use this provider
359        401 => true, // Unauthorized - auth required but not provided
360        403 => true, // Forbidden - node is blocking requests or auth issues
361        404 => true, // Not Found - endpoint doesn't exist or misconfigured
362        410 => true, // Gone - endpoint permanently removed
363
364        _ => false,
365    }
366}
367
368pub fn should_mark_provider_failed(error: &ProviderError) -> bool {
369    match error {
370        ProviderError::RequestError { status_code, .. } => {
371            should_mark_provider_failed_by_status_code(*status_code)
372        }
373        _ => false,
374    }
375}
376
377// Errors that are retriable
378pub fn is_retriable_error(error: &ProviderError) -> bool {
379    match error {
380        // HTTP-level errors that are retriable
381        ProviderError::Timeout
382        | ProviderError::RateLimited
383        | ProviderError::BadGateway
384        | ProviderError::TransportError(_) => true,
385
386        ProviderError::RequestError { status_code, .. } => {
387            match *status_code {
388                // Non-retriable 5xx: persistent server-side issues
389                501 | 505 => false, // Not Implemented, HTTP Version Not Supported
390
391                // Retriable 5xx: temporary server-side issues
392                500 | 502..=504 | 506..=599 => true,
393
394                // Retriable 4xx: timeout or rate-limit related
395                408 | 425 | 429 => true,
396
397                // Non-retriable 4xx: client errors
398                400..=499 => false,
399
400                // Other status codes: not retriable
401                _ => false,
402            }
403        }
404
405        // JSON-RPC error codes (EIP-1474)
406        ProviderError::RpcErrorCode { code, .. } => {
407            match code {
408                // -32002: Resource unavailable (temporary state)
409                -32002 => true,
410                // -32005: Limit exceeded / rate limited
411                -32005 => true,
412                // -32603: Internal error (may be temporary)
413                -32603 => true,
414                // -32000: Invalid input
415                -32000 => false,
416                // -32001: Resource not found
417                -32001 => false,
418                // -32003: Transaction rejected
419                -32003 => false,
420                // -32004: Method not supported
421                -32004 => false,
422
423                // Standard JSON-RPC 2.0 errors (not retriable)
424                // -32700: Parse error
425                // -32600: Invalid request
426                // -32601: Method not found
427                // -32602: Invalid params
428                -32700..=-32600 => false,
429
430                // All other error codes: not retriable by default
431                _ => false,
432            }
433        }
434
435        ProviderError::SolanaRpcError(err) => err.is_transient(),
436
437        // Any other errors: check message for network-related issues
438        _ => {
439            let err_msg = format!("{error}");
440            let msg_lower = err_msg.to_lowercase();
441            msg_lower.contains("timeout")
442                || msg_lower.contains("connection")
443                || msg_lower.contains("reset")
444        }
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use lazy_static::lazy_static;
452    use std::env;
453    use std::sync::Mutex;
454    use std::time::Duration;
455
456    // Use a mutex to ensure tests don't run in parallel when modifying env vars
457    lazy_static! {
458        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
459    }
460
461    fn setup_test_env() {
462        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); // noboost
463        env::set_var("REDIS_URL", "redis://localhost:6379");
464        env::set_var("RPC_TIMEOUT_MS", "5000");
465    }
466
467    fn cleanup_test_env() {
468        env::remove_var("API_KEY");
469        env::remove_var("REDIS_URL");
470        env::remove_var("RPC_TIMEOUT_MS");
471    }
472
473    fn create_test_evm_network() -> EvmNetwork {
474        EvmNetwork {
475            network: "test-evm".to_string(),
476            rpc_urls: vec![RpcConfig::new("https://rpc.example.com".to_string())],
477            explorer_urls: None,
478            average_blocktime_ms: 12000,
479            is_testnet: true,
480            tags: vec![],
481            chain_id: 1337,
482            required_confirmations: 1,
483            features: vec![],
484            symbol: "ETH".to_string(),
485            gas_price_cache: None,
486        }
487    }
488
489    fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
490        SolanaNetwork {
491            network: network_str.to_string(),
492            rpc_urls: vec![RpcConfig::new("https://api.testnet.solana.com".to_string())],
493            explorer_urls: None,
494            average_blocktime_ms: 400,
495            is_testnet: true,
496            tags: vec![],
497        }
498    }
499
500    fn create_test_stellar_network() -> StellarNetwork {
501        StellarNetwork {
502            network: "testnet".to_string(),
503            rpc_urls: vec![RpcConfig::new(
504                "https://soroban-testnet.stellar.org".to_string(),
505            )],
506            explorer_urls: None,
507            average_blocktime_ms: 5000,
508            is_testnet: true,
509            tags: vec![],
510            passphrase: "Test SDF Network ; September 2015".to_string(),
511            horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
512        }
513    }
514
515    #[test]
516    fn test_from_hex_error() {
517        let hex_error = hex::FromHexError::OddLength;
518        let provider_error: ProviderError = hex_error.into();
519        assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
520    }
521
522    #[test]
523    fn test_from_addr_parse_error() {
524        let addr_error = "invalid:address"
525            .parse::<std::net::SocketAddr>()
526            .unwrap_err();
527        let provider_error: ProviderError = addr_error.into();
528        assert!(matches!(
529            provider_error,
530            ProviderError::NetworkConfiguration(_)
531        ));
532    }
533
534    #[test]
535    fn test_from_parse_int_error() {
536        let parse_error = "not_a_number".parse::<u64>().unwrap_err();
537        let provider_error: ProviderError = parse_error.into();
538        assert!(matches!(provider_error, ProviderError::Other(_)));
539    }
540
541    #[actix_rt::test]
542    async fn test_categorize_reqwest_error_timeout() {
543        let client = reqwest::Client::new();
544        let timeout_err = client
545            .get("http://example.com")
546            .timeout(Duration::from_nanos(1))
547            .send()
548            .await
549            .unwrap_err();
550
551        assert!(timeout_err.is_timeout());
552
553        let provider_error = categorize_reqwest_error(&timeout_err);
554        assert!(matches!(provider_error, ProviderError::Timeout));
555    }
556
557    #[actix_rt::test]
558    async fn test_categorize_reqwest_error_rate_limited() {
559        let mut mock_server = mockito::Server::new_async().await;
560
561        let _mock = mock_server
562            .mock("GET", mockito::Matcher::Any)
563            .with_status(429)
564            .create_async()
565            .await;
566
567        let client = reqwest::Client::new();
568        let response = client
569            .get(mock_server.url())
570            .send()
571            .await
572            .expect("Failed to get response");
573
574        let err = response
575            .error_for_status()
576            .expect_err("Expected error for status 429");
577
578        assert!(err.status().is_some());
579        assert_eq!(err.status().unwrap().as_u16(), 429);
580
581        let provider_error = categorize_reqwest_error(&err);
582        assert!(matches!(provider_error, ProviderError::RateLimited));
583    }
584
585    #[actix_rt::test]
586    async fn test_categorize_reqwest_error_bad_gateway() {
587        let mut mock_server = mockito::Server::new_async().await;
588
589        let _mock = mock_server
590            .mock("GET", mockito::Matcher::Any)
591            .with_status(502)
592            .create_async()
593            .await;
594
595        let client = reqwest::Client::new();
596        let response = client
597            .get(mock_server.url())
598            .send()
599            .await
600            .expect("Failed to get response");
601
602        let err = response
603            .error_for_status()
604            .expect_err("Expected error for status 502");
605
606        assert!(err.status().is_some());
607        assert_eq!(err.status().unwrap().as_u16(), 502);
608
609        let provider_error = categorize_reqwest_error(&err);
610        assert!(matches!(provider_error, ProviderError::BadGateway));
611    }
612
613    #[actix_rt::test]
614    async fn test_categorize_reqwest_error_other() {
615        let client = reqwest::Client::new();
616        let err = client
617            .get("http://non-existent-host-12345.local")
618            .send()
619            .await
620            .unwrap_err();
621
622        assert!(!err.is_timeout());
623        assert!(err.status().is_none()); // No status code
624
625        let provider_error = categorize_reqwest_error(&err);
626        assert!(matches!(provider_error, ProviderError::Other(_)));
627    }
628
629    #[test]
630    fn test_from_eyre_report_other_error() {
631        let eyre_error: eyre::Report = eyre::eyre!("Generic error");
632        let provider_error: ProviderError = eyre_error.into();
633        assert!(matches!(provider_error, ProviderError::Other(_)));
634    }
635
636    #[test]
637    fn test_get_evm_network_provider_valid_network() {
638        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
639        setup_test_env();
640
641        let network = create_test_evm_network();
642        let result = get_network_provider(&network, None);
643
644        cleanup_test_env();
645        assert!(result.is_ok());
646    }
647
648    #[test]
649    fn test_get_evm_network_provider_with_custom_urls() {
650        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
651        setup_test_env();
652
653        let network = create_test_evm_network();
654        let custom_urls = vec![
655            RpcConfig {
656                url: "https://custom-rpc1.example.com".to_string(),
657                weight: 1,
658                ..Default::default()
659            },
660            RpcConfig {
661                url: "https://custom-rpc2.example.com".to_string(),
662                weight: 1,
663                ..Default::default()
664            },
665        ];
666        let result = get_network_provider(&network, Some(custom_urls));
667
668        cleanup_test_env();
669        assert!(result.is_ok());
670    }
671
672    #[test]
673    fn test_get_evm_network_provider_with_empty_custom_urls() {
674        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
675        setup_test_env();
676
677        let network = create_test_evm_network();
678        let custom_urls: Vec<RpcConfig> = vec![];
679        let result = get_network_provider(&network, Some(custom_urls));
680
681        cleanup_test_env();
682        assert!(result.is_ok()); // Should fall back to public URLs
683    }
684
685    #[test]
686    fn test_get_solana_network_provider_valid_network_mainnet_beta() {
687        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
688        setup_test_env();
689
690        let network = create_test_solana_network("mainnet-beta");
691        let result = get_network_provider(&network, None);
692
693        cleanup_test_env();
694        assert!(result.is_ok());
695    }
696
697    #[test]
698    fn test_get_solana_network_provider_valid_network_testnet() {
699        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
700        setup_test_env();
701
702        let network = create_test_solana_network("testnet");
703        let result = get_network_provider(&network, None);
704
705        cleanup_test_env();
706        assert!(result.is_ok());
707    }
708
709    #[test]
710    fn test_get_solana_network_provider_with_custom_urls() {
711        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
712        setup_test_env();
713
714        let network = create_test_solana_network("testnet");
715        let custom_urls = vec![
716            RpcConfig {
717                url: "https://custom-rpc1.example.com".to_string(),
718                weight: 1,
719                ..Default::default()
720            },
721            RpcConfig {
722                url: "https://custom-rpc2.example.com".to_string(),
723                weight: 1,
724                ..Default::default()
725            },
726        ];
727        let result = get_network_provider(&network, Some(custom_urls));
728
729        cleanup_test_env();
730        assert!(result.is_ok());
731    }
732
733    #[test]
734    fn test_get_solana_network_provider_with_empty_custom_urls() {
735        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
736        setup_test_env();
737
738        let network = create_test_solana_network("testnet");
739        let custom_urls: Vec<RpcConfig> = vec![];
740        let result = get_network_provider(&network, Some(custom_urls));
741
742        cleanup_test_env();
743        assert!(result.is_ok()); // Should fall back to public URLs
744    }
745
746    // Tests for Stellar Network Provider
747    #[test]
748    fn test_get_stellar_network_provider_valid_network_fallback_public() {
749        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
750        setup_test_env();
751
752        let network = create_test_stellar_network();
753        let result = get_network_provider(&network, None); // No custom URLs
754
755        cleanup_test_env();
756        assert!(result.is_ok()); // Should fall back to public URLs for testnet
757                                 // StellarProvider::new will use the first public URL: https://soroban-testnet.stellar.org
758    }
759
760    #[test]
761    fn test_get_stellar_network_provider_with_custom_urls() {
762        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
763        setup_test_env();
764
765        let network = create_test_stellar_network();
766        let custom_urls = vec![
767            RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
768            RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
769                .unwrap(),
770        ];
771        let result = get_network_provider(&network, Some(custom_urls));
772
773        cleanup_test_env();
774        assert!(result.is_ok());
775        // StellarProvider::new will pick custom-stellar-rpc1 (default weight 100) over custom-stellar-rpc2 (weight 50)
776    }
777
778    #[test]
779    fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
780        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
781        setup_test_env();
782
783        let network = create_test_stellar_network();
784        let custom_urls: Vec<RpcConfig> = vec![]; // Empty custom URLs
785        let result = get_network_provider(&network, Some(custom_urls));
786
787        cleanup_test_env();
788        assert!(result.is_ok()); // Should fall back to public URLs for mainnet
789                                 // StellarProvider::new will use the first public URL: https://horizon.stellar.org
790    }
791
792    #[test]
793    fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
794        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
795        setup_test_env();
796
797        let network = create_test_stellar_network();
798        let custom_urls = vec![
799            RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
800            RpcConfig::new("http://active-rpc.example.com".to_string()), // Default weight 100
801        ];
802        let result = get_network_provider(&network, Some(custom_urls));
803        cleanup_test_env();
804        assert!(result.is_ok()); // active-rpc should be chosen
805    }
806
807    #[test]
808    fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
809        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
810        setup_test_env();
811
812        let network = create_test_stellar_network();
813        let custom_urls = vec![
814            RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
815            RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
816        ];
817        // Since StellarProvider::new filters out zero-weight URLs, and if the list becomes empty,
818        // get_network_provider does NOT re-trigger fallback to public. Instead, StellarProvider::new itself will error.
819        // The current get_network_provider logic passes the custom_urls to N::new_provider if Some and not empty.
820        // If custom_urls becomes effectively empty *inside* N::new_provider (like StellarProvider::new after filtering weights),
821        // then N::new_provider is responsible for erroring or handling.
822        let result = get_network_provider(&network, Some(custom_urls));
823        cleanup_test_env();
824        assert!(result.is_err());
825        match result.unwrap_err() {
826            ProviderError::NetworkConfiguration(msg) => {
827                assert!(msg.contains("No active RPC configurations provided"));
828            }
829            _ => panic!("Unexpected error type"),
830        }
831    }
832
833    #[test]
834    fn test_provider_error_rpc_error_code_variant() {
835        let error = ProviderError::RpcErrorCode {
836            code: -32000,
837            message: "insufficient funds".to_string(),
838        };
839        let error_string = format!("{}", error);
840        assert!(error_string.contains("-32000"));
841        assert!(error_string.contains("insufficient funds"));
842    }
843
844    #[test]
845    fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
846        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
847        setup_test_env();
848        let network = create_test_stellar_network();
849        let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
850        let result = get_network_provider(&network, Some(custom_urls));
851        cleanup_test_env();
852        assert!(result.is_err());
853        match result.unwrap_err() {
854            ProviderError::NetworkConfiguration(msg) => {
855                // This error comes from RpcConfig::validate_list inside StellarProvider::new
856                assert!(msg.contains("Invalid URL scheme"));
857            }
858            _ => panic!("Unexpected error type"),
859        }
860    }
861
862    #[test]
863    fn test_should_mark_provider_failed_server_errors() {
864        // 5xx errors should mark provider as failed
865        for status_code in 500..=599 {
866            let error = ProviderError::RequestError {
867                error: format!("Server error {}", status_code),
868                status_code,
869            };
870            assert!(
871                should_mark_provider_failed(&error),
872                "Status code {} should mark provider as failed",
873                status_code
874            );
875        }
876    }
877
878    #[test]
879    fn test_should_mark_provider_failed_auth_errors() {
880        // Authentication/authorization errors should mark provider as failed
881        let auth_errors = [401, 403];
882        for &status_code in &auth_errors {
883            let error = ProviderError::RequestError {
884                error: format!("Auth error {}", status_code),
885                status_code,
886            };
887            assert!(
888                should_mark_provider_failed(&error),
889                "Status code {} should mark provider as failed",
890                status_code
891            );
892        }
893    }
894
895    #[test]
896    fn test_should_mark_provider_failed_not_found_errors() {
897        // 404 and 410 should mark provider as failed (endpoint issues)
898        let not_found_errors = [404, 410];
899        for &status_code in &not_found_errors {
900            let error = ProviderError::RequestError {
901                error: format!("Not found error {}", status_code),
902                status_code,
903            };
904            assert!(
905                should_mark_provider_failed(&error),
906                "Status code {} should mark provider as failed",
907                status_code
908            );
909        }
910    }
911
912    #[test]
913    fn test_should_mark_provider_failed_client_errors_not_failed() {
914        // These 4xx errors should NOT mark provider as failed (client-side issues)
915        let client_errors = [400, 405, 413, 414, 415, 422, 429];
916        for &status_code in &client_errors {
917            let error = ProviderError::RequestError {
918                error: format!("Client error {}", status_code),
919                status_code,
920            };
921            assert!(
922                !should_mark_provider_failed(&error),
923                "Status code {} should NOT mark provider as failed",
924                status_code
925            );
926        }
927    }
928
929    #[test]
930    fn test_should_mark_provider_failed_other_error_types() {
931        // Test non-RequestError types - these should NOT mark provider as failed
932        let errors = [
933            ProviderError::Timeout,
934            ProviderError::RateLimited,
935            ProviderError::BadGateway,
936            ProviderError::InvalidAddress("test".to_string()),
937            ProviderError::NetworkConfiguration("test".to_string()),
938            ProviderError::Other("test".to_string()),
939        ];
940
941        for error in errors {
942            assert!(
943                !should_mark_provider_failed(&error),
944                "Error type {:?} should NOT mark provider as failed",
945                error
946            );
947        }
948    }
949
950    #[test]
951    fn test_should_mark_provider_failed_edge_cases() {
952        // Test some edge case status codes
953        let edge_cases = [
954            (200, false), // Success - shouldn't happen in error context but test anyway
955            (300, false), // Redirection
956            (418, false), // I'm a teapot - should not mark as failed
957            (451, false), // Unavailable for legal reasons - client issue
958            (499, false), // Client closed request - client issue
959        ];
960
961        for (status_code, should_fail) in edge_cases {
962            let error = ProviderError::RequestError {
963                error: format!("Edge case error {}", status_code),
964                status_code,
965            };
966            assert_eq!(
967                should_mark_provider_failed(&error),
968                should_fail,
969                "Status code {} should {} mark provider as failed",
970                status_code,
971                if should_fail { "" } else { "NOT" }
972            );
973        }
974    }
975
976    #[test]
977    fn test_is_retriable_error_retriable_types() {
978        // These error types should be retriable
979        let retriable_errors = [
980            ProviderError::Timeout,
981            ProviderError::RateLimited,
982            ProviderError::BadGateway,
983            ProviderError::TransportError("test".to_string()),
984        ];
985
986        for error in retriable_errors {
987            assert!(
988                is_retriable_error(&error),
989                "Error type {:?} should be retriable",
990                error
991            );
992        }
993    }
994
995    #[test]
996    fn test_is_retriable_error_non_retriable_types() {
997        // These error types should NOT be retriable
998        let non_retriable_errors = [
999            ProviderError::InvalidAddress("test".to_string()),
1000            ProviderError::NetworkConfiguration("test".to_string()),
1001            ProviderError::RequestError {
1002                error: "Some error".to_string(),
1003                status_code: 400,
1004            },
1005        ];
1006
1007        for error in non_retriable_errors {
1008            assert!(
1009                !is_retriable_error(&error),
1010                "Error type {:?} should NOT be retriable",
1011                error
1012            );
1013        }
1014    }
1015
1016    #[test]
1017    fn test_is_retriable_error_message_based_detection() {
1018        // Test errors that should be retriable based on message content
1019        let retriable_messages = [
1020            "Connection timeout occurred",
1021            "Network connection reset",
1022            "Connection refused",
1023            "TIMEOUT error happened",
1024            "Connection was reset by peer",
1025        ];
1026
1027        for message in retriable_messages {
1028            let error = ProviderError::Other(message.to_string());
1029            assert!(
1030                is_retriable_error(&error),
1031                "Error with message '{}' should be retriable",
1032                message
1033            );
1034        }
1035    }
1036
1037    #[test]
1038    fn test_is_retriable_error_message_based_non_retriable() {
1039        // Test errors that should NOT be retriable based on message content
1040        let non_retriable_messages = [
1041            "Invalid address format",
1042            "Bad request parameters",
1043            "Authentication failed",
1044            "Method not found",
1045            "Some other error",
1046        ];
1047
1048        for message in non_retriable_messages {
1049            let error = ProviderError::Other(message.to_string());
1050            assert!(
1051                !is_retriable_error(&error),
1052                "Error with message '{}' should NOT be retriable",
1053                message
1054            );
1055        }
1056    }
1057
1058    #[test]
1059    fn test_is_retriable_error_case_insensitive() {
1060        // Test that message-based detection is case insensitive
1061        let case_variations = [
1062            "TIMEOUT",
1063            "Timeout",
1064            "timeout",
1065            "CONNECTION",
1066            "Connection",
1067            "connection",
1068            "RESET",
1069            "Reset",
1070            "reset",
1071        ];
1072
1073        for message in case_variations {
1074            let error = ProviderError::Other(message.to_string());
1075            assert!(
1076                is_retriable_error(&error),
1077                "Error with message '{}' should be retriable (case insensitive)",
1078                message
1079            );
1080        }
1081    }
1082
1083    #[test]
1084    fn test_is_retriable_error_request_error_retriable_5xx() {
1085        // Test retriable 5xx status codes
1086        let retriable_5xx = vec![
1087            (500, "Internal Server Error"),
1088            (502, "Bad Gateway"),
1089            (503, "Service Unavailable"),
1090            (504, "Gateway Timeout"),
1091            (506, "Variant Also Negotiates"),
1092            (507, "Insufficient Storage"),
1093            (508, "Loop Detected"),
1094            (510, "Not Extended"),
1095            (511, "Network Authentication Required"),
1096            (599, "Network Connect Timeout Error"),
1097        ];
1098
1099        for (status_code, description) in retriable_5xx {
1100            let error = ProviderError::RequestError {
1101                error: description.to_string(),
1102                status_code,
1103            };
1104            assert!(
1105                is_retriable_error(&error),
1106                "Status code {} ({}) should be retriable",
1107                status_code,
1108                description
1109            );
1110        }
1111    }
1112
1113    #[test]
1114    fn test_is_retriable_error_request_error_non_retriable_5xx() {
1115        // Test non-retriable 5xx status codes (persistent server issues)
1116        let non_retriable_5xx = vec![
1117            (501, "Not Implemented"),
1118            (505, "HTTP Version Not Supported"),
1119        ];
1120
1121        for (status_code, description) in non_retriable_5xx {
1122            let error = ProviderError::RequestError {
1123                error: description.to_string(),
1124                status_code,
1125            };
1126            assert!(
1127                !is_retriable_error(&error),
1128                "Status code {} ({}) should NOT be retriable",
1129                status_code,
1130                description
1131            );
1132        }
1133    }
1134
1135    #[test]
1136    fn test_is_retriable_error_request_error_retriable_4xx() {
1137        // Test retriable 4xx status codes (timeout/rate-limit related)
1138        let retriable_4xx = vec![
1139            (408, "Request Timeout"),
1140            (425, "Too Early"),
1141            (429, "Too Many Requests"),
1142        ];
1143
1144        for (status_code, description) in retriable_4xx {
1145            let error = ProviderError::RequestError {
1146                error: description.to_string(),
1147                status_code,
1148            };
1149            assert!(
1150                is_retriable_error(&error),
1151                "Status code {} ({}) should be retriable",
1152                status_code,
1153                description
1154            );
1155        }
1156    }
1157
1158    #[test]
1159    fn test_is_retriable_error_request_error_non_retriable_4xx() {
1160        // Test non-retriable 4xx status codes (client errors)
1161        let non_retriable_4xx = vec![
1162            (400, "Bad Request"),
1163            (401, "Unauthorized"),
1164            (403, "Forbidden"),
1165            (404, "Not Found"),
1166            (405, "Method Not Allowed"),
1167            (406, "Not Acceptable"),
1168            (407, "Proxy Authentication Required"),
1169            (409, "Conflict"),
1170            (410, "Gone"),
1171            (411, "Length Required"),
1172            (412, "Precondition Failed"),
1173            (413, "Payload Too Large"),
1174            (414, "URI Too Long"),
1175            (415, "Unsupported Media Type"),
1176            (416, "Range Not Satisfiable"),
1177            (417, "Expectation Failed"),
1178            (418, "I'm a teapot"),
1179            (421, "Misdirected Request"),
1180            (422, "Unprocessable Entity"),
1181            (423, "Locked"),
1182            (424, "Failed Dependency"),
1183            (426, "Upgrade Required"),
1184            (428, "Precondition Required"),
1185            (431, "Request Header Fields Too Large"),
1186            (451, "Unavailable For Legal Reasons"),
1187            (499, "Client Closed Request"),
1188        ];
1189
1190        for (status_code, description) in non_retriable_4xx {
1191            let error = ProviderError::RequestError {
1192                error: description.to_string(),
1193                status_code,
1194            };
1195            assert!(
1196                !is_retriable_error(&error),
1197                "Status code {} ({}) should NOT be retriable",
1198                status_code,
1199                description
1200            );
1201        }
1202    }
1203
1204    #[test]
1205    fn test_is_retriable_error_request_error_other_status_codes() {
1206        // Test other status codes (1xx, 2xx, 3xx) - should not be retriable
1207        let other_status_codes = vec![
1208            (100, "Continue"),
1209            (101, "Switching Protocols"),
1210            (200, "OK"),
1211            (201, "Created"),
1212            (204, "No Content"),
1213            (300, "Multiple Choices"),
1214            (301, "Moved Permanently"),
1215            (302, "Found"),
1216            (304, "Not Modified"),
1217            (600, "Custom status"),
1218            (999, "Unknown status"),
1219        ];
1220
1221        for (status_code, description) in other_status_codes {
1222            let error = ProviderError::RequestError {
1223                error: description.to_string(),
1224                status_code,
1225            };
1226            assert!(
1227                !is_retriable_error(&error),
1228                "Status code {} ({}) should NOT be retriable",
1229                status_code,
1230                description
1231            );
1232        }
1233    }
1234
1235    #[test]
1236    fn test_is_retriable_error_request_error_boundary_cases() {
1237        // Test boundary cases for our ranges
1238        let test_cases = vec![
1239            // Just before retriable 4xx range
1240            (407, false, "Proxy Authentication Required"),
1241            (408, true, "Request Timeout - first retriable 4xx"),
1242            (409, false, "Conflict"),
1243            // Around 425
1244            (424, false, "Failed Dependency"),
1245            (425, true, "Too Early"),
1246            (426, false, "Upgrade Required"),
1247            // Around 429
1248            (428, false, "Precondition Required"),
1249            (429, true, "Too Many Requests"),
1250            (430, false, "Would be non-retriable if it existed"),
1251            // 5xx boundaries
1252            (499, false, "Last 4xx"),
1253            (500, true, "First 5xx - retriable"),
1254            (501, false, "Not Implemented - exception"),
1255            (502, true, "Bad Gateway - retriable"),
1256            (505, false, "HTTP Version Not Supported - exception"),
1257            (506, true, "First after 505 exception"),
1258            (599, true, "Last defined 5xx"),
1259        ];
1260
1261        for (status_code, should_be_retriable, description) in test_cases {
1262            let error = ProviderError::RequestError {
1263                error: description.to_string(),
1264                status_code,
1265            };
1266            assert_eq!(
1267                is_retriable_error(&error),
1268                should_be_retriable,
1269                "Status code {} ({}) should{} be retriable",
1270                status_code,
1271                description,
1272                if should_be_retriable { "" } else { " NOT" }
1273            );
1274        }
1275    }
1276}