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 DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECONDS,
11 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_INTERVAL_SECONDS,
12 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_TIMEOUT_SECONDS,
13 DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECONDS, DEFAULT_HTTP_CLIENT_POOL_MAX_IDLE_PER_HOST,
14 DEFAULT_HTTP_CLIENT_TCP_KEEPALIVE_SECONDS,
15};
16use crate::models::{EvmNetwork, RpcConfig, SolanaNetwork, StellarNetwork};
17use crate::utils::create_secure_redirect_policy;
18use serde::Serialize;
19use thiserror::Error;
20
21use alloy::transports::RpcError;
22
23pub mod evm;
24pub use evm::*;
25
26mod solana;
27pub use solana::*;
28
29mod stellar;
30pub use stellar::*;
31
32mod retry;
33pub use retry::*;
34
35pub mod rpc_health_store;
36pub mod rpc_selector;
37
38pub use rpc_health_store::{RpcConfigMetadata, RpcHealthStore};
39
40#[derive(Debug, Clone)]
45pub struct ProviderConfig {
46 pub rpc_configs: Vec<RpcConfig>,
48 pub timeout_seconds: u64,
50 pub failure_threshold: u32,
52 pub pause_duration_secs: u64,
54 pub failure_expiration_secs: u64,
56}
57
58impl ProviderConfig {
59 pub fn new(
68 rpc_configs: Vec<RpcConfig>,
69 timeout_seconds: u64,
70 failure_threshold: u32,
71 pause_duration_secs: u64,
72 failure_expiration_secs: u64,
73 ) -> Self {
74 Self {
75 rpc_configs,
76 timeout_seconds,
77 failure_threshold,
78 pause_duration_secs,
79 failure_expiration_secs,
80 }
81 }
82
83 pub fn from_server_config(server_config: &ServerConfig, rpc_configs: Vec<RpcConfig>) -> Self {
92 let timeout_seconds = server_config.rpc_timeout_ms / 1000; Self {
94 rpc_configs,
95 timeout_seconds,
96 failure_threshold: server_config.provider_failure_threshold,
97 pause_duration_secs: server_config.provider_pause_duration_secs,
98 failure_expiration_secs: server_config.provider_failure_expiration_secs,
99 }
100 }
101
102 pub fn from_env(rpc_configs: Vec<RpcConfig>) -> Self {
109 let server_config = ServerConfig::from_env();
110 Self::from_server_config(&server_config, rpc_configs)
111 }
112}
113
114fn base_rpc_client_builder() -> reqwest::ClientBuilder {
117 ReqwestClient::builder()
118 .connect_timeout(Duration::from_secs(
119 DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECONDS,
120 ))
121 .pool_max_idle_per_host(DEFAULT_HTTP_CLIENT_POOL_MAX_IDLE_PER_HOST)
122 .pool_idle_timeout(Duration::from_secs(
123 DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECONDS,
124 ))
125 .tcp_keepalive(Duration::from_secs(
126 DEFAULT_HTTP_CLIENT_TCP_KEEPALIVE_SECONDS,
127 ))
128 .http2_keep_alive_interval(Some(Duration::from_secs(
129 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_INTERVAL_SECONDS,
130 )))
131 .http2_keep_alive_timeout(Duration::from_secs(
132 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_TIMEOUT_SECONDS,
133 ))
134 .use_rustls_tls()
135 .redirect(create_secure_redirect_policy())
136}
137
138static SHARED_RPC_HTTP_CLIENT: Lazy<Result<ReqwestClient, String>> = Lazy::new(|| {
141 debug!("Creating shared RPC HTTP client");
142 base_rpc_client_builder()
143 .build()
144 .map_err(|e| format!("Failed to create shared RPC HTTP client: {e}"))
145});
146
147pub fn get_shared_rpc_http_client() -> Result<ReqwestClient, ProviderError> {
149 SHARED_RPC_HTTP_CLIENT
150 .as_ref()
151 .map(|c| c.clone())
152 .map_err(|e| ProviderError::NetworkConfiguration(e.clone()))
153}
154
155pub fn build_rpc_http_client_with_timeout(
158 timeout: Duration,
159) -> Result<ReqwestClient, ProviderError> {
160 base_rpc_client_builder()
161 .timeout(timeout)
162 .build()
163 .map_err(|e| {
164 ProviderError::NetworkConfiguration(format!("Failed to build RPC HTTP client: {e}"))
165 })
166}
167
168#[derive(Error, Debug, Serialize)]
169pub enum ProviderError {
170 #[error("RPC client error: {0}")]
171 SolanaRpcError(#[from] SolanaProviderError),
172 #[error("Invalid address: {0}")]
173 InvalidAddress(String),
174 #[error("Network configuration error: {0}")]
175 NetworkConfiguration(String),
176 #[error("Request timeout")]
177 Timeout,
178 #[error("Rate limited (HTTP 429)")]
179 RateLimited,
180 #[error("Bad gateway (HTTP 502)")]
181 BadGateway,
182 #[error("Request error (HTTP {status_code}): {error}")]
183 RequestError { error: String, status_code: u16 },
184 #[error("JSON-RPC error (code {code}): {message}")]
185 RpcErrorCode { code: i64, message: String },
186 #[error("Transport error: {0}")]
187 TransportError(String),
188 #[error("Other provider error: {0}")]
189 Other(String),
190}
191
192impl ProviderError {
193 pub fn is_transient(&self) -> bool {
195 is_retriable_error(self)
196 }
197}
198
199impl From<hex::FromHexError> for ProviderError {
200 fn from(err: hex::FromHexError) -> Self {
201 ProviderError::InvalidAddress(err.to_string())
202 }
203}
204
205impl From<std::net::AddrParseError> for ProviderError {
206 fn from(err: std::net::AddrParseError) -> Self {
207 ProviderError::NetworkConfiguration(format!("Invalid network address: {err}"))
208 }
209}
210
211impl From<ParseIntError> for ProviderError {
212 fn from(err: ParseIntError) -> Self {
213 ProviderError::Other(format!("Number parsing error: {err}"))
214 }
215}
216
217fn categorize_reqwest_error(err: &reqwest::Error) -> ProviderError {
234 if err.is_timeout() {
235 return ProviderError::Timeout;
236 }
237
238 if let Some(status) = err.status() {
239 match status.as_u16() {
240 429 => return ProviderError::RateLimited,
241 502 => return ProviderError::BadGateway,
242 _ => {
243 return ProviderError::RequestError {
244 error: err.to_string(),
245 status_code: status.as_u16(),
246 }
247 }
248 }
249 }
250
251 ProviderError::Other(err.to_string())
252}
253
254impl From<reqwest::Error> for ProviderError {
255 fn from(err: reqwest::Error) -> Self {
256 categorize_reqwest_error(&err)
257 }
258}
259
260impl From<&reqwest::Error> for ProviderError {
261 fn from(err: &reqwest::Error) -> Self {
262 categorize_reqwest_error(err)
263 }
264}
265
266impl From<eyre::Report> for ProviderError {
267 fn from(err: eyre::Report) -> Self {
268 if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
270 return ProviderError::from(reqwest_err);
271 }
272
273 ProviderError::Other(err.to_string())
275 }
276}
277
278impl From<String> for ProviderError {
280 fn from(error: String) -> Self {
281 ProviderError::Other(error)
282 }
283}
284
285impl<E> From<RpcError<E>> for ProviderError
287where
288 E: std::fmt::Display + std::any::Any + 'static,
289{
290 fn from(err: RpcError<E>) -> Self {
291 match err {
292 RpcError::Transport(transport_err) => {
293 if let Some(reqwest_err) =
295 (&transport_err as &dyn std::any::Any).downcast_ref::<reqwest::Error>()
296 {
297 return categorize_reqwest_error(reqwest_err);
298 }
299
300 ProviderError::TransportError(transport_err.to_string())
301 }
302 RpcError::ErrorResp(json_rpc_err) => ProviderError::RpcErrorCode {
303 code: json_rpc_err.code,
304 message: json_rpc_err.message.to_string(),
305 },
306 _ => ProviderError::Other(format!("Other RPC error: {err}")),
307 }
308 }
309}
310
311impl From<rpc_selector::RpcSelectorError> for ProviderError {
313 fn from(err: rpc_selector::RpcSelectorError) -> Self {
314 ProviderError::NetworkConfiguration(format!("RPC selector error: {err}"))
315 }
316}
317
318pub trait NetworkConfiguration: Sized {
319 type Provider;
320
321 fn public_rpc_urls(&self) -> Vec<RpcConfig>;
322
323 fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError>;
328}
329
330impl NetworkConfiguration for EvmNetwork {
331 type Provider = EvmProvider;
332
333 fn public_rpc_urls(&self) -> Vec<RpcConfig> {
334 self.rpc_urls.clone()
335 }
336
337 fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
338 EvmProvider::new(config)
339 }
340}
341
342impl NetworkConfiguration for SolanaNetwork {
343 type Provider = SolanaProvider;
344
345 fn public_rpc_urls(&self) -> Vec<RpcConfig> {
346 self.rpc_urls.clone()
347 }
348
349 fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
350 SolanaProvider::new(config)
351 }
352}
353
354impl NetworkConfiguration for StellarNetwork {
355 type Provider = StellarProvider;
356
357 fn public_rpc_urls(&self) -> Vec<RpcConfig> {
358 self.rpc_urls.clone()
359 }
360
361 fn new_provider(config: ProviderConfig) -> Result<Self::Provider, ProviderError> {
362 StellarProvider::new(config)
363 }
364}
365
366pub fn get_network_provider<N: NetworkConfiguration>(
389 network: &N,
390 custom_rpc_urls: Option<Vec<RpcConfig>>,
391) -> Result<N::Provider, ProviderError> {
392 let rpc_urls = match custom_rpc_urls {
393 Some(configs) if !configs.is_empty() => configs,
394 _ => {
395 let configs = network.public_rpc_urls();
396 if configs.is_empty() {
397 return Err(ProviderError::NetworkConfiguration(
398 "No public RPC URLs available for this network".to_string(),
399 ));
400 }
401 configs
402 }
403 };
404
405 let provider_config = ProviderConfig::from_env(rpc_urls);
406 N::new_provider(provider_config)
407}
408
409pub fn should_mark_provider_failed_by_status_code(status_code: u16) -> bool {
421 match status_code {
422 500..=599 => true,
424
425 401 => true, 403 => true, 404 => true, 410 => true, _ => false,
432 }
433}
434
435pub fn should_mark_provider_failed(error: &ProviderError) -> bool {
436 match error {
437 ProviderError::RequestError { status_code, .. } => {
438 should_mark_provider_failed_by_status_code(*status_code)
439 }
440 _ => false,
441 }
442}
443
444pub fn is_retriable_error(error: &ProviderError) -> bool {
446 match error {
447 ProviderError::Timeout
449 | ProviderError::RateLimited
450 | ProviderError::BadGateway
451 | ProviderError::TransportError(_) => true,
452
453 ProviderError::RequestError { status_code, .. } => {
454 match *status_code {
455 501 | 505 => false, 500 | 502..=504 | 506..=599 => true,
460
461 408 | 425 | 429 => true,
463
464 400..=499 => false,
466
467 _ => false,
469 }
470 }
471
472 ProviderError::RpcErrorCode { code, .. } => {
474 match code {
475 -32002 => true,
477 -32005 => true,
479 -32603 => true,
481 -32000 => false,
483 -32001 => false,
485 -32003 => false,
487 -32004 => false,
489
490 -32700..=-32600 => false,
496
497 _ => false,
499 }
500 }
501
502 ProviderError::SolanaRpcError(err) => err.is_transient(),
503
504 _ => {
506 let err_msg = format!("{error}");
507 let msg_lower = err_msg.to_lowercase();
508 msg_lower.contains("timeout")
509 || msg_lower.contains("connection")
510 || msg_lower.contains("reset")
511 }
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use lazy_static::lazy_static;
519 use std::env;
520 use std::sync::Mutex;
521 use std::time::Duration;
522
523 lazy_static! {
525 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
526 }
527
528 fn setup_test_env() {
529 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); env::set_var("REDIS_URL", "redis://localhost:6379");
531 env::set_var("RPC_TIMEOUT_MS", "5000");
532 }
533
534 fn cleanup_test_env() {
535 env::remove_var("API_KEY");
536 env::remove_var("REDIS_URL");
537 env::remove_var("RPC_TIMEOUT_MS");
538 }
539
540 fn create_test_evm_network() -> EvmNetwork {
541 EvmNetwork {
542 network: "test-evm".to_string(),
543 rpc_urls: vec![RpcConfig::new("https://rpc.example.com".to_string())],
544 explorer_urls: None,
545 average_blocktime_ms: 12000,
546 is_testnet: true,
547 tags: vec![],
548 chain_id: 1337,
549 required_confirmations: 1,
550 features: vec![],
551 symbol: "ETH".to_string(),
552 gas_price_cache: None,
553 }
554 }
555
556 fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
557 SolanaNetwork {
558 network: network_str.to_string(),
559 rpc_urls: vec![RpcConfig::new("https://api.testnet.solana.com".to_string())],
560 explorer_urls: None,
561 average_blocktime_ms: 400,
562 is_testnet: true,
563 tags: vec![],
564 }
565 }
566
567 fn create_test_stellar_network() -> StellarNetwork {
568 StellarNetwork {
569 network: "testnet".to_string(),
570 rpc_urls: vec![RpcConfig::new(
571 "https://soroban-testnet.stellar.org".to_string(),
572 )],
573 explorer_urls: None,
574 average_blocktime_ms: 5000,
575 is_testnet: true,
576 tags: vec![],
577 passphrase: "Test SDF Network ; September 2015".to_string(),
578 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
579 }
580 }
581
582 #[test]
583 fn test_from_hex_error() {
584 let hex_error = hex::FromHexError::OddLength;
585 let provider_error: ProviderError = hex_error.into();
586 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
587 }
588
589 #[test]
590 fn test_from_addr_parse_error() {
591 let addr_error = "invalid:address"
592 .parse::<std::net::SocketAddr>()
593 .unwrap_err();
594 let provider_error: ProviderError = addr_error.into();
595 assert!(matches!(
596 provider_error,
597 ProviderError::NetworkConfiguration(_)
598 ));
599 }
600
601 #[test]
602 fn test_from_parse_int_error() {
603 let parse_error = "not_a_number".parse::<u64>().unwrap_err();
604 let provider_error: ProviderError = parse_error.into();
605 assert!(matches!(provider_error, ProviderError::Other(_)));
606 }
607
608 #[actix_rt::test]
609 async fn test_categorize_reqwest_error_timeout() {
610 let client = reqwest::Client::new();
611 let timeout_err = client
612 .get("http://example.com")
613 .timeout(Duration::from_nanos(1))
614 .send()
615 .await
616 .unwrap_err();
617
618 assert!(timeout_err.is_timeout());
619
620 let provider_error = categorize_reqwest_error(&timeout_err);
621 assert!(matches!(provider_error, ProviderError::Timeout));
622 }
623
624 #[actix_rt::test]
625 async fn test_categorize_reqwest_error_rate_limited() {
626 let mut mock_server = mockito::Server::new_async().await;
627
628 let _mock = mock_server
629 .mock("GET", mockito::Matcher::Any)
630 .with_status(429)
631 .create_async()
632 .await;
633
634 let client = reqwest::Client::new();
635 let response = client
636 .get(mock_server.url())
637 .send()
638 .await
639 .expect("Failed to get response");
640
641 let err = response
642 .error_for_status()
643 .expect_err("Expected error for status 429");
644
645 assert!(err.status().is_some());
646 assert_eq!(err.status().unwrap().as_u16(), 429);
647
648 let provider_error = categorize_reqwest_error(&err);
649 assert!(matches!(provider_error, ProviderError::RateLimited));
650 }
651
652 #[actix_rt::test]
653 async fn test_categorize_reqwest_error_bad_gateway() {
654 let mut mock_server = mockito::Server::new_async().await;
655
656 let _mock = mock_server
657 .mock("GET", mockito::Matcher::Any)
658 .with_status(502)
659 .create_async()
660 .await;
661
662 let client = reqwest::Client::new();
663 let response = client
664 .get(mock_server.url())
665 .send()
666 .await
667 .expect("Failed to get response");
668
669 let err = response
670 .error_for_status()
671 .expect_err("Expected error for status 502");
672
673 assert!(err.status().is_some());
674 assert_eq!(err.status().unwrap().as_u16(), 502);
675
676 let provider_error = categorize_reqwest_error(&err);
677 assert!(matches!(provider_error, ProviderError::BadGateway));
678 }
679
680 #[actix_rt::test]
681 async fn test_categorize_reqwest_error_other() {
682 let client = reqwest::Client::new();
683 let err = client
684 .get("http://non-existent-host-12345.local")
685 .send()
686 .await
687 .unwrap_err();
688
689 assert!(!err.is_timeout());
690 assert!(err.status().is_none()); let provider_error = categorize_reqwest_error(&err);
693 assert!(matches!(provider_error, ProviderError::Other(_)));
694 }
695
696 #[test]
697 fn test_from_eyre_report_other_error() {
698 let eyre_error: eyre::Report = eyre::eyre!("Generic error");
699 let provider_error: ProviderError = eyre_error.into();
700 assert!(matches!(provider_error, ProviderError::Other(_)));
701 }
702
703 #[test]
704 fn test_get_evm_network_provider_valid_network() {
705 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
706 setup_test_env();
707
708 let network = create_test_evm_network();
709 let result = get_network_provider(&network, None);
710
711 cleanup_test_env();
712 assert!(result.is_ok());
713 }
714
715 #[test]
716 fn test_get_evm_network_provider_with_custom_urls() {
717 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
718 setup_test_env();
719
720 let network = create_test_evm_network();
721 let custom_urls = vec![
722 RpcConfig {
723 url: "https://custom-rpc1.example.com".to_string(),
724 weight: 1,
725 ..Default::default()
726 },
727 RpcConfig {
728 url: "https://custom-rpc2.example.com".to_string(),
729 weight: 1,
730 ..Default::default()
731 },
732 ];
733 let result = get_network_provider(&network, Some(custom_urls));
734
735 cleanup_test_env();
736 assert!(result.is_ok());
737 }
738
739 #[test]
740 fn test_get_evm_network_provider_with_empty_custom_urls() {
741 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
742 setup_test_env();
743
744 let network = create_test_evm_network();
745 let custom_urls: Vec<RpcConfig> = vec![];
746 let result = get_network_provider(&network, Some(custom_urls));
747
748 cleanup_test_env();
749 assert!(result.is_ok()); }
751
752 #[test]
753 fn test_get_solana_network_provider_valid_network_mainnet_beta() {
754 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
755 setup_test_env();
756
757 let network = create_test_solana_network("mainnet-beta");
758 let result = get_network_provider(&network, None);
759
760 cleanup_test_env();
761 assert!(result.is_ok());
762 }
763
764 #[test]
765 fn test_get_solana_network_provider_valid_network_testnet() {
766 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
767 setup_test_env();
768
769 let network = create_test_solana_network("testnet");
770 let result = get_network_provider(&network, None);
771
772 cleanup_test_env();
773 assert!(result.is_ok());
774 }
775
776 #[test]
777 fn test_get_solana_network_provider_with_custom_urls() {
778 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
779 setup_test_env();
780
781 let network = create_test_solana_network("testnet");
782 let custom_urls = vec![
783 RpcConfig {
784 url: "https://custom-rpc1.example.com".to_string(),
785 weight: 1,
786 ..Default::default()
787 },
788 RpcConfig {
789 url: "https://custom-rpc2.example.com".to_string(),
790 weight: 1,
791 ..Default::default()
792 },
793 ];
794 let result = get_network_provider(&network, Some(custom_urls));
795
796 cleanup_test_env();
797 assert!(result.is_ok());
798 }
799
800 #[test]
801 fn test_get_solana_network_provider_with_empty_custom_urls() {
802 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
803 setup_test_env();
804
805 let network = create_test_solana_network("testnet");
806 let custom_urls: Vec<RpcConfig> = vec![];
807 let result = get_network_provider(&network, Some(custom_urls));
808
809 cleanup_test_env();
810 assert!(result.is_ok()); }
812
813 #[test]
815 fn test_get_stellar_network_provider_valid_network_fallback_public() {
816 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
817 setup_test_env();
818
819 let network = create_test_stellar_network();
820 let result = get_network_provider(&network, None); cleanup_test_env();
823 assert!(result.is_ok()); }
826
827 #[test]
828 fn test_get_stellar_network_provider_with_custom_urls() {
829 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
830 setup_test_env();
831
832 let network = create_test_stellar_network();
833 let custom_urls = vec![
834 RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
835 RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
836 .unwrap(),
837 ];
838 let result = get_network_provider(&network, Some(custom_urls));
839
840 cleanup_test_env();
841 assert!(result.is_ok());
842 }
844
845 #[test]
846 fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
847 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
848 setup_test_env();
849
850 let network = create_test_stellar_network();
851 let custom_urls: Vec<RpcConfig> = vec![]; let result = get_network_provider(&network, Some(custom_urls));
853
854 cleanup_test_env();
855 assert!(result.is_ok()); }
858
859 #[test]
860 fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
861 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
862 setup_test_env();
863
864 let network = create_test_stellar_network();
865 let custom_urls = vec![
866 RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
867 RpcConfig::new("http://active-rpc.example.com".to_string()), ];
869 let result = get_network_provider(&network, Some(custom_urls));
870 cleanup_test_env();
871 assert!(result.is_ok()); }
873
874 #[test]
875 fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
876 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
877 setup_test_env();
878
879 let network = create_test_stellar_network();
880 let custom_urls = vec![
881 RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
882 RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
883 ];
884 let result = get_network_provider(&network, Some(custom_urls));
890 cleanup_test_env();
891 assert!(result.is_err());
892 match result.unwrap_err() {
893 ProviderError::NetworkConfiguration(msg) => {
894 assert!(msg.contains("No active RPC configurations provided"));
895 }
896 _ => panic!("Unexpected error type"),
897 }
898 }
899
900 #[test]
901 fn test_provider_error_rpc_error_code_variant() {
902 let error = ProviderError::RpcErrorCode {
903 code: -32000,
904 message: "insufficient funds".to_string(),
905 };
906 let error_string = format!("{error}");
907 assert!(error_string.contains("-32000"));
908 assert!(error_string.contains("insufficient funds"));
909 }
910
911 #[test]
912 fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
913 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
914 setup_test_env();
915 let network = create_test_stellar_network();
916 let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
917 let result = get_network_provider(&network, Some(custom_urls));
918 cleanup_test_env();
919 assert!(result.is_err());
920 match result.unwrap_err() {
921 ProviderError::NetworkConfiguration(msg) => {
922 assert!(msg.contains("Invalid URL scheme"));
924 }
925 _ => panic!("Unexpected error type"),
926 }
927 }
928
929 #[test]
930 fn test_should_mark_provider_failed_server_errors() {
931 for status_code in 500..=599 {
933 let error = ProviderError::RequestError {
934 error: format!("Server error {status_code}"),
935 status_code,
936 };
937 assert!(
938 should_mark_provider_failed(&error),
939 "Status code {status_code} should mark provider as failed"
940 );
941 }
942 }
943
944 #[test]
945 fn test_should_mark_provider_failed_auth_errors() {
946 let auth_errors = [401, 403];
948 for &status_code in &auth_errors {
949 let error = ProviderError::RequestError {
950 error: format!("Auth error {status_code}"),
951 status_code,
952 };
953 assert!(
954 should_mark_provider_failed(&error),
955 "Status code {status_code} should mark provider as failed"
956 );
957 }
958 }
959
960 #[test]
961 fn test_should_mark_provider_failed_not_found_errors() {
962 let not_found_errors = [404, 410];
964 for &status_code in ¬_found_errors {
965 let error = ProviderError::RequestError {
966 error: format!("Not found error {status_code}"),
967 status_code,
968 };
969 assert!(
970 should_mark_provider_failed(&error),
971 "Status code {status_code} should mark provider as failed"
972 );
973 }
974 }
975
976 #[test]
977 fn test_should_mark_provider_failed_client_errors_not_failed() {
978 let client_errors = [400, 405, 413, 414, 415, 422, 429];
980 for &status_code in &client_errors {
981 let error = ProviderError::RequestError {
982 error: format!("Client error {status_code}"),
983 status_code,
984 };
985 assert!(
986 !should_mark_provider_failed(&error),
987 "Status code {status_code} should NOT mark provider as failed"
988 );
989 }
990 }
991
992 #[test]
993 fn test_should_mark_provider_failed_other_error_types() {
994 let errors = [
996 ProviderError::Timeout,
997 ProviderError::RateLimited,
998 ProviderError::BadGateway,
999 ProviderError::InvalidAddress("test".to_string()),
1000 ProviderError::NetworkConfiguration("test".to_string()),
1001 ProviderError::Other("test".to_string()),
1002 ];
1003
1004 for error in errors {
1005 assert!(
1006 !should_mark_provider_failed(&error),
1007 "Error type {error:?} should NOT mark provider as failed"
1008 );
1009 }
1010 }
1011
1012 #[test]
1013 fn test_should_mark_provider_failed_edge_cases() {
1014 let edge_cases = [
1016 (200, false), (300, false), (418, false), (451, false), (499, false), ];
1022
1023 for (status_code, should_fail) in edge_cases {
1024 let error = ProviderError::RequestError {
1025 error: format!("Edge case error {status_code}"),
1026 status_code,
1027 };
1028 assert_eq!(
1029 should_mark_provider_failed(&error),
1030 should_fail,
1031 "Status code {} should {} mark provider as failed",
1032 status_code,
1033 if should_fail { "" } else { "NOT" }
1034 );
1035 }
1036 }
1037
1038 #[test]
1039 fn test_is_retriable_error_retriable_types() {
1040 let retriable_errors = [
1042 ProviderError::Timeout,
1043 ProviderError::RateLimited,
1044 ProviderError::BadGateway,
1045 ProviderError::TransportError("test".to_string()),
1046 ];
1047
1048 for error in retriable_errors {
1049 assert!(
1050 is_retriable_error(&error),
1051 "Error type {error:?} should be retriable"
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn test_is_retriable_error_non_retriable_types() {
1058 let non_retriable_errors = [
1060 ProviderError::InvalidAddress("test".to_string()),
1061 ProviderError::NetworkConfiguration("test".to_string()),
1062 ProviderError::RequestError {
1063 error: "Some error".to_string(),
1064 status_code: 400,
1065 },
1066 ];
1067
1068 for error in non_retriable_errors {
1069 assert!(
1070 !is_retriable_error(&error),
1071 "Error type {error:?} should NOT be retriable"
1072 );
1073 }
1074 }
1075
1076 #[test]
1077 fn test_is_retriable_error_message_based_detection() {
1078 let retriable_messages = [
1080 "Connection timeout occurred",
1081 "Network connection reset",
1082 "Connection refused",
1083 "TIMEOUT error happened",
1084 "Connection was reset by peer",
1085 ];
1086
1087 for message in retriable_messages {
1088 let error = ProviderError::Other(message.to_string());
1089 assert!(
1090 is_retriable_error(&error),
1091 "Error with message '{message}' should be retriable"
1092 );
1093 }
1094 }
1095
1096 #[test]
1097 fn test_is_retriable_error_message_based_non_retriable() {
1098 let non_retriable_messages = [
1100 "Invalid address format",
1101 "Bad request parameters",
1102 "Authentication failed",
1103 "Method not found",
1104 "Some other error",
1105 ];
1106
1107 for message in non_retriable_messages {
1108 let error = ProviderError::Other(message.to_string());
1109 assert!(
1110 !is_retriable_error(&error),
1111 "Error with message '{message}' should NOT be retriable"
1112 );
1113 }
1114 }
1115
1116 #[test]
1117 fn test_is_retriable_error_case_insensitive() {
1118 let case_variations = [
1120 "TIMEOUT",
1121 "Timeout",
1122 "timeout",
1123 "CONNECTION",
1124 "Connection",
1125 "connection",
1126 "RESET",
1127 "Reset",
1128 "reset",
1129 ];
1130
1131 for message in case_variations {
1132 let error = ProviderError::Other(message.to_string());
1133 assert!(
1134 is_retriable_error(&error),
1135 "Error with message '{message}' should be retriable (case insensitive)"
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn test_is_retriable_error_request_error_retriable_5xx() {
1142 let retriable_5xx = vec![
1144 (500, "Internal Server Error"),
1145 (502, "Bad Gateway"),
1146 (503, "Service Unavailable"),
1147 (504, "Gateway Timeout"),
1148 (506, "Variant Also Negotiates"),
1149 (507, "Insufficient Storage"),
1150 (508, "Loop Detected"),
1151 (510, "Not Extended"),
1152 (511, "Network Authentication Required"),
1153 (599, "Network Connect Timeout Error"),
1154 ];
1155
1156 for (status_code, description) in retriable_5xx {
1157 let error = ProviderError::RequestError {
1158 error: description.to_string(),
1159 status_code,
1160 };
1161 assert!(
1162 is_retriable_error(&error),
1163 "Status code {status_code} ({description}) should be retriable"
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn test_is_retriable_error_request_error_non_retriable_5xx() {
1170 let non_retriable_5xx = vec![
1172 (501, "Not Implemented"),
1173 (505, "HTTP Version Not Supported"),
1174 ];
1175
1176 for (status_code, description) in non_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 NOT be retriable"
1184 );
1185 }
1186 }
1187
1188 #[test]
1189 fn test_is_retriable_error_request_error_retriable_4xx() {
1190 let retriable_4xx = vec![
1192 (408, "Request Timeout"),
1193 (425, "Too Early"),
1194 (429, "Too Many Requests"),
1195 ];
1196
1197 for (status_code, description) in retriable_4xx {
1198 let error = ProviderError::RequestError {
1199 error: description.to_string(),
1200 status_code,
1201 };
1202 assert!(
1203 is_retriable_error(&error),
1204 "Status code {status_code} ({description}) should be retriable"
1205 );
1206 }
1207 }
1208
1209 #[test]
1210 fn test_is_retriable_error_request_error_non_retriable_4xx() {
1211 let non_retriable_4xx = vec![
1213 (400, "Bad Request"),
1214 (401, "Unauthorized"),
1215 (403, "Forbidden"),
1216 (404, "Not Found"),
1217 (405, "Method Not Allowed"),
1218 (406, "Not Acceptable"),
1219 (407, "Proxy Authentication Required"),
1220 (409, "Conflict"),
1221 (410, "Gone"),
1222 (411, "Length Required"),
1223 (412, "Precondition Failed"),
1224 (413, "Payload Too Large"),
1225 (414, "URI Too Long"),
1226 (415, "Unsupported Media Type"),
1227 (416, "Range Not Satisfiable"),
1228 (417, "Expectation Failed"),
1229 (418, "I'm a teapot"),
1230 (421, "Misdirected Request"),
1231 (422, "Unprocessable Entity"),
1232 (423, "Locked"),
1233 (424, "Failed Dependency"),
1234 (426, "Upgrade Required"),
1235 (428, "Precondition Required"),
1236 (431, "Request Header Fields Too Large"),
1237 (451, "Unavailable For Legal Reasons"),
1238 (499, "Client Closed Request"),
1239 ];
1240
1241 for (status_code, description) in non_retriable_4xx {
1242 let error = ProviderError::RequestError {
1243 error: description.to_string(),
1244 status_code,
1245 };
1246 assert!(
1247 !is_retriable_error(&error),
1248 "Status code {status_code} ({description}) should NOT be retriable"
1249 );
1250 }
1251 }
1252
1253 #[test]
1254 fn test_is_retriable_error_request_error_other_status_codes() {
1255 let other_status_codes = vec![
1257 (100, "Continue"),
1258 (101, "Switching Protocols"),
1259 (200, "OK"),
1260 (201, "Created"),
1261 (204, "No Content"),
1262 (300, "Multiple Choices"),
1263 (301, "Moved Permanently"),
1264 (302, "Found"),
1265 (304, "Not Modified"),
1266 (600, "Custom status"),
1267 (999, "Unknown status"),
1268 ];
1269
1270 for (status_code, description) in other_status_codes {
1271 let error = ProviderError::RequestError {
1272 error: description.to_string(),
1273 status_code,
1274 };
1275 assert!(
1276 !is_retriable_error(&error),
1277 "Status code {status_code} ({description}) should NOT be retriable"
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn test_is_retriable_error_request_error_boundary_cases() {
1284 let test_cases = vec![
1286 (407, false, "Proxy Authentication Required"),
1288 (408, true, "Request Timeout - first retriable 4xx"),
1289 (409, false, "Conflict"),
1290 (424, false, "Failed Dependency"),
1292 (425, true, "Too Early"),
1293 (426, false, "Upgrade Required"),
1294 (428, false, "Precondition Required"),
1296 (429, true, "Too Many Requests"),
1297 (430, false, "Would be non-retriable if it existed"),
1298 (499, false, "Last 4xx"),
1300 (500, true, "First 5xx - retriable"),
1301 (501, false, "Not Implemented - exception"),
1302 (502, true, "Bad Gateway - retriable"),
1303 (505, false, "HTTP Version Not Supported - exception"),
1304 (506, true, "First after 505 exception"),
1305 (599, true, "Last defined 5xx"),
1306 ];
1307
1308 for (status_code, should_be_retriable, description) in test_cases {
1309 let error = ProviderError::RequestError {
1310 error: description.to_string(),
1311 status_code,
1312 };
1313 assert_eq!(
1314 is_retriable_error(&error),
1315 should_be_retriable,
1316 "Status code {} ({}) should{} be retriable",
1317 status_code,
1318 description,
1319 if should_be_retriable { "" } else { " NOT" }
1320 );
1321 }
1322 }
1323}