openzeppelin_relayer/services/provider/
mod.rs1use std::num::ParseIntError;
2
3use crate::config::ServerConfig;
4use crate::models::{EvmNetwork, RpcConfig, SolanaNetwork, StellarNetwork};
5use serde::Serialize;
6use thiserror::Error;
7
8use alloy::transports::RpcError;
9
10pub mod evm;
11pub use evm::*;
12
13mod solana;
14pub use solana::*;
15
16mod stellar;
17pub use stellar::*;
18
19mod retry;
20pub use retry::*;
21
22pub mod rpc_selector;
23
24#[derive(Error, Debug, Serialize)]
25pub enum ProviderError {
26 #[error("RPC client error: {0}")]
27 SolanaRpcError(#[from] SolanaProviderError),
28 #[error("Invalid address: {0}")]
29 InvalidAddress(String),
30 #[error("Network configuration error: {0}")]
31 NetworkConfiguration(String),
32 #[error("Request timeout")]
33 Timeout,
34 #[error("Rate limited (HTTP 429)")]
35 RateLimited,
36 #[error("Bad gateway (HTTP 502)")]
37 BadGateway,
38 #[error("Request error (HTTP {status_code}): {error}")]
39 RequestError { error: String, status_code: u16 },
40 #[error("Other provider error: {0}")]
41 Other(String),
42}
43
44impl From<hex::FromHexError> for ProviderError {
45 fn from(err: hex::FromHexError) -> Self {
46 ProviderError::InvalidAddress(err.to_string())
47 }
48}
49
50impl From<std::net::AddrParseError> for ProviderError {
51 fn from(err: std::net::AddrParseError) -> Self {
52 ProviderError::NetworkConfiguration(format!("Invalid network address: {}", err))
53 }
54}
55
56impl From<ParseIntError> for ProviderError {
57 fn from(err: ParseIntError) -> Self {
58 ProviderError::Other(format!("Number parsing error: {}", err))
59 }
60}
61
62fn categorize_reqwest_error(err: &reqwest::Error) -> ProviderError {
79 if err.is_timeout() {
80 return ProviderError::Timeout;
81 }
82
83 if let Some(status) = err.status() {
84 match status.as_u16() {
85 429 => return ProviderError::RateLimited,
86 502 => return ProviderError::BadGateway,
87 _ => {
88 return ProviderError::RequestError {
89 error: err.to_string(),
90 status_code: status.as_u16(),
91 }
92 }
93 }
94 }
95
96 ProviderError::Other(err.to_string())
97}
98
99impl From<reqwest::Error> for ProviderError {
100 fn from(err: reqwest::Error) -> Self {
101 categorize_reqwest_error(&err)
102 }
103}
104
105impl From<&reqwest::Error> for ProviderError {
106 fn from(err: &reqwest::Error) -> Self {
107 categorize_reqwest_error(err)
108 }
109}
110
111impl From<eyre::Report> for ProviderError {
112 fn from(err: eyre::Report) -> Self {
113 if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
115 return ProviderError::from(reqwest_err);
116 }
117
118 ProviderError::Other(err.to_string())
120 }
121}
122
123impl From<String> for ProviderError {
125 fn from(error: String) -> Self {
126 ProviderError::Other(error)
127 }
128}
129
130impl<E> From<RpcError<E>> for ProviderError
132where
133 E: std::fmt::Display + std::any::Any + 'static,
134{
135 fn from(err: RpcError<E>) -> Self {
136 match err {
137 RpcError::Transport(transport_err) => {
138 if let Some(reqwest_err) =
140 (&transport_err as &dyn std::any::Any).downcast_ref::<reqwest::Error>()
141 {
142 return categorize_reqwest_error(reqwest_err);
143 }
144
145 ProviderError::Other(format!("Transport error: {}", transport_err))
147 }
148 RpcError::ErrorResp(json_rpc_err) => ProviderError::Other(format!(
149 "JSON-RPC error ({}): {}",
150 json_rpc_err.code, json_rpc_err.message
151 )),
152 _ => ProviderError::Other(format!("Other RPC error: {}", err)),
153 }
154 }
155}
156
157impl From<super::rpc_selector::RpcSelectorError> for ProviderError {
159 fn from(err: super::rpc_selector::RpcSelectorError) -> Self {
160 ProviderError::NetworkConfiguration(format!("RPC selector error: {}", err))
161 }
162}
163
164pub trait NetworkConfiguration: Sized {
165 type Provider;
166
167 fn public_rpc_urls(&self) -> Vec<String>;
168
169 fn new_provider(
170 rpc_urls: Vec<RpcConfig>,
171 timeout_seconds: u64,
172 ) -> Result<Self::Provider, ProviderError>;
173}
174
175impl NetworkConfiguration for EvmNetwork {
176 type Provider = EvmProvider;
177
178 fn public_rpc_urls(&self) -> Vec<String> {
179 (*self)
180 .public_rpc_urls()
181 .map(|urls| urls.iter().map(|url| url.to_string()).collect())
182 .unwrap_or_default()
183 }
184
185 fn new_provider(
186 rpc_urls: Vec<RpcConfig>,
187 timeout_seconds: u64,
188 ) -> Result<Self::Provider, ProviderError> {
189 EvmProvider::new(rpc_urls, timeout_seconds)
190 }
191}
192
193impl NetworkConfiguration for SolanaNetwork {
194 type Provider = SolanaProvider;
195
196 fn public_rpc_urls(&self) -> Vec<String> {
197 (*self)
198 .public_rpc_urls()
199 .map(|urls| urls.to_vec())
200 .unwrap_or_default()
201 }
202
203 fn new_provider(
204 rpc_urls: Vec<RpcConfig>,
205 timeout_seconds: u64,
206 ) -> Result<Self::Provider, ProviderError> {
207 SolanaProvider::new(rpc_urls, timeout_seconds)
208 }
209}
210
211impl NetworkConfiguration for StellarNetwork {
212 type Provider = StellarProvider;
213
214 fn public_rpc_urls(&self) -> Vec<String> {
215 (*self)
216 .public_rpc_urls()
217 .map(|urls| urls.to_vec())
218 .unwrap_or_default()
219 }
220
221 fn new_provider(
222 rpc_urls: Vec<RpcConfig>,
223 timeout_seconds: u64,
224 ) -> Result<Self::Provider, ProviderError> {
225 StellarProvider::new(rpc_urls, timeout_seconds)
226 }
227}
228
229pub fn get_network_provider<N: NetworkConfiguration>(
252 network: &N,
253 custom_rpc_urls: Option<Vec<RpcConfig>>,
254) -> Result<N::Provider, ProviderError> {
255 let rpc_timeout_ms = ServerConfig::from_env().rpc_timeout_ms;
256 let timeout_seconds = rpc_timeout_ms / 1000; let rpc_urls = match custom_rpc_urls {
259 Some(configs) if !configs.is_empty() => configs,
260 _ => {
261 let urls = network.public_rpc_urls();
262 if urls.is_empty() {
263 return Err(ProviderError::NetworkConfiguration(
264 "No public RPC URLs available for this network".to_string(),
265 ));
266 }
267 urls.into_iter().map(RpcConfig::new).collect()
268 }
269 };
270
271 N::new_provider(rpc_urls, timeout_seconds)
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use lazy_static::lazy_static;
278 use std::env;
279 use std::sync::Mutex;
280 use std::time::Duration;
281 use wiremock::matchers::any;
282 use wiremock::{Mock, MockServer, ResponseTemplate};
283
284 lazy_static! {
286 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
287 }
288
289 fn setup_test_env() {
290 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); env::set_var("REDIS_URL", "redis://localhost:6379");
292 env::set_var("RPC_TIMEOUT_MS", "5000");
293 }
294
295 fn cleanup_test_env() {
296 env::remove_var("API_KEY");
297 env::remove_var("REDIS_URL");
298 env::remove_var("RPC_TIMEOUT_MS");
299 }
300
301 fn create_test_evm_network() -> EvmNetwork {
302 EvmNetwork {
303 network: "test-evm".to_string(),
304 rpc_urls: vec!["https://rpc.example.com".to_string()],
305 explorer_urls: None,
306 average_blocktime_ms: 12000,
307 is_testnet: true,
308 tags: vec![],
309 chain_id: 1337,
310 required_confirmations: 1,
311 features: vec![],
312 symbol: "ETH".to_string(),
313 }
314 }
315
316 fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
317 SolanaNetwork {
318 network: network_str.to_string(),
319 rpc_urls: vec!["https://api.testnet.solana.com".to_string()],
320 explorer_urls: None,
321 average_blocktime_ms: 400,
322 is_testnet: true,
323 tags: vec![],
324 }
325 }
326
327 fn create_test_stellar_network() -> StellarNetwork {
328 StellarNetwork {
329 network: "testnet".to_string(),
330 rpc_urls: vec!["https://soroban-testnet.stellar.org".to_string()],
331 explorer_urls: None,
332 average_blocktime_ms: 5000,
333 is_testnet: true,
334 tags: vec![],
335 passphrase: "Test SDF Network ; September 2015".to_string(),
336 }
337 }
338
339 #[test]
340 fn test_from_hex_error() {
341 let hex_error = hex::FromHexError::OddLength;
342 let provider_error: ProviderError = hex_error.into();
343 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
344 }
345
346 #[test]
347 fn test_from_addr_parse_error() {
348 let addr_error = "invalid:address"
349 .parse::<std::net::SocketAddr>()
350 .unwrap_err();
351 let provider_error: ProviderError = addr_error.into();
352 assert!(matches!(
353 provider_error,
354 ProviderError::NetworkConfiguration(_)
355 ));
356 }
357
358 #[test]
359 fn test_from_parse_int_error() {
360 let parse_error = "not_a_number".parse::<u64>().unwrap_err();
361 let provider_error: ProviderError = parse_error.into();
362 assert!(matches!(provider_error, ProviderError::Other(_)));
363 }
364
365 #[actix_rt::test]
366 async fn test_categorize_reqwest_error_timeout() {
367 let client = reqwest::Client::new();
368 let timeout_err = client
369 .get("http://example.com")
370 .timeout(Duration::from_nanos(1))
371 .send()
372 .await
373 .unwrap_err();
374
375 assert!(timeout_err.is_timeout());
376
377 let provider_error = categorize_reqwest_error(&timeout_err);
378 assert!(matches!(provider_error, ProviderError::Timeout));
379 }
380
381 #[actix_rt::test]
382 async fn test_categorize_reqwest_error_rate_limited() {
383 let mock_server = MockServer::start().await;
384
385 Mock::given(any())
386 .respond_with(ResponseTemplate::new(429))
387 .mount(&mock_server)
388 .await;
389
390 let client = reqwest::Client::new();
391 let response = client
392 .get(mock_server.uri())
393 .send()
394 .await
395 .expect("Failed to get response");
396
397 let err = response
398 .error_for_status()
399 .expect_err("Expected error for status 429");
400
401 assert!(err.status().is_some());
402 assert_eq!(err.status().unwrap().as_u16(), 429);
403
404 let provider_error = categorize_reqwest_error(&err);
405 assert!(matches!(provider_error, ProviderError::RateLimited));
406 }
407
408 #[actix_rt::test]
409 async fn test_categorize_reqwest_error_bad_gateway() {
410 let mock_server = MockServer::start().await;
411
412 Mock::given(any())
413 .respond_with(ResponseTemplate::new(502))
414 .mount(&mock_server)
415 .await;
416
417 let client = reqwest::Client::new();
418 let response = client
419 .get(mock_server.uri())
420 .send()
421 .await
422 .expect("Failed to get response");
423
424 let err = response
425 .error_for_status()
426 .expect_err("Expected error for status 502");
427
428 assert!(err.status().is_some());
429 assert_eq!(err.status().unwrap().as_u16(), 502);
430
431 let provider_error = categorize_reqwest_error(&err);
432 assert!(matches!(provider_error, ProviderError::BadGateway));
433 }
434
435 #[actix_rt::test]
436 async fn test_categorize_reqwest_error_other() {
437 let client = reqwest::Client::new();
438 let err = client
439 .get("http://non-existent-host-12345.local")
440 .send()
441 .await
442 .unwrap_err();
443
444 assert!(!err.is_timeout());
445 assert!(err.status().is_none()); let provider_error = categorize_reqwest_error(&err);
448 assert!(matches!(provider_error, ProviderError::Other(_)));
449 }
450
451 #[test]
452 fn test_from_eyre_report_other_error() {
453 let eyre_error: eyre::Report = eyre::eyre!("Generic error");
454 let provider_error: ProviderError = eyre_error.into();
455 assert!(matches!(provider_error, ProviderError::Other(_)));
456 }
457
458 #[test]
459 fn test_get_evm_network_provider_valid_network() {
460 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
461 setup_test_env();
462
463 let network = create_test_evm_network();
464 let result = get_network_provider(&network, None);
465
466 cleanup_test_env();
467 assert!(result.is_ok());
468 }
469
470 #[test]
471 fn test_get_evm_network_provider_with_custom_urls() {
472 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
473 setup_test_env();
474
475 let network = create_test_evm_network();
476 let custom_urls = vec![
477 RpcConfig {
478 url: "https://custom-rpc1.example.com".to_string(),
479 weight: 1,
480 },
481 RpcConfig {
482 url: "https://custom-rpc2.example.com".to_string(),
483 weight: 1,
484 },
485 ];
486 let result = get_network_provider(&network, Some(custom_urls));
487
488 cleanup_test_env();
489 assert!(result.is_ok());
490 }
491
492 #[test]
493 fn test_get_evm_network_provider_with_empty_custom_urls() {
494 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
495 setup_test_env();
496
497 let network = create_test_evm_network();
498 let custom_urls: Vec<RpcConfig> = vec![];
499 let result = get_network_provider(&network, Some(custom_urls));
500
501 cleanup_test_env();
502 assert!(result.is_ok()); }
504
505 #[test]
506 fn test_get_solana_network_provider_valid_network_mainnet_beta() {
507 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
508 setup_test_env();
509
510 let network = create_test_solana_network("mainnet-beta");
511 let result = get_network_provider(&network, None);
512
513 cleanup_test_env();
514 assert!(result.is_ok());
515 }
516
517 #[test]
518 fn test_get_solana_network_provider_valid_network_testnet() {
519 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
520 setup_test_env();
521
522 let network = create_test_solana_network("testnet");
523 let result = get_network_provider(&network, None);
524
525 cleanup_test_env();
526 assert!(result.is_ok());
527 }
528
529 #[test]
530 fn test_get_solana_network_provider_with_custom_urls() {
531 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
532 setup_test_env();
533
534 let network = create_test_solana_network("testnet");
535 let custom_urls = vec![
536 RpcConfig {
537 url: "https://custom-rpc1.example.com".to_string(),
538 weight: 1,
539 },
540 RpcConfig {
541 url: "https://custom-rpc2.example.com".to_string(),
542 weight: 1,
543 },
544 ];
545 let result = get_network_provider(&network, Some(custom_urls));
546
547 cleanup_test_env();
548 assert!(result.is_ok());
549 }
550
551 #[test]
552 fn test_get_solana_network_provider_with_empty_custom_urls() {
553 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
554 setup_test_env();
555
556 let network = create_test_solana_network("testnet");
557 let custom_urls: Vec<RpcConfig> = vec![];
558 let result = get_network_provider(&network, Some(custom_urls));
559
560 cleanup_test_env();
561 assert!(result.is_ok()); }
563
564 #[test]
566 fn test_get_stellar_network_provider_valid_network_fallback_public() {
567 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
568 setup_test_env();
569
570 let network = create_test_stellar_network();
571 let result = get_network_provider(&network, None); cleanup_test_env();
574 assert!(result.is_ok()); }
577
578 #[test]
579 fn test_get_stellar_network_provider_with_custom_urls() {
580 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
581 setup_test_env();
582
583 let network = create_test_stellar_network();
584 let custom_urls = vec![
585 RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
586 RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
587 .unwrap(),
588 ];
589 let result = get_network_provider(&network, Some(custom_urls));
590
591 cleanup_test_env();
592 assert!(result.is_ok());
593 }
595
596 #[test]
597 fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
598 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
599 setup_test_env();
600
601 let network = create_test_stellar_network();
602 let custom_urls: Vec<RpcConfig> = vec![]; let result = get_network_provider(&network, Some(custom_urls));
604
605 cleanup_test_env();
606 assert!(result.is_ok()); }
609
610 #[test]
611 fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
612 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
613 setup_test_env();
614
615 let network = create_test_stellar_network();
616 let custom_urls = vec![
617 RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
618 RpcConfig::new("http://active-rpc.example.com".to_string()), ];
620 let result = get_network_provider(&network, Some(custom_urls));
621 cleanup_test_env();
622 assert!(result.is_ok()); }
624
625 #[test]
626 fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
627 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
628 setup_test_env();
629
630 let network = create_test_stellar_network();
631 let custom_urls = vec![
632 RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
633 RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
634 ];
635 let result = get_network_provider(&network, Some(custom_urls));
641 cleanup_test_env();
642 assert!(result.is_err());
643 match result.unwrap_err() {
644 ProviderError::NetworkConfiguration(msg) => {
645 assert!(msg.contains("No active RPC configurations provided"));
646 }
647 _ => panic!("Unexpected error type"),
648 }
649 }
650
651 #[test]
652 fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
653 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
654 setup_test_env();
655 let network = create_test_stellar_network();
656 let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
657 let result = get_network_provider(&network, Some(custom_urls));
658 cleanup_test_env();
659 assert!(result.is_err());
660 match result.unwrap_err() {
661 ProviderError::NetworkConfiguration(msg) => {
662 assert!(msg.contains("Invalid URL scheme"));
664 }
665 _ => panic!("Unexpected error type"),
666 }
667 }
668}