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