openzeppelin_relayer/domain/transaction/
util.rs

1//! This module provides utility functions for handling transactions within the application.
2//!
3//! It includes functions to retrieve transactions by ID, create relayer transactions, and
4//! handle unsupported operations for specific relayers. The module interacts with various
5//! repositories and factories to perform these operations.
6use actix_web::web::ThinData;
7use chrono::{DateTime, Duration, Utc};
8
9use crate::{
10    domain::get_relayer_by_id,
11    jobs::JobProducerTrait,
12    models::{
13        ApiError, DefaultAppState, NetworkRepoModel, NotificationRepoModel, RelayerRepoModel,
14        SignerRepoModel, ThinDataAppState, TransactionError, TransactionRepoModel,
15    },
16    repositories::{
17        ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
18        Repository, TransactionCounterTrait, TransactionRepository,
19    },
20};
21
22use super::{NetworkTransaction, RelayerTransactionFactory};
23
24/// Retrieves a transaction by its ID.
25///
26/// # Arguments
27///
28/// * `transaction_id` - A `String` representing the ID of the transaction to retrieve.
29/// * `state` - A reference to the application state, wrapped in `ThinData`.
30///
31/// # Returns
32///
33/// A `Result` containing a `TransactionRepoModel` if successful, or an `ApiError` if an error
34/// occurs.
35pub async fn get_transaction_by_id<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
36    transaction_id: String,
37    state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
38) -> Result<TransactionRepoModel, ApiError>
39where
40    J: JobProducerTrait + Send + Sync + 'static,
41    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
42    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
43    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
45    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
46    TCR: TransactionCounterTrait + Send + Sync + 'static,
47    PR: PluginRepositoryTrait + Send + Sync + 'static,
48    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
49{
50    state
51        .transaction_repository
52        .get_by_id(transaction_id)
53        .await
54        .map_err(|e| e.into())
55}
56
57/// Creates a relayer network transaction instance based on the relayer ID.
58///
59/// # Arguments
60///
61/// * `relayer_id` - A `String` representing the ID of the relayer.
62/// * `state` - A reference to the application state, wrapped in `ThinData`.
63///
64/// # Returns
65///
66/// A `Result` containing a `NetworkTransaction` if successful, or an `ApiError` if an error occurs.
67pub async fn get_relayer_transaction(
68    relayer_id: String,
69    state: &ThinData<DefaultAppState>,
70) -> Result<NetworkTransaction, ApiError> {
71    let relayer_model = get_relayer_by_id(relayer_id, state).await?;
72    let signer_model = state
73        .signer_repository
74        .get_by_id(relayer_model.signer_id.clone())
75        .await?;
76
77    RelayerTransactionFactory::create_transaction(
78        relayer_model,
79        signer_model,
80        state.relayer_repository(),
81        state.network_repository(),
82        state.transaction_repository(),
83        state.transaction_counter_store(),
84        state.job_producer(),
85    )
86    .await
87    .map_err(|e| e.into())
88}
89
90/// Creates a relayer network transaction using a relayer model.
91///
92/// # Arguments
93///
94/// * `relayer_model` - A `RelayerRepoModel` representing the relayer.
95/// * `state` - A reference to the application state, wrapped in `ThinData`.
96///
97/// # Returns
98///
99/// A `Result` containing a `NetworkTransaction` if successful, or an `ApiError` if an error occurs.
100pub async fn get_relayer_transaction_by_model(
101    relayer_model: RelayerRepoModel,
102    state: &ThinData<DefaultAppState>,
103) -> Result<NetworkTransaction, ApiError> {
104    let signer_model = state
105        .signer_repository
106        .get_by_id(relayer_model.signer_id.clone())
107        .await?;
108
109    RelayerTransactionFactory::create_transaction(
110        relayer_model,
111        signer_model,
112        state.relayer_repository(),
113        state.network_repository(),
114        state.transaction_repository(),
115        state.transaction_counter_store(),
116        state.job_producer(),
117    )
118    .await
119    .map_err(|e| e.into())
120}
121
122/// Returns an error indicating that Solana relayers are not supported.
123///
124/// # Returns
125///
126/// A `Result` that always contains a `TransactionError::NotSupported` error.
127pub fn solana_not_supported_transaction<T>() -> Result<T, TransactionError> {
128    Err(TransactionError::NotSupported(
129        "Endpoint is not supported for Solana relayers".to_string(),
130    ))
131}
132
133/// Gets the age of a transaction since it was created.
134///
135/// # Arguments
136///
137/// * `tx` - The transaction repository model
138///
139/// # Returns
140///
141/// A `Result` containing the `Duration` since the transaction was created,
142/// or a `TransactionError` if the created_at timestamp cannot be parsed.
143pub fn get_age_since_created(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
144    let created = DateTime::parse_from_rfc3339(&tx.created_at)
145        .map_err(|e| {
146            TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
147        })?
148        .with_timezone(&Utc);
149    Ok(Utc::now().signed_duration_since(created))
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::utils::mocks::mockutils::create_mock_transaction;
156
157    mod get_age_since_created_tests {
158        use super::*;
159
160        /// Helper to create a test transaction with a specific created_at timestamp
161        fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
162            let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
163            let mut tx = create_mock_transaction();
164            tx.created_at = created_at;
165            tx
166        }
167
168        #[test]
169        fn test_returns_correct_age_for_recent_transaction() {
170            let tx = create_test_tx_with_age(30); // 30 seconds ago
171            let age = get_age_since_created(&tx).unwrap();
172
173            // Allow for small timing differences (within 1 second)
174            assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
175        }
176
177        #[test]
178        fn test_returns_correct_age_for_old_transaction() {
179            let tx = create_test_tx_with_age(3600); // 1 hour ago
180            let age = get_age_since_created(&tx).unwrap();
181
182            // Allow for small timing differences
183            assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
184        }
185
186        #[test]
187        fn test_returns_zero_age_for_just_created_transaction() {
188            let tx = create_test_tx_with_age(0); // Just now
189            let age = get_age_since_created(&tx).unwrap();
190
191            // Should be very close to 0
192            assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
193        }
194
195        #[test]
196        fn test_handles_negative_age_gracefully() {
197            // Create transaction with future timestamp (clock skew scenario)
198            let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
199            let mut tx = create_mock_transaction();
200            tx.created_at = created_at;
201
202            let age = get_age_since_created(&tx).unwrap();
203
204            // Age should be negative
205            assert!(age.num_seconds() < 0);
206        }
207
208        #[test]
209        fn test_returns_error_for_invalid_created_at() {
210            let mut tx = create_mock_transaction();
211            tx.created_at = "invalid-timestamp".to_string();
212
213            let result = get_age_since_created(&tx);
214            assert!(result.is_err());
215
216            match result.unwrap_err() {
217                TransactionError::UnexpectedError(msg) => {
218                    assert!(msg.contains("Invalid created_at timestamp"));
219                }
220                _ => panic!("Expected UnexpectedError"),
221            }
222        }
223
224        #[test]
225        fn test_returns_error_for_empty_created_at() {
226            let mut tx = create_mock_transaction();
227            tx.created_at = "".to_string();
228
229            let result = get_age_since_created(&tx);
230            assert!(result.is_err());
231
232            match result.unwrap_err() {
233                TransactionError::UnexpectedError(msg) => {
234                    assert!(msg.contains("Invalid created_at timestamp"));
235                }
236                _ => panic!("Expected UnexpectedError"),
237            }
238        }
239
240        #[test]
241        fn test_handles_various_rfc3339_formats() {
242            let mut tx = create_mock_transaction();
243
244            // Test with UTC timezone
245            tx.created_at = "2025-01-01T12:00:00Z".to_string();
246            assert!(get_age_since_created(&tx).is_ok());
247
248            // Test with offset timezone
249            tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
250            assert!(get_age_since_created(&tx).is_ok());
251
252            // Test with milliseconds
253            tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
254            assert!(get_age_since_created(&tx).is_ok());
255
256            // Test with microseconds
257            tx.created_at = "2025-01-01T12:00:00.123456Z".to_string();
258            assert!(get_age_since_created(&tx).is_ok());
259        }
260
261        #[test]
262        fn test_handles_different_timezones() {
263            let mut tx = create_mock_transaction();
264
265            // Test with positive offset
266            tx.created_at = "2025-01-01T12:00:00+05:30".to_string();
267            assert!(get_age_since_created(&tx).is_ok());
268
269            // Test with negative offset
270            tx.created_at = "2025-01-01T12:00:00-08:00".to_string();
271            assert!(get_age_since_created(&tx).is_ok());
272        }
273
274        #[test]
275        fn test_age_calculation_is_consistent() {
276            let tx = create_test_tx_with_age(60); // 1 minute ago
277
278            // Call multiple times in quick succession
279            let age1 = get_age_since_created(&tx).unwrap();
280            let age2 = get_age_since_created(&tx).unwrap();
281            let age3 = get_age_since_created(&tx).unwrap();
282
283            // All should be very close (within 1 second of each other)
284            let diff1 = (age2.num_seconds() - age1.num_seconds()).abs();
285            let diff2 = (age3.num_seconds() - age2.num_seconds()).abs();
286
287            assert!(diff1 <= 1);
288            assert!(diff2 <= 1);
289        }
290
291        #[test]
292        fn test_returns_error_for_malformed_timestamp() {
293            let mut tx = create_mock_transaction();
294
295            // Various malformed timestamps
296            let invalid_timestamps = vec![
297                "2025-13-01T12:00:00Z", // Invalid month
298                "2025-01-32T12:00:00Z", // Invalid day
299                "2025-01-01T25:00:00Z", // Invalid hour
300                "2025-01-01T12:60:00Z", // Invalid minute
301                "not-a-date",
302                "2025/01/01",
303                "12:00:00",
304                "just some text",
305                "2025-01-01", // Missing time
306                "12:00:00Z",  // Missing date
307            ];
308
309            for invalid_ts in invalid_timestamps {
310                tx.created_at = invalid_ts.to_string();
311                let result = get_age_since_created(&tx);
312                assert!(result.is_err(), "Expected error for: {}", invalid_ts);
313            }
314        }
315    }
316
317    mod solana_not_supported_transaction_tests {
318        use super::*;
319
320        #[test]
321        fn test_returns_not_supported_error() {
322            let result: Result<(), TransactionError> = solana_not_supported_transaction();
323
324            assert!(result.is_err());
325            match result.unwrap_err() {
326                TransactionError::NotSupported(msg) => {
327                    assert_eq!(msg, "Endpoint is not supported for Solana relayers");
328                }
329                _ => panic!("Expected NotSupported error"),
330            }
331        }
332
333        #[test]
334        fn test_works_with_different_return_types() {
335            // Test with String return type
336            let result: Result<String, TransactionError> = solana_not_supported_transaction();
337            assert!(result.is_err());
338
339            // Test with i32 return type
340            let result: Result<i32, TransactionError> = solana_not_supported_transaction();
341            assert!(result.is_err());
342
343            // Test with TransactionRepoModel return type
344            let result: Result<TransactionRepoModel, TransactionError> =
345                solana_not_supported_transaction();
346            assert!(result.is_err());
347        }
348
349        #[test]
350        fn test_error_message_is_descriptive() {
351            let result: Result<(), TransactionError> = solana_not_supported_transaction();
352
353            let error = result.unwrap_err();
354            let error_msg = error.to_string();
355
356            assert!(error_msg.contains("Solana"));
357            assert!(error_msg.contains("not supported"));
358        }
359
360        #[test]
361        fn test_multiple_calls_return_same_error() {
362            let result1: Result<(), TransactionError> = solana_not_supported_transaction();
363            let result2: Result<(), TransactionError> = solana_not_supported_transaction();
364            let result3: Result<(), TransactionError> = solana_not_supported_transaction();
365
366            // All should be errors
367            assert!(result1.is_err());
368            assert!(result2.is_err());
369            assert!(result3.is_err());
370
371            // All should have the same message
372            let msg1 = match result1.unwrap_err() {
373                TransactionError::NotSupported(m) => m,
374                _ => panic!("Wrong error type"),
375            };
376            let msg2 = match result2.unwrap_err() {
377                TransactionError::NotSupported(m) => m,
378                _ => panic!("Wrong error type"),
379            };
380            let msg3 = match result3.unwrap_err() {
381                TransactionError::NotSupported(m) => m,
382                _ => panic!("Wrong error type"),
383            };
384
385            assert_eq!(msg1, msg2);
386            assert_eq!(msg2, msg3);
387        }
388    }
389}