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