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