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