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        Ok(TransactionRequest {
503            from: Some(tx.from.clone().parse().map_err(|_| {
504                TransactionError::InvalidType("Invalid address format".to_string())
505            })?),
506            to: Some(TxKind::Call(
507                tx.to
508                    .clone()
509                    .unwrap_or("".to_string())
510                    .parse()
511                    .map_err(|_| {
512                        TransactionError::InvalidType("Invalid address format".to_string())
513                    })?,
514            )),
515            gas_price: tx
516                .gas_price
517                .map(|gp| {
518                    Uint::<256, 4>::from(gp)
519                        .try_into()
520                        .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))
521                })
522                .transpose()?,
523            value: Some(Uint::<256, 4>::from(tx.value)),
524            input: TransactionInput::from(tx.data_to_bytes()?),
525            nonce: tx
526                .nonce
527                .map(|n| {
528                    Uint::<256, 4>::from(n)
529                        .try_into()
530                        .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))
531                })
532                .transpose()?,
533            chain_id: Some(tx.chain_id),
534            max_fee_per_gas: tx
535                .max_fee_per_gas
536                .map(|mfpg| {
537                    Uint::<256, 4>::from(mfpg).try_into().map_err(|_| {
538                        TransactionError::InvalidType("Invalid max fee per gas".to_string())
539                    })
540                })
541                .transpose()?,
542            max_priority_fee_per_gas: tx
543                .max_priority_fee_per_gas
544                .map(|mpfpg| {
545                    Uint::<256, 4>::from(mpfpg).try_into().map_err(|_| {
546                        TransactionError::InvalidType(
547                            "Invalid max priority fee per gas".to_string(),
548                        )
549                    })
550                })
551                .transpose()?,
552            ..Default::default()
553        })
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use alloy::primitives::Address;
561    use futures::FutureExt;
562    use lazy_static::lazy_static;
563    use std::str::FromStr;
564    use std::sync::Mutex;
565
566    lazy_static! {
567        static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
568    }
569
570    struct EvmTestEnvGuard {
571        _mutex_guard: std::sync::MutexGuard<'static, ()>,
572    }
573
574    impl EvmTestEnvGuard {
575        fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
576            std::env::set_var(
577                "API_KEY",
578                "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
579            );
580            std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
581
582            Self {
583                _mutex_guard: mutex_guard,
584            }
585        }
586    }
587
588    impl Drop for EvmTestEnvGuard {
589        fn drop(&mut self) {
590            std::env::remove_var("API_KEY");
591            std::env::remove_var("REDIS_URL");
592        }
593    }
594
595    // Helper function to set up the test environment
596    fn setup_test_env() -> EvmTestEnvGuard {
597        let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
598        EvmTestEnvGuard::new(guard)
599    }
600
601    #[tokio::test]
602    async fn test_reqwest_error_conversion() {
603        // Create a reqwest timeout error
604        let client = reqwest::Client::new();
605        let result = client
606            .get("https://www.openzeppelin.com/")
607            .timeout(Duration::from_millis(1))
608            .send()
609            .await;
610
611        assert!(
612            result.is_err(),
613            "Expected the send operation to result in an error."
614        );
615        let err = result.unwrap_err();
616
617        assert!(
618            err.is_timeout(),
619            "The reqwest error should be a timeout. Actual error: {:?}",
620            err
621        );
622
623        let provider_error = ProviderError::from(err);
624        assert!(
625            matches!(provider_error, ProviderError::Timeout),
626            "ProviderError should be Timeout. Actual: {:?}",
627            provider_error
628        );
629    }
630
631    #[test]
632    fn test_address_parse_error_conversion() {
633        // Create an address parse error
634        let err = "invalid-address".parse::<Address>().unwrap_err();
635        // Map the error manually using the same approach as in our From implementation
636        let provider_error = ProviderError::InvalidAddress(err.to_string());
637        assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
638    }
639
640    #[test]
641    fn test_new_provider() {
642        let _env_guard = setup_test_env();
643
644        let config = ProviderConfig::new(
645            vec![RpcConfig::new("http://localhost:8545".to_string())],
646            30,
647            3,
648            60,
649            60,
650        );
651        let provider = EvmProvider::new(config);
652        assert!(provider.is_ok());
653
654        // Test with invalid URL
655        let config = ProviderConfig::new(
656            vec![RpcConfig::new("invalid-url".to_string())],
657            30,
658            3,
659            60,
660            60,
661        );
662        let provider = EvmProvider::new(config);
663        assert!(provider.is_err());
664    }
665
666    #[test]
667    fn test_new_provider_with_timeout() {
668        let _env_guard = setup_test_env();
669
670        // Test with valid URL and timeout
671        let config = ProviderConfig::new(
672            vec![RpcConfig::new("http://localhost:8545".to_string())],
673            30,
674            3,
675            60,
676            60,
677        );
678        let provider = EvmProvider::new(config);
679        assert!(provider.is_ok());
680
681        // Test with invalid URL
682        let config = ProviderConfig::new(
683            vec![RpcConfig::new("invalid-url".to_string())],
684            30,
685            3,
686            60,
687            60,
688        );
689        let provider = EvmProvider::new(config);
690        assert!(provider.is_err());
691
692        // Test with zero timeout
693        let config = ProviderConfig::new(
694            vec![RpcConfig::new("http://localhost:8545".to_string())],
695            0,
696            3,
697            60,
698            60,
699        );
700        let provider = EvmProvider::new(config);
701        assert!(provider.is_ok());
702
703        // Test with large timeout
704        let config = ProviderConfig::new(
705            vec![RpcConfig::new("http://localhost:8545".to_string())],
706            3600,
707            3,
708            60,
709            60,
710        );
711        let provider = EvmProvider::new(config);
712        assert!(provider.is_ok());
713    }
714
715    #[test]
716    fn test_transaction_request_conversion() {
717        let tx_data = EvmTransactionData {
718            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
719            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
720            gas_price: Some(1000000000),
721            value: Uint::<256, 4>::from(1000000000),
722            data: Some("0x".to_string()),
723            nonce: Some(1),
724            chain_id: 1,
725            gas_limit: Some(21000),
726            hash: None,
727            signature: None,
728            speed: None,
729            max_fee_per_gas: None,
730            max_priority_fee_per_gas: None,
731            raw: None,
732        };
733
734        let result = TransactionRequest::try_from(&tx_data);
735        assert!(result.is_ok());
736
737        let tx_request = result.unwrap();
738        assert_eq!(
739            tx_request.from,
740            Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
741        );
742        assert_eq!(tx_request.chain_id, Some(1));
743    }
744
745    #[tokio::test]
746    async fn test_mock_provider_methods() {
747        let mut mock = MockEvmProviderTrait::new();
748
749        mock.expect_get_balance()
750            .with(mockall::predicate::eq(
751                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
752            ))
753            .times(1)
754            .returning(|_| async { Ok(U256::from(100)) }.boxed());
755
756        mock.expect_get_block_number()
757            .times(1)
758            .returning(|| async { Ok(12345) }.boxed());
759
760        mock.expect_get_gas_price()
761            .times(1)
762            .returning(|| async { Ok(20000000000) }.boxed());
763
764        mock.expect_health_check()
765            .times(1)
766            .returning(|| async { Ok(true) }.boxed());
767
768        mock.expect_get_transaction_count()
769            .with(mockall::predicate::eq(
770                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
771            ))
772            .times(1)
773            .returning(|_| async { Ok(42) }.boxed());
774
775        mock.expect_get_fee_history()
776            .with(
777                mockall::predicate::eq(10u64),
778                mockall::predicate::eq(BlockNumberOrTag::Latest),
779                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
780            )
781            .times(1)
782            .returning(|_, _, _| {
783                async {
784                    Ok(FeeHistory {
785                        oldest_block: 100,
786                        base_fee_per_gas: vec![1000],
787                        gas_used_ratio: vec![0.5],
788                        reward: Some(vec![vec![500]]),
789                        base_fee_per_blob_gas: vec![1000],
790                        blob_gas_used_ratio: vec![0.5],
791                    })
792                }
793                .boxed()
794            });
795
796        // Test all methods
797        let balance = mock
798            .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
799            .await;
800        assert!(balance.is_ok());
801        assert_eq!(balance.unwrap(), U256::from(100));
802
803        let block_number = mock.get_block_number().await;
804        assert!(block_number.is_ok());
805        assert_eq!(block_number.unwrap(), 12345);
806
807        let gas_price = mock.get_gas_price().await;
808        assert!(gas_price.is_ok());
809        assert_eq!(gas_price.unwrap(), 20000000000);
810
811        let health = mock.health_check().await;
812        assert!(health.is_ok());
813        assert!(health.unwrap());
814
815        let count = mock
816            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
817            .await;
818        assert!(count.is_ok());
819        assert_eq!(count.unwrap(), 42);
820
821        let fee_history = mock
822            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
823            .await;
824        assert!(fee_history.is_ok());
825        let fee_history = fee_history.unwrap();
826        assert_eq!(fee_history.oldest_block, 100);
827        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
828    }
829
830    #[tokio::test]
831    async fn test_mock_transaction_operations() {
832        let mut mock = MockEvmProviderTrait::new();
833
834        // Setup mock for estimate_gas
835        let tx_data = EvmTransactionData {
836            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
837            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
838            gas_price: Some(1000000000),
839            value: Uint::<256, 4>::from(1000000000),
840            data: Some("0x".to_string()),
841            nonce: Some(1),
842            chain_id: 1,
843            gas_limit: Some(21000),
844            hash: None,
845            signature: None,
846            speed: None,
847            max_fee_per_gas: None,
848            max_priority_fee_per_gas: None,
849            raw: None,
850        };
851
852        mock.expect_estimate_gas()
853            .with(mockall::predicate::always())
854            .times(1)
855            .returning(|_| async { Ok(21000) }.boxed());
856
857        // Setup mock for send_raw_transaction
858        mock.expect_send_raw_transaction()
859            .with(mockall::predicate::always())
860            .times(1)
861            .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
862
863        // Test the mocked methods
864        let gas_estimate = mock.estimate_gas(&tx_data).await;
865        assert!(gas_estimate.is_ok());
866        assert_eq!(gas_estimate.unwrap(), 21000);
867
868        let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
869        assert!(tx_hash.is_ok());
870        assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
871    }
872
873    #[test]
874    fn test_invalid_transaction_request_conversion() {
875        let tx_data = EvmTransactionData {
876            from: "invalid-address".to_string(),
877            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
878            gas_price: Some(1000000000),
879            value: Uint::<256, 4>::from(1000000000),
880            data: Some("0x".to_string()),
881            nonce: Some(1),
882            chain_id: 1,
883            gas_limit: Some(21000),
884            hash: None,
885            signature: None,
886            speed: None,
887            max_fee_per_gas: None,
888            max_priority_fee_per_gas: None,
889            raw: None,
890        };
891
892        let result = TransactionRequest::try_from(&tx_data);
893        assert!(result.is_err());
894    }
895
896    #[tokio::test]
897    async fn test_mock_additional_methods() {
898        let mut mock = MockEvmProviderTrait::new();
899
900        // Setup mock for health_check
901        mock.expect_health_check()
902            .times(1)
903            .returning(|| async { Ok(true) }.boxed());
904
905        // Setup mock for get_transaction_count
906        mock.expect_get_transaction_count()
907            .with(mockall::predicate::eq(
908                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
909            ))
910            .times(1)
911            .returning(|_| async { Ok(42) }.boxed());
912
913        // Setup mock for get_fee_history
914        mock.expect_get_fee_history()
915            .with(
916                mockall::predicate::eq(10u64),
917                mockall::predicate::eq(BlockNumberOrTag::Latest),
918                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
919            )
920            .times(1)
921            .returning(|_, _, _| {
922                async {
923                    Ok(FeeHistory {
924                        oldest_block: 100,
925                        base_fee_per_gas: vec![1000],
926                        gas_used_ratio: vec![0.5],
927                        reward: Some(vec![vec![500]]),
928                        base_fee_per_blob_gas: vec![1000],
929                        blob_gas_used_ratio: vec![0.5],
930                    })
931                }
932                .boxed()
933            });
934
935        // Test health check
936        let health = mock.health_check().await;
937        assert!(health.is_ok());
938        assert!(health.unwrap());
939
940        // Test get_transaction_count
941        let count = mock
942            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
943            .await;
944        assert!(count.is_ok());
945        assert_eq!(count.unwrap(), 42);
946
947        // Test get_fee_history
948        let fee_history = mock
949            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
950            .await;
951        assert!(fee_history.is_ok());
952        let fee_history = fee_history.unwrap();
953        assert_eq!(fee_history.oldest_block, 100);
954        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
955    }
956
957    #[test]
958    fn test_is_retriable_error_json_rpc_retriable_codes() {
959        // Retriable JSON-RPC error codes per EIP-1474
960        let retriable_codes = vec![
961            (-32002, "Resource unavailable"),
962            (-32005, "Limit exceeded"),
963            (-32603, "Internal error"),
964        ];
965
966        for (code, message) in retriable_codes {
967            let error = ProviderError::RpcErrorCode {
968                code,
969                message: message.to_string(),
970            };
971            assert!(
972                is_retriable_error(&error),
973                "Error code {} should be retriable",
974                code
975            );
976        }
977    }
978
979    #[test]
980    fn test_is_retriable_error_json_rpc_non_retriable_codes() {
981        // Non-retriable JSON-RPC error codes per EIP-1474
982        let non_retriable_codes = vec![
983            (-32000, "insufficient funds"),
984            (-32000, "execution reverted"),
985            (-32000, "already known"),
986            (-32000, "nonce too low"),
987            (-32000, "invalid sender"),
988            (-32001, "Resource not found"),
989            (-32003, "Transaction rejected"),
990            (-32004, "Method not supported"),
991            (-32700, "Parse error"),
992            (-32600, "Invalid request"),
993            (-32601, "Method not found"),
994            (-32602, "Invalid params"),
995        ];
996
997        for (code, message) in non_retriable_codes {
998            let error = ProviderError::RpcErrorCode {
999                code,
1000                message: message.to_string(),
1001            };
1002            assert!(
1003                !is_retriable_error(&error),
1004                "Error code {} with message '{}' should NOT be retriable",
1005                code,
1006                message
1007            );
1008        }
1009    }
1010
1011    #[test]
1012    fn test_is_retriable_error_json_rpc_32000_specific_cases() {
1013        // Test specific -32000 error messages that users commonly encounter
1014        // -32000 is a catch-all for client errors and should NOT be retriable
1015        let test_cases = vec![
1016            (
1017                "tx already exists in cache",
1018                false,
1019                "Transaction already in mempool",
1020            ),
1021            ("already known", false, "Duplicate transaction submission"),
1022            (
1023                "insufficient funds for gas * price + value",
1024                false,
1025                "User needs more funds",
1026            ),
1027            ("execution reverted", false, "Smart contract rejected"),
1028            ("nonce too low", false, "Transaction already processed"),
1029            ("invalid sender", false, "Configuration issue"),
1030            ("gas required exceeds allowance", false, "Gas limit too low"),
1031            (
1032                "replacement transaction underpriced",
1033                false,
1034                "Need higher gas price",
1035            ),
1036        ];
1037
1038        for (message, should_retry, description) in test_cases {
1039            let error = ProviderError::RpcErrorCode {
1040                code: -32000,
1041                message: message.to_string(),
1042            };
1043            assert_eq!(
1044                is_retriable_error(&error),
1045                should_retry,
1046                "{}: -32000 with '{}' should{} be retriable",
1047                description,
1048                message,
1049                if should_retry { "" } else { " NOT" }
1050            );
1051        }
1052    }
1053
1054    #[tokio::test]
1055    async fn test_call_contract() {
1056        let mut mock = MockEvmProviderTrait::new();
1057
1058        let tx = TransactionRequest {
1059            from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1060            to: Some(TxKind::Call(
1061                Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1062            )),
1063            input: TransactionInput::from(
1064                hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1065            ),
1066            ..Default::default()
1067        };
1068
1069        // Setup mock for call_contract
1070        mock.expect_call_contract()
1071            .with(mockall::predicate::always())
1072            .times(1)
1073            .returning(|_| {
1074                async {
1075                    Ok(Bytes::from(
1076                        hex::decode(
1077                            "0000000000000000000000000000000000000000000000000000000000000001",
1078                        )
1079                        .unwrap(),
1080                    ))
1081                }
1082                .boxed()
1083            });
1084
1085        let result = mock.call_contract(&tx).await;
1086        assert!(result.is_ok());
1087
1088        let data = result.unwrap();
1089        assert_eq!(
1090            hex::encode(data),
1091            "0000000000000000000000000000000000000000000000000000000000000001"
1092        );
1093    }
1094}