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 log::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 log::warn!("Failed to parse valid_until timestamp: {}", e);
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 log::warn!("Failed to parse created_at timestamp: {}", e);
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::models::{evm::Speed, NetworkTransactionData};
129 use crate::services::{MockEvmProviderTrait, ProviderError};
130
131 fn create_standard_network() -> EvmNetwork {
132 EvmNetwork {
133 network: "ethereum".to_string(),
134 rpc_urls: vec!["https://mainnet.infura.io".to_string()],
135 explorer_urls: None,
136 average_blocktime_ms: 12000,
137 is_testnet: false,
138 tags: vec!["mainnet".to_string()],
139 chain_id: 1,
140 required_confirmations: 12,
141 features: vec!["eip1559".to_string()],
142 symbol: "ETH".to_string(),
143 }
144 }
145
146 fn create_arbitrum_network() -> EvmNetwork {
147 EvmNetwork {
148 network: "arbitrum".to_string(),
149 rpc_urls: vec!["https://arb1.arbitrum.io/rpc".to_string()],
150 explorer_urls: None,
151 average_blocktime_ms: 1000,
152 is_testnet: false,
153 tags: vec!["rollup".to_string(), "arbitrum-based".to_string()],
154 chain_id: 42161,
155 required_confirmations: 1,
156 features: vec!["eip1559".to_string()],
157 symbol: "ETH".to_string(),
158 }
159 }
160
161 fn create_arbitrum_nova_network() -> EvmNetwork {
162 EvmNetwork {
163 network: "arbitrum-nova".to_string(),
164 rpc_urls: vec!["https://nova.arbitrum.io/rpc".to_string()],
165 explorer_urls: None,
166 average_blocktime_ms: 1000,
167 is_testnet: false,
168 tags: vec!["rollup".to_string(), "arbitrum-based".to_string()],
169 chain_id: 42170,
170 required_confirmations: 1,
171 features: vec!["eip1559".to_string()],
172 symbol: "ETH".to_string(),
173 }
174 }
175
176 #[tokio::test]
177 async fn test_make_noop_standard_network() {
178 let mut evm_data = EvmTransactionData {
179 from: "0x1234567890123456789012345678901234567890".to_string(),
180 to: Some("0xoriginal_destination".to_string()),
181 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
183 gas_limit: Some(50000),
184 gas_price: Some(10_000_000_000),
185 max_fee_per_gas: None,
186 max_priority_fee_per_gas: None,
187 nonce: Some(42),
188 signature: None,
189 hash: Some("0xoriginal_hash".to_string()),
190 speed: Some(Speed::Fast),
191 chain_id: 1,
192 raw: Some(vec![1, 2, 3]),
193 };
194
195 let network = create_standard_network();
196 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
197 assert!(result.is_ok());
198
199 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)); }
206
207 #[tokio::test]
208 async fn test_make_noop_arbitrum_network() {
209 let mut evm_data = EvmTransactionData {
210 from: "0x1234567890123456789012345678901234567890".to_string(),
211 to: Some("0xoriginal_destination".to_string()),
212 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
214 gas_limit: Some(50000),
215 gas_price: Some(10_000_000_000),
216 max_fee_per_gas: None,
217 max_priority_fee_per_gas: None,
218 nonce: Some(42),
219 signature: None,
220 hash: Some("0xoriginal_hash".to_string()),
221 speed: Some(Speed::Fast),
222 chain_id: 42161, raw: Some(vec![1, 2, 3]),
224 };
225
226 let network = create_arbitrum_network();
227 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
228 assert!(result.is_ok());
229
230 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); }
238
239 #[tokio::test]
240 async fn test_make_noop_arbitrum_nova() {
241 let mut evm_data = EvmTransactionData {
242 from: "0x1234567890123456789012345678901234567890".to_string(),
243 to: Some("0xoriginal_destination".to_string()),
244 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
246 gas_limit: Some(30000),
247 gas_price: Some(10_000_000_000),
248 max_fee_per_gas: None,
249 max_priority_fee_per_gas: None,
250 nonce: Some(42),
251 signature: None,
252 hash: Some("0xoriginal_hash".to_string()),
253 speed: Some(Speed::Fast),
254 chain_id: 42170, raw: Some(vec![1, 2, 3]),
256 };
257
258 let network = create_arbitrum_nova_network();
259 let result = make_noop(&mut evm_data, &network, None::<&MockEvmProviderTrait>).await;
260 assert!(result.is_ok());
261
262 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); }
270
271 #[tokio::test]
272 async fn test_make_noop_arbitrum_with_provider() {
273 let mut mock_provider = MockEvmProviderTrait::new();
274
275 mock_provider
277 .expect_estimate_gas()
278 .times(1)
279 .returning(|_| Box::pin(async move { Ok(35_000) }));
280
281 let mut evm_data = EvmTransactionData {
282 from: "0x1234567890123456789012345678901234567890".to_string(),
283 to: Some("0xoriginal_destination".to_string()),
284 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
286 gas_limit: Some(30000),
287 gas_price: Some(10_000_000_000),
288 max_fee_per_gas: None,
289 max_priority_fee_per_gas: None,
290 nonce: Some(42),
291 signature: None,
292 hash: Some("0xoriginal_hash".to_string()),
293 speed: Some(Speed::Fast),
294 chain_id: 42161, raw: Some(vec![1, 2, 3]),
296 };
297
298 let network = create_arbitrum_network();
299 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
300 assert!(result.is_ok());
301
302 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); }
310
311 #[tokio::test]
312 async fn test_make_noop_arbitrum_provider_estimation_fails() {
313 let mut mock_provider = MockEvmProviderTrait::new();
314
315 mock_provider.expect_estimate_gas().times(1).returning(|_| {
317 Box::pin(async move { Err(ProviderError::Other("Network error".to_string())) })
318 });
319
320 let mut evm_data = EvmTransactionData {
321 from: "0x1234567890123456789012345678901234567890".to_string(),
322 to: Some("0xoriginal_destination".to_string()),
323 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
325 gas_limit: Some(30000),
326 gas_price: Some(10_000_000_000),
327 max_fee_per_gas: None,
328 max_priority_fee_per_gas: None,
329 nonce: Some(42),
330 signature: None,
331 hash: Some("0xoriginal_hash".to_string()),
332 speed: Some(Speed::Fast),
333 chain_id: 42161, raw: Some(vec![1, 2, 3]),
335 };
336
337 let network = create_arbitrum_network();
338 let result = make_noop(&mut evm_data, &network, Some(&mock_provider)).await;
339 assert!(result.is_ok());
340
341 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); }
349
350 #[test]
351 fn test_is_noop() {
352 let noop_tx = EvmTransactionData {
354 from: "0x1234567890123456789012345678901234567890".to_string(),
355 to: Some("0x1234567890123456789012345678901234567890".to_string()), value: U256::from(0u64),
357 data: Some("0x".to_string()),
358 gas_limit: Some(21000),
359 gas_price: Some(10_000_000_000),
360 max_fee_per_gas: None,
361 max_priority_fee_per_gas: None,
362 nonce: Some(42),
363 signature: None,
364 hash: None,
365 speed: Some(Speed::Fast),
366 chain_id: 1,
367 raw: None,
368 };
369 assert!(is_noop(&noop_tx));
370
371 let mut non_noop = noop_tx.clone();
373 non_noop.value = U256::from(1000000000000000000u64); assert!(!is_noop(&non_noop));
375
376 let mut non_noop = noop_tx.clone();
377 non_noop.data = Some("0x123456".to_string());
378 assert!(!is_noop(&non_noop));
379
380 let mut non_noop = noop_tx.clone();
381 non_noop.to = Some("0x9876543210987654321098765432109876543210".to_string());
382 assert!(!is_noop(&non_noop));
383
384 let mut non_noop = noop_tx;
385 non_noop.speed = None;
386 assert!(!is_noop(&non_noop));
387 }
388
389 #[test]
390 fn test_too_many_attempts() {
391 let mut tx = TransactionRepoModel {
392 id: "test-tx".to_string(),
393 relayer_id: "test-relayer".to_string(),
394 status: TransactionStatus::Pending,
395 status_reason: None,
396 created_at: "2024-01-01T00:00:00Z".to_string(),
397 sent_at: None,
398 confirmed_at: None,
399 valid_until: None,
400 network_type: crate::models::NetworkType::Evm,
401 network_data: NetworkTransactionData::Evm(EvmTransactionData {
402 from: "0x1234".to_string(),
403 to: Some("0x5678".to_string()),
404 value: U256::from(0u64),
405 data: Some("0x".to_string()),
406 gas_limit: Some(21000),
407 gas_price: Some(10_000_000_000),
408 max_fee_per_gas: None,
409 max_priority_fee_per_gas: None,
410 nonce: Some(42),
411 signature: None,
412 hash: None,
413 speed: Some(Speed::Fast),
414 chain_id: 1,
415 raw: None,
416 }),
417 priced_at: None,
418 hashes: vec![], noop_count: None,
420 is_canceled: Some(false),
421 delete_at: None,
422 };
423
424 assert!(!too_many_attempts(&tx));
426
427 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS];
429 assert!(!too_many_attempts(&tx));
430
431 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS + 1];
433 assert!(too_many_attempts(&tx));
434 }
435
436 #[test]
437 fn test_too_many_noop_attempts() {
438 let mut tx = TransactionRepoModel {
439 id: "test-tx".to_string(),
440 relayer_id: "test-relayer".to_string(),
441 status: TransactionStatus::Pending,
442 status_reason: None,
443 created_at: "2024-01-01T00:00:00Z".to_string(),
444 sent_at: None,
445 confirmed_at: None,
446 valid_until: None,
447 network_type: crate::models::NetworkType::Evm,
448 network_data: NetworkTransactionData::Evm(EvmTransactionData {
449 from: "0x1234".to_string(),
450 to: Some("0x5678".to_string()),
451 value: U256::from(0u64),
452 data: Some("0x".to_string()),
453 gas_limit: Some(21000),
454 gas_price: Some(10_000_000_000),
455 max_fee_per_gas: None,
456 max_priority_fee_per_gas: None,
457 nonce: Some(42),
458 signature: None,
459 hash: None,
460 speed: Some(Speed::Fast),
461 chain_id: 1,
462 raw: None,
463 }),
464 priced_at: None,
465 hashes: vec![],
466 noop_count: None,
467 is_canceled: Some(false),
468 delete_at: None,
469 };
470
471 assert!(!too_many_noop_attempts(&tx));
473
474 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS);
476 assert!(!too_many_noop_attempts(&tx));
477
478 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS + 1);
480 assert!(too_many_noop_attempts(&tx));
481 }
482
483 #[test]
484 fn test_has_enough_confirmations() {
485 let tx_block_number = 100;
487 let current_block_number = 110; let required_confirmations = 12;
489 assert!(!has_enough_confirmations(
490 tx_block_number,
491 current_block_number,
492 required_confirmations
493 ));
494
495 let current_block_number = 112; assert!(has_enough_confirmations(
498 tx_block_number,
499 current_block_number,
500 required_confirmations
501 ));
502
503 let current_block_number = 120; assert!(has_enough_confirmations(
506 tx_block_number,
507 current_block_number,
508 required_confirmations
509 ));
510 }
511
512 #[test]
513 fn test_is_transaction_valid_with_future_timestamp() {
514 let now = Utc::now();
515 let valid_until = Some((now + Duration::hours(1)).to_rfc3339());
516 let created_at = now.to_rfc3339();
517
518 assert!(is_transaction_valid(&created_at, &valid_until));
519 }
520
521 #[test]
522 fn test_is_transaction_valid_with_past_timestamp() {
523 let now = Utc::now();
524 let valid_until = Some((now - Duration::hours(1)).to_rfc3339());
525 let created_at = now.to_rfc3339();
526
527 assert!(!is_transaction_valid(&created_at, &valid_until));
528 }
529
530 #[test]
531 fn test_is_transaction_valid_with_valid_until() {
532 let created_at = Utc::now().to_rfc3339();
534 let valid_until = Some((Utc::now() + Duration::hours(1)).to_rfc3339());
535 assert!(is_transaction_valid(&created_at, &valid_until));
536
537 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().to_rfc3339());
543 assert!(!is_transaction_valid(&created_at, &valid_until));
544
545 let valid_until = Some((Utc::now() + Duration::days(365)).to_rfc3339());
547 assert!(is_transaction_valid(&created_at, &valid_until));
548
549 let valid_until = Some("invalid-date-format".to_string());
551 assert!(!is_transaction_valid(&created_at, &valid_until));
552
553 let valid_until = Some("".to_string());
555 assert!(!is_transaction_valid(&created_at, &valid_until));
556 }
557
558 #[test]
559 fn test_is_transaction_valid_without_valid_until() {
560 let created_at = Utc::now().to_rfc3339();
562 let valid_until = None;
563 assert!(is_transaction_valid(&created_at, &valid_until));
564
565 let old_created_at =
567 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN + 1000)).to_rfc3339();
568 assert!(!is_transaction_valid(&old_created_at, &valid_until));
569
570 let boundary_created_at =
572 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN)).to_rfc3339();
573 assert!(!is_transaction_valid(&boundary_created_at, &valid_until));
574
575 let within_boundary_created_at =
577 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN - 1000)).to_rfc3339();
578 assert!(is_transaction_valid(
579 &within_boundary_created_at,
580 &valid_until
581 ));
582
583 let invalid_created_at = "invalid-date-format";
585 assert!(!is_transaction_valid(invalid_created_at, &valid_until));
586
587 assert!(!is_transaction_valid("", &valid_until));
589 }
590
591 #[test]
592 fn test_is_pending_transaction() {
593 assert!(is_pending_transaction(&TransactionStatus::Pending));
595
596 assert!(is_pending_transaction(&TransactionStatus::Sent));
598
599 assert!(is_pending_transaction(&TransactionStatus::Submitted));
601
602 assert!(!is_pending_transaction(&TransactionStatus::Confirmed));
604 assert!(!is_pending_transaction(&TransactionStatus::Failed));
605 assert!(!is_pending_transaction(&TransactionStatus::Canceled));
606 assert!(!is_pending_transaction(&TransactionStatus::Mined));
607 assert!(!is_pending_transaction(&TransactionStatus::Expired));
608 }
609
610 #[test]
611 fn test_get_age_of_sent_at() {
612 let now = Utc::now();
613
614 let sent_at_time = now - Duration::hours(1);
616 let tx = TransactionRepoModel {
617 id: "test-tx".to_string(),
618 relayer_id: "test-relayer".to_string(),
619 status: TransactionStatus::Sent,
620 status_reason: None,
621 created_at: "2024-01-01T00:00:00Z".to_string(),
622 sent_at: Some(sent_at_time.to_rfc3339()),
623 confirmed_at: None,
624 valid_until: None,
625 network_type: crate::models::NetworkType::Evm,
626 network_data: NetworkTransactionData::Evm(EvmTransactionData {
627 from: "0x1234".to_string(),
628 to: Some("0x5678".to_string()),
629 value: U256::from(0u64),
630 data: Some("0x".to_string()),
631 gas_limit: Some(21000),
632 gas_price: Some(10_000_000_000),
633 max_fee_per_gas: None,
634 max_priority_fee_per_gas: None,
635 nonce: Some(42),
636 signature: None,
637 hash: None,
638 speed: Some(Speed::Fast),
639 chain_id: 1,
640 raw: None,
641 }),
642 priced_at: None,
643 hashes: vec![],
644 noop_count: None,
645 is_canceled: Some(false),
646 delete_at: None,
647 };
648
649 let age_result = get_age_of_sent_at(&tx);
650 assert!(age_result.is_ok());
651 let age = age_result.unwrap();
652 assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
654 }
655
656 #[test]
657 fn test_get_age_of_sent_at_missing_sent_at() {
658 let tx = TransactionRepoModel {
659 id: "test-tx".to_string(),
660 relayer_id: "test-relayer".to_string(),
661 status: TransactionStatus::Pending,
662 status_reason: None,
663 created_at: "2024-01-01T00:00:00Z".to_string(),
664 sent_at: None, confirmed_at: None,
666 valid_until: None,
667 network_type: crate::models::NetworkType::Evm,
668 network_data: NetworkTransactionData::Evm(EvmTransactionData {
669 from: "0x1234".to_string(),
670 to: Some("0x5678".to_string()),
671 value: U256::from(0u64),
672 data: Some("0x".to_string()),
673 gas_limit: Some(21000),
674 gas_price: Some(10_000_000_000),
675 max_fee_per_gas: None,
676 max_priority_fee_per_gas: None,
677 nonce: Some(42),
678 signature: None,
679 hash: None,
680 speed: Some(Speed::Fast),
681 chain_id: 1,
682 raw: None,
683 }),
684 priced_at: None,
685 hashes: vec![],
686 noop_count: None,
687 is_canceled: Some(false),
688 delete_at: None,
689 };
690
691 let result = get_age_of_sent_at(&tx);
692 assert!(result.is_err());
693 match result.unwrap_err() {
694 TransactionError::UnexpectedError(msg) => {
695 assert!(msg.contains("sent_at time is missing"));
696 }
697 _ => panic!("Expected UnexpectedError for missing sent_at"),
698 }
699 }
700
701 #[test]
702 fn test_get_age_of_sent_at_invalid_timestamp() {
703 let tx = TransactionRepoModel {
704 id: "test-tx".to_string(),
705 relayer_id: "test-relayer".to_string(),
706 status: TransactionStatus::Sent,
707 status_reason: None,
708 created_at: "2024-01-01T00:00:00Z".to_string(),
709 sent_at: Some("invalid-timestamp".to_string()), confirmed_at: None,
711 valid_until: None,
712 network_type: crate::models::NetworkType::Evm,
713 network_data: NetworkTransactionData::Evm(EvmTransactionData {
714 from: "0x1234".to_string(),
715 to: Some("0x5678".to_string()),
716 value: U256::from(0u64),
717 data: Some("0x".to_string()),
718 gas_limit: Some(21000),
719 gas_price: Some(10_000_000_000),
720 max_fee_per_gas: None,
721 max_priority_fee_per_gas: None,
722 nonce: Some(42),
723 signature: None,
724 hash: None,
725 speed: Some(Speed::Fast),
726 chain_id: 1,
727 raw: None,
728 }),
729 priced_at: None,
730 hashes: vec![],
731 noop_count: None,
732 is_canceled: Some(false),
733 delete_at: None,
734 };
735
736 let result = get_age_of_sent_at(&tx);
737 assert!(result.is_err());
738 match result.unwrap_err() {
739 TransactionError::UnexpectedError(msg) => {
740 assert!(msg.contains("Error parsing sent_at time"));
741 }
742 _ => panic!("Expected UnexpectedError for invalid timestamp"),
743 }
744 }
745}