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 };
526
527 assert!(!too_many_attempts(&tx));
529
530 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS];
532 assert!(!too_many_attempts(&tx));
533
534 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS + 1];
536 assert!(too_many_attempts(&tx));
537 }
538
539 #[test]
540 fn test_too_many_noop_attempts() {
541 let mut tx = TransactionRepoModel {
542 id: "test-tx".to_string(),
543 relayer_id: "test-relayer".to_string(),
544 status: TransactionStatus::Pending,
545 status_reason: None,
546 created_at: "2024-01-01T00:00:00Z".to_string(),
547 sent_at: None,
548 confirmed_at: None,
549 valid_until: None,
550 network_type: crate::models::NetworkType::Evm,
551 network_data: NetworkTransactionData::Evm(EvmTransactionData {
552 from: "0x1234".to_string(),
553 to: Some("0x5678".to_string()),
554 value: U256::from(0u64),
555 data: Some("0x".to_string()),
556 gas_limit: Some(21000),
557 gas_price: Some(10_000_000_000),
558 max_fee_per_gas: None,
559 max_priority_fee_per_gas: None,
560 nonce: Some(42),
561 signature: None,
562 hash: None,
563 speed: Some(Speed::Fast),
564 chain_id: 1,
565 raw: None,
566 }),
567 priced_at: None,
568 hashes: vec![],
569 noop_count: None,
570 is_canceled: Some(false),
571 delete_at: None,
572 };
573
574 assert!(!too_many_noop_attempts(&tx));
576
577 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS);
579 assert!(!too_many_noop_attempts(&tx));
580
581 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS + 1);
583 assert!(too_many_noop_attempts(&tx));
584 }
585
586 #[test]
587 fn test_has_enough_confirmations() {
588 let tx_block_number = 100;
590 let current_block_number = 110; let required_confirmations = 12;
592 assert!(!has_enough_confirmations(
593 tx_block_number,
594 current_block_number,
595 required_confirmations
596 ));
597
598 let current_block_number = 112; assert!(has_enough_confirmations(
601 tx_block_number,
602 current_block_number,
603 required_confirmations
604 ));
605
606 let current_block_number = 120; assert!(has_enough_confirmations(
609 tx_block_number,
610 current_block_number,
611 required_confirmations
612 ));
613 }
614
615 #[test]
616 fn test_is_transaction_valid_with_future_timestamp() {
617 let now = Utc::now();
618 let valid_until = Some((now + Duration::hours(1)).to_rfc3339());
619 let created_at = now.to_rfc3339();
620
621 assert!(is_transaction_valid(&created_at, &valid_until));
622 }
623
624 #[test]
625 fn test_is_transaction_valid_with_past_timestamp() {
626 let now = Utc::now();
627 let valid_until = Some((now - Duration::hours(1)).to_rfc3339());
628 let created_at = now.to_rfc3339();
629
630 assert!(!is_transaction_valid(&created_at, &valid_until));
631 }
632
633 #[test]
634 fn test_is_transaction_valid_with_valid_until() {
635 let created_at = Utc::now().to_rfc3339();
637 let valid_until = Some((Utc::now() + Duration::hours(1)).to_rfc3339());
638 assert!(is_transaction_valid(&created_at, &valid_until));
639
640 let valid_until = Some((Utc::now() - Duration::hours(1)).to_rfc3339());
642 assert!(!is_transaction_valid(&created_at, &valid_until));
643
644 let valid_until = Some(Utc::now().to_rfc3339());
646 assert!(!is_transaction_valid(&created_at, &valid_until));
647
648 let valid_until = Some((Utc::now() + Duration::days(365)).to_rfc3339());
650 assert!(is_transaction_valid(&created_at, &valid_until));
651
652 let valid_until = Some("invalid-date-format".to_string());
654 assert!(!is_transaction_valid(&created_at, &valid_until));
655
656 let valid_until = Some("".to_string());
658 assert!(!is_transaction_valid(&created_at, &valid_until));
659 }
660
661 #[test]
662 fn test_is_transaction_valid_without_valid_until() {
663 let created_at = Utc::now().to_rfc3339();
665 let valid_until = None;
666 assert!(is_transaction_valid(&created_at, &valid_until));
667
668 let old_created_at =
670 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN + 1000)).to_rfc3339();
671 assert!(!is_transaction_valid(&old_created_at, &valid_until));
672
673 let boundary_created_at =
675 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN)).to_rfc3339();
676 assert!(!is_transaction_valid(&boundary_created_at, &valid_until));
677
678 let within_boundary_created_at =
680 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN - 1000)).to_rfc3339();
681 assert!(is_transaction_valid(
682 &within_boundary_created_at,
683 &valid_until
684 ));
685
686 let invalid_created_at = "invalid-date-format";
688 assert!(!is_transaction_valid(invalid_created_at, &valid_until));
689
690 assert!(!is_transaction_valid("", &valid_until));
692 }
693
694 #[test]
695 fn test_ensure_status_success() {
696 let tx = make_test_transaction(TransactionStatus::Pending);
697
698 let result = ensure_status(&tx, TransactionStatus::Pending, Some("test_operation"));
700 assert!(result.is_ok());
701 }
702
703 #[test]
704 fn test_ensure_status_failure_with_operation() {
705 let tx = make_test_transaction(TransactionStatus::Sent);
706
707 let result = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"));
709 assert!(result.is_err());
710
711 if let Err(TransactionError::ValidationError(msg)) = result {
712 assert!(msg.contains("prepare_transaction"));
713 assert!(msg.contains("Sent"));
714 assert!(msg.contains("Pending"));
715 } else {
716 panic!("Expected ValidationError");
717 }
718 }
719
720 #[test]
721 fn test_ensure_status_failure_without_operation() {
722 let tx = make_test_transaction(TransactionStatus::Sent);
723
724 let result = ensure_status(&tx, TransactionStatus::Pending, None);
726 assert!(result.is_err());
727
728 if let Err(TransactionError::ValidationError(msg)) = result {
729 assert!(!msg.contains("for"));
730 assert!(msg.contains("Sent"));
731 assert!(msg.contains("Pending"));
732 } else {
733 panic!("Expected ValidationError");
734 }
735 }
736
737 #[test]
738 fn test_ensure_status_all_states() {
739 let statuses = vec![
741 TransactionStatus::Pending,
742 TransactionStatus::Sent,
743 TransactionStatus::Submitted,
744 TransactionStatus::Mined,
745 TransactionStatus::Confirmed,
746 TransactionStatus::Failed,
747 TransactionStatus::Expired,
748 TransactionStatus::Canceled,
749 ];
750
751 for status in &statuses {
752 let tx = make_test_transaction(status.clone());
753
754 assert!(ensure_status(&tx, status.clone(), Some("test")).is_ok());
756
757 for other_status in &statuses {
759 if other_status != status {
760 assert!(ensure_status(&tx, other_status.clone(), Some("test")).is_err());
761 }
762 }
763 }
764 }
765
766 #[test]
767 fn test_ensure_status_one_of_success() {
768 let tx = make_test_transaction(TransactionStatus::Submitted);
769
770 let result = ensure_status_one_of(
772 &tx,
773 &[TransactionStatus::Submitted, TransactionStatus::Mined],
774 Some("resubmit_transaction"),
775 );
776 assert!(result.is_ok());
777 }
778
779 #[test]
780 fn test_ensure_status_one_of_success_first_in_list() {
781 let tx = make_test_transaction(TransactionStatus::Pending);
782
783 let result = ensure_status_one_of(
785 &tx,
786 &[
787 TransactionStatus::Pending,
788 TransactionStatus::Sent,
789 TransactionStatus::Submitted,
790 ],
791 Some("cancel_transaction"),
792 );
793 assert!(result.is_ok());
794 }
795
796 #[test]
797 fn test_ensure_status_one_of_success_last_in_list() {
798 let tx = make_test_transaction(TransactionStatus::Submitted);
799
800 let result = ensure_status_one_of(
802 &tx,
803 &[
804 TransactionStatus::Pending,
805 TransactionStatus::Sent,
806 TransactionStatus::Submitted,
807 ],
808 Some("cancel_transaction"),
809 );
810 assert!(result.is_ok());
811 }
812
813 #[test]
814 fn test_ensure_status_one_of_failure_with_operation() {
815 let tx = make_test_transaction(TransactionStatus::Confirmed);
816
817 let result = ensure_status_one_of(
819 &tx,
820 &[TransactionStatus::Pending, TransactionStatus::Sent],
821 Some("cancel_transaction"),
822 );
823 assert!(result.is_err());
824
825 if let Err(TransactionError::ValidationError(msg)) = result {
826 assert!(msg.contains("cancel_transaction"));
827 assert!(msg.contains("Confirmed"));
828 assert!(msg.contains("Pending"));
829 assert!(msg.contains("Sent"));
830 } else {
831 panic!("Expected ValidationError");
832 }
833 }
834
835 #[test]
836 fn test_ensure_status_one_of_failure_without_operation() {
837 let tx = make_test_transaction(TransactionStatus::Confirmed);
838
839 let result = ensure_status_one_of(
841 &tx,
842 &[TransactionStatus::Pending, TransactionStatus::Sent],
843 None,
844 );
845 assert!(result.is_err());
846
847 if let Err(TransactionError::ValidationError(msg)) = result {
848 assert!(!msg.contains("for"));
849 assert!(msg.contains("Confirmed"));
850 } else {
851 panic!("Expected ValidationError");
852 }
853 }
854
855 #[test]
856 fn test_ensure_status_one_of_single_status() {
857 let tx = make_test_transaction(TransactionStatus::Pending);
858
859 let result = ensure_status_one_of(&tx, &[TransactionStatus::Pending], Some("test"));
861 assert!(result.is_ok());
862
863 let tx2 = make_test_transaction(TransactionStatus::Sent);
865 let result = ensure_status_one_of(&tx2, &[TransactionStatus::Pending], Some("test"));
866 assert!(result.is_err());
867 }
868
869 #[test]
870 fn test_ensure_status_one_of_all_states() {
871 let all_statuses = vec![
872 TransactionStatus::Pending,
873 TransactionStatus::Sent,
874 TransactionStatus::Submitted,
875 TransactionStatus::Mined,
876 TransactionStatus::Confirmed,
877 TransactionStatus::Failed,
878 TransactionStatus::Expired,
879 TransactionStatus::Canceled,
880 ];
881
882 for status in &all_statuses {
884 let tx = make_test_transaction(status.clone());
885 let result = ensure_status_one_of(&tx, &all_statuses, Some("test"));
886 assert!(result.is_ok());
887 }
888 }
889
890 #[test]
891 fn test_ensure_status_one_of_empty_list() {
892 let tx = make_test_transaction(TransactionStatus::Pending);
893
894 let result = ensure_status_one_of(&tx, &[], Some("test"));
896 assert!(result.is_err());
897 }
898
899 #[test]
900 fn test_ensure_status_error_message_formatting() {
901 let tx = make_test_transaction(TransactionStatus::Confirmed);
902
903 let result = ensure_status(&tx, TransactionStatus::Pending, Some("my_operation"));
905 if let Err(TransactionError::ValidationError(msg)) = result {
906 assert!(msg.starts_with("Invalid transaction state for my_operation"));
908 assert!(msg.contains("Current: Confirmed"));
909 assert!(msg.contains("Expected: Pending"));
910 } else {
911 panic!("Expected ValidationError");
912 }
913
914 let result = ensure_status_one_of(
916 &tx,
917 &[TransactionStatus::Pending, TransactionStatus::Sent],
918 Some("another_operation"),
919 );
920 if let Err(TransactionError::ValidationError(msg)) = result {
921 assert!(msg.starts_with("Invalid transaction state for another_operation"));
923 assert!(msg.contains("Current: Confirmed"));
924 assert!(msg.contains("Expected one of:"));
925 } else {
926 panic!("Expected ValidationError");
927 }
928 }
929
930 #[test]
931 fn test_get_age_since_created() {
932 let now = Utc::now();
933
934 let created_time = now - Duration::hours(2);
936 let tx = TransactionRepoModel {
937 created_at: created_time.to_rfc3339(),
938 ..create_mock_transaction()
939 };
940
941 let age_result = get_age_since_created(&tx);
942 assert!(age_result.is_ok());
943 let age = age_result.unwrap();
944 assert!(age.num_minutes() >= 119 && age.num_minutes() <= 121);
946 }
947
948 #[test]
949 fn test_get_age_since_created_invalid_timestamp() {
950 let tx = TransactionRepoModel {
951 created_at: "invalid-timestamp".to_string(),
952 ..create_mock_transaction()
953 };
954
955 let result = get_age_since_created(&tx);
956 assert!(result.is_err());
957 match result.unwrap_err() {
958 TransactionError::UnexpectedError(msg) => {
959 assert!(msg.contains("Invalid created_at timestamp"));
960 }
961 _ => panic!("Expected UnexpectedError for invalid timestamp"),
962 }
963 }
964
965 #[test]
966 fn test_get_age_since_created_recent_transaction() {
967 let now = Utc::now();
968
969 let created_time = now - Duration::minutes(1);
971 let tx = TransactionRepoModel {
972 created_at: created_time.to_rfc3339(),
973 ..create_mock_transaction()
974 };
975
976 let age_result = get_age_since_created(&tx);
977 assert!(age_result.is_ok());
978 let age = age_result.unwrap();
979 assert!(age.num_seconds() >= 59 && age.num_seconds() <= 61);
981 }
982
983 #[test]
984 fn test_get_age_since_status_change_with_sent_at() {
985 let now = Utc::now();
986
987 let sent_time = now - Duration::hours(1);
989 let created_time = now - Duration::hours(3); let tx = TransactionRepoModel {
991 status: TransactionStatus::Sent,
992 created_at: created_time.to_rfc3339(),
993 sent_at: Some(sent_time.to_rfc3339()),
994 ..create_mock_transaction()
995 };
996
997 let age_result = get_age_since_status_change(&tx);
998 assert!(age_result.is_ok());
999 let age = age_result.unwrap();
1000 assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
1002 }
1003
1004 #[test]
1005 fn test_get_age_since_status_change_without_sent_at() {
1006 let now = Utc::now();
1007
1008 let created_time = now - Duration::hours(2);
1010 let tx = TransactionRepoModel {
1011 created_at: created_time.to_rfc3339(),
1012 ..create_mock_transaction()
1013 };
1014
1015 let age_result = get_age_since_status_change(&tx);
1016 assert!(age_result.is_ok());
1017 let age = age_result.unwrap();
1018 assert!(age.num_minutes() >= 119 && age.num_minutes() <= 121);
1020 }
1021
1022 #[test]
1023 fn test_get_age_since_status_change_invalid_sent_at() {
1024 let now = Utc::now();
1025 let created_time = now - Duration::hours(2);
1026
1027 let tx = TransactionRepoModel {
1028 status: TransactionStatus::Sent,
1029 created_at: created_time.to_rfc3339(),
1030 sent_at: Some("invalid-timestamp".to_string()),
1031 ..create_mock_transaction()
1032 };
1033
1034 let result = get_age_since_status_change(&tx);
1035 assert!(result.is_err());
1036 match result.unwrap_err() {
1037 TransactionError::UnexpectedError(msg) => {
1038 assert!(msg.contains("Error parsing sent_at time"));
1039 }
1040 _ => panic!("Expected UnexpectedError for invalid sent_at timestamp"),
1041 }
1042 }
1043
1044 #[test]
1045 fn test_is_too_early_to_resubmit_recent_transaction() {
1046 let now = Utc::now();
1047
1048 let created_time = now - Duration::seconds(1);
1050 let tx = TransactionRepoModel {
1051 created_at: created_time.to_rfc3339(),
1052 ..create_mock_transaction()
1053 };
1054
1055 let result = is_too_early_to_resubmit(&tx);
1056 assert!(result.is_ok());
1057 assert!(result.unwrap()); }
1059
1060 #[test]
1061 fn test_is_too_early_to_resubmit_old_transaction() {
1062 let now = Utc::now();
1063
1064 let created_time = now - Duration::seconds(EVM_MIN_AGE_FOR_RESUBMIT_SECONDS + 10);
1066 let tx = TransactionRepoModel {
1067 created_at: created_time.to_rfc3339(),
1068 ..create_mock_transaction()
1069 };
1070
1071 let result = is_too_early_to_resubmit(&tx);
1072 assert!(result.is_ok());
1073 assert!(!result.unwrap()); }
1075
1076 #[test]
1077 fn test_is_too_early_to_resubmit_boundary() {
1078 let now = Utc::now();
1079
1080 let created_time = now - Duration::seconds(EVM_MIN_AGE_FOR_RESUBMIT_SECONDS);
1082 let tx = TransactionRepoModel {
1083 created_at: created_time.to_rfc3339(),
1084 ..create_mock_transaction()
1085 };
1086
1087 let result = is_too_early_to_resubmit(&tx);
1088 assert!(result.is_ok());
1089 assert!(!result.unwrap());
1091 }
1092
1093 #[test]
1094 fn test_is_too_early_to_resubmit_invalid_timestamp() {
1095 let tx = TransactionRepoModel {
1096 created_at: "invalid-timestamp".to_string(),
1097 ..create_mock_transaction()
1098 };
1099
1100 let result = is_too_early_to_resubmit(&tx);
1101 assert!(result.is_err());
1102 match result.unwrap_err() {
1103 TransactionError::UnexpectedError(msg) => {
1104 assert!(msg.contains("Invalid created_at timestamp"));
1105 }
1106 _ => panic!("Expected UnexpectedError for invalid timestamp"),
1107 }
1108 }
1109}