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