1use 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
15pub 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
30pub 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 if !has_explicit_prices(new_evm_data) {
50 return Ok(());
51 }
52
53 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
69pub 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(old_evm_data, new_evm_data)?;
91
92 if has_explicit_prices(new_evm_data) {
93 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
108pub 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 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 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 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 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 if !network_lacks_mempool {
171 validate_price_bump_requirements(old_evm_data, new_evm_data)?;
172 }
173
174 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 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
201fn 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 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 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_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 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 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 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 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
280pub 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 let use_legacy = old_evm_data.is_legacy()
302 || relayer.policies.get_evm_policy().eip1559_pricing == Some(false);
303
304 let mut price_params = price_calculator
306 .get_transaction_price_params(new_evm_data, relayer)
307 .await?;
308
309 if network_lacks_mempool {
311 price_params.is_min_bumped = Some(true);
312 return Ok(price_params);
313 }
314
315 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 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 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 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), gas_limit: Some(21000),
430 nonce: Some(1),
431 value: U256::from(1000000000000000000u128), 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), 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), max_priority_fee_per_gas: Some(2_000_000_000), 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), 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 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 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 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 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 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 let result = check_transaction_compatibility(&old_legacy, &old_eip1559);
542 assert!(result.is_err());
543
544 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); 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); 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); 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 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 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), 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)); 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), max_priority_fee_per_gas: Some(4_000_000_000), 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 let expected_priority_bump = calculate_min_bump(5_000_000_000); let capped_priority = expected_priority_bump.min(33_000_000_000); 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), 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)); 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()); 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 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 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()); }
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}