openzeppelin_relayer/services/gas/fetchers/
polygon_zkevm.rs

1//! Polygon zkEVM specialized gas price fetcher.
2//!
3//! This module provides enhanced gas price fetching for Polygon zkEVM networks
4//! using their custom `zkevm_estimateGasPrice` RPC method, with automatic fallback
5//! to standard methods when the custom method is unavailable.
6
7use crate::{
8    models::EvmNetwork,
9    services::provider::{evm::EvmProviderTrait, ProviderError},
10};
11use tracing::{debug, error, info, warn};
12
13/// Specialized fetcher for Polygon zkEVM networks.
14#[derive(Debug, Clone)]
15pub struct PolygonZkEvmGasPriceFetcher;
16
17impl PolygonZkEvmGasPriceFetcher {
18    /// Fetches gas price using zkEVM-specific methods with fallback.
19    pub async fn fetch_gas_price<P: EvmProviderTrait>(
20        &self,
21        provider: &P,
22        network: &EvmNetwork,
23    ) -> Result<Option<u128>, ProviderError> {
24        if let Some(zkevm_price) = self.try_zkevm_fetch(provider, network).await? {
25            return Ok(Some(zkevm_price));
26        }
27        self.fallback_to_standard(provider, network).await
28    }
29
30    /// Attempts zkEVM gas price fetch.
31    async fn try_zkevm_fetch<P: EvmProviderTrait>(
32        &self,
33        provider: &P,
34        network: &EvmNetwork,
35    ) -> Result<Option<u128>, ProviderError> {
36        let result = provider
37            .raw_request_dyn("zkevm_estimateGasPrice", serde_json::Value::Array(vec![]))
38            .await;
39
40        match result {
41            Ok(response) => self.parse_zkevm_response(response, network.chain_id),
42            Err(ProviderError::RpcErrorCode { code, .. }) if code == -32601 || code == -32004 => {
43                debug!(
44                    "zkEVM gas price method not available for chain_id {} (error code: {})",
45                    network.chain_id, code
46                );
47                Ok(None)
48            }
49            Err(e) => {
50                debug!(
51                    "zkEVM gas price estimation failed for chain_id {}: {}",
52                    network.chain_id, e
53                );
54                Ok(None)
55            }
56        }
57    }
58
59    /// Parses zkEVM response into gas price.
60    fn parse_zkevm_response(
61        &self,
62        response: serde_json::Value,
63        chain_id: u64,
64    ) -> Result<Option<u128>, ProviderError> {
65        let Some(gas_price_hex) = response.as_str() else {
66            warn!(
67                "Invalid zkEVM gas price response format for chain_id {}",
68                chain_id
69            );
70            return Ok(None);
71        };
72
73        match u128::from_str_radix(gas_price_hex.trim_start_matches("0x"), 16) {
74            Ok(gas_price) => {
75                info!(
76                    "zkEVM gas price estimated for chain_id {}: {} wei",
77                    chain_id, gas_price
78                );
79                Ok(Some(gas_price))
80            }
81            Err(e) => {
82                warn!(
83                    "Failed to parse zkEVM gas price response for chain_id {}: {}",
84                    chain_id, e
85                );
86                Ok(None)
87            }
88        }
89    }
90
91    /// Falls back to standard gas price methods.
92    async fn fallback_to_standard<P: EvmProviderTrait>(
93        &self,
94        provider: &P,
95        network: &EvmNetwork,
96    ) -> Result<Option<u128>, ProviderError> {
97        match provider.get_gas_price().await {
98            Ok(standard_price) => {
99                info!(
100                    "Using standard gas price fallback for zkEVM chain_id {}: {} wei",
101                    network.chain_id, standard_price
102                );
103                Ok(Some(standard_price))
104            }
105            Err(e) => {
106                error!(
107                    "Both zkEVM and standard gas price methods failed for chain_id {}",
108                    network.chain_id
109                );
110                Err(e)
111            }
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::{constants::POLYGON_ZKEVM_TAG, services::provider::evm::MockEvmProviderTrait};
120    use futures::FutureExt;
121    use mockall::predicate::*;
122
123    fn create_zkevm_network() -> EvmNetwork {
124        EvmNetwork {
125            network: "polygon-zkevm".to_string(),
126            rpc_urls: vec![crate::models::RpcConfig::new(
127                "https://zkevm-rpc.com".to_string(),
128            )],
129            explorer_urls: None,
130            average_blocktime_ms: 2000,
131            is_testnet: false,
132            tags: vec![POLYGON_ZKEVM_TAG.to_string()],
133            chain_id: 1101,
134            required_confirmations: 1,
135            features: vec!["eip1559".to_string()],
136            symbol: "ETH".to_string(),
137            gas_price_cache: None,
138        }
139    }
140
141    #[tokio::test]
142    async fn test_zkevm_fetcher_success() {
143        let mut mock_provider = MockEvmProviderTrait::new();
144        mock_provider
145            .expect_raw_request_dyn()
146            .with(
147                eq("zkevm_estimateGasPrice"),
148                eq(serde_json::Value::Array(vec![])),
149            )
150            .times(1)
151            .returning(|_, _| {
152                async { Ok(serde_json::Value::String("0x174876e800".to_string())) }.boxed()
153            });
154
155        let fetcher = PolygonZkEvmGasPriceFetcher;
156        let network = create_zkevm_network();
157
158        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
159        assert!(result.is_ok());
160        assert_eq!(result.unwrap(), Some(100_000_000_000u128));
161    }
162
163    #[tokio::test]
164    async fn test_zkevm_estimator_method_not_available() {
165        let mut mock_provider = MockEvmProviderTrait::new();
166        mock_provider
167            .expect_raw_request_dyn()
168            .with(
169                eq("zkevm_estimateGasPrice"),
170                eq(serde_json::Value::Array(vec![])),
171            )
172            .times(1)
173            .returning(|_, _| {
174                async {
175                    Err(ProviderError::RpcErrorCode {
176                        code: -32601,
177                        message: "Method not found".to_string(),
178                    })
179                }
180                .boxed()
181            });
182        mock_provider
183            .expect_get_gas_price()
184            .times(1)
185            .returning(|| async { Ok(20_000_000_000u128) }.boxed());
186
187        let fetcher = PolygonZkEvmGasPriceFetcher;
188        let network = create_zkevm_network();
189
190        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
191        assert!(result.is_ok());
192        assert_eq!(result.unwrap(), Some(20_000_000_000u128));
193    }
194
195    #[tokio::test]
196    async fn test_zkevm_estimator_invalid_response() {
197        let mut mock_provider = MockEvmProviderTrait::new();
198        mock_provider
199            .expect_raw_request_dyn()
200            .with(
201                eq("zkevm_estimateGasPrice"),
202                eq(serde_json::Value::Array(vec![])),
203            )
204            .times(1)
205            .returning(|_, _| {
206                async { Ok(serde_json::Value::Number(serde_json::Number::from(123))) }.boxed()
207            });
208        mock_provider
209            .expect_get_gas_price()
210            .times(1)
211            .returning(|| async { Ok(15_000_000_000u128) }.boxed());
212
213        let fetcher = PolygonZkEvmGasPriceFetcher;
214        let network = create_zkevm_network();
215
216        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
217        assert!(result.is_ok());
218        assert_eq!(result.unwrap(), Some(15_000_000_000u128));
219    }
220
221    #[tokio::test]
222    async fn test_zkevm_estimator_invalid_hex_response() {
223        let mut mock_provider = MockEvmProviderTrait::new();
224        mock_provider
225            .expect_raw_request_dyn()
226            .with(
227                eq("zkevm_estimateGasPrice"),
228                eq(serde_json::Value::Array(vec![])),
229            )
230            .times(1)
231            .returning(|_, _| {
232                async { Ok(serde_json::Value::String("invalid_hex".to_string())) }.boxed()
233            });
234        mock_provider
235            .expect_get_gas_price()
236            .times(1)
237            .returning(|| async { Ok(18_000_000_000u128) }.boxed());
238
239        let fetcher = PolygonZkEvmGasPriceFetcher;
240        let network = create_zkevm_network();
241
242        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
243        assert!(result.is_ok());
244        assert_eq!(result.unwrap(), Some(18_000_000_000u128));
245    }
246
247    #[tokio::test]
248    async fn test_zkevm_estimator_other_error() {
249        let mut mock_provider = MockEvmProviderTrait::new();
250        mock_provider
251            .expect_raw_request_dyn()
252            .with(
253                eq("zkevm_estimateGasPrice"),
254                eq(serde_json::Value::Array(vec![])),
255            )
256            .times(1)
257            .returning(|_, _| {
258                async { Err(ProviderError::Other("Network timeout".to_string())) }.boxed()
259            });
260        mock_provider
261            .expect_get_gas_price()
262            .times(1)
263            .returning(|| async { Ok(22_000_000_000u128) }.boxed());
264
265        let fetcher = PolygonZkEvmGasPriceFetcher;
266        let network = create_zkevm_network();
267
268        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
269        assert!(result.is_ok());
270        assert_eq!(result.unwrap(), Some(22_000_000_000u128));
271    }
272
273    #[tokio::test]
274    async fn test_zkevm_estimator_both_methods_fail() {
275        let mut mock_provider = MockEvmProviderTrait::new();
276        mock_provider
277            .expect_raw_request_dyn()
278            .with(
279                eq("zkevm_estimateGasPrice"),
280                eq(serde_json::Value::Array(vec![])),
281            )
282            .times(1)
283            .returning(|_, _| {
284                async { Err(ProviderError::Other("zkEVM method failed".to_string())) }.boxed()
285            });
286        mock_provider
287            .expect_get_gas_price()
288            .times(1)
289            .returning(|| async { Err(ProviderError::Timeout) }.boxed());
290
291        let fetcher = PolygonZkEvmGasPriceFetcher;
292        let network = create_zkevm_network();
293
294        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
295        assert!(result.is_err());
296        assert!(matches!(result.unwrap_err(), ProviderError::Timeout));
297    }
298
299    #[tokio::test]
300    async fn test_zkevm_estimator_hex_with_0x_prefix() {
301        let mut mock_provider = MockEvmProviderTrait::new();
302        mock_provider
303            .expect_raw_request_dyn()
304            .with(
305                eq("zkevm_estimateGasPrice"),
306                eq(serde_json::Value::Array(vec![])),
307            )
308            .times(1)
309            .returning(|_, _| {
310                async { Ok(serde_json::Value::String("0x3b9aca00".to_string())) }.boxed()
311            });
312
313        let fetcher = PolygonZkEvmGasPriceFetcher;
314        let network = create_zkevm_network();
315
316        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
317        assert!(result.is_ok());
318        assert_eq!(result.unwrap(), Some(1_000_000_000u128));
319    }
320
321    #[tokio::test]
322    async fn test_zkevm_estimator_hex_without_0x_prefix() {
323        let mut mock_provider = MockEvmProviderTrait::new();
324        mock_provider
325            .expect_raw_request_dyn()
326            .with(
327                eq("zkevm_estimateGasPrice"),
328                eq(serde_json::Value::Array(vec![])),
329            )
330            .times(1)
331            .returning(|_, _| {
332                async { Ok(serde_json::Value::String("3b9aca00".to_string())) }.boxed()
333            });
334
335        let fetcher = PolygonZkEvmGasPriceFetcher;
336        let network = create_zkevm_network();
337
338        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
339        assert!(result.is_ok());
340        assert_eq!(result.unwrap(), Some(1_000_000_000u128));
341    }
342
343    #[test]
344    fn test_parse_zkevm_response_valid_hex() {
345        let fetcher = PolygonZkEvmGasPriceFetcher;
346        let response = serde_json::Value::String("0x174876e800".to_string());
347
348        let result = fetcher.parse_zkevm_response(response, 1101);
349        assert!(result.is_ok());
350        assert_eq!(result.unwrap(), Some(100_000_000_000u128));
351    }
352
353    #[test]
354    fn test_parse_zkevm_response_invalid_format() {
355        let fetcher = PolygonZkEvmGasPriceFetcher;
356        let response = serde_json::Value::Number(serde_json::Number::from(123));
357
358        let result = fetcher.parse_zkevm_response(response, 1101);
359        assert!(result.is_ok());
360        assert_eq!(result.unwrap(), None);
361    }
362
363    #[test]
364    fn test_parse_zkevm_response_invalid_hex() {
365        let fetcher = PolygonZkEvmGasPriceFetcher;
366        let response = serde_json::Value::String("not_hex".to_string());
367
368        let result = fetcher.parse_zkevm_response(response, 1101);
369        assert!(result.is_ok());
370        assert_eq!(result.unwrap(), None);
371    }
372}