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