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