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: tx
482 .gas_price
483 .map(|gp| {
484 Uint::<256, 4>::from(gp)
485 .try_into()
486 .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
487 })
488 .transpose()?,
489 value: Some(Uint::<256, 4>::from(tx.value)),
490 input: TransactionInput::from(tx.data_to_bytes()?),
491 nonce: tx
492 .nonce
493 .map(|n| {
494 Uint::<256, 4>::from(n)
495 .try_into()
496 .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
497 })
498 .transpose()?,
499 chain_id: Some(tx.chain_id),
500 max_fee_per_gas: tx
501 .max_fee_per_gas
502 .map(|mfpg| {
503 Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
504 TransactionError::InvalidType("Invalid max fee per gas".to_string())
505 })
506 })
507 .transpose()?,
508 max_priority_fee_per_gas: tx
509 .max_priority_fee_per_gas
510 .map(|mpfpg| {
511 Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
512 TransactionError::InvalidType(
513 "Invalid max priority fee per gas".to_string(),
514 )
515 })
516 })
517 .transpose()?,
518 ..Default::default()
519 })
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use alloy::primitives::Address;
527 use futures::FutureExt;
528 use lazy_static::lazy_static;
529 use std::str::FromStr;
530 use std::sync::Mutex;
531
532 lazy_static! {
533 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
534 }
535
536 struct EvmTestEnvGuard {
537 _mutex_guard: std::sync::MutexGuard<'static, ()>,
538 }
539
540 impl EvmTestEnvGuard {
541 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
542 std::env::set_var(
543 "API_KEY",
544 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
545 );
546 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
547
548 Self {
549 _mutex_guard: mutex_guard,
550 }
551 }
552 }
553
554 impl Drop for EvmTestEnvGuard {
555 fn drop(&mut self) {
556 std::env::remove_var("API_KEY");
557 std::env::remove_var("REDIS_URL");
558 }
559 }
560
561 fn setup_test_env() -> EvmTestEnvGuard {
563 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
564 EvmTestEnvGuard::new(guard)
565 }
566
567 #[tokio::test]
568 async fn test_reqwest_error_conversion() {
569 let client = reqwest::Client::new();
571 let result = client
572 .get("https://www.openzeppelin.com/")
573 .timeout(Duration::from_millis(1))
574 .send()
575 .await;
576
577 assert!(
578 result.is_err(),
579 "Expected the send operation to result in an error."
580 );
581 let err = result.unwrap_err();
582
583 assert!(
584 err.is_timeout(),
585 "The reqwest error should be a timeout. Actual error: {:?}",
586 err
587 );
588
589 let provider_error = ProviderError::from(err);
590 assert!(
591 matches!(provider_error, ProviderError::Timeout),
592 "ProviderError should be Timeout. Actual: {:?}",
593 provider_error
594 );
595 }
596
597 #[test]
598 fn test_address_parse_error_conversion() {
599 let err = "invalid-address".parse::<Address>().unwrap_err();
601 let provider_error = ProviderError::InvalidAddress(err.to_string());
603 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
604 }
605
606 #[test]
607 fn test_new_provider() {
608 let _env_guard = setup_test_env();
609
610 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
621 #[test]
622 fn test_new_provider_with_timeout() {
623 let _env_guard = setup_test_env();
624
625 let provider = EvmProvider::new(
627 vec![RpcConfig::new("http://localhost:8545".to_string())],
628 30,
629 );
630 assert!(provider.is_ok());
631
632 let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
634 assert!(provider.is_err());
635
636 let provider =
638 EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
639 assert!(provider.is_ok());
640
641 let provider = EvmProvider::new(
643 vec![RpcConfig::new("http://localhost:8545".to_string())],
644 3600,
645 );
646 assert!(provider.is_ok());
647 }
648
649 #[test]
650 fn test_transaction_request_conversion() {
651 let tx_data = EvmTransactionData {
652 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
653 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
654 gas_price: Some(1000000000),
655 value: Uint::<256, 4>::from(1000000000),
656 data: Some("0x".to_string()),
657 nonce: Some(1),
658 chain_id: 1,
659 gas_limit: Some(21000),
660 hash: None,
661 signature: None,
662 speed: None,
663 max_fee_per_gas: None,
664 max_priority_fee_per_gas: None,
665 raw: None,
666 };
667
668 let result = TransactionRequest::try_from(&tx_data);
669 assert!(result.is_ok());
670
671 let tx_request = result.unwrap();
672 assert_eq!(
673 tx_request.from,
674 Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
675 );
676 assert_eq!(tx_request.chain_id, Some(1));
677 }
678
679 #[test]
680 fn test_should_mark_provider_failed_server_errors() {
681 for status_code in 500..=599 {
683 let error = ProviderError::RequestError {
684 error: format!("Server error {}", status_code),
685 status_code,
686 };
687 assert!(
688 EvmProvider::should_mark_provider_failed(&error),
689 "Status code {} should mark provider as failed",
690 status_code
691 );
692 }
693 }
694
695 #[test]
696 fn test_should_mark_provider_failed_auth_errors() {
697 let auth_errors = [401, 403];
699 for &status_code in &auth_errors {
700 let error = ProviderError::RequestError {
701 error: format!("Auth error {}", status_code),
702 status_code,
703 };
704 assert!(
705 EvmProvider::should_mark_provider_failed(&error),
706 "Status code {} should mark provider as failed",
707 status_code
708 );
709 }
710 }
711
712 #[test]
713 fn test_should_mark_provider_failed_not_found_errors() {
714 let not_found_errors = [404, 410];
716 for &status_code in ¬_found_errors {
717 let error = ProviderError::RequestError {
718 error: format!("Not found error {}", status_code),
719 status_code,
720 };
721 assert!(
722 EvmProvider::should_mark_provider_failed(&error),
723 "Status code {} should mark provider as failed",
724 status_code
725 );
726 }
727 }
728
729 #[test]
730 fn test_should_mark_provider_failed_client_errors_not_failed() {
731 let client_errors = [400, 405, 413, 414, 415, 422, 429];
733 for &status_code in &client_errors {
734 let error = ProviderError::RequestError {
735 error: format!("Client error {}", status_code),
736 status_code,
737 };
738 assert!(
739 !EvmProvider::should_mark_provider_failed(&error),
740 "Status code {} should NOT mark provider as failed",
741 status_code
742 );
743 }
744 }
745
746 #[test]
747 fn test_should_mark_provider_failed_other_error_types() {
748 let errors = [
750 ProviderError::Timeout,
751 ProviderError::RateLimited,
752 ProviderError::BadGateway,
753 ProviderError::InvalidAddress("test".to_string()),
754 ProviderError::NetworkConfiguration("test".to_string()),
755 ProviderError::Other("test".to_string()),
756 ];
757
758 for error in errors {
759 assert!(
760 !EvmProvider::should_mark_provider_failed(&error),
761 "Error type {:?} should NOT mark provider as failed",
762 error
763 );
764 }
765 }
766
767 #[test]
768 fn test_should_mark_provider_failed_edge_cases() {
769 let edge_cases = [
771 (200, false), (300, false), (418, false), (451, false), (499, false), ];
777
778 for (status_code, should_fail) in edge_cases {
779 let error = ProviderError::RequestError {
780 error: format!("Edge case error {}", status_code),
781 status_code,
782 };
783 assert_eq!(
784 EvmProvider::should_mark_provider_failed(&error),
785 should_fail,
786 "Status code {} should {} mark provider as failed",
787 status_code,
788 if should_fail { "" } else { "NOT" }
789 );
790 }
791 }
792
793 #[test]
794 fn test_is_retriable_error_retriable_types() {
795 let retriable_errors = [
797 ProviderError::Timeout,
798 ProviderError::RateLimited,
799 ProviderError::BadGateway,
800 ];
801
802 for error in retriable_errors {
803 assert!(
804 EvmProvider::is_retriable_error(&error),
805 "Error type {:?} should be retriable",
806 error
807 );
808 }
809 }
810
811 #[test]
812 fn test_is_retriable_error_non_retriable_types() {
813 let non_retriable_errors = [
815 ProviderError::InvalidAddress("test".to_string()),
816 ProviderError::NetworkConfiguration("test".to_string()),
817 ProviderError::RequestError {
818 error: "Some error".to_string(),
819 status_code: 400,
820 },
821 ];
822
823 for error in non_retriable_errors {
824 assert!(
825 !EvmProvider::is_retriable_error(&error),
826 "Error type {:?} should NOT be retriable",
827 error
828 );
829 }
830 }
831
832 #[test]
833 fn test_is_retriable_error_message_based_detection() {
834 let retriable_messages = [
836 "Connection timeout occurred",
837 "Network connection reset",
838 "Connection refused",
839 "TIMEOUT error happened",
840 "Connection was reset by peer",
841 ];
842
843 for message in retriable_messages {
844 let error = ProviderError::Other(message.to_string());
845 assert!(
846 EvmProvider::is_retriable_error(&error),
847 "Error with message '{}' should be retriable",
848 message
849 );
850 }
851 }
852
853 #[test]
854 fn test_is_retriable_error_message_based_non_retriable() {
855 let non_retriable_messages = [
857 "Invalid address format",
858 "Bad request parameters",
859 "Authentication failed",
860 "Method not found",
861 "Some other error",
862 ];
863
864 for message in non_retriable_messages {
865 let error = ProviderError::Other(message.to_string());
866 assert!(
867 !EvmProvider::is_retriable_error(&error),
868 "Error with message '{}' should NOT be retriable",
869 message
870 );
871 }
872 }
873
874 #[test]
875 fn test_is_retriable_error_case_insensitive() {
876 let case_variations = [
878 "TIMEOUT",
879 "Timeout",
880 "timeout",
881 "CONNECTION",
882 "Connection",
883 "connection",
884 "RESET",
885 "Reset",
886 "reset",
887 ];
888
889 for message in case_variations {
890 let error = ProviderError::Other(message.to_string());
891 assert!(
892 EvmProvider::is_retriable_error(&error),
893 "Error with message '{}' should be retriable (case insensitive)",
894 message
895 );
896 }
897 }
898
899 #[tokio::test]
900 async fn test_mock_provider_methods() {
901 let mut mock = MockEvmProviderTrait::new();
902
903 mock.expect_get_balance()
904 .with(mockall::predicate::eq(
905 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
906 ))
907 .times(1)
908 .returning(|_| async { Ok(U256::from(100)) }.boxed());
909
910 mock.expect_get_block_number()
911 .times(1)
912 .returning(|| async { Ok(12345) }.boxed());
913
914 mock.expect_get_gas_price()
915 .times(1)
916 .returning(|| async { Ok(20000000000) }.boxed());
917
918 mock.expect_health_check()
919 .times(1)
920 .returning(|| async { Ok(true) }.boxed());
921
922 mock.expect_get_transaction_count()
923 .with(mockall::predicate::eq(
924 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
925 ))
926 .times(1)
927 .returning(|_| async { Ok(42) }.boxed());
928
929 mock.expect_get_fee_history()
930 .with(
931 mockall::predicate::eq(10u64),
932 mockall::predicate::eq(BlockNumberOrTag::Latest),
933 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
934 )
935 .times(1)
936 .returning(|_, _, _| {
937 async {
938 Ok(FeeHistory {
939 oldest_block: 100,
940 base_fee_per_gas: vec![1000],
941 gas_used_ratio: vec![0.5],
942 reward: Some(vec![vec![500]]),
943 base_fee_per_blob_gas: vec![1000],
944 blob_gas_used_ratio: vec![0.5],
945 })
946 }
947 .boxed()
948 });
949
950 let balance = mock
952 .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
953 .await;
954 assert!(balance.is_ok());
955 assert_eq!(balance.unwrap(), U256::from(100));
956
957 let block_number = mock.get_block_number().await;
958 assert!(block_number.is_ok());
959 assert_eq!(block_number.unwrap(), 12345);
960
961 let gas_price = mock.get_gas_price().await;
962 assert!(gas_price.is_ok());
963 assert_eq!(gas_price.unwrap(), 20000000000);
964
965 let health = mock.health_check().await;
966 assert!(health.is_ok());
967 assert!(health.unwrap());
968
969 let count = mock
970 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
971 .await;
972 assert!(count.is_ok());
973 assert_eq!(count.unwrap(), 42);
974
975 let fee_history = mock
976 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
977 .await;
978 assert!(fee_history.is_ok());
979 let fee_history = fee_history.unwrap();
980 assert_eq!(fee_history.oldest_block, 100);
981 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
982 }
983
984 #[tokio::test]
985 async fn test_mock_transaction_operations() {
986 let mut mock = MockEvmProviderTrait::new();
987
988 let tx_data = EvmTransactionData {
990 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
991 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
992 gas_price: Some(1000000000),
993 value: Uint::<256, 4>::from(1000000000),
994 data: Some("0x".to_string()),
995 nonce: Some(1),
996 chain_id: 1,
997 gas_limit: Some(21000),
998 hash: None,
999 signature: None,
1000 speed: None,
1001 max_fee_per_gas: None,
1002 max_priority_fee_per_gas: None,
1003 raw: None,
1004 };
1005
1006 mock.expect_estimate_gas()
1007 .with(mockall::predicate::always())
1008 .times(1)
1009 .returning(|_| async { Ok(21000) }.boxed());
1010
1011 mock.expect_send_raw_transaction()
1013 .with(mockall::predicate::always())
1014 .times(1)
1015 .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
1016
1017 let gas_estimate = mock.estimate_gas(&tx_data).await;
1019 assert!(gas_estimate.is_ok());
1020 assert_eq!(gas_estimate.unwrap(), 21000);
1021
1022 let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
1023 assert!(tx_hash.is_ok());
1024 assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
1025 }
1026
1027 #[test]
1028 fn test_invalid_transaction_request_conversion() {
1029 let tx_data = EvmTransactionData {
1030 from: "invalid-address".to_string(),
1031 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1032 gas_price: Some(1000000000),
1033 value: Uint::<256, 4>::from(1000000000),
1034 data: Some("0x".to_string()),
1035 nonce: Some(1),
1036 chain_id: 1,
1037 gas_limit: Some(21000),
1038 hash: None,
1039 signature: None,
1040 speed: None,
1041 max_fee_per_gas: None,
1042 max_priority_fee_per_gas: None,
1043 raw: None,
1044 };
1045
1046 let result = TransactionRequest::try_from(&tx_data);
1047 assert!(result.is_err());
1048 }
1049
1050 #[tokio::test]
1051 async fn test_mock_additional_methods() {
1052 let mut mock = MockEvmProviderTrait::new();
1053
1054 mock.expect_health_check()
1056 .times(1)
1057 .returning(|| async { Ok(true) }.boxed());
1058
1059 mock.expect_get_transaction_count()
1061 .with(mockall::predicate::eq(
1062 "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
1063 ))
1064 .times(1)
1065 .returning(|_| async { Ok(42) }.boxed());
1066
1067 mock.expect_get_fee_history()
1069 .with(
1070 mockall::predicate::eq(10u64),
1071 mockall::predicate::eq(BlockNumberOrTag::Latest),
1072 mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
1073 )
1074 .times(1)
1075 .returning(|_, _, _| {
1076 async {
1077 Ok(FeeHistory {
1078 oldest_block: 100,
1079 base_fee_per_gas: vec![1000],
1080 gas_used_ratio: vec![0.5],
1081 reward: Some(vec![vec![500]]),
1082 base_fee_per_blob_gas: vec![1000],
1083 blob_gas_used_ratio: vec![0.5],
1084 })
1085 }
1086 .boxed()
1087 });
1088
1089 let health = mock.health_check().await;
1091 assert!(health.is_ok());
1092 assert!(health.unwrap());
1093
1094 let count = mock
1096 .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1097 .await;
1098 assert!(count.is_ok());
1099 assert_eq!(count.unwrap(), 42);
1100
1101 let fee_history = mock
1103 .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1104 .await;
1105 assert!(fee_history.is_ok());
1106 let fee_history = fee_history.unwrap();
1107 assert_eq!(fee_history.oldest_block, 100);
1108 assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1109 }
1110
1111 #[tokio::test]
1112 async fn test_call_contract() {
1113 let mut mock = MockEvmProviderTrait::new();
1114
1115 let tx = TransactionRequest {
1116 from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1117 to: Some(TxKind::Call(
1118 Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1119 )),
1120 input: TransactionInput::from(
1121 hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1122 ),
1123 ..Default::default()
1124 };
1125
1126 mock.expect_call_contract()
1128 .with(mockall::predicate::always())
1129 .times(1)
1130 .returning(|_| {
1131 async {
1132 Ok(Bytes::from(
1133 hex::decode(
1134 "0000000000000000000000000000000000000000000000000000000000000001",
1135 )
1136 .unwrap(),
1137 ))
1138 }
1139 .boxed()
1140 });
1141
1142 let result = mock.call_contract(&tx).await;
1143 assert!(result.is_ok());
1144
1145 let data = result.unwrap();
1146 assert_eq!(
1147 hex::encode(data),
1148 "0000000000000000000000000000000000000000000000000000000000000001"
1149 );
1150 }
1151}