openzeppelin_relayer/domain/transaction/
util.rs1use actix_web::web::ThinData;
7use chrono::{DateTime, Duration, Utc};
8use tracing::{instrument, warn};
9
10use crate::{
11 domain::get_relayer_by_id,
12 jobs::JobProducerTrait,
13 models::{
14 ApiError, DefaultAppState, NetworkRepoModel, NotificationRepoModel, RelayerRepoModel,
15 SignerRepoModel, ThinDataAppState, TransactionError, TransactionRepoModel,
16 },
17 repositories::{
18 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
19 Repository, TransactionCounterTrait, TransactionRepository,
20 },
21};
22
23use super::{NetworkTransaction, RelayerTransactionFactory};
24
25#[instrument(
37 level = "debug",
38 skip(state),
39 fields(
40 request_id = ?crate::observability::request_id::get_request_id(),
41 tx_id = %transaction_id,
42 )
43)]
44pub async fn get_transaction_by_id<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
45 transaction_id: String,
46 state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
47) -> Result<TransactionRepoModel, ApiError>
48where
49 J: JobProducerTrait + Send + Sync + 'static,
50 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
51 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
52 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
53 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
54 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
55 TCR: TransactionCounterTrait + Send + Sync + 'static,
56 PR: PluginRepositoryTrait + Send + Sync + 'static,
57 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
58{
59 state
60 .transaction_repository
61 .get_by_id(transaction_id)
62 .await
63 .map_err(|e| e.into())
64}
65
66#[instrument(
77 level = "debug",
78 skip(state),
79 fields(
80 request_id = ?crate::observability::request_id::get_request_id(),
81 relayer_id = %relayer_id,
82 )
83)]
84pub async fn get_relayer_transaction(
85 relayer_id: String,
86 state: &ThinData<DefaultAppState>,
87) -> Result<NetworkTransaction, ApiError> {
88 let relayer_model = get_relayer_by_id(relayer_id, state).await?;
89 let signer_model = state
90 .signer_repository
91 .get_by_id(relayer_model.signer_id.clone())
92 .await?;
93
94 RelayerTransactionFactory::create_transaction(
95 relayer_model,
96 signer_model,
97 state.relayer_repository(),
98 state.network_repository(),
99 state.transaction_repository(),
100 state.transaction_counter_store(),
101 state.job_producer(),
102 )
103 .await
104 .map_err(|e| e.into())
105}
106
107#[instrument(
118 level = "debug",
119 skip(state, relayer_model),
120 fields(
121 request_id = ?crate::observability::request_id::get_request_id(),
122 relayer_id = %relayer_model.id,
123 )
124)]
125pub async fn get_relayer_transaction_by_model(
126 relayer_model: RelayerRepoModel,
127 state: &ThinData<DefaultAppState>,
128) -> Result<NetworkTransaction, ApiError> {
129 let signer_model = state
130 .signer_repository
131 .get_by_id(relayer_model.signer_id.clone())
132 .await?;
133
134 RelayerTransactionFactory::create_transaction(
135 relayer_model,
136 signer_model,
137 state.relayer_repository(),
138 state.network_repository(),
139 state.transaction_repository(),
140 state.transaction_counter_store(),
141 state.job_producer(),
142 )
143 .await
144 .map_err(|e| e.into())
145}
146
147pub fn solana_not_supported_transaction<T>() -> Result<T, TransactionError> {
153 Err(TransactionError::NotSupported(
154 "Endpoint is not supported for Solana relayers".to_string(),
155 ))
156}
157
158pub fn get_age_since_created(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
169 let created = DateTime::parse_from_rfc3339(&tx.created_at)
170 .map_err(|e| {
171 TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
172 })?
173 .with_timezone(&Utc);
174 Ok(Utc::now().signed_duration_since(created))
175}
176
177pub fn get_age_since_sent(tx: &TransactionRepoModel) -> Result<Duration, TransactionError> {
182 let sent_at = tx.sent_at.as_deref().ok_or_else(|| {
183 TransactionError::UnexpectedError("Missing sent_at timestamp".to_string())
184 })?;
185 let sent = DateTime::parse_from_rfc3339(sent_at).map_err(|e| {
186 TransactionError::UnexpectedError(format!("Invalid sent_at timestamp: {e}"))
187 })?;
188 Ok(Utc::now().signed_duration_since(sent.with_timezone(&Utc)))
189}
190
191pub fn get_age_since_sent_or_created(
195 tx: &TransactionRepoModel,
196) -> Result<Duration, TransactionError> {
197 match get_age_since_sent(tx) {
198 Ok(age) => Ok(age),
199 Err(e) => {
200 if let Some(sent_at) = tx.sent_at.as_deref() {
201 warn!(
202 tx_id = %tx.id,
203 ts = %sent_at,
204 error = %e,
205 "failed to parse sent_at timestamp, falling back to created_at"
206 );
207 }
208 get_age_since_created(tx)
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::utils::mocks::mockutils::create_mock_transaction;
217
218 mod get_age_since_created_tests {
219 use super::*;
220
221 fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
223 let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
224 let mut tx = create_mock_transaction();
225 tx.created_at = created_at;
226 tx
227 }
228
229 #[test]
230 fn test_returns_correct_age_for_recent_transaction() {
231 let tx = create_test_tx_with_age(30); let age = get_age_since_created(&tx).unwrap();
233
234 assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
236 }
237
238 #[test]
239 fn test_returns_correct_age_for_old_transaction() {
240 let tx = create_test_tx_with_age(3600); let age = get_age_since_created(&tx).unwrap();
242
243 assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
245 }
246
247 #[test]
248 fn test_returns_zero_age_for_just_created_transaction() {
249 let tx = create_test_tx_with_age(0); let age = get_age_since_created(&tx).unwrap();
251
252 assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
254 }
255
256 #[test]
257 fn test_handles_negative_age_gracefully() {
258 let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
260 let mut tx = create_mock_transaction();
261 tx.created_at = created_at;
262
263 let age = get_age_since_created(&tx).unwrap();
264
265 assert!(age.num_seconds() < 0);
267 }
268
269 #[test]
270 fn test_returns_error_for_invalid_created_at() {
271 let mut tx = create_mock_transaction();
272 tx.created_at = "invalid-timestamp".to_string();
273
274 let result = get_age_since_created(&tx);
275 assert!(result.is_err());
276
277 match result.unwrap_err() {
278 TransactionError::UnexpectedError(msg) => {
279 assert!(msg.contains("Invalid created_at timestamp"));
280 }
281 _ => panic!("Expected UnexpectedError"),
282 }
283 }
284
285 #[test]
286 fn test_returns_error_for_empty_created_at() {
287 let mut tx = create_mock_transaction();
288 tx.created_at = "".to_string();
289
290 let result = get_age_since_created(&tx);
291 assert!(result.is_err());
292
293 match result.unwrap_err() {
294 TransactionError::UnexpectedError(msg) => {
295 assert!(msg.contains("Invalid created_at timestamp"));
296 }
297 _ => panic!("Expected UnexpectedError"),
298 }
299 }
300
301 #[test]
302 fn test_handles_various_rfc3339_formats() {
303 let mut tx = create_mock_transaction();
304
305 tx.created_at = "2025-01-01T12:00:00Z".to_string();
307 assert!(get_age_since_created(&tx).is_ok());
308
309 tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
311 assert!(get_age_since_created(&tx).is_ok());
312
313 tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
315 assert!(get_age_since_created(&tx).is_ok());
316
317 tx.created_at = "2025-01-01T12:00:00.123456Z".to_string();
319 assert!(get_age_since_created(&tx).is_ok());
320 }
321
322 #[test]
323 fn test_handles_different_timezones() {
324 let mut tx = create_mock_transaction();
325
326 tx.created_at = "2025-01-01T12:00:00+05:30".to_string();
328 assert!(get_age_since_created(&tx).is_ok());
329
330 tx.created_at = "2025-01-01T12:00:00-08:00".to_string();
332 assert!(get_age_since_created(&tx).is_ok());
333 }
334
335 #[test]
336 fn test_age_calculation_is_consistent() {
337 let tx = create_test_tx_with_age(60); let age1 = get_age_since_created(&tx).unwrap();
341 let age2 = get_age_since_created(&tx).unwrap();
342 let age3 = get_age_since_created(&tx).unwrap();
343
344 let diff1 = (age2.num_seconds() - age1.num_seconds()).abs();
346 let diff2 = (age3.num_seconds() - age2.num_seconds()).abs();
347
348 assert!(diff1 <= 1);
349 assert!(diff2 <= 1);
350 }
351
352 #[test]
353 fn test_returns_error_for_malformed_timestamp() {
354 let mut tx = create_mock_transaction();
355
356 let invalid_timestamps = vec![
358 "2025-13-01T12:00:00Z", "2025-01-32T12:00:00Z", "2025-01-01T25:00:00Z", "2025-01-01T12:60:00Z", "not-a-date",
363 "2025/01/01",
364 "12:00:00",
365 "just some text",
366 "2025-01-01", "12:00:00Z", ];
369
370 for invalid_ts in invalid_timestamps {
371 tx.created_at = invalid_ts.to_string();
372 let result = get_age_since_created(&tx);
373 assert!(result.is_err(), "Expected error for: {invalid_ts}");
374 }
375 }
376 }
377
378 mod get_age_since_sent_or_created_tests {
379 use super::*;
380
381 fn create_test_tx_with_timestamps(
383 created_at: String,
384 sent_at: Option<String>,
385 ) -> TransactionRepoModel {
386 let mut tx = create_mock_transaction();
387 tx.created_at = created_at;
388 tx.sent_at = sent_at;
389 tx
390 }
391
392 #[test]
393 fn test_uses_sent_at_when_present() {
394 let sent_at = (Utc::now() - Duration::minutes(5)).to_rfc3339();
395 let created_at = (Utc::now() - Duration::minutes(30)).to_rfc3339();
396 let tx = create_test_tx_with_timestamps(created_at, Some(sent_at));
397
398 let age = get_age_since_sent_or_created(&tx).unwrap();
399 assert!(age.num_minutes() >= 5);
400 }
401
402 #[test]
403 fn test_falls_back_to_created_at_when_sent_at_missing() {
404 let created_at = (Utc::now() - Duration::minutes(10)).to_rfc3339();
405 let tx = create_test_tx_with_timestamps(created_at, None);
406
407 let age = get_age_since_sent_or_created(&tx).unwrap();
408 assert!(age.num_minutes() >= 10);
409 }
410
411 #[test]
412 fn test_falls_back_to_created_at_when_sent_at_invalid() {
413 let created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
414 let tx = create_test_tx_with_timestamps(created_at, Some("not-a-date".to_string()));
415
416 let age = get_age_since_sent_or_created(&tx).unwrap();
417 assert!(age.num_minutes() >= 2);
418 }
419 }
420
421 mod solana_not_supported_transaction_tests {
422 use super::*;
423
424 #[test]
425 fn test_returns_not_supported_error() {
426 let result: Result<(), TransactionError> = solana_not_supported_transaction();
427
428 assert!(result.is_err());
429 match result.unwrap_err() {
430 TransactionError::NotSupported(msg) => {
431 assert_eq!(msg, "Endpoint is not supported for Solana relayers");
432 }
433 _ => panic!("Expected NotSupported error"),
434 }
435 }
436
437 #[test]
438 fn test_works_with_different_return_types() {
439 let result: Result<String, TransactionError> = solana_not_supported_transaction();
441 assert!(result.is_err());
442
443 let result: Result<i32, TransactionError> = solana_not_supported_transaction();
445 assert!(result.is_err());
446
447 let result: Result<TransactionRepoModel, TransactionError> =
449 solana_not_supported_transaction();
450 assert!(result.is_err());
451 }
452
453 #[test]
454 fn test_error_message_is_descriptive() {
455 let result: Result<(), TransactionError> = solana_not_supported_transaction();
456
457 let error = result.unwrap_err();
458 let error_msg = error.to_string();
459
460 assert!(error_msg.contains("Solana"));
461 assert!(error_msg.contains("not supported"));
462 }
463
464 #[test]
465 fn test_multiple_calls_return_same_error() {
466 let result1: Result<(), TransactionError> = solana_not_supported_transaction();
467 let result2: Result<(), TransactionError> = solana_not_supported_transaction();
468 let result3: Result<(), TransactionError> = solana_not_supported_transaction();
469
470 assert!(result1.is_err());
472 assert!(result2.is_err());
473 assert!(result3.is_err());
474
475 let msg1 = match result1.unwrap_err() {
477 TransactionError::NotSupported(m) => m,
478 _ => panic!("Wrong error type"),
479 };
480 let msg2 = match result2.unwrap_err() {
481 TransactionError::NotSupported(m) => m,
482 _ => panic!("Wrong error type"),
483 };
484 let msg3 = match result3.unwrap_err() {
485 TransactionError::NotSupported(m) => m,
486 _ => panic!("Wrong error type"),
487 };
488
489 assert_eq!(msg1, msg2);
490 assert_eq!(msg2, msg3);
491 }
492 }
493}