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