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/// 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        || NONCE_TOO_HIGH_PATTERNS
457            .iter()
458            .any(|p| msg_lower.contains(p))
459        || matches_known_transaction(&msg_lower)
460}
461
462// Errors that are retriable
463pub fn is_retriable_error(error: &ProviderError) -> bool {
464    match error {
465        // HTTP-level errors that are retriable
466        ProviderError::Timeout
467        | ProviderError::RateLimited
468        | ProviderError::BadGateway
469        | ProviderError::TransportError(_) => true,
470
471        ProviderError::RequestError { status_code, .. } => {
472            match *status_code {
473                // Non-retriable 5xx: persistent server-side issues
474                501 | 505 => false, // Not Implemented, HTTP Version Not Supported
475
476                // Retriable 5xx: temporary server-side issues
477                500 | 502..=504 | 506..=599 => true,
478
479                // Retriable 4xx: timeout or rate-limit related
480                408 | 425 | 429 => true,
481
482                // Non-retriable 4xx: client errors
483                400..=499 => false,
484
485                // Other status codes: not retriable
486                _ => false,
487            }
488        }
489
490        // JSON-RPC error codes (EIP-1474)
491        ProviderError::RpcErrorCode { code, message } => {
492            match code {
493                // -32002: Resource unavailable — retriable unless the message indicates a
494                // transaction-level rejection (some providers wrap nonce/tx errors here)
495                -32002 => !is_non_retriable_transaction_rpc_message(message),
496                // -32005: Limit exceeded / rate limited
497                -32005 => true,
498                // -32603: Internal error — retriable unless the message indicates a
499                // transaction-level rejection (some providers wrap nonce/tx errors here)
500                -32603 => !is_non_retriable_transaction_rpc_message(message),
501                // -32000: Invalid input
502                -32000 => false,
503                // -32001: Resource not found
504                -32001 => false,
505                // -32003: Transaction rejected
506                -32003 => false,
507                // -32004: Method not supported
508                -32004 => false,
509
510                // Standard JSON-RPC 2.0 errors (not retriable)
511                // -32700: Parse error
512                // -32600: Invalid request
513                // -32601: Method not found
514                // -32602: Invalid params
515                -32700..=-32600 => false,
516
517                // All other error codes: not retriable by default
518                _ => false,
519            }
520        }
521
522        ProviderError::SolanaRpcError(err) => err.is_transient(),
523
524        // Any other errors: check message for network-related issues
525        _ => {
526            let err_msg = format!("{error}");
527            let msg_lower = err_msg.to_lowercase();
528            msg_lower.contains("timeout")
529                || msg_lower.contains("connection")
530                || msg_lower.contains("reset")
531        }
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use lazy_static::lazy_static;
539    use std::env;
540    use std::sync::Mutex;
541    use std::time::Duration;
542
543    // Use a mutex to ensure tests don't run in parallel when modifying env vars
544    lazy_static! {
545        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
546    }
547
548    fn setup_test_env() {
549        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); // noboost
550        env::set_var("REDIS_URL", "redis://localhost:6379");
551        env::set_var("RPC_TIMEOUT_MS", "5000");
552    }
553
554    fn cleanup_test_env() {
555        env::remove_var("API_KEY");
556        env::remove_var("REDIS_URL");
557        env::remove_var("RPC_TIMEOUT_MS");
558    }
559
560    fn create_test_evm_network() -> EvmNetwork {
561        EvmNetwork {
562            network: "test-evm".to_string(),
563            rpc_urls: vec![RpcConfig::new("https://rpc.example.com".to_string())],
564            explorer_urls: None,
565            average_blocktime_ms: 12000,
566            is_testnet: true,
567            tags: vec![],
568            chain_id: 1337,
569            required_confirmations: 1,
570            features: vec![],
571            symbol: "ETH".to_string(),
572            gas_price_cache: None,
573        }
574    }
575
576    fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
577        SolanaNetwork {
578            network: network_str.to_string(),
579            rpc_urls: vec![RpcConfig::new("https://api.testnet.solana.com".to_string())],
580            explorer_urls: None,
581            average_blocktime_ms: 400,
582            is_testnet: true,
583            tags: vec![],
584        }
585    }
586
587    fn create_test_stellar_network() -> StellarNetwork {
588        StellarNetwork {
589            network: "testnet".to_string(),
590            rpc_urls: vec![RpcConfig::new(
591                "https://soroban-testnet.stellar.org".to_string(),
592            )],
593            explorer_urls: None,
594            average_blocktime_ms: 5000,
595            is_testnet: true,
596            tags: vec![],
597            passphrase: "Test SDF Network ; September 2015".to_string(),
598            horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
599        }
600    }
601
602    #[test]
603    fn test_from_hex_error() {
604        let hex_error = hex::FromHexError::OddLength;
605        let provider_error: ProviderError = hex_error.into();
606        assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
607    }
608
609    #[test]
610    fn test_from_addr_parse_error() {
611        let addr_error = "invalid:address"
612            .parse::<std::net::SocketAddr>()
613            .unwrap_err();
614        let provider_error: ProviderError = addr_error.into();
615        assert!(matches!(
616            provider_error,
617            ProviderError::NetworkConfiguration(_)
618        ));
619    }
620
621    #[test]
622    fn test_from_parse_int_error() {
623        let parse_error = "not_a_number".parse::<u64>().unwrap_err();
624        let provider_error: ProviderError = parse_error.into();
625        assert!(matches!(provider_error, ProviderError::Other(_)));
626    }
627
628    #[actix_rt::test]
629    async fn test_categorize_reqwest_error_timeout() {
630        let client = reqwest::Client::new();
631        let timeout_err = client
632            .get("http://example.com")
633            .timeout(Duration::from_nanos(1))
634            .send()
635            .await
636            .unwrap_err();
637
638        assert!(timeout_err.is_timeout());
639
640        let provider_error = categorize_reqwest_error(&timeout_err);
641        assert!(matches!(provider_error, ProviderError::Timeout));
642    }
643
644    #[actix_rt::test]
645    async fn test_categorize_reqwest_error_rate_limited() {
646        let mut mock_server = mockito::Server::new_async().await;
647
648        let _mock = mock_server
649            .mock("GET", mockito::Matcher::Any)
650            .with_status(429)
651            .create_async()
652            .await;
653
654        let client = reqwest::Client::new();
655        let response = client
656            .get(mock_server.url())
657            .send()
658            .await
659            .expect("Failed to get response");
660
661        let err = response
662            .error_for_status()
663            .expect_err("Expected error for status 429");
664
665        assert!(err.status().is_some());
666        assert_eq!(err.status().unwrap().as_u16(), 429);
667
668        let provider_error = categorize_reqwest_error(&err);
669        assert!(matches!(provider_error, ProviderError::RateLimited));
670    }
671
672    #[actix_rt::test]
673    async fn test_categorize_reqwest_error_bad_gateway() {
674        let mut mock_server = mockito::Server::new_async().await;
675
676        let _mock = mock_server
677            .mock("GET", mockito::Matcher::Any)
678            .with_status(502)
679            .create_async()
680            .await;
681
682        let client = reqwest::Client::new();
683        let response = client
684            .get(mock_server.url())
685            .send()
686            .await
687            .expect("Failed to get response");
688
689        let err = response
690            .error_for_status()
691            .expect_err("Expected error for status 502");
692
693        assert!(err.status().is_some());
694        assert_eq!(err.status().unwrap().as_u16(), 502);
695
696        let provider_error = categorize_reqwest_error(&err);
697        assert!(matches!(provider_error, ProviderError::BadGateway));
698    }
699
700    #[actix_rt::test]
701    async fn test_categorize_reqwest_error_other() {
702        let client = reqwest::Client::new();
703        let err = client
704            .get("http://non-existent-host-12345.local")
705            .send()
706            .await
707            .unwrap_err();
708
709        assert!(!err.is_timeout());
710        assert!(err.status().is_none()); // No status code
711
712        let provider_error = categorize_reqwest_error(&err);
713        assert!(matches!(provider_error, ProviderError::Other(_)));
714    }
715
716    #[test]
717    fn test_from_eyre_report_other_error() {
718        let eyre_error: eyre::Report = eyre::eyre!("Generic error");
719        let provider_error: ProviderError = eyre_error.into();
720        assert!(matches!(provider_error, ProviderError::Other(_)));
721    }
722
723    #[test]
724    fn test_get_evm_network_provider_valid_network() {
725        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
726        setup_test_env();
727
728        let network = create_test_evm_network();
729        let result = get_network_provider(&network, None);
730
731        cleanup_test_env();
732        assert!(result.is_ok());
733    }
734
735    #[test]
736    fn test_get_evm_network_provider_with_custom_urls() {
737        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
738        setup_test_env();
739
740        let network = create_test_evm_network();
741        let custom_urls = vec![
742            RpcConfig {
743                url: "https://custom-rpc1.example.com".to_string(),
744                weight: 1,
745                ..Default::default()
746            },
747            RpcConfig {
748                url: "https://custom-rpc2.example.com".to_string(),
749                weight: 1,
750                ..Default::default()
751            },
752        ];
753        let result = get_network_provider(&network, Some(custom_urls));
754
755        cleanup_test_env();
756        assert!(result.is_ok());
757    }
758
759    #[test]
760    fn test_get_evm_network_provider_with_empty_custom_urls() {
761        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
762        setup_test_env();
763
764        let network = create_test_evm_network();
765        let custom_urls: Vec<RpcConfig> = vec![];
766        let result = get_network_provider(&network, Some(custom_urls));
767
768        cleanup_test_env();
769        assert!(result.is_ok()); // Should fall back to public URLs
770    }
771
772    #[test]
773    fn test_get_solana_network_provider_valid_network_mainnet_beta() {
774        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
775        setup_test_env();
776
777        let network = create_test_solana_network("mainnet-beta");
778        let result = get_network_provider(&network, None);
779
780        cleanup_test_env();
781        assert!(result.is_ok());
782    }
783
784    #[test]
785    fn test_get_solana_network_provider_valid_network_testnet() {
786        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
787        setup_test_env();
788
789        let network = create_test_solana_network("testnet");
790        let result = get_network_provider(&network, None);
791
792        cleanup_test_env();
793        assert!(result.is_ok());
794    }
795
796    #[test]
797    fn test_get_solana_network_provider_with_custom_urls() {
798        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
799        setup_test_env();
800
801        let network = create_test_solana_network("testnet");
802        let custom_urls = vec![
803            RpcConfig {
804                url: "https://custom-rpc1.example.com".to_string(),
805                weight: 1,
806                ..Default::default()
807            },
808            RpcConfig {
809                url: "https://custom-rpc2.example.com".to_string(),
810                weight: 1,
811                ..Default::default()
812            },
813        ];
814        let result = get_network_provider(&network, Some(custom_urls));
815
816        cleanup_test_env();
817        assert!(result.is_ok());
818    }
819
820    #[test]
821    fn test_get_solana_network_provider_with_empty_custom_urls() {
822        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
823        setup_test_env();
824
825        let network = create_test_solana_network("testnet");
826        let custom_urls: Vec<RpcConfig> = vec![];
827        let result = get_network_provider(&network, Some(custom_urls));
828
829        cleanup_test_env();
830        assert!(result.is_ok()); // Should fall back to public URLs
831    }
832
833    // Tests for Stellar Network Provider
834    #[test]
835    fn test_get_stellar_network_provider_valid_network_fallback_public() {
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 result = get_network_provider(&network, None); // No custom URLs
841
842        cleanup_test_env();
843        assert!(result.is_ok()); // Should fall back to public URLs for testnet
844                                 // StellarProvider::new will use the first public URL: https://soroban-testnet.stellar.org
845    }
846
847    #[test]
848    fn test_get_stellar_network_provider_with_custom_urls() {
849        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
850        setup_test_env();
851
852        let network = create_test_stellar_network();
853        let custom_urls = vec![
854            RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
855            RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
856                .unwrap(),
857        ];
858        let result = get_network_provider(&network, Some(custom_urls));
859
860        cleanup_test_env();
861        assert!(result.is_ok());
862        // StellarProvider::new will pick custom-stellar-rpc1 (default weight 100) over custom-stellar-rpc2 (weight 50)
863    }
864
865    #[test]
866    fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
867        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
868        setup_test_env();
869
870        let network = create_test_stellar_network();
871        let custom_urls: Vec<RpcConfig> = vec![]; // Empty custom URLs
872        let result = get_network_provider(&network, Some(custom_urls));
873
874        cleanup_test_env();
875        assert!(result.is_ok()); // Should fall back to public URLs for mainnet
876                                 // StellarProvider::new will use the first public URL: https://horizon.stellar.org
877    }
878
879    #[test]
880    fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
881        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
882        setup_test_env();
883
884        let network = create_test_stellar_network();
885        let custom_urls = vec![
886            RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
887            RpcConfig::new("http://active-rpc.example.com".to_string()), // Default weight 100
888        ];
889        let result = get_network_provider(&network, Some(custom_urls));
890        cleanup_test_env();
891        assert!(result.is_ok()); // active-rpc should be chosen
892    }
893
894    #[test]
895    fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
896        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
897        setup_test_env();
898
899        let network = create_test_stellar_network();
900        let custom_urls = vec![
901            RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
902            RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
903        ];
904        // Since StellarProvider::new filters out zero-weight URLs, and if the list becomes empty,
905        // get_network_provider does NOT re-trigger fallback to public. Instead, StellarProvider::new itself will error.
906        // The current get_network_provider logic passes the custom_urls to N::new_provider if Some and not empty.
907        // If custom_urls becomes effectively empty *inside* N::new_provider (like StellarProvider::new after filtering weights),
908        // then N::new_provider is responsible for erroring or handling.
909        let result = get_network_provider(&network, Some(custom_urls));
910        cleanup_test_env();
911        assert!(result.is_err());
912        match result.unwrap_err() {
913            ProviderError::NetworkConfiguration(msg) => {
914                assert!(msg.contains("No active RPC configurations provided"));
915            }
916            _ => panic!("Unexpected error type"),
917        }
918    }
919
920    #[test]
921    fn test_provider_error_rpc_error_code_variant() {
922        let error = ProviderError::RpcErrorCode {
923            code: -32000,
924            message: "insufficient funds".to_string(),
925        };
926        let error_string = format!("{error}");
927        assert!(error_string.contains("-32000"));
928        assert!(error_string.contains("insufficient funds"));
929    }
930
931    #[test]
932    fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
933        let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
934        setup_test_env();
935        let network = create_test_stellar_network();
936        let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
937        let result = get_network_provider(&network, Some(custom_urls));
938        cleanup_test_env();
939        assert!(result.is_err());
940        match result.unwrap_err() {
941            ProviderError::NetworkConfiguration(msg) => {
942                // This error comes from RpcConfig::validate_list inside StellarProvider::new
943                assert!(msg.contains("Invalid URL scheme"));
944            }
945            _ => panic!("Unexpected error type"),
946        }
947    }
948
949    #[test]
950    fn test_should_mark_provider_failed_server_errors() {
951        // 5xx errors should mark provider as failed
952        for status_code in 500..=599 {
953            let error = ProviderError::RequestError {
954                error: format!("Server error {status_code}"),
955                status_code,
956            };
957            assert!(
958                should_mark_provider_failed(&error),
959                "Status code {status_code} should mark provider as failed"
960            );
961        }
962    }
963
964    #[test]
965    fn test_should_mark_provider_failed_auth_errors() {
966        // Authentication/authorization errors should mark provider as failed
967        let auth_errors = [401, 403];
968        for &status_code in &auth_errors {
969            let error = ProviderError::RequestError {
970                error: format!("Auth error {status_code}"),
971                status_code,
972            };
973            assert!(
974                should_mark_provider_failed(&error),
975                "Status code {status_code} should mark provider as failed"
976            );
977        }
978    }
979
980    #[test]
981    fn test_should_mark_provider_failed_not_found_errors() {
982        // 404 and 410 should mark provider as failed (endpoint issues)
983        let not_found_errors = [404, 410];
984        for &status_code in &not_found_errors {
985            let error = ProviderError::RequestError {
986                error: format!("Not found error {status_code}"),
987                status_code,
988            };
989            assert!(
990                should_mark_provider_failed(&error),
991                "Status code {status_code} should mark provider as failed"
992            );
993        }
994    }
995
996    #[test]
997    fn test_should_mark_provider_failed_client_errors_not_failed() {
998        // These 4xx errors should NOT mark provider as failed (client-side issues)
999        let client_errors = [400, 405, 413, 414, 415, 422, 429];
1000        for &status_code in &client_errors {
1001            let error = ProviderError::RequestError {
1002                error: format!("Client error {status_code}"),
1003                status_code,
1004            };
1005            assert!(
1006                !should_mark_provider_failed(&error),
1007                "Status code {status_code} should NOT mark provider as failed"
1008            );
1009        }
1010    }
1011
1012    #[test]
1013    fn test_should_mark_provider_failed_other_error_types() {
1014        // Test non-RequestError types - these should NOT mark provider as failed
1015        let errors = [
1016            ProviderError::Timeout,
1017            ProviderError::RateLimited,
1018            ProviderError::BadGateway,
1019            ProviderError::InvalidAddress("test".to_string()),
1020            ProviderError::NetworkConfiguration("test".to_string()),
1021            ProviderError::Other("test".to_string()),
1022        ];
1023
1024        for error in errors {
1025            assert!(
1026                !should_mark_provider_failed(&error),
1027                "Error type {error:?} should NOT mark provider as failed"
1028            );
1029        }
1030    }
1031
1032    #[test]
1033    fn test_should_mark_provider_failed_edge_cases() {
1034        // Test some edge case status codes
1035        let edge_cases = [
1036            (200, false), // Success - shouldn't happen in error context but test anyway
1037            (300, false), // Redirection
1038            (418, false), // I'm a teapot - should not mark as failed
1039            (451, false), // Unavailable for legal reasons - client issue
1040            (499, false), // Client closed request - client issue
1041        ];
1042
1043        for (status_code, should_fail) in edge_cases {
1044            let error = ProviderError::RequestError {
1045                error: format!("Edge case error {status_code}"),
1046                status_code,
1047            };
1048            assert_eq!(
1049                should_mark_provider_failed(&error),
1050                should_fail,
1051                "Status code {} should {} mark provider as failed",
1052                status_code,
1053                if should_fail { "" } else { "NOT" }
1054            );
1055        }
1056    }
1057
1058    #[test]
1059    fn test_is_retriable_error_retriable_types() {
1060        // These error types should be retriable
1061        let retriable_errors = [
1062            ProviderError::Timeout,
1063            ProviderError::RateLimited,
1064            ProviderError::BadGateway,
1065            ProviderError::TransportError("test".to_string()),
1066        ];
1067
1068        for error in retriable_errors {
1069            assert!(
1070                is_retriable_error(&error),
1071                "Error type {error:?} should be retriable"
1072            );
1073        }
1074    }
1075
1076    #[test]
1077    fn test_is_retriable_error_non_retriable_types() {
1078        // These error types should NOT be retriable
1079        let non_retriable_errors = [
1080            ProviderError::InvalidAddress("test".to_string()),
1081            ProviderError::NetworkConfiguration("test".to_string()),
1082            ProviderError::RequestError {
1083                error: "Some error".to_string(),
1084                status_code: 400,
1085            },
1086        ];
1087
1088        for error in non_retriable_errors {
1089            assert!(
1090                !is_retriable_error(&error),
1091                "Error type {error:?} should NOT be retriable"
1092            );
1093        }
1094    }
1095
1096    #[test]
1097    fn test_is_retriable_error_message_based_detection() {
1098        // Test errors that should be retriable based on message content
1099        let retriable_messages = [
1100            "Connection timeout occurred",
1101            "Network connection reset",
1102            "Connection refused",
1103            "TIMEOUT error happened",
1104            "Connection was reset by peer",
1105        ];
1106
1107        for message in retriable_messages {
1108            let error = ProviderError::Other(message.to_string());
1109            assert!(
1110                is_retriable_error(&error),
1111                "Error with message '{message}' should be retriable"
1112            );
1113        }
1114    }
1115
1116    #[test]
1117    fn test_is_retriable_error_message_based_non_retriable() {
1118        // Test errors that should NOT be retriable based on message content
1119        let non_retriable_messages = [
1120            "Invalid address format",
1121            "Bad request parameters",
1122            "Authentication failed",
1123            "Method not found",
1124            "Some other error",
1125        ];
1126
1127        for message in non_retriable_messages {
1128            let error = ProviderError::Other(message.to_string());
1129            assert!(
1130                !is_retriable_error(&error),
1131                "Error with message '{message}' should NOT be retriable"
1132            );
1133        }
1134    }
1135
1136    #[test]
1137    fn test_is_retriable_error_case_insensitive() {
1138        // Test that message-based detection is case insensitive
1139        let case_variations = [
1140            "TIMEOUT",
1141            "Timeout",
1142            "timeout",
1143            "CONNECTION",
1144            "Connection",
1145            "connection",
1146            "RESET",
1147            "Reset",
1148            "reset",
1149        ];
1150
1151        for message in case_variations {
1152            let error = ProviderError::Other(message.to_string());
1153            assert!(
1154                is_retriable_error(&error),
1155                "Error with message '{message}' should be retriable (case insensitive)"
1156            );
1157        }
1158    }
1159
1160    #[test]
1161    fn test_is_retriable_error_request_error_retriable_5xx() {
1162        // Test retriable 5xx status codes
1163        let retriable_5xx = vec![
1164            (500, "Internal Server Error"),
1165            (502, "Bad Gateway"),
1166            (503, "Service Unavailable"),
1167            (504, "Gateway Timeout"),
1168            (506, "Variant Also Negotiates"),
1169            (507, "Insufficient Storage"),
1170            (508, "Loop Detected"),
1171            (510, "Not Extended"),
1172            (511, "Network Authentication Required"),
1173            (599, "Network Connect Timeout Error"),
1174        ];
1175
1176        for (status_code, description) in retriable_5xx {
1177            let error = ProviderError::RequestError {
1178                error: description.to_string(),
1179                status_code,
1180            };
1181            assert!(
1182                is_retriable_error(&error),
1183                "Status code {status_code} ({description}) should be retriable"
1184            );
1185        }
1186    }
1187
1188    #[test]
1189    fn test_is_retriable_error_request_error_non_retriable_5xx() {
1190        // Test non-retriable 5xx status codes (persistent server issues)
1191        let non_retriable_5xx = vec![
1192            (501, "Not Implemented"),
1193            (505, "HTTP Version Not Supported"),
1194        ];
1195
1196        for (status_code, description) in non_retriable_5xx {
1197            let error = ProviderError::RequestError {
1198                error: description.to_string(),
1199                status_code,
1200            };
1201            assert!(
1202                !is_retriable_error(&error),
1203                "Status code {status_code} ({description}) should NOT be retriable"
1204            );
1205        }
1206    }
1207
1208    #[test]
1209    fn test_is_retriable_error_request_error_retriable_4xx() {
1210        // Test retriable 4xx status codes (timeout/rate-limit related)
1211        let retriable_4xx = vec![
1212            (408, "Request Timeout"),
1213            (425, "Too Early"),
1214            (429, "Too Many Requests"),
1215        ];
1216
1217        for (status_code, description) in retriable_4xx {
1218            let error = ProviderError::RequestError {
1219                error: description.to_string(),
1220                status_code,
1221            };
1222            assert!(
1223                is_retriable_error(&error),
1224                "Status code {status_code} ({description}) should be retriable"
1225            );
1226        }
1227    }
1228
1229    #[test]
1230    fn test_is_retriable_error_request_error_non_retriable_4xx() {
1231        // Test non-retriable 4xx status codes (client errors)
1232        let non_retriable_4xx = vec![
1233            (400, "Bad Request"),
1234            (401, "Unauthorized"),
1235            (403, "Forbidden"),
1236            (404, "Not Found"),
1237            (405, "Method Not Allowed"),
1238            (406, "Not Acceptable"),
1239            (407, "Proxy Authentication Required"),
1240            (409, "Conflict"),
1241            (410, "Gone"),
1242            (411, "Length Required"),
1243            (412, "Precondition Failed"),
1244            (413, "Payload Too Large"),
1245            (414, "URI Too Long"),
1246            (415, "Unsupported Media Type"),
1247            (416, "Range Not Satisfiable"),
1248            (417, "Expectation Failed"),
1249            (418, "I'm a teapot"),
1250            (421, "Misdirected Request"),
1251            (422, "Unprocessable Entity"),
1252            (423, "Locked"),
1253            (424, "Failed Dependency"),
1254            (426, "Upgrade Required"),
1255            (428, "Precondition Required"),
1256            (431, "Request Header Fields Too Large"),
1257            (451, "Unavailable For Legal Reasons"),
1258            (499, "Client Closed Request"),
1259        ];
1260
1261        for (status_code, description) in non_retriable_4xx {
1262            let error = ProviderError::RequestError {
1263                error: description.to_string(),
1264                status_code,
1265            };
1266            assert!(
1267                !is_retriable_error(&error),
1268                "Status code {status_code} ({description}) should NOT be retriable"
1269            );
1270        }
1271    }
1272
1273    #[test]
1274    fn test_is_retriable_error_request_error_other_status_codes() {
1275        // Test other status codes (1xx, 2xx, 3xx) - should not be retriable
1276        let other_status_codes = vec![
1277            (100, "Continue"),
1278            (101, "Switching Protocols"),
1279            (200, "OK"),
1280            (201, "Created"),
1281            (204, "No Content"),
1282            (300, "Multiple Choices"),
1283            (301, "Moved Permanently"),
1284            (302, "Found"),
1285            (304, "Not Modified"),
1286            (600, "Custom status"),
1287            (999, "Unknown status"),
1288        ];
1289
1290        for (status_code, description) in other_status_codes {
1291            let error = ProviderError::RequestError {
1292                error: description.to_string(),
1293                status_code,
1294            };
1295            assert!(
1296                !is_retriable_error(&error),
1297                "Status code {status_code} ({description}) should NOT be retriable"
1298            );
1299        }
1300    }
1301
1302    #[test]
1303    fn test_is_retriable_error_request_error_boundary_cases() {
1304        // Test boundary cases for our ranges
1305        let test_cases = vec![
1306            // Just before retriable 4xx range
1307            (407, false, "Proxy Authentication Required"),
1308            (408, true, "Request Timeout - first retriable 4xx"),
1309            (409, false, "Conflict"),
1310            // Around 425
1311            (424, false, "Failed Dependency"),
1312            (425, true, "Too Early"),
1313            (426, false, "Upgrade Required"),
1314            // Around 429
1315            (428, false, "Precondition Required"),
1316            (429, true, "Too Many Requests"),
1317            (430, false, "Would be non-retriable if it existed"),
1318            // 5xx boundaries
1319            (499, false, "Last 4xx"),
1320            (500, true, "First 5xx - retriable"),
1321            (501, false, "Not Implemented - exception"),
1322            (502, true, "Bad Gateway - retriable"),
1323            (505, false, "HTTP Version Not Supported - exception"),
1324            (506, true, "First after 505 exception"),
1325            (599, true, "Last defined 5xx"),
1326        ];
1327
1328        for (status_code, should_be_retriable, description) in test_cases {
1329            let error = ProviderError::RequestError {
1330                error: description.to_string(),
1331                status_code,
1332            };
1333            assert_eq!(
1334                is_retriable_error(&error),
1335                should_be_retriable,
1336                "Status code {} ({}) should{} be retriable",
1337                status_code,
1338                description,
1339                if should_be_retriable { "" } else { " NOT" }
1340            );
1341        }
1342    }
1343
1344    #[test]
1345    fn test_is_non_retriable_transaction_rpc_message() {
1346        // Positive cases: these messages should be recognized as non-retriable
1347        assert!(is_non_retriable_transaction_rpc_message("nonce too low"));
1348        assert!(is_non_retriable_transaction_rpc_message("Nonce Too Low"));
1349        assert!(is_non_retriable_transaction_rpc_message("nonce is too low"));
1350        assert!(is_non_retriable_transaction_rpc_message("already known"));
1351        assert!(is_non_retriable_transaction_rpc_message(
1352            "known transaction"
1353        ));
1354        assert!(is_non_retriable_transaction_rpc_message(
1355            "Known Transaction"
1356        ));
1357        assert!(is_non_retriable_transaction_rpc_message(
1358            "replacement transaction underpriced"
1359        ));
1360        assert!(is_non_retriable_transaction_rpc_message(
1361            "same hash was already imported"
1362        ));
1363        assert!(is_non_retriable_transaction_rpc_message(
1364            "Transaction nonce too low"
1365        ));
1366
1367        // Negative cases: generic/unrelated messages should not match
1368        assert!(!is_non_retriable_transaction_rpc_message("Internal error"));
1369        assert!(!is_non_retriable_transaction_rpc_message("server busy"));
1370        assert!(!is_non_retriable_transaction_rpc_message(""));
1371        // "unknown transaction" must NOT match "known transaction"
1372        assert!(!is_non_retriable_transaction_rpc_message(
1373            "Unknown transaction status"
1374        ));
1375
1376        // Nonce-too-high patterns are also non-retriable
1377        assert!(is_non_retriable_transaction_rpc_message("nonce too high"));
1378        assert!(is_non_retriable_transaction_rpc_message(
1379            "nonce too far in the future",
1380        ));
1381        assert!(is_non_retriable_transaction_rpc_message(
1382            "exceeds next nonce"
1383        ));
1384        assert!(is_non_retriable_transaction_rpc_message(
1385            "Nonce Too Far In The Future"
1386        ));
1387    }
1388
1389    #[test]
1390    fn test_is_retriable_error_rpc_tx_errors_not_retriable() {
1391        // Transaction-level messages that should NOT be retriable regardless of code
1392        let non_retriable_messages = vec![
1393            "Transaction nonce too low",
1394            "nonce too low",
1395            "nonce is too low",
1396            "already known",
1397            "known transaction",
1398            "replacement transaction underpriced",
1399            "same hash was already imported",
1400        ];
1401
1402        // Messages that should remain retriable (generic/unrelated)
1403        let retriable_messages = vec![
1404            "Internal error",
1405            "",
1406            // "unknown transaction" must NOT false-positive on "known transaction"
1407            "Unknown transaction status",
1408            "Resource unavailable",
1409        ];
1410
1411        // Both -32603 and -32002 should behave the same way for tx-level messages
1412        for code in [-32603, -32002] {
1413            for message in &non_retriable_messages {
1414                let error = ProviderError::RpcErrorCode {
1415                    code,
1416                    message: message.to_string(),
1417                };
1418                assert!(
1419                    !is_retriable_error(&error),
1420                    "{code} with message {message:?} should NOT be retriable"
1421                );
1422            }
1423
1424            for message in &retriable_messages {
1425                let error = ProviderError::RpcErrorCode {
1426                    code,
1427                    message: message.to_string(),
1428                };
1429                assert!(
1430                    is_retriable_error(&error),
1431                    "{code} with message {message:?} should be retriable"
1432                );
1433            }
1434        }
1435    }
1436}