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