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("JSON-RPC error (code {code}): {message}")]
41 RpcErrorCode { code: i64, message: String },
42 #[error("Other provider error: {0}")]
43 Other(String),
44}
45
46impl From<hex::FromHexError> for ProviderError {
47 fn from(err: hex::FromHexError) -> Self {
48 ProviderError::InvalidAddress(err.to_string())
49 }
50}
51
52impl From<std::net::AddrParseError> for ProviderError {
53 fn from(err: std::net::AddrParseError) -> Self {
54 ProviderError::NetworkConfiguration(format!("Invalid network address: {}", err))
55 }
56}
57
58impl From<ParseIntError> for ProviderError {
59 fn from(err: ParseIntError) -> Self {
60 ProviderError::Other(format!("Number parsing error: {}", err))
61 }
62}
63
64fn categorize_reqwest_error(err: &reqwest::Error) -> ProviderError {
81 if err.is_timeout() {
82 return ProviderError::Timeout;
83 }
84
85 if let Some(status) = err.status() {
86 match status.as_u16() {
87 429 => return ProviderError::RateLimited,
88 502 => return ProviderError::BadGateway,
89 _ => {
90 return ProviderError::RequestError {
91 error: err.to_string(),
92 status_code: status.as_u16(),
93 }
94 }
95 }
96 }
97
98 ProviderError::Other(err.to_string())
99}
100
101impl From<reqwest::Error> for ProviderError {
102 fn from(err: reqwest::Error) -> Self {
103 categorize_reqwest_error(&err)
104 }
105}
106
107impl From<&reqwest::Error> for ProviderError {
108 fn from(err: &reqwest::Error) -> Self {
109 categorize_reqwest_error(err)
110 }
111}
112
113impl From<eyre::Report> for ProviderError {
114 fn from(err: eyre::Report) -> Self {
115 if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
117 return ProviderError::from(reqwest_err);
118 }
119
120 ProviderError::Other(err.to_string())
122 }
123}
124
125impl From<String> for ProviderError {
127 fn from(error: String) -> Self {
128 ProviderError::Other(error)
129 }
130}
131
132impl<E> From<RpcError<E>> for ProviderError
134where
135 E: std::fmt::Display + std::any::Any + 'static,
136{
137 fn from(err: RpcError<E>) -> Self {
138 match err {
139 RpcError::Transport(transport_err) => {
140 if let Some(reqwest_err) =
142 (&transport_err as &dyn std::any::Any).downcast_ref::<reqwest::Error>()
143 {
144 return categorize_reqwest_error(reqwest_err);
145 }
146
147 ProviderError::Other(format!("Transport error: {}", transport_err))
149 }
150 RpcError::ErrorResp(json_rpc_err) => ProviderError::RpcErrorCode {
151 code: json_rpc_err.code,
152 message: json_rpc_err.message.to_string(),
153 },
154 _ => ProviderError::Other(format!("Other RPC error: {}", err)),
155 }
156 }
157}
158
159impl From<super::rpc_selector::RpcSelectorError> for ProviderError {
161 fn from(err: super::rpc_selector::RpcSelectorError) -> Self {
162 ProviderError::NetworkConfiguration(format!("RPC selector error: {}", err))
163 }
164}
165
166pub trait NetworkConfiguration: Sized {
167 type Provider;
168
169 fn public_rpc_urls(&self) -> Vec<String>;
170
171 fn new_provider(
172 rpc_urls: Vec<RpcConfig>,
173 timeout_seconds: u64,
174 ) -> Result<Self::Provider, ProviderError>;
175}
176
177impl NetworkConfiguration for EvmNetwork {
178 type Provider = EvmProvider;
179
180 fn public_rpc_urls(&self) -> Vec<String> {
181 (*self)
182 .public_rpc_urls()
183 .map(|urls| urls.iter().map(|url| url.to_string()).collect())
184 .unwrap_or_default()
185 }
186
187 fn new_provider(
188 rpc_urls: Vec<RpcConfig>,
189 timeout_seconds: u64,
190 ) -> Result<Self::Provider, ProviderError> {
191 EvmProvider::new(rpc_urls, timeout_seconds)
192 }
193}
194
195impl NetworkConfiguration for SolanaNetwork {
196 type Provider = SolanaProvider;
197
198 fn public_rpc_urls(&self) -> Vec<String> {
199 (*self)
200 .public_rpc_urls()
201 .map(|urls| urls.to_vec())
202 .unwrap_or_default()
203 }
204
205 fn new_provider(
206 rpc_urls: Vec<RpcConfig>,
207 timeout_seconds: u64,
208 ) -> Result<Self::Provider, ProviderError> {
209 SolanaProvider::new(rpc_urls, timeout_seconds)
210 }
211}
212
213impl NetworkConfiguration for StellarNetwork {
214 type Provider = StellarProvider;
215
216 fn public_rpc_urls(&self) -> Vec<String> {
217 (*self)
218 .public_rpc_urls()
219 .map(|urls| urls.to_vec())
220 .unwrap_or_default()
221 }
222
223 fn new_provider(
224 rpc_urls: Vec<RpcConfig>,
225 timeout_seconds: u64,
226 ) -> Result<Self::Provider, ProviderError> {
227 StellarProvider::new(rpc_urls, timeout_seconds)
228 }
229}
230
231pub fn get_network_provider<N: NetworkConfiguration>(
254 network: &N,
255 custom_rpc_urls: Option<Vec<RpcConfig>>,
256) -> Result<N::Provider, ProviderError> {
257 let rpc_timeout_ms = ServerConfig::from_env().rpc_timeout_ms;
258 let timeout_seconds = rpc_timeout_ms / 1000; let rpc_urls = match custom_rpc_urls {
261 Some(configs) if !configs.is_empty() => configs,
262 _ => {
263 let urls = network.public_rpc_urls();
264 if urls.is_empty() {
265 return Err(ProviderError::NetworkConfiguration(
266 "No public RPC URLs available for this network".to_string(),
267 ));
268 }
269 urls.into_iter().map(RpcConfig::new).collect()
270 }
271 };
272
273 N::new_provider(rpc_urls, timeout_seconds)
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use lazy_static::lazy_static;
280 use std::env;
281 use std::sync::Mutex;
282 use std::time::Duration;
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 gas_price_cache: None,
314 }
315 }
316
317 fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
318 SolanaNetwork {
319 network: network_str.to_string(),
320 rpc_urls: vec!["https://api.testnet.solana.com".to_string()],
321 explorer_urls: None,
322 average_blocktime_ms: 400,
323 is_testnet: true,
324 tags: vec![],
325 }
326 }
327
328 fn create_test_stellar_network() -> StellarNetwork {
329 StellarNetwork {
330 network: "testnet".to_string(),
331 rpc_urls: vec!["https://soroban-testnet.stellar.org".to_string()],
332 explorer_urls: None,
333 average_blocktime_ms: 5000,
334 is_testnet: true,
335 tags: vec![],
336 passphrase: "Test SDF Network ; September 2015".to_string(),
337 }
338 }
339
340 #[test]
341 fn test_from_hex_error() {
342 let hex_error = hex::FromHexError::OddLength;
343 let provider_error: ProviderError = hex_error.into();
344 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
345 }
346
347 #[test]
348 fn test_from_addr_parse_error() {
349 let addr_error = "invalid:address"
350 .parse::<std::net::SocketAddr>()
351 .unwrap_err();
352 let provider_error: ProviderError = addr_error.into();
353 assert!(matches!(
354 provider_error,
355 ProviderError::NetworkConfiguration(_)
356 ));
357 }
358
359 #[test]
360 fn test_from_parse_int_error() {
361 let parse_error = "not_a_number".parse::<u64>().unwrap_err();
362 let provider_error: ProviderError = parse_error.into();
363 assert!(matches!(provider_error, ProviderError::Other(_)));
364 }
365
366 #[actix_rt::test]
367 async fn test_categorize_reqwest_error_timeout() {
368 let client = reqwest::Client::new();
369 let timeout_err = client
370 .get("http://example.com")
371 .timeout(Duration::from_nanos(1))
372 .send()
373 .await
374 .unwrap_err();
375
376 assert!(timeout_err.is_timeout());
377
378 let provider_error = categorize_reqwest_error(&timeout_err);
379 assert!(matches!(provider_error, ProviderError::Timeout));
380 }
381
382 #[actix_rt::test]
383 async fn test_categorize_reqwest_error_rate_limited() {
384 let mut mock_server = mockito::Server::new_async().await;
385
386 let _mock = mock_server
387 .mock("GET", mockito::Matcher::Any)
388 .with_status(429)
389 .create_async()
390 .await;
391
392 let client = reqwest::Client::new();
393 let response = client
394 .get(mock_server.url())
395 .send()
396 .await
397 .expect("Failed to get response");
398
399 let err = response
400 .error_for_status()
401 .expect_err("Expected error for status 429");
402
403 assert!(err.status().is_some());
404 assert_eq!(err.status().unwrap().as_u16(), 429);
405
406 let provider_error = categorize_reqwest_error(&err);
407 assert!(matches!(provider_error, ProviderError::RateLimited));
408 }
409
410 #[actix_rt::test]
411 async fn test_categorize_reqwest_error_bad_gateway() {
412 let mut mock_server = mockito::Server::new_async().await;
413
414 let _mock = mock_server
415 .mock("GET", mockito::Matcher::Any)
416 .with_status(502)
417 .create_async()
418 .await;
419
420 let client = reqwest::Client::new();
421 let response = client
422 .get(mock_server.url())
423 .send()
424 .await
425 .expect("Failed to get response");
426
427 let err = response
428 .error_for_status()
429 .expect_err("Expected error for status 502");
430
431 assert!(err.status().is_some());
432 assert_eq!(err.status().unwrap().as_u16(), 502);
433
434 let provider_error = categorize_reqwest_error(&err);
435 assert!(matches!(provider_error, ProviderError::BadGateway));
436 }
437
438 #[actix_rt::test]
439 async fn test_categorize_reqwest_error_other() {
440 let client = reqwest::Client::new();
441 let err = client
442 .get("http://non-existent-host-12345.local")
443 .send()
444 .await
445 .unwrap_err();
446
447 assert!(!err.is_timeout());
448 assert!(err.status().is_none()); let provider_error = categorize_reqwest_error(&err);
451 assert!(matches!(provider_error, ProviderError::Other(_)));
452 }
453
454 #[test]
455 fn test_from_eyre_report_other_error() {
456 let eyre_error: eyre::Report = eyre::eyre!("Generic error");
457 let provider_error: ProviderError = eyre_error.into();
458 assert!(matches!(provider_error, ProviderError::Other(_)));
459 }
460
461 #[test]
462 fn test_get_evm_network_provider_valid_network() {
463 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
464 setup_test_env();
465
466 let network = create_test_evm_network();
467 let result = get_network_provider(&network, None);
468
469 cleanup_test_env();
470 assert!(result.is_ok());
471 }
472
473 #[test]
474 fn test_get_evm_network_provider_with_custom_urls() {
475 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
476 setup_test_env();
477
478 let network = create_test_evm_network();
479 let custom_urls = vec![
480 RpcConfig {
481 url: "https://custom-rpc1.example.com".to_string(),
482 weight: 1,
483 },
484 RpcConfig {
485 url: "https://custom-rpc2.example.com".to_string(),
486 weight: 1,
487 },
488 ];
489 let result = get_network_provider(&network, Some(custom_urls));
490
491 cleanup_test_env();
492 assert!(result.is_ok());
493 }
494
495 #[test]
496 fn test_get_evm_network_provider_with_empty_custom_urls() {
497 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
498 setup_test_env();
499
500 let network = create_test_evm_network();
501 let custom_urls: Vec<RpcConfig> = vec![];
502 let result = get_network_provider(&network, Some(custom_urls));
503
504 cleanup_test_env();
505 assert!(result.is_ok()); }
507
508 #[test]
509 fn test_get_solana_network_provider_valid_network_mainnet_beta() {
510 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
511 setup_test_env();
512
513 let network = create_test_solana_network("mainnet-beta");
514 let result = get_network_provider(&network, None);
515
516 cleanup_test_env();
517 assert!(result.is_ok());
518 }
519
520 #[test]
521 fn test_get_solana_network_provider_valid_network_testnet() {
522 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
523 setup_test_env();
524
525 let network = create_test_solana_network("testnet");
526 let result = get_network_provider(&network, None);
527
528 cleanup_test_env();
529 assert!(result.is_ok());
530 }
531
532 #[test]
533 fn test_get_solana_network_provider_with_custom_urls() {
534 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
535 setup_test_env();
536
537 let network = create_test_solana_network("testnet");
538 let custom_urls = vec![
539 RpcConfig {
540 url: "https://custom-rpc1.example.com".to_string(),
541 weight: 1,
542 },
543 RpcConfig {
544 url: "https://custom-rpc2.example.com".to_string(),
545 weight: 1,
546 },
547 ];
548 let result = get_network_provider(&network, Some(custom_urls));
549
550 cleanup_test_env();
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn test_get_solana_network_provider_with_empty_custom_urls() {
556 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
557 setup_test_env();
558
559 let network = create_test_solana_network("testnet");
560 let custom_urls: Vec<RpcConfig> = vec![];
561 let result = get_network_provider(&network, Some(custom_urls));
562
563 cleanup_test_env();
564 assert!(result.is_ok()); }
566
567 #[test]
569 fn test_get_stellar_network_provider_valid_network_fallback_public() {
570 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
571 setup_test_env();
572
573 let network = create_test_stellar_network();
574 let result = get_network_provider(&network, None); cleanup_test_env();
577 assert!(result.is_ok()); }
580
581 #[test]
582 fn test_get_stellar_network_provider_with_custom_urls() {
583 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
584 setup_test_env();
585
586 let network = create_test_stellar_network();
587 let custom_urls = vec![
588 RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
589 RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
590 .unwrap(),
591 ];
592 let result = get_network_provider(&network, Some(custom_urls));
593
594 cleanup_test_env();
595 assert!(result.is_ok());
596 }
598
599 #[test]
600 fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
601 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
602 setup_test_env();
603
604 let network = create_test_stellar_network();
605 let custom_urls: Vec<RpcConfig> = vec![]; let result = get_network_provider(&network, Some(custom_urls));
607
608 cleanup_test_env();
609 assert!(result.is_ok()); }
612
613 #[test]
614 fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
615 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
616 setup_test_env();
617
618 let network = create_test_stellar_network();
619 let custom_urls = vec![
620 RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
621 RpcConfig::new("http://active-rpc.example.com".to_string()), ];
623 let result = get_network_provider(&network, Some(custom_urls));
624 cleanup_test_env();
625 assert!(result.is_ok()); }
627
628 #[test]
629 fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
630 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
631 setup_test_env();
632
633 let network = create_test_stellar_network();
634 let custom_urls = vec![
635 RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
636 RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
637 ];
638 let result = get_network_provider(&network, Some(custom_urls));
644 cleanup_test_env();
645 assert!(result.is_err());
646 match result.unwrap_err() {
647 ProviderError::NetworkConfiguration(msg) => {
648 assert!(msg.contains("No active RPC configurations provided"));
649 }
650 _ => panic!("Unexpected error type"),
651 }
652 }
653
654 #[test]
655 fn test_provider_error_rpc_error_code_variant() {
656 let error = ProviderError::RpcErrorCode {
657 code: -32000,
658 message: "insufficient funds".to_string(),
659 };
660 let error_string = format!("{}", error);
661 assert!(error_string.contains("-32000"));
662 assert!(error_string.contains("insufficient funds"));
663 }
664
665 #[test]
666 fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
667 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
668 setup_test_env();
669 let network = create_test_stellar_network();
670 let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
671 let result = get_network_provider(&network, Some(custom_urls));
672 cleanup_test_env();
673 assert!(result.is_err());
674 match result.unwrap_err() {
675 ProviderError::NetworkConfiguration(msg) => {
676 assert!(msg.contains("Invalid URL scheme"));
678 }
679 _ => panic!("Unexpected error type"),
680 }
681 }
682}