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