1use std::time::Duration;
8
9use alloy::{
10 network::AnyNetwork,
11 primitives::{Bytes, TxKind, Uint},
12 providers::{
13 fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller},
14 Identity, Provider, ProviderBuilder, RootProvider,
15 },
16 rpc::{
17 client::ClientBuilder,
18 types::{BlockNumberOrTag, FeeHistory, TransactionInput, TransactionRequest},
19 },
20 transports::http::Http,
21};
22
23type EvmProviderType = FillProvider<
24 JoinFill<
25 Identity,
26 JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
27 >,
28 RootProvider<AnyNetwork>,
29 AnyNetwork,
30>;
31use async_trait::async_trait;
32use eyre::Result;
33use reqwest::ClientBuilder as ReqwestClientBuilder;
34use serde_json;
35
36use super::rpc_selector::RpcSelector;
37use super::{retry_rpc_call, RetryConfig};
38use crate::models::{
39 BlockResponse, EvmTransactionData, RpcConfig, TransactionError, TransactionReceipt, U256,
40};
41
42#[cfg(test)]
43use mockall::automock;
44
45use super::ProviderError;
46
47#[derive(Clone)]
51pub struct EvmProvider {
52 selector: RpcSelector,
54 timeout_seconds: u64,
56 retry_config: RetryConfig,
58}
59
60#[async_trait]
65#[cfg_attr(test, automock)]
66#[allow(dead_code)]
67pub trait EvmProviderTrait: Send + Sync {
68 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError>;
73
74 async fn get_block_number(&self) -> Result<u64, ProviderError>;
76
77 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError>;
82
83 async fn get_gas_price(&self) -> Result<u128, ProviderError>;
85
86 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError>;
91
92 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError>;
97
98 async fn health_check(&self) -> Result<bool, ProviderError>;
100
101 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError>;
106
107 async fn get_fee_history(
114 &self,
115 block_count: u64,
116 newest_block: BlockNumberOrTag,
117 reward_percentiles: Vec<f64>,
118 ) -> Result<FeeHistory, ProviderError>;
119
120 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError>;
122
123 async fn get_transaction_receipt(
128 &self,
129 tx_hash: &str,
130 ) -> Result<Option<TransactionReceipt>, ProviderError>;
131
132 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError>;
137
138 async fn raw_request_dyn(
144 &self,
145 method: &str,
146 params: serde_json::Value,
147 ) -> Result<serde_json::Value, ProviderError>;
148}
149
150impl EvmProvider {
151 pub fn new(configs: Vec<RpcConfig>, timeout_seconds: u64) -> Result<Self, ProviderError> {
160 if configs.is_empty() {
161 return Err(ProviderError::NetworkConfiguration(
162 "At least one RPC configuration must be provided".to_string(),
163 ));
164 }
165
166 RpcConfig::validate_list(&configs)
167 .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {}", e)))?;
168
169 let selector = RpcSelector::new(configs).map_err(|e| {
171 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {}", e))
172 })?;
173
174 let retry_config = RetryConfig::from_env();
175
176 Ok(Self {
177 selector,
178 timeout_seconds,
179 retry_config,
180 })
181 }
182
183 fn should_mark_provider_failed(error: &ProviderError) -> bool {
185 match error {
186 ProviderError::RequestError { status_code, .. } => {
187 match *status_code {
188 500..=599 => true,
190
191 401 => true, 403 => true, 404 => true, 410 => true, _ => false,
198 }
199 }
200 _ => false,
201 }
202 }
203
204 fn is_retriable_error(error: &ProviderError) -> bool {
206 match error {
207 ProviderError::Timeout | ProviderError::RateLimited | ProviderError::BadGateway => true,
209
210 ProviderError::RpcErrorCode { code, .. } => {
212 match code {
213 -32002 => true,
215 -32005 => true,
217 -32603 => true,
219 -32000 => false,
221 -32001 => false,
223 -32003 => false,
225 -32004 => false,
227
228 -32700..=-32600 => false,
234
235 _ => false,
237 }
238 }
239
240 _ => {
242 let err_msg = format!("{}", error);
243 let msg_lower = err_msg.to_lowercase();
244 msg_lower.contains("timeout")
245 || msg_lower.contains("connection")
246 || msg_lower.contains("reset")
247 }
248 }
249 }
250
251 fn initialize_provider(&self, url: &str) -> Result<EvmProviderType, ProviderError> {
253 let rpc_url = url.parse().map_err(|e| {
254 ProviderError::NetworkConfiguration(format!("Invalid URL format: {}", e))
255 })?;
256
257 let client = ReqwestClientBuilder::default()
258 .timeout(Duration::from_secs(self.timeout_seconds))
259 .build()
260 .map_err(|e| ProviderError::Other(format!("Failed to build HTTP client: {}", e)))?;
261
262 let mut transport = Http::new(rpc_url);
263 transport.set_client(client);
264
265 let is_local = transport.guess_local();
266 let client = ClientBuilder::default().transport(transport, is_local);
267
268 let provider = ProviderBuilder::new()
269 .network::<AnyNetwork>()
270 .connect_client(client);
271
272 Ok(provider)
273 }
274
275 async fn retry_rpc_call<T, F, Fut>(
279 &self,
280 operation_name: &str,
281 operation: F,
282 ) -> Result<T, ProviderError>
283 where
284 F: Fn(EvmProviderType) -> Fut,
285 Fut: std::future::Future<Output = Result<T, ProviderError>>,
286 {
287 tracing::debug!(
290 "Starting RPC operation '{}' with timeout: {}s",
291 operation_name,
292 self.timeout_seconds
293 );
294
295 retry_rpc_call(
296 &self.selector,
297 operation_name,
298 Self::is_retriable_error,
299 Self::should_mark_provider_failed,
300 |url| match self.initialize_provider(url) {
301 Ok(provider) => Ok(provider),
302 Err(e) => Err(e),
303 },
304 operation,
305 Some(self.retry_config.clone()),
306 )
307 .await
308 }
309}
310
311impl AsRef<EvmProvider> for EvmProvider {
312 fn as_ref(&self) -> &EvmProvider {
313 self
314 }
315}
316
317#[async_trait]
318impl EvmProviderTrait for EvmProvider {
319 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError> {
320 let parsed_address = address
321 .parse::<alloy::primitives::Address>()
322 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
323
324 self.retry_rpc_call("get_balance", move |provider| async move {
325 provider
326 .get_balance(parsed_address)
327 .await
328 .map_err(ProviderError::from)
329 })
330 .await
331 }
332
333 async fn get_block_number(&self) -> Result<u64, ProviderError> {
334 self.retry_rpc_call("get_block_number", |provider| async move {
335 provider
336 .get_block_number()
337 .await
338 .map_err(ProviderError::from)
339 })
340 .await
341 }
342
343 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError> {
344 let transaction_request = TransactionRequest::try_from(tx)
345 .map_err(|e| ProviderError::Other(format!("Failed to convert transaction: {}", e)))?;
346
347 self.retry_rpc_call("estimate_gas", move |provider| {
348 let tx_req = transaction_request.clone();
349 async move {
350 provider
351 .estimate_gas(tx_req.into())
352 .await
353 .map_err(ProviderError::from)
354 }
355 })
356 .await
357 }
358
359 async fn get_gas_price(&self) -> Result<u128, ProviderError> {
360 self.retry_rpc_call("get_gas_price", |provider| async move {
361 provider.get_gas_price().await.map_err(ProviderError::from)
362 })
363 .await
364 }
365
366 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError> {
367 let pending_tx = self
368 .retry_rpc_call("send_transaction", move |provider| {
369 let tx_req = tx.clone();
370 async move {
371 provider
372 .send_transaction(tx_req.into())
373 .await
374 .map_err(ProviderError::from)
375 }
376 })
377 .await?;
378
379 let tx_hash = pending_tx.tx_hash().to_string();
380 Ok(tx_hash)
381 }
382
383 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError> {
384 let pending_tx = self
385 .retry_rpc_call("send_raw_transaction", move |provider| {
386 let tx_data = tx.to_vec();
387 async move {
388 provider
389 .send_raw_transaction(&tx_data)
390 .await
391 .map_err(ProviderError::from)
392 }
393 })
394 .await?;
395
396 let tx_hash = pending_tx.tx_hash().to_string();
397 Ok(tx_hash)
398 }
399
400 async fn health_check(&self) -> Result<bool, ProviderError> {
401 match self.get_block_number().await {
402 Ok(_) => Ok(true),
403 Err(e) => Err(e),
404 }
405 }
406
407 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError> {
408 let parsed_address = address
409 .parse::<alloy::primitives::Address>()
410 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
411
412 self.retry_rpc_call("get_transaction_count", move |provider| async move {
413 provider
414 .get_transaction_count(parsed_address)
415 .await
416 .map_err(ProviderError::from)
417 })
418 .await
419 }
420
421 async fn get_fee_history(
422 &self,
423 block_count: u64,
424 newest_block: BlockNumberOrTag,
425 reward_percentiles: Vec<f64>,
426 ) -> Result<FeeHistory, ProviderError> {
427 self.retry_rpc_call("get_fee_history", move |provider| {
428 let reward_percentiles_clone = reward_percentiles.clone();
429 async move {
430 provider
431 .get_fee_history(block_count, newest_block, &reward_percentiles_clone)
432 .await
433 .map_err(ProviderError::from)
434 }
435 })
436 .await
437 }
438
439 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError> {
440 let block_result = self
441 .retry_rpc_call("get_block_by_number", |provider| async move {
442 provider
443 .get_block_by_number(BlockNumberOrTag::Latest)
444 .await
445 .map_err(ProviderError::from)
446 })
447 .await?;
448
449 match block_result {
450 Some(block) => Ok(block),
451 None => Err(ProviderError::Other("Block not found".to_string())),
452 }
453 }
454
455 async fn get_transaction_receipt(
456 &self,
457 tx_hash: &str,
458 ) -> Result<Option<TransactionReceipt>, ProviderError> {
459 let parsed_tx_hash = tx_hash
460 .parse::<alloy::primitives::TxHash>()
461 .map_err(|e| ProviderError::Other(format!("Invalid transaction hash: {}", e)))?;
462
463 self.retry_rpc_call("get_transaction_receipt", move |provider| async move {
464 provider
465 .get_transaction_receipt(parsed_tx_hash)
466 .await
467 .map_err(ProviderError::from)
468 })
469 .await
470 }
471
472 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError> {
473 self.retry_rpc_call("call_contract", move |provider| {
474 let tx_req = tx.clone();
475 async move {
476 provider
477 .call(tx_req.into())
478 .await
479 .map_err(ProviderError::from)
480 }
481 })
482 .await
483 }
484
485 async fn raw_request_dyn(
486 &self,
487 method: &str,
488 params: serde_json::Value,
489 ) -> Result<serde_json::Value, ProviderError> {
490 self.retry_rpc_call("raw_request_dyn", move |provider| {
491 let params_clone = params.clone();
492 async move {
493 let params_raw = serde_json::value::to_raw_value(¶ms_clone).map_err(|e| {
495 ProviderError::Other(format!("Failed to serialize params: {}", e))
496 })?;
497
498 let result = provider
499 .raw_request_dyn(std::borrow::Cow::Owned(method.to_string()), ¶ms_raw)
500 .await
501 .map_err(ProviderError::from)?;
502
503 serde_json::from_str(result.get()).map_err(|e| {
505 ProviderError::Other(format!("Failed to deserialize result: {}", e))
506 })
507 }
508 })
509 .await
510 }
511}
512
513impl TryFrom<&EvmTransactionData> for TransactionRequest {
514 type Error = TransactionError;
515 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
516 Ok(TransactionRequest {
517 from: Some(tx.from.clone().parse().map_err(|_| {
518 TransactionError::InvalidType("Invalid address format".to_string())
519 })?),
520 to: Some(TxKind::Call(
521 tx.to
522 .clone()
523 .unwrap_or("".to_string())
524 .parse()
525 .map_err(|_| {
526 TransactionError::InvalidType("Invalid address format".to_string())
527 })?,
528 )),
529 gas_price: tx
530 .gas_price
531 .map(|gp| {
532 Uint::<256, 4>::from(gp)
533 .try_into()
534 .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
535 })
536 .transpose()?,
537 value: Some(Uint::<256, 4>::from(tx.value)),
538 input: TransactionInput::from(tx.data_to_bytes()?),
539 nonce: tx
540 .nonce
541 .map(|n| {
542 Uint::<256, 4>::from(n)
543 .try_into()
544 .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
545 })
546 .transpose()?,
547 chain_id: Some(tx.chain_id),
548 max_fee_per_gas: tx
549 .max_fee_per_gas
550 .map(|mfpg| {
551 Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
552 TransactionError::InvalidType("Invalid max fee per gas".to_string())
553 })
554 })
555 .transpose()?,
556 max_priority_fee_per_gas: tx
557 .max_priority_fee_per_gas
558 .map(|mpfpg| {
559 Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
560 TransactionError::InvalidType(
561 "Invalid max priority fee per gas".to_string(),
562 )
563 })
564 })
565 .transpose()?,
566 ..Default::default()
567 })
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use alloy::primitives::Address;
575 use futures::FutureExt;
576 use lazy_static::lazy_static;
577 use std::str::FromStr;
578 use std::sync::Mutex;
579
580 lazy_static! {
581 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
582 }
583
584 struct EvmTestEnvGuard {
585 _mutex_guard: std::sync::MutexGuard<'static, ()>,
586 }
587
588 impl EvmTestEnvGuard {
589 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
590 std::env::set_var(
591 "API_KEY",
592 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
593 );
594 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
595
596 Self {
597 _mutex_guard: mutex_guard,
598 }
599 }
600 }
601
602 impl Drop for EvmTestEnvGuard {
603 fn drop(&mut self) {
604 std::env::remove_var("API_KEY");
605 std::env::remove_var("REDIS_URL");
606 }
607 }
608
609 fn setup_test_env() -> EvmTestEnvGuard {
611 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
612 EvmTestEnvGuard::new(guard)
613 }
614
615 #[tokio::test]
616 async fn test_reqwest_error_conversion() {
617 let client = reqwest::Client::new();
619 let result = client
620 .get("https://www.openzeppelin.com/")
621 .timeout(Duration::from_millis(1))
622 .send()
623 .await;
624
625 assert!(
626 result.is_err(),
627 "Expected the send operation to result in an error."
628 );
629 let err = result.unwrap_err();
630
631 assert!(
632 err.is_timeout(),
633 "The reqwest error should be a timeout. Actual error: {:?}",
634 err
635 );
636
637 let provider_error = ProviderError::from(err);
638 assert!(
639 matches!(provider_error, ProviderError::Timeout),
640 "ProviderError should be Timeout. Actual: {:?}",
641 provider_error
642 );
643 }
644
645 #[test]
646 fn test_address_parse_error_conversion() {
647 let err = "invalid-address".parse::<Address>().unwrap_err();
649 let provider_error = ProviderError::InvalidAddress(err.to_string());
651 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
652 }
653
654 #[test]
655 fn test_new_provider() {
656 let _env_guard = setup_test_env();
657
658 let provider = EvmProvider::new(
659 vec![RpcConfig::new("http://localhost:8545".to_string())],
660 30,
661 );
662 assert!(provider.is_ok());
663
664 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
666 assert!(provider.is_err());
667 }
668
669 #[test]
670 fn test_new_provider_with_timeout() {
671 let _env_guard = setup_test_env();
672
673 let provider = EvmProvider::new(
675 vec![RpcConfig::new("http://localhost:8545".to_string())],
676 30,
677 );
678 assert!(provider.is_ok());
679
680 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
682 assert!(provider.is_err());
683
684 let provider =
686 EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
687 assert!(provider.is_ok());
688
689 let provider = EvmProvider::new(
691 vec![RpcConfig::new("http://localhost:8545".to_string())],
692 3600,
693 );
694 assert!(provider.is_ok());
695 }
696
697 #[test]
698 fn test_transaction_request_conversion() {
699 let tx_data = EvmTransactionData {
700 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
701 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
702 gas_price: Some(1000000000),
703 value: Uint::<256, 4>::from(1000000000),
704 data: Some("0x".to_string()),
705 nonce: Some(1),
706 chain_id: 1,
707 gas_limit: Some(21000),
708 hash: None,
709 signature: None,
710 speed: None,
711 max_fee_per_gas: None,
712 max_priority_fee_per_gas: None,
713 raw: None,
714 };
715
716 let result = TransactionRequest::try_from(&tx_data);
717 assert!(result.is_ok());
718
719 let tx_request = result.unwrap();
720 assert_eq!(
721 tx_request.from,
722 Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
723 );
724 assert_eq!(tx_request.chain_id, Some(1));
725 }
726
727 #[test]
728 fn test_should_mark_provider_failed_server_errors() {
729 for status_code in 500..=599 {
731 let error = ProviderError::RequestError {
732 error: format!("Server error {}", status_code),
733 status_code,
734 };
735 assert!(
736 EvmProvider::should_mark_provider_failed(&error),
737 "Status code {} should mark provider as failed",
738 status_code
739 );
740 }
741 }
742
743 #[test]
744 fn test_should_mark_provider_failed_auth_errors() {
745 let auth_errors = [401, 403];
747 for &status_code in &auth_errors {
748 let error = ProviderError::RequestError {
749 error: format!("Auth error {}", status_code),
750 status_code,
751 };
752 assert!(
753 EvmProvider::should_mark_provider_failed(&error),
754 "Status code {} should mark provider as failed",
755 status_code
756 );
757 }
758 }
759
760 #[test]
761 fn test_should_mark_provider_failed_not_found_errors() {
762 let not_found_errors = [404, 410];
764 for &status_code in ¬_found_errors {
765 let error = ProviderError::RequestError {
766 error: format!("Not found error {}", status_code),
767 status_code,
768 };
769 assert!(
770 EvmProvider::should_mark_provider_failed(&error),
771 "Status code {} should mark provider as failed",
772 status_code
773 );
774 }
775 }
776
777 #[test]
778 fn test_should_mark_provider_failed_client_errors_not_failed() {
779 let client_errors = [400, 405, 413, 414, 415, 422, 429];
781 for &status_code in &client_errors {
782 let error = ProviderError::RequestError {
783 error: format!("Client error {}", status_code),
784 status_code,
785 };
786 assert!(
787 !EvmProvider::should_mark_provider_failed(&error),
788 "Status code {} should NOT mark provider as failed",
789 status_code
790 );
791 }
792 }
793
794 #[test]
795 fn test_should_mark_provider_failed_other_error_types() {
796 let errors = [
798 ProviderError::Timeout,
799 ProviderError::RateLimited,
800 ProviderError::BadGateway,
801 ProviderError::InvalidAddress("test".to_string()),
802 ProviderError::NetworkConfiguration("test".to_string()),
803 ProviderError::Other("test".to_string()),
804 ];
805
806 for error in errors {
807 assert!(
808 !EvmProvider::should_mark_provider_failed(&error),
809 "Error type {:?} should NOT mark provider as failed",
810 error
811 );
812 }
813 }
814
815 #[test]
816 fn test_should_mark_provider_failed_edge_cases() {
817 let edge_cases = [
819 (200, false), (300, false), (418, false), (451, false), (499, false), ];
825
826 for (status_code, should_fail) in edge_cases {
827 let error = ProviderError::RequestError {
828 error: format!("Edge case error {}", status_code),
829 status_code,
830 };
831 assert_eq!(
832 EvmProvider::should_mark_provider_failed(&error),
833 should_fail,
834 "Status code {} should {} mark provider as failed",
835 status_code,
836 if should_fail { "" } else { "NOT" }
837 );
838 }
839 }
840
841 #[test]
842 fn test_is_retriable_error_retriable_types() {
843 let retriable_errors = [
845 ProviderError::Timeout,
846 ProviderError::RateLimited,
847 ProviderError::BadGateway,
848 ];
849
850 for error in retriable_errors {
851 assert!(
852 EvmProvider::is_retriable_error(&error),
853 "Error type {:?} should be retriable",
854 error
855 );
856 }
857 }
858
859 #[test]
860 fn test_is_retriable_error_non_retriable_types() {
861 let non_retriable_errors = [
863 ProviderError::InvalidAddress("test".to_string()),
864 ProviderError::NetworkConfiguration("test".to_string()),
865 ProviderError::RequestError {
866 error: "Some error".to_string(),
867 status_code: 400,
868 },
869 ];
870
871 for error in non_retriable_errors {
872 assert!(
873 !EvmProvider::is_retriable_error(&error),
874 "Error type {:?} should NOT be retriable",
875 error
876 );
877 }
878 }
879
880 #[test]
881 fn test_is_retriable_error_message_based_detection() {
882 let retriable_messages = [
884 "Connection timeout occurred",
885 "Network connection reset",
886 "Connection refused",
887 "TIMEOUT error happened",
888 "Connection was reset by peer",
889 ];
890
891 for message in retriable_messages {
892 let error = ProviderError::Other(message.to_string());
893 assert!(
894 EvmProvider::is_retriable_error(&error),
895 "Error with message '{}' should be retriable",
896 message
897 );
898 }
899 }
900
901 #[test]
902 fn test_is_retriable_error_message_based_non_retriable() {
903 let non_retriable_messages = [
905 "Invalid address format",
906 "Bad request parameters",
907 "Authentication failed",
908 "Method not found",
909 "Some other error",
910 ];
911
912 for message in non_retriable_messages {
913 let error = ProviderError::Other(message.to_string());
914 assert!(
915 !EvmProvider::is_retriable_error(&error),
916 "Error with message '{}' should NOT be retriable",
917 message
918 );
919 }
920 }
921
922 #[test]
923 fn test_is_retriable_error_case_insensitive() {
924 let case_variations = [
926 "TIMEOUT",
927 "Timeout",
928 "timeout",
929 "CONNECTION",
930 "Connection",
931 "connection",
932 "RESET",
933 "Reset",
934 "reset",
935 ];
936
937 for message in case_variations {
938 let error = ProviderError::Other(message.to_string());
939 assert!(
940 EvmProvider::is_retriable_error(&error),
941 "Error with message '{}' should be retriable (case insensitive)",
942 message
943 );
944 }
945 }
946
947 #[tokio::test]
948 async fn test_mock_provider_methods() {
949 let mut mock = MockEvmProviderTrait::new();
950
951 mock.expect_get_balance()
952 .with(mockall::predicate::eq(
953 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
954 ))
955 .times(1)
956 .returning(|_| async { Ok(U256::from(100)) }.boxed());
957
958 mock.expect_get_block_number()
959 .times(1)
960 .returning(|| async { Ok(12345) }.boxed());
961
962 mock.expect_get_gas_price()
963 .times(1)
964 .returning(|| async { Ok(20000000000) }.boxed());
965
966 mock.expect_health_check()
967 .times(1)
968 .returning(|| async { Ok(true) }.boxed());
969
970 mock.expect_get_transaction_count()
971 .with(mockall::predicate::eq(
972 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
973 ))
974 .times(1)
975 .returning(|_| async { Ok(42) }.boxed());
976
977 mock.expect_get_fee_history()
978 .with(
979 mockall::predicate::eq(10u64),
980 mockall::predicate::eq(BlockNumberOrTag::Latest),
981 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
982 )
983 .times(1)
984 .returning(|_, _, _| {
985 async {
986 Ok(FeeHistory {
987 oldest_block: 100,
988 base_fee_per_gas: vec![1000],
989 gas_used_ratio: vec![0.5],
990 reward: Some(vec![vec![500]]),
991 base_fee_per_blob_gas: vec![1000],
992 blob_gas_used_ratio: vec![0.5],
993 })
994 }
995 .boxed()
996 });
997
998 let balance = mock
1000 .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1001 .await;
1002 assert!(balance.is_ok());
1003 assert_eq!(balance.unwrap(), U256::from(100));
1004
1005 let block_number = mock.get_block_number().await;
1006 assert!(block_number.is_ok());
1007 assert_eq!(block_number.unwrap(), 12345);
1008
1009 let gas_price = mock.get_gas_price().await;
1010 assert!(gas_price.is_ok());
1011 assert_eq!(gas_price.unwrap(), 20000000000);
1012
1013 let health = mock.health_check().await;
1014 assert!(health.is_ok());
1015 assert!(health.unwrap());
1016
1017 let count = mock
1018 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1019 .await;
1020 assert!(count.is_ok());
1021 assert_eq!(count.unwrap(), 42);
1022
1023 let fee_history = mock
1024 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1025 .await;
1026 assert!(fee_history.is_ok());
1027 let fee_history = fee_history.unwrap();
1028 assert_eq!(fee_history.oldest_block, 100);
1029 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1030 }
1031
1032 #[tokio::test]
1033 async fn test_mock_transaction_operations() {
1034 let mut mock = MockEvmProviderTrait::new();
1035
1036 let tx_data = EvmTransactionData {
1038 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1039 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1040 gas_price: Some(1000000000),
1041 value: Uint::<256, 4>::from(1000000000),
1042 data: Some("0x".to_string()),
1043 nonce: Some(1),
1044 chain_id: 1,
1045 gas_limit: Some(21000),
1046 hash: None,
1047 signature: None,
1048 speed: None,
1049 max_fee_per_gas: None,
1050 max_priority_fee_per_gas: None,
1051 raw: None,
1052 };
1053
1054 mock.expect_estimate_gas()
1055 .with(mockall::predicate::always())
1056 .times(1)
1057 .returning(|_| async { Ok(21000) }.boxed());
1058
1059 mock.expect_send_raw_transaction()
1061 .with(mockall::predicate::always())
1062 .times(1)
1063 .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
1064
1065 let gas_estimate = mock.estimate_gas(&tx_data).await;
1067 assert!(gas_estimate.is_ok());
1068 assert_eq!(gas_estimate.unwrap(), 21000);
1069
1070 let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
1071 assert!(tx_hash.is_ok());
1072 assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
1073 }
1074
1075 #[test]
1076 fn test_invalid_transaction_request_conversion() {
1077 let tx_data = EvmTransactionData {
1078 from: "invalid-address".to_string(),
1079 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1080 gas_price: Some(1000000000),
1081 value: Uint::<256, 4>::from(1000000000),
1082 data: Some("0x".to_string()),
1083 nonce: Some(1),
1084 chain_id: 1,
1085 gas_limit: Some(21000),
1086 hash: None,
1087 signature: None,
1088 speed: None,
1089 max_fee_per_gas: None,
1090 max_priority_fee_per_gas: None,
1091 raw: None,
1092 };
1093
1094 let result = TransactionRequest::try_from(&tx_data);
1095 assert!(result.is_err());
1096 }
1097
1098 #[tokio::test]
1099 async fn test_mock_additional_methods() {
1100 let mut mock = MockEvmProviderTrait::new();
1101
1102 mock.expect_health_check()
1104 .times(1)
1105 .returning(|| async { Ok(true) }.boxed());
1106
1107 mock.expect_get_transaction_count()
1109 .with(mockall::predicate::eq(
1110 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
1111 ))
1112 .times(1)
1113 .returning(|_| async { Ok(42) }.boxed());
1114
1115 mock.expect_get_fee_history()
1117 .with(
1118 mockall::predicate::eq(10u64),
1119 mockall::predicate::eq(BlockNumberOrTag::Latest),
1120 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
1121 )
1122 .times(1)
1123 .returning(|_, _, _| {
1124 async {
1125 Ok(FeeHistory {
1126 oldest_block: 100,
1127 base_fee_per_gas: vec![1000],
1128 gas_used_ratio: vec![0.5],
1129 reward: Some(vec![vec![500]]),
1130 base_fee_per_blob_gas: vec![1000],
1131 blob_gas_used_ratio: vec![0.5],
1132 })
1133 }
1134 .boxed()
1135 });
1136
1137 let health = mock.health_check().await;
1139 assert!(health.is_ok());
1140 assert!(health.unwrap());
1141
1142 let count = mock
1144 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1145 .await;
1146 assert!(count.is_ok());
1147 assert_eq!(count.unwrap(), 42);
1148
1149 let fee_history = mock
1151 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1152 .await;
1153 assert!(fee_history.is_ok());
1154 let fee_history = fee_history.unwrap();
1155 assert_eq!(fee_history.oldest_block, 100);
1156 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1157 }
1158
1159 #[test]
1160 fn test_is_retriable_error_json_rpc_retriable_codes() {
1161 let retriable_codes = vec![
1163 (-32002, "Resource unavailable"),
1164 (-32005, "Limit exceeded"),
1165 (-32603, "Internal error"),
1166 ];
1167
1168 for (code, message) in retriable_codes {
1169 let error = ProviderError::RpcErrorCode {
1170 code,
1171 message: message.to_string(),
1172 };
1173 assert!(
1174 EvmProvider::is_retriable_error(&error),
1175 "Error code {} should be retriable",
1176 code
1177 );
1178 }
1179 }
1180
1181 #[test]
1182 fn test_is_retriable_error_json_rpc_non_retriable_codes() {
1183 let non_retriable_codes = vec![
1185 (-32000, "insufficient funds"),
1186 (-32000, "execution reverted"),
1187 (-32000, "already known"),
1188 (-32000, "nonce too low"),
1189 (-32000, "invalid sender"),
1190 (-32001, "Resource not found"),
1191 (-32003, "Transaction rejected"),
1192 (-32004, "Method not supported"),
1193 (-32700, "Parse error"),
1194 (-32600, "Invalid request"),
1195 (-32601, "Method not found"),
1196 (-32602, "Invalid params"),
1197 ];
1198
1199 for (code, message) in non_retriable_codes {
1200 let error = ProviderError::RpcErrorCode {
1201 code,
1202 message: message.to_string(),
1203 };
1204 assert!(
1205 !EvmProvider::is_retriable_error(&error),
1206 "Error code {} with message '{}' should NOT be retriable",
1207 code,
1208 message
1209 );
1210 }
1211 }
1212
1213 #[test]
1214 fn test_is_retriable_error_json_rpc_32000_specific_cases() {
1215 let test_cases = vec![
1218 (
1219 "tx already exists in cache",
1220 false,
1221 "Transaction already in mempool",
1222 ),
1223 ("already known", false, "Duplicate transaction submission"),
1224 (
1225 "insufficient funds for gas * price + value",
1226 false,
1227 "User needs more funds",
1228 ),
1229 ("execution reverted", false, "Smart contract rejected"),
1230 ("nonce too low", false, "Transaction already processed"),
1231 ("invalid sender", false, "Configuration issue"),
1232 ("gas required exceeds allowance", false, "Gas limit too low"),
1233 (
1234 "replacement transaction underpriced",
1235 false,
1236 "Need higher gas price",
1237 ),
1238 ];
1239
1240 for (message, should_retry, description) in test_cases {
1241 let error = ProviderError::RpcErrorCode {
1242 code: -32000,
1243 message: message.to_string(),
1244 };
1245 assert_eq!(
1246 EvmProvider::is_retriable_error(&error),
1247 should_retry,
1248 "{}: -32000 with '{}' should{} be retriable",
1249 description,
1250 message,
1251 if should_retry { "" } else { " NOT" }
1252 );
1253 }
1254 }
1255
1256 #[tokio::test]
1257 async fn test_call_contract() {
1258 let mut mock = MockEvmProviderTrait::new();
1259
1260 let tx = TransactionRequest {
1261 from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1262 to: Some(TxKind::Call(
1263 Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1264 )),
1265 input: TransactionInput::from(
1266 hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1267 ),
1268 ..Default::default()
1269 };
1270
1271 mock.expect_call_contract()
1273 .with(mockall::predicate::always())
1274 .times(1)
1275 .returning(|_| {
1276 async {
1277 Ok(Bytes::from(
1278 hex::decode(
1279 "0000000000000000000000000000000000000000000000000000000000000001",
1280 )
1281 .unwrap(),
1282 ))
1283 }
1284 .boxed()
1285 });
1286
1287 let result = mock.call_contract(&tx).await;
1288 assert!(result.is_ok());
1289
1290 let data = result.unwrap();
1291 assert_eq!(
1292 hex::encode(data),
1293 "0000000000000000000000000000000000000000000000000000000000000001"
1294 );
1295 }
1296}