openzeppelin_relayer/services/gas/handlers/
polygon_zkevm.rs

1use crate::{
2    domain::evm::PriceParams,
3    models::{EvmTransactionData, TransactionError, U256},
4    services::provider::{evm::EvmProviderTrait, ProviderError},
5    utils::{EthereumJsonRpcError, StandardJsonRpcError},
6};
7use serde_json;
8
9/// Builds zkEVM RPC transaction parameters from EvmTransactionData.
10///
11/// This helper function converts transaction data into the JSON format expected
12/// by zkEVM RPC methods like `zkevm_estimateFee`.
13///
14/// # Arguments
15/// * `tx` - The transaction data to convert
16///
17/// # Returns
18/// A JSON object with hex-encoded transaction parameters
19fn build_zkevm_transaction_params(tx: &EvmTransactionData) -> serde_json::Value {
20    serde_json::json!({
21        "from": tx.from,
22        "to": tx.to.clone(),
23        "value": format!("0x{:x}", tx.value),
24        "data": tx.data.as_ref().map(|d| {
25            if d.starts_with("0x") { d.clone() } else { format!("0x{d}") }
26        }).unwrap_or("0x".to_string()),
27        "gas": tx.gas_limit.map(|g| format!("0x{g:x}")),
28        "gasPrice": tx.gas_price.map(|gp| format!("0x{gp:x}")),
29        "maxFeePerGas": tx.max_fee_per_gas.map(|mfpg| format!("0x{mfpg:x}")),
30        "maxPriorityFeePerGas": tx.max_priority_fee_per_gas.map(|mpfpg| format!("0x{mpfpg:x}")),
31    })
32}
33
34/// Price parameter handler for Polygon zkEVM networks
35///
36/// This implementation uses the custom zkEVM endpoints introduced by Polygon to solve
37/// the gas estimation accuracy problem. As documented in Polygon's blog post
38/// (https://polygon.technology/blog/new-custom-endpoint-for-dapps-on-polygon-zkevm),
39/// these endpoints provide up to 20% more accurate fee estimation compared to standard methods.
40#[derive(Debug, Clone)]
41pub struct PolygonZKEvmPriceHandler<P> {
42    provider: P,
43}
44
45impl<P: EvmProviderTrait> PolygonZKEvmPriceHandler<P> {
46    pub fn new(provider: P) -> Self {
47        Self { provider }
48    }
49
50    /// zkEVM-specific method to estimate fee for a transaction using the native zkEVM endpoint.
51    ///
52    /// This method calls `zkevm_estimateFee` which provides more accurate
53    /// fee estimation that includes L1 data availability costs for Polygon zkEVM networks.
54    ///
55    /// # Arguments
56    /// * `tx` - The transaction request to estimate fee for
57    async fn zkevm_estimate_fee(&self, tx: &EvmTransactionData) -> Result<U256, ProviderError> {
58        let tx_params = build_zkevm_transaction_params(tx);
59
60        let result = self
61            .provider
62            .raw_request_dyn("zkevm_estimateFee", serde_json::json!([tx_params]))
63            .await?;
64
65        let fee_hex = result
66            .as_str()
67            .ok_or_else(|| ProviderError::Other("Invalid fee response".to_string()))?;
68
69        let fee = U256::from_str_radix(fee_hex.trim_start_matches("0x"), 16)
70            .map_err(|e| ProviderError::Other(format!("Failed to parse fee: {e}")))?;
71
72        Ok(fee)
73    }
74
75    pub async fn handle_price_params(
76        &self,
77        tx: &EvmTransactionData,
78        mut original_params: PriceParams,
79    ) -> Result<PriceParams, TransactionError> {
80        // Use zkEVM-specific endpoints for accurate pricing (recommended by Polygon)
81        let zkevm_fee_estimate = self.zkevm_estimate_fee(tx).await;
82
83        // Handle case where zkEVM methods are not available on this rpc or network
84        let zkevm_fee_estimate = match zkevm_fee_estimate {
85            Err(ProviderError::RpcErrorCode { code, .. })
86                if code == StandardJsonRpcError::MethodNotFound.code()
87                    || code == EthereumJsonRpcError::MethodNotSupported.code() =>
88            {
89                // zkEVM methods not supported on this rpc or network, return original params
90                return Ok(original_params);
91            }
92            Ok(fee_estimate) => fee_estimate,
93            Err(e) => {
94                return Err(TransactionError::UnexpectedError(format!(
95                    "Failed to estimate zkEVM fee: {e}"
96                )))
97            }
98        };
99
100        // The zkEVM fee estimate provides a more accurate total cost calculation
101        // that includes both L2 execution costs and L1 data availability costs
102        original_params.total_cost = zkevm_fee_estimate;
103
104        Ok(original_params)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::{models::U256, services::provider::evm::MockEvmProviderTrait};
112    use mockall::predicate::*;
113
114    #[tokio::test]
115    async fn test_polygon_zkevm_price_handler_legacy() {
116        // Create mock provider
117        let mut mock_provider = MockEvmProviderTrait::new();
118
119        // Mock zkevm_estimateFee to return 0.0005 ETH fee
120        mock_provider
121            .expect_raw_request_dyn()
122            .with(eq("zkevm_estimateFee"), always())
123            .returning(|_, _| {
124                Box::pin(async move {
125                    Ok(serde_json::json!("0x1c6bf52634000")) // 500_000_000_000_000 in hex
126                })
127            });
128
129        let handler = PolygonZKEvmPriceHandler::new(mock_provider);
130
131        // Create test transaction with data
132        let tx = EvmTransactionData {
133            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
134            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
135            value: U256::from(1_000_000_000_000_000_000u128), // 1 ETH
136            data: Some("0x1234567890abcdef".to_string()),     // 8 bytes of data
137            gas_limit: Some(21000),
138            gas_price: Some(20_000_000_000), // 20 Gwei
139            max_fee_per_gas: None,
140            max_priority_fee_per_gas: None,
141            speed: None,
142            nonce: None,
143            chain_id: 1101,
144            hash: None,
145            signature: None,
146            raw: None,
147        };
148
149        // Create original price params (legacy)
150        let original_params = PriceParams {
151            gas_price: Some(20_000_000_000), // 20 Gwei
152            max_fee_per_gas: None,
153            max_priority_fee_per_gas: None,
154            is_min_bumped: None,
155            extra_fee: None,
156            total_cost: U256::ZERO,
157        };
158
159        // Handle the price params
160        let result = handler.handle_price_params(&tx, original_params).await;
161
162        assert!(result.is_ok());
163        let handled_params = result.unwrap();
164
165        // Verify that the original gas price remains unchanged
166        assert_eq!(handled_params.gas_price.unwrap(), 20_000_000_000); // Should remain original
167
168        // Verify that total cost was set from zkEVM fee estimate
169        assert_eq!(
170            handled_params.total_cost,
171            U256::from(500_000_000_000_000u128)
172        );
173    }
174
175    #[tokio::test]
176    async fn test_polygon_zkevm_price_handler_eip1559() {
177        // Create mock provider
178        let mut mock_provider = MockEvmProviderTrait::new();
179
180        // Mock zkevm_estimateFee to return 0.00075 ETH fee
181        mock_provider
182            .expect_raw_request_dyn()
183            .with(eq("zkevm_estimateFee"), always())
184            .returning(|_, _| {
185                Box::pin(async move {
186                    Ok(serde_json::json!("0x2aa1efb94e000")) // 750_000_000_000_000 in hex
187                })
188            });
189
190        let handler = PolygonZKEvmPriceHandler::new(mock_provider);
191
192        // Create test transaction with data
193        let tx = EvmTransactionData {
194            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
195            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
196            value: U256::from(1_000_000_000_000_000_000u128), // 1 ETH
197            data: Some("0x1234567890abcdef".to_string()),     // 8 bytes of data
198            gas_limit: Some(21000),
199            gas_price: None,
200            max_fee_per_gas: Some(30_000_000_000), // 30 Gwei
201            max_priority_fee_per_gas: Some(2_000_000_000), // 2 Gwei
202            speed: None,
203            nonce: None,
204            chain_id: 1101,
205            hash: None,
206            signature: None,
207            raw: None,
208        };
209
210        // Create original price params (EIP1559)
211        let original_params = PriceParams {
212            gas_price: None,
213            max_fee_per_gas: Some(30_000_000_000), // 30 Gwei
214            max_priority_fee_per_gas: Some(2_000_000_000), // 2 Gwei
215            is_min_bumped: None,
216            extra_fee: None,
217            total_cost: U256::ZERO,
218        };
219
220        // Handle the price params
221        let result = handler.handle_price_params(&tx, original_params).await;
222
223        assert!(result.is_ok());
224        let handled_params = result.unwrap();
225
226        // Verify that the original EIP1559 fees remain unchanged
227        assert_eq!(handled_params.max_fee_per_gas.unwrap(), 30_000_000_000); // Should remain original
228        assert_eq!(
229            handled_params.max_priority_fee_per_gas.unwrap(),
230            2_000_000_000
231        ); // Should remain original
232
233        // Verify that total cost was set from zkEVM fee estimate
234        assert_eq!(
235            handled_params.total_cost,
236            U256::from(750_000_000_000_000u128)
237        );
238    }
239
240    #[tokio::test]
241    async fn test_polygon_zkevm_fee_estimation_integration() {
242        // Test with empty data - create mock provider for no data scenario
243        let mut mock_provider_no_data = MockEvmProviderTrait::new();
244        mock_provider_no_data
245            .expect_raw_request_dyn()
246            .with(eq("zkevm_estimateFee"), always())
247            .returning(|_, _| {
248                Box::pin(async move {
249                    Ok(serde_json::json!("0xbefe6f672000")) // 210_000_000_000_000 in hex
250                })
251            });
252
253        let handler_no_data = PolygonZKEvmPriceHandler::new(mock_provider_no_data);
254
255        let empty_tx = EvmTransactionData {
256            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
257            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
258            value: U256::from(1_000_000_000_000_000_000u128),
259            data: None,
260            gas_limit: Some(21000),
261            gas_price: Some(15_000_000_000), // Lower than zkEVM estimate
262            max_fee_per_gas: None,
263            max_priority_fee_per_gas: None,
264            speed: None,
265            nonce: None,
266            chain_id: 1101,
267            hash: None,
268            signature: None,
269            raw: None,
270        };
271
272        let original_params = PriceParams {
273            gas_price: Some(15_000_000_000),
274            max_fee_per_gas: None,
275            max_priority_fee_per_gas: None,
276            is_min_bumped: None,
277            extra_fee: None,
278            total_cost: U256::ZERO,
279        };
280
281        let result = handler_no_data
282            .handle_price_params(&empty_tx, original_params)
283            .await;
284        assert!(result.is_ok());
285        let handled_params = result.unwrap();
286
287        // Gas price should remain unchanged (already set)
288        assert_eq!(handled_params.gas_price.unwrap(), 15_000_000_000);
289        assert_eq!(
290            handled_params.total_cost,
291            U256::from(210_000_000_000_000u128)
292        );
293
294        // Test with data - create mock provider for data scenario
295        let mut mock_provider_with_data = MockEvmProviderTrait::new();
296        mock_provider_with_data
297            .expect_raw_request_dyn()
298            .with(eq("zkevm_estimateFee"), always())
299            .returning(|_, _| {
300                Box::pin(async move {
301                    Ok(serde_json::json!("0x16bcc41e90000")) // 400_000_000_000_000 in hex (correct)
302                })
303            });
304
305        let handler_with_data = PolygonZKEvmPriceHandler::new(mock_provider_with_data);
306
307        let data_tx = EvmTransactionData {
308            data: Some("0x1234567890abcdef".to_string()), // 8 bytes
309            ..empty_tx
310        };
311
312        let original_params_with_data = PriceParams {
313            gas_price: Some(15_000_000_000),
314            max_fee_per_gas: None,
315            max_priority_fee_per_gas: None,
316            is_min_bumped: None,
317            extra_fee: None,
318            total_cost: U256::ZERO,
319        };
320
321        let result_with_data = handler_with_data
322            .handle_price_params(&data_tx, original_params_with_data)
323            .await;
324        assert!(result_with_data.is_ok());
325        let handled_params_with_data = result_with_data.unwrap();
326
327        // Should have higher total cost due to data
328        assert!(handled_params_with_data.total_cost > handled_params.total_cost);
329        assert_eq!(
330            handled_params_with_data.total_cost,
331            U256::from(400_000_000_000_000u128)
332        );
333    }
334
335    #[tokio::test]
336    async fn test_polygon_zkevm_uses_gas_price_when_not_set() {
337        // Create mock provider
338        let mut mock_provider = MockEvmProviderTrait::new();
339        mock_provider
340            .expect_raw_request_dyn()
341            .with(eq("zkevm_estimateFee"), always())
342            .returning(|_, _| {
343                Box::pin(async move {
344                    Ok(serde_json::json!("0x221b262dd8000")) // 600_000_000_000_000 in hex
345                })
346            });
347
348        let handler = PolygonZKEvmPriceHandler::new(mock_provider);
349
350        // Test with no gas price set initially
351        let tx = EvmTransactionData {
352            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
353            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
354            value: U256::from(1_000_000_000_000_000_000u128),
355            data: Some("0x1234".to_string()),
356            gas_limit: Some(21000),
357            gas_price: None, // No gas price set
358            max_fee_per_gas: None,
359            max_priority_fee_per_gas: None,
360            speed: None,
361            nonce: None,
362            chain_id: 1101,
363            hash: None,
364            signature: None,
365            raw: None,
366        };
367
368        let original_params = PriceParams {
369            gas_price: None, // No gas price set
370            max_fee_per_gas: None,
371            max_priority_fee_per_gas: None,
372            is_min_bumped: None,
373            extra_fee: None,
374            total_cost: U256::ZERO,
375        };
376
377        let result = handler.handle_price_params(&tx, original_params).await;
378        assert!(result.is_ok());
379        let handled_params = result.unwrap();
380
381        // Gas price should remain None since handler no longer sets it
382        assert!(handled_params.gas_price.is_none());
383        assert_eq!(
384            handled_params.total_cost,
385            U256::from(600_000_000_000_000u128)
386        );
387    }
388
389    #[tokio::test]
390    async fn test_polygon_zkevm_method_not_available() {
391        // Create mock provider that returns MethodNotFound error
392        let mut mock_provider = MockEvmProviderTrait::new();
393        mock_provider
394            .expect_raw_request_dyn()
395            .with(eq("zkevm_estimateFee"), always())
396            .returning(|_, _| {
397                Box::pin(async move {
398                    Err(ProviderError::RpcErrorCode {
399                        code: StandardJsonRpcError::MethodNotFound.code(),
400                        message: "Method not found".to_string(),
401                    })
402                })
403            });
404
405        let handler = PolygonZKEvmPriceHandler::new(mock_provider);
406
407        let tx = EvmTransactionData {
408            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
409            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
410            value: U256::from(1_000_000_000_000_000_000u128),
411            data: Some("0x1234".to_string()),
412            gas_limit: Some(21000),
413            gas_price: Some(15_000_000_000), // 15 Gwei
414            max_fee_per_gas: None,
415            max_priority_fee_per_gas: None,
416            speed: None,
417            nonce: None,
418            chain_id: 1101,
419            hash: None,
420            signature: None,
421            raw: None,
422        };
423
424        let original_params = PriceParams {
425            gas_price: Some(15_000_000_000),
426            max_fee_per_gas: None,
427            max_priority_fee_per_gas: None,
428            is_min_bumped: None,
429            extra_fee: None,
430            total_cost: U256::from(100_000),
431        };
432
433        let result = handler
434            .handle_price_params(&tx, original_params.clone())
435            .await;
436
437        assert!(result.is_ok());
438        let handled_params = result.unwrap();
439
440        // Should return original params unchanged when zkEVM methods are not available
441        assert_eq!(handled_params.gas_price, original_params.gas_price);
442        assert_eq!(
443            handled_params.max_fee_per_gas,
444            original_params.max_fee_per_gas
445        );
446        assert_eq!(
447            handled_params.max_priority_fee_per_gas,
448            original_params.max_priority_fee_per_gas
449        );
450        assert_eq!(handled_params.total_cost, original_params.total_cost);
451    }
452
453    #[tokio::test]
454    async fn test_polygon_zkevm_partial_method_not_available() {
455        // Create mock provider that returns MethodNotSupported error
456        let mut mock_provider = MockEvmProviderTrait::new();
457        mock_provider
458            .expect_raw_request_dyn()
459            .with(eq("zkevm_estimateFee"), always())
460            .returning(|_, _| {
461                Box::pin(async move {
462                    Err(ProviderError::RpcErrorCode {
463                        code: EthereumJsonRpcError::MethodNotSupported.code(),
464                        message: "Method not supported".to_string(),
465                    })
466                })
467            });
468
469        let handler = PolygonZKEvmPriceHandler::new(mock_provider);
470
471        let tx = EvmTransactionData {
472            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
473            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
474            value: U256::from(1_000_000_000_000_000_000u128),
475            data: Some("0x1234".to_string()),
476            gas_limit: Some(21000),
477            gas_price: Some(15_000_000_000),
478            max_fee_per_gas: None,
479            max_priority_fee_per_gas: None,
480            speed: None,
481            nonce: None,
482            chain_id: 1101,
483            hash: None,
484            signature: None,
485            raw: None,
486        };
487
488        let original_params = PriceParams {
489            gas_price: Some(15_000_000_000),
490            max_fee_per_gas: None,
491            max_priority_fee_per_gas: None,
492            is_min_bumped: None,
493            extra_fee: None,
494            total_cost: U256::from(100_000),
495        };
496
497        let result = handler
498            .handle_price_params(&tx, original_params.clone())
499            .await;
500
501        assert!(result.is_ok());
502        let handled_params = result.unwrap();
503
504        // Should return original params unchanged when any zkEVM method is not available
505        assert_eq!(handled_params.gas_price, original_params.gas_price);
506        assert_eq!(
507            handled_params.max_fee_per_gas,
508            original_params.max_fee_per_gas
509        );
510        assert_eq!(
511            handled_params.max_priority_fee_per_gas,
512            original_params.max_priority_fee_per_gas
513        );
514        assert_eq!(handled_params.total_cost, original_params.total_cost);
515    }
516
517    #[test]
518    fn test_build_zkevm_transaction_params() {
519        // Test with complete transaction data
520        let tx = EvmTransactionData {
521            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
522            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
523            value: U256::from(1000000000000000000u64), // 1 ETH
524            data: Some("0x1234567890abcdef".to_string()),
525            gas_limit: Some(21000),
526            gas_price: Some(20000000000),               // 20 Gwei
527            max_fee_per_gas: Some(30000000000),         // 30 Gwei
528            max_priority_fee_per_gas: Some(2000000000), // 2 Gwei
529            speed: None,
530            nonce: Some(42),
531            chain_id: 1101,
532            hash: None,
533            signature: None,
534            raw: None,
535        };
536
537        let params = build_zkevm_transaction_params(&tx);
538
539        // Verify the structure and values
540        assert_eq!(params["from"], "0x742d35Cc6634C0532925a3b844Bc454e4438f44e");
541        assert_eq!(params["to"], "0x742d35Cc6634C0532925a3b844Bc454e4438f44f");
542        assert_eq!(params["value"], "0xde0b6b3a7640000"); // 1 ETH in hex
543        assert_eq!(params["data"], "0x1234567890abcdef");
544        assert_eq!(params["gas"], "0x5208"); // 21000 in hex
545        assert_eq!(params["gasPrice"], "0x4a817c800"); // 20 Gwei in hex
546        assert_eq!(params["maxFeePerGas"], "0x6fc23ac00"); // 30 Gwei in hex
547        assert_eq!(params["maxPriorityFeePerGas"], "0x77359400"); // 2 Gwei in hex
548
549        // Test with minimal transaction data
550        let minimal_tx = EvmTransactionData {
551            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
552            to: None,
553            value: U256::ZERO,
554            data: None,
555            gas_limit: None,
556            gas_price: None,
557            max_fee_per_gas: None,
558            max_priority_fee_per_gas: None,
559            speed: None,
560            nonce: None,
561            chain_id: 1101,
562            hash: None,
563            signature: None,
564            raw: None,
565        };
566
567        let minimal_params = build_zkevm_transaction_params(&minimal_tx);
568
569        assert_eq!(
570            minimal_params["from"],
571            "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
572        );
573        assert_eq!(minimal_params["to"], serde_json::Value::Null); // None becomes JSON null
574        assert_eq!(minimal_params["value"], "0x0");
575        assert_eq!(minimal_params["data"], "0x");
576        assert_eq!(minimal_params["gas"], serde_json::Value::Null);
577        assert_eq!(minimal_params["gasPrice"], serde_json::Value::Null);
578        assert_eq!(minimal_params["maxFeePerGas"], serde_json::Value::Null);
579        assert_eq!(
580            minimal_params["maxPriorityFeePerGas"],
581            serde_json::Value::Null
582        );
583
584        // Test data field normalization (without 0x prefix)
585        let tx_without_prefix = EvmTransactionData {
586            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
587            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
588            value: U256::ZERO,
589            data: Some("abcdef1234".to_string()), // No 0x prefix
590            gas_limit: None,
591            gas_price: None,
592            max_fee_per_gas: None,
593            max_priority_fee_per_gas: None,
594            speed: None,
595            nonce: None,
596            chain_id: 1101,
597            hash: None,
598            signature: None,
599            raw: None,
600        };
601
602        let params_no_prefix = build_zkevm_transaction_params(&tx_without_prefix);
603        assert_eq!(params_no_prefix["data"], "0xabcdef1234"); // Should add 0x prefix
604    }
605}