openzeppelin_relayer/services/provider/evm/
mod.rs

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