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            ..Default::default()
482        }
483    }
484
485    fn create_relayer_with_gas_cap(gas_cap: u128) -> RelayerRepoModel {
486        let mut relayer = create_test_relayer();
487        if let RelayerNetworkPolicy::Evm(ref mut policy) = relayer.policies {
488            policy.gas_price_cap = Some(gas_cap);
489        }
490        relayer
491    }
492
493    #[test]
494    fn test_has_explicit_prices() {
495        let legacy_tx = create_legacy_transaction_data();
496        assert!(has_explicit_prices(&legacy_tx));
497
498        let eip1559_tx = create_eip1559_transaction_data();
499        assert!(has_explicit_prices(&eip1559_tx));
500
501        let mut no_prices_tx = create_legacy_transaction_data();
502        no_prices_tx.gas_price = None;
503        assert!(!has_explicit_prices(&no_prices_tx));
504
505        // Test partial EIP1559 (only max_fee_per_gas)
506        let mut partial_eip1559 = create_legacy_transaction_data();
507        partial_eip1559.gas_price = None;
508        partial_eip1559.max_fee_per_gas = Some(30_000_000_000);
509        assert!(has_explicit_prices(&partial_eip1559));
510
511        // Test partial EIP1559 (only max_priority_fee_per_gas)
512        let mut partial_priority = create_legacy_transaction_data();
513        partial_priority.gas_price = None;
514        partial_priority.max_priority_fee_per_gas = Some(2_000_000_000);
515        assert!(has_explicit_prices(&partial_priority));
516    }
517
518    #[test]
519    fn test_check_transaction_compatibility_success() {
520        // Legacy to legacy - should succeed
521        let old_legacy = create_legacy_transaction_data();
522        let new_legacy = create_legacy_transaction_data();
523        assert!(check_transaction_compatibility(&old_legacy, &new_legacy).is_ok());
524
525        // EIP1559 to EIP1559 - should succeed
526        let old_eip1559 = create_eip1559_transaction_data();
527        let new_eip1559 = create_eip1559_transaction_data();
528        assert!(check_transaction_compatibility(&old_eip1559, &new_eip1559).is_ok());
529
530        // No explicit prices - should succeed
531        let mut no_prices = create_legacy_transaction_data();
532        no_prices.gas_price = None;
533        assert!(check_transaction_compatibility(&old_legacy, &no_prices).is_ok());
534    }
535
536    #[test]
537    fn test_check_transaction_compatibility_failures() {
538        let old_legacy = create_legacy_transaction_data();
539        let old_eip1559 = create_eip1559_transaction_data();
540
541        // Legacy to EIP1559 - should fail
542        let result = check_transaction_compatibility(&old_legacy, &old_eip1559);
543        assert!(result.is_err());
544
545        // EIP1559 to Legacy - should fail
546        let result = check_transaction_compatibility(&old_eip1559, &old_legacy);
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn test_validate_explicit_price_bump_gas_price_cap() {
552        let old_tx = create_legacy_transaction_data();
553        let relayer = create_relayer_with_gas_cap(25_000_000_000);
554
555        let mut new_tx = create_legacy_transaction_data();
556        new_tx.gas_price = Some(50_000_000_000);
557
558        let result = validate_explicit_price_bump(&old_tx, &new_tx, &relayer, false);
559        assert!(result.is_err());
560
561        let mut new_eip1559 = create_eip1559_transaction_data();
562        new_eip1559.max_fee_per_gas = Some(50_000_000_000);
563
564        let old_eip1559 = create_eip1559_transaction_data();
565        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
566        assert!(result.is_err());
567    }
568
569    #[test]
570    fn test_validate_explicit_price_bump_insufficient_bump() {
571        let relayer = create_test_relayer();
572
573        let old_legacy = create_legacy_transaction_data();
574        let mut new_legacy = create_legacy_transaction_data();
575        new_legacy.gas_price = Some(21_000_000_000); // 21 gwei (insufficient because minimum bump const)
576
577        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, false);
578        assert!(result.is_err());
579
580        let old_eip1559 = create_eip1559_transaction_data();
581        let mut new_eip1559 = create_eip1559_transaction_data();
582        new_eip1559.max_fee_per_gas = Some(32_000_000_000); // 32 gwei (insufficient because minimum bump const)
583
584        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
585        assert!(result.is_err());
586    }
587
588    #[test]
589    fn test_validate_explicit_price_bump_sufficient_bump() {
590        let relayer = create_test_relayer();
591
592        let old_legacy = create_legacy_transaction_data();
593        let mut new_legacy = create_legacy_transaction_data();
594        new_legacy.gas_price = Some(22_000_000_000);
595
596        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, false);
597        assert!(result.is_ok());
598
599        let old_eip1559 = create_eip1559_transaction_data();
600        let mut new_eip1559 = create_eip1559_transaction_data();
601        new_eip1559.max_fee_per_gas = Some(33_000_000_000);
602        new_eip1559.max_priority_fee_per_gas = Some(3_000_000_000);
603
604        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
605        assert!(result.is_ok());
606    }
607
608    #[test]
609    fn test_validate_explicit_price_bump_network_lacks_mempool() {
610        let relayer = create_test_relayer();
611        let old_legacy = create_legacy_transaction_data();
612        let mut new_legacy = create_legacy_transaction_data();
613        new_legacy.gas_price = Some(15_000_000_000); // 15 gwei (would normally be insufficient)
614
615        // Should succeed when network lacks mempool (bump validation skipped)
616        let result = validate_explicit_price_bump(&old_legacy, &new_legacy, &relayer, true);
617        assert!(result.is_ok());
618    }
619
620    #[test]
621    fn test_validate_explicit_price_bump_partial_eip1559_error() {
622        let relayer = create_test_relayer();
623        let old_eip1559 = create_eip1559_transaction_data();
624
625        // Test only max_fee_per_gas provided
626        let mut partial_max_fee = create_legacy_transaction_data();
627        partial_max_fee.gas_price = None;
628        partial_max_fee.max_fee_per_gas = Some(35_000_000_000);
629        partial_max_fee.max_priority_fee_per_gas = None;
630
631        let result = validate_explicit_price_bump(&old_eip1559, &partial_max_fee, &relayer, false);
632        assert!(result.is_err());
633
634        // Test only max_priority_fee_per_gas provided
635        let mut partial_priority = create_legacy_transaction_data();
636        partial_priority.gas_price = None;
637        partial_priority.max_fee_per_gas = None;
638        partial_priority.max_priority_fee_per_gas = Some(3_000_000_000);
639
640        let result = validate_explicit_price_bump(&old_eip1559, &partial_priority, &relayer, false);
641        assert!(result.is_err());
642    }
643
644    #[test]
645    fn test_validate_explicit_price_bump_priority_fee_exceeds_max_fee() {
646        let relayer = create_test_relayer();
647        let old_eip1559 = create_eip1559_transaction_data();
648        let mut new_eip1559 = create_eip1559_transaction_data();
649        new_eip1559.max_fee_per_gas = Some(35_000_000_000);
650        new_eip1559.max_priority_fee_per_gas = Some(40_000_000_000);
651
652        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
653        assert!(result.is_err());
654    }
655
656    #[test]
657    fn test_validate_explicit_price_bump_priority_fee_equals_max_fee() {
658        let relayer = create_test_relayer();
659        let old_eip1559 = create_eip1559_transaction_data();
660        let mut new_eip1559 = create_eip1559_transaction_data();
661        new_eip1559.max_fee_per_gas = Some(35_000_000_000);
662        new_eip1559.max_priority_fee_per_gas = Some(35_000_000_000);
663
664        let result = validate_explicit_price_bump(&old_eip1559, &new_eip1559, &relayer, false);
665        assert!(result.is_ok());
666    }
667
668    #[tokio::test]
669    async fn test_calculate_replacement_price_legacy_sufficient_market_price() {
670        let old_tx = create_legacy_transaction_data();
671        let new_tx = create_legacy_transaction_data();
672        let relayer = create_test_relayer();
673
674        let price_calculator = MockPriceCalculator {
675            gas_price: Some(25_000_000_000),
676            max_fee_per_gas: None,
677            max_priority_fee_per_gas: None,
678            should_error: false,
679        };
680
681        let result =
682            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
683        assert!(result.is_ok());
684
685        let price_params = result.unwrap();
686        assert_eq!(price_params.gas_price, Some(25_000_000_000));
687        assert_eq!(price_params.is_min_bumped, Some(true));
688    }
689
690    #[tokio::test]
691    async fn test_calculate_replacement_price_legacy_insufficient_market_price() {
692        let old_tx = create_legacy_transaction_data();
693        let new_tx = create_legacy_transaction_data();
694        let relayer = create_test_relayer();
695
696        let price_calculator = MockPriceCalculator {
697            gas_price: Some(18_000_000_000), // 18 gwei (insufficient, needs 22 gwei)
698            max_fee_per_gas: None,
699            max_priority_fee_per_gas: None,
700            should_error: false,
701        };
702
703        let result =
704            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
705        assert!(result.is_ok());
706
707        let price_params = result.unwrap();
708        assert_eq!(price_params.gas_price, Some(22_000_000_000)); // Should be bumped to minimum
709        assert_eq!(price_params.is_min_bumped, Some(true));
710    }
711
712    #[tokio::test]
713    async fn test_calculate_replacement_price_eip1559_sufficient() {
714        let old_tx = create_eip1559_transaction_data();
715        let new_tx = create_eip1559_transaction_data();
716        let relayer = create_test_relayer();
717
718        let price_calculator = MockPriceCalculator {
719            gas_price: None,
720            max_fee_per_gas: Some(40_000_000_000),
721            max_priority_fee_per_gas: Some(3_000_000_000),
722            should_error: false,
723        };
724
725        let result =
726            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
727        assert!(result.is_ok());
728
729        let price_params = result.unwrap();
730        assert_eq!(price_params.max_fee_per_gas, Some(40_000_000_000));
731        assert_eq!(price_params.is_min_bumped, Some(true));
732    }
733
734    #[tokio::test]
735    async fn test_calculate_replacement_price_eip1559_insufficient_with_priority_fee_bump() {
736        let mut old_tx = create_eip1559_transaction_data();
737        old_tx.max_fee_per_gas = Some(30_000_000_000);
738        old_tx.max_priority_fee_per_gas = Some(5_000_000_000);
739
740        let new_tx = create_eip1559_transaction_data();
741        let relayer = create_test_relayer();
742
743        let price_calculator = MockPriceCalculator {
744            gas_price: None,
745            max_fee_per_gas: Some(25_000_000_000), // 25 gwei (insufficient, needs 33 gwei)
746            max_priority_fee_per_gas: Some(4_000_000_000), // 4 gwei (insufficient, needs 5.5 gwei)
747            should_error: false,
748        };
749
750        let result =
751            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
752        assert!(result.is_ok());
753
754        let price_params = result.unwrap();
755        assert_eq!(price_params.max_fee_per_gas, Some(33_000_000_000));
756
757        // Priority fee should also be bumped if old transaction had it
758        let expected_priority_bump = calculate_min_bump(5_000_000_000); // 5.5 gwei
759        let capped_priority = expected_priority_bump.min(33_000_000_000); // Capped at max_fee
760        assert_eq!(price_params.max_priority_fee_per_gas, Some(capped_priority));
761    }
762
763    #[tokio::test]
764    async fn test_calculate_replacement_price_network_lacks_mempool() {
765        let old_tx = create_legacy_transaction_data();
766        let new_tx = create_legacy_transaction_data();
767        let relayer = create_test_relayer();
768
769        let price_calculator = MockPriceCalculator {
770            gas_price: Some(15_000_000_000), // 15 gwei (would be insufficient normally)
771            max_fee_per_gas: None,
772            max_priority_fee_per_gas: None,
773            should_error: false,
774        };
775
776        let result =
777            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, true).await;
778        assert!(result.is_ok());
779
780        let price_params = result.unwrap();
781        assert_eq!(price_params.gas_price, Some(15_000_000_000)); // Uses market price as-is
782        assert_eq!(price_params.is_min_bumped, Some(true));
783    }
784
785    #[tokio::test]
786    async fn test_calculate_replacement_price_calculator_error() {
787        let old_tx = create_legacy_transaction_data();
788        let new_tx = create_legacy_transaction_data();
789        let relayer = create_test_relayer();
790
791        let price_calculator = MockPriceCalculator {
792            gas_price: None,
793            max_fee_per_gas: None,
794            max_priority_fee_per_gas: None,
795            should_error: true,
796        };
797
798        let result =
799            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
800        assert!(result.is_err());
801    }
802
803    #[tokio::test]
804    async fn test_determine_replacement_pricing_explicit_prices() {
805        let old_tx = create_legacy_transaction_data();
806        let mut new_tx = create_legacy_transaction_data();
807        new_tx.gas_price = Some(25_000_000_000);
808        let relayer = create_test_relayer();
809
810        let price_calculator = MockPriceCalculator {
811            gas_price: Some(30_000_000_000),
812            max_fee_per_gas: None,
813            max_priority_fee_per_gas: None,
814            should_error: false,
815        };
816
817        let result =
818            determine_replacement_pricing(&old_tx, &new_tx, &relayer, &price_calculator, false)
819                .await;
820        assert!(result.is_ok());
821
822        let price_params = result.unwrap();
823        assert_eq!(price_params.gas_price, Some(25_000_000_000));
824    }
825
826    #[tokio::test]
827    async fn test_determine_replacement_pricing_market_prices() {
828        let old_tx = create_legacy_transaction_data();
829        let mut new_tx = create_legacy_transaction_data();
830        new_tx.gas_price = None;
831        let relayer = create_test_relayer();
832
833        let price_calculator = MockPriceCalculator {
834            gas_price: Some(30_000_000_000),
835            max_fee_per_gas: None,
836            max_priority_fee_per_gas: None,
837            should_error: false,
838        };
839
840        let result =
841            determine_replacement_pricing(&old_tx, &new_tx, &relayer, &price_calculator, false)
842                .await;
843        assert!(result.is_ok());
844
845        let price_params = result.unwrap();
846        assert_eq!(price_params.gas_price, Some(30_000_000_000));
847    }
848
849    #[tokio::test]
850    async fn test_determine_replacement_pricing_compatibility_error() {
851        let old_legacy = create_legacy_transaction_data();
852        let new_eip1559 = create_eip1559_transaction_data();
853        let relayer = create_test_relayer();
854
855        let price_calculator = MockPriceCalculator {
856            gas_price: None,
857            max_fee_per_gas: None,
858            max_priority_fee_per_gas: None,
859            should_error: false,
860        };
861
862        let result = determine_replacement_pricing(
863            &old_legacy,
864            &new_eip1559,
865            &relayer,
866            &price_calculator,
867            false,
868        )
869        .await;
870        assert!(result.is_err());
871    }
872
873    #[test]
874    fn test_validate_price_bump_requirements_legacy() {
875        let old_tx = create_legacy_transaction_data();
876
877        let mut new_tx_sufficient = create_legacy_transaction_data();
878        new_tx_sufficient.gas_price = Some(22_000_000_000);
879        assert!(validate_price_bump_requirements(&old_tx, &new_tx_sufficient).is_ok());
880
881        let mut new_tx_insufficient = create_legacy_transaction_data();
882        new_tx_insufficient.gas_price = Some(21_000_000_000);
883        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient).is_err());
884    }
885
886    #[test]
887    fn test_validate_price_bump_requirements_eip1559() {
888        let old_tx = create_eip1559_transaction_data();
889
890        let mut new_tx_sufficient = create_eip1559_transaction_data();
891        new_tx_sufficient.max_fee_per_gas = Some(33_000_000_000);
892        new_tx_sufficient.max_priority_fee_per_gas = Some(3_000_000_000);
893        assert!(validate_price_bump_requirements(&old_tx, &new_tx_sufficient).is_ok());
894
895        let mut new_tx_insufficient_max = create_eip1559_transaction_data();
896        new_tx_insufficient_max.max_fee_per_gas = Some(32_000_000_000);
897        new_tx_insufficient_max.max_priority_fee_per_gas = Some(3_000_000_000);
898        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient_max).is_err());
899
900        let mut new_tx_insufficient_priority = create_eip1559_transaction_data();
901        new_tx_insufficient_priority.max_fee_per_gas = Some(33_000_000_000);
902        new_tx_insufficient_priority.max_priority_fee_per_gas = Some(2_100_000_000);
903        assert!(validate_price_bump_requirements(&old_tx, &new_tx_insufficient_priority).is_err());
904    }
905
906    #[test]
907    fn test_validate_price_bump_requirements_partial_eip1559() {
908        let mut old_tx = create_eip1559_transaction_data();
909        old_tx.max_fee_per_gas = Some(30_000_000_000);
910        old_tx.max_priority_fee_per_gas = Some(5_000_000_000);
911
912        let mut new_tx_only_priority = create_legacy_transaction_data();
913        new_tx_only_priority.gas_price = None;
914        new_tx_only_priority.max_fee_per_gas = None;
915        new_tx_only_priority.max_priority_fee_per_gas = Some(6_000_000_000);
916        let result = validate_price_bump_requirements(&old_tx, &new_tx_only_priority);
917        assert!(result.is_err());
918
919        let mut new_tx_only_max = create_legacy_transaction_data();
920        new_tx_only_max.gas_price = None;
921        new_tx_only_max.max_fee_per_gas = Some(33_000_000_000);
922        new_tx_only_max.max_priority_fee_per_gas = None;
923        let result = validate_price_bump_requirements(&old_tx, &new_tx_only_max);
924        assert!(result.is_err());
925
926        let new_legacy = create_legacy_transaction_data();
927        let result = validate_price_bump_requirements(&old_tx, &new_legacy);
928        assert!(result.is_err());
929
930        let old_legacy = create_legacy_transaction_data();
931        let result = validate_price_bump_requirements(&old_legacy, &new_tx_only_priority);
932        assert!(result.is_err());
933    }
934
935    #[test]
936    fn test_validate_price_bump_requirements_missing_pricing_data() {
937        let mut old_tx_no_price = create_legacy_transaction_data();
938        old_tx_no_price.gas_price = None;
939        old_tx_no_price.max_fee_per_gas = None;
940        old_tx_no_price.max_priority_fee_per_gas = None;
941
942        let mut new_tx_no_price = create_legacy_transaction_data();
943        new_tx_no_price.gas_price = None;
944        new_tx_no_price.max_fee_per_gas = None;
945        new_tx_no_price.max_priority_fee_per_gas = None;
946
947        let result = validate_price_bump_requirements(&old_tx_no_price, &new_tx_no_price);
948        assert!(result.is_err()); // Should fail because new transaction has no pricing
949
950        // Test old transaction with no pricing, new with legacy pricing - should succeed
951        let new_legacy = create_legacy_transaction_data();
952        let result = validate_price_bump_requirements(&old_tx_no_price, &new_legacy);
953        assert!(result.is_ok());
954
955        // Test old transaction with no pricing, new with EIP1559 pricing - should succeed
956        let new_eip1559 = create_eip1559_transaction_data();
957        let result = validate_price_bump_requirements(&old_tx_no_price, &new_eip1559);
958        assert!(result.is_ok());
959
960        // Test old legacy, new with no pricing - should fail
961        let old_legacy = create_legacy_transaction_data();
962        let result = validate_price_bump_requirements(&old_legacy, &new_tx_no_price);
963        assert!(result.is_err()); // Should fail because new transaction has no pricing
964    }
965
966    #[test]
967    fn test_validate_explicit_price_bump_zero_gas_price_cap() {
968        let old_tx = create_legacy_transaction_data();
969        let relayer = create_relayer_with_gas_cap(0);
970        let mut new_tx = create_legacy_transaction_data();
971        new_tx.gas_price = Some(1);
972
973        let result = validate_explicit_price_bump(&old_tx, &new_tx, &relayer, false);
974        assert!(result.is_err());
975    }
976
977    #[tokio::test]
978    async fn test_calculate_replacement_price_legacy_missing_old_gas_price() {
979        let mut old_tx = create_legacy_transaction_data();
980        old_tx.gas_price = None;
981        let new_tx = create_legacy_transaction_data();
982        let relayer = create_test_relayer();
983
984        let price_calculator = MockPriceCalculator {
985            gas_price: Some(25_000_000_000),
986            max_fee_per_gas: None,
987            max_priority_fee_per_gas: None,
988            should_error: false,
989        };
990
991        let result =
992            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
993        assert!(result.is_err());
994    }
995
996    #[tokio::test]
997    async fn test_calculate_replacement_price_eip1559_missing_old_fees() {
998        let mut old_tx = create_eip1559_transaction_data();
999        old_tx.max_fee_per_gas = None;
1000        old_tx.max_priority_fee_per_gas = None;
1001        let new_tx = create_eip1559_transaction_data();
1002        let relayer = create_test_relayer();
1003
1004        let price_calculator = MockPriceCalculator {
1005            gas_price: None,
1006            max_fee_per_gas: Some(40_000_000_000),
1007            max_priority_fee_per_gas: Some(3_000_000_000),
1008            should_error: false,
1009        };
1010
1011        let result =
1012            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
1013        assert!(result.is_err());
1014    }
1015
1016    #[tokio::test]
1017    async fn test_calculate_replacement_price_force_legacy_with_eip1559_policy_disabled() {
1018        let old_tx = create_eip1559_transaction_data();
1019        let new_tx = create_eip1559_transaction_data();
1020        let mut relayer = create_test_relayer();
1021        if let crate::models::RelayerNetworkPolicy::Evm(ref mut policy) = relayer.policies {
1022            policy.eip1559_pricing = Some(false);
1023        }
1024
1025        let price_calculator = MockPriceCalculator {
1026            gas_price: Some(25_000_000_000),
1027            max_fee_per_gas: None,
1028            max_priority_fee_per_gas: None,
1029            should_error: false,
1030        };
1031
1032        let result =
1033            calculate_replacement_price(&old_tx, &new_tx, &relayer, &price_calculator, false).await;
1034        assert!(result.is_err());
1035    }
1036}