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::{
39 models::{
40 BlockResponse, EvmTransactionData, RpcConfig, TransactionError, TransactionReceipt, U256,
41 },
42 services::provider::{is_retriable_error, should_mark_provider_failed},
43};
44
45#[cfg(test)]
46use mockall::automock;
47
48use super::ProviderError;
49
50#[derive(Clone)]
54pub struct EvmProvider {
55 selector: RpcSelector,
57 timeout_seconds: u64,
59 retry_config: RetryConfig,
61}
62
63#[async_trait]
68#[cfg_attr(test, automock)]
69#[allow(dead_code)]
70pub trait EvmProviderTrait: Send + Sync {
71 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError>;
76
77 async fn get_block_number(&self) -> Result<u64, ProviderError>;
79
80 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError>;
85
86 async fn get_gas_price(&self) -> Result<u128, ProviderError>;
88
89 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError>;
94
95 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError>;
100
101 async fn health_check(&self) -> Result<bool, ProviderError>;
103
104 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError>;
109
110 async fn get_fee_history(
117 &self,
118 block_count: u64,
119 newest_block: BlockNumberOrTag,
120 reward_percentiles: Vec<f64>,
121 ) -> Result<FeeHistory, ProviderError>;
122
123 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError>;
125
126 async fn get_transaction_receipt(
131 &self,
132 tx_hash: &str,
133 ) -> Result<Option<TransactionReceipt>, ProviderError>;
134
135 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError>;
140
141 async fn raw_request_dyn(
147 &self,
148 method: &str,
149 params: serde_json::Value,
150 ) -> Result<serde_json::Value, ProviderError>;
151}
152
153impl EvmProvider {
154 pub fn new(configs: Vec<RpcConfig>, timeout_seconds: u64) -> Result<Self, ProviderError> {
163 if configs.is_empty() {
164 return Err(ProviderError::NetworkConfiguration(
165 "At least one RPC configuration must be provided".to_string(),
166 ));
167 }
168
169 RpcConfig::validate_list(&configs)
170 .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {e}")))?;
171
172 let selector = RpcSelector::new(configs).map_err(|e| {
174 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
175 })?;
176
177 let retry_config = RetryConfig::from_env();
178
179 Ok(Self {
180 selector,
181 timeout_seconds,
182 retry_config,
183 })
184 }
185
186 fn initialize_provider(&self, url: &str) -> Result<EvmProviderType, ProviderError> {
188 let rpc_url = url
189 .parse()
190 .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL format: {e}")))?;
191
192 let client = ReqwestClientBuilder::new()
194 .timeout(Duration::from_secs(self.timeout_seconds))
195 .use_rustls_tls()
196 .build()
197 .map_err(|e| ProviderError::Other(format!("Failed to build HTTP client: {e}")))?;
198
199 let mut transport = Http::new(rpc_url);
200 transport.set_client(client);
201
202 let is_local = transport.guess_local();
203 let client = ClientBuilder::default().transport(transport, is_local);
204
205 let provider = ProviderBuilder::new()
206 .network::<AnyNetwork>()
207 .connect_client(client);
208
209 Ok(provider)
210 }
211
212 async fn retry_rpc_call<T, F, Fut>(
216 &self,
217 operation_name: &str,
218 operation: F,
219 ) -> Result<T, ProviderError>
220 where
221 F: Fn(EvmProviderType) -> Fut,
222 Fut: std::future::Future<Output = Result<T, ProviderError>>,
223 {
224 tracing::debug!(
227 "Starting RPC operation '{}' with timeout: {}s",
228 operation_name,
229 self.timeout_seconds
230 );
231
232 retry_rpc_call(
233 &self.selector,
234 operation_name,
235 is_retriable_error,
236 should_mark_provider_failed,
237 |url| match self.initialize_provider(url) {
238 Ok(provider) => Ok(provider),
239 Err(e) => Err(e),
240 },
241 operation,
242 Some(self.retry_config.clone()),
243 )
244 .await
245 }
246}
247
248impl AsRef<EvmProvider> for EvmProvider {
249 fn as_ref(&self) -> &EvmProvider {
250 self
251 }
252}
253
254#[async_trait]
255impl EvmProviderTrait for EvmProvider {
256 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError> {
257 let parsed_address = address
258 .parse::<alloy::primitives::Address>()
259 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
260
261 self.retry_rpc_call("get_balance", move |provider| async move {
262 provider
263 .get_balance(parsed_address)
264 .await
265 .map_err(ProviderError::from)
266 })
267 .await
268 }
269
270 async fn get_block_number(&self) -> Result<u64, ProviderError> {
271 self.retry_rpc_call("get_block_number", |provider| async move {
272 provider
273 .get_block_number()
274 .await
275 .map_err(ProviderError::from)
276 })
277 .await
278 }
279
280 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError> {
281 let transaction_request = TransactionRequest::try_from(tx)
282 .map_err(|e| ProviderError::Other(format!("Failed to convert transaction: {e}")))?;
283
284 self.retry_rpc_call("estimate_gas", move |provider| {
285 let tx_req = transaction_request.clone();
286 async move {
287 provider
288 .estimate_gas(tx_req.into())
289 .await
290 .map_err(ProviderError::from)
291 }
292 })
293 .await
294 }
295
296 async fn get_gas_price(&self) -> Result<u128, ProviderError> {
297 self.retry_rpc_call("get_gas_price", |provider| async move {
298 provider.get_gas_price().await.map_err(ProviderError::from)
299 })
300 .await
301 }
302
303 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError> {
304 let pending_tx = self
305 .retry_rpc_call("send_transaction", move |provider| {
306 let tx_req = tx.clone();
307 async move {
308 provider
309 .send_transaction(tx_req.into())
310 .await
311 .map_err(ProviderError::from)
312 }
313 })
314 .await?;
315
316 let tx_hash = pending_tx.tx_hash().to_string();
317 Ok(tx_hash)
318 }
319
320 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError> {
321 let pending_tx = self
322 .retry_rpc_call("send_raw_transaction", move |provider| {
323 let tx_data = tx.to_vec();
324 async move {
325 provider
326 .send_raw_transaction(&tx_data)
327 .await
328 .map_err(ProviderError::from)
329 }
330 })
331 .await?;
332
333 let tx_hash = pending_tx.tx_hash().to_string();
334 Ok(tx_hash)
335 }
336
337 async fn health_check(&self) -> Result<bool, ProviderError> {
338 match self.get_block_number().await {
339 Ok(_) => Ok(true),
340 Err(e) => Err(e),
341 }
342 }
343
344 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError> {
345 let parsed_address = address
346 .parse::<alloy::primitives::Address>()
347 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
348
349 self.retry_rpc_call("get_transaction_count", move |provider| async move {
350 provider
351 .get_transaction_count(parsed_address)
352 .await
353 .map_err(ProviderError::from)
354 })
355 .await
356 }
357
358 async fn get_fee_history(
359 &self,
360 block_count: u64,
361 newest_block: BlockNumberOrTag,
362 reward_percentiles: Vec<f64>,
363 ) -> Result<FeeHistory, ProviderError> {
364 self.retry_rpc_call("get_fee_history", move |provider| {
365 let reward_percentiles_clone = reward_percentiles.clone();
366 async move {
367 provider
368 .get_fee_history(block_count, newest_block, &reward_percentiles_clone)
369 .await
370 .map_err(ProviderError::from)
371 }
372 })
373 .await
374 }
375
376 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError> {
377 let block_result = self
378 .retry_rpc_call("get_block_by_number", |provider| async move {
379 provider
380 .get_block_by_number(BlockNumberOrTag::Latest)
381 .await
382 .map_err(ProviderError::from)
383 })
384 .await?;
385
386 match block_result {
387 Some(block) => Ok(block),
388 None => Err(ProviderError::Other("Block not found".to_string())),
389 }
390 }
391
392 async fn get_transaction_receipt(
393 &self,
394 tx_hash: &str,
395 ) -> Result<Option<TransactionReceipt>, ProviderError> {
396 let parsed_tx_hash = tx_hash
397 .parse::<alloy::primitives::TxHash>()
398 .map_err(|e| ProviderError::Other(format!("Invalid transaction hash: {e}")))?;
399
400 self.retry_rpc_call("get_transaction_receipt", move |provider| async move {
401 provider
402 .get_transaction_receipt(parsed_tx_hash)
403 .await
404 .map_err(ProviderError::from)
405 })
406 .await
407 }
408
409 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError> {
410 self.retry_rpc_call("call_contract", move |provider| {
411 let tx_req = tx.clone();
412 async move {
413 provider
414 .call(tx_req.into())
415 .await
416 .map_err(ProviderError::from)
417 }
418 })
419 .await
420 }
421
422 async fn raw_request_dyn(
423 &self,
424 method: &str,
425 params: serde_json::Value,
426 ) -> Result<serde_json::Value, ProviderError> {
427 self.retry_rpc_call("raw_request_dyn", move |provider| {
428 let params_clone = params.clone();
429 async move {
430 let params_raw = serde_json::value::to_raw_value(¶ms_clone).map_err(|e| {
432 ProviderError::Other(format!("Failed to serialize params: {e}"))
433 })?;
434
435 let result = provider
436 .raw_request_dyn(std::borrow::Cow::Owned(method.to_string()), ¶ms_raw)
437 .await
438 .map_err(ProviderError::from)?;
439
440 serde_json::from_str(result.get())
442 .map_err(|e| ProviderError::Other(format!("Failed to deserialize result: {e}")))
443 }
444 })
445 .await
446 }
447}
448
449impl TryFrom<&EvmTransactionData> for TransactionRequest {
450 type Error = TransactionError;
451 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
452 Ok(TransactionRequest {
453 from: Some(tx.from.clone().parse().map_err(|_| {
454 TransactionError::InvalidType("Invalid address format".to_string())
455 })?),
456 to: Some(TxKind::Call(
457 tx.to
458 .clone()
459 .unwrap_or("".to_string())
460 .parse()
461 .map_err(|_| {
462 TransactionError::InvalidType("Invalid address format".to_string())
463 })?,
464 )),
465 gas_price: tx
466 .gas_price
467 .map(|gp| {
468 Uint::<256, 4>::from(gp)
469 .try_into()
470 .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
471 })
472 .transpose()?,
473 value: Some(Uint::<256, 4>::from(tx.value)),
474 input: TransactionInput::from(tx.data_to_bytes()?),
475 nonce: tx
476 .nonce
477 .map(|n| {
478 Uint::<256, 4>::from(n)
479 .try_into()
480 .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
481 })
482 .transpose()?,
483 chain_id: Some(tx.chain_id),
484 max_fee_per_gas: tx
485 .max_fee_per_gas
486 .map(|mfpg| {
487 Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
488 TransactionError::InvalidType("Invalid max fee per gas".to_string())
489 })
490 })
491 .transpose()?,
492 max_priority_fee_per_gas: tx
493 .max_priority_fee_per_gas
494 .map(|mpfpg| {
495 Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
496 TransactionError::InvalidType(
497 "Invalid max priority fee per gas".to_string(),
498 )
499 })
500 })
501 .transpose()?,
502 ..Default::default()
503 })
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use alloy::primitives::Address;
511 use futures::FutureExt;
512 use lazy_static::lazy_static;
513 use std::str::FromStr;
514 use std::sync::Mutex;
515
516 lazy_static! {
517 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
518 }
519
520 struct EvmTestEnvGuard {
521 _mutex_guard: std::sync::MutexGuard<'static, ()>,
522 }
523
524 impl EvmTestEnvGuard {
525 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
526 std::env::set_var(
527 "API_KEY",
528 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
529 );
530 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
531
532 Self {
533 _mutex_guard: mutex_guard,
534 }
535 }
536 }
537
538 impl Drop for EvmTestEnvGuard {
539 fn drop(&mut self) {
540 std::env::remove_var("API_KEY");
541 std::env::remove_var("REDIS_URL");
542 }
543 }
544
545 fn setup_test_env() -> EvmTestEnvGuard {
547 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
548 EvmTestEnvGuard::new(guard)
549 }
550
551 #[tokio::test]
552 async fn test_reqwest_error_conversion() {
553 let client = reqwest::Client::new();
555 let result = client
556 .get("https://www.openzeppelin.com/")
557 .timeout(Duration::from_millis(1))
558 .send()
559 .await;
560
561 assert!(
562 result.is_err(),
563 "Expected the send operation to result in an error."
564 );
565 let err = result.unwrap_err();
566
567 assert!(
568 err.is_timeout(),
569 "The reqwest error should be a timeout. Actual error: {:?}",
570 err
571 );
572
573 let provider_error = ProviderError::from(err);
574 assert!(
575 matches!(provider_error, ProviderError::Timeout),
576 "ProviderError should be Timeout. Actual: {:?}",
577 provider_error
578 );
579 }
580
581 #[test]
582 fn test_address_parse_error_conversion() {
583 let err = "invalid-address".parse::<Address>().unwrap_err();
585 let provider_error = ProviderError::InvalidAddress(err.to_string());
587 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
588 }
589
590 #[test]
591 fn test_new_provider() {
592 let _env_guard = setup_test_env();
593
594 let provider = EvmProvider::new(
595 vec![RpcConfig::new("http://localhost:8545".to_string())],
596 30,
597 );
598 assert!(provider.is_ok());
599
600 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
602 assert!(provider.is_err());
603 }
604
605 #[test]
606 fn test_new_provider_with_timeout() {
607 let _env_guard = setup_test_env();
608
609 let provider = EvmProvider::new(
611 vec![RpcConfig::new("http://localhost:8545".to_string())],
612 30,
613 );
614 assert!(provider.is_ok());
615
616 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
618 assert!(provider.is_err());
619
620 let provider =
622 EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
623 assert!(provider.is_ok());
624
625 let provider = EvmProvider::new(
627 vec![RpcConfig::new("http://localhost:8545".to_string())],
628 3600,
629 );
630 assert!(provider.is_ok());
631 }
632
633 #[test]
634 fn test_transaction_request_conversion() {
635 let tx_data = EvmTransactionData {
636 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
637 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
638 gas_price: Some(1000000000),
639 value: Uint::<256, 4>::from(1000000000),
640 data: Some("0x".to_string()),
641 nonce: Some(1),
642 chain_id: 1,
643 gas_limit: Some(21000),
644 hash: None,
645 signature: None,
646 speed: None,
647 max_fee_per_gas: None,
648 max_priority_fee_per_gas: None,
649 raw: None,
650 };
651
652 let result = TransactionRequest::try_from(&tx_data);
653 assert!(result.is_ok());
654
655 let tx_request = result.unwrap();
656 assert_eq!(
657 tx_request.from,
658 Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
659 );
660 assert_eq!(tx_request.chain_id, Some(1));
661 }
662
663 #[tokio::test]
664 async fn test_mock_provider_methods() {
665 let mut mock = MockEvmProviderTrait::new();
666
667 mock.expect_get_balance()
668 .with(mockall::predicate::eq(
669 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
670 ))
671 .times(1)
672 .returning(|_| async { Ok(U256::from(100)) }.boxed());
673
674 mock.expect_get_block_number()
675 .times(1)
676 .returning(|| async { Ok(12345) }.boxed());
677
678 mock.expect_get_gas_price()
679 .times(1)
680 .returning(|| async { Ok(20000000000) }.boxed());
681
682 mock.expect_health_check()
683 .times(1)
684 .returning(|| async { Ok(true) }.boxed());
685
686 mock.expect_get_transaction_count()
687 .with(mockall::predicate::eq(
688 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
689 ))
690 .times(1)
691 .returning(|_| async { Ok(42) }.boxed());
692
693 mock.expect_get_fee_history()
694 .with(
695 mockall::predicate::eq(10u64),
696 mockall::predicate::eq(BlockNumberOrTag::Latest),
697 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
698 )
699 .times(1)
700 .returning(|_, _, _| {
701 async {
702 Ok(FeeHistory {
703 oldest_block: 100,
704 base_fee_per_gas: vec![1000],
705 gas_used_ratio: vec![0.5],
706 reward: Some(vec![vec![500]]),
707 base_fee_per_blob_gas: vec![1000],
708 blob_gas_used_ratio: vec![0.5],
709 })
710 }
711 .boxed()
712 });
713
714 let balance = mock
716 .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
717 .await;
718 assert!(balance.is_ok());
719 assert_eq!(balance.unwrap(), U256::from(100));
720
721 let block_number = mock.get_block_number().await;
722 assert!(block_number.is_ok());
723 assert_eq!(block_number.unwrap(), 12345);
724
725 let gas_price = mock.get_gas_price().await;
726 assert!(gas_price.is_ok());
727 assert_eq!(gas_price.unwrap(), 20000000000);
728
729 let health = mock.health_check().await;
730 assert!(health.is_ok());
731 assert!(health.unwrap());
732
733 let count = mock
734 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
735 .await;
736 assert!(count.is_ok());
737 assert_eq!(count.unwrap(), 42);
738
739 let fee_history = mock
740 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
741 .await;
742 assert!(fee_history.is_ok());
743 let fee_history = fee_history.unwrap();
744 assert_eq!(fee_history.oldest_block, 100);
745 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
746 }
747
748 #[tokio::test]
749 async fn test_mock_transaction_operations() {
750 let mut mock = MockEvmProviderTrait::new();
751
752 let tx_data = EvmTransactionData {
754 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
755 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
756 gas_price: Some(1000000000),
757 value: Uint::<256, 4>::from(1000000000),
758 data: Some("0x".to_string()),
759 nonce: Some(1),
760 chain_id: 1,
761 gas_limit: Some(21000),
762 hash: None,
763 signature: None,
764 speed: None,
765 max_fee_per_gas: None,
766 max_priority_fee_per_gas: None,
767 raw: None,
768 };
769
770 mock.expect_estimate_gas()
771 .with(mockall::predicate::always())
772 .times(1)
773 .returning(|_| async { Ok(21000) }.boxed());
774
775 mock.expect_send_raw_transaction()
777 .with(mockall::predicate::always())
778 .times(1)
779 .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
780
781 let gas_estimate = mock.estimate_gas(&tx_data).await;
783 assert!(gas_estimate.is_ok());
784 assert_eq!(gas_estimate.unwrap(), 21000);
785
786 let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
787 assert!(tx_hash.is_ok());
788 assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
789 }
790
791 #[test]
792 fn test_invalid_transaction_request_conversion() {
793 let tx_data = EvmTransactionData {
794 from: "invalid-address".to_string(),
795 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
796 gas_price: Some(1000000000),
797 value: Uint::<256, 4>::from(1000000000),
798 data: Some("0x".to_string()),
799 nonce: Some(1),
800 chain_id: 1,
801 gas_limit: Some(21000),
802 hash: None,
803 signature: None,
804 speed: None,
805 max_fee_per_gas: None,
806 max_priority_fee_per_gas: None,
807 raw: None,
808 };
809
810 let result = TransactionRequest::try_from(&tx_data);
811 assert!(result.is_err());
812 }
813
814 #[tokio::test]
815 async fn test_mock_additional_methods() {
816 let mut mock = MockEvmProviderTrait::new();
817
818 mock.expect_health_check()
820 .times(1)
821 .returning(|| async { Ok(true) }.boxed());
822
823 mock.expect_get_transaction_count()
825 .with(mockall::predicate::eq(
826 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
827 ))
828 .times(1)
829 .returning(|_| async { Ok(42) }.boxed());
830
831 mock.expect_get_fee_history()
833 .with(
834 mockall::predicate::eq(10u64),
835 mockall::predicate::eq(BlockNumberOrTag::Latest),
836 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
837 )
838 .times(1)
839 .returning(|_, _, _| {
840 async {
841 Ok(FeeHistory {
842 oldest_block: 100,
843 base_fee_per_gas: vec![1000],
844 gas_used_ratio: vec![0.5],
845 reward: Some(vec![vec![500]]),
846 base_fee_per_blob_gas: vec![1000],
847 blob_gas_used_ratio: vec![0.5],
848 })
849 }
850 .boxed()
851 });
852
853 let health = mock.health_check().await;
855 assert!(health.is_ok());
856 assert!(health.unwrap());
857
858 let count = mock
860 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
861 .await;
862 assert!(count.is_ok());
863 assert_eq!(count.unwrap(), 42);
864
865 let fee_history = mock
867 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
868 .await;
869 assert!(fee_history.is_ok());
870 let fee_history = fee_history.unwrap();
871 assert_eq!(fee_history.oldest_block, 100);
872 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
873 }
874
875 #[test]
876 fn test_is_retriable_error_json_rpc_retriable_codes() {
877 let retriable_codes = vec![
879 (-32002, "Resource unavailable"),
880 (-32005, "Limit exceeded"),
881 (-32603, "Internal error"),
882 ];
883
884 for (code, message) in retriable_codes {
885 let error = ProviderError::RpcErrorCode {
886 code,
887 message: message.to_string(),
888 };
889 assert!(
890 is_retriable_error(&error),
891 "Error code {} should be retriable",
892 code
893 );
894 }
895 }
896
897 #[test]
898 fn test_is_retriable_error_json_rpc_non_retriable_codes() {
899 let non_retriable_codes = vec![
901 (-32000, "insufficient funds"),
902 (-32000, "execution reverted"),
903 (-32000, "already known"),
904 (-32000, "nonce too low"),
905 (-32000, "invalid sender"),
906 (-32001, "Resource not found"),
907 (-32003, "Transaction rejected"),
908 (-32004, "Method not supported"),
909 (-32700, "Parse error"),
910 (-32600, "Invalid request"),
911 (-32601, "Method not found"),
912 (-32602, "Invalid params"),
913 ];
914
915 for (code, message) in non_retriable_codes {
916 let error = ProviderError::RpcErrorCode {
917 code,
918 message: message.to_string(),
919 };
920 assert!(
921 !is_retriable_error(&error),
922 "Error code {} with message '{}' should NOT be retriable",
923 code,
924 message
925 );
926 }
927 }
928
929 #[test]
930 fn test_is_retriable_error_json_rpc_32000_specific_cases() {
931 let test_cases = vec![
934 (
935 "tx already exists in cache",
936 false,
937 "Transaction already in mempool",
938 ),
939 ("already known", false, "Duplicate transaction submission"),
940 (
941 "insufficient funds for gas * price + value",
942 false,
943 "User needs more funds",
944 ),
945 ("execution reverted", false, "Smart contract rejected"),
946 ("nonce too low", false, "Transaction already processed"),
947 ("invalid sender", false, "Configuration issue"),
948 ("gas required exceeds allowance", false, "Gas limit too low"),
949 (
950 "replacement transaction underpriced",
951 false,
952 "Need higher gas price",
953 ),
954 ];
955
956 for (message, should_retry, description) in test_cases {
957 let error = ProviderError::RpcErrorCode {
958 code: -32000,
959 message: message.to_string(),
960 };
961 assert_eq!(
962 is_retriable_error(&error),
963 should_retry,
964 "{}: -32000 with '{}' should{} be retriable",
965 description,
966 message,
967 if should_retry { "" } else { " NOT" }
968 );
969 }
970 }
971
972 #[tokio::test]
973 async fn test_call_contract() {
974 let mut mock = MockEvmProviderTrait::new();
975
976 let tx = TransactionRequest {
977 from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
978 to: Some(TxKind::Call(
979 Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
980 )),
981 input: TransactionInput::from(
982 hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
983 ),
984 ..Default::default()
985 };
986
987 mock.expect_call_contract()
989 .with(mockall::predicate::always())
990 .times(1)
991 .returning(|_| {
992 async {
993 Ok(Bytes::from(
994 hex::decode(
995 "0000000000000000000000000000000000000000000000000000000000000001",
996 )
997 .unwrap(),
998 ))
999 }
1000 .boxed()
1001 });
1002
1003 let result = mock.call_contract(&tx).await;
1004 assert!(result.is_ok());
1005
1006 let data = result.unwrap();
1007 assert_eq!(
1008 hex::encode(data),
1009 "0000000000000000000000000000000000000000000000000000000000000001"
1010 );
1011 }
1012}