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