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 let to = match tx.to.as_ref() {
503 Some(address) => TxKind::Call(address.parse().map_err(|_| {
504 TransactionError::InvalidType("Invalid address format".to_string())
505 })?),
506 None => TxKind::Create,
507 };
508
509 Ok(TransactionRequest {
510 from: Some(tx.from.clone().parse().map_err(|_| {
511 TransactionError::InvalidType("Invalid address format".to_string())
512 })?),
513 to: Some(to),
514 gas_price: tx
515 .gas_price
516 .map(|gp| {
517 Uint::<256, 4>::from(gp)
518 .try_into()
519 .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
520 })
521 .transpose()?,
522 value: Some(Uint::<256, 4>::from(tx.value)),
523 input: TransactionInput::from(tx.data_to_bytes()?),
524 nonce: tx
525 .nonce
526 .map(|n| {
527 Uint::<256, 4>::from(n)
528 .try_into()
529 .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
530 })
531 .transpose()?,
532 chain_id: Some(tx.chain_id),
533 max_fee_per_gas: tx
534 .max_fee_per_gas
535 .map(|mfpg| {
536 Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
537 TransactionError::InvalidType("Invalid max fee per gas".to_string())
538 })
539 })
540 .transpose()?,
541 max_priority_fee_per_gas: tx
542 .max_priority_fee_per_gas
543 .map(|mpfpg| {
544 Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
545 TransactionError::InvalidType(
546 "Invalid max priority fee per gas".to_string(),
547 )
548 })
549 })
550 .transpose()?,
551 ..Default::default()
552 })
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use alloy::primitives::Address;
560 use futures::FutureExt;
561 use lazy_static::lazy_static;
562 use std::str::FromStr;
563 use std::sync::Mutex;
564
565 lazy_static! {
566 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
567 }
568
569 struct EvmTestEnvGuard {
570 _mutex_guard: std::sync::MutexGuard<'static, ()>,
571 }
572
573 impl EvmTestEnvGuard {
574 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
575 std::env::set_var(
576 "API_KEY",
577 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
578 );
579 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
580
581 Self {
582 _mutex_guard: mutex_guard,
583 }
584 }
585 }
586
587 impl Drop for EvmTestEnvGuard {
588 fn drop(&mut self) {
589 std::env::remove_var("API_KEY");
590 std::env::remove_var("REDIS_URL");
591 }
592 }
593
594 fn setup_test_env() -> EvmTestEnvGuard {
596 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
597 EvmTestEnvGuard::new(guard)
598 }
599
600 #[tokio::test]
601 async fn test_reqwest_error_conversion() {
602 let client = reqwest::Client::new();
604 let result = client
605 .get("https://www.openzeppelin.com/")
606 .timeout(Duration::from_millis(1))
607 .send()
608 .await;
609
610 assert!(
611 result.is_err(),
612 "Expected the send operation to result in an error."
613 );
614 let err = result.unwrap_err();
615
616 assert!(
617 err.is_timeout(),
618 "The reqwest error should be a timeout. Actual error: {err:?}"
619 );
620
621 let provider_error = ProviderError::from(err);
622 assert!(
623 matches!(provider_error, ProviderError::Timeout),
624 "ProviderError should be Timeout. Actual: {provider_error:?}"
625 );
626 }
627
628 #[test]
629 fn test_address_parse_error_conversion() {
630 let err = "invalid-address".parse::<Address>().unwrap_err();
632 let provider_error = ProviderError::InvalidAddress(err.to_string());
634 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
635 }
636
637 #[test]
638 fn test_new_provider() {
639 let _env_guard = setup_test_env();
640
641 let config = ProviderConfig::new(
642 vec![RpcConfig::new("http://localhost:8545".to_string())],
643 30,
644 3,
645 60,
646 60,
647 );
648 let provider = EvmProvider::new(config);
649 assert!(provider.is_ok());
650
651 let config = ProviderConfig::new(
653 vec![RpcConfig::new("invalid-url".to_string())],
654 30,
655 3,
656 60,
657 60,
658 );
659 let provider = EvmProvider::new(config);
660 assert!(provider.is_err());
661 }
662
663 #[test]
664 fn test_new_provider_with_timeout() {
665 let _env_guard = setup_test_env();
666
667 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 let config = ProviderConfig::new(
691 vec![RpcConfig::new("http://localhost:8545".to_string())],
692 0,
693 3,
694 60,
695 60,
696 );
697 let provider = EvmProvider::new(config);
698 assert!(provider.is_ok());
699
700 let config = ProviderConfig::new(
702 vec![RpcConfig::new("http://localhost:8545".to_string())],
703 3600,
704 3,
705 60,
706 60,
707 );
708 let provider = EvmProvider::new(config);
709 assert!(provider.is_ok());
710 }
711
712 #[test]
713 fn test_transaction_request_conversion() {
714 let tx_data = EvmTransactionData {
715 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
716 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
717 gas_price: Some(1000000000),
718 value: Uint::<256, 4>::from(1000000000),
719 data: Some("0x".to_string()),
720 nonce: Some(1),
721 chain_id: 1,
722 gas_limit: Some(21000),
723 hash: None,
724 signature: None,
725 speed: None,
726 max_fee_per_gas: None,
727 max_priority_fee_per_gas: None,
728 raw: None,
729 };
730
731 let result = TransactionRequest::try_from(&tx_data);
732 assert!(result.is_ok());
733
734 let tx_request = result.unwrap();
735 assert_eq!(
736 tx_request.from,
737 Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
738 );
739 assert_eq!(tx_request.chain_id, Some(1));
740 }
741
742 #[tokio::test]
743 async fn test_mock_provider_methods() {
744 let mut mock = MockEvmProviderTrait::new();
745
746 mock.expect_get_balance()
747 .with(mockall::predicate::eq(
748 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
749 ))
750 .times(1)
751 .returning(|_| async { Ok(U256::from(100)) }.boxed());
752
753 mock.expect_get_block_number()
754 .times(1)
755 .returning(|| async { Ok(12345) }.boxed());
756
757 mock.expect_get_gas_price()
758 .times(1)
759 .returning(|| async { Ok(20000000000) }.boxed());
760
761 mock.expect_health_check()
762 .times(1)
763 .returning(|| async { Ok(true) }.boxed());
764
765 mock.expect_get_transaction_count()
766 .with(mockall::predicate::eq(
767 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
768 ))
769 .times(1)
770 .returning(|_| async { Ok(42) }.boxed());
771
772 mock.expect_get_fee_history()
773 .with(
774 mockall::predicate::eq(10u64),
775 mockall::predicate::eq(BlockNumberOrTag::Latest),
776 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
777 )
778 .times(1)
779 .returning(|_, _, _| {
780 async {
781 Ok(FeeHistory {
782 oldest_block: 100,
783 base_fee_per_gas: vec![1000],
784 gas_used_ratio: vec![0.5],
785 reward: Some(vec![vec![500]]),
786 base_fee_per_blob_gas: vec![1000],
787 blob_gas_used_ratio: vec![0.5],
788 })
789 }
790 .boxed()
791 });
792
793 let balance = mock
795 .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
796 .await;
797 assert!(balance.is_ok());
798 assert_eq!(balance.unwrap(), U256::from(100));
799
800 let block_number = mock.get_block_number().await;
801 assert!(block_number.is_ok());
802 assert_eq!(block_number.unwrap(), 12345);
803
804 let gas_price = mock.get_gas_price().await;
805 assert!(gas_price.is_ok());
806 assert_eq!(gas_price.unwrap(), 20000000000);
807
808 let health = mock.health_check().await;
809 assert!(health.is_ok());
810 assert!(health.unwrap());
811
812 let count = mock
813 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
814 .await;
815 assert!(count.is_ok());
816 assert_eq!(count.unwrap(), 42);
817
818 let fee_history = mock
819 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
820 .await;
821 assert!(fee_history.is_ok());
822 let fee_history = fee_history.unwrap();
823 assert_eq!(fee_history.oldest_block, 100);
824 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
825 }
826
827 #[tokio::test]
828 async fn test_mock_transaction_operations() {
829 let mut mock = MockEvmProviderTrait::new();
830
831 let tx_data = EvmTransactionData {
833 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
834 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
835 gas_price: Some(1000000000),
836 value: Uint::<256, 4>::from(1000000000),
837 data: Some("0x".to_string()),
838 nonce: Some(1),
839 chain_id: 1,
840 gas_limit: Some(21000),
841 hash: None,
842 signature: None,
843 speed: None,
844 max_fee_per_gas: None,
845 max_priority_fee_per_gas: None,
846 raw: None,
847 };
848
849 mock.expect_estimate_gas()
850 .with(mockall::predicate::always())
851 .times(1)
852 .returning(|_| async { Ok(21000) }.boxed());
853
854 mock.expect_send_raw_transaction()
856 .with(mockall::predicate::always())
857 .times(1)
858 .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
859
860 let gas_estimate = mock.estimate_gas(&tx_data).await;
862 assert!(gas_estimate.is_ok());
863 assert_eq!(gas_estimate.unwrap(), 21000);
864
865 let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
866 assert!(tx_hash.is_ok());
867 assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
868 }
869
870 #[test]
871 fn test_invalid_transaction_request_conversion() {
872 let tx_data = EvmTransactionData {
873 from: "invalid-address".to_string(),
874 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
875 gas_price: Some(1000000000),
876 value: Uint::<256, 4>::from(1000000000),
877 data: Some("0x".to_string()),
878 nonce: Some(1),
879 chain_id: 1,
880 gas_limit: Some(21000),
881 hash: None,
882 signature: None,
883 speed: None,
884 max_fee_per_gas: None,
885 max_priority_fee_per_gas: None,
886 raw: None,
887 };
888
889 let result = TransactionRequest::try_from(&tx_data);
890 assert!(result.is_err());
891 }
892
893 #[test]
894 fn test_transaction_request_conversion_contract_creation() {
895 let tx_data = EvmTransactionData {
896 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
897 to: None,
898 gas_price: Some(1000000000),
899 value: Uint::<256, 4>::from(0),
900 data: Some("0x6080604052348015600f57600080fd5b".to_string()),
901 nonce: Some(1),
902 chain_id: 1,
903 gas_limit: None,
904 hash: None,
905 signature: None,
906 speed: None,
907 max_fee_per_gas: None,
908 max_priority_fee_per_gas: None,
909 raw: None,
910 };
911
912 let result = TransactionRequest::try_from(&tx_data);
913
914 assert!(result.is_ok());
915 assert_eq!(result.unwrap().to, Some(TxKind::Create));
916 }
917
918 #[test]
919 fn test_transaction_request_conversion_invalid_to_address() {
920 let tx_data = EvmTransactionData {
921 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
922 to: Some("invalid-address".to_string()),
923 gas_price: Some(1000000000),
924 value: Uint::<256, 4>::from(0),
925 data: Some("0x".to_string()),
926 nonce: Some(1),
927 chain_id: 1,
928 gas_limit: None,
929 hash: None,
930 signature: None,
931 speed: None,
932 max_fee_per_gas: None,
933 max_priority_fee_per_gas: None,
934 raw: None,
935 };
936
937 let result = TransactionRequest::try_from(&tx_data);
938
939 assert!(result.is_err());
940 assert!(matches!(
941 result,
942 Err(TransactionError::InvalidType(ref msg)) if msg == "Invalid address format"
943 ));
944 }
945
946 #[tokio::test]
947 async fn test_mock_additional_methods() {
948 let mut mock = MockEvmProviderTrait::new();
949
950 mock.expect_health_check()
952 .times(1)
953 .returning(|| async { Ok(true) }.boxed());
954
955 mock.expect_get_transaction_count()
957 .with(mockall::predicate::eq(
958 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
959 ))
960 .times(1)
961 .returning(|_| async { Ok(42) }.boxed());
962
963 mock.expect_get_fee_history()
965 .with(
966 mockall::predicate::eq(10u64),
967 mockall::predicate::eq(BlockNumberOrTag::Latest),
968 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
969 )
970 .times(1)
971 .returning(|_, _, _| {
972 async {
973 Ok(FeeHistory {
974 oldest_block: 100,
975 base_fee_per_gas: vec![1000],
976 gas_used_ratio: vec![0.5],
977 reward: Some(vec![vec![500]]),
978 base_fee_per_blob_gas: vec![1000],
979 blob_gas_used_ratio: vec![0.5],
980 })
981 }
982 .boxed()
983 });
984
985 let health = mock.health_check().await;
987 assert!(health.is_ok());
988 assert!(health.unwrap());
989
990 let count = mock
992 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
993 .await;
994 assert!(count.is_ok());
995 assert_eq!(count.unwrap(), 42);
996
997 let fee_history = mock
999 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1000 .await;
1001 assert!(fee_history.is_ok());
1002 let fee_history = fee_history.unwrap();
1003 assert_eq!(fee_history.oldest_block, 100);
1004 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1005 }
1006
1007 #[test]
1008 fn test_is_retriable_error_json_rpc_retriable_codes() {
1009 let retriable_codes = vec![
1011 (-32002, "Resource unavailable"),
1012 (-32005, "Limit exceeded"),
1013 (-32603, "Internal error"),
1014 ];
1015
1016 for (code, message) in retriable_codes {
1017 let error = ProviderError::RpcErrorCode {
1018 code,
1019 message: message.to_string(),
1020 };
1021 assert!(
1022 is_retriable_error(&error),
1023 "Error code {code} should be retriable"
1024 );
1025 }
1026 }
1027
1028 #[test]
1029 fn test_is_retriable_error_json_rpc_non_retriable_codes() {
1030 let non_retriable_codes = vec![
1032 (-32000, "insufficient funds"),
1033 (-32000, "execution reverted"),
1034 (-32000, "already known"),
1035 (-32000, "nonce too low"),
1036 (-32000, "invalid sender"),
1037 (-32001, "Resource not found"),
1038 (-32003, "Transaction rejected"),
1039 (-32004, "Method not supported"),
1040 (-32700, "Parse error"),
1041 (-32600, "Invalid request"),
1042 (-32601, "Method not found"),
1043 (-32602, "Invalid params"),
1044 ];
1045
1046 for (code, message) in non_retriable_codes {
1047 let error = ProviderError::RpcErrorCode {
1048 code,
1049 message: message.to_string(),
1050 };
1051 assert!(
1052 !is_retriable_error(&error),
1053 "Error code {code} with message '{message}' should NOT be retriable"
1054 );
1055 }
1056 }
1057
1058 #[test]
1059 fn test_is_retriable_error_json_rpc_32000_specific_cases() {
1060 let test_cases = vec![
1063 (
1064 "tx already exists in cache",
1065 false,
1066 "Transaction already in mempool",
1067 ),
1068 ("already known", false, "Duplicate transaction submission"),
1069 (
1070 "insufficient funds for gas * price + value",
1071 false,
1072 "User needs more funds",
1073 ),
1074 ("execution reverted", false, "Smart contract rejected"),
1075 ("nonce too low", false, "Transaction already processed"),
1076 ("invalid sender", false, "Configuration issue"),
1077 ("gas required exceeds allowance", false, "Gas limit too low"),
1078 (
1079 "replacement transaction underpriced",
1080 false,
1081 "Need higher gas price",
1082 ),
1083 ];
1084
1085 for (message, should_retry, description) in test_cases {
1086 let error = ProviderError::RpcErrorCode {
1087 code: -32000,
1088 message: message.to_string(),
1089 };
1090 assert_eq!(
1091 is_retriable_error(&error),
1092 should_retry,
1093 "{}: -32000 with '{}' should{} be retriable",
1094 description,
1095 message,
1096 if should_retry { "" } else { " NOT" }
1097 );
1098 }
1099 }
1100
1101 #[tokio::test]
1102 async fn test_call_contract() {
1103 let mut mock = MockEvmProviderTrait::new();
1104
1105 let tx = TransactionRequest {
1106 from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1107 to: Some(TxKind::Call(
1108 Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1109 )),
1110 input: TransactionInput::from(
1111 hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1112 ),
1113 ..Default::default()
1114 };
1115
1116 mock.expect_call_contract()
1118 .with(mockall::predicate::always())
1119 .times(1)
1120 .returning(|_| {
1121 async {
1122 Ok(Bytes::from(
1123 hex::decode(
1124 "0000000000000000000000000000000000000000000000000000000000000001",
1125 )
1126 .unwrap(),
1127 ))
1128 }
1129 .boxed()
1130 });
1131
1132 let result = mock.call_contract(&tx).await;
1133 assert!(result.is_ok());
1134
1135 let data = result.unwrap();
1136 assert_eq!(
1137 hex::encode(data),
1138 "0000000000000000000000000000000000000000000000000000000000000001"
1139 );
1140 }
1141}