1use crate::constants::{
2 DEFAULT_TX_VALID_TIMESPAN, MAXIMUM_NOOP_RETRY_ATTEMPTS, MAXIMUM_TX_ATTEMPTS,
3};
4use crate::models::{
5 EvmTransactionData, TransactionError, TransactionRepoModel, TransactionStatus, U256,
6};
7use chrono::{DateTime, Duration, Utc};
8use eyre::Result;
9
10pub async fn make_noop(evm_data: &mut EvmTransactionData) -> Result<(), TransactionError> {
13 evm_data.gas_limit = 21_000;
15 evm_data.value = U256::from(0u64);
16 evm_data.data = Some("0x".to_string());
17 evm_data.to = Some(evm_data.from.clone());
18
19 Ok(())
20}
21
22pub fn is_noop(evm_data: &EvmTransactionData) -> bool {
24 evm_data.value == U256::from(0u64)
25 && evm_data.data.as_ref().is_some_and(|data| data == "0x")
26 && evm_data.to.as_ref() == Some(&evm_data.from)
27 && evm_data.speed.is_some()
28}
29
30pub fn too_many_attempts(tx: &TransactionRepoModel) -> bool {
32 tx.hashes.len() > MAXIMUM_TX_ATTEMPTS
33}
34
35pub fn too_many_noop_attempts(tx: &TransactionRepoModel) -> bool {
37 tx.noop_count.unwrap_or(0) > MAXIMUM_NOOP_RETRY_ATTEMPTS
38}
39
40pub fn is_pending_transaction(tx_status: &TransactionStatus) -> bool {
41 tx_status == &TransactionStatus::Pending
42 || tx_status == &TransactionStatus::Sent
43 || tx_status == &TransactionStatus::Submitted
44}
45
46pub fn has_enough_confirmations(
48 tx_block_number: u64,
49 current_block_number: u64,
50 required_confirmations: u64,
51) -> bool {
52 current_block_number >= tx_block_number + required_confirmations
53}
54
55pub fn is_transaction_valid(created_at: &str, valid_until: &Option<String>) -> bool {
57 if let Some(valid_until_str) = valid_until {
58 match DateTime::parse_from_rfc3339(valid_until_str) {
59 Ok(valid_until_time) => return Utc::now() < valid_until_time,
60 Err(e) => {
61 log::warn!("Failed to parse valid_until timestamp: {}", e);
62 return false;
63 }
64 }
65 }
66 match DateTime::parse_from_rfc3339(created_at) {
67 Ok(created_time) => {
68 let default_valid_until =
69 created_time + Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN);
70 Utc::now() < default_valid_until
71 }
72 Err(e) => {
73 log::warn!("Failed to parse created_at timestamp: {}", e);
74 false
75 }
76 }
77}
78
79pub fn get_age_of_sent_at(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
81 let now = Utc::now();
82 let sent_at_str = tx.sent_at.as_ref().ok_or_else(|| {
83 TransactionError::UnexpectedError("Transaction sent_at time is missing".to_string())
84 })?;
85 let sent_time = DateTime::parse_from_rfc3339(sent_at_str)
86 .map_err(|_| TransactionError::UnexpectedError("Error parsing sent_at time".to_string()))?
87 .with_timezone(&Utc);
88 Ok(now.signed_duration_since(sent_time))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::models::{evm::Speed, NetworkTransactionData};
95
96 #[tokio::test]
97 async fn test_make_noop_standard_network() {
98 let mut evm_data = EvmTransactionData {
99 from: "0x1234567890123456789012345678901234567890".to_string(),
100 to: Some("0xoriginal_destination".to_string()),
101 value: U256::from(1000000000000000000u64), data: Some("0xoriginal_data".to_string()),
103 gas_limit: 50000,
104 gas_price: Some(10_000_000_000),
105 max_fee_per_gas: None,
106 max_priority_fee_per_gas: None,
107 nonce: Some(42),
108 signature: None,
109 hash: Some("0xoriginal_hash".to_string()),
110 speed: Some(Speed::Fast),
111 chain_id: 1,
112 raw: Some(vec![1, 2, 3]),
113 };
114
115 let result = make_noop(&mut evm_data).await;
116 assert!(result.is_ok());
117
118 assert_eq!(evm_data.gas_limit, 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)); }
125
126 #[test]
127 fn test_is_noop() {
128 let noop_tx = EvmTransactionData {
130 from: "0x1234567890123456789012345678901234567890".to_string(),
131 to: Some("0x1234567890123456789012345678901234567890".to_string()), value: U256::from(0u64),
133 data: Some("0x".to_string()),
134 gas_limit: 21000,
135 gas_price: Some(10_000_000_000),
136 max_fee_per_gas: None,
137 max_priority_fee_per_gas: None,
138 nonce: Some(42),
139 signature: None,
140 hash: None,
141 speed: Some(Speed::Fast),
142 chain_id: 1,
143 raw: None,
144 };
145 assert!(is_noop(&noop_tx));
146
147 let mut non_noop = noop_tx.clone();
149 non_noop.value = U256::from(1000000000000000000u64); assert!(!is_noop(&non_noop));
151
152 let mut non_noop = noop_tx.clone();
153 non_noop.data = Some("0x123456".to_string());
154 assert!(!is_noop(&non_noop));
155
156 let mut non_noop = noop_tx.clone();
157 non_noop.to = Some("0x9876543210987654321098765432109876543210".to_string());
158 assert!(!is_noop(&non_noop));
159
160 let mut non_noop = noop_tx;
161 non_noop.speed = None;
162 assert!(!is_noop(&non_noop));
163 }
164
165 #[test]
166 fn test_too_many_attempts() {
167 let mut tx = TransactionRepoModel {
168 id: "test-tx".to_string(),
169 relayer_id: "test-relayer".to_string(),
170 status: TransactionStatus::Pending,
171 status_reason: None,
172 created_at: "2024-01-01T00:00:00Z".to_string(),
173 sent_at: None,
174 confirmed_at: None,
175 valid_until: None,
176 network_type: crate::models::NetworkType::Evm,
177 network_data: NetworkTransactionData::Evm(EvmTransactionData {
178 from: "0x1234".to_string(),
179 to: Some("0x5678".to_string()),
180 value: U256::from(0u64),
181 data: Some("0x".to_string()),
182 gas_limit: 21000,
183 gas_price: Some(10_000_000_000),
184 max_fee_per_gas: None,
185 max_priority_fee_per_gas: None,
186 nonce: Some(42),
187 signature: None,
188 hash: None,
189 speed: Some(Speed::Fast),
190 chain_id: 1,
191 raw: None,
192 }),
193 priced_at: None,
194 hashes: vec![], noop_count: None,
196 is_canceled: Some(false),
197 };
198
199 assert!(!too_many_attempts(&tx));
201
202 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS];
204 assert!(!too_many_attempts(&tx));
205
206 tx.hashes = vec!["hash".to_string(); MAXIMUM_TX_ATTEMPTS + 1];
208 assert!(too_many_attempts(&tx));
209 }
210
211 #[test]
212 fn test_too_many_noop_attempts() {
213 let mut tx = TransactionRepoModel {
214 id: "test-tx".to_string(),
215 relayer_id: "test-relayer".to_string(),
216 status: TransactionStatus::Pending,
217 status_reason: None,
218 created_at: "2024-01-01T00:00:00Z".to_string(),
219 sent_at: None,
220 confirmed_at: None,
221 valid_until: None,
222 network_type: crate::models::NetworkType::Evm,
223 network_data: NetworkTransactionData::Evm(EvmTransactionData {
224 from: "0x1234".to_string(),
225 to: Some("0x5678".to_string()),
226 value: U256::from(0u64),
227 data: Some("0x".to_string()),
228 gas_limit: 21000,
229 gas_price: Some(10_000_000_000),
230 max_fee_per_gas: None,
231 max_priority_fee_per_gas: None,
232 nonce: Some(42),
233 signature: None,
234 hash: None,
235 speed: Some(Speed::Fast),
236 chain_id: 1,
237 raw: None,
238 }),
239 priced_at: None,
240 hashes: vec![],
241 noop_count: None,
242 is_canceled: Some(false),
243 };
244
245 assert!(!too_many_noop_attempts(&tx));
247
248 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS);
250 assert!(!too_many_noop_attempts(&tx));
251
252 tx.noop_count = Some(MAXIMUM_NOOP_RETRY_ATTEMPTS + 1);
254 assert!(too_many_noop_attempts(&tx));
255 }
256
257 #[test]
258 fn test_has_enough_confirmations() {
259 let tx_block_number = 100;
261 let current_block_number = 110; let required_confirmations = 12;
263 assert!(!has_enough_confirmations(
264 tx_block_number,
265 current_block_number,
266 required_confirmations
267 ));
268
269 let current_block_number = 112; assert!(has_enough_confirmations(
272 tx_block_number,
273 current_block_number,
274 required_confirmations
275 ));
276
277 let current_block_number = 120; assert!(has_enough_confirmations(
280 tx_block_number,
281 current_block_number,
282 required_confirmations
283 ));
284 }
285
286 #[test]
287 fn test_is_transaction_valid_with_future_timestamp() {
288 let now = Utc::now();
289 let valid_until = Some((now + Duration::hours(1)).to_rfc3339());
290 let created_at = now.to_rfc3339();
291
292 assert!(is_transaction_valid(&created_at, &valid_until));
293 }
294
295 #[test]
296 fn test_is_transaction_valid_with_past_timestamp() {
297 let now = Utc::now();
298 let valid_until = Some((now - Duration::hours(1)).to_rfc3339());
299 let created_at = now.to_rfc3339();
300
301 assert!(!is_transaction_valid(&created_at, &valid_until));
302 }
303
304 #[test]
305 fn test_is_transaction_valid_with_valid_until() {
306 let created_at = Utc::now().to_rfc3339();
308 let valid_until = Some((Utc::now() + Duration::hours(1)).to_rfc3339());
309 assert!(is_transaction_valid(&created_at, &valid_until));
310
311 let valid_until = Some((Utc::now() - Duration::hours(1)).to_rfc3339());
313 assert!(!is_transaction_valid(&created_at, &valid_until));
314
315 let valid_until = Some(Utc::now().to_rfc3339());
317 assert!(!is_transaction_valid(&created_at, &valid_until));
318
319 let valid_until = Some((Utc::now() + Duration::days(365)).to_rfc3339());
321 assert!(is_transaction_valid(&created_at, &valid_until));
322
323 let valid_until = Some("invalid-date-format".to_string());
325 assert!(!is_transaction_valid(&created_at, &valid_until));
326
327 let valid_until = Some("".to_string());
329 assert!(!is_transaction_valid(&created_at, &valid_until));
330 }
331
332 #[test]
333 fn test_is_transaction_valid_without_valid_until() {
334 let created_at = Utc::now().to_rfc3339();
336 let valid_until = None;
337 assert!(is_transaction_valid(&created_at, &valid_until));
338
339 let old_created_at =
341 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN + 1000)).to_rfc3339();
342 assert!(!is_transaction_valid(&old_created_at, &valid_until));
343
344 let boundary_created_at =
346 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN)).to_rfc3339();
347 assert!(!is_transaction_valid(&boundary_created_at, &valid_until));
348
349 let within_boundary_created_at =
351 (Utc::now() - Duration::milliseconds(DEFAULT_TX_VALID_TIMESPAN - 1000)).to_rfc3339();
352 assert!(is_transaction_valid(
353 &within_boundary_created_at,
354 &valid_until
355 ));
356
357 let invalid_created_at = "invalid-date-format";
359 assert!(!is_transaction_valid(invalid_created_at, &valid_until));
360
361 assert!(!is_transaction_valid("", &valid_until));
363 }
364
365 #[test]
366 fn test_is_pending_transaction() {
367 assert!(is_pending_transaction(&TransactionStatus::Pending));
369
370 assert!(is_pending_transaction(&TransactionStatus::Sent));
372
373 assert!(is_pending_transaction(&TransactionStatus::Submitted));
375
376 assert!(!is_pending_transaction(&TransactionStatus::Confirmed));
378 assert!(!is_pending_transaction(&TransactionStatus::Failed));
379 assert!(!is_pending_transaction(&TransactionStatus::Canceled));
380 assert!(!is_pending_transaction(&TransactionStatus::Mined));
381 assert!(!is_pending_transaction(&TransactionStatus::Expired));
382 }
383
384 #[test]
385 fn test_get_age_of_sent_at() {
386 let now = Utc::now();
387
388 let sent_at_time = now - Duration::hours(1);
390 let tx = TransactionRepoModel {
391 id: "test-tx".to_string(),
392 relayer_id: "test-relayer".to_string(),
393 status: TransactionStatus::Sent,
394 status_reason: None,
395 created_at: "2024-01-01T00:00:00Z".to_string(),
396 sent_at: Some(sent_at_time.to_rfc3339()),
397 confirmed_at: None,
398 valid_until: None,
399 network_type: crate::models::NetworkType::Evm,
400 network_data: NetworkTransactionData::Evm(EvmTransactionData {
401 from: "0x1234".to_string(),
402 to: Some("0x5678".to_string()),
403 value: U256::from(0u64),
404 data: Some("0x".to_string()),
405 gas_limit: 21000,
406 gas_price: Some(10_000_000_000),
407 max_fee_per_gas: None,
408 max_priority_fee_per_gas: None,
409 nonce: Some(42),
410 signature: None,
411 hash: None,
412 speed: Some(Speed::Fast),
413 chain_id: 1,
414 raw: None,
415 }),
416 priced_at: None,
417 hashes: vec![],
418 noop_count: None,
419 is_canceled: Some(false),
420 };
421
422 let age_result = get_age_of_sent_at(&tx);
423 assert!(age_result.is_ok());
424 let age = age_result.unwrap();
425 assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
427 }
428
429 #[test]
430 fn test_get_age_of_sent_at_missing_sent_at() {
431 let tx = TransactionRepoModel {
432 id: "test-tx".to_string(),
433 relayer_id: "test-relayer".to_string(),
434 status: TransactionStatus::Pending,
435 status_reason: None,
436 created_at: "2024-01-01T00:00:00Z".to_string(),
437 sent_at: None, confirmed_at: None,
439 valid_until: None,
440 network_type: crate::models::NetworkType::Evm,
441 network_data: NetworkTransactionData::Evm(EvmTransactionData {
442 from: "0x1234".to_string(),
443 to: Some("0x5678".to_string()),
444 value: U256::from(0u64),
445 data: Some("0x".to_string()),
446 gas_limit: 21000,
447 gas_price: Some(10_000_000_000),
448 max_fee_per_gas: None,
449 max_priority_fee_per_gas: None,
450 nonce: Some(42),
451 signature: None,
452 hash: None,
453 speed: Some(Speed::Fast),
454 chain_id: 1,
455 raw: None,
456 }),
457 priced_at: None,
458 hashes: vec![],
459 noop_count: None,
460 is_canceled: Some(false),
461 };
462
463 let result = get_age_of_sent_at(&tx);
464 assert!(result.is_err());
465 match result.unwrap_err() {
466 TransactionError::UnexpectedError(msg) => {
467 assert!(msg.contains("sent_at time is missing"));
468 }
469 _ => panic!("Expected UnexpectedError for missing sent_at"),
470 }
471 }
472
473 #[test]
474 fn test_get_age_of_sent_at_invalid_timestamp() {
475 let tx = TransactionRepoModel {
476 id: "test-tx".to_string(),
477 relayer_id: "test-relayer".to_string(),
478 status: TransactionStatus::Sent,
479 status_reason: None,
480 created_at: "2024-01-01T00:00:00Z".to_string(),
481 sent_at: Some("invalid-timestamp".to_string()), confirmed_at: None,
483 valid_until: None,
484 network_type: crate::models::NetworkType::Evm,
485 network_data: NetworkTransactionData::Evm(EvmTransactionData {
486 from: "0x1234".to_string(),
487 to: Some("0x5678".to_string()),
488 value: U256::from(0u64),
489 data: Some("0x".to_string()),
490 gas_limit: 21000,
491 gas_price: Some(10_000_000_000),
492 max_fee_per_gas: None,
493 max_priority_fee_per_gas: None,
494 nonce: Some(42),
495 signature: None,
496 hash: None,
497 speed: Some(Speed::Fast),
498 chain_id: 1,
499 raw: None,
500 }),
501 priced_at: None,
502 hashes: vec![],
503 noop_count: None,
504 is_canceled: Some(false),
505 };
506
507 let result = get_age_of_sent_at(&tx);
508 assert!(result.is_err());
509 match result.unwrap_err() {
510 TransactionError::UnexpectedError(msg) => {
511 assert!(msg.contains("Error parsing sent_at time"));
512 }
513 _ => panic!("Expected UnexpectedError for invalid timestamp"),
514 }
515 }
516}