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