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
36pub fn is_unsubmitted_transaction(tx_status: &TransactionStatus) -> bool {
37    matches!(
38        tx_status,
39        TransactionStatus::Pending | TransactionStatus::Sent
40    )
41}
42
43/// Gets the age of a transaction since it was sent.
44pub fn get_age_of_sent_at(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
45    let now = Utc::now();
46    let sent_at_str = tx.sent_at.as_ref().ok_or_else(|| {
47        TransactionError::UnexpectedError("Transaction sent_at time is missing".to_string())
48    })?;
49    let sent_time = DateTime::parse_from_rfc3339(sent_at_str)
50        .map_err(|_| TransactionError::UnexpectedError("Error parsing sent_at time".to_string()))?
51        .with_timezone(&Utc);
52    Ok(now.signed_duration_since(sent_time))
53}
54
55#[cfg(test)]
56mod tests {
57    use crate::utils::mocks::mockutils::create_mock_transaction;
58
59    use super::*;
60
61    #[test]
62    fn test_is_final_state() {
63        // Final states should return true
64        assert!(is_final_state(&TransactionStatus::Confirmed));
65        assert!(is_final_state(&TransactionStatus::Failed));
66        assert!(is_final_state(&TransactionStatus::Expired));
67        assert!(is_final_state(&TransactionStatus::Canceled));
68
69        // Non-final states should return false
70        assert!(!is_final_state(&TransactionStatus::Pending));
71        assert!(!is_final_state(&TransactionStatus::Sent));
72        assert!(!is_final_state(&TransactionStatus::Submitted));
73        assert!(!is_final_state(&TransactionStatus::Mined));
74    }
75
76    #[test]
77    fn test_is_pending_transaction() {
78        // Test pending status
79        assert!(is_pending_transaction(&TransactionStatus::Pending));
80
81        // Test sent status
82        assert!(is_pending_transaction(&TransactionStatus::Sent));
83
84        // Test submitted status
85        assert!(is_pending_transaction(&TransactionStatus::Submitted));
86
87        // Test non-pending statuses
88        assert!(!is_pending_transaction(&TransactionStatus::Confirmed));
89        assert!(!is_pending_transaction(&TransactionStatus::Failed));
90        assert!(!is_pending_transaction(&TransactionStatus::Canceled));
91        assert!(!is_pending_transaction(&TransactionStatus::Mined));
92        assert!(!is_pending_transaction(&TransactionStatus::Expired));
93    }
94
95    #[test]
96    fn test_is_unsubmitted_transaction() {
97        // Unsubmitted statuses should return true
98        assert!(is_unsubmitted_transaction(&TransactionStatus::Pending));
99        assert!(is_unsubmitted_transaction(&TransactionStatus::Sent));
100
101        // Submitted and other statuses should return false
102        assert!(!is_unsubmitted_transaction(&TransactionStatus::Submitted));
103        assert!(!is_unsubmitted_transaction(&TransactionStatus::Mined));
104        assert!(!is_unsubmitted_transaction(&TransactionStatus::Confirmed));
105        assert!(!is_unsubmitted_transaction(&TransactionStatus::Failed));
106        assert!(!is_unsubmitted_transaction(&TransactionStatus::Canceled));
107        assert!(!is_unsubmitted_transaction(&TransactionStatus::Expired));
108    }
109
110    #[test]
111    fn test_get_age_of_sent_at() {
112        let now = Utc::now();
113
114        // Test with valid sent_at timestamp (1 hour ago)
115        let sent_at_time = now - Duration::hours(1);
116        let mut tx = create_mock_transaction();
117        tx.sent_at = Some(sent_at_time.to_rfc3339());
118
119        let age_result = get_age_of_sent_at(&tx);
120        assert!(age_result.is_ok());
121        let age = age_result.unwrap();
122        // Age should be approximately 1 hour (with some tolerance for test execution time)
123        assert!(age.num_minutes() >= 59 && age.num_minutes() <= 61);
124    }
125
126    #[test]
127    fn test_get_age_of_sent_at_missing_sent_at() {
128        let mut tx = create_mock_transaction();
129        tx.sent_at = None; // Missing sent_at
130
131        let result = get_age_of_sent_at(&tx);
132        assert!(result.is_err());
133        match result.unwrap_err() {
134            TransactionError::UnexpectedError(msg) => {
135                assert!(msg.contains("sent_at time is missing"));
136            }
137            _ => panic!("Expected UnexpectedError for missing sent_at"),
138        }
139    }
140
141    #[test]
142    fn test_get_age_of_sent_at_invalid_timestamp() {
143        let mut tx = create_mock_transaction();
144        tx.sent_at = Some("invalid-timestamp".to_string()); // Invalid timestamp format
145
146        let result = get_age_of_sent_at(&tx);
147        assert!(result.is_err());
148        match result.unwrap_err() {
149            TransactionError::UnexpectedError(msg) => {
150                assert!(msg.contains("Error parsing sent_at time"));
151            }
152            _ => panic!("Expected UnexpectedError for invalid timestamp"),
153        }
154    }
155}