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!["https://zkevm-rpc.com".to_string()],
127            explorer_urls: None,
128            average_blocktime_ms: 2000,
129            is_testnet: false,
130            tags: vec![POLYGON_ZKEVM_TAG.to_string()],
131            chain_id: 1101,
132            required_confirmations: 1,
133            features: vec!["eip1559".to_string()],
134            symbol: "ETH".to_string(),
135            gas_price_cache: None,
136        }
137    }
138
139    #[tokio::test]
140    async fn test_zkevm_fetcher_success() {
141        let mut mock_provider = MockEvmProviderTrait::new();
142        mock_provider
143            .expect_raw_request_dyn()
144            .with(
145                eq("zkevm_estimateGasPrice"),
146                eq(serde_json::Value::Array(vec![])),
147            )
148            .times(1)
149            .returning(|_, _| {
150                async { Ok(serde_json::Value::String("0x174876e800".to_string())) }.boxed()
151            });
152
153        let fetcher = PolygonZkEvmGasPriceFetcher;
154        let network = create_zkevm_network();
155
156        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
157        assert!(result.is_ok());
158        assert_eq!(result.unwrap(), Some(100_000_000_000u128));
159    }
160
161    #[tokio::test]
162    async fn test_zkevm_estimator_method_not_available() {
163        let mut mock_provider = MockEvmProviderTrait::new();
164        mock_provider
165            .expect_raw_request_dyn()
166            .with(
167                eq("zkevm_estimateGasPrice"),
168                eq(serde_json::Value::Array(vec![])),
169            )
170            .times(1)
171            .returning(|_, _| {
172                async {
173                    Err(ProviderError::RpcErrorCode {
174                        code: -32601,
175                        message: "Method not found".to_string(),
176                    })
177                }
178                .boxed()
179            });
180        mock_provider
181            .expect_get_gas_price()
182            .times(1)
183            .returning(|| async { Ok(20_000_000_000u128) }.boxed());
184
185        let fetcher = PolygonZkEvmGasPriceFetcher;
186        let network = create_zkevm_network();
187
188        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
189        assert!(result.is_ok());
190        assert_eq!(result.unwrap(), Some(20_000_000_000u128));
191    }
192
193    #[tokio::test]
194    async fn test_zkevm_estimator_invalid_response() {
195        let mut mock_provider = MockEvmProviderTrait::new();
196        mock_provider
197            .expect_raw_request_dyn()
198            .with(
199                eq("zkevm_estimateGasPrice"),
200                eq(serde_json::Value::Array(vec![])),
201            )
202            .times(1)
203            .returning(|_, _| {
204                async { Ok(serde_json::Value::Number(serde_json::Number::from(123))) }.boxed()
205            });
206        mock_provider
207            .expect_get_gas_price()
208            .times(1)
209            .returning(|| async { Ok(15_000_000_000u128) }.boxed());
210
211        let fetcher = PolygonZkEvmGasPriceFetcher;
212        let network = create_zkevm_network();
213
214        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
215        assert!(result.is_ok());
216        assert_eq!(result.unwrap(), Some(15_000_000_000u128));
217    }
218
219    #[tokio::test]
220    async fn test_zkevm_estimator_invalid_hex_response() {
221        let mut mock_provider = MockEvmProviderTrait::new();
222        mock_provider
223            .expect_raw_request_dyn()
224            .with(
225                eq("zkevm_estimateGasPrice"),
226                eq(serde_json::Value::Array(vec![])),
227            )
228            .times(1)
229            .returning(|_, _| {
230                async { Ok(serde_json::Value::String("invalid_hex".to_string())) }.boxed()
231            });
232        mock_provider
233            .expect_get_gas_price()
234            .times(1)
235            .returning(|| async { Ok(18_000_000_000u128) }.boxed());
236
237        let fetcher = PolygonZkEvmGasPriceFetcher;
238        let network = create_zkevm_network();
239
240        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
241        assert!(result.is_ok());
242        assert_eq!(result.unwrap(), Some(18_000_000_000u128));
243    }
244
245    #[tokio::test]
246    async fn test_zkevm_estimator_other_error() {
247        let mut mock_provider = MockEvmProviderTrait::new();
248        mock_provider
249            .expect_raw_request_dyn()
250            .with(
251                eq("zkevm_estimateGasPrice"),
252                eq(serde_json::Value::Array(vec![])),
253            )
254            .times(1)
255            .returning(|_, _| {
256                async { Err(ProviderError::Other("Network timeout".to_string())) }.boxed()
257            });
258        mock_provider
259            .expect_get_gas_price()
260            .times(1)
261            .returning(|| async { Ok(22_000_000_000u128) }.boxed());
262
263        let fetcher = PolygonZkEvmGasPriceFetcher;
264        let network = create_zkevm_network();
265
266        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
267        assert!(result.is_ok());
268        assert_eq!(result.unwrap(), Some(22_000_000_000u128));
269    }
270
271    #[tokio::test]
272    async fn test_zkevm_estimator_both_methods_fail() {
273        let mut mock_provider = MockEvmProviderTrait::new();
274        mock_provider
275            .expect_raw_request_dyn()
276            .with(
277                eq("zkevm_estimateGasPrice"),
278                eq(serde_json::Value::Array(vec![])),
279            )
280            .times(1)
281            .returning(|_, _| {
282                async { Err(ProviderError::Other("zkEVM method failed".to_string())) }.boxed()
283            });
284        mock_provider
285            .expect_get_gas_price()
286            .times(1)
287            .returning(|| async { Err(ProviderError::Timeout) }.boxed());
288
289        let fetcher = PolygonZkEvmGasPriceFetcher;
290        let network = create_zkevm_network();
291
292        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
293        assert!(result.is_err());
294        assert!(matches!(result.unwrap_err(), ProviderError::Timeout));
295    }
296
297    #[tokio::test]
298    async fn test_zkevm_estimator_hex_with_0x_prefix() {
299        let mut mock_provider = MockEvmProviderTrait::new();
300        mock_provider
301            .expect_raw_request_dyn()
302            .with(
303                eq("zkevm_estimateGasPrice"),
304                eq(serde_json::Value::Array(vec![])),
305            )
306            .times(1)
307            .returning(|_, _| {
308                async { Ok(serde_json::Value::String("0x3b9aca00".to_string())) }.boxed()
309            });
310
311        let fetcher = PolygonZkEvmGasPriceFetcher;
312        let network = create_zkevm_network();
313
314        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
315        assert!(result.is_ok());
316        assert_eq!(result.unwrap(), Some(1_000_000_000u128));
317    }
318
319    #[tokio::test]
320    async fn test_zkevm_estimator_hex_without_0x_prefix() {
321        let mut mock_provider = MockEvmProviderTrait::new();
322        mock_provider
323            .expect_raw_request_dyn()
324            .with(
325                eq("zkevm_estimateGasPrice"),
326                eq(serde_json::Value::Array(vec![])),
327            )
328            .times(1)
329            .returning(|_, _| {
330                async { Ok(serde_json::Value::String("3b9aca00".to_string())) }.boxed()
331            });
332
333        let fetcher = PolygonZkEvmGasPriceFetcher;
334        let network = create_zkevm_network();
335
336        let result = fetcher.fetch_gas_price(&mock_provider, &network).await;
337        assert!(result.is_ok());
338        assert_eq!(result.unwrap(), Some(1_000_000_000u128));
339    }
340
341    #[test]
342    fn test_parse_zkevm_response_valid_hex() {
343        let fetcher = PolygonZkEvmGasPriceFetcher;
344        let response = serde_json::Value::String("0x174876e800".to_string());
345
346        let result = fetcher.parse_zkevm_response(response, 1101);
347        assert!(result.is_ok());
348        assert_eq!(result.unwrap(), Some(100_000_000_000u128));
349    }
350
351    #[test]
352    fn test_parse_zkevm_response_invalid_format() {
353        let fetcher = PolygonZkEvmGasPriceFetcher;
354        let response = serde_json::Value::Number(serde_json::Number::from(123));
355
356        let result = fetcher.parse_zkevm_response(response, 1101);
357        assert!(result.is_ok());
358        assert_eq!(result.unwrap(), None);
359    }
360
361    #[test]
362    fn test_parse_zkevm_response_invalid_hex() {
363        let fetcher = PolygonZkEvmGasPriceFetcher;
364        let response = serde_json::Value::String("not_hex".to_string());
365
366        let result = fetcher.parse_zkevm_response(response, 1101);
367        assert!(result.is_ok());
368        assert_eq!(result.unwrap(), None);
369    }
370}