openzeppelin_relayer/services/provider/evm/
mod.rs

1//! EVM Provider implementation for interacting with EVM-compatible blockchain networks.
2//!
3//! This module provides functionality to interact with EVM-based blockchains through RPC calls.
4//! It implements common operations like getting balances, sending transactions, and querying
5//! blockchain state.
6
7use 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;
35
36use super::rpc_selector::RpcSelector;
37use super::{retry_rpc_call, RetryConfig};
38use crate::{
39    models::{
40        BlockResponse, EvmTransactionData, RpcConfig, TransactionError, TransactionReceipt, U256,
41    },
42    services::provider::{is_retriable_error, should_mark_provider_failed},
43};
44
45#[cfg(test)]
46use mockall::automock;
47
48use super::ProviderError;
49
50/// Provider implementation for EVM-compatible blockchain networks.
51///
52/// Wraps an HTTP RPC provider to interact with EVM chains like Ethereum, Polygon, etc.
53#[derive(Clone)]
54pub struct EvmProvider {
55    /// RPC selector for managing and selecting providers
56    selector: RpcSelector,
57    /// Timeout in seconds for new HTTP clients
58    timeout_seconds: u64,
59    /// Configuration for retry behavior
60    retry_config: RetryConfig,
61}
62
63/// Trait defining the interface for EVM blockchain interactions.
64///
65/// This trait provides methods for common blockchain operations like querying balances,
66/// sending transactions, and getting network state.
67#[async_trait]
68#[cfg_attr(test, automock)]
69#[allow(dead_code)]
70pub trait EvmProviderTrait: Send + Sync {
71    /// Gets the balance of an address in the native currency.
72    ///
73    /// # Arguments
74    /// * `address` - The address to query the balance for
75    async fn get_balance(&self, address: &str) -> Result<U256, ProviderError>;
76
77    /// Gets the current block number of the chain.
78    async fn get_block_number(&self) -> Result<u64, ProviderError>;
79
80    /// Estimates the gas required for a transaction.
81    ///
82    /// # Arguments
83    /// * `tx` - The transaction data to estimate gas for
84    async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError>;
85
86    /// Gets the current gas price from the network.
87    async fn get_gas_price(&self) -> Result<u128, ProviderError>;
88
89    /// Sends a transaction to the network.
90    ///
91    /// # Arguments
92    /// * `tx` - The transaction request to send
93    async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError>;
94
95    /// Sends a raw signed transaction to the network.
96    ///
97    /// # Arguments
98    /// * `tx` - The raw transaction bytes to send
99    async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError>;
100
101    /// Performs a health check by attempting to get the latest block number.
102    async fn health_check(&self) -> Result<bool, ProviderError>;
103
104    /// Gets the transaction count (nonce) for an address.
105    ///
106    /// # Arguments
107    /// * `address` - The address to query the transaction count for
108    async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError>;
109
110    /// Gets the fee history for a range of blocks.
111    ///
112    /// # Arguments
113    /// * `block_count` - Number of blocks to get fee history for
114    /// * `newest_block` - The newest block to start from
115    /// * `reward_percentiles` - Percentiles to sample reward data from
116    async fn get_fee_history(
117        &self,
118        block_count: u64,
119        newest_block: BlockNumberOrTag,
120        reward_percentiles: Vec<f64>,
121    ) -> Result<FeeHistory, ProviderError>;
122
123    /// Gets the latest block from the network.
124    async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError>;
125
126    /// Gets a transaction receipt by its hash.
127    ///
128    /// # Arguments
129    /// * `tx_hash` - The transaction hash to query
130    async fn get_transaction_receipt(
131        &self,
132        tx_hash: &str,
133    ) -> Result<Option<TransactionReceipt>, ProviderError>;
134
135    /// Calls a contract function.
136    ///
137    /// # Arguments
138    /// * `tx` - The transaction request to call the contract function
139    async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError>;
140
141    /// Sends a raw JSON-RPC request.
142    ///
143    /// # Arguments
144    /// * `method` - The JSON-RPC method name
145    /// * `params` - The parameters as a JSON value
146    async fn raw_request_dyn(
147        &self,
148        method: &str,
149        params: serde_json::Value,
150    ) -> Result<serde_json::Value, ProviderError>;
151}
152
153impl EvmProvider {
154    /// Creates a new EVM provider instance.
155    ///
156    /// # Arguments
157    /// * `configs` - A vector of RPC configurations (URL and weight)
158    /// * `timeout_seconds` - The timeout duration in seconds (defaults to 30 if None)
159    ///
160    /// # Returns
161    /// * `Result<Self>` - A new provider instance or an error
162    pub fn new(configs: Vec<RpcConfig>, timeout_seconds: u64) -> Result<Self, ProviderError> {
163        if configs.is_empty() {
164            return Err(ProviderError::NetworkConfiguration(
165                "At least one RPC configuration must be provided".to_string(),
166            ));
167        }
168
169        RpcConfig::validate_list(&configs)
170            .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {e}")))?;
171
172        // Create the RPC selector
173        let selector = RpcSelector::new(configs).map_err(|e| {
174            ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
175        })?;
176
177        let retry_config = RetryConfig::from_env();
178
179        Ok(Self {
180            selector,
181            timeout_seconds,
182            retry_config,
183        })
184    }
185
186    /// Initialize a provider for a given URL
187    fn initialize_provider(&self, url: &str) -> Result<EvmProviderType, ProviderError> {
188        let rpc_url = url
189            .parse()
190            .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL format: {e}")))?;
191
192        // Using use_rustls_tls() forces the use of rustls instead of native-tls to support TLS 1.3
193        let client = ReqwestClientBuilder::new()
194            .timeout(Duration::from_secs(self.timeout_seconds))
195            .use_rustls_tls()
196            .build()
197            .map_err(|e| ProviderError::Other(format!("Failed to build HTTP client: {e}")))?;
198
199        let mut transport = Http::new(rpc_url);
200        transport.set_client(client);
201
202        let is_local = transport.guess_local();
203        let client = ClientBuilder::default().transport(transport, is_local);
204
205        let provider = ProviderBuilder::new()
206            .network::<AnyNetwork>()
207            .connect_client(client);
208
209        Ok(provider)
210    }
211
212    /// Helper method to retry RPC calls with exponential backoff
213    ///
214    /// Uses the generic retry_rpc_call utility to handle retries and provider failover
215    async fn retry_rpc_call<T, F, Fut>(
216        &self,
217        operation_name: &str,
218        operation: F,
219    ) -> Result<T, ProviderError>
220    where
221        F: Fn(EvmProviderType) -> Fut,
222        Fut: std::future::Future<Output = Result<T, ProviderError>>,
223    {
224        // Classify which errors should be retried
225
226        tracing::debug!(
227            "Starting RPC operation '{}' with timeout: {}s",
228            operation_name,
229            self.timeout_seconds
230        );
231
232        retry_rpc_call(
233            &self.selector,
234            operation_name,
235            is_retriable_error,
236            should_mark_provider_failed,
237            |url| match self.initialize_provider(url) {
238                Ok(provider) => Ok(provider),
239                Err(e) => Err(e),
240            },
241            operation,
242            Some(self.retry_config.clone()),
243        )
244        .await
245    }
246}
247
248impl AsRef<EvmProvider> for EvmProvider {
249    fn as_ref(&self) -> &EvmProvider {
250        self
251    }
252}
253
254#[async_trait]
255impl EvmProviderTrait for EvmProvider {
256    async fn get_balance(&self, address: &str) -> Result<U256, ProviderError> {
257        let parsed_address = address
258            .parse::<alloy::primitives::Address>()
259            .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
260
261        self.retry_rpc_call("get_balance", move |provider| async move {
262            provider
263                .get_balance(parsed_address)
264                .await
265                .map_err(ProviderError::from)
266        })
267        .await
268    }
269
270    async fn get_block_number(&self) -> Result<u64, ProviderError> {
271        self.retry_rpc_call("get_block_number", |provider| async move {
272            provider
273                .get_block_number()
274                .await
275                .map_err(ProviderError::from)
276        })
277        .await
278    }
279
280    async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError> {
281        let transaction_request = TransactionRequest::try_from(tx)
282            .map_err(|e| ProviderError::Other(format!("Failed to convert transaction: {e}")))?;
283
284        self.retry_rpc_call("estimate_gas", move |provider| {
285            let tx_req = transaction_request.clone();
286            async move {
287                provider
288                    .estimate_gas(tx_req.into())
289                    .await
290                    .map_err(ProviderError::from)
291            }
292        })
293        .await
294    }
295
296    async fn get_gas_price(&self) -> Result<u128, ProviderError> {
297        self.retry_rpc_call("get_gas_price", |provider| async move {
298            provider.get_gas_price().await.map_err(ProviderError::from)
299        })
300        .await
301    }
302
303    async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError> {
304        let pending_tx = self
305            .retry_rpc_call("send_transaction", move |provider| {
306                let tx_req = tx.clone();
307                async move {
308                    provider
309                        .send_transaction(tx_req.into())
310                        .await
311                        .map_err(ProviderError::from)
312                }
313            })
314            .await?;
315
316        let tx_hash = pending_tx.tx_hash().to_string();
317        Ok(tx_hash)
318    }
319
320    async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError> {
321        let pending_tx = self
322            .retry_rpc_call("send_raw_transaction", move |provider| {
323                let tx_data = tx.to_vec();
324                async move {
325                    provider
326                        .send_raw_transaction(&tx_data)
327                        .await
328                        .map_err(ProviderError::from)
329                }
330            })
331            .await?;
332
333        let tx_hash = pending_tx.tx_hash().to_string();
334        Ok(tx_hash)
335    }
336
337    async fn health_check(&self) -> Result<bool, ProviderError> {
338        match self.get_block_number().await {
339            Ok(_) => Ok(true),
340            Err(e) => Err(e),
341        }
342    }
343
344    async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError> {
345        let parsed_address = address
346            .parse::<alloy::primitives::Address>()
347            .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
348
349        self.retry_rpc_call("get_transaction_count", move |provider| async move {
350            provider
351                .get_transaction_count(parsed_address)
352                .await
353                .map_err(ProviderError::from)
354        })
355        .await
356    }
357
358    async fn get_fee_history(
359        &self,
360        block_count: u64,
361        newest_block: BlockNumberOrTag,
362        reward_percentiles: Vec<f64>,
363    ) -> Result<FeeHistory, ProviderError> {
364        self.retry_rpc_call("get_fee_history", move |provider| {
365            let reward_percentiles_clone = reward_percentiles.clone();
366            async move {
367                provider
368                    .get_fee_history(block_count, newest_block, &reward_percentiles_clone)
369                    .await
370                    .map_err(ProviderError::from)
371            }
372        })
373        .await
374    }
375
376    async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError> {
377        let block_result = self
378            .retry_rpc_call("get_block_by_number", |provider| async move {
379                provider
380                    .get_block_by_number(BlockNumberOrTag::Latest)
381                    .await
382                    .map_err(ProviderError::from)
383            })
384            .await?;
385
386        match block_result {
387            Some(block) => Ok(block),
388            None => Err(ProviderError::Other("Block not found".to_string())),
389        }
390    }
391
392    async fn get_transaction_receipt(
393        &self,
394        tx_hash: &str,
395    ) -> Result<Option<TransactionReceipt>, ProviderError> {
396        let parsed_tx_hash = tx_hash
397            .parse::<alloy::primitives::TxHash>()
398            .map_err(|e| ProviderError::Other(format!("Invalid transaction hash: {e}")))?;
399
400        self.retry_rpc_call("get_transaction_receipt", move |provider| async move {
401            provider
402                .get_transaction_receipt(parsed_tx_hash)
403                .await
404                .map_err(ProviderError::from)
405        })
406        .await
407    }
408
409    async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError> {
410        self.retry_rpc_call("call_contract", move |provider| {
411            let tx_req = tx.clone();
412            async move {
413                provider
414                    .call(tx_req.into())
415                    .await
416                    .map_err(ProviderError::from)
417            }
418        })
419        .await
420    }
421
422    async fn raw_request_dyn(
423        &self,
424        method: &str,
425        params: serde_json::Value,
426    ) -> Result<serde_json::Value, ProviderError> {
427        self.retry_rpc_call("raw_request_dyn", move |provider| {
428            let params_clone = params.clone();
429            async move {
430                // Convert params to RawValue and use Cow for method
431                let params_raw = serde_json::value::to_raw_value(&params_clone).map_err(|e| {
432                    ProviderError::Other(format!("Failed to serialize params: {e}"))
433                })?;
434
435                let result = provider
436                    .raw_request_dyn(std::borrow::Cow::Owned(method.to_string()), &params_raw)
437                    .await
438                    .map_err(ProviderError::from)?;
439
440                // Convert RawValue back to Value
441                serde_json::from_str(result.get())
442                    .map_err(|e| ProviderError::Other(format!("Failed to deserialize result: {e}")))
443            }
444        })
445        .await
446    }
447}
448
449impl TryFrom<&EvmTransactionData> for TransactionRequest {
450    type Error = TransactionError;
451    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
452        Ok(TransactionRequest {
453            from: Some(tx.from.clone().parse().map_err(|_| {
454                TransactionError::InvalidType("Invalid address format".to_string())
455            })?),
456            to: Some(TxKind::Call(
457                tx.to
458                    .clone()
459                    .unwrap_or("".to_string())
460                    .parse()
461                    .map_err(|_| {
462                        TransactionError::InvalidType("Invalid address format".to_string())
463                    })?,
464            )),
465            gas_price: tx
466                .gas_price
467                .map(|gp| {
468                    Uint::<256, 4>::from(gp)
469                        .try_into()
470                        .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
471                })
472                .transpose()?,
473            value: Some(Uint::<256, 4>::from(tx.value)),
474            input: TransactionInput::from(tx.data_to_bytes()?),
475            nonce: tx
476                .nonce
477                .map(|n| {
478                    Uint::<256, 4>::from(n)
479                        .try_into()
480                        .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
481                })
482                .transpose()?,
483            chain_id: Some(tx.chain_id),
484            max_fee_per_gas: tx
485                .max_fee_per_gas
486                .map(|mfpg| {
487                    Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
488                        TransactionError::InvalidType("Invalid max fee per gas".to_string())
489                    })
490                })
491                .transpose()?,
492            max_priority_fee_per_gas: tx
493                .max_priority_fee_per_gas
494                .map(|mpfpg| {
495                    Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
496                        TransactionError::InvalidType(
497                            "Invalid max priority fee per gas".to_string(),
498                        )
499                    })
500                })
501                .transpose()?,
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    // Helper function to set up the test environment
546    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        // Create a reqwest timeout error
554        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        // Create an address parse error
584        let err = "invalid-address".parse::<Address>().unwrap_err();
585        // Map the error manually using the same approach as in our From implementation
586        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        // Test with invalid URL
601        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        // Test with valid URL and timeout
610        let provider = EvmProvider::new(
611            vec![RpcConfig::new("http://localhost:8545".to_string())],
612            30,
613        );
614        assert!(provider.is_ok());
615
616        // Test with invalid URL
617        let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
618        assert!(provider.is_err());
619
620        // Test with zero timeout
621        let provider =
622            EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
623        assert!(provider.is_ok());
624
625        // Test with large timeout
626        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: Some(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    #[tokio::test]
664    async fn test_mock_provider_methods() {
665        let mut mock = MockEvmProviderTrait::new();
666
667        mock.expect_get_balance()
668            .with(mockall::predicate::eq(
669                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
670            ))
671            .times(1)
672            .returning(|_| async { Ok(U256::from(100)) }.boxed());
673
674        mock.expect_get_block_number()
675            .times(1)
676            .returning(|| async { Ok(12345) }.boxed());
677
678        mock.expect_get_gas_price()
679            .times(1)
680            .returning(|| async { Ok(20000000000) }.boxed());
681
682        mock.expect_health_check()
683            .times(1)
684            .returning(|| async { Ok(true) }.boxed());
685
686        mock.expect_get_transaction_count()
687            .with(mockall::predicate::eq(
688                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
689            ))
690            .times(1)
691            .returning(|_| async { Ok(42) }.boxed());
692
693        mock.expect_get_fee_history()
694            .with(
695                mockall::predicate::eq(10u64),
696                mockall::predicate::eq(BlockNumberOrTag::Latest),
697                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
698            )
699            .times(1)
700            .returning(|_, _, _| {
701                async {
702                    Ok(FeeHistory {
703                        oldest_block: 100,
704                        base_fee_per_gas: vec![1000],
705                        gas_used_ratio: vec![0.5],
706                        reward: Some(vec![vec![500]]),
707                        base_fee_per_blob_gas: vec![1000],
708                        blob_gas_used_ratio: vec![0.5],
709                    })
710                }
711                .boxed()
712            });
713
714        // Test all methods
715        let balance = mock
716            .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
717            .await;
718        assert!(balance.is_ok());
719        assert_eq!(balance.unwrap(), U256::from(100));
720
721        let block_number = mock.get_block_number().await;
722        assert!(block_number.is_ok());
723        assert_eq!(block_number.unwrap(), 12345);
724
725        let gas_price = mock.get_gas_price().await;
726        assert!(gas_price.is_ok());
727        assert_eq!(gas_price.unwrap(), 20000000000);
728
729        let health = mock.health_check().await;
730        assert!(health.is_ok());
731        assert!(health.unwrap());
732
733        let count = mock
734            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
735            .await;
736        assert!(count.is_ok());
737        assert_eq!(count.unwrap(), 42);
738
739        let fee_history = mock
740            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
741            .await;
742        assert!(fee_history.is_ok());
743        let fee_history = fee_history.unwrap();
744        assert_eq!(fee_history.oldest_block, 100);
745        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
746    }
747
748    #[tokio::test]
749    async fn test_mock_transaction_operations() {
750        let mut mock = MockEvmProviderTrait::new();
751
752        // Setup mock for estimate_gas
753        let tx_data = EvmTransactionData {
754            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
755            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
756            gas_price: Some(1000000000),
757            value: Uint::<256, 4>::from(1000000000),
758            data: Some("0x".to_string()),
759            nonce: Some(1),
760            chain_id: 1,
761            gas_limit: Some(21000),
762            hash: None,
763            signature: None,
764            speed: None,
765            max_fee_per_gas: None,
766            max_priority_fee_per_gas: None,
767            raw: None,
768        };
769
770        mock.expect_estimate_gas()
771            .with(mockall::predicate::always())
772            .times(1)
773            .returning(|_| async { Ok(21000) }.boxed());
774
775        // Setup mock for send_raw_transaction
776        mock.expect_send_raw_transaction()
777            .with(mockall::predicate::always())
778            .times(1)
779            .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
780
781        // Test the mocked methods
782        let gas_estimate = mock.estimate_gas(&tx_data).await;
783        assert!(gas_estimate.is_ok());
784        assert_eq!(gas_estimate.unwrap(), 21000);
785
786        let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
787        assert!(tx_hash.is_ok());
788        assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
789    }
790
791    #[test]
792    fn test_invalid_transaction_request_conversion() {
793        let tx_data = EvmTransactionData {
794            from: "invalid-address".to_string(),
795            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
796            gas_price: Some(1000000000),
797            value: Uint::<256, 4>::from(1000000000),
798            data: Some("0x".to_string()),
799            nonce: Some(1),
800            chain_id: 1,
801            gas_limit: Some(21000),
802            hash: None,
803            signature: None,
804            speed: None,
805            max_fee_per_gas: None,
806            max_priority_fee_per_gas: None,
807            raw: None,
808        };
809
810        let result = TransactionRequest::try_from(&tx_data);
811        assert!(result.is_err());
812    }
813
814    #[tokio::test]
815    async fn test_mock_additional_methods() {
816        let mut mock = MockEvmProviderTrait::new();
817
818        // Setup mock for health_check
819        mock.expect_health_check()
820            .times(1)
821            .returning(|| async { Ok(true) }.boxed());
822
823        // Setup mock for get_transaction_count
824        mock.expect_get_transaction_count()
825            .with(mockall::predicate::eq(
826                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
827            ))
828            .times(1)
829            .returning(|_| async { Ok(42) }.boxed());
830
831        // Setup mock for get_fee_history
832        mock.expect_get_fee_history()
833            .with(
834                mockall::predicate::eq(10u64),
835                mockall::predicate::eq(BlockNumberOrTag::Latest),
836                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
837            )
838            .times(1)
839            .returning(|_, _, _| {
840                async {
841                    Ok(FeeHistory {
842                        oldest_block: 100,
843                        base_fee_per_gas: vec![1000],
844                        gas_used_ratio: vec![0.5],
845                        reward: Some(vec![vec![500]]),
846                        base_fee_per_blob_gas: vec![1000],
847                        blob_gas_used_ratio: vec![0.5],
848                    })
849                }
850                .boxed()
851            });
852
853        // Test health check
854        let health = mock.health_check().await;
855        assert!(health.is_ok());
856        assert!(health.unwrap());
857
858        // Test get_transaction_count
859        let count = mock
860            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
861            .await;
862        assert!(count.is_ok());
863        assert_eq!(count.unwrap(), 42);
864
865        // Test get_fee_history
866        let fee_history = mock
867            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
868            .await;
869        assert!(fee_history.is_ok());
870        let fee_history = fee_history.unwrap();
871        assert_eq!(fee_history.oldest_block, 100);
872        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
873    }
874
875    #[test]
876    fn test_is_retriable_error_json_rpc_retriable_codes() {
877        // Retriable JSON-RPC error codes per EIP-1474
878        let retriable_codes = vec![
879            (-32002, "Resource unavailable"),
880            (-32005, "Limit exceeded"),
881            (-32603, "Internal error"),
882        ];
883
884        for (code, message) in retriable_codes {
885            let error = ProviderError::RpcErrorCode {
886                code,
887                message: message.to_string(),
888            };
889            assert!(
890                is_retriable_error(&error),
891                "Error code {} should be retriable",
892                code
893            );
894        }
895    }
896
897    #[test]
898    fn test_is_retriable_error_json_rpc_non_retriable_codes() {
899        // Non-retriable JSON-RPC error codes per EIP-1474
900        let non_retriable_codes = vec![
901            (-32000, "insufficient funds"),
902            (-32000, "execution reverted"),
903            (-32000, "already known"),
904            (-32000, "nonce too low"),
905            (-32000, "invalid sender"),
906            (-32001, "Resource not found"),
907            (-32003, "Transaction rejected"),
908            (-32004, "Method not supported"),
909            (-32700, "Parse error"),
910            (-32600, "Invalid request"),
911            (-32601, "Method not found"),
912            (-32602, "Invalid params"),
913        ];
914
915        for (code, message) in non_retriable_codes {
916            let error = ProviderError::RpcErrorCode {
917                code,
918                message: message.to_string(),
919            };
920            assert!(
921                !is_retriable_error(&error),
922                "Error code {} with message '{}' should NOT be retriable",
923                code,
924                message
925            );
926        }
927    }
928
929    #[test]
930    fn test_is_retriable_error_json_rpc_32000_specific_cases() {
931        // Test specific -32000 error messages that users commonly encounter
932        // -32000 is a catch-all for client errors and should NOT be retriable
933        let test_cases = vec![
934            (
935                "tx already exists in cache",
936                false,
937                "Transaction already in mempool",
938            ),
939            ("already known", false, "Duplicate transaction submission"),
940            (
941                "insufficient funds for gas * price + value",
942                false,
943                "User needs more funds",
944            ),
945            ("execution reverted", false, "Smart contract rejected"),
946            ("nonce too low", false, "Transaction already processed"),
947            ("invalid sender", false, "Configuration issue"),
948            ("gas required exceeds allowance", false, "Gas limit too low"),
949            (
950                "replacement transaction underpriced",
951                false,
952                "Need higher gas price",
953            ),
954        ];
955
956        for (message, should_retry, description) in test_cases {
957            let error = ProviderError::RpcErrorCode {
958                code: -32000,
959                message: message.to_string(),
960            };
961            assert_eq!(
962                is_retriable_error(&error),
963                should_retry,
964                "{}: -32000 with '{}' should{} be retriable",
965                description,
966                message,
967                if should_retry { "" } else { " NOT" }
968            );
969        }
970    }
971
972    #[tokio::test]
973    async fn test_call_contract() {
974        let mut mock = MockEvmProviderTrait::new();
975
976        let tx = TransactionRequest {
977            from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
978            to: Some(TxKind::Call(
979                Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
980            )),
981            input: TransactionInput::from(
982                hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
983            ),
984            ..Default::default()
985        };
986
987        // Setup mock for call_contract
988        mock.expect_call_contract()
989            .with(mockall::predicate::always())
990            .times(1)
991            .returning(|_| {
992                async {
993                    Ok(Bytes::from(
994                        hex::decode(
995                            "0000000000000000000000000000000000000000000000000000000000000001",
996                        )
997                        .unwrap(),
998                    ))
999                }
1000                .boxed()
1001            });
1002
1003        let result = mock.call_contract(&tx).await;
1004        assert!(result.is_ok());
1005
1006        let data = result.unwrap();
1007        assert_eq!(
1008            hex::encode(data),
1009            "0000000000000000000000000000000000000000000000000000000000000001"
1010        );
1011    }
1012}