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 alloy::{
8    network::AnyNetwork,
9    primitives::{Bytes, TxKind, Uint},
10    providers::{
11        fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller},
12        Identity, Provider, ProviderBuilder, RootProvider,
13    },
14    rpc::{
15        client::ClientBuilder,
16        types::{BlockNumberOrTag, FeeHistory, TransactionInput, TransactionRequest},
17    },
18    transports::http::Http,
19};
20
21type EvmProviderType = FillProvider<
22    JoinFill<
23        Identity,
24        JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
25    >,
26    RootProvider<AnyNetwork>,
27    AnyNetwork,
28>;
29use async_trait::async_trait;
30use eyre::Result;
31use serde_json;
32use tracing::debug;
33
34use super::rpc_selector::RpcSelector;
35use super::{retry_rpc_call, ProviderConfig, RetryConfig};
36use crate::{
37    models::{
38        BlockResponse, EvmTransactionData, RpcConfig, TransactionError, TransactionReceipt, U256,
39    },
40    services::provider::{is_retriable_error, should_mark_provider_failed},
41    utils::mask_url,
42};
43
44use crate::utils::validate_safe_url;
45
46#[cfg(test)]
47use mockall::automock;
48
49use super::ProviderError;
50
51/// Provider implementation for EVM-compatible blockchain networks.
52///
53/// Wraps an HTTP RPC provider to interact with EVM chains like Ethereum, Polygon, etc.
54#[derive(Clone)]
55pub struct EvmProvider {
56    /// RPC selector for managing and selecting providers
57    selector: RpcSelector,
58    /// Timeout in seconds for new HTTP clients
59    timeout_seconds: u64,
60    /// Configuration for retry behavior
61    retry_config: RetryConfig,
62}
63
64/// Trait defining the interface for EVM blockchain interactions.
65///
66/// This trait provides methods for common blockchain operations like querying balances,
67/// sending transactions, and getting network state.
68#[async_trait]
69#[cfg_attr(test, automock)]
70#[allow(dead_code)]
71pub trait EvmProviderTrait: Send + Sync {
72    fn get_configs(&self) -> Vec<RpcConfig>;
73    /// Gets the balance of an address in the native currency.
74    ///
75    /// # Arguments
76    /// * `address` - The address to query the balance for
77    async fn get_balance(&self, address: &str) -> Result<U256, ProviderError>;
78
79    /// Gets the current block number of the chain.
80    async fn get_block_number(&self) -> Result<u64, ProviderError>;
81
82    /// Estimates the gas required for a transaction.
83    ///
84    /// # Arguments
85    /// * `tx` - The transaction data to estimate gas for
86    async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError>;
87
88    /// Gets the current gas price from the network.
89    async fn get_gas_price(&self) -> Result<u128, ProviderError>;
90
91    /// Sends a transaction to the network.
92    ///
93    /// # Arguments
94    /// * `tx` - The transaction request to send
95    async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError>;
96
97    /// Sends a raw signed transaction to the network.
98    ///
99    /// # Arguments
100    /// * `tx` - The raw transaction bytes to send
101    async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError>;
102
103    /// Performs a health check by attempting to get the latest block number.
104    async fn health_check(&self) -> Result<bool, ProviderError>;
105
106    /// Gets the transaction count (nonce) for an address.
107    ///
108    /// # Arguments
109    /// * `address` - The address to query the transaction count for
110    async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError>;
111
112    /// Gets the fee history for a range of blocks.
113    ///
114    /// # Arguments
115    /// * `block_count` - Number of blocks to get fee history for
116    /// * `newest_block` - The newest block to start from
117    /// * `reward_percentiles` - Percentiles to sample reward data from
118    async fn get_fee_history(
119        &self,
120        block_count: u64,
121        newest_block: BlockNumberOrTag,
122        reward_percentiles: Vec<f64>,
123    ) -> Result<FeeHistory, ProviderError>;
124
125    /// Gets the latest block from the network.
126    async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError>;
127
128    /// Gets a transaction receipt by its hash.
129    ///
130    /// # Arguments
131    /// * `tx_hash` - The transaction hash to query
132    async fn get_transaction_receipt(
133        &self,
134        tx_hash: &str,
135    ) -> Result<Option<TransactionReceipt>, ProviderError>;
136
137    /// Calls a contract function.
138    ///
139    /// # Arguments
140    /// * `tx` - The transaction request to call the contract function
141    async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError>;
142
143    /// Sends a raw JSON-RPC request.
144    ///
145    /// # Arguments
146    /// * `method` - The JSON-RPC method name
147    /// * `params` - The parameters as a JSON value
148    async fn raw_request_dyn(
149        &self,
150        method: &str,
151        params: serde_json::Value,
152    ) -> Result<serde_json::Value, ProviderError>;
153}
154
155impl EvmProvider {
156    /// Creates a new EVM provider instance.
157    ///
158    /// # Arguments
159    /// * `config` - Provider configuration containing RPC configs, timeout, and failure handling settings
160    ///
161    /// # Returns
162    /// * `Result<Self>` - A new provider instance or an error
163    pub fn new(config: ProviderConfig) -> Result<Self, ProviderError> {
164        if config.rpc_configs.is_empty() {
165            return Err(ProviderError::NetworkConfiguration(
166                "At least one RPC configuration must be provided".to_string(),
167            ));
168        }
169
170        RpcConfig::validate_list(&config.rpc_configs)
171            .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {e}")))?;
172
173        // Create the RPC selector
174        let selector = RpcSelector::new(
175            config.rpc_configs,
176            config.failure_threshold,
177            config.pause_duration_secs,
178            config.failure_expiration_secs,
179        )
180        .map_err(|e| {
181            ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
182        })?;
183
184        let retry_config = RetryConfig::from_env();
185
186        Ok(Self {
187            selector,
188            timeout_seconds: config.timeout_seconds,
189            retry_config,
190        })
191    }
192
193    /// Gets the current RPC configurations.
194    ///
195    /// # Returns
196    /// * `Vec<RpcConfig>` - The current configurations
197    pub fn get_configs(&self) -> Vec<RpcConfig> {
198        self.selector.get_configs()
199    }
200
201    /// Initialize a provider for a given URL
202    fn initialize_provider(&self, url: &str) -> Result<EvmProviderType, ProviderError> {
203        let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
204        let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
205        validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
206            ProviderError::NetworkConfiguration(format!("RPC URL security validation failed: {e}"))
207        })?;
208
209        debug!("Initializing provider for URL: {}", mask_url(url));
210        let rpc_url = url
211            .parse()
212            .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL format: {e}")))?;
213
214        let client = super::build_rpc_http_client_with_timeout(std::time::Duration::from_secs(
215            self.timeout_seconds,
216        ))?;
217
218        let mut transport = Http::new(rpc_url);
219        transport.set_client(client);
220
221        let is_local = transport.guess_local();
222        let client = ClientBuilder::default().transport(transport, is_local);
223
224        let provider = ProviderBuilder::new()
225            .network::<AnyNetwork>()
226            .connect_client(client);
227
228        Ok(provider)
229    }
230
231    /// Helper method to retry RPC calls with exponential backoff
232    ///
233    /// Uses the generic retry_rpc_call utility to handle retries and provider failover
234    async fn retry_rpc_call<T, F, Fut>(
235        &self,
236        operation_name: &str,
237        operation: F,
238    ) -> Result<T, ProviderError>
239    where
240        F: Fn(EvmProviderType) -> Fut,
241        Fut: std::future::Future<Output = Result<T, ProviderError>>,
242    {
243        // Classify which errors should be retried
244
245        tracing::debug!(
246            "Starting RPC operation '{}' with timeout: {}s",
247            operation_name,
248            self.timeout_seconds
249        );
250
251        retry_rpc_call(
252            &self.selector,
253            operation_name,
254            is_retriable_error,
255            should_mark_provider_failed,
256            |url| match self.initialize_provider(url) {
257                Ok(provider) => Ok(provider),
258                Err(e) => Err(e),
259            },
260            operation,
261            Some(self.retry_config.clone()),
262        )
263        .await
264    }
265}
266
267impl AsRef<EvmProvider> for EvmProvider {
268    fn as_ref(&self) -> &EvmProvider {
269        self
270    }
271}
272
273#[async_trait]
274impl EvmProviderTrait for EvmProvider {
275    fn get_configs(&self) -> Vec<RpcConfig> {
276        self.get_configs()
277    }
278
279    async fn get_balance(&self, address: &str) -> Result<U256, ProviderError> {
280        let parsed_address = address
281            .parse::<alloy::primitives::Address>()
282            .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
283
284        self.retry_rpc_call("get_balance", move |provider| async move {
285            provider
286                .get_balance(parsed_address)
287                .await
288                .map_err(ProviderError::from)
289        })
290        .await
291    }
292
293    async fn get_block_number(&self) -> Result<u64, ProviderError> {
294        self.retry_rpc_call("get_block_number", |provider| async move {
295            provider
296                .get_block_number()
297                .await
298                .map_err(ProviderError::from)
299        })
300        .await
301    }
302
303    async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64, ProviderError> {
304        let transaction_request = TransactionRequest::try_from(tx)
305            .map_err(|e| ProviderError::Other(format!("Failed to convert transaction: {e}")))?;
306
307        self.retry_rpc_call("estimate_gas", move |provider| {
308            let tx_req = transaction_request.clone();
309            async move {
310                provider
311                    .estimate_gas(tx_req.into())
312                    .await
313                    .map_err(ProviderError::from)
314            }
315        })
316        .await
317    }
318
319    async fn get_gas_price(&self) -> Result<u128, ProviderError> {
320        self.retry_rpc_call("get_gas_price", |provider| async move {
321            provider.get_gas_price().await.map_err(ProviderError::from)
322        })
323        .await
324    }
325
326    async fn send_transaction(&self, tx: TransactionRequest) -> Result<String, ProviderError> {
327        let pending_tx = self
328            .retry_rpc_call("send_transaction", move |provider| {
329                let tx_req = tx.clone();
330                async move {
331                    provider
332                        .send_transaction(tx_req.into())
333                        .await
334                        .map_err(ProviderError::from)
335                }
336            })
337            .await?;
338
339        let tx_hash = pending_tx.tx_hash().to_string();
340        Ok(tx_hash)
341    }
342
343    async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String, ProviderError> {
344        let pending_tx = self
345            .retry_rpc_call("send_raw_transaction", move |provider| {
346                let tx_data = tx.to_vec();
347                async move {
348                    provider
349                        .send_raw_transaction(&tx_data)
350                        .await
351                        .map_err(ProviderError::from)
352                }
353            })
354            .await?;
355
356        let tx_hash = pending_tx.tx_hash().to_string();
357        Ok(tx_hash)
358    }
359
360    async fn health_check(&self) -> Result<bool, ProviderError> {
361        match self.get_block_number().await {
362            Ok(_) => Ok(true),
363            Err(e) => Err(e),
364        }
365    }
366
367    async fn get_transaction_count(&self, address: &str) -> Result<u64, ProviderError> {
368        let parsed_address = address
369            .parse::<alloy::primitives::Address>()
370            .map_err(|e| ProviderError::InvalidAddress(e.to_string()))?;
371
372        self.retry_rpc_call("get_transaction_count", move |provider| async move {
373            provider
374                .get_transaction_count(parsed_address)
375                .await
376                .map_err(ProviderError::from)
377        })
378        .await
379    }
380
381    async fn get_fee_history(
382        &self,
383        block_count: u64,
384        newest_block: BlockNumberOrTag,
385        reward_percentiles: Vec<f64>,
386    ) -> Result<FeeHistory, ProviderError> {
387        self.retry_rpc_call("get_fee_history", move |provider| {
388            let reward_percentiles_clone = reward_percentiles.clone();
389            async move {
390                provider
391                    .get_fee_history(block_count, newest_block, &reward_percentiles_clone)
392                    .await
393                    .map_err(ProviderError::from)
394            }
395        })
396        .await
397    }
398
399    async fn get_block_by_number(&self) -> Result<BlockResponse, ProviderError> {
400        let block_result = self
401            .retry_rpc_call("get_block_by_number", |provider| async move {
402                provider
403                    .get_block_by_number(BlockNumberOrTag::Latest)
404                    .await
405                    .map_err(ProviderError::from)
406            })
407            .await?;
408
409        match block_result {
410            Some(block) => Ok(block),
411            None => Err(ProviderError::Other("Block not found".to_string())),
412        }
413    }
414
415    async fn get_transaction_receipt(
416        &self,
417        tx_hash: &str,
418    ) -> Result<Option<TransactionReceipt>, ProviderError> {
419        let parsed_tx_hash = tx_hash
420            .parse::<alloy::primitives::TxHash>()
421            .map_err(|e| ProviderError::Other(format!("Invalid transaction hash: {e}")))?;
422
423        self.retry_rpc_call("get_transaction_receipt", move |provider| async move {
424            provider
425                .get_transaction_receipt(parsed_tx_hash)
426                .await
427                .map_err(ProviderError::from)
428        })
429        .await
430    }
431
432    async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes, ProviderError> {
433        self.retry_rpc_call("call_contract", move |provider| {
434            let tx_req = tx.clone();
435            async move {
436                provider
437                    .call(tx_req.into())
438                    .await
439                    .map_err(ProviderError::from)
440            }
441        })
442        .await
443    }
444
445    async fn raw_request_dyn(
446        &self,
447        method: &str,
448        params: serde_json::Value,
449    ) -> Result<serde_json::Value, ProviderError> {
450        self.retry_rpc_call("raw_request_dyn", move |provider| {
451            let params_clone = params.clone();
452            async move {
453                // Convert params to RawValue and use Cow for method
454                let params_raw = serde_json::value::to_raw_value(&params_clone).map_err(|e| {
455                    ProviderError::Other(format!("Failed to serialize params: {e}"))
456                })?;
457
458                let result = provider
459                    .raw_request_dyn(std::borrow::Cow::Owned(method.to_string()), &params_raw)
460                    .await
461                    .map_err(ProviderError::from)?;
462
463                // Convert RawValue back to Value
464                serde_json::from_str(result.get())
465                    .map_err(|e| ProviderError::Other(format!("Failed to deserialize result: {e}")))
466            }
467        })
468        .await
469    }
470}
471
472impl TryFrom<&EvmTransactionData> for TransactionRequest {
473    type Error = TransactionError;
474    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
475        let to = match tx.to.as_ref() {
476            Some(address) => TxKind::Call(address.parse().map_err(|_| {
477                TransactionError::InvalidType("Invalid address format".to_string())
478            })?),
479            None => TxKind::Create,
480        };
481
482        Ok(TransactionRequest {
483            from: Some(tx.from.clone().parse().map_err(|_| {
484                TransactionError::InvalidType("Invalid address format".to_string())
485            })?),
486            to: Some(to),
487            gas_price: tx
488                .gas_price
489                .map(|gp| {
490                    Uint::<256, 4>::from(gp)
491                        .try_into()
492                        .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
493                })
494                .transpose()?,
495            value: Some(Uint::<256, 4>::from(tx.value)),
496            input: TransactionInput::from(tx.data_to_bytes()?),
497            nonce: tx
498                .nonce
499                .map(|n| {
500                    Uint::<256, 4>::from(n)
501                        .try_into()
502                        .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
503                })
504                .transpose()?,
505            chain_id: Some(tx.chain_id),
506            max_fee_per_gas: tx
507                .max_fee_per_gas
508                .map(|mfpg| {
509                    Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
510                        TransactionError::InvalidType("Invalid max fee per gas".to_string())
511                    })
512                })
513                .transpose()?,
514            max_priority_fee_per_gas: tx
515                .max_priority_fee_per_gas
516                .map(|mpfpg| {
517                    Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
518                        TransactionError::InvalidType(
519                            "Invalid max priority fee per gas".to_string(),
520                        )
521                    })
522                })
523                .transpose()?,
524            ..Default::default()
525        })
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    use alloy::primitives::Address;
533    use futures::FutureExt;
534    use lazy_static::lazy_static;
535    use std::str::FromStr;
536    use std::sync::Mutex;
537    use std::time::Duration;
538
539    lazy_static! {
540        static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
541    }
542
543    struct EvmTestEnvGuard {
544        _mutex_guard: std::sync::MutexGuard<'static, ()>,
545    }
546
547    impl EvmTestEnvGuard {
548        fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
549            std::env::set_var(
550                "API_KEY",
551                "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
552            );
553            std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
554
555            Self {
556                _mutex_guard: mutex_guard,
557            }
558        }
559    }
560
561    impl Drop for EvmTestEnvGuard {
562        fn drop(&mut self) {
563            std::env::remove_var("API_KEY");
564            std::env::remove_var("REDIS_URL");
565        }
566    }
567
568    // Helper function to set up the test environment
569    fn setup_test_env() -> EvmTestEnvGuard {
570        let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
571        EvmTestEnvGuard::new(guard)
572    }
573
574    #[tokio::test]
575    async fn test_reqwest_error_conversion() {
576        // Create a reqwest timeout error
577        let client = reqwest::Client::new();
578        let result = client
579            .get("https://www.openzeppelin.com/")
580            .timeout(Duration::from_millis(1))
581            .send()
582            .await;
583
584        assert!(
585            result.is_err(),
586            "Expected the send operation to result in an error."
587        );
588        let err = result.unwrap_err();
589
590        assert!(
591            err.is_timeout(),
592            "The reqwest error should be a timeout. Actual error: {err:?}"
593        );
594
595        let provider_error = ProviderError::from(err);
596        assert!(
597            matches!(provider_error, ProviderError::Timeout),
598            "ProviderError should be Timeout. Actual: {provider_error:?}"
599        );
600    }
601
602    #[test]
603    fn test_address_parse_error_conversion() {
604        // Create an address parse error
605        let err = "invalid-address".parse::<Address>().unwrap_err();
606        // Map the error manually using the same approach as in our From implementation
607        let provider_error = ProviderError::InvalidAddress(err.to_string());
608        assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
609    }
610
611    #[test]
612    fn test_new_provider() {
613        let _env_guard = setup_test_env();
614
615        let config = ProviderConfig::new(
616            vec![RpcConfig::new("http://localhost:8545".to_string())],
617            30,
618            3,
619            60,
620            60,
621        );
622        let provider = EvmProvider::new(config);
623        assert!(provider.is_ok());
624
625        // Test with invalid URL
626        let config = ProviderConfig::new(
627            vec![RpcConfig::new("invalid-url".to_string())],
628            30,
629            3,
630            60,
631            60,
632        );
633        let provider = EvmProvider::new(config);
634        assert!(provider.is_err());
635    }
636
637    #[test]
638    fn test_new_provider_with_timeout() {
639        let _env_guard = setup_test_env();
640
641        // Test with valid URL and timeout
642        let config = ProviderConfig::new(
643            vec![RpcConfig::new("http://localhost:8545".to_string())],
644            30,
645            3,
646            60,
647            60,
648        );
649        let provider = EvmProvider::new(config);
650        assert!(provider.is_ok());
651
652        // Test with invalid URL
653        let config = ProviderConfig::new(
654            vec![RpcConfig::new("invalid-url".to_string())],
655            30,
656            3,
657            60,
658            60,
659        );
660        let provider = EvmProvider::new(config);
661        assert!(provider.is_err());
662
663        // Test with zero timeout
664        let config = ProviderConfig::new(
665            vec![RpcConfig::new("http://localhost:8545".to_string())],
666            0,
667            3,
668            60,
669            60,
670        );
671        let provider = EvmProvider::new(config);
672        assert!(provider.is_ok());
673
674        // Test with large timeout
675        let config = ProviderConfig::new(
676            vec![RpcConfig::new("http://localhost:8545".to_string())],
677            3600,
678            3,
679            60,
680            60,
681        );
682        let provider = EvmProvider::new(config);
683        assert!(provider.is_ok());
684    }
685
686    #[test]
687    fn test_transaction_request_conversion() {
688        let tx_data = EvmTransactionData {
689            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
690            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
691            gas_price: Some(1000000000),
692            value: Uint::<256, 4>::from(1000000000),
693            data: Some("0x".to_string()),
694            nonce: Some(1),
695            chain_id: 1,
696            gas_limit: Some(21000),
697            hash: None,
698            signature: None,
699            speed: None,
700            max_fee_per_gas: None,
701            max_priority_fee_per_gas: None,
702            raw: None,
703        };
704
705        let result = TransactionRequest::try_from(&tx_data);
706        assert!(result.is_ok());
707
708        let tx_request = result.unwrap();
709        assert_eq!(
710            tx_request.from,
711            Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
712        );
713        assert_eq!(tx_request.chain_id, Some(1));
714    }
715
716    #[tokio::test]
717    async fn test_mock_provider_methods() {
718        let mut mock = MockEvmProviderTrait::new();
719
720        mock.expect_get_balance()
721            .with(mockall::predicate::eq(
722                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
723            ))
724            .times(1)
725            .returning(|_| async { Ok(U256::from(100)) }.boxed());
726
727        mock.expect_get_block_number()
728            .times(1)
729            .returning(|| async { Ok(12345) }.boxed());
730
731        mock.expect_get_gas_price()
732            .times(1)
733            .returning(|| async { Ok(20000000000) }.boxed());
734
735        mock.expect_health_check()
736            .times(1)
737            .returning(|| async { Ok(true) }.boxed());
738
739        mock.expect_get_transaction_count()
740            .with(mockall::predicate::eq(
741                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
742            ))
743            .times(1)
744            .returning(|_| async { Ok(42) }.boxed());
745
746        mock.expect_get_fee_history()
747            .with(
748                mockall::predicate::eq(10u64),
749                mockall::predicate::eq(BlockNumberOrTag::Latest),
750                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
751            )
752            .times(1)
753            .returning(|_, _, _| {
754                async {
755                    Ok(FeeHistory {
756                        oldest_block: 100,
757                        base_fee_per_gas: vec![1000],
758                        gas_used_ratio: vec![0.5],
759                        reward: Some(vec![vec![500]]),
760                        base_fee_per_blob_gas: vec![1000],
761                        blob_gas_used_ratio: vec![0.5],
762                    })
763                }
764                .boxed()
765            });
766
767        // Test all methods
768        let balance = mock
769            .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
770            .await;
771        assert!(balance.is_ok());
772        assert_eq!(balance.unwrap(), U256::from(100));
773
774        let block_number = mock.get_block_number().await;
775        assert!(block_number.is_ok());
776        assert_eq!(block_number.unwrap(), 12345);
777
778        let gas_price = mock.get_gas_price().await;
779        assert!(gas_price.is_ok());
780        assert_eq!(gas_price.unwrap(), 20000000000);
781
782        let health = mock.health_check().await;
783        assert!(health.is_ok());
784        assert!(health.unwrap());
785
786        let count = mock
787            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
788            .await;
789        assert!(count.is_ok());
790        assert_eq!(count.unwrap(), 42);
791
792        let fee_history = mock
793            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
794            .await;
795        assert!(fee_history.is_ok());
796        let fee_history = fee_history.unwrap();
797        assert_eq!(fee_history.oldest_block, 100);
798        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
799    }
800
801    #[tokio::test]
802    async fn test_mock_transaction_operations() {
803        let mut mock = MockEvmProviderTrait::new();
804
805        // Setup mock for estimate_gas
806        let tx_data = EvmTransactionData {
807            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
808            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
809            gas_price: Some(1000000000),
810            value: Uint::<256, 4>::from(1000000000),
811            data: Some("0x".to_string()),
812            nonce: Some(1),
813            chain_id: 1,
814            gas_limit: Some(21000),
815            hash: None,
816            signature: None,
817            speed: None,
818            max_fee_per_gas: None,
819            max_priority_fee_per_gas: None,
820            raw: None,
821        };
822
823        mock.expect_estimate_gas()
824            .with(mockall::predicate::always())
825            .times(1)
826            .returning(|_| async { Ok(21000) }.boxed());
827
828        // Setup mock for send_raw_transaction
829        mock.expect_send_raw_transaction()
830            .with(mockall::predicate::always())
831            .times(1)
832            .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
833
834        // Test the mocked methods
835        let gas_estimate = mock.estimate_gas(&tx_data).await;
836        assert!(gas_estimate.is_ok());
837        assert_eq!(gas_estimate.unwrap(), 21000);
838
839        let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
840        assert!(tx_hash.is_ok());
841        assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
842    }
843
844    #[test]
845    fn test_invalid_transaction_request_conversion() {
846        let tx_data = EvmTransactionData {
847            from: "invalid-address".to_string(),
848            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
849            gas_price: Some(1000000000),
850            value: Uint::<256, 4>::from(1000000000),
851            data: Some("0x".to_string()),
852            nonce: Some(1),
853            chain_id: 1,
854            gas_limit: Some(21000),
855            hash: None,
856            signature: None,
857            speed: None,
858            max_fee_per_gas: None,
859            max_priority_fee_per_gas: None,
860            raw: None,
861        };
862
863        let result = TransactionRequest::try_from(&tx_data);
864        assert!(result.is_err());
865    }
866
867    #[test]
868    fn test_transaction_request_conversion_contract_creation() {
869        let tx_data = EvmTransactionData {
870            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
871            to: None,
872            gas_price: Some(1000000000),
873            value: Uint::<256, 4>::from(0),
874            data: Some("0x6080604052348015600f57600080fd5b".to_string()),
875            nonce: Some(1),
876            chain_id: 1,
877            gas_limit: None,
878            hash: None,
879            signature: None,
880            speed: None,
881            max_fee_per_gas: None,
882            max_priority_fee_per_gas: None,
883            raw: None,
884        };
885
886        let result = TransactionRequest::try_from(&tx_data);
887
888        assert!(result.is_ok());
889        assert_eq!(result.unwrap().to, Some(TxKind::Create));
890    }
891
892    #[test]
893    fn test_transaction_request_conversion_invalid_to_address() {
894        let tx_data = EvmTransactionData {
895            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
896            to: Some("invalid-address".to_string()),
897            gas_price: Some(1000000000),
898            value: Uint::<256, 4>::from(0),
899            data: Some("0x".to_string()),
900            nonce: Some(1),
901            chain_id: 1,
902            gas_limit: None,
903            hash: None,
904            signature: None,
905            speed: None,
906            max_fee_per_gas: None,
907            max_priority_fee_per_gas: None,
908            raw: None,
909        };
910
911        let result = TransactionRequest::try_from(&tx_data);
912
913        assert!(result.is_err());
914        assert!(matches!(
915            result,
916            Err(TransactionError::InvalidType(ref msg)) if msg == "Invalid address format"
917        ));
918    }
919
920    #[tokio::test]
921    async fn test_mock_additional_methods() {
922        let mut mock = MockEvmProviderTrait::new();
923
924        // Setup mock for health_check
925        mock.expect_health_check()
926            .times(1)
927            .returning(|| async { Ok(true) }.boxed());
928
929        // Setup mock for get_transaction_count
930        mock.expect_get_transaction_count()
931            .with(mockall::predicate::eq(
932                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
933            ))
934            .times(1)
935            .returning(|_| async { Ok(42) }.boxed());
936
937        // Setup mock for get_fee_history
938        mock.expect_get_fee_history()
939            .with(
940                mockall::predicate::eq(10u64),
941                mockall::predicate::eq(BlockNumberOrTag::Latest),
942                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
943            )
944            .times(1)
945            .returning(|_, _, _| {
946                async {
947                    Ok(FeeHistory {
948                        oldest_block: 100,
949                        base_fee_per_gas: vec![1000],
950                        gas_used_ratio: vec![0.5],
951                        reward: Some(vec![vec![500]]),
952                        base_fee_per_blob_gas: vec![1000],
953                        blob_gas_used_ratio: vec![0.5],
954                    })
955                }
956                .boxed()
957            });
958
959        // Test health check
960        let health = mock.health_check().await;
961        assert!(health.is_ok());
962        assert!(health.unwrap());
963
964        // Test get_transaction_count
965        let count = mock
966            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
967            .await;
968        assert!(count.is_ok());
969        assert_eq!(count.unwrap(), 42);
970
971        // Test get_fee_history
972        let fee_history = mock
973            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
974            .await;
975        assert!(fee_history.is_ok());
976        let fee_history = fee_history.unwrap();
977        assert_eq!(fee_history.oldest_block, 100);
978        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
979    }
980
981    #[test]
982    fn test_is_retriable_error_json_rpc_retriable_codes() {
983        // Retriable JSON-RPC error codes per EIP-1474
984        let retriable_codes = vec![
985            (-32002, "Resource unavailable"),
986            (-32005, "Limit exceeded"),
987            (-32603, "Internal error"),
988        ];
989
990        for (code, message) in retriable_codes {
991            let error = ProviderError::RpcErrorCode {
992                code,
993                message: message.to_string(),
994            };
995            assert!(
996                is_retriable_error(&error),
997                "Error code {code} should be retriable"
998            );
999        }
1000    }
1001
1002    #[test]
1003    fn test_is_retriable_error_json_rpc_non_retriable_codes() {
1004        // Non-retriable JSON-RPC error codes per EIP-1474
1005        let non_retriable_codes = vec![
1006            (-32000, "insufficient funds"),
1007            (-32000, "execution reverted"),
1008            (-32000, "already known"),
1009            (-32000, "nonce too low"),
1010            (-32000, "invalid sender"),
1011            (-32001, "Resource not found"),
1012            (-32003, "Transaction rejected"),
1013            (-32004, "Method not supported"),
1014            (-32700, "Parse error"),
1015            (-32600, "Invalid request"),
1016            (-32601, "Method not found"),
1017            (-32602, "Invalid params"),
1018        ];
1019
1020        for (code, message) in non_retriable_codes {
1021            let error = ProviderError::RpcErrorCode {
1022                code,
1023                message: message.to_string(),
1024            };
1025            assert!(
1026                !is_retriable_error(&error),
1027                "Error code {code} with message '{message}' should NOT be retriable"
1028            );
1029        }
1030    }
1031
1032    #[test]
1033    fn test_is_retriable_error_json_rpc_32000_specific_cases() {
1034        // Test specific -32000 error messages that users commonly encounter
1035        // -32000 is a catch-all for client errors and should NOT be retriable
1036        let test_cases = vec![
1037            (
1038                "tx already exists in cache",
1039                false,
1040                "Transaction already in mempool",
1041            ),
1042            ("already known", false, "Duplicate transaction submission"),
1043            (
1044                "insufficient funds for gas * price + value",
1045                false,
1046                "User needs more funds",
1047            ),
1048            ("execution reverted", false, "Smart contract rejected"),
1049            ("nonce too low", false, "Transaction already processed"),
1050            ("invalid sender", false, "Configuration issue"),
1051            ("gas required exceeds allowance", false, "Gas limit too low"),
1052            (
1053                "replacement transaction underpriced",
1054                false,
1055                "Need higher gas price",
1056            ),
1057        ];
1058
1059        for (message, should_retry, description) in test_cases {
1060            let error = ProviderError::RpcErrorCode {
1061                code: -32000,
1062                message: message.to_string(),
1063            };
1064            assert_eq!(
1065                is_retriable_error(&error),
1066                should_retry,
1067                "{}: -32000 with '{}' should{} be retriable",
1068                description,
1069                message,
1070                if should_retry { "" } else { " NOT" }
1071            );
1072        }
1073    }
1074
1075    #[tokio::test]
1076    async fn test_call_contract() {
1077        let mut mock = MockEvmProviderTrait::new();
1078
1079        let tx = TransactionRequest {
1080            from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1081            to: Some(TxKind::Call(
1082                Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1083            )),
1084            input: TransactionInput::from(
1085                hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1086            ),
1087            ..Default::default()
1088        };
1089
1090        // Setup mock for call_contract
1091        mock.expect_call_contract()
1092            .with(mockall::predicate::always())
1093            .times(1)
1094            .returning(|_| {
1095                async {
1096                    Ok(Bytes::from(
1097                        hex::decode(
1098                            "0000000000000000000000000000000000000000000000000000000000000001",
1099                        )
1100                        .unwrap(),
1101                    ))
1102                }
1103                .boxed()
1104            });
1105
1106        let result = mock.call_contract(&tx).await;
1107        assert!(result.is_ok());
1108
1109        let data = result.unwrap();
1110        assert_eq!(
1111            hex::encode(data),
1112            "0000000000000000000000000000000000000000000000000000000000000001"
1113        );
1114    }
1115}