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
156pub 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 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
218fn 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 if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
271 return ProviderError::from(reqwest_err);
272 }
273
274 ProviderError::Other(err.to_string())
276 }
277}
278
279impl From<String> for ProviderError {
281 fn from(error: String) -> Self {
282 ProviderError::Other(error)
283 }
284}
285
286impl<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 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
312impl 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 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
367pub 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
410pub fn should_mark_provider_failed_by_status_code(status_code: u16) -> bool {
422 match status_code {
423 500..=599 => true,
425
426 401 => true, 403 => true, 404 => true, 410 => true, _ => 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
445fn 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
462pub fn is_retriable_error(error: &ProviderError) -> bool {
464 match error {
465 ProviderError::Timeout
467 | ProviderError::RateLimited
468 | ProviderError::BadGateway
469 | ProviderError::TransportError(_) => true,
470
471 ProviderError::RequestError { status_code, .. } => {
472 match *status_code {
473 501 | 505 => false, 500 | 502..=504 | 506..=599 => true,
478
479 408 | 425 | 429 => true,
481
482 400..=499 => false,
484
485 _ => false,
487 }
488 }
489
490 ProviderError::RpcErrorCode { code, message } => {
492 match code {
493 -32002 => !is_non_retriable_transaction_rpc_message(message),
496 -32005 => true,
498 -32603 => !is_non_retriable_transaction_rpc_message(message),
501 -32000 => false,
503 -32001 => false,
505 -32003 => false,
507 -32004 => false,
509
510 -32700..=-32600 => false,
516
517 _ => false,
519 }
520 }
521
522 ProviderError::SolanaRpcError(err) => err.is_transient(),
523
524 _ => {
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 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"); 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()); 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()); }
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()); }
832
833 #[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); cleanup_test_env();
843 assert!(result.is_ok()); }
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 }
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![]; let result = get_network_provider(&network, Some(custom_urls));
873
874 cleanup_test_env();
875 assert!(result.is_ok()); }
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()), ];
889 let result = get_network_provider(&network, Some(custom_urls));
890 cleanup_test_env();
891 assert!(result.is_ok()); }
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 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 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 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 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 let not_found_errors = [404, 410];
984 for &status_code in ¬_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 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 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 let edge_cases = [
1036 (200, false), (300, false), (418, false), (451, false), (499, false), ];
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 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 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 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 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 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 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 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 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 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 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 let test_cases = vec![
1306 (407, false, "Proxy Authentication Required"),
1308 (408, true, "Request Timeout - first retriable 4xx"),
1309 (409, false, "Conflict"),
1310 (424, false, "Failed Dependency"),
1312 (425, true, "Too Early"),
1313 (426, false, "Upgrade Required"),
1314 (428, false, "Precondition Required"),
1316 (429, true, "Too Many Requests"),
1317 (430, false, "Would be non-retriable if it existed"),
1318 (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 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 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 assert!(!is_non_retriable_transaction_rpc_message(
1373 "Unknown transaction status"
1374 ));
1375
1376 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 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 let retriable_messages = vec![
1404 "Internal error",
1405 "",
1406 "Unknown transaction status",
1408 "Resource unavailable",
1409 ];
1410
1411 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}