openzeppelin_relayer/utils/
transaction.rs

1use crate::constants::{
2    COMPLEX_GAS_LIMIT, DEFAULT_GAS_LIMIT, DEFAULT_TRANSACTION_SPEED, ERC20_TRANSFER_GAS_LIMIT,
3    ERC721_TRANSFER_GAS_LIMIT, GAS_TX_CREATE_CONTRACT, GAS_TX_DATA_NONZERO, GAS_TX_DATA_ZERO,
4};
5use crate::models::evm::Speed;
6use crate::models::{EvmTransactionData, EvmTransactionRequest};
7use crate::utils::time::minutes_ms;
8
9/// Gets the resubmit timeout for a given speed
10/// Returns the timeout in milliseconds based on the speed:
11/// - SafeLow: 10 minutes
12/// - Average: 5 minutes
13/// - Fast: 3 minutes
14/// - Fastest: 2 minutes
15///   If no speed is provided, uses the default transaction speed
16pub fn get_resubmit_timeout_for_speed(speed: &Option<Speed>) -> i64 {
17    let speed_value = speed.clone().unwrap_or(DEFAULT_TRANSACTION_SPEED);
18
19    match speed_value {
20        Speed::SafeLow => minutes_ms(10),
21        Speed::Average => minutes_ms(5),
22        Speed::Fast => minutes_ms(3),
23        Speed::Fastest => minutes_ms(2),
24    }
25}
26
27/// Calculates the resubmit age with exponential backoff
28///
29/// # Arguments
30/// * `timeout` - The base timeout in milliseconds
31/// * `attempts` - The number of attempts made so far
32///
33/// # Returns
34/// The new timeout with exponential backoff applied: timeout * 2^(attempts-1)
35pub fn get_resubmit_timeout_with_backoff(timeout: i64, attempts: usize) -> i64 {
36    if attempts <= 1 {
37        timeout
38    } else {
39        timeout * 2_i64.pow((attempts - 1) as u32)
40    }
41}
42
43/// Gets the default gas limit for a given transaction
44///
45/// # Arguments
46/// * `tx` - The transaction data
47///
48/// # Returns
49/// The default gas limit for the transaction
50pub fn get_evm_default_gas_limit_for_tx(tx: &EvmTransactionData) -> u64 {
51    if tx.data.is_none() {
52        DEFAULT_GAS_LIMIT
53    } else if tx.data.as_ref().unwrap().starts_with("0xa9059cbb") {
54        ERC20_TRANSFER_GAS_LIMIT
55    } else if tx.data.as_ref().unwrap().starts_with("0x23b872dd") {
56        ERC721_TRANSFER_GAS_LIMIT
57    } else {
58        COMPLEX_GAS_LIMIT
59    }
60}
61
62/// Calculates the intrinsic gas for a given transaction
63///
64/// # Arguments
65/// * `tx` - The transaction data
66///
67/// # Returns
68/// The intrinsic gas for the transaction
69pub fn calculate_intrinsic_gas(tx: &EvmTransactionRequest) -> u64 {
70    let base_gas = if tx.to.is_none() {
71        GAS_TX_CREATE_CONTRACT
72    } else {
73        DEFAULT_GAS_LIMIT
74    };
75
76    let data_gas = match &tx.data {
77        Some(data_str) => {
78            let hex_str = data_str.strip_prefix("0x").unwrap_or(data_str);
79            hex::decode(hex_str)
80                .map(|bytes| calculate_data_gas(&bytes))
81                .unwrap_or(0)
82        }
83        None => 0,
84    };
85
86    base_gas + data_gas
87}
88
89/// Calculates the gas for a given transaction data
90///
91/// # Arguments
92/// * `data` - The transaction data
93///
94/// # Returns
95/// The gas for the transaction data
96fn calculate_data_gas(data: &[u8]) -> u64 {
97    let mut gas = 0;
98    for &byte in data {
99        if byte == 0 {
100            gas += GAS_TX_DATA_ZERO;
101        } else {
102            gas += GAS_TX_DATA_NONZERO;
103        }
104    }
105    gas
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::models::evm::Speed;
112    use crate::models::EvmTransactionData;
113
114    #[test]
115    fn test_get_resubmit_timeout_for_speed() {
116        // Test with existing speeds
117        assert_eq!(
118            get_resubmit_timeout_for_speed(&Some(Speed::SafeLow)),
119            minutes_ms(10)
120        );
121        assert_eq!(
122            get_resubmit_timeout_for_speed(&Some(Speed::Average)),
123            minutes_ms(5)
124        );
125        assert_eq!(
126            get_resubmit_timeout_for_speed(&Some(Speed::Fast)),
127            minutes_ms(3)
128        );
129        assert_eq!(
130            get_resubmit_timeout_for_speed(&Some(Speed::Fastest)),
131            minutes_ms(2)
132        );
133
134        // Test with None speed (should return default)
135        assert_eq!(
136            get_resubmit_timeout_for_speed(&None),
137            minutes_ms(3) // DEFAULT_TRANSACTION_SPEED is Speed::Fast
138        );
139    }
140
141    #[test]
142    fn test_get_resubmit_timeout_with_backoff() {
143        let base_timeout = 300000; // 5 minutes in ms
144
145        // First attempt - no backoff
146        assert_eq!(get_resubmit_timeout_with_backoff(base_timeout, 1), 300000);
147
148        // Second attempt - 2x backoff
149        assert_eq!(get_resubmit_timeout_with_backoff(base_timeout, 2), 600000);
150
151        // Third attempt - 4x backoff
152        assert_eq!(get_resubmit_timeout_with_backoff(base_timeout, 3), 1200000);
153
154        // Fourth attempt - 8x backoff
155        assert_eq!(get_resubmit_timeout_with_backoff(base_timeout, 4), 2400000);
156
157        // Edge case - attempt 0 should be treated as attempt 1
158        assert_eq!(get_resubmit_timeout_with_backoff(base_timeout, 0), 300000);
159    }
160
161    #[test]
162    fn test_get_evm_default_gas_limit_for_tx_no_data() {
163        let tx = EvmTransactionData {
164            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
165            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
166            value: crate::models::U256::from(1000000000000000000u128),
167            data: None,
168            gas_limit: None,
169            gas_price: Some(20_000_000_000),
170            nonce: Some(1),
171            chain_id: 1,
172            hash: None,
173            signature: None,
174            speed: Some(Speed::Average),
175            max_fee_per_gas: None,
176            max_priority_fee_per_gas: None,
177            raw: None,
178        };
179
180        assert_eq!(get_evm_default_gas_limit_for_tx(&tx), DEFAULT_GAS_LIMIT);
181    }
182
183    #[test]
184    fn test_get_evm_default_gas_limit_for_tx_erc20_transfer() {
185        let tx = EvmTransactionData {
186            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
187            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
188            value: crate::models::U256::from(0u128),
189            data: Some("0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000".to_string()),
190            gas_limit: None,
191            gas_price: Some(20_000_000_000),
192            nonce: Some(1),
193            chain_id: 1,
194            hash: None,
195            signature: None,
196            speed: Some(Speed::Average),
197            max_fee_per_gas: None,
198            max_priority_fee_per_gas: None,
199            raw: None,
200        };
201
202        assert_eq!(
203            get_evm_default_gas_limit_for_tx(&tx),
204            ERC20_TRANSFER_GAS_LIMIT
205        );
206    }
207
208    #[test]
209    fn test_get_evm_default_gas_limit_for_tx_transfer_from() {
210        let tx = EvmTransactionData {
211            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
212            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
213            value: crate::models::U256::from(0u128),
214            data: Some("0x23b872dd000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000005aaeb6053f3e94c9b9a09f33669435e7ef1beaed0000000000000000000000000000000000000000000000000de0b6b3a7640000".to_string()),
215            gas_limit: None,
216            gas_price: Some(20_000_000_000),
217            nonce: Some(1),
218            chain_id: 1,
219            hash: None,
220            signature: None,
221            speed: Some(Speed::Average),
222            max_fee_per_gas: None,
223            max_priority_fee_per_gas: None,
224            raw: None,
225        };
226
227        assert_eq!(
228            get_evm_default_gas_limit_for_tx(&tx),
229            ERC721_TRANSFER_GAS_LIMIT
230        );
231    }
232
233    #[test]
234    fn test_get_evm_default_gas_limit_for_tx_complex_transaction() {
235        let tx = EvmTransactionData {
236            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
237            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
238            value: crate::models::U256::from(0u128),
239            data: Some("0x095ea7b3000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000".to_string()),
240            gas_limit: None,
241            gas_price: Some(20_000_000_000),
242            nonce: Some(1),
243            chain_id: 1,
244            hash: None,
245            signature: None,
246            speed: Some(Speed::Average),
247            max_fee_per_gas: None,
248            max_priority_fee_per_gas: None,
249            raw: None,
250        };
251
252        assert_eq!(get_evm_default_gas_limit_for_tx(&tx), COMPLEX_GAS_LIMIT);
253    }
254
255    #[test]
256    fn test_get_evm_default_gas_limit_for_tx_empty_data() {
257        let tx = EvmTransactionData {
258            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
259            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
260            value: crate::models::U256::from(1000000000000000000u128),
261            data: Some("0x".to_string()),
262            gas_limit: None,
263            gas_price: Some(20_000_000_000),
264            nonce: Some(1),
265            chain_id: 1,
266            hash: None,
267            signature: None,
268            speed: Some(Speed::Average),
269            max_fee_per_gas: None,
270            max_priority_fee_per_gas: None,
271            raw: None,
272        };
273
274        assert_eq!(get_evm_default_gas_limit_for_tx(&tx), COMPLEX_GAS_LIMIT);
275    }
276
277    #[test]
278    fn test_get_evm_default_gas_limit_for_tx_malformed_data() {
279        let tx = EvmTransactionData {
280            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
281            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
282            value: crate::models::U256::from(0u128),
283            data: Some("0xa9059c".to_string()), // Short data that starts with ERC20 transfer but is incomplete
284            gas_limit: None,
285            gas_price: Some(20_000_000_000),
286            nonce: Some(1),
287            chain_id: 1,
288            hash: None,
289            signature: None,
290            speed: Some(Speed::Average),
291            max_fee_per_gas: None,
292            max_priority_fee_per_gas: None,
293            raw: None,
294        };
295
296        assert_eq!(get_evm_default_gas_limit_for_tx(&tx), COMPLEX_GAS_LIMIT);
297    }
298
299    #[test]
300    fn test_get_evm_default_gas_limit_for_tx_partial_signature_match() {
301        // Test with data that starts with ERC20 transfer signature but has additional data
302        let tx = EvmTransactionData {
303            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
304            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
305            value: crate::models::U256::from(0u128),
306            data: Some("0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000001".to_string()),
307            gas_limit: None,
308            gas_price: Some(20_000_000_000),
309            nonce: Some(1),
310            chain_id: 1,
311            hash: None,
312            signature: None,
313            speed: Some(Speed::Average),
314            max_fee_per_gas: None,
315            max_priority_fee_per_gas: None,
316            raw: None,
317        };
318
319        // Should still match ERC20 transfer since it starts with the signature
320        assert_eq!(
321            get_evm_default_gas_limit_for_tx(&tx),
322            ERC20_TRANSFER_GAS_LIMIT
323        );
324    }
325
326    #[test]
327    fn test_get_evm_default_gas_limit_for_tx_case_sensitivity() {
328        // Test with uppercase hex data
329        let tx = EvmTransactionData {
330            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
331            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
332            value: crate::models::U256::from(0u128),
333            data: Some("0xA9059CBB000000000000000000000000742D35CC6634C0532925A3B844BC454E4438F44E0000000000000000000000000000000000000000000000000DE0B6B3A7640000".to_string()),
334            gas_limit: None,
335            gas_price: Some(20_000_000_000),
336            nonce: Some(1),
337            chain_id: 1,
338            hash: None,
339            signature: None,
340            speed: Some(Speed::Average),
341            max_fee_per_gas: None,
342            max_priority_fee_per_gas: None,
343            raw: None,
344        };
345
346        // Should not match since the function signature is case-sensitive
347        assert_eq!(get_evm_default_gas_limit_for_tx(&tx), COMPLEX_GAS_LIMIT);
348    }
349
350    #[test]
351    fn test_calculate_data_gas_empty_data() {
352        let data = &[];
353        assert_eq!(calculate_data_gas(data), 0);
354    }
355
356    #[test]
357    fn test_calculate_data_gas_all_zero_bytes() {
358        let data = &[0x00, 0x00, 0x00, 0x00];
359        // 4 zero bytes * 4 gas per zero byte = 16 gas
360        assert_eq!(calculate_data_gas(data), 4 * GAS_TX_DATA_ZERO);
361    }
362
363    #[test]
364    fn test_calculate_data_gas_all_nonzero_bytes() {
365        let data = &[0x01, 0x02, 0x03, 0x04];
366        // 4 non-zero bytes * 16 gas per non-zero byte = 64 gas
367        assert_eq!(calculate_data_gas(data), 4 * GAS_TX_DATA_NONZERO);
368    }
369
370    #[test]
371    fn test_calculate_data_gas_mixed_bytes() {
372        let data = &[0x00, 0x01, 0x00, 0x02, 0x03, 0x00];
373        // 3 zero bytes (4 gas each) + 3 non-zero bytes (16 gas each) = 12 + 48 = 60 gas
374        assert_eq!(
375            calculate_data_gas(data),
376            3 * GAS_TX_DATA_ZERO + 3 * GAS_TX_DATA_NONZERO
377        );
378    }
379
380    #[test]
381    fn test_calculate_intrinsic_gas_regular_transaction_no_data() {
382        let tx = EvmTransactionRequest {
383            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
384            value: crate::models::U256::from(1000000000000000000u128),
385            data: None,
386            gas_limit: None,
387            gas_price: Some(20_000_000_000),
388            speed: Some(Speed::Average),
389            max_fee_per_gas: None,
390            max_priority_fee_per_gas: None,
391            valid_until: None,
392        };
393
394        assert_eq!(calculate_intrinsic_gas(&tx), DEFAULT_GAS_LIMIT);
395    }
396
397    #[test]
398    fn test_calculate_intrinsic_gas_contract_creation_no_data() {
399        let tx = EvmTransactionRequest {
400            to: None, // Contract creation
401            value: crate::models::U256::from(0u128),
402            data: None,
403            gas_limit: None,
404            gas_price: Some(20_000_000_000),
405            speed: Some(Speed::Average),
406            max_fee_per_gas: None,
407            max_priority_fee_per_gas: None,
408            valid_until: None,
409        };
410
411        assert_eq!(calculate_intrinsic_gas(&tx), GAS_TX_CREATE_CONTRACT);
412    }
413
414    #[test]
415    fn test_calculate_intrinsic_gas_with_data() {
416        let tx = EvmTransactionRequest {
417            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
418            value: crate::models::U256::from(0u128),
419            data: Some("0x01020304".to_string()), // 4 bytes of non-zero data
420            gas_limit: None,
421            gas_price: Some(20_000_000_000),
422            speed: Some(Speed::Average),
423            max_fee_per_gas: None,
424            max_priority_fee_per_gas: None,
425            valid_until: None,
426        };
427
428        let expected_gas = DEFAULT_GAS_LIMIT + 4 * GAS_TX_DATA_NONZERO;
429        assert_eq!(calculate_intrinsic_gas(&tx), expected_gas);
430    }
431
432    #[test]
433    fn test_calculate_intrinsic_gas_with_hex_prefix() {
434        let tx = EvmTransactionRequest {
435            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
436            value: crate::models::U256::from(0u128),
437            data: Some("0x00010203".to_string()), // Mix of zero and non-zero bytes
438            gas_limit: None,
439            gas_price: Some(20_000_000_000),
440            speed: Some(Speed::Average),
441            max_fee_per_gas: None,
442            max_priority_fee_per_gas: None,
443            valid_until: None,
444        };
445
446        // 1 zero byte + 3 non-zero bytes
447        let expected_gas = DEFAULT_GAS_LIMIT + GAS_TX_DATA_ZERO + 3 * GAS_TX_DATA_NONZERO;
448        assert_eq!(calculate_intrinsic_gas(&tx), expected_gas);
449    }
450
451    #[test]
452    fn test_calculate_intrinsic_gas_without_hex_prefix() {
453        let tx = EvmTransactionRequest {
454            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
455            value: crate::models::U256::from(0u128),
456            data: Some("00010203".to_string()), // Same data without 0x prefix
457            gas_limit: None,
458            gas_price: Some(20_000_000_000),
459            speed: Some(Speed::Average),
460            max_fee_per_gas: None,
461            max_priority_fee_per_gas: None,
462            valid_until: None,
463        };
464
465        // 1 zero byte + 3 non-zero bytes
466        let expected_gas = DEFAULT_GAS_LIMIT + GAS_TX_DATA_ZERO + 3 * GAS_TX_DATA_NONZERO;
467        assert_eq!(calculate_intrinsic_gas(&tx), expected_gas);
468    }
469
470    #[test]
471    fn test_calculate_intrinsic_gas_invalid_hex_data() {
472        let tx = EvmTransactionRequest {
473            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
474            value: crate::models::U256::from(0u128),
475            data: Some("0xINVALID_HEX".to_string()), // Invalid hex data
476            gas_limit: None,
477            gas_price: Some(20_000_000_000),
478            speed: Some(Speed::Average),
479            max_fee_per_gas: None,
480            max_priority_fee_per_gas: None,
481            valid_until: None,
482        };
483
484        // Invalid hex should result in 0 data gas
485        assert_eq!(calculate_intrinsic_gas(&tx), DEFAULT_GAS_LIMIT);
486    }
487
488    #[test]
489    fn test_calculate_intrinsic_gas_empty_hex_data() {
490        let tx = EvmTransactionRequest {
491            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
492            value: crate::models::U256::from(0u128),
493            data: Some("0x".to_string()), // Empty hex data
494            gas_limit: None,
495            gas_price: Some(20_000_000_000),
496            speed: Some(Speed::Average),
497            max_fee_per_gas: None,
498            max_priority_fee_per_gas: None,
499            valid_until: None,
500        };
501
502        // Empty data should result in 0 data gas
503        assert_eq!(calculate_intrinsic_gas(&tx), DEFAULT_GAS_LIMIT);
504    }
505
506    #[test]
507    fn test_calculate_intrinsic_gas_typical_erc20_transfer() {
508        let tx = EvmTransactionRequest {
509            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
510            value: crate::models::U256::from(0u128),
511            data: Some("0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000".to_string()),
512            gas_limit: None,
513            gas_price: Some(20_000_000_000),
514            speed: Some(Speed::Average),
515            max_fee_per_gas: None,
516            max_priority_fee_per_gas: None,
517            valid_until: None,
518        };
519
520        let data_bytes = hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap();
521        let data_gas = calculate_data_gas(&data_bytes);
522        let expected_gas = DEFAULT_GAS_LIMIT + data_gas;
523
524        assert_eq!(calculate_intrinsic_gas(&tx), expected_gas);
525    }
526
527    #[test]
528    fn test_calculate_intrinsic_gas_large_data() {
529        // Create a large data payload (1000 bytes)
530        let large_data = "0x".to_string() + &"01".repeat(1000);
531
532        let tx = EvmTransactionRequest {
533            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
534            value: crate::models::U256::from(0u128),
535            data: Some(large_data),
536            gas_limit: None,
537            gas_price: Some(20_000_000_000),
538            speed: Some(Speed::Average),
539            max_fee_per_gas: None,
540            max_priority_fee_per_gas: None,
541            valid_until: None,
542        };
543
544        // 1000 non-zero bytes
545        let expected_gas = DEFAULT_GAS_LIMIT + 1000 * GAS_TX_DATA_NONZERO;
546        assert_eq!(calculate_intrinsic_gas(&tx), expected_gas);
547    }
548}