openzeppelin_relayer/domain/transaction/
common.rs

1//! Common transaction utilities shared across all blockchain networks.
2//!
3//! This module contains utility functions and constants that are used
4//! across multiple blockchain domains (EVM, Solana, Stellar) to avoid
5//! cross-domain dependencies.
6
7use chrono::{DateTime, Duration, Utc};
8
9use crate::constants::FINAL_TRANSACTION_STATUSES;
10use crate::models::{TransactionError, TransactionRepoModel, TransactionStatus};
11
12/// Checks if a transaction is in a final state (confirmed, failed, canceled, or expired).
13///
14/// Final states are terminal states where no further status updates are expected.
15/// This is used across all blockchain implementations to determine if a transaction
16/// has completed processing.
17///
18/// # Arguments
19///
20/// * `tx_status` - The transaction status to check
21///
22/// # Returns
23///
24/// `true` if the transaction is in a final state, `false` otherwise
25pub fn is_final_state(tx_status: &TransactionStatus) -> bool {
26    FINAL_TRANSACTION_STATUSES.contains(tx_status)
27}
28
29pub fn is_pending_transaction(tx_status: &TransactionStatus) -> bool {
30    matches!(
31        tx_status,
32        TransactionStatus::Pending | TransactionStatus::Sent | TransactionStatus::Submitted
33    )
34}
35
36/// Gets the age of a transaction since it was sent.
37pub fn get_age_of_sent_at(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
38    let now = Utc::now();
39    let sent_at_str = tx.sent_at.as_ref().ok_or_else(|| {
40        TransactionError::UnexpectedError("Transaction sent_at time is missing".to_string())
41    })?;
42    let sent_time = DateTime::parse_from_rfc3339(sent_at_str)
43        .map_err(|_| TransactionError::UnexpectedError("Error parsing sent_at time".to_string()))?
44        .with_timezone(&Utc);
45    Ok(now.signed_duration_since(sent_time))
46}
47
48#[cfg(test)]
49mod tests {
50    use crate::utils::mocks::mockutils::create_mock_transaction;
51
52    use super::*;
53
54    #[test]
55    fn test_is_final_state() {
56        // Final states should return true
57        assert!(is_final_state(&TransactionStatus::Confirmed));
58        assert!(is_final_state(&TransactionStatus::Failed));
59        assert!(is_final_state(&TransactionStatus::Expired));
60        assert!(is_final_state(&TransactionStatus::Canceled));
61
62        // Non-final states should return false
63        assert!(!is_final_state(&TransactionStatus::Pending));
64        assert!(!is_final_state(&TransactionStatus::Sent));
65        assert!(!is_final_state(&TransactionStatus::Submitted));
66        assert!(!is_final_state(&TransactionStatus::Mined));
67    }
68
69    #[test]
70    fn test_is_pending_transaction() {
71        // Test pending status
72        assert!(is_pending_transaction(&TransactionStatus::Pending));
73
74        // Test sent status
75        assert!(is_pending_transaction(&TransactionStatus::Sent));
76
77        // Test submitted status
78        assert!(is_pending_transaction(&TransactionStatus::Submitted));
79
80        // Test non-pending statuses
81        assert!(!is_pending_transaction(&TransactionStatus::Confirmed));
82        assert!(!is_pending_transaction(&TransactionStatus::Failed));
83        assert!(!is_pending_transaction(&TransactionStatus::Canceled));
84        assert!(!is_pending_transaction(&TransactionStatus::Mined));
85        assert!(!is_pending_transaction(&TransactionStatus::Expired));
86    }
87
88    #[test]
89    fn test_get_age_of_sent_at() {
90        let now = Utc::now();
91
92        // Test with valid sent_at timestamp (1 hour ago)
93        let sent_at_time = now - Duration::hours(1);
94        let mut tx = create_mock_transaction();
95        tx.sent_at = Some(sent_at_time.to_rfc3339());
96
97        let age_result = get_age_of_sent_at(&tx);
98        assert!(age_result.is_ok());
99        let age = age_result.unwrap();
100        // Age should be approximately 1 hour (with some tolerance for test execution time)
101        assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
102    }
103
104    #[test]
105    fn test_get_age_of_sent_at_missing_sent_at() {
106        let mut tx = create_mock_transaction();
107        tx.sent_at = None; // Missing sent_at
108
109        let result = get_age_of_sent_at(&tx);
110        assert!(result.is_err());
111        match result.unwrap_err() {
112            TransactionError::UnexpectedError(msg) => {
113                assert!(msg.contains("sent_at time is missing"));
114            }
115            _ => panic!("Expected UnexpectedError for missing sent_at"),
116        }
117    }
118
119    #[test]
120    fn test_get_age_of_sent_at_invalid_timestamp() {
121        let mut tx = create_mock_transaction();
122        tx.sent_at = Some("invalid-timestamp".to_string()); // Invalid timestamp format
123
124        let result = get_age_of_sent_at(&tx);
125        assert!(result.is_err());
126        match result.unwrap_err() {
127            TransactionError::UnexpectedError(msg) => {
128                assert!(msg.contains("Error parsing sent_at time"));
129            }
130            _ => panic!("Expected UnexpectedError for invalid timestamp"),
131        }
132    }
133}