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