1use crate::constants::{
2 ARBITRUM_GAS_LIMIT, DEFAULT_GAS_LIMIT, DEFAULT_TX_VALID_TIMESPAN,
3 EVM_MIN_AGE_FOR_RESUBMIT_SECONDS, MAXIMUM_NOOP_RETRY_ATTEMPTS, MAXIMUM_TX_ATTEMPTS,
4};
5use crate::domain::get_age_since_created;
6use crate::models::EvmNetwork;
7use crate::models::{
8 EvmTransactionData, TransactionError, TransactionRepoModel, TransactionStatus, U256,
9};
10use crate::services::provider::EvmProviderTrait;
11use chrono::{DateTime, Duration, Utc};
12use eyre::Result;
13
14pub async fn make_noop<P: EvmProviderTrait>(
18 evm_data: &mut EvmTransactionData,
19 network: &EvmNetwork,
20 provider: Option<&P>,
21) -> Result<(), TransactionError> {
22 evm_data.value = U256::from(0u64);
24 evm_data.data = Some("0x".to_string());
25 evm_data.to = Some(evm_data.from.clone());
26
27 if network.is_arbitrum() {
29 if let Some(provider) = provider {
31 match provider.estimate_gas(evm_data).await {
32 Ok(estimated_gas) => {
33 evm_data.gas_limit = Some(estimated_gas.max(DEFAULT_GAS_LIMIT));
35 }
36 Err(e) => {
37 tracing::warn!(
39 "Failed to estimate gas for Arbitrum noop transaction: {:?}",
40 e
41 );
42 evm_data.gas_limit = Some(ARBITRUM_GAS_LIMIT);
43 }
44 }
45 } else {
46 evm_data.gas_limit = Some(ARBITRUM_GAS_LIMIT);
48 }
49 } else {
50 evm_data.gas_limit = Some(DEFAULT_GAS_LIMIT);
52 }
53
54 Ok(())
55}
56
57pub fn is_noop(evm_data: &EvmTransactionData) -> bool {
59 evm_data.value == U256::from(0u64)
60 && evm_data.data.as_ref().is_some_and(|data| data == "0x")
61 && evm_data.to.as_ref() == Some(&evm_data.from)
62 && evm_data.speed.is_some()
63}
64
65pub fn too_many_attempts(tx: &TransactionRepoModel) -> bool {
67 tx.hashes.len() > MAXIMUM_TX_ATTEMPTS
68}
69
70pub fn too_many_noop_attempts(tx: &TransactionRepoModel) -> bool {
72 tx.noop_count.unwrap_or(0) > MAXIMUM_NOOP_RETRY_ATTEMPTS
73}
74
75pub fn ensure_status(
90 tx: &TransactionRepoModel,
91 expected: TransactionStatus,
92 operation: Option<&str>,
93) -> Result<(), TransactionError> {
94 if tx.status != expected {
95 let error_msg = if let Some(op) = operation {
96 format!(
97 "Invalid transaction state for {}. Current: {:?}, Expected: {:?}",
98 op, tx.status, expected
99 )
100 } else {
101 format!(
102 "Invalid transaction state. Current: {:?}, Expected: {:?}",
103 tx.status, expected
104 )
105 };
106 return Err(TransactionError::ValidationError(error_msg));
107 }
108 Ok(())
109}
110
111pub fn ensure_status_one_of(
126 tx: &TransactionRepoModel,
127 expected: &[TransactionStatus],
128 operation: Option<&str>,
129) -> Result<(), TransactionError> {
130 if !expected.contains(&tx.status) {
131 let error_msg = if let Some(op) = operation {
132 format!(
133 "Invalid transaction state for {}. Current: {:?}, Expected one of: {:?}",
134 op, tx.status, expected
135 )
136 } else {
137 format!(
138 "Invalid transaction state. Current: {:?}, Expected one of: {:?}",
139 tx.status, expected
140 )
141 };
142 return Err(TransactionError::ValidationError(error_msg));
143 }
144 Ok(())
145}
146
147pub fn has_enough_confirmations(
149 tx_block_number: u64,
150 current_block_number: u64,
151 required_confirmations: u64,
152) -> bool {
153 current_block_number >= tx_block_number + required_confirmations
154}
155
156pub fn is_transaction_valid(created_at: &str, valid_until: &Option<String>) -> bool {
158 if let Some(valid_until_str) = valid_until {
159 match DateTime::parse_from_rfc3339(valid_until_str) {
160 Ok(valid_until_time) => return Utc::now() < valid_until_time,
161 Err(e) => {
162 tracing::warn!(error = %e, "failed to parse valid_until timestamp");
163 return false;
164 }
165 }
166 }
167 match DateTime::parse_from_rfc3339(created_at) {
168 Ok(created_time) => {
169 let default_valid_until =
170 created_time + Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN);
171 Utc::now() < default_valid_until
172 }
173 Err(e) => {
174 tracing::warn!(error = %e, "failed to parse created_at timestamp");
175 false
176 }
177 }
178}
179
180pub fn get_age_since_status_change(
183 tx: &TransactionRepoModel,
184) -> Result<Duration, TransactionError> {
185 if let Some(sent_at) = &tx.sent_at {
187 let sent = DateTime::parse_from_rfc3339(sent_at)
188 .map_err(|e| {
189 TransactionError::UnexpectedError(format!("Error parsing sent_at time: {e}"))
190 })?
191 .with_timezone(&Utc);
192 return Ok(Utc::now().signed_duration_since(sent));
193 }
194
195 get_age_since_created(tx)
197}
198
199pub fn is_too_early_to_resubmit(tx: &TransactionRepoModel) -> Result<bool, TransactionError> {
205 let age = get_age_since_created(tx)?;
206 Ok(age < Duration::seconds(EVM_MIN_AGE_FOR_RESUBMIT_SECONDS))
207}
208
209#[deprecated(since = "1.1.0", note = "Use `is_too_early_to_resubmit` instead")]
212pub fn is_too_early_to_check(tx: &TransactionRepoModel) -> Result<bool, TransactionError> {
213 is_too_early_to_resubmit(tx)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::constants::{ARBITRUM_BASED_TAG, ROLLUP_TAG};
220 use crate::domain::transaction::evm::test_helpers::test_utils::make_test_transaction;
221 use crate::models::{evm::Speed, EvmTransactionData, NetworkTransactionData, U256};
222 use crate::services::provider::{MockEvmProviderTrait, ProviderError};
223 use crate::utils::mocks::mockutils::create_mock_transaction;
224
225 fn create_standard_network() -> EvmNetwork {
226 EvmNetwork {
227 network: "ethereum".to_string(),
228 rpc_urls: vec!["https://mainnet.infura.io".to_string()],
229 explorer_urls: None,
230 average_blocktime_ms: 12000,
231 is_testnet: false,
232 tags: vec!["mainnet".to_string()],
233 chain_id: 1,
234 required_confirmations: 12,
235 features: vec!["eip1559".to_string()],
236 symbol: "ETH".to_string(),
237 gas_price_cache: None,
238 }
239 }
240
241 fn create_arbitrum_network() -> EvmNetwork {
242 EvmNetwork {
243 network: "arbitrum".to_string(),
244 rpc_urls: vec!["https://arb1.arbitrum.io/rpc".to_string()],
245 explorer_urls: None,
246 average_blocktime_ms: 1000,
247 is_testnet: false,
248 tags: vec![ROLLUP_TAG.to_string(), ARBITRUM_BASED_TAG.to_string()],
249 chain_id: 42161,
250 required_confirmations: 1,
251 features: vec!["eip1559".to_string()],
252 symbol: "ETH".to_string(),
253 gas_price_cache: None,
254 }
255 }
256
257 fn create_arbitrum_nova_network() -> EvmNetwork {
258 EvmNetwork {
259 network: "arbitrum-nova".to_string(),
260 rpc_urls: vec!["https://nova.arbitrum.io/rpc".to_string()],
261 explorer_urls: None,
262 average_blocktime_ms: 1000,
263 is_testnet: false,
264 tags: vec![ROLLUP_TAG.to_string(), ARBITRUM_BASED_TAG.to_string()],
265 chain_id: 42170,
266 required_confirmations: 1,
267 features: vec!["eip1559".to_string()],
268 symbol: "ETH".to_string(),
269 gas_price_cache: None,
270 }
271 }
272
273 #[tokio::test]
274 async fn test_make_noop_standard_network() {
275 let mut evm_data = EvmTransactionData {
276 from: "0x1234567890123456789012345678901234567890".to_string(),
277 to: Some("0xoriginal_destination".to_string()),
278 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
280 gas_limit: Some(50000),
281 gas_price: Some(10_000_000_000),
282 max_fee_per_gas: None,
283 max_priority_fee_per_gas: None,
284 nonce: Some(42),
285 signature: None,
286 hash: Some("0xoriginal_hash".to_string()),
287 speed: Some(Speed::Fast),
288 chain_id: 1,
289 raw: Some(vec![1, 2, 3]),
290 };
291
292 let network = create_standard_network();
293 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
294 assert!(result.is_ok());
295
296 assert_eq!(evm_data.gas_limit, Some(21_000)); assert_eq!(evm_data.to.unwrap(), evm_data.from); assert_eq!(evm_data.value, U256::from(0u64)); assert_eq!(evm_data.data.unwrap(), "0x"); assert_eq!(evm_data.nonce, Some(42)); }
303
304 #[tokio::test]
305 async fn test_make_noop_arbitrum_network() {
306 let mut evm_data = EvmTransactionData {
307 from: "0x1234567890123456789012345678901234567890".to_string(),
308 to: Some("0xoriginal_destination".to_string()),
309 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
311 gas_limit: Some(50000),
312 gas_price: Some(10_000_000_000),
313 max_fee_per_gas: None,
314 max_priority_fee_per_gas: None,
315 nonce: Some(42),
316 signature: None,
317 hash: Some("0xoriginal_hash".to_string()),
318 speed: Some(Speed::Fast),
319 chain_id: 42161, raw: Some(vec![1, 2, 3]),
321 };
322
323 let network = create_arbitrum_network();
324 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
325 assert!(result.is_ok());
326
327 assert_eq!(evm_data.gas_limit, Some(50_000)); assert_eq!(evm_data.to.unwrap(), evm_data.from); assert_eq!(evm_data.value, U256::from(0u64)); assert_eq!(evm_data.data.unwrap(), "0x"); assert_eq!(evm_data.nonce, Some(42)); assert_eq!(evm_data.chain_id, 42161); }
335
336 #[tokio::test]
337 async fn test_make_noop_arbitrum_nova() {
338 let mut evm_data = EvmTransactionData {
339 from: "0x1234567890123456789012345678901234567890".to_string(),
340 to: Some("0xoriginal_destination".to_string()),
341 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
343 gas_limit: Some(30000),
344 gas_price: Some(10_000_000_000),
345 max_fee_per_gas: None,
346 max_priority_fee_per_gas: None,
347 nonce: Some(42),
348 signature: None,
349 hash: Some("0xoriginal_hash".to_string()),
350 speed: Some(Speed::Fast),
351 chain_id: 42170, raw: Some(vec![1, 2, 3]),
353 };
354
355 let network = create_arbitrum_nova_network();
356 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
357 assert!(result.is_ok());
358
359 assert_eq!(evm_data.gas_limit, Some(50_000)); assert_eq!(evm_data.to.unwrap(), evm_data.from); assert_eq!(evm_data.value, U256::from(0u64)); assert_eq!(evm_data.data.unwrap(), "0x"); assert_eq!(evm_data.nonce, Some(42)); assert_eq!(evm_data.chain_id, 42170); }
367
368 #[tokio::test]
369 async fn test_make_noop_arbitrum_with_provider() {
370 let mut mock_provider = MockEvmProviderTrait::new();
371
372 mock_provider
374 .expect_estimate_gas()
375 .times(1)
376 .returning(|_| Box::pin(async move { Ok(35_000) }));
377
378 let mut evm_data = EvmTransactionData {
379 from: "0x1234567890123456789012345678901234567890".to_string(),
380 to: Some("0xoriginal_destination".to_string()),
381 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
383 gas_limit: Some(30000),
384 gas_price: Some(10_000_000_000),
385 max_fee_per_gas: None,
386 max_priority_fee_per_gas: None,
387 nonce: Some(42),
388 signature: None,
389 hash: Some("0xoriginal_hash".to_string()),
390 speed: Some(Speed::Fast),
391 chain_id: 42161, raw: Some(vec![1, 2, 3]),
393 };
394
395 let network = create_arbitrum_network();
396 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
397 assert!(result.is_ok());
398
399 assert_eq!(evm_data.gas_limit, Some(35_000)); assert_eq!(evm_data.to.unwrap(), evm_data.from); assert_eq!(evm_data.value, U256::from(0u64)); assert_eq!(evm_data.data.unwrap(), "0x"); assert_eq!(evm_data.nonce, Some(42)); assert_eq!(evm_data.chain_id, 42161); }
407
408 #[tokio::test]
409 async fn test_make_noop_arbitrum_provider_estimation_fails() {
410 let mut mock_provider = MockEvmProviderTrait::new();
411
412 mock_provider.expect_estimate_gas().times(1).returning(|_| {
414 Box::pin(async move { Err(ProviderError::Other("Network error".to_string())) })
415 });
416
417 let mut evm_data = EvmTransactionData {
418 from: "0x1234567890123456789012345678901234567890".to_string(),
419 to: Some("0xoriginal_destination".to_string()),
420 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
422 gas_limit: Some(30000),
423 gas_price: Some(10_000_000_000),
424 max_fee_per_gas: None,
425 max_priority_fee_per_gas: None,
426 nonce: Some(42),
427 signature: None,
428 hash: Some("0xoriginal_hash".to_string()),
429 speed: Some(Speed::Fast),
430 chain_id: 42161, raw: Some(vec![1, 2, 3]),
432 };
433
434 let network = create_arbitrum_network();
435 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
436 assert!(result.is_ok());
437
438 assert_eq!(evm_data.gas_limit, Some(50_000)); assert_eq!(evm_data.to.unwrap(), evm_data.from); assert_eq!(evm_data.value, U256::from(0u64)); assert_eq!(evm_data.data.unwrap(), "0x"); assert_eq!(evm_data.nonce, Some(42)); assert_eq!(evm_data.chain_id, 42161); }
446
447 #[test]
448 fn test_is_noop() {
449 let noop_tx = EvmTransactionData {
451 from: "0x1234567890123456789012345678901234567890".to_string(),
452 to: Some("0x1234567890123456789012345678901234567890".to_string()), value: U256::from(0u64),
454 data: Some("0x".to_string()),
455 gas_limit: Some(21000),
456 gas_price: Some(10_000_000_000),
457 max_fee_per_gas: None,
458 max_priority_fee_per_gas: None,
459 nonce: Some(42),
460 signature: None,
461 hash: None,
462 speed: Some(Speed::Fast),
463 chain_id: 1,
464 raw: None,
465 };
466 assert!(is_noop(&noop_tx));
467
468 let mut non_noop = noop_tx.clone();
470 non_noop.value = U256::from(1000000000000000000u64); assert!(!is_noop(&non_noop));
472
473 let mut non_noop = noop_tx.clone();
474 non_noop.data = Some("0x123456".to_string());
475 assert!(!is_noop(&non_noop));
476
477 let mut non_noop = noop_tx.clone();
478 non_noop.to = Some("0x9876543210987654321098765432109876543210".to_string());
479 assert!(!is_noop(&non_noop));
480
481 let mut non_noop = noop_tx;
482 non_noop.speed = None;
483 assert!(!is_noop(&non_noop));
484 }
485
486 #[test]
487 fn test_too_many_attempts() {
488 let mut tx = TransactionRepoModel {
489 id: "test-tx".to_string(),
490 relayer_id: "test-relayer".to_string(),
491 status: TransactionStatus::Pending,
492 status_reason: None,
493 created_at: "2024-01-01T00:00:00Z".to_string(),
494 sent_at: None,
495 confirmed_at: None,
496 valid_until: None,
497 network_type: crate::models::NetworkType::Evm,
498 network_data: NetworkTransactionData::Evm(EvmTransactionData {
499 from: "0x1234".to_string(),
500 to: Some("0x5678".to_string()),
501 value: U256::from(0u64),
502 data: Some("0x".to_string()),
503 gas_limit: Some(21000),
504 gas_price: Some(10_000_000_000),
505 max_fee_per_gas: None,
506 max_priority_fee_per_gas: None,
507 nonce: Some(42),
508 signature: None,
509 hash: None,
510 speed: Some(Speed::Fast),
511 chain_id: 1,
512 raw: None,
513 }),
514 priced_at: None,
515 hashes: vec![], noop_count: None,
517 is_canceled: Some(false),
518 delete_at: None,
519 };
520
521 assert!(!too_many_attempts(&tx));
523
524 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS];
526 assert!(!too_many_attempts(&tx));
527
528 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS + 1];
530 assert!(too_many_attempts(&tx));
531 }
532
533 #[test]
534 fn test_too_many_noop_attempts() {
535 let mut tx = TransactionRepoModel {
536 id: "test-tx".to_string(),
537 relayer_id: "test-relayer".to_string(),
538 status: TransactionStatus::Pending,
539 status_reason: None,
540 created_at: "2024-01-01T00:00:00Z".to_string(),
541 sent_at: None,
542 confirmed_at: None,
543 valid_until: None,
544 network_type: crate::models::NetworkType::Evm,
545 network_data: NetworkTransactionData::Evm(EvmTransactionData {
546 from: "0x1234".to_string(),
547 to: Some("0x5678".to_string()),
548 value: U256::from(0u64),
549 data: Some("0x".to_string()),
550 gas_limit: Some(21000),
551 gas_price: Some(10_000_000_000),
552 max_fee_per_gas: None,
553 max_priority_fee_per_gas: None,
554 nonce: Some(42),
555 signature: None,
556 hash: None,
557 speed: Some(Speed::Fast),
558 chain_id: 1,
559 raw: None,
560 }),
561 priced_at: None,
562 hashes: vec![],
563 noop_count: None,
564 is_canceled: Some(false),
565 delete_at: None,
566 };
567
568 assert!(!too_many_noop_attempts(&tx));
570
571 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS);
573 assert!(!too_many_noop_attempts(&tx));
574
575 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS + 1);
577 assert!(too_many_noop_attempts(&tx));
578 }
579
580 #[test]
581 fn test_has_enough_confirmations() {
582 let tx_block_number = 100;
584 let current_block_number = 110; let required_confirmations = 12;
586 assert!(!has_enough_confirmations(
587 tx_block_number,
588 current_block_number,
589 required_confirmations
590 ));
591
592 let current_block_number = 112; assert!(has_enough_confirmations(
595 tx_block_number,
596 current_block_number,
597 required_confirmations
598 ));
599
600 let current_block_number = 120; assert!(has_enough_confirmations(
603 tx_block_number,
604 current_block_number,
605 required_confirmations
606 ));
607 }
608
609 #[test]
610 fn test_is_transaction_valid_with_future_timestamp() {
611 let now = Utc::now();
612 let valid_until = Some((now + Duration::hours(1)).to_rfc3339());
613 let created_at = now.to_rfc3339();
614
615 assert!(is_transaction_valid(&created_at, &valid_until));
616 }
617
618 #[test]
619 fn test_is_transaction_valid_with_past_timestamp() {
620 let now = Utc::now();
621 let valid_until = Some((now - Duration::hours(1)).to_rfc3339());
622 let created_at = now.to_rfc3339();
623
624 assert!(!is_transaction_valid(&created_at, &valid_until));
625 }
626
627 #[test]
628 fn test_is_transaction_valid_with_valid_until() {
629 let created_at = Utc::now().to_rfc3339();
631 let valid_until = Some((Utc::now() + Duration::hours(1)).to_rfc3339());
632 assert!(is_transaction_valid(&created_at, &valid_until));
633
634 let valid_until = Some((Utc::now() - Duration::hours(1)).to_rfc3339());
636 assert!(!is_transaction_valid(&created_at, &valid_until));
637
638 let valid_until = Some(Utc::now().to_rfc3339());
640 assert!(!is_transaction_valid(&created_at, &valid_until));
641
642 let valid_until = Some((Utc::now() + Duration::days(365)).to_rfc3339());
644 assert!(is_transaction_valid(&created_at, &valid_until));
645
646 let valid_until = Some("invalid-date-format".to_string());
648 assert!(!is_transaction_valid(&created_at, &valid_until));
649
650 let valid_until = Some("".to_string());
652 assert!(!is_transaction_valid(&created_at, &valid_until));
653 }
654
655 #[test]
656 fn test_is_transaction_valid_without_valid_until() {
657 let created_at = Utc::now().to_rfc3339();
659 let valid_until = None;
660 assert!(is_transaction_valid(&created_at, &valid_until));
661
662 let old_created_at =
664 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN + 1000)).to_rfc3339();
665 assert!(!is_transaction_valid(&old_created_at, &valid_until));
666
667 let boundary_created_at =
669 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN)).to_rfc3339();
670 assert!(!is_transaction_valid(&boundary_created_at, &valid_until));
671
672 let within_boundary_created_at =
674 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN - 1000)).to_rfc3339();
675 assert!(is_transaction_valid(
676 &within_boundary_created_at,
677 &valid_until
678 ));
679
680 let invalid_created_at = "invalid-date-format";
682 assert!(!is_transaction_valid(invalid_created_at, &valid_until));
683
684 assert!(!is_transaction_valid("", &valid_until));
686 }
687
688 #[test]
689 fn test_ensure_status_success() {
690 let tx = make_test_transaction(TransactionStatus::Pending);
691
692 let result = ensure_status(&tx, TransactionStatus::Pending, Some("test_operation"));
694 assert!(result.is_ok());
695 }
696
697 #[test]
698 fn test_ensure_status_failure_with_operation() {
699 let tx = make_test_transaction(TransactionStatus::Sent);
700
701 let result = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"));
703 assert!(result.is_err());
704
705 if let Err(TransactionError::ValidationError(msg)) = result {
706 assert!(msg.contains("prepare_transaction"));
707 assert!(msg.contains("Sent"));
708 assert!(msg.contains("Pending"));
709 } else {
710 panic!("Expected ValidationError");
711 }
712 }
713
714 #[test]
715 fn test_ensure_status_failure_without_operation() {
716 let tx = make_test_transaction(TransactionStatus::Sent);
717
718 let result = ensure_status(&tx, TransactionStatus::Pending, None);
720 assert!(result.is_err());
721
722 if let Err(TransactionError::ValidationError(msg)) = result {
723 assert!(!msg.contains("for"));
724 assert!(msg.contains("Sent"));
725 assert!(msg.contains("Pending"));
726 } else {
727 panic!("Expected ValidationError");
728 }
729 }
730
731 #[test]
732 fn test_ensure_status_all_states() {
733 let statuses = vec![
735 TransactionStatus::Pending,
736 TransactionStatus::Sent,
737 TransactionStatus::Submitted,
738 TransactionStatus::Mined,
739 TransactionStatus::Confirmed,
740 TransactionStatus::Failed,
741 TransactionStatus::Expired,
742 TransactionStatus::Canceled,
743 ];
744
745 for status in &statuses {
746 let tx = make_test_transaction(status.clone());
747
748 assert!(ensure_status(&tx, status.clone(), Some("test")).is_ok());
750
751 for other_status in &statuses {
753 if other_status != status {
754 assert!(ensure_status(&tx, other_status.clone(), Some("test")).is_err());
755 }
756 }
757 }
758 }
759
760 #[test]
761 fn test_ensure_status_one_of_success() {
762 let tx = make_test_transaction(TransactionStatus::Submitted);
763
764 let result = ensure_status_one_of(
766 &tx,
767 &[TransactionStatus::Submitted, TransactionStatus::Mined],
768 Some("resubmit_transaction"),
769 );
770 assert!(result.is_ok());
771 }
772
773 #[test]
774 fn test_ensure_status_one_of_success_first_in_list() {
775 let tx = make_test_transaction(TransactionStatus::Pending);
776
777 let result = ensure_status_one_of(
779 &tx,
780 &[
781 TransactionStatus::Pending,
782 TransactionStatus::Sent,
783 TransactionStatus::Submitted,
784 ],
785 Some("cancel_transaction"),
786 );
787 assert!(result.is_ok());
788 }
789
790 #[test]
791 fn test_ensure_status_one_of_success_last_in_list() {
792 let tx = make_test_transaction(TransactionStatus::Submitted);
793
794 let result = ensure_status_one_of(
796 &tx,
797 &[
798 TransactionStatus::Pending,
799 TransactionStatus::Sent,
800 TransactionStatus::Submitted,
801 ],
802 Some("cancel_transaction"),
803 );
804 assert!(result.is_ok());
805 }
806
807 #[test]
808 fn test_ensure_status_one_of_failure_with_operation() {
809 let tx = make_test_transaction(TransactionStatus::Confirmed);
810
811 let result = ensure_status_one_of(
813 &tx,
814 &[TransactionStatus::Pending, TransactionStatus::Sent],
815 Some("cancel_transaction"),
816 );
817 assert!(result.is_err());
818
819 if let Err(TransactionError::ValidationError(msg)) = result {
820 assert!(msg.contains("cancel_transaction"));
821 assert!(msg.contains("Confirmed"));
822 assert!(msg.contains("Pending"));
823 assert!(msg.contains("Sent"));
824 } else {
825 panic!("Expected ValidationError");
826 }
827 }
828
829 #[test]
830 fn test_ensure_status_one_of_failure_without_operation() {
831 let tx = make_test_transaction(TransactionStatus::Confirmed);
832
833 let result = ensure_status_one_of(
835 &tx,
836 &[TransactionStatus::Pending, TransactionStatus::Sent],
837 None,
838 );
839 assert!(result.is_err());
840
841 if let Err(TransactionError::ValidationError(msg)) = result {
842 assert!(!msg.contains("for"));
843 assert!(msg.contains("Confirmed"));
844 } else {
845 panic!("Expected ValidationError");
846 }
847 }
848
849 #[test]
850 fn test_ensure_status_one_of_single_status() {
851 let tx = make_test_transaction(TransactionStatus::Pending);
852
853 let result = ensure_status_one_of(&tx, &[TransactionStatus::Pending], Some("test"));
855 assert!(result.is_ok());
856
857 let tx2 = make_test_transaction(TransactionStatus::Sent);
859 let result = ensure_status_one_of(&tx2, &[TransactionStatus::Pending], Some("test"));
860 assert!(result.is_err());
861 }
862
863 #[test]
864 fn test_ensure_status_one_of_all_states() {
865 let all_statuses = vec![
866 TransactionStatus::Pending,
867 TransactionStatus::Sent,
868 TransactionStatus::Submitted,
869 TransactionStatus::Mined,
870 TransactionStatus::Confirmed,
871 TransactionStatus::Failed,
872 TransactionStatus::Expired,
873 TransactionStatus::Canceled,
874 ];
875
876 for status in &all_statuses {
878 let tx = make_test_transaction(status.clone());
879 let result = ensure_status_one_of(&tx, &all_statuses, Some("test"));
880 assert!(result.is_ok());
881 }
882 }
883
884 #[test]
885 fn test_ensure_status_one_of_empty_list() {
886 let tx = make_test_transaction(TransactionStatus::Pending);
887
888 let result = ensure_status_one_of(&tx, &[], Some("test"));
890 assert!(result.is_err());
891 }
892
893 #[test]
894 fn test_ensure_status_error_message_formatting() {
895 let tx = make_test_transaction(TransactionStatus::Confirmed);
896
897 let result = ensure_status(&tx, TransactionStatus::Pending, Some("my_operation"));
899 if let Err(TransactionError::ValidationError(msg)) = result {
900 assert!(msg.starts_with("Invalid transaction state for my_operation"));
902 assert!(msg.contains("Current: Confirmed"));
903 assert!(msg.contains("Expected: Pending"));
904 } else {
905 panic!("Expected ValidationError");
906 }
907
908 let result = ensure_status_one_of(
910 &tx,
911 &[TransactionStatus::Pending, TransactionStatus::Sent],
912 Some("another_operation"),
913 );
914 if let Err(TransactionError::ValidationError(msg)) = result {
915 assert!(msg.starts_with("Invalid transaction state for another_operation"));
917 assert!(msg.contains("Current: Confirmed"));
918 assert!(msg.contains("Expected one of:"));
919 } else {
920 panic!("Expected ValidationError");
921 }
922 }
923
924 #[test]
925 fn test_get_age_since_created() {
926 let now = Utc::now();
927
928 let created_time = now - Duration::hours(2);
930 let tx = TransactionRepoModel {
931 created_at: created_time.to_rfc3339(),
932 ..create_mock_transaction()
933 };
934
935 let age_result = get_age_since_created(&tx);
936 assert!(age_result.is_ok());
937 let age = age_result.unwrap();
938 assert!(age.num_minutes() >= 119 && age.num_minutes() <= 121);
940 }
941
942 #[test]
943 fn test_get_age_since_created_invalid_timestamp() {
944 let tx = TransactionRepoModel {
945 created_at: "invalid-timestamp".to_string(),
946 ..create_mock_transaction()
947 };
948
949 let result = get_age_since_created(&tx);
950 assert!(result.is_err());
951 match result.unwrap_err() {
952 TransactionError::UnexpectedError(msg) => {
953 assert!(msg.contains("Invalid created_at timestamp"));
954 }
955 _ => panic!("Expected UnexpectedError for invalid timestamp"),
956 }
957 }
958
959 #[test]
960 fn test_get_age_since_created_recent_transaction() {
961 let now = Utc::now();
962
963 let created_time = now - Duration::minutes(1);
965 let tx = TransactionRepoModel {
966 created_at: created_time.to_rfc3339(),
967 ..create_mock_transaction()
968 };
969
970 let age_result = get_age_since_created(&tx);
971 assert!(age_result.is_ok());
972 let age = age_result.unwrap();
973 assert!(age.num_seconds() >= 59 && age.num_seconds() <= 61);
975 }
976
977 #[test]
978 fn test_get_age_since_status_change_with_sent_at() {
979 let now = Utc::now();
980
981 let sent_time = now - Duration::hours(1);
983 let created_time = now - Duration::hours(3); let tx = TransactionRepoModel {
985 status: TransactionStatus::Sent,
986 created_at: created_time.to_rfc3339(),
987 sent_at: Some(sent_time.to_rfc3339()),
988 ..create_mock_transaction()
989 };
990
991 let age_result = get_age_since_status_change(&tx);
992 assert!(age_result.is_ok());
993 let age = age_result.unwrap();
994 assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
996 }
997
998 #[test]
999 fn test_get_age_since_status_change_without_sent_at() {
1000 let now = Utc::now();
1001
1002 let created_time = now - Duration::hours(2);
1004 let tx = TransactionRepoModel {
1005 created_at: created_time.to_rfc3339(),
1006 ..create_mock_transaction()
1007 };
1008
1009 let age_result = get_age_since_status_change(&tx);
1010 assert!(age_result.is_ok());
1011 let age = age_result.unwrap();
1012 assert!(age.num_minutes() >= 119 && age.num_minutes() <= 121);
1014 }
1015
1016 #[test]
1017 fn test_get_age_since_status_change_invalid_sent_at() {
1018 let now = Utc::now();
1019 let created_time = now - Duration::hours(2);
1020
1021 let tx = TransactionRepoModel {
1022 status: TransactionStatus::Sent,
1023 created_at: created_time.to_rfc3339(),
1024 sent_at: Some("invalid-timestamp".to_string()),
1025 ..create_mock_transaction()
1026 };
1027
1028 let result = get_age_since_status_change(&tx);
1029 assert!(result.is_err());
1030 match result.unwrap_err() {
1031 TransactionError::UnexpectedError(msg) => {
1032 assert!(msg.contains("Error parsing sent_at time"));
1033 }
1034 _ => panic!("Expected UnexpectedError for invalid sent_at timestamp"),
1035 }
1036 }
1037
1038 #[test]
1039 fn test_is_too_early_to_resubmit_recent_transaction() {
1040 let now = Utc::now();
1041
1042 let created_time = now - Duration::seconds(1);
1044 let tx = TransactionRepoModel {
1045 created_at: created_time.to_rfc3339(),
1046 ..create_mock_transaction()
1047 };
1048
1049 let result = is_too_early_to_resubmit(&tx);
1050 assert!(result.is_ok());
1051 assert!(result.unwrap()); }
1053
1054 #[test]
1055 fn test_is_too_early_to_resubmit_old_transaction() {
1056 let now = Utc::now();
1057
1058 let created_time = now - Duration::seconds(EVM_MIN_AGE_FOR_RESUBMIT_SECONDS + 10);
1060 let tx = TransactionRepoModel {
1061 created_at: created_time.to_rfc3339(),
1062 ..create_mock_transaction()
1063 };
1064
1065 let result = is_too_early_to_resubmit(&tx);
1066 assert!(result.is_ok());
1067 assert!(!result.unwrap()); }
1069
1070 #[test]
1071 fn test_is_too_early_to_resubmit_boundary() {
1072 let now = Utc::now();
1073
1074 let created_time = now - Duration::seconds(EVM_MIN_AGE_FOR_RESUBMIT_SECONDS);
1076 let tx = TransactionRepoModel {
1077 created_at: created_time.to_rfc3339(),
1078 ..create_mock_transaction()
1079 };
1080
1081 let result = is_too_early_to_resubmit(&tx);
1082 assert!(result.is_ok());
1083 assert!(!result.unwrap());
1085 }
1086
1087 #[test]
1088 fn test_is_too_early_to_resubmit_invalid_timestamp() {
1089 let tx = TransactionRepoModel {
1090 created_at: "invalid-timestamp".to_string(),
1091 ..create_mock_transaction()
1092 };
1093
1094 let result = is_too_early_to_resubmit(&tx);
1095 assert!(result.is_err());
1096 match result.unwrap_err() {
1097 TransactionError::UnexpectedError(msg) => {
1098 assert!(msg.contains("Invalid created_at timestamp"));
1099 }
1100 _ => panic!("Expected UnexpectedError for invalid timestamp"),
1101 }
1102 }
1103}