openzeppelin_relayer/domain/transaction/
util.rs1use 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
24pub 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
57pub 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
90pub 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
122pub 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
133pub 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 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); let age = get_age_since_created(&tx).unwrap();
172
173 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); let age = get_age_since_created(&tx).unwrap();
181
182 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); let age = get_age_since_created(&tx).unwrap();
190
191 assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
193 }
194
195 #[test]
196 fn test_handles_negative_age_gracefully() {
197 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 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 tx.created_at = "2025-01-01T12:00:00Z".to_string();
246 assert!(get_age_since_created(&tx).is_ok());
247
248 tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
250 assert!(get_age_since_created(&tx).is_ok());
251
252 tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
254 assert!(get_age_since_created(&tx).is_ok());
255
256 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 tx.created_at = "2025-01-01T12:00:00+05:30".to_string();
267 assert!(get_age_since_created(&tx).is_ok());
268
269 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); 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 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 let invalid_timestamps = vec![
297 "2025-13-01T12:00:00Z", "2025-01-32T12:00:00Z", "2025-01-01T25:00:00Z", "2025-01-01T12:60:00Z", "not-a-date",
302 "2025/01/01",
303 "12:00:00",
304 "just some text",
305 "2025-01-01", "12:00:00Z", ];
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 let result: Result<String, TransactionError> = solana_not_supported_transaction();
337 assert!(result.is_err());
338
339 let result: Result<i32, TransactionError> = solana_not_supported_transaction();
341 assert!(result.is_err());
342
343 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 assert!(result1.is_err());
368 assert!(result2.is_err());
369 assert!(result3.is_err());
370
371 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}