1use crate::constants::{
2 ARBITRUM_GAS_LIMIT, DEFAULT_GAS_LIMIT, DEFAULT_TX_VALID_TIMESPAN, MAXIMUM_NOOP_RETRY_ATTEMPTS,
3 MAXIMUM_TX_ATTEMPTS,
4};
5use crate::models::EvmNetwork;
6use crate::models::{
7 EvmTransactionData, TransactionError, TransactionRepoModel, TransactionStatus, U256,
8};
9use crate::services::EvmProviderTrait;
10use chrono::{DateTime, Duration, Utc};
11use eyre::Result;
12
13pub async fn make_noop<P: EvmProviderTrait>(
17 evm_data: &mut EvmTransactionData,
18 network: &EvmNetwork,
19 provider: Option<&P>,
20) -> Result<(), TransactionError> {
21 evm_data.value = U256::from(0u64);
23 evm_data.data = Some("0x".to_string());
24 evm_data.to = Some(evm_data.from.clone());
25
26 if network.is_arbitrum() {
28 if let Some(provider) = provider {
30 match provider.estimate_gas(evm_data).await {
31 Ok(estimated_gas) => {
32 evm_data.gas_limit = Some(estimated_gas.max(DEFAULT_GAS_LIMIT));
34 }
35 Err(e) => {
36 tracing::warn!(
38 "Failed to estimate gas for Arbitrum noop transaction: {:?}",
39 e
40 );
41 evm_data.gas_limit = Some(ARBITRUM_GAS_LIMIT);
42 }
43 }
44 } else {
45 evm_data.gas_limit = Some(ARBITRUM_GAS_LIMIT);
47 }
48 } else {
49 evm_data.gas_limit = Some(DEFAULT_GAS_LIMIT);
51 }
52
53 Ok(())
54}
55
56pub fn is_noop(evm_data: &EvmTransactionData) -> bool {
58 evm_data.value == U256::from(0u64)
59 && evm_data.data.as_ref().is_some_and(|data| data == "0x")
60 && evm_data.to.as_ref() == Some(&evm_data.from)
61 && evm_data.speed.is_some()
62}
63
64pub fn too_many_attempts(tx: &TransactionRepoModel) -> bool {
66 tx.hashes.len() > MAXIMUM_TX_ATTEMPTS
67}
68
69pub fn too_many_noop_attempts(tx: &TransactionRepoModel) -> bool {
71 tx.noop_count.unwrap_or(0) > MAXIMUM_NOOP_RETRY_ATTEMPTS
72}
73
74pub fn is_pending_transaction(tx_status: &TransactionStatus) -> bool {
75 tx_status == &TransactionStatus::Pending
76 || tx_status == &TransactionStatus::Sent
77 || tx_status == &TransactionStatus::Submitted
78}
79
80pub fn has_enough_confirmations(
82 tx_block_number: u64,
83 current_block_number: u64,
84 required_confirmations: u64,
85) -> bool {
86 current_block_number >= tx_block_number + required_confirmations
87}
88
89pub fn is_transaction_valid(created_at: &str, valid_until: &Option<String>) -> bool {
91 if let Some(valid_until_str) = valid_until {
92 match DateTime::parse_from_rfc3339(valid_until_str) {
93 Ok(valid_until_time) => return Utc::now() < valid_until_time,
94 Err(e) => {
95 tracing::warn!(error = %e, "failed to parse valid_until timestamp");
96 return false;
97 }
98 }
99 }
100 match DateTime::parse_from_rfc3339(created_at) {
101 Ok(created_time) => {
102 let default_valid_until =
103 created_time + Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN);
104 Utc::now() < default_valid_until
105 }
106 Err(e) => {
107 tracing::warn!(error = %e, "failed to parse created_at timestamp");
108 false
109 }
110 }
111}
112
113pub fn get_age_of_sent_at(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
115 let now = Utc::now();
116 let sent_at_str = tx.sent_at.as_ref().ok_or_else(|| {
117 TransactionError::UnexpectedError("Transaction sent_at time is missing".to_string())
118 })?;
119 let sent_time = DateTime::parse_from_rfc3339(sent_at_str)
120 .map_err(|_| TransactionError::UnexpectedError("Error parsing sent_at time".to_string()))?
121 .with_timezone(&Utc);
122 Ok(now.signed_duration_since(sent_time))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::constants::{ARBITRUM_BASED_TAG, ROLLUP_TAG};
129 use crate::models::{evm::Speed, NetworkTransactionData};
130 use crate::services::{MockEvmProviderTrait, ProviderError};
131
132 fn create_standard_network() -> EvmNetwork {
133 EvmNetwork {
134 network: "ethereum".to_string(),
135 rpc_urls: vec!["https://mainnet.infura.io".to_string()],
136 explorer_urls: None,
137 average_blocktime_ms: 12000,
138 is_testnet: false,
139 tags: vec!["mainnet".to_string()],
140 chain_id: 1,
141 required_confirmations: 12,
142 features: vec!["eip1559".to_string()],
143 symbol: "ETH".to_string(),
144 gas_price_cache: None,
145 }
146 }
147
148 fn create_arbitrum_network() -> EvmNetwork {
149 EvmNetwork {
150 network: "arbitrum".to_string(),
151 rpc_urls: vec!["https://arb1.arbitrum.io/rpc".to_string()],
152 explorer_urls: None,
153 average_blocktime_ms: 1000,
154 is_testnet: false,
155 tags: vec![ROLLUP_TAG.to_string(), ARBITRUM_BASED_TAG.to_string()],
156 chain_id: 42161,
157 required_confirmations: 1,
158 features: vec!["eip1559".to_string()],
159 symbol: "ETH".to_string(),
160 gas_price_cache: None,
161 }
162 }
163
164 fn create_arbitrum_nova_network() -> EvmNetwork {
165 EvmNetwork {
166 network: "arbitrum-nova".to_string(),
167 rpc_urls: vec!["https://nova.arbitrum.io/rpc".to_string()],
168 explorer_urls: None,
169 average_blocktime_ms: 1000,
170 is_testnet: false,
171 tags: vec![ROLLUP_TAG.to_string(), ARBITRUM_BASED_TAG.to_string()],
172 chain_id: 42170,
173 required_confirmations: 1,
174 features: vec!["eip1559".to_string()],
175 symbol: "ETH".to_string(),
176 gas_price_cache: None,
177 }
178 }
179
180 #[tokio::test]
181 async fn test_make_noop_standard_network() {
182 let mut evm_data = EvmTransactionData {
183 from: "0x1234567890123456789012345678901234567890".to_string(),
184 to: Some("0xoriginal_destination".to_string()),
185 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
187 gas_limit: Some(50000),
188 gas_price: Some(10_000_000_000),
189 max_fee_per_gas: None,
190 max_priority_fee_per_gas: None,
191 nonce: Some(42),
192 signature: None,
193 hash: Some("0xoriginal_hash".to_string()),
194 speed: Some(Speed::Fast),
195 chain_id: 1,
196 raw: Some(vec![1, 2, 3]),
197 };
198
199 let network = create_standard_network();
200 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
201 assert!(result.is_ok());
202
203 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)); }
210
211 #[tokio::test]
212 async fn test_make_noop_arbitrum_network() {
213 let mut evm_data = EvmTransactionData {
214 from: "0x1234567890123456789012345678901234567890".to_string(),
215 to: Some("0xoriginal_destination".to_string()),
216 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
218 gas_limit: Some(50000),
219 gas_price: Some(10_000_000_000),
220 max_fee_per_gas: None,
221 max_priority_fee_per_gas: None,
222 nonce: Some(42),
223 signature: None,
224 hash: Some("0xoriginal_hash".to_string()),
225 speed: Some(Speed::Fast),
226 chain_id: 42161, raw: Some(vec![1, 2, 3]),
228 };
229
230 let network = create_arbitrum_network();
231 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
232 assert!(result.is_ok());
233
234 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); }
242
243 #[tokio::test]
244 async fn test_make_noop_arbitrum_nova() {
245 let mut evm_data = EvmTransactionData {
246 from: "0x1234567890123456789012345678901234567890".to_string(),
247 to: Some("0xoriginal_destination".to_string()),
248 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
250 gas_limit: Some(30000),
251 gas_price: Some(10_000_000_000),
252 max_fee_per_gas: None,
253 max_priority_fee_per_gas: None,
254 nonce: Some(42),
255 signature: None,
256 hash: Some("0xoriginal_hash".to_string()),
257 speed: Some(Speed::Fast),
258 chain_id: 42170, raw: Some(vec![1, 2, 3]),
260 };
261
262 let network = create_arbitrum_nova_network();
263 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
264 assert!(result.is_ok());
265
266 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); }
274
275 #[tokio::test]
276 async fn test_make_noop_arbitrum_with_provider() {
277 let mut mock_provider = MockEvmProviderTrait::new();
278
279 mock_provider
281 .expect_estimate_gas()
282 .times(1)
283 .returning(|_| Box::pin(async move { Ok(35_000) }));
284
285 let mut evm_data = EvmTransactionData {
286 from: "0x1234567890123456789012345678901234567890".to_string(),
287 to: Some("0xoriginal_destination".to_string()),
288 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
290 gas_limit: Some(30000),
291 gas_price: Some(10_000_000_000),
292 max_fee_per_gas: None,
293 max_priority_fee_per_gas: None,
294 nonce: Some(42),
295 signature: None,
296 hash: Some("0xoriginal_hash".to_string()),
297 speed: Some(Speed::Fast),
298 chain_id: 42161, raw: Some(vec![1, 2, 3]),
300 };
301
302 let network = create_arbitrum_network();
303 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
304 assert!(result.is_ok());
305
306 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); }
314
315 #[tokio::test]
316 async fn test_make_noop_arbitrum_provider_estimation_fails() {
317 let mut mock_provider = MockEvmProviderTrait::new();
318
319 mock_provider.expect_estimate_gas().times(1).returning(|_| {
321 Box::pin(async move { Err(ProviderError::Other("Network error".to_string())) })
322 });
323
324 let mut evm_data = EvmTransactionData {
325 from: "0x1234567890123456789012345678901234567890".to_string(),
326 to: Some("0xoriginal_destination".to_string()),
327 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
329 gas_limit: Some(30000),
330 gas_price: Some(10_000_000_000),
331 max_fee_per_gas: None,
332 max_priority_fee_per_gas: None,
333 nonce: Some(42),
334 signature: None,
335 hash: Some("0xoriginal_hash".to_string()),
336 speed: Some(Speed::Fast),
337 chain_id: 42161, raw: Some(vec![1, 2, 3]),
339 };
340
341 let network = create_arbitrum_network();
342 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
343 assert!(result.is_ok());
344
345 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); }
353
354 #[test]
355 fn test_is_noop() {
356 let noop_tx = EvmTransactionData {
358 from: "0x1234567890123456789012345678901234567890".to_string(),
359 to: Some("0x1234567890123456789012345678901234567890".to_string()), value: U256::from(0u64),
361 data: Some("0x".to_string()),
362 gas_limit: Some(21000),
363 gas_price: Some(10_000_000_000),
364 max_fee_per_gas: None,
365 max_priority_fee_per_gas: None,
366 nonce: Some(42),
367 signature: None,
368 hash: None,
369 speed: Some(Speed::Fast),
370 chain_id: 1,
371 raw: None,
372 };
373 assert!(is_noop(&noop_tx));
374
375 let mut non_noop = noop_tx.clone();
377 non_noop.value = U256::from(1000000000000000000u64); assert!(!is_noop(&non_noop));
379
380 let mut non_noop = noop_tx.clone();
381 non_noop.data = Some("0x123456".to_string());
382 assert!(!is_noop(&non_noop));
383
384 let mut non_noop = noop_tx.clone();
385 non_noop.to = Some("0x9876543210987654321098765432109876543210".to_string());
386 assert!(!is_noop(&non_noop));
387
388 let mut non_noop = noop_tx;
389 non_noop.speed = None;
390 assert!(!is_noop(&non_noop));
391 }
392
393 #[test]
394 fn test_too_many_attempts() {
395 let mut tx = TransactionRepoModel {
396 id: "test-tx".to_string(),
397 relayer_id: "test-relayer".to_string(),
398 status: TransactionStatus::Pending,
399 status_reason: None,
400 created_at: "2024-01-01T00:00:00Z".to_string(),
401 sent_at: None,
402 confirmed_at: None,
403 valid_until: None,
404 network_type: crate::models::NetworkType::Evm,
405 network_data: NetworkTransactionData::Evm(EvmTransactionData {
406 from: "0x1234".to_string(),
407 to: Some("0x5678".to_string()),
408 value: U256::from(0u64),
409 data: Some("0x".to_string()),
410 gas_limit: Some(21000),
411 gas_price: Some(10_000_000_000),
412 max_fee_per_gas: None,
413 max_priority_fee_per_gas: None,
414 nonce: Some(42),
415 signature: None,
416 hash: None,
417 speed: Some(Speed::Fast),
418 chain_id: 1,
419 raw: None,
420 }),
421 priced_at: None,
422 hashes: vec![], noop_count: None,
424 is_canceled: Some(false),
425 delete_at: None,
426 };
427
428 assert!(!too_many_attempts(&tx));
430
431 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS];
433 assert!(!too_many_attempts(&tx));
434
435 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS + 1];
437 assert!(too_many_attempts(&tx));
438 }
439
440 #[test]
441 fn test_too_many_noop_attempts() {
442 let mut tx = TransactionRepoModel {
443 id: "test-tx".to_string(),
444 relayer_id: "test-relayer".to_string(),
445 status: TransactionStatus::Pending,
446 status_reason: None,
447 created_at: "2024-01-01T00:00:00Z".to_string(),
448 sent_at: None,
449 confirmed_at: None,
450 valid_until: None,
451 network_type: crate::models::NetworkType::Evm,
452 network_data: NetworkTransactionData::Evm(EvmTransactionData {
453 from: "0x1234".to_string(),
454 to: Some("0x5678".to_string()),
455 value: U256::from(0u64),
456 data: Some("0x".to_string()),
457 gas_limit: Some(21000),
458 gas_price: Some(10_000_000_000),
459 max_fee_per_gas: None,
460 max_priority_fee_per_gas: None,
461 nonce: Some(42),
462 signature: None,
463 hash: None,
464 speed: Some(Speed::Fast),
465 chain_id: 1,
466 raw: None,
467 }),
468 priced_at: None,
469 hashes: vec![],
470 noop_count: None,
471 is_canceled: Some(false),
472 delete_at: None,
473 };
474
475 assert!(!too_many_noop_attempts(&tx));
477
478 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS);
480 assert!(!too_many_noop_attempts(&tx));
481
482 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS + 1);
484 assert!(too_many_noop_attempts(&tx));
485 }
486
487 #[test]
488 fn test_has_enough_confirmations() {
489 let tx_block_number = 100;
491 let current_block_number = 110; let required_confirmations = 12;
493 assert!(!has_enough_confirmations(
494 tx_block_number,
495 current_block_number,
496 required_confirmations
497 ));
498
499 let current_block_number = 112; assert!(has_enough_confirmations(
502 tx_block_number,
503 current_block_number,
504 required_confirmations
505 ));
506
507 let current_block_number = 120; assert!(has_enough_confirmations(
510 tx_block_number,
511 current_block_number,
512 required_confirmations
513 ));
514 }
515
516 #[test]
517 fn test_is_transaction_valid_with_future_timestamp() {
518 let now = Utc::now();
519 let valid_until = Some((now + Duration::hours(1)).to_rfc3339());
520 let created_at = now.to_rfc3339();
521
522 assert!(is_transaction_valid(&created_at, &valid_until));
523 }
524
525 #[test]
526 fn test_is_transaction_valid_with_past_timestamp() {
527 let now = Utc::now();
528 let valid_until = Some((now - Duration::hours(1)).to_rfc3339());
529 let created_at = now.to_rfc3339();
530
531 assert!(!is_transaction_valid(&created_at, &valid_until));
532 }
533
534 #[test]
535 fn test_is_transaction_valid_with_valid_until() {
536 let created_at = Utc::now().to_rfc3339();
538 let valid_until = Some((Utc::now() + Duration::hours(1)).to_rfc3339());
539 assert!(is_transaction_valid(&created_at, &valid_until));
540
541 let valid_until = Some((Utc::now() - Duration::hours(1)).to_rfc3339());
543 assert!(!is_transaction_valid(&created_at, &valid_until));
544
545 let valid_until = Some(Utc::now().to_rfc3339());
547 assert!(!is_transaction_valid(&created_at, &valid_until));
548
549 let valid_until = Some((Utc::now() + Duration::days(365)).to_rfc3339());
551 assert!(is_transaction_valid(&created_at, &valid_until));
552
553 let valid_until = Some("invalid-date-format".to_string());
555 assert!(!is_transaction_valid(&created_at, &valid_until));
556
557 let valid_until = Some("".to_string());
559 assert!(!is_transaction_valid(&created_at, &valid_until));
560 }
561
562 #[test]
563 fn test_is_transaction_valid_without_valid_until() {
564 let created_at = Utc::now().to_rfc3339();
566 let valid_until = None;
567 assert!(is_transaction_valid(&created_at, &valid_until));
568
569 let old_created_at =
571 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN + 1000)).to_rfc3339();
572 assert!(!is_transaction_valid(&old_created_at, &valid_until));
573
574 let boundary_created_at =
576 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN)).to_rfc3339();
577 assert!(!is_transaction_valid(&boundary_created_at, &valid_until));
578
579 let within_boundary_created_at =
581 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN - 1000)).to_rfc3339();
582 assert!(is_transaction_valid(
583 &within_boundary_created_at,
584 &valid_until
585 ));
586
587 let invalid_created_at = "invalid-date-format";
589 assert!(!is_transaction_valid(invalid_created_at, &valid_until));
590
591 assert!(!is_transaction_valid("", &valid_until));
593 }
594
595 #[test]
596 fn test_is_pending_transaction() {
597 assert!(is_pending_transaction(&TransactionStatus::Pending));
599
600 assert!(is_pending_transaction(&TransactionStatus::Sent));
602
603 assert!(is_pending_transaction(&TransactionStatus::Submitted));
605
606 assert!(!is_pending_transaction(&TransactionStatus::Confirmed));
608 assert!(!is_pending_transaction(&TransactionStatus::Failed));
609 assert!(!is_pending_transaction(&TransactionStatus::Canceled));
610 assert!(!is_pending_transaction(&TransactionStatus::Mined));
611 assert!(!is_pending_transaction(&TransactionStatus::Expired));
612 }
613
614 #[test]
615 fn test_get_age_of_sent_at() {
616 let now = Utc::now();
617
618 let sent_at_time = now - Duration::hours(1);
620 let tx = TransactionRepoModel {
621 id: "test-tx".to_string(),
622 relayer_id: "test-relayer".to_string(),
623 status: TransactionStatus::Sent,
624 status_reason: None,
625 created_at: "2024-01-01T00:00:00Z".to_string(),
626 sent_at: Some(sent_at_time.to_rfc3339()),
627 confirmed_at: None,
628 valid_until: None,
629 network_type: crate::models::NetworkType::Evm,
630 network_data: NetworkTransactionData::Evm(EvmTransactionData {
631 from: "0x1234".to_string(),
632 to: Some("0x5678".to_string()),
633 value: U256::from(0u64),
634 data: Some("0x".to_string()),
635 gas_limit: Some(21000),
636 gas_price: Some(10_000_000_000),
637 max_fee_per_gas: None,
638 max_priority_fee_per_gas: None,
639 nonce: Some(42),
640 signature: None,
641 hash: None,
642 speed: Some(Speed::Fast),
643 chain_id: 1,
644 raw: None,
645 }),
646 priced_at: None,
647 hashes: vec![],
648 noop_count: None,
649 is_canceled: Some(false),
650 delete_at: None,
651 };
652
653 let age_result = get_age_of_sent_at(&tx);
654 assert!(age_result.is_ok());
655 let age = age_result.unwrap();
656 assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
658 }
659
660 #[test]
661 fn test_get_age_of_sent_at_missing_sent_at() {
662 let tx = TransactionRepoModel {
663 id: "test-tx".to_string(),
664 relayer_id: "test-relayer".to_string(),
665 status: TransactionStatus::Pending,
666 status_reason: None,
667 created_at: "2024-01-01T00:00:00Z".to_string(),
668 sent_at: None, confirmed_at: None,
670 valid_until: None,
671 network_type: crate::models::NetworkType::Evm,
672 network_data: NetworkTransactionData::Evm(EvmTransactionData {
673 from: "0x1234".to_string(),
674 to: Some("0x5678".to_string()),
675 value: U256::from(0u64),
676 data: Some("0x".to_string()),
677 gas_limit: Some(21000),
678 gas_price: Some(10_000_000_000),
679 max_fee_per_gas: None,
680 max_priority_fee_per_gas: None,
681 nonce: Some(42),
682 signature: None,
683 hash: None,
684 speed: Some(Speed::Fast),
685 chain_id: 1,
686 raw: None,
687 }),
688 priced_at: None,
689 hashes: vec![],
690 noop_count: None,
691 is_canceled: Some(false),
692 delete_at: None,
693 };
694
695 let result = get_age_of_sent_at(&tx);
696 assert!(result.is_err());
697 match result.unwrap_err() {
698 TransactionError::UnexpectedError(msg) => {
699 assert!(msg.contains("sent_at time is missing"));
700 }
701 _ => panic!("Expected UnexpectedError for missing sent_at"),
702 }
703 }
704
705 #[test]
706 fn test_get_age_of_sent_at_invalid_timestamp() {
707 let tx = TransactionRepoModel {
708 id: "test-tx".to_string(),
709 relayer_id: "test-relayer".to_string(),
710 status: TransactionStatus::Sent,
711 status_reason: None,
712 created_at: "2024-01-01T00:00:00Z".to_string(),
713 sent_at: Some("invalid-timestamp".to_string()), confirmed_at: None,
715 valid_until: None,
716 network_type: crate::models::NetworkType::Evm,
717 network_data: NetworkTransactionData::Evm(EvmTransactionData {
718 from: "0x1234".to_string(),
719 to: Some("0x5678".to_string()),
720 value: U256::from(0u64),
721 data: Some("0x".to_string()),
722 gas_limit: Some(21000),
723 gas_price: Some(10_000_000_000),
724 max_fee_per_gas: None,
725 max_priority_fee_per_gas: None,
726 nonce: Some(42),
727 signature: None,
728 hash: None,
729 speed: Some(Speed::Fast),
730 chain_id: 1,
731 raw: None,
732 }),
733 priced_at: None,
734 hashes: vec![],
735 noop_count: None,
736 is_canceled: Some(false),
737 delete_at: None,
738 };
739
740 let result = get_age_of_sent_at(&tx);
741 assert!(result.is_err());
742 match result.unwrap_err() {
743 TransactionError::UnexpectedError(msg) => {
744 assert!(msg.contains("Error parsing sent_at time"));
745 }
746 _ => panic!("Expected UnexpectedError for invalid timestamp"),
747 }
748 }
749}