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