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