openzeppelin_relayer/domain/transaction/evm/
replacement.rs

1//! This module contains the replacement and resubmission functionality for EVM transactions.
2//! It includes methods for determining replacement pricing, validating price bumps,
3//! and handling transaction compatibility checks.
4
5use crate::{
6    constants::{DEFAULT_EVM_GAS_PRICE_CAP, DEFAULT_GAS_LIMIT},
7    domain::transaction::evm::price_calculator::{calculate_min_bump, PriceCalculatorTrait},
8    models::{
9        EvmTransactionData, EvmTransactionDataTrait, RelayerRepoModel, TransactionError, U256,
10    },
11};
12
13use super::PriceParams;
14
15/// Checks if an EVM transaction data has explicit prices.
16///
17/// # Arguments
18///
19/// * `evm_data` - The EVM transaction data to check
20///
21/// # Returns
22///
23/// A `bool` indicating whether the transaction data has explicit prices.
24pub fn has_explicit_prices(evm_data: &EvmTransactionData) -> bool {
25    evm_data.gas_price.is_some()
26        || evm_data.max_fee_per_gas.is_some()
27        || evm_data.max_priority_fee_per_gas.is_some()
28}
29
30/// Checks if an old transaction and new transaction request are compatible for replacement.
31///
32/// # Arguments
33///
34/// * `old_evm_data` - The EVM transaction data from the old transaction
35/// * `new_evm_data` - The EVM transaction data for the new transaction
36///
37/// # Returns
38///
39/// A `Result` indicating compatibility or a `TransactionError` if incompatible.
40pub fn check_transaction_compatibility(
41    old_evm_data: &EvmTransactionData,
42    new_evm_data: &EvmTransactionData,
43) -> Result<(), TransactionError> {
44    let old_is_legacy = old_evm_data.is_legacy();
45    let new_is_legacy = new_evm_data.is_legacy();
46    let new_is_eip1559 = new_evm_data.is_eip1559();
47
48    // Allow replacement if new transaction has no explicit prices (will use market prices)
49    if !has_explicit_prices(new_evm_data) {
50        return Ok(());
51    }
52
53    // Check incompatible combinations when explicit prices are provided
54    if old_is_legacy && new_is_eip1559 {
55        return Err(TransactionError::ValidationError(
56            "Cannot replace legacy transaction with EIP1559 transaction".to_string(),
57        ));
58    }
59
60    if !old_is_legacy && new_is_legacy {
61        return Err(TransactionError::ValidationError(
62            "Cannot replace EIP1559 transaction with legacy transaction".to_string(),
63        ));
64    }
65
66    Ok(())
67}
68
69/// Determines the pricing strategy for a replacement transaction.
70///
71/// # Arguments
72///
73/// * `old_evm_data` - The EVM transaction data from the old transaction
74/// * `new_evm_data` - The EVM transaction data for the new transaction
75/// * `relayer` - The relayer model for policy validation
76/// * `price_calculator` - The price calculator instance
77/// * `network_lacks_mempool` - Whether the network lacks mempool (skips bump validation)
78///
79/// # Returns
80///
81/// A `Result` containing the price parameters or a `TransactionError`.
82pub async fn determine_replacement_pricing<PC: PriceCalculatorTrait>(
83    old_evm_data: &EvmTransactionData,
84    new_evm_data: &EvmTransactionData,
85    relayer: &RelayerRepoModel,
86    price_calculator: &PC,
87    network_lacks_mempool: bool,
88) -> Result<PriceParams, TransactionError> {
89    // Check transaction compatibility first for both paths
90    check_transaction_compatibility(old_evm_data, new_evm_data)?;
91
92    if has_explicit_prices(new_evm_data) {
93        // User provided explicit gas prices - validate they meet bump requirements
94        // Skip validation if network lacks mempool
95        validate_explicit_price_bump(old_evm_data, new_evm_data, relayer, network_lacks_mempool)
96    } else {
97        calculate_replacement_price(
98            old_evm_data,
99            new_evm_data,
100            relayer,
101            price_calculator,
102            network_lacks_mempool,
103        )
104        .await
105    }
106}
107
108/// Validates explicit gas prices from a replacement request against bump requirements.
109///
110/// # Arguments
111///
112/// * `old_evm_data` - The original transaction data
113/// * `new_evm_data` - The new transaction data with explicit prices
114/// * `relayer` - The relayer model for policy validation
115/// * `network_lacks_mempool` - Whether the network lacks mempool (skips bump validation)
116///
117/// # Returns
118///
119/// A `Result` containing validated price parameters or a `TransactionError`.
120pub fn validate_explicit_price_bump(
121    old_evm_data: &EvmTransactionData,
122    new_evm_data: &EvmTransactionData,
123    relayer: &RelayerRepoModel,
124    network_lacks_mempool: bool,
125) -> Result<PriceParams, TransactionError> {
126    // Create price params from the explicit values in the request
127    let mut price_params = PriceParams {
128        gas_price: new_evm_data.gas_price,
129        max_fee_per_gas: new_evm_data.max_fee_per_gas,
130        max_priority_fee_per_gas: new_evm_data.max_priority_fee_per_gas,
131        is_min_bumped: None,
132        extra_fee: None,
133        total_cost: U256::ZERO,
134    };
135
136    // First check gas price cap before bump validation
137    let gas_price_cap = relayer
138        .policies
139        .get_evm_policy()
140        .gas_price_cap
141        .unwrap_or(DEFAULT_EVM_GAS_PRICE_CAP);
142
143    // Check if gas prices exceed gas price cap
144    if let Some(gas_price) = new_evm_data.gas_price {
145        if gas_price > gas_price_cap {
146            return Err(TransactionError::ValidationError(format!(
147                "Gas price {gas_price} exceeds gas price cap {gas_price_cap}"
148            )));
149        }
150    }
151
152    if let Some(max_fee) = new_evm_data.max_fee_per_gas {
153        if max_fee > gas_price_cap {
154            return Err(TransactionError::ValidationError(format!(
155                "Max fee per gas {max_fee} exceeds gas price cap {gas_price_cap}"
156            )));
157        }
158    }
159
160    // both max_fee_per_gas and max_priority_fee_per_gas must be provided together
161    if price_params.max_fee_per_gas.is_some() != price_params.max_priority_fee_per_gas.is_some() {
162        return Err(TransactionError::ValidationError(
163            "Partial EIP1559 transaction: both max_fee_per_gas and max_priority_fee_per_gas must be provided together".to_string(),
164        ));
165    }
166
167    // Skip bump validation if network lacks mempool
168    if !network_lacks_mempool {
169        validate_price_bump_requirements(old_evm_data, new_evm_data)?;
170    }
171
172    // Ensure max priority fee doesn't exceed max fee per gas for EIP1559 transactions
173    if let (Some(max_fee), Some(max_priority)) = (
174        price_params.max_fee_per_gas,
175        price_params.max_priority_fee_per_gas,
176    ) {
177        if max_priority > max_fee {
178            return Err(TransactionError::ValidationError(
179                "Max priority fee cannot exceed max fee per gas".to_string(),
180            ));
181        }
182    }
183
184    // Calculate total cost
185    let gas_limit = old_evm_data.gas_limit;
186    let value = new_evm_data.value;
187    let is_eip1559 = price_params.max_fee_per_gas.is_some();
188
189    price_params.total_cost = price_params.calculate_total_cost(
190        is_eip1559,
191        gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
192        value,
193    );
194    price_params.is_min_bumped = Some(true);
195
196    Ok(price_params)
197}
198
199/// Validates that explicit prices meet bump requirements
200fn validate_price_bump_requirements(
201    old_evm_data: &EvmTransactionData,
202    new_evm_data: &EvmTransactionData,
203) -> Result<(), TransactionError> {
204    let old_has_legacy_pricing = old_evm_data.gas_price.is_some();
205    let old_has_eip1559_pricing =
206        old_evm_data.max_fee_per_gas.is_some() && old_evm_data.max_priority_fee_per_gas.is_some();
207    let new_has_legacy_pricing = new_evm_data.gas_price.is_some();
208    let new_has_eip1559_pricing =
209        new_evm_data.max_fee_per_gas.is_some() && new_evm_data.max_priority_fee_per_gas.is_some();
210
211    // New transaction must always have pricing data
212    if !new_has_legacy_pricing && !new_has_eip1559_pricing {
213        return Err(TransactionError::ValidationError(
214            "New transaction must have pricing data".to_string(),
215        ));
216    }
217
218    // Validate EIP1559 consistency in new transaction
219    if !new_evm_data.is_legacy()
220        && new_evm_data.max_fee_per_gas.is_some() != new_evm_data.max_priority_fee_per_gas.is_some()
221    {
222        return Err(TransactionError::ValidationError(
223            "Partial EIP1559 transaction: both max_fee_per_gas and max_priority_fee_per_gas must be provided together".to_string(),
224        ));
225    }
226
227    // If old transaction has no pricing data, accept any new pricing that has data
228    if !old_has_legacy_pricing && !old_has_eip1559_pricing {
229        return Ok(());
230    }
231
232    let is_sufficient_bump = if let (Some(old_gas_price), Some(new_gas_price)) =
233        (old_evm_data.gas_price, new_evm_data.gas_price)
234    {
235        // Legacy transaction comparison
236        let min_required = calculate_min_bump(old_gas_price);
237        new_gas_price >= min_required
238    } else if let (Some(old_max_fee), Some(new_max_fee)) =
239        (old_evm_data.max_fee_per_gas, new_evm_data.max_fee_per_gas)
240    {
241        // EIP1559 transaction comparison - max_fee_per_gas must meet bump requirements
242        let min_required_max_fee = calculate_min_bump(old_max_fee);
243        let max_fee_sufficient = new_max_fee >= min_required_max_fee;
244
245        // Check max_priority_fee_per_gas if both transactions have it
246        let priority_fee_sufficient = match (
247            old_evm_data.max_priority_fee_per_gas,
248            new_evm_data.max_priority_fee_per_gas,
249        ) {
250            (Some(old_priority), Some(new_priority)) => {
251                let min_required_priority = calculate_min_bump(old_priority);
252                new_priority >= min_required_priority
253            }
254            _ => {
255                return Err(TransactionError::ValidationError(
256                    "Partial EIP1559 transaction: both max_fee_per_gas and max_priority_fee_per_gas must be provided together".to_string(),
257                ));
258            }
259        };
260
261        max_fee_sufficient && priority_fee_sufficient
262    } else {
263        // Handle missing data - return early with error
264        return Err(TransactionError::ValidationError(
265            "Partial EIP1559 transaction: both max_fee_per_gas and max_priority_fee_per_gas must be provided together".to_string(),
266        ));
267    };
268
269    if !is_sufficient_bump {
270        return Err(TransactionError::ValidationError(
271            "Gas price increase does not meet minimum bump requirement".to_string(),
272        ));
273    }
274
275    Ok(())
276}
277
278/// Calculates replacement pricing with fresh market rates.
279///
280/// # Arguments
281///
282/// * `old_evm_data` - The original transaction data for bump validation
283/// * `new_evm_data` - The new transaction data
284/// * `relayer` - The relayer model for policy validation
285/// * `price_calculator` - The price calculator instance
286/// * `network_lacks_mempool` - Whether the network lacks mempool (skips bump validation)
287///
288/// # Returns
289///
290/// A `Result` containing calculated price parameters or a `TransactionError`.
291pub async fn calculate_replacement_price<PC: PriceCalculatorTrait>(
292    old_evm_data: &EvmTransactionData,
293    new_evm_data: &EvmTransactionData,
294    relayer: &RelayerRepoModel,
295    price_calculator: &PC,
296    network_lacks_mempool: bool,
297) -> Result<PriceParams, TransactionError> {
298    // Determine transaction type based on old transaction and network policy
299    let use_legacy = old_evm_data.is_legacy()
300        || relayer.policies.get_evm_policy().eip1559_pricing == Some(false);
301
302    // Get fresh market price for the updated transaction data
303    let mut price_params = price_calculator
304        .get_transaction_price_params(new_evm_data, relayer)
305        .await?;
306
307    // Skip bump requirements if network lacks mempool
308    if network_lacks_mempool {
309        price_params.is_min_bumped = Some(true);
310        return Ok(price_params);
311    }
312
313    // For replacement transactions, we need to ensure the new price meets bump requirements
314    // compared to the old transaction
315    let is_sufficient_bump = if use_legacy {
316        if let (Some(old_gas_price), Some(new_gas_price)) =
317            (old_evm_data.gas_price, price_params.gas_price)
318        {
319            let min_required = calculate_min_bump(old_gas_price);
320            if new_gas_price < min_required {
321                // Market price is too low, use minimum bump
322                price_params.gas_price = Some(min_required);
323            }
324            price_params.is_min_bumped = Some(true);
325            true
326        } else {
327            false
328        }
329    } else {
330        // EIP1559 comparison
331        if let (Some(old_max_fee), Some(new_max_fee), Some(old_priority), Some(new_priority)) = (
332            old_evm_data.max_fee_per_gas,
333            price_params.max_fee_per_gas,
334            old_evm_data.max_priority_fee_per_gas,
335            price_params.max_priority_fee_per_gas,
336        ) {
337            let min_required = calculate_min_bump(old_max_fee);
338            let min_required_priority = calculate_min_bump(old_priority);
339            if new_max_fee < min_required {
340                price_params.max_fee_per_gas = Some(min_required);
341            }
342
343            if new_priority < min_required_priority {
344                price_params.max_priority_fee_per_gas = Some(min_required_priority);
345            }
346
347            price_params.is_min_bumped = Some(true);
348            true
349        } else {
350            false
351        }
352    };
353
354    if !is_sufficient_bump {
355        return Err(TransactionError::ValidationError(
356            "Unable to calculate sufficient price bump for speed-based replacement".to_string(),
357        ));
358    }
359
360    Ok(price_params)
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::{
367        domain::transaction::evm::price_calculator::PriceCalculatorTrait,
368        models::{
369            evm::Speed, EvmTransactionData, RelayerEvmPolicy, RelayerNetworkPolicy,
370            RelayerRepoModel, TransactionError, U256,
371        },
372    };
373    use async_trait::async_trait;
374
375    // Mock price calculator for testing
376    struct MockPriceCalculator {
377        pub gas_price: Option<u128>,
378        pub max_fee_per_gas: Option<u128>,
379        pub max_priority_fee_per_gas: Option<u128>,
380        pub should_error: bool,
381    }
382
383    #[async_trait]
384    impl PriceCalculatorTrait for MockPriceCalculator {
385        async fn get_transaction_price_params(
386            &self,
387            _evm_data: &EvmTransactionData,
388            _relayer: &RelayerRepoModel,
389        ) -> Result<PriceParams, TransactionError> {
390            if self.should_error {
391                return Err(TransactionError::ValidationError("Mock error".to_string()));
392            }
393
394            Ok(PriceParams {
395                gas_price: self.gas_price,
396                max_fee_per_gas: self.max_fee_per_gas,
397                max_priority_fee_per_gas: self.max_priority_fee_per_gas,
398                is_min_bumped: Some(false),
399                extra_fee: None,
400                total_cost: U256::ZERO,
401            })
402        }
403
404        async fn calculate_bumped_gas_price(
405            &self,
406            _evm_data: &EvmTransactionData,
407            _relayer: &RelayerRepoModel,
408        ) -> Result<PriceParams, TransactionError> {
409            if self.should_error {
410                return Err(TransactionError::ValidationError("Mock error".to_string()));
411            }
412
413            Ok(PriceParams {
414                gas_price: self.gas_price,
415                max_fee_per_gas: self.max_fee_per_gas,
416                max_priority_fee_per_gas: self.max_priority_fee_per_gas,
417                is_min_bumped: Some(true),
418                extra_fee: None,
419                total_cost: U256::ZERO,
420            })
421        }
422    }
423
424    fn create_legacy_transaction_data() -> EvmTransactionData {
425        EvmTransactionData {
426            gas_price: Some(20_000_000_000), // 20 gwei
427            gas_limit: Some(21000),
428            nonce: Some(1),
429            value: U256::from(1000000000000000000u128), // 1 ETH
430            data: Some("0x".to_string()),
431            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
432            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
433            chain_id: 1,
434            hash: None,
435            signature: None,
436            speed: Some(Speed::Average),
437            max_fee_per_gas: None,
438            max_priority_fee_per_gas: None,
439            raw: None,
440        }
441    }
442
443    fn create_eip1559_transaction_data() -> EvmTransactionData {
444        EvmTransactionData {
445            gas_price: None,
446            gas_limit: Some(21000),
447            nonce: Some(1),
448            value: U256::from(1000000000000000000u128), // 1 ETH
449            data: Some("0x".to_string()),
450            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
451            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
452            chain_id: 1,
453            hash: None,
454            signature: None,
455            speed: Some(Speed::Average),
456            max_fee_per_gas: Some(30_000_000_000), // 30 gwei
457            max_priority_fee_per_gas: Some(2_000_000_000), // 2 gwei
458            raw: None,
459        }
460    }
461
462    fn create_test_relayer() -> RelayerRepoModel {
463        RelayerRepoModel {
464            id: "test-relayer".to_string(),
465            name: "Test Relayer".to_string(),
466            network: "ethereum".to_string(),
467            paused: false,
468            network_type: crate::models::NetworkType::Evm,
469            signer_id: "test-signer".to_string(),
470            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
471                gas_price_cap: Some(100_000_000_000), // 100 gwei
472                eip1559_pricing: Some(true),
473                ..Default::default()
474            }),
475            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
476            notification_id: None,
477            system_disabled: false,
478            custom_rpc_urls: None,
479            ..Default::default()
480        }
481    }
482
483    fn create_relayer_with_gas_cap(gas_cap: u128) -> RelayerRepoModel {
484        let mut relayer = create_test_relayer();
485        if let RelayerNetworkPolicy::Evm(ref mut policy) = relayer.policies {
486            policy.gas_price_cap = Some(gas_cap);
487        }
488        relayer
489    }
490
491    #[test]
492    fn test_has_explicit_prices() {
493        let legacy_tx = create_legacy_transaction_data();
494        assert!(has_explicit_prices(&legacy_tx));
495
496        let eip1559_tx = create_eip1559_transaction_data();
497        assert!(has_explicit_prices(&eip1559_tx));
498
499        let mut no_prices_tx = create_legacy_transaction_data();
500        no_prices_tx.gas_price = None;
501        assert!(!has_explicit_prices(&no_prices_tx));
502
503        // Test partial EIP1559 (only max_fee_per_gas)
504        let mut partial_eip1559 = create_legacy_transaction_data();
505        partial_eip1559.gas_price = None;
506        partial_eip1559.max_fee_per_gas = Some(30_000_000_000);
507        assert!(has_explicit_prices(&partial_eip1559));
508
509        // Test partial EIP1559 (only max_priority_fee_per_gas)
510        let mut partial_priority = create_legacy_transaction_data();
511        partial_priority.gas_price = None;
512        partial_priority.max_priority_fee_per_gas = Some(2_000_000_000);
513        assert!(has_explicit_prices(&partial_priority));
514    }
515
516    #[test]
517    fn test_check_transaction_compatibility_success() {
518        // Legacy to legacy - should succeed
519        let old_legacy = create_legacy_transaction_data();
520        let new_legacy = create_legacy_transaction_data();
521        assert!(check_transaction_compatibility(&old_legacy, &new_legacy).is_ok());
522
523        // EIP1559 to EIP1559 - should succeed
524        let old_eip1559 = create_eip1559_transaction_data();
525        let new_eip1559 = create_eip1559_transaction_data();
526        assert!(check_transaction_compatibility(&old_eip1559, &new_eip1559).is_ok());
527
528        // No explicit prices - should succeed
529        let mut no_prices = create_legacy_transaction_data();
530        no_prices.gas_price = None;
531        assert!(check_transaction_compatibility(&old_legacy, &no_prices).is_ok());
532    }
533
534    #[test]
535    fn test_check_transaction_compatibility_failures() {
536        let old_legacy = create_legacy_transaction_data();
537        let old_eip1559 = create_eip1559_transaction_data();
538
539        // Legacy to EIP1559 - should fail
540        let result = check_transaction_compatibility(&old_legacy, &old_eip1559);
541        assert!(result.is_err());
542
543        // EIP1559 to Legacy - should fail
544        let result = check_transaction_compatibility(&old_eip1559, &old_legacy);
545        assert!(result.is_err());
546    }
547
548    #[test]
549    fn test_validate_explicit_price_bump_gas_price_cap() {
550        let old_tx = create_legacy_transaction_data();
551        let relayer = create_relayer_with_gas_cap(25_000_000_000);
552
553        let mut new_tx = create_legacy_transaction_data();
554        new_tx.gas_price = Some(50_000_000_000);
555
556        let result = validate_explicit_price_bump(&old_tx, &new_tx, &relayer, false);
557        assert!(result.is_err());
558
559        let mut new_eip1559 = create_eip1559_transaction_data();
560        new_eip1559.max_fee_per_gas = Some(50_000_000_000);
561
562        let old_eip1559 = create_eip1559_transaction_data();
563        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
564        assert!(result.is_err());
565    }
566
567    #[test]
568    fn test_validate_explicit_price_bump_insufficient_bump() {
569        let relayer = create_test_relayer();
570
571        let old_legacy = create_legacy_transaction_data();
572        let mut new_legacy = create_legacy_transaction_data();
573        new_legacy.gas_price = Some(21_000_000_000); // 21 gwei (insufficient because minimum bump const)
574
575        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, false);
576        assert!(result.is_err());
577
578        let old_eip1559 = create_eip1559_transaction_data();
579        let mut new_eip1559 = create_eip1559_transaction_data();
580        new_eip1559.max_fee_per_gas = Some(32_000_000_000); // 32 gwei (insufficient because minimum bump const)
581
582        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
583        assert!(result.is_err());
584    }
585
586    #[test]
587    fn test_validate_explicit_price_bump_sufficient_bump() {
588        let relayer = create_test_relayer();
589
590        let old_legacy = create_legacy_transaction_data();
591        let mut new_legacy = create_legacy_transaction_data();
592        new_legacy.gas_price = Some(22_000_000_000);
593
594        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, false);
595        assert!(result.is_ok());
596
597        let old_eip1559 = create_eip1559_transaction_data();
598        let mut new_eip1559 = create_eip1559_transaction_data();
599        new_eip1559.max_fee_per_gas = Some(33_000_000_000);
600        new_eip1559.max_priority_fee_per_gas = Some(3_000_000_000);
601
602        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
603        assert!(result.is_ok());
604    }
605
606    #[test]
607    fn test_validate_explicit_price_bump_network_lacks_mempool() {
608        let relayer = create_test_relayer();
609        let old_legacy = create_legacy_transaction_data();
610        let mut new_legacy = create_legacy_transaction_data();
611        new_legacy.gas_price = Some(15_000_000_000); // 15 gwei (would normally be insufficient)
612
613        // Should succeed when network lacks mempool (bump validation skipped)
614        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, true);
615        assert!(result.is_ok());
616    }
617
618    #[test]
619    fn test_validate_explicit_price_bump_partial_eip1559_error() {
620        let relayer = create_test_relayer();
621        let old_eip1559 = create_eip1559_transaction_data();
622
623        // Test only max_fee_per_gas provided
624        let mut partial_max_fee = create_legacy_transaction_data();
625        partial_max_fee.gas_price = None;
626        partial_max_fee.max_fee_per_gas = Some(35_000_000_000);
627        partial_max_fee.max_priority_fee_per_gas = None;
628
629        let result = validate_explicit_price_bump(&old_eip1559, &partial_max_fee, &relayer, false);
630        assert!(result.is_err());
631
632        // Test only max_priority_fee_per_gas provided
633        let mut partial_priority = create_legacy_transaction_data();
634        partial_priority.gas_price = None;
635        partial_priority.max_fee_per_gas = None;
636        partial_priority.max_priority_fee_per_gas = Some(3_000_000_000);
637
638        let result = validate_explicit_price_bump(&old_eip1559, &partial_priority, &relayer, false);
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_validate_explicit_price_bump_priority_fee_exceeds_max_fee() {
644        let relayer = create_test_relayer();
645        let old_eip1559 = create_eip1559_transaction_data();
646        let mut new_eip1559 = create_eip1559_transaction_data();
647        new_eip1559.max_fee_per_gas = Some(35_000_000_000);
648        new_eip1559.max_priority_fee_per_gas = Some(40_000_000_000);
649
650        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
651        assert!(result.is_err());
652    }
653
654    #[test]
655    fn test_validate_explicit_price_bump_priority_fee_equals_max_fee() {
656        let relayer = create_test_relayer();
657        let old_eip1559 = create_eip1559_transaction_data();
658        let mut new_eip1559 = create_eip1559_transaction_data();
659        new_eip1559.max_fee_per_gas = Some(35_000_000_000);
660        new_eip1559.max_priority_fee_per_gas = Some(35_000_000_000);
661
662        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
663        assert!(result.is_ok());
664    }
665
666    #[tokio::test]
667    async fn test_calculate_replacement_price_legacy_sufficient_market_price() {
668        let old_tx = create_legacy_transaction_data();
669        let new_tx = create_legacy_transaction_data();
670        let relayer = create_test_relayer();
671
672        let price_calculator = MockPriceCalculator {
673            gas_price: Some(25_000_000_000),
674            max_fee_per_gas: None,
675            max_priority_fee_per_gas: None,
676            should_error: false,
677        };
678
679        let result =
680            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
681        assert!(result.is_ok());
682
683        let price_params = result.unwrap();
684        assert_eq!(price_params.gas_price, Some(25_000_000_000));
685        assert_eq!(price_params.is_min_bumped, Some(true));
686    }
687
688    #[tokio::test]
689    async fn test_calculate_replacement_price_legacy_insufficient_market_price() {
690        let old_tx = create_legacy_transaction_data();
691        let new_tx = create_legacy_transaction_data();
692        let relayer = create_test_relayer();
693
694        let price_calculator = MockPriceCalculator {
695            gas_price: Some(18_000_000_000), // 18 gwei (insufficient, needs 22 gwei)
696            max_fee_per_gas: None,
697            max_priority_fee_per_gas: None,
698            should_error: false,
699        };
700
701        let result =
702            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
703        assert!(result.is_ok());
704
705        let price_params = result.unwrap();
706        assert_eq!(price_params.gas_price, Some(22_000_000_000)); // Should be bumped to minimum
707        assert_eq!(price_params.is_min_bumped, Some(true));
708    }
709
710    #[tokio::test]
711    async fn test_calculate_replacement_price_eip1559_sufficient() {
712        let old_tx = create_eip1559_transaction_data();
713        let new_tx = create_eip1559_transaction_data();
714        let relayer = create_test_relayer();
715
716        let price_calculator = MockPriceCalculator {
717            gas_price: None,
718            max_fee_per_gas: Some(40_000_000_000),
719            max_priority_fee_per_gas: Some(3_000_000_000),
720            should_error: false,
721        };
722
723        let result =
724            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
725        assert!(result.is_ok());
726
727        let price_params = result.unwrap();
728        assert_eq!(price_params.max_fee_per_gas, Some(40_000_000_000));
729        assert_eq!(price_params.is_min_bumped, Some(true));
730    }
731
732    #[tokio::test]
733    async fn test_calculate_replacement_price_eip1559_insufficient_with_priority_fee_bump() {
734        let mut old_tx = create_eip1559_transaction_data();
735        old_tx.max_fee_per_gas = Some(30_000_000_000);
736        old_tx.max_priority_fee_per_gas = Some(5_000_000_000);
737
738        let new_tx = create_eip1559_transaction_data();
739        let relayer = create_test_relayer();
740
741        let price_calculator = MockPriceCalculator {
742            gas_price: None,
743            max_fee_per_gas: Some(25_000_000_000), // 25 gwei (insufficient, needs 33 gwei)
744            max_priority_fee_per_gas: Some(4_000_000_000), // 4 gwei (insufficient, needs 5.5 gwei)
745            should_error: false,
746        };
747
748        let result =
749            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
750        assert!(result.is_ok());
751
752        let price_params = result.unwrap();
753        assert_eq!(price_params.max_fee_per_gas, Some(33_000_000_000));
754
755        // Priority fee should also be bumped if old transaction had it
756        let expected_priority_bump = calculate_min_bump(5_000_000_000); // 5.5 gwei
757        let capped_priority = expected_priority_bump.min(33_000_000_000); // Capped at max_fee
758        assert_eq!(price_params.max_priority_fee_per_gas, Some(capped_priority));
759    }
760
761    #[tokio::test]
762    async fn test_calculate_replacement_price_network_lacks_mempool() {
763        let old_tx = create_legacy_transaction_data();
764        let new_tx = create_legacy_transaction_data();
765        let relayer = create_test_relayer();
766
767        let price_calculator = MockPriceCalculator {
768            gas_price: Some(15_000_000_000), // 15 gwei (would be insufficient normally)
769            max_fee_per_gas: None,
770            max_priority_fee_per_gas: None,
771            should_error: false,
772        };
773
774        let result =
775            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, true).await;
776        assert!(result.is_ok());
777
778        let price_params = result.unwrap();
779        assert_eq!(price_params.gas_price, Some(15_000_000_000)); // Uses market price as-is
780        assert_eq!(price_params.is_min_bumped, Some(true));
781    }
782
783    #[tokio::test]
784    async fn test_calculate_replacement_price_calculator_error() {
785        let old_tx = create_legacy_transaction_data();
786        let new_tx = create_legacy_transaction_data();
787        let relayer = create_test_relayer();
788
789        let price_calculator = MockPriceCalculator {
790            gas_price: None,
791            max_fee_per_gas: None,
792            max_priority_fee_per_gas: None,
793            should_error: true,
794        };
795
796        let result =
797            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
798        assert!(result.is_err());
799    }
800
801    #[tokio::test]
802    async fn test_determine_replacement_pricing_explicit_prices() {
803        let old_tx = create_legacy_transaction_data();
804        let mut new_tx = create_legacy_transaction_data();
805        new_tx.gas_price = Some(25_000_000_000);
806        let relayer = create_test_relayer();
807
808        let price_calculator = MockPriceCalculator {
809            gas_price: Some(30_000_000_000),
810            max_fee_per_gas: None,
811            max_priority_fee_per_gas: None,
812            should_error: false,
813        };
814
815        let result =
816            determine_replacement_pricing(&old_tx, &new_tx, &relayer, &price_calculator, false)
817                .await;
818        assert!(result.is_ok());
819
820        let price_params = result.unwrap();
821        assert_eq!(price_params.gas_price, Some(25_000_000_000));
822    }
823
824    #[tokio::test]
825    async fn test_determine_replacement_pricing_market_prices() {
826        let old_tx = create_legacy_transaction_data();
827        let mut new_tx = create_legacy_transaction_data();
828        new_tx.gas_price = None;
829        let relayer = create_test_relayer();
830
831        let price_calculator = MockPriceCalculator {
832            gas_price: Some(30_000_000_000),
833            max_fee_per_gas: None,
834            max_priority_fee_per_gas: None,
835            should_error: false,
836        };
837
838        let result =
839            determine_replacement_pricing(&old_tx, &new_tx, &relayer, &price_calculator, false)
840                .await;
841        assert!(result.is_ok());
842
843        let price_params = result.unwrap();
844        assert_eq!(price_params.gas_price, Some(30_000_000_000));
845    }
846
847    #[tokio::test]
848    async fn test_determine_replacement_pricing_compatibility_error() {
849        let old_legacy = create_legacy_transaction_data();
850        let new_eip1559 = create_eip1559_transaction_data();
851        let relayer = create_test_relayer();
852
853        let price_calculator = MockPriceCalculator {
854            gas_price: None,
855            max_fee_per_gas: None,
856            max_priority_fee_per_gas: None,
857            should_error: false,
858        };
859
860        let result = determine_replacement_pricing(
861            &old_legacy,
862            &new_eip1559,
863            &relayer,
864            &price_calculator,
865            false,
866        )
867        .await;
868        assert!(result.is_err());
869    }
870
871    #[test]
872    fn test_validate_price_bump_requirements_legacy() {
873        let old_tx = create_legacy_transaction_data();
874
875        let mut new_tx_sufficient = create_legacy_transaction_data();
876        new_tx_sufficient.gas_price = Some(22_000_000_000);
877        assert!(validate_price_bump_requirements(&old_tx, &new_tx_sufficient).is_ok());
878
879        let mut new_tx_insufficient = create_legacy_transaction_data();
880        new_tx_insufficient.gas_price = Some(21_000_000_000);
881        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient).is_err());
882    }
883
884    #[test]
885    fn test_validate_price_bump_requirements_eip1559() {
886        let old_tx = create_eip1559_transaction_data();
887
888        let mut new_tx_sufficient = create_eip1559_transaction_data();
889        new_tx_sufficient.max_fee_per_gas = Some(33_000_000_000);
890        new_tx_sufficient.max_priority_fee_per_gas = Some(3_000_000_000);
891        assert!(validate_price_bump_requirements(&old_tx, &new_tx_sufficient).is_ok());
892
893        let mut new_tx_insufficient_max = create_eip1559_transaction_data();
894        new_tx_insufficient_max.max_fee_per_gas = Some(32_000_000_000);
895        new_tx_insufficient_max.max_priority_fee_per_gas = Some(3_000_000_000);
896        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient_max).is_err());
897
898        let mut new_tx_insufficient_priority = create_eip1559_transaction_data();
899        new_tx_insufficient_priority.max_fee_per_gas = Some(33_000_000_000);
900        new_tx_insufficient_priority.max_priority_fee_per_gas = Some(2_100_000_000);
901        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient_priority).is_err());
902    }
903
904    #[test]
905    fn test_validate_price_bump_requirements_partial_eip1559() {
906        let mut old_tx = create_eip1559_transaction_data();
907        old_tx.max_fee_per_gas = Some(30_000_000_000);
908        old_tx.max_priority_fee_per_gas = Some(5_000_000_000);
909
910        let mut new_tx_only_priority = create_legacy_transaction_data();
911        new_tx_only_priority.gas_price = None;
912        new_tx_only_priority.max_fee_per_gas = None;
913        new_tx_only_priority.max_priority_fee_per_gas = Some(6_000_000_000);
914        let result = validate_price_bump_requirements(&old_tx, &new_tx_only_priority);
915        assert!(result.is_err());
916
917        let mut new_tx_only_max = create_legacy_transaction_data();
918        new_tx_only_max.gas_price = None;
919        new_tx_only_max.max_fee_per_gas = Some(33_000_000_000);
920        new_tx_only_max.max_priority_fee_per_gas = None;
921        let result = validate_price_bump_requirements(&old_tx, &new_tx_only_max);
922        assert!(result.is_err());
923
924        let new_legacy = create_legacy_transaction_data();
925        let result = validate_price_bump_requirements(&old_tx, &new_legacy);
926        assert!(result.is_err());
927
928        let old_legacy = create_legacy_transaction_data();
929        let result = validate_price_bump_requirements(&old_legacy, &new_tx_only_priority);
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_validate_price_bump_requirements_missing_pricing_data() {
935        let mut old_tx_no_price = create_legacy_transaction_data();
936        old_tx_no_price.gas_price = None;
937        old_tx_no_price.max_fee_per_gas = None;
938        old_tx_no_price.max_priority_fee_per_gas = None;
939
940        let mut new_tx_no_price = create_legacy_transaction_data();
941        new_tx_no_price.gas_price = None;
942        new_tx_no_price.max_fee_per_gas = None;
943        new_tx_no_price.max_priority_fee_per_gas = None;
944
945        let result = validate_price_bump_requirements(&old_tx_no_price, &new_tx_no_price);
946        assert!(result.is_err()); // Should fail because new transaction has no pricing
947
948        // Test old transaction with no pricing, new with legacy pricing - should succeed
949        let new_legacy = create_legacy_transaction_data();
950        let result = validate_price_bump_requirements(&old_tx_no_price, &new_legacy);
951        assert!(result.is_ok());
952
953        // Test old transaction with no pricing, new with EIP1559 pricing - should succeed
954        let new_eip1559 = create_eip1559_transaction_data();
955        let result = validate_price_bump_requirements(&old_tx_no_price, &new_eip1559);
956        assert!(result.is_ok());
957
958        // Test old legacy, new with no pricing - should fail
959        let old_legacy = create_legacy_transaction_data();
960        let result = validate_price_bump_requirements(&old_legacy, &new_tx_no_price);
961        assert!(result.is_err()); // Should fail because new transaction has no pricing
962    }
963
964    #[test]
965    fn test_validate_explicit_price_bump_zero_gas_price_cap() {
966        let old_tx = create_legacy_transaction_data();
967        let relayer = create_relayer_with_gas_cap(0);
968        let mut new_tx = create_legacy_transaction_data();
969        new_tx.gas_price = Some(1);
970
971        let result = validate_explicit_price_bump(&old_tx, &new_tx, &relayer, false);
972        assert!(result.is_err());
973    }
974
975    #[tokio::test]
976    async fn test_calculate_replacement_price_legacy_missing_old_gas_price() {
977        let mut old_tx = create_legacy_transaction_data();
978        old_tx.gas_price = None;
979        let new_tx = create_legacy_transaction_data();
980        let relayer = create_test_relayer();
981
982        let price_calculator = MockPriceCalculator {
983            gas_price: Some(25_000_000_000),
984            max_fee_per_gas: None,
985            max_priority_fee_per_gas: None,
986            should_error: false,
987        };
988
989        let result =
990            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
991        assert!(result.is_err());
992    }
993
994    #[tokio::test]
995    async fn test_calculate_replacement_price_eip1559_missing_old_fees() {
996        let mut old_tx = create_eip1559_transaction_data();
997        old_tx.max_fee_per_gas = None;
998        old_tx.max_priority_fee_per_gas = None;
999        let new_tx = create_eip1559_transaction_data();
1000        let relayer = create_test_relayer();
1001
1002        let price_calculator = MockPriceCalculator {
1003            gas_price: None,
1004            max_fee_per_gas: Some(40_000_000_000),
1005            max_priority_fee_per_gas: Some(3_000_000_000),
1006            should_error: false,
1007        };
1008
1009        let result =
1010            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
1011        assert!(result.is_err());
1012    }
1013
1014    #[tokio::test]
1015    async fn test_calculate_replacement_price_force_legacy_with_eip1559_policy_disabled() {
1016        let old_tx = create_eip1559_transaction_data();
1017        let new_tx = create_eip1559_transaction_data();
1018        let mut relayer = create_test_relayer();
1019        if let crate::models::RelayerNetworkPolicy::Evm(ref mut policy) = relayer.policies {
1020            policy.eip1559_pricing = Some(false);
1021        }
1022
1023        let price_calculator = MockPriceCalculator {
1024            gas_price: Some(25_000_000_000),
1025            max_fee_per_gas: None,
1026            max_priority_fee_per_gas: None,
1027            should_error: false,
1028        };
1029
1030        let result =
1031            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
1032        assert!(result.is_err());
1033    }
1034}