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