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