openzeppelin_relayer/services/provider/
mod.rs

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