openzeppelin_relayer/services/gas/fetchers/
polygon_zkevm.rs1use crate::{
8 models::EvmNetwork,
9 services::provider::{evm::EvmProviderTrait, ProviderError},
10};
11use tracing::{debug, error, info, warn};
12
13#[derive(Debug, Clone)]
15pub struct PolygonZkEvmGasPriceFetcher;
16
17impl PolygonZkEvmGasPriceFetcher {
18 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 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 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 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}