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: Some(
482                Uint::<256, 4>::from(tx.gas_price.unwrap_or(0))
483                    .try_into()
484                    .map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))?,
485            ),
486            // we should not set gas here
487            // gas: Some(
488            //     Uint::<256, 4>::from(tx.gas_limit)
489            //         .try_into()
490            //         .map_err(|_| TransactionError::InvalidType("Invalid gas
491            // limit".to_string()))?, ),
492            value: Some(Uint::<256, 4>::from(tx.value)),
493            input: TransactionInput::from(tx.data.clone().unwrap_or("".to_string()).into_bytes()),
494            nonce: Some(
495                Uint::<256, 4>::from(tx.nonce.ok_or_else(|| {
496                    TransactionError::InvalidType("Nonce must be defined".to_string())
497                })?)
498                .try_into()
499                .map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))?,
500            ),
501            chain_id: Some(tx.chain_id),
502            ..Default::default()
503        })
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use alloy::primitives::Address;
511    use futures::FutureExt;
512    use lazy_static::lazy_static;
513    use std::str::FromStr;
514    use std::sync::Mutex;
515
516    lazy_static! {
517        static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
518    }
519
520    struct EvmTestEnvGuard {
521        _mutex_guard: std::sync::MutexGuard<'static, ()>,
522    }
523
524    impl EvmTestEnvGuard {
525        fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
526            std::env::set_var(
527                "API_KEY",
528                "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
529            );
530            std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
531
532            Self {
533                _mutex_guard: mutex_guard,
534            }
535        }
536    }
537
538    impl Drop for EvmTestEnvGuard {
539        fn drop(&mut self) {
540            std::env::remove_var("API_KEY");
541            std::env::remove_var("REDIS_URL");
542        }
543    }
544
545    // Helper function to set up the test environment
546    fn setup_test_env() -> EvmTestEnvGuard {
547        let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
548        EvmTestEnvGuard::new(guard)
549    }
550
551    #[tokio::test]
552    async fn test_reqwest_error_conversion() {
553        // Create a reqwest timeout error
554        let client = reqwest::Client::new();
555        let result = client
556            .get("https://www.openzeppelin.com/")
557            .timeout(Duration::from_millis(1))
558            .send()
559            .await;
560
561        assert!(
562            result.is_err(),
563            "Expected the send operation to result in an error."
564        );
565        let err = result.unwrap_err();
566
567        assert!(
568            err.is_timeout(),
569            "The reqwest error should be a timeout. Actual error: {:?}",
570            err
571        );
572
573        let provider_error = ProviderError::from(err);
574        assert!(
575            matches!(provider_error, ProviderError::Timeout),
576            "ProviderError should be Timeout. Actual: {:?}",
577            provider_error
578        );
579    }
580
581    #[test]
582    fn test_address_parse_error_conversion() {
583        // Create an address parse error
584        let err = "invalid-address".parse::<Address>().unwrap_err();
585        // Map the error manually using the same approach as in our From implementation
586        let provider_error = ProviderError::InvalidAddress(err.to_string());
587        assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
588    }
589
590    #[test]
591    fn test_new_provider() {
592        let _env_guard = setup_test_env();
593
594        let provider = EvmProvider::new(
595            vec![RpcConfig::new("http://localhost:8545".to_string())],
596            30,
597        );
598        assert!(provider.is_ok());
599
600        // Test with invalid URL
601        let provider = EvmProvider::new(vec![RpcConfig::new("invalid-url".to_string())], 30);
602        assert!(provider.is_err());
603    }
604
605    #[test]
606    fn test_new_provider_with_timeout() {
607        let _env_guard = setup_test_env();
608
609        // Test with valid URL and timeout
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        // Test with zero timeout
621        let provider =
622            EvmProvider::new(vec![RpcConfig::new("http://localhost:8545".to_string())], 0);
623        assert!(provider.is_ok());
624
625        // Test with large timeout
626        let provider = EvmProvider::new(
627            vec![RpcConfig::new("http://localhost:8545".to_string())],
628            3600,
629        );
630        assert!(provider.is_ok());
631    }
632
633    #[test]
634    fn test_transaction_request_conversion() {
635        let tx_data = EvmTransactionData {
636            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
637            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
638            gas_price: Some(1000000000),
639            value: Uint::<256, 4>::from(1000000000),
640            data: Some("0x".to_string()),
641            nonce: Some(1),
642            chain_id: 1,
643            gas_limit: 21000,
644            hash: None,
645            signature: None,
646            speed: None,
647            max_fee_per_gas: None,
648            max_priority_fee_per_gas: None,
649            raw: None,
650        };
651
652        let result = TransactionRequest::try_from(&tx_data);
653        assert!(result.is_ok());
654
655        let tx_request = result.unwrap();
656        assert_eq!(
657            tx_request.from,
658            Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
659        );
660        assert_eq!(tx_request.chain_id, Some(1));
661    }
662
663    #[test]
664    fn test_should_mark_provider_failed_server_errors() {
665        // 5xx errors should mark provider as failed
666        for status_code in 500..=599 {
667            let error = ProviderError::RequestError {
668                error: format!("Server error {}", status_code),
669                status_code,
670            };
671            assert!(
672                EvmProvider::should_mark_provider_failed(&error),
673                "Status code {} should mark provider as failed",
674                status_code
675            );
676        }
677    }
678
679    #[test]
680    fn test_should_mark_provider_failed_auth_errors() {
681        // Authentication/authorization errors should mark provider as failed
682        let auth_errors = [401, 403];
683        for &status_code in &auth_errors {
684            let error = ProviderError::RequestError {
685                error: format!("Auth error {}", status_code),
686                status_code,
687            };
688            assert!(
689                EvmProvider::should_mark_provider_failed(&error),
690                "Status code {} should mark provider as failed",
691                status_code
692            );
693        }
694    }
695
696    #[test]
697    fn test_should_mark_provider_failed_not_found_errors() {
698        // 404 and 410 should mark provider as failed (endpoint issues)
699        let not_found_errors = [404, 410];
700        for &status_code in &not_found_errors {
701            let error = ProviderError::RequestError {
702                error: format!("Not found error {}", status_code),
703                status_code,
704            };
705            assert!(
706                EvmProvider::should_mark_provider_failed(&error),
707                "Status code {} should mark provider as failed",
708                status_code
709            );
710        }
711    }
712
713    #[test]
714    fn test_should_mark_provider_failed_client_errors_not_failed() {
715        // These 4xx errors should NOT mark provider as failed (client-side issues)
716        let client_errors = [400, 405, 413, 414, 415, 422, 429];
717        for &status_code in &client_errors {
718            let error = ProviderError::RequestError {
719                error: format!("Client error {}", status_code),
720                status_code,
721            };
722            assert!(
723                !EvmProvider::should_mark_provider_failed(&error),
724                "Status code {} should NOT mark provider as failed",
725                status_code
726            );
727        }
728    }
729
730    #[test]
731    fn test_should_mark_provider_failed_other_error_types() {
732        // Test non-RequestError types - these should NOT mark provider as failed
733        let errors = [
734            ProviderError::Timeout,
735            ProviderError::RateLimited,
736            ProviderError::BadGateway,
737            ProviderError::InvalidAddress("test".to_string()),
738            ProviderError::NetworkConfiguration("test".to_string()),
739            ProviderError::Other("test".to_string()),
740        ];
741
742        for error in errors {
743            assert!(
744                !EvmProvider::should_mark_provider_failed(&error),
745                "Error type {:?} should NOT mark provider as failed",
746                error
747            );
748        }
749    }
750
751    #[test]
752    fn test_should_mark_provider_failed_edge_cases() {
753        // Test some edge case status codes
754        let edge_cases = [
755            (200, false), // Success - shouldn't happen in error context but test anyway
756            (300, false), // Redirection
757            (418, false), // I'm a teapot - should not mark as failed
758            (451, false), // Unavailable for legal reasons - client issue
759            (499, false), // Client closed request - client issue
760        ];
761
762        for (status_code, should_fail) in edge_cases {
763            let error = ProviderError::RequestError {
764                error: format!("Edge case error {}", status_code),
765                status_code,
766            };
767            assert_eq!(
768                EvmProvider::should_mark_provider_failed(&error),
769                should_fail,
770                "Status code {} should {} mark provider as failed",
771                status_code,
772                if should_fail { "" } else { "NOT" }
773            );
774        }
775    }
776
777    #[test]
778    fn test_is_retriable_error_retriable_types() {
779        // These error types should be retriable
780        let retriable_errors = [
781            ProviderError::Timeout,
782            ProviderError::RateLimited,
783            ProviderError::BadGateway,
784        ];
785
786        for error in retriable_errors {
787            assert!(
788                EvmProvider::is_retriable_error(&error),
789                "Error type {:?} should be retriable",
790                error
791            );
792        }
793    }
794
795    #[test]
796    fn test_is_retriable_error_non_retriable_types() {
797        // These error types should NOT be retriable
798        let non_retriable_errors = [
799            ProviderError::InvalidAddress("test".to_string()),
800            ProviderError::NetworkConfiguration("test".to_string()),
801            ProviderError::RequestError {
802                error: "Some error".to_string(),
803                status_code: 400,
804            },
805        ];
806
807        for error in non_retriable_errors {
808            assert!(
809                !EvmProvider::is_retriable_error(&error),
810                "Error type {:?} should NOT be retriable",
811                error
812            );
813        }
814    }
815
816    #[test]
817    fn test_is_retriable_error_message_based_detection() {
818        // Test errors that should be retriable based on message content
819        let retriable_messages = [
820            "Connection timeout occurred",
821            "Network connection reset",
822            "Connection refused",
823            "TIMEOUT error happened",
824            "Connection was reset by peer",
825        ];
826
827        for message in retriable_messages {
828            let error = ProviderError::Other(message.to_string());
829            assert!(
830                EvmProvider::is_retriable_error(&error),
831                "Error with message '{}' should be retriable",
832                message
833            );
834        }
835    }
836
837    #[test]
838    fn test_is_retriable_error_message_based_non_retriable() {
839        // Test errors that should NOT be retriable based on message content
840        let non_retriable_messages = [
841            "Invalid address format",
842            "Bad request parameters",
843            "Authentication failed",
844            "Method not found",
845            "Some other error",
846        ];
847
848        for message in non_retriable_messages {
849            let error = ProviderError::Other(message.to_string());
850            assert!(
851                !EvmProvider::is_retriable_error(&error),
852                "Error with message '{}' should NOT be retriable",
853                message
854            );
855        }
856    }
857
858    #[test]
859    fn test_is_retriable_error_case_insensitive() {
860        // Test that message-based detection is case insensitive
861        let case_variations = [
862            "TIMEOUT",
863            "Timeout",
864            "timeout",
865            "CONNECTION",
866            "Connection",
867            "connection",
868            "RESET",
869            "Reset",
870            "reset",
871        ];
872
873        for message in case_variations {
874            let error = ProviderError::Other(message.to_string());
875            assert!(
876                EvmProvider::is_retriable_error(&error),
877                "Error with message '{}' should be retriable (case insensitive)",
878                message
879            );
880        }
881    }
882
883    #[tokio::test]
884    async fn test_mock_provider_methods() {
885        let mut mock = MockEvmProviderTrait::new();
886
887        mock.expect_get_balance()
888            .with(mockall::predicate::eq(
889                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
890            ))
891            .times(1)
892            .returning(|_| async { Ok(U256::from(100)) }.boxed());
893
894        mock.expect_get_block_number()
895            .times(1)
896            .returning(|| async { Ok(12345) }.boxed());
897
898        mock.expect_get_gas_price()
899            .times(1)
900            .returning(|| async { Ok(20000000000) }.boxed());
901
902        mock.expect_health_check()
903            .times(1)
904            .returning(|| async { Ok(true) }.boxed());
905
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        mock.expect_get_fee_history()
914            .with(
915                mockall::predicate::eq(10u64),
916                mockall::predicate::eq(BlockNumberOrTag::Latest),
917                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
918            )
919            .times(1)
920            .returning(|_, _, _| {
921                async {
922                    Ok(FeeHistory {
923                        oldest_block: 100,
924                        base_fee_per_gas: vec![1000],
925                        gas_used_ratio: vec![0.5],
926                        reward: Some(vec![vec![500]]),
927                        base_fee_per_blob_gas: vec![1000],
928                        blob_gas_used_ratio: vec![0.5],
929                    })
930                }
931                .boxed()
932            });
933
934        // Test all methods
935        let balance = mock
936            .get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
937            .await;
938        assert!(balance.is_ok());
939        assert_eq!(balance.unwrap(), U256::from(100));
940
941        let block_number = mock.get_block_number().await;
942        assert!(block_number.is_ok());
943        assert_eq!(block_number.unwrap(), 12345);
944
945        let gas_price = mock.get_gas_price().await;
946        assert!(gas_price.is_ok());
947        assert_eq!(gas_price.unwrap(), 20000000000);
948
949        let health = mock.health_check().await;
950        assert!(health.is_ok());
951        assert!(health.unwrap());
952
953        let count = mock
954            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
955            .await;
956        assert!(count.is_ok());
957        assert_eq!(count.unwrap(), 42);
958
959        let fee_history = mock
960            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
961            .await;
962        assert!(fee_history.is_ok());
963        let fee_history = fee_history.unwrap();
964        assert_eq!(fee_history.oldest_block, 100);
965        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
966    }
967
968    #[tokio::test]
969    async fn test_mock_transaction_operations() {
970        let mut mock = MockEvmProviderTrait::new();
971
972        // Setup mock for estimate_gas
973        let tx_data = EvmTransactionData {
974            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
975            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
976            gas_price: Some(1000000000),
977            value: Uint::<256, 4>::from(1000000000),
978            data: Some("0x".to_string()),
979            nonce: Some(1),
980            chain_id: 1,
981            gas_limit: 21000,
982            hash: None,
983            signature: None,
984            speed: None,
985            max_fee_per_gas: None,
986            max_priority_fee_per_gas: None,
987            raw: None,
988        };
989
990        mock.expect_estimate_gas()
991            .with(mockall::predicate::always())
992            .times(1)
993            .returning(|_| async { Ok(21000) }.boxed());
994
995        // Setup mock for send_raw_transaction
996        mock.expect_send_raw_transaction()
997            .with(mockall::predicate::always())
998            .times(1)
999            .returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
1000
1001        // Test the mocked methods
1002        let gas_estimate = mock.estimate_gas(&tx_data).await;
1003        assert!(gas_estimate.is_ok());
1004        assert_eq!(gas_estimate.unwrap(), 21000);
1005
1006        let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
1007        assert!(tx_hash.is_ok());
1008        assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
1009    }
1010
1011    #[test]
1012    fn test_invalid_transaction_request_conversion() {
1013        let tx_data = EvmTransactionData {
1014            from: "invalid-address".to_string(),
1015            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1016            gas_price: Some(1000000000),
1017            value: Uint::<256, 4>::from(1000000000),
1018            data: Some("0x".to_string()),
1019            nonce: Some(1),
1020            chain_id: 1,
1021            gas_limit: 21000,
1022            hash: None,
1023            signature: None,
1024            speed: None,
1025            max_fee_per_gas: None,
1026            max_priority_fee_per_gas: None,
1027            raw: None,
1028        };
1029
1030        let result = TransactionRequest::try_from(&tx_data);
1031        assert!(result.is_err());
1032    }
1033
1034    #[tokio::test]
1035    async fn test_mock_additional_methods() {
1036        let mut mock = MockEvmProviderTrait::new();
1037
1038        // Setup mock for health_check
1039        mock.expect_health_check()
1040            .times(1)
1041            .returning(|| async { Ok(true) }.boxed());
1042
1043        // Setup mock for get_transaction_count
1044        mock.expect_get_transaction_count()
1045            .with(mockall::predicate::eq(
1046                "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
1047            ))
1048            .times(1)
1049            .returning(|_| async { Ok(42) }.boxed());
1050
1051        // Setup mock for get_fee_history
1052        mock.expect_get_fee_history()
1053            .with(
1054                mockall::predicate::eq(10u64),
1055                mockall::predicate::eq(BlockNumberOrTag::Latest),
1056                mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
1057            )
1058            .times(1)
1059            .returning(|_, _, _| {
1060                async {
1061                    Ok(FeeHistory {
1062                        oldest_block: 100,
1063                        base_fee_per_gas: vec![1000],
1064                        gas_used_ratio: vec![0.5],
1065                        reward: Some(vec![vec![500]]),
1066                        base_fee_per_blob_gas: vec![1000],
1067                        blob_gas_used_ratio: vec![0.5],
1068                    })
1069                }
1070                .boxed()
1071            });
1072
1073        // Test health check
1074        let health = mock.health_check().await;
1075        assert!(health.is_ok());
1076        assert!(health.unwrap());
1077
1078        // Test get_transaction_count
1079        let count = mock
1080            .get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
1081            .await;
1082        assert!(count.is_ok());
1083        assert_eq!(count.unwrap(), 42);
1084
1085        // Test get_fee_history
1086        let fee_history = mock
1087            .get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
1088            .await;
1089        assert!(fee_history.is_ok());
1090        let fee_history = fee_history.unwrap();
1091        assert_eq!(fee_history.oldest_block, 100);
1092        assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
1093    }
1094
1095    #[tokio::test]
1096    async fn test_call_contract() {
1097        let mut mock = MockEvmProviderTrait::new();
1098
1099        let tx = TransactionRequest {
1100            from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
1101            to: Some(TxKind::Call(
1102                Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
1103            )),
1104            input: TransactionInput::from(
1105                hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
1106            ),
1107            ..Default::default()
1108        };
1109
1110        // Setup mock for call_contract
1111        mock.expect_call_contract()
1112            .with(mockall::predicate::always())
1113            .times(1)
1114            .returning(|_| {
1115                async {
1116                    Ok(Bytes::from(
1117                        hex::decode(
1118                            "0000000000000000000000000000000000000000000000000000000000000001",
1119                        )
1120                        .unwrap(),
1121                    ))
1122                }
1123                .boxed()
1124            });
1125
1126        let result = mock.call_contract(&tx).await;
1127        assert!(result.is_ok());
1128
1129        let data = result.unwrap();
1130        assert_eq!(
1131            hex::encode(data),
1132            "0000000000000000000000000000000000000000000000000000000000000001"
1133        );
1134    }
1135}