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