1use std::time::Duration;
8
9use alloy::{
10 primitives::{Bytes, TxKind, Uint},
11 providers::{Provider, ProviderBuilder, RootProvider},
12 rpc::{
13 client::ClientBuilder,
14 types::{
15 Block as BlockResponse, BlockNumberOrTag, BlockTransactionsKind, FeeHistory,
16 TransactionInput, TransactionReceipt, TransactionRequest,
17 },
18 },
19 transports::http::{Client, Http},
20};
21use async_trait::async_trait;
22use eyre::Result;
23use reqwest::ClientBuilder as ReqwestClientBuilder;
24use serde_json;
25
26use super::rpc_selector::RpcSelector;
27use super::{retry_rpc_call, RetryConfig};
28use crate::models::{EvmTransactionData, RpcConfig, TransactionError, U256};
29
30#[cfg(test)]
31use mockall::automock;
32
33use super::ProviderError;
34
35#[derive(Clone)]
39pub struct EvmProvider {
40 selector: RpcSelector,
42 timeout_seconds: u64,
44 retry_config: RetryConfig,
46}
47
48#[async_trait]
53#[cfg_attr(test, automock)]
54#[allow(dead_code)]
55pub trait EvmProviderTrait: Send + Sync {
56 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError>;
61
62 async fn get_block_number(&self) -> Result<u64, ProviderError>;
64
65 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError>;
70
71 async fn get_gas_price(&self) -> Result<u128, ProviderError>;
73
74 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError>;
79
80 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError>;
85
86 async fn health_check(&self) -> Result<bool, ProviderError>;
88
89 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError>;
94
95 async fn get_fee_history(
102 &self,
103 block_count: u64,
104 newest_block: BlockNumberOrTag,
105 reward_percentiles: Vec<f64>,
106 ) -> Result<FeeHistory, ProviderError>;
107
108 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError>;
110
111 async fn get_transaction_receipt(
116 &self,
117 tx_hash: &str,
118 ) -> Result<Option<TransactionReceipt>, ProviderError>;
119
120 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError>;
125
126 async fn raw_request_dyn(
132 &self,
133 method: &str,
134 params: serde_json::Value,
135 ) -> Result<serde_json::Value, ProviderError>;
136}
137
138impl EvmProvider {
139 pub fn new(configs: Vec<RpcConfig>, timeout_seconds: u64) -> Result<Self, ProviderError> {
148 if configs.is_empty() {
149 return Err(ProviderError::NetworkConfiguration(
150 "At least one RPC configuration must be provided".to_string(),
151 ));
152 }
153
154 RpcConfig::validate_list(&configs)
155 .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {}", e)))?;
156
157 let selector = RpcSelector::new(configs).map_err(|e| {
159 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {}", e))
160 })?;
161
162 let retry_config = RetryConfig::from_env();
163
164 Ok(Self {
165 selector,
166 timeout_seconds,
167 retry_config,
168 })
169 }
170
171 fn should_mark_provider_failed(error: &ProviderError) -> bool {
173 match error {
174 ProviderError::RequestError { status_code, .. } => {
175 match *status_code {
176 500..=599 => true,
178
179 401 => true, 403 => true, 404 => true, 410 => true, _ => false,
186 }
187 }
188 _ => false,
189 }
190 }
191
192 fn is_retriable_error(error: &ProviderError) -> bool {
194 match error {
195 ProviderError::Timeout | ProviderError::RateLimited | ProviderError::BadGateway => true,
197
198 _ => {
200 let err_msg = format!("{}", error);
202 err_msg.to_lowercase().contains("timeout")
203 || err_msg.to_lowercase().contains("connection")
204 || err_msg.to_lowercase().contains("reset")
205 }
206 }
207 }
208
209 fn initialize_provider(&self, url: &str) -> Result<RootProvider<Http<Client>>, ProviderError> {
211 let rpc_url = url.parse().map_err(|e| {
212 ProviderError::NetworkConfiguration(format!("Invalid URL format: {}", e))
213 })?;
214
215 let client = ReqwestClientBuilder::default()
216 .timeout(Duration::from_secs(self.timeout_seconds))
217 .build()
218 .map_err(|e| ProviderError::Other(format!("Failed to build HTTP client: {}", e)))?;
219
220 let mut transport = Http::new(rpc_url);
221 transport.set_client(client);
222
223 let is_local = transport.guess_local();
224 let client = ClientBuilder::default().transport(transport, is_local);
225
226 let provider = ProviderBuilder::new().on_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(RootProvider<Http<Client>>) -> Fut,
241 Fut: std::future::Future<Output = Result<T, ProviderError>>,
242 {
243 log::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 Self::is_retriable_error,
255 Self::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 async fn get_balance(&self, address: &str) -> Result<U256, ProviderError> {
276 let parsed_address = address
277 .parse::<alloy::primitives::Address>()
278 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
279
280 self.retry_rpc_call("get_balance", move |provider| async move {
281 provider
282 .get_balance(parsed_address)
283 .await
284 .map_err(ProviderError::from)
285 })
286 .await
287 }
288
289 async fn get_block_number(&self) -> Result<u64, ProviderError> {
290 self.retry_rpc_call("get_block_number", |provider| async move {
291 provider
292 .get_block_number()
293 .await
294 .map_err(ProviderError::from)
295 })
296 .await
297 }
298
299 async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError> {
300 let transaction_request = TransactionRequest::try_from(tx)
301 .map_err(|e| ProviderError::Other(format!("Failed to convert transaction: {}", e)))?;
302
303 self.retry_rpc_call("estimate_gas", move |provider| {
304 let tx_req = transaction_request.clone();
305 async move {
306 provider
307 .estimate_gas(&tx_req)
308 .await
309 .map_err(ProviderError::from)
310 }
311 })
312 .await
313 }
314
315 async fn get_gas_price(&self) -> Result<u128, ProviderError> {
316 self.retry_rpc_call("get_gas_price", |provider| async move {
317 provider.get_gas_price().await.map_err(ProviderError::from)
318 })
319 .await
320 }
321
322 async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError> {
323 let pending_tx = self
324 .retry_rpc_call("send_transaction", move |provider| {
325 let tx_req = tx.clone();
326 async move {
327 provider
328 .send_transaction(tx_req)
329 .await
330 .map_err(ProviderError::from)
331 }
332 })
333 .await?;
334
335 let tx_hash = pending_tx.tx_hash().to_string();
336 Ok(tx_hash)
337 }
338
339 async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError> {
340 let pending_tx = self
341 .retry_rpc_call("send_raw_transaction", move |provider| {
342 let tx_data = tx.to_vec();
343 async move {
344 provider
345 .send_raw_transaction(&tx_data)
346 .await
347 .map_err(ProviderError::from)
348 }
349 })
350 .await?;
351
352 let tx_hash = pending_tx.tx_hash().to_string();
353 Ok(tx_hash)
354 }
355
356 async fn health_check(&self) -> Result<bool, ProviderError> {
357 match self.get_block_number().await {
358 Ok(_) => Ok(true),
359 Err(e) => Err(e),
360 }
361 }
362
363 async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError> {
364 let parsed_address = address
365 .parse::<alloy::primitives::Address>()
366 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
367
368 self.retry_rpc_call("get_transaction_count", move |provider| async move {
369 provider
370 .get_transaction_count(parsed_address)
371 .await
372 .map_err(ProviderError::from)
373 })
374 .await
375 }
376
377 async fn get_fee_history(
378 &self,
379 block_count: u64,
380 newest_block: BlockNumberOrTag,
381 reward_percentiles: Vec<f64>,
382 ) -> Result<FeeHistory, ProviderError> {
383 self.retry_rpc_call("get_fee_history", move |provider| {
384 let reward_percentiles_clone = reward_percentiles.clone();
385 async move {
386 provider
387 .get_fee_history(block_count, newest_block, &reward_percentiles_clone)
388 .await
389 .map_err(ProviderError::from)
390 }
391 })
392 .await
393 }
394
395 async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError> {
396 let block_result = self
397 .retry_rpc_call("get_block_by_number", |provider| async move {
398 provider
399 .get_block_by_number(BlockNumberOrTag::Latest, BlockTransactionsKind::Hashes)
400 .await
401 .map_err(ProviderError::from)
402 })
403 .await?;
404
405 match block_result {
406 Some(block) => Ok(block),
407 None => Err(ProviderError::Other("Block not found".to_string())),
408 }
409 }
410
411 async fn get_transaction_receipt(
412 &self,
413 tx_hash: &str,
414 ) -> Result<Option<TransactionReceipt>, ProviderError> {
415 let parsed_tx_hash = tx_hash
416 .parse::<alloy::primitives::TxHash>()
417 .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
418
419 self.retry_rpc_call("get_transaction_receipt", move |provider| async move {
420 provider
421 .get_transaction_receipt(parsed_tx_hash)
422 .await
423 .map_err(ProviderError::from)
424 })
425 .await
426 }
427
428 async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError> {
429 self.retry_rpc_call("call_contract", move |provider| {
430 let tx_req = tx.clone();
431 async move { provider.call(&tx_req).await.map_err(ProviderError::from) }
432 })
433 .await
434 }
435
436 async fn raw_request_dyn(
437 &self,
438 method: &str,
439 params: serde_json::Value,
440 ) -> Result<serde_json::Value, ProviderError> {
441 self.retry_rpc_call("raw_request_dyn", move |provider| {
442 let method_clone = method.to_string();
443 let params_clone = params.clone();
444 async move {
445 let params_raw = serde_json::value::to_raw_value(¶ms_clone).map_err(|e| {
447 ProviderError::Other(format!("Failed to serialize params: {}", e))
448 })?;
449
450 let result = provider
451 .raw_request_dyn(std::borrow::Cow::Owned(method_clone), ¶ms_raw)
452 .await
453 .map_err(ProviderError::from)?;
454
455 serde_json::from_str(result.get()).map_err(|e| {
457 ProviderError::Other(format!("Failed to deserialize result: {}", e))
458 })
459 }
460 })
461 .await
462 }
463}
464
465impl TryFrom<&EvmTransactionData> for TransactionRequest {
466 type Error = TransactionError;
467 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
468 Ok(TransactionRequest {
469 from: Some(tx.from.clone().parse().map_err(|_| {
470 TransactionError::InvalidType("Invalid address format".to_string())
471 })?),
472 to: Some(TxKind::Call(
473 tx.to
474 .clone()
475 .unwrap_or("".to_string())
476 .parse()
477 .map_err(|_| {
478 TransactionError::InvalidType("Invalid address format".to_string())
479 })?,
480 )),
481 gas_price: Some(
482 Uint::<256, 4>::from(tx.gas_price.unwrap_or(0))
483 .try_into()
484 .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))?,
485 ),
486 value: Some(Uint::<256, 4>::from(tx.value)),
493 input: TransactionInput::from(tx.data.clone().unwrap_or("".to_string()).into_bytes()),
494 nonce: Some(
495 Uint::<256, 4>::from(tx.nonce.ok_or_else(|| {
496 TransactionError::InvalidType("Nonce must be defined".to_string())
497 })?)
498 .try_into()
499 .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))?,
500 ),
501 chain_id: Some(tx.chain_id),
502 ..Default::default()
503 })
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use alloy::primitives::Address;
511 use futures::FutureExt;
512 use lazy_static::lazy_static;
513 use std::str::FromStr;
514 use std::sync::Mutex;
515
516 lazy_static! {
517 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
518 }
519
520 struct EvmTestEnvGuard {
521 _mutex_guard: std::sync::MutexGuard<'static, ()>,
522 }
523
524 impl EvmTestEnvGuard {
525 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
526 std::env::set_var(
527 "API_KEY",
528 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
529 );
530 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
531
532 Self {
533 _mutex_guard: mutex_guard,
534 }
535 }
536 }
537
538 impl Drop for EvmTestEnvGuard {
539 fn drop(&mut self) {
540 std::env::remove_var("API_KEY");
541 std::env::remove_var("REDIS_URL");
542 }
543 }
544
545 fn setup_test_env() -> EvmTestEnvGuard {
547 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
548 EvmTestEnvGuard::new(guard)
549 }
550
551 #[tokio::test]
552 async fn test_reqwest_error_conversion() {
553 let client = reqwest::Client::new();
555 let result = client
556 .get("https://www.openzeppelin.com/")
557 .timeout(Duration::from_millis(1))
558 .send()
559 .await;
560
561 assert!(
562 result.is_err(),
563 "Expected the send operation to result in an error."
564 );
565 let err = result.unwrap_err();
566
567 assert!(
568 err.is_timeout(),
569 "The reqwest error should be a timeout. Actual error: {:?}",
570 err
571 );
572
573 let provider_error = ProviderError::from(err);
574 assert!(
575 matches!(provider_error, ProviderError::Timeout),
576 "ProviderError should be Timeout. Actual: {:?}",
577 provider_error
578 );
579 }
580
581 #[test]
582 fn test_address_parse_error_conversion() {
583 let err = "invalid-address".parse::<Address>().unwrap_err();
585 let provider_error = ProviderError::InvalidAddress(err.to_string());
587 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
588 }
589
590 #[test]
591 fn test_new_provider() {
592 let _env_guard = setup_test_env();
593
594 let provider = EvmProvider::new(
595 vec![RpcConfig::new("http://localhost:8545".to_string())],
596 30,
597 );
598 assert!(provider.is_ok());
599
600 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
602 assert!(provider.is_err());
603 }
604
605 #[test]
606 fn test_new_provider_with_timeout() {
607 let _env_guard = setup_test_env();
608
609 let provider = EvmProvider::new(
611 vec![RpcConfig::new("http://localhost:8545".to_string())],
612 30,
613 );
614 assert!(provider.is_ok());
615
616 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
618 assert!(provider.is_err());
619
620 let provider =
622 EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
623 assert!(provider.is_ok());
624
625 let provider = EvmProvider::new(
627 vec![RpcConfig::new("http://localhost:8545".to_string())],
628 3600,
629 );
630 assert!(provider.is_ok());
631 }
632
633 #[test]
634 fn test_transaction_request_conversion() {
635 let tx_data = EvmTransactionData {
636 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
637 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
638 gas_price: Some(1000000000),
639 value: Uint::<256, 4>::from(1000000000),
640 data: Some("0x".to_string()),
641 nonce: Some(1),
642 chain_id: 1,
643 gas_limit: 21000,
644 hash: None,
645 signature: None,
646 speed: None,
647 max_fee_per_gas: None,
648 max_priority_fee_per_gas: None,
649 raw: None,
650 };
651
652 let result = TransactionRequest::try_from(&tx_data);
653 assert!(result.is_ok());
654
655 let tx_request = result.unwrap();
656 assert_eq!(
657 tx_request.from,
658 Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
659 );
660 assert_eq!(tx_request.chain_id, Some(1));
661 }
662
663 #[test]
664 fn test_should_mark_provider_failed_server_errors() {
665 for status_code in 500..=599 {
667 let error = ProviderError::RequestError {
668 error: format!("Server error {}", status_code),
669 status_code,
670 };
671 assert!(
672 EvmProvider::should_mark_provider_failed(&error),
673 "Status code {} should mark provider as failed",
674 status_code
675 );
676 }
677 }
678
679 #[test]
680 fn test_should_mark_provider_failed_auth_errors() {
681 let auth_errors = [401, 403];
683 for &status_code in &auth_errors {
684 let error = ProviderError::RequestError {
685 error: format!("Auth error {}", status_code),
686 status_code,
687 };
688 assert!(
689 EvmProvider::should_mark_provider_failed(&error),
690 "Status code {} should mark provider as failed",
691 status_code
692 );
693 }
694 }
695
696 #[test]
697 fn test_should_mark_provider_failed_not_found_errors() {
698 let not_found_errors = [404, 410];
700 for &status_code in ¬_found_errors {
701 let error = ProviderError::RequestError {
702 error: format!("Not found error {}", status_code),
703 status_code,
704 };
705 assert!(
706 EvmProvider::should_mark_provider_failed(&error),
707 "Status code {} should mark provider as failed",
708 status_code
709 );
710 }
711 }
712
713 #[test]
714 fn test_should_mark_provider_failed_client_errors_not_failed() {
715 let client_errors = [400, 405, 413, 414, 415, 422, 429];
717 for &status_code in &client_errors {
718 let error = ProviderError::RequestError {
719 error: format!("Client error {}", status_code),
720 status_code,
721 };
722 assert!(
723 !EvmProvider::should_mark_provider_failed(&error),
724 "Status code {} should NOT mark provider as failed",
725 status_code
726 );
727 }
728 }
729
730 #[test]
731 fn test_should_mark_provider_failed_other_error_types() {
732 let errors = [
734 ProviderError::Timeout,
735 ProviderError::RateLimited,
736 ProviderError::BadGateway,
737 ProviderError::InvalidAddress("test".to_string()),
738 ProviderError::NetworkConfiguration("test".to_string()),
739 ProviderError::Other("test".to_string()),
740 ];
741
742 for error in errors {
743 assert!(
744 !EvmProvider::should_mark_provider_failed(&error),
745 "Error type {:?} should NOT mark provider as failed",
746 error
747 );
748 }
749 }
750
751 #[test]
752 fn test_should_mark_provider_failed_edge_cases() {
753 let edge_cases = [
755 (200, false), (300, false), (418, false), (451, false), (499, false), ];
761
762 for (status_code, should_fail) in edge_cases {
763 let error = ProviderError::RequestError {
764 error: format!("Edge case error {}", status_code),
765 status_code,
766 };
767 assert_eq!(
768 EvmProvider::should_mark_provider_failed(&error),
769 should_fail,
770 "Status code {} should {} mark provider as failed",
771 status_code,
772 if should_fail { "" } else { "NOT" }
773 );
774 }
775 }
776
777 #[test]
778 fn test_is_retriable_error_retriable_types() {
779 let retriable_errors = [
781 ProviderError::Timeout,
782 ProviderError::RateLimited,
783 ProviderError::BadGateway,
784 ];
785
786 for error in retriable_errors {
787 assert!(
788 EvmProvider::is_retriable_error(&error),
789 "Error type {:?} should be retriable",
790 error
791 );
792 }
793 }
794
795 #[test]
796 fn test_is_retriable_error_non_retriable_types() {
797 let non_retriable_errors = [
799 ProviderError::InvalidAddress("test".to_string()),
800 ProviderError::NetworkConfiguration("test".to_string()),
801 ProviderError::RequestError {
802 error: "Some error".to_string(),
803 status_code: 400,
804 },
805 ];
806
807 for error in non_retriable_errors {
808 assert!(
809 !EvmProvider::is_retriable_error(&error),
810 "Error type {:?} should NOT be retriable",
811 error
812 );
813 }
814 }
815
816 #[test]
817 fn test_is_retriable_error_message_based_detection() {
818 let retriable_messages = [
820 "Connection timeout occurred",
821 "Network connection reset",
822 "Connection refused",
823 "TIMEOUT error happened",
824 "Connection was reset by peer",
825 ];
826
827 for message in retriable_messages {
828 let error = ProviderError::Other(message.to_string());
829 assert!(
830 EvmProvider::is_retriable_error(&error),
831 "Error with message '{}' should be retriable",
832 message
833 );
834 }
835 }
836
837 #[test]
838 fn test_is_retriable_error_message_based_non_retriable() {
839 let non_retriable_messages = [
841 "Invalid address format",
842 "Bad request parameters",
843 "Authentication failed",
844 "Method not found",
845 "Some other error",
846 ];
847
848 for message in non_retriable_messages {
849 let error = ProviderError::Other(message.to_string());
850 assert!(
851 !EvmProvider::is_retriable_error(&error),
852 "Error with message '{}' should NOT be retriable",
853 message
854 );
855 }
856 }
857
858 #[test]
859 fn test_is_retriable_error_case_insensitive() {
860 let case_variations = [
862 "TIMEOUT",
863 "Timeout",
864 "timeout",
865 "CONNECTION",
866 "Connection",
867 "connection",
868 "RESET",
869 "Reset",
870 "reset",
871 ];
872
873 for message in case_variations {
874 let error = ProviderError::Other(message.to_string());
875 assert!(
876 EvmProvider::is_retriable_error(&error),
877 "Error with message '{}' should be retriable (case insensitive)",
878 message
879 );
880 }
881 }
882
883 #[tokio::test]
884 async fn test_mock_provider_methods() {
885 let mut mock = MockEvmProviderTrait::new();
886
887 mock.expect_get_balance()
888 .with(mockall::predicate::eq(
889 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
890 ))
891 .times(1)
892 .returning(|_| async { Ok(U256::from(100)) }.boxed());
893
894 mock.expect_get_block_number()
895 .times(1)
896 .returning(|| async { Ok(12345) }.boxed());
897
898 mock.expect_get_gas_price()
899 .times(1)
900 .returning(|| async { Ok(20000000000) }.boxed());
901
902 mock.expect_health_check()
903 .times(1)
904 .returning(|| async { Ok(true) }.boxed());
905
906 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()
914 .with(
915 mockall::predicate::eq(10u64),
916 mockall::predicate::eq(BlockNumberOrTag::Latest),
917 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
918 )
919 .times(1)
920 .returning(|_, _, _| {
921 async {
922 Ok(FeeHistory {
923 oldest_block: 100,
924 base_fee_per_gas: vec![1000],
925 gas_used_ratio: vec![0.5],
926 reward: Some(vec![vec![500]]),
927 base_fee_per_blob_gas: vec![1000],
928 blob_gas_used_ratio: vec![0.5],
929 })
930 }
931 .boxed()
932 });
933
934 let balance = mock
936 .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
937 .await;
938 assert!(balance.is_ok());
939 assert_eq!(balance.unwrap(), U256::from(100));
940
941 let block_number = mock.get_block_number().await;
942 assert!(block_number.is_ok());
943 assert_eq!(block_number.unwrap(), 12345);
944
945 let gas_price = mock.get_gas_price().await;
946 assert!(gas_price.is_ok());
947 assert_eq!(gas_price.unwrap(), 20000000000);
948
949 let health = mock.health_check().await;
950 assert!(health.is_ok());
951 assert!(health.unwrap());
952
953 let count = mock
954 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
955 .await;
956 assert!(count.is_ok());
957 assert_eq!(count.unwrap(), 42);
958
959 let fee_history = mock
960 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
961 .await;
962 assert!(fee_history.is_ok());
963 let fee_history = fee_history.unwrap();
964 assert_eq!(fee_history.oldest_block, 100);
965 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
966 }
967
968 #[tokio::test]
969 async fn test_mock_transaction_operations() {
970 let mut mock = MockEvmProviderTrait::new();
971
972 let tx_data = EvmTransactionData {
974 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
975 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
976 gas_price: Some(1000000000),
977 value: Uint::<256, 4>::from(1000000000),
978 data: Some("0x".to_string()),
979 nonce: Some(1),
980 chain_id: 1,
981 gas_limit: 21000,
982 hash: None,
983 signature: None,
984 speed: None,
985 max_fee_per_gas: None,
986 max_priority_fee_per_gas: None,
987 raw: None,
988 };
989
990 mock.expect_estimate_gas()
991 .with(mockall::predicate::always())
992 .times(1)
993 .returning(|_| async { Ok(21000) }.boxed());
994
995 mock.expect_send_raw_transaction()
997 .with(mockall::predicate::always())
998 .times(1)
999 .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
1000
1001 let gas_estimate = mock.estimate_gas(&tx_data).await;
1003 assert!(gas_estimate.is_ok());
1004 assert_eq!(gas_estimate.unwrap(), 21000);
1005
1006 let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
1007 assert!(tx_hash.is_ok());
1008 assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
1009 }
1010
1011 #[test]
1012 fn test_invalid_transaction_request_conversion() {
1013 let tx_data = EvmTransactionData {
1014 from: "invalid-address".to_string(),
1015 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1016 gas_price: Some(1000000000),
1017 value: Uint::<256, 4>::from(1000000000),
1018 data: Some("0x".to_string()),
1019 nonce: Some(1),
1020 chain_id: 1,
1021 gas_limit: 21000,
1022 hash: None,
1023 signature: None,
1024 speed: None,
1025 max_fee_per_gas: None,
1026 max_priority_fee_per_gas: None,
1027 raw: None,
1028 };
1029
1030 let result = TransactionRequest::try_from(&tx_data);
1031 assert!(result.is_err());
1032 }
1033
1034 #[tokio::test]
1035 async fn test_mock_additional_methods() {
1036 let mut mock = MockEvmProviderTrait::new();
1037
1038 mock.expect_health_check()
1040 .times(1)
1041 .returning(|| async { Ok(true) }.boxed());
1042
1043 mock.expect_get_transaction_count()
1045 .with(mockall::predicate::eq(
1046 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
1047 ))
1048 .times(1)
1049 .returning(|_| async { Ok(42) }.boxed());
1050
1051 mock.expect_get_fee_history()
1053 .with(
1054 mockall::predicate::eq(10u64),
1055 mockall::predicate::eq(BlockNumberOrTag::Latest),
1056 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
1057 )
1058 .times(1)
1059 .returning(|_, _, _| {
1060 async {
1061 Ok(FeeHistory {
1062 oldest_block: 100,
1063 base_fee_per_gas: vec![1000],
1064 gas_used_ratio: vec![0.5],
1065 reward: Some(vec![vec![500]]),
1066 base_fee_per_blob_gas: vec![1000],
1067 blob_gas_used_ratio: vec![0.5],
1068 })
1069 }
1070 .boxed()
1071 });
1072
1073 let health = mock.health_check().await;
1075 assert!(health.is_ok());
1076 assert!(health.unwrap());
1077
1078 let count = mock
1080 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1081 .await;
1082 assert!(count.is_ok());
1083 assert_eq!(count.unwrap(), 42);
1084
1085 let fee_history = mock
1087 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1088 .await;
1089 assert!(fee_history.is_ok());
1090 let fee_history = fee_history.unwrap();
1091 assert_eq!(fee_history.oldest_block, 100);
1092 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1093 }
1094
1095 #[tokio::test]
1096 async fn test_call_contract() {
1097 let mut mock = MockEvmProviderTrait::new();
1098
1099 let tx = TransactionRequest {
1100 from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1101 to: Some(TxKind::Call(
1102 Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1103 )),
1104 input: TransactionInput::from(
1105 hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1106 ),
1107 ..Default::default()
1108 };
1109
1110 mock.expect_call_contract()
1112 .with(mockall::predicate::always())
1113 .times(1)
1114 .returning(|_| {
1115 async {
1116 Ok(Bytes::from(
1117 hex::decode(
1118 "0000000000000000000000000000000000000000000000000000000000000001",
1119 )
1120 .unwrap(),
1121 ))
1122 }
1123 .boxed()
1124 });
1125
1126 let result = mock.call_contract(&tx).await;
1127 assert!(result.is_ok());
1128
1129 let data = result.unwrap();
1130 assert_eq!(
1131 hex::encode(data),
1132 "0000000000000000000000000000000000000000000000000000000000000001"
1133 );
1134 }
1135}