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