1use crate::{
8 models::{
9 NetworkTransactionData, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
10 },
11 repositories::*,
12};
13use async_trait::async_trait;
14use eyre::Result;
15use itertools::Itertools;
16use std::collections::HashMap;
17use tokio::sync::{Mutex, MutexGuard};
18
19#[derive(Debug)]
20pub struct InMemoryTransactionRepository {
21 store: Mutex<HashMap<String, TransactionRepoModel>>,
22}
23
24impl Clone for InMemoryTransactionRepository {
25 fn clone(&self) -> Self {
26 let data = self
28 .store
29 .try_lock()
30 .map(|guard| guard.clone())
31 .unwrap_or_else(|_| HashMap::new());
32
33 Self {
34 store: Mutex::new(data),
35 }
36 }
37}
38
39impl InMemoryTransactionRepository {
40 pub fn new() -> Self {
41 Self {
42 store: Mutex::new(HashMap::new()),
43 }
44 }
45
46 async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
47 Ok(lock.lock().await)
48 }
49
50 fn get_sort_key(tx: &TransactionRepoModel) -> (&str, bool) {
56 if tx.status == TransactionStatus::Confirmed {
57 if let Some(ref confirmed_at) = tx.confirmed_at {
58 return (confirmed_at, true);
59 }
60 }
62 (&tx.created_at, false)
63 }
64
65 fn compare_for_sort(a: &TransactionRepoModel, b: &TransactionRepoModel) -> std::cmp::Ordering {
68 let (a_key, _) = Self::get_sort_key(a);
69 let (b_key, _) = Self::get_sort_key(b);
70 b_key
71 .cmp(a_key) .then_with(|| b.id.cmp(&a.id)) }
74}
75
76#[async_trait]
79impl Repository<TransactionRepoModel, String> for InMemoryTransactionRepository {
80 async fn create(
81 &self,
82 tx: TransactionRepoModel,
83 ) -> Result<TransactionRepoModel, RepositoryError> {
84 let mut store = Self::acquire_lock(&self.store).await?;
85 if store.contains_key(&tx.id) {
86 return Err(RepositoryError::ConstraintViolation(format!(
87 "Transaction with ID {} already exists",
88 tx.id
89 )));
90 }
91 store.insert(tx.id.clone(), tx.clone());
92 Ok(tx)
93 }
94
95 async fn get_by_id(&self, id: String) -> Result<TransactionRepoModel, RepositoryError> {
96 let store = Self::acquire_lock(&self.store).await?;
97 store
98 .get(&id)
99 .cloned()
100 .ok_or_else(|| RepositoryError::NotFound(format!("Transaction with ID {id} not found")))
101 }
102
103 #[allow(clippy::map_entry)]
104 async fn update(
105 &self,
106 id: String,
107 tx: TransactionRepoModel,
108 ) -> Result<TransactionRepoModel, RepositoryError> {
109 let mut store = Self::acquire_lock(&self.store).await?;
110 if store.contains_key(&id) {
111 let mut updated_tx = tx;
112 updated_tx.id = id.clone();
113 store.insert(id, updated_tx.clone());
114 Ok(updated_tx)
115 } else {
116 Err(RepositoryError::NotFound(format!(
117 "Transaction with ID {id} not found"
118 )))
119 }
120 }
121
122 async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
123 let mut store = Self::acquire_lock(&self.store).await?;
124 if store.remove(&id).is_some() {
125 Ok(())
126 } else {
127 Err(RepositoryError::NotFound(format!(
128 "Transaction with ID {id} not found"
129 )))
130 }
131 }
132
133 async fn list_all(&self) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
134 let store = Self::acquire_lock(&self.store).await?;
135 Ok(store.values().cloned().collect())
136 }
137
138 async fn list_paginated(
139 &self,
140 query: PaginationQuery,
141 ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
142 let total = self.count().await?;
143 let start = ((query.page - 1) * query.per_page) as usize;
144 let store = Self::acquire_lock(&self.store).await?;
145 let items: Vec<TransactionRepoModel> = store
146 .values()
147 .skip(start)
148 .take(query.per_page as usize)
149 .cloned()
150 .collect();
151
152 Ok(PaginatedResult {
153 items,
154 total: total as u64,
155 page: query.page,
156 per_page: query.per_page,
157 })
158 }
159
160 async fn count(&self) -> Result<usize, RepositoryError> {
161 let store = Self::acquire_lock(&self.store).await?;
162 Ok(store.len())
163 }
164
165 async fn has_entries(&self) -> Result<bool, RepositoryError> {
166 let store = Self::acquire_lock(&self.store).await?;
167 Ok(!store.is_empty())
168 }
169
170 async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
171 let mut store = Self::acquire_lock(&self.store).await?;
172 store.clear();
173 Ok(())
174 }
175}
176
177#[async_trait]
178impl TransactionRepository for InMemoryTransactionRepository {
179 async fn find_by_relayer_id(
180 &self,
181 relayer_id: &str,
182 query: PaginationQuery,
183 ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
184 let store = Self::acquire_lock(&self.store).await?;
185 let filtered: Vec<TransactionRepoModel> = store
186 .values()
187 .filter(|tx| tx.relayer_id == relayer_id)
188 .cloned()
189 .collect();
190
191 let total = filtered.len() as u64;
192
193 if total == 0 {
194 return Ok(PaginatedResult::<TransactionRepoModel> {
195 items: vec![],
196 total: 0,
197 page: query.page,
198 per_page: query.per_page,
199 });
200 }
201
202 let start = ((query.page - 1) * query.per_page) as usize;
203
204 let items = filtered
206 .into_iter()
207 .sorted_by(|a, b| b.created_at.cmp(&a.created_at)) .skip(start)
209 .take(query.per_page as usize)
210 .collect();
211
212 Ok(PaginatedResult {
213 items,
214 total,
215 page: query.page,
216 per_page: query.per_page,
217 })
218 }
219
220 async fn find_by_status(
221 &self,
222 relayer_id: &str,
223 statuses: &[TransactionStatus],
224 ) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
225 let store = Self::acquire_lock(&self.store).await?;
226 let filtered: Vec<TransactionRepoModel> = store
227 .values()
228 .filter(|tx| tx.relayer_id == relayer_id && statuses.contains(&tx.status))
229 .cloned()
230 .collect();
231
232 let sorted = filtered
234 .into_iter()
235 .sorted_by(|a, b| b.created_at.cmp(&a.created_at))
236 .collect();
237
238 Ok(sorted)
239 }
240
241 async fn find_by_status_paginated(
242 &self,
243 relayer_id: &str,
244 statuses: &[TransactionStatus],
245 query: PaginationQuery,
246 oldest_first: bool,
247 ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
248 let store = Self::acquire_lock(&self.store).await?;
249
250 let filtered: Vec<TransactionRepoModel> = store
252 .values()
253 .filter(|tx| tx.relayer_id == relayer_id && statuses.contains(&tx.status))
254 .cloned()
255 .collect();
256
257 let total = filtered.len() as u64;
258 let start = ((query.page.saturating_sub(1)) * query.per_page) as usize;
259
260 let items: Vec<TransactionRepoModel> = if oldest_first {
263 filtered
264 .into_iter()
265 .sorted_by(|a, b| {
266 let (a_key, _) = Self::get_sort_key(a);
267 let (b_key, _) = Self::get_sort_key(b);
268 a_key
269 .cmp(b_key) .then_with(|| a.id.cmp(&b.id)) })
272 .skip(start)
273 .take(query.per_page as usize)
274 .collect()
275 } else {
276 filtered
277 .into_iter()
278 .sorted_by(Self::compare_for_sort) .skip(start)
280 .take(query.per_page as usize)
281 .collect()
282 };
283
284 Ok(PaginatedResult {
285 items,
286 total,
287 page: query.page,
288 per_page: query.per_page,
289 })
290 }
291
292 async fn find_by_nonce(
293 &self,
294 relayer_id: &str,
295 nonce: u64,
296 ) -> Result<Option<TransactionRepoModel>, RepositoryError> {
297 let store = Self::acquire_lock(&self.store).await?;
298 let filtered: Vec<TransactionRepoModel> = store
299 .values()
300 .filter(|tx| {
301 tx.relayer_id == relayer_id
302 && match &tx.network_data {
303 NetworkTransactionData::Evm(data) => data.nonce == Some(nonce),
304 _ => false,
305 }
306 })
307 .cloned()
308 .collect();
309
310 Ok(filtered.into_iter().next())
311 }
312
313 async fn update_status(
314 &self,
315 tx_id: String,
316 status: TransactionStatus,
317 ) -> Result<TransactionRepoModel, RepositoryError> {
318 let update = TransactionUpdateRequest {
319 status: Some(status),
320 ..Default::default()
321 };
322 self.partial_update(tx_id, update).await
323 }
324
325 async fn partial_update(
326 &self,
327 tx_id: String,
328 update: TransactionUpdateRequest,
329 ) -> Result<TransactionRepoModel, RepositoryError> {
330 let mut store = Self::acquire_lock(&self.store).await?;
331
332 if let Some(tx) = store.get_mut(&tx_id) {
333 tx.apply_partial_update(update);
335 Ok(tx.clone())
336 } else {
337 Err(RepositoryError::NotFound(format!(
338 "Transaction with ID {tx_id} not found"
339 )))
340 }
341 }
342
343 async fn update_network_data(
344 &self,
345 tx_id: String,
346 network_data: NetworkTransactionData,
347 ) -> Result<TransactionRepoModel, RepositoryError> {
348 let mut tx = self.get_by_id(tx_id.clone()).await?;
349 tx.network_data = network_data;
350 self.update(tx_id, tx).await
351 }
352
353 async fn set_sent_at(
354 &self,
355 tx_id: String,
356 sent_at: String,
357 ) -> Result<TransactionRepoModel, RepositoryError> {
358 let mut tx = self.get_by_id(tx_id.clone()).await?;
359 tx.sent_at = Some(sent_at);
360 self.update(tx_id, tx).await
361 }
362
363 async fn set_confirmed_at(
364 &self,
365 tx_id: String,
366 confirmed_at: String,
367 ) -> Result<TransactionRepoModel, RepositoryError> {
368 let mut tx = self.get_by_id(tx_id.clone()).await?;
369 tx.confirmed_at = Some(confirmed_at);
370 self.update(tx_id, tx).await
371 }
372
373 async fn count_by_status(
374 &self,
375 relayer_id: &str,
376 statuses: &[TransactionStatus],
377 ) -> Result<u64, RepositoryError> {
378 let store = Self::acquire_lock(&self.store).await?;
379 let count = store
380 .values()
381 .filter(|tx| tx.relayer_id == relayer_id && statuses.contains(&tx.status))
382 .count() as u64;
383 Ok(count)
384 }
385
386 async fn delete_by_ids(&self, ids: Vec<String>) -> Result<BatchDeleteResult, RepositoryError> {
387 if ids.is_empty() {
388 return Ok(BatchDeleteResult::default());
389 }
390
391 let mut store = Self::acquire_lock(&self.store).await?;
392 let mut deleted_count = 0;
393 let mut failed = Vec::new();
394
395 for id in ids {
396 if store.remove(&id).is_some() {
397 deleted_count += 1;
398 } else {
399 failed.push((id.clone(), format!("Transaction with ID {id} not found")));
400 }
401 }
402
403 Ok(BatchDeleteResult {
404 deleted_count,
405 failed,
406 })
407 }
408
409 async fn delete_by_requests(
410 &self,
411 requests: Vec<TransactionDeleteRequest>,
412 ) -> Result<BatchDeleteResult, RepositoryError> {
413 if requests.is_empty() {
414 return Ok(BatchDeleteResult::default());
415 }
416
417 let ids: Vec<String> = requests.into_iter().map(|r| r.id).collect();
419 self.delete_by_ids(ids).await
420 }
421}
422
423impl Default for InMemoryTransactionRepository {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use crate::models::{evm::Speed, EvmTransactionData, NetworkType};
432 use lazy_static::lazy_static;
433 use std::str::FromStr;
434
435 use crate::models::U256;
436
437 use super::*;
438
439 use tokio::sync::Mutex;
440
441 lazy_static! {
442 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
443 }
444 fn create_test_transaction(id: &str) -> TransactionRepoModel {
446 TransactionRepoModel {
447 id: id.to_string(),
448 relayer_id: "relayer-1".to_string(),
449 status: TransactionStatus::Pending,
450 status_reason: None,
451 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
452 sent_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
453 confirmed_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
454 valid_until: None,
455 delete_at: None,
456 network_type: NetworkType::Evm,
457 priced_at: None,
458 hashes: vec![],
459 network_data: NetworkTransactionData::Evm(EvmTransactionData {
460 gas_price: Some(1000000000),
461 gas_limit: Some(21000),
462 nonce: Some(1),
463 value: U256::from_str("1000000000000000000").unwrap(),
464 data: Some("0x".to_string()),
465 from: "0xSender".to_string(),
466 to: Some("0xRecipient".to_string()),
467 chain_id: 1,
468 signature: None,
469 hash: Some(format!("0x{id}")),
470 speed: Some(Speed::Fast),
471 max_fee_per_gas: None,
472 max_priority_fee_per_gas: None,
473 raw: None,
474 }),
475 noop_count: None,
476 is_canceled: Some(false),
477 metadata: None,
478 }
479 }
480
481 fn create_test_transaction_pending_state(id: &str) -> TransactionRepoModel {
482 TransactionRepoModel {
483 id: id.to_string(),
484 relayer_id: "relayer-1".to_string(),
485 status: TransactionStatus::Pending,
486 status_reason: None,
487 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
488 sent_at: None,
489 confirmed_at: None,
490 valid_until: None,
491 delete_at: None,
492 network_type: NetworkType::Evm,
493 priced_at: None,
494 hashes: vec![],
495 network_data: NetworkTransactionData::Evm(EvmTransactionData {
496 gas_price: Some(1000000000),
497 gas_limit: Some(21000),
498 nonce: Some(1),
499 value: U256::from_str("1000000000000000000").unwrap(),
500 data: Some("0x".to_string()),
501 from: "0xSender".to_string(),
502 to: Some("0xRecipient".to_string()),
503 chain_id: 1,
504 signature: None,
505 hash: Some(format!("0x{id}")),
506 speed: Some(Speed::Fast),
507 max_fee_per_gas: None,
508 max_priority_fee_per_gas: None,
509 raw: None,
510 }),
511 noop_count: None,
512 is_canceled: Some(false),
513 metadata: None,
514 }
515 }
516
517 #[tokio::test]
518 async fn test_create_transaction() {
519 let repo = InMemoryTransactionRepository::new();
520 let tx = create_test_transaction("test-1");
521
522 let result = repo.create(tx.clone()).await.unwrap();
523 assert_eq!(result.id, tx.id);
524 assert_eq!(repo.count().await.unwrap(), 1);
525 }
526
527 #[tokio::test]
528 async fn test_get_transaction() {
529 let repo = InMemoryTransactionRepository::new();
530 let tx = create_test_transaction("test-1");
531
532 repo.create(tx.clone()).await.unwrap();
533 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
534 if let NetworkTransactionData::Evm(stored_data) = &stored.network_data {
535 if let NetworkTransactionData::Evm(tx_data) = &tx.network_data {
536 assert_eq!(stored_data.hash, tx_data.hash);
537 }
538 }
539 }
540
541 #[tokio::test]
542 async fn test_update_transaction() {
543 let repo = InMemoryTransactionRepository::new();
544 let mut tx = create_test_transaction("test-1");
545
546 repo.create(tx.clone()).await.unwrap();
547 tx.status = TransactionStatus::Confirmed;
548
549 let updated = repo.update("test-1".to_string(), tx).await.unwrap();
550 assert!(matches!(updated.status, TransactionStatus::Confirmed));
551 }
552
553 #[tokio::test]
554 async fn test_delete_transaction() {
555 let repo = InMemoryTransactionRepository::new();
556 let tx = create_test_transaction("test-1");
557
558 repo.create(tx).await.unwrap();
559 repo.delete_by_id("test-1".to_string()).await.unwrap();
560
561 let result = repo.get_by_id("test-1".to_string()).await;
562 assert!(result.is_err());
563 }
564
565 #[tokio::test]
566 async fn test_list_all_transactions() {
567 let repo = InMemoryTransactionRepository::new();
568 let tx1 = create_test_transaction("test-1");
569 let tx2 = create_test_transaction("test-2");
570
571 repo.create(tx1).await.unwrap();
572 repo.create(tx2).await.unwrap();
573
574 let transactions = repo.list_all().await.unwrap();
575 assert_eq!(transactions.len(), 2);
576 }
577
578 #[tokio::test]
579 async fn test_count_transactions() {
580 let repo = InMemoryTransactionRepository::new();
581 let tx = create_test_transaction("test-1");
582
583 assert_eq!(repo.count().await.unwrap(), 0);
584 repo.create(tx).await.unwrap();
585 assert_eq!(repo.count().await.unwrap(), 1);
586 }
587
588 #[tokio::test]
589 async fn test_get_nonexistent_transaction() {
590 let repo = InMemoryTransactionRepository::new();
591 let result = repo.get_by_id("nonexistent".to_string()).await;
592 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
593 }
594
595 #[tokio::test]
596 async fn test_duplicate_transaction_creation() {
597 let repo = InMemoryTransactionRepository::new();
598 let tx = create_test_transaction("test-1");
599
600 repo.create(tx.clone()).await.unwrap();
601 let result = repo.create(tx).await;
602
603 assert!(matches!(
604 result,
605 Err(RepositoryError::ConstraintViolation(_))
606 ));
607 }
608
609 #[tokio::test]
610 async fn test_update_nonexistent_transaction() {
611 let repo = InMemoryTransactionRepository::new();
612 let tx = create_test_transaction("test-1");
613
614 let result = repo.update("nonexistent".to_string(), tx).await;
615 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
616 }
617
618 #[tokio::test]
619 async fn test_partial_update() {
620 let repo = InMemoryTransactionRepository::new();
621 let tx = create_test_transaction_pending_state("test-tx-id");
622 repo.create(tx.clone()).await.unwrap();
623
624 let update1 = TransactionUpdateRequest {
626 status: Some(TransactionStatus::Sent),
627 status_reason: None,
628 sent_at: None,
629 confirmed_at: None,
630 network_data: None,
631 hashes: None,
632 priced_at: None,
633 noop_count: None,
634 is_canceled: None,
635 delete_at: None,
636 metadata: None,
637 };
638 let updated_tx1 = repo
639 .partial_update("test-tx-id".to_string(), update1)
640 .await
641 .unwrap();
642 assert_eq!(updated_tx1.status, TransactionStatus::Sent);
643 assert_eq!(updated_tx1.sent_at, None);
644
645 let update2 = TransactionUpdateRequest {
647 status: Some(TransactionStatus::Confirmed),
648 status_reason: None,
649 sent_at: Some("2023-01-01T12:00:00Z".to_string()),
650 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
651 network_data: None,
652 hashes: None,
653 priced_at: None,
654 noop_count: None,
655 is_canceled: None,
656 delete_at: None,
657 metadata: None,
658 };
659 let updated_tx2 = repo
660 .partial_update("test-tx-id".to_string(), update2)
661 .await
662 .unwrap();
663 assert_eq!(updated_tx2.status, TransactionStatus::Confirmed);
664 assert_eq!(
665 updated_tx2.sent_at,
666 Some("2023-01-01T12:00:00Z".to_string())
667 );
668 assert_eq!(
669 updated_tx2.confirmed_at,
670 Some("2023-01-01T12:05:00Z".to_string())
671 );
672
673 let update3 = TransactionUpdateRequest {
675 status: Some(TransactionStatus::Failed),
676 status_reason: None,
677 sent_at: None,
678 confirmed_at: None,
679 network_data: None,
680 hashes: None,
681 priced_at: None,
682 noop_count: None,
683 is_canceled: None,
684 delete_at: None,
685 metadata: None,
686 };
687 let result = repo
688 .partial_update("non-existent-id".to_string(), update3)
689 .await;
690 assert!(result.is_err());
691 assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
692 }
693
694 #[tokio::test]
695 async fn test_update_status() {
696 let repo = InMemoryTransactionRepository::new();
697 let tx = create_test_transaction("test-1");
698
699 repo.create(tx).await.unwrap();
700
701 let updated = repo
703 .update_status("test-1".to_string(), TransactionStatus::Confirmed)
704 .await
705 .unwrap();
706
707 assert_eq!(updated.status, TransactionStatus::Confirmed);
709
710 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
712 assert_eq!(stored.status, TransactionStatus::Confirmed);
713
714 let updated = repo
716 .update_status("test-1".to_string(), TransactionStatus::Failed)
717 .await
718 .unwrap();
719
720 assert_eq!(updated.status, TransactionStatus::Failed);
722
723 let result = repo
725 .update_status("non-existent".to_string(), TransactionStatus::Confirmed)
726 .await;
727 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
728 }
729
730 #[tokio::test]
731 async fn test_list_paginated() {
732 let repo = InMemoryTransactionRepository::new();
733
734 for i in 1..=10 {
736 let tx = create_test_transaction(&format!("test-{i}"));
737 repo.create(tx).await.unwrap();
738 }
739
740 let query = PaginationQuery {
742 page: 1,
743 per_page: 3,
744 };
745 let result = repo.list_paginated(query).await.unwrap();
746 assert_eq!(result.items.len(), 3);
747 assert_eq!(result.total, 10);
748 assert_eq!(result.page, 1);
749 assert_eq!(result.per_page, 3);
750
751 let query = PaginationQuery {
753 page: 2,
754 per_page: 3,
755 };
756 let result = repo.list_paginated(query).await.unwrap();
757 assert_eq!(result.items.len(), 3);
758 assert_eq!(result.total, 10);
759 assert_eq!(result.page, 2);
760 assert_eq!(result.per_page, 3);
761
762 let query = PaginationQuery {
764 page: 4,
765 per_page: 3,
766 };
767 let result = repo.list_paginated(query).await.unwrap();
768 assert_eq!(result.items.len(), 1);
769 assert_eq!(result.total, 10);
770 assert_eq!(result.page, 4);
771 assert_eq!(result.per_page, 3);
772
773 let query = PaginationQuery {
775 page: 5,
776 per_page: 3,
777 };
778 let result = repo.list_paginated(query).await.unwrap();
779 assert_eq!(result.items.len(), 0);
780 assert_eq!(result.total, 10);
781 }
782
783 #[tokio::test]
784 async fn test_find_by_nonce() {
785 let repo = InMemoryTransactionRepository::new();
786
787 let tx1 = create_test_transaction("test-1");
789
790 let mut tx2 = create_test_transaction("test-2");
791 if let NetworkTransactionData::Evm(ref mut data) = tx2.network_data {
792 data.nonce = Some(2);
793 }
794
795 let mut tx3 = create_test_transaction("test-3");
796 tx3.relayer_id = "relayer-2".to_string();
797 if let NetworkTransactionData::Evm(ref mut data) = tx3.network_data {
798 data.nonce = Some(1);
799 }
800
801 repo.create(tx1).await.unwrap();
802 repo.create(tx2).await.unwrap();
803 repo.create(tx3).await.unwrap();
804
805 let result = repo.find_by_nonce("relayer-1", 1).await.unwrap();
807 assert!(result.is_some());
808 assert_eq!(result.as_ref().unwrap().id, "test-1");
809
810 let result = repo.find_by_nonce("relayer-1", 2).await.unwrap();
812 assert!(result.is_some());
813 assert_eq!(result.as_ref().unwrap().id, "test-2");
814
815 let result = repo.find_by_nonce("relayer-2", 1).await.unwrap();
817 assert!(result.is_some());
818 assert_eq!(result.as_ref().unwrap().id, "test-3");
819
820 let result = repo.find_by_nonce("relayer-1", 99).await.unwrap();
822 assert!(result.is_none());
823 }
824
825 #[tokio::test]
826 async fn test_update_network_data() {
827 let repo = InMemoryTransactionRepository::new();
828 let tx = create_test_transaction("test-1");
829
830 repo.create(tx.clone()).await.unwrap();
831
832 let updated_network_data = NetworkTransactionData::Evm(EvmTransactionData {
834 gas_price: Some(2000000000),
835 gas_limit: Some(30000),
836 nonce: Some(2),
837 value: U256::from_str("2000000000000000000").unwrap(),
838 data: Some("0xUpdated".to_string()),
839 from: "0xSender".to_string(),
840 to: Some("0xRecipient".to_string()),
841 chain_id: 1,
842 signature: None,
843 hash: Some("0xUpdated".to_string()),
844 raw: None,
845 speed: None,
846 max_fee_per_gas: None,
847 max_priority_fee_per_gas: None,
848 });
849
850 let updated = repo
851 .update_network_data("test-1".to_string(), updated_network_data)
852 .await
853 .unwrap();
854
855 if let NetworkTransactionData::Evm(data) = &updated.network_data {
857 assert_eq!(data.gas_price, Some(2000000000));
858 assert_eq!(data.gas_limit, Some(30000));
859 assert_eq!(data.nonce, Some(2));
860 assert_eq!(data.hash, Some("0xUpdated".to_string()));
861 assert_eq!(data.data, Some("0xUpdated".to_string()));
862 } else {
863 panic!("Expected EVM network data");
864 }
865 }
866
867 #[tokio::test]
868 async fn test_set_sent_at() {
869 let repo = InMemoryTransactionRepository::new();
870 let tx = create_test_transaction("test-1");
871
872 repo.create(tx).await.unwrap();
873
874 let new_sent_at = "2025-02-01T10:00:00.000000+00:00".to_string();
876
877 let updated = repo
878 .set_sent_at("test-1".to_string(), new_sent_at.clone())
879 .await
880 .unwrap();
881
882 assert_eq!(updated.sent_at, Some(new_sent_at.clone()));
884
885 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
887 assert_eq!(stored.sent_at, Some(new_sent_at.clone()));
888 }
889
890 #[tokio::test]
891 async fn test_set_confirmed_at() {
892 let repo = InMemoryTransactionRepository::new();
893 let tx = create_test_transaction("test-1");
894
895 repo.create(tx).await.unwrap();
896
897 let new_confirmed_at = "2025-02-01T11:30:45.123456+00:00".to_string();
899
900 let updated = repo
901 .set_confirmed_at("test-1".to_string(), new_confirmed_at.clone())
902 .await
903 .unwrap();
904
905 assert_eq!(updated.confirmed_at, Some(new_confirmed_at.clone()));
907
908 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
910 assert_eq!(stored.confirmed_at, Some(new_confirmed_at.clone()));
911 }
912
913 #[tokio::test]
914 async fn test_find_by_relayer_id() {
915 let repo = InMemoryTransactionRepository::new();
916 let tx1 = create_test_transaction("test-1");
917 let tx2 = create_test_transaction("test-2");
918
919 let mut tx3 = create_test_transaction("test-3");
921 tx3.relayer_id = "relayer-2".to_string();
922
923 repo.create(tx1).await.unwrap();
924 repo.create(tx2).await.unwrap();
925 repo.create(tx3).await.unwrap();
926
927 let query = PaginationQuery {
929 page: 1,
930 per_page: 10,
931 };
932 let result = repo
933 .find_by_relayer_id("relayer-1", query.clone())
934 .await
935 .unwrap();
936 assert_eq!(result.total, 2);
937 assert_eq!(result.items.len(), 2);
938 assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-1"));
939
940 let result = repo
942 .find_by_relayer_id("relayer-2", query.clone())
943 .await
944 .unwrap();
945 assert_eq!(result.total, 1);
946 assert_eq!(result.items.len(), 1);
947 assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-2"));
948
949 let result = repo
951 .find_by_relayer_id("non-existent", query.clone())
952 .await
953 .unwrap();
954 assert_eq!(result.total, 0);
955 assert_eq!(result.items.len(), 0);
956 }
957
958 #[tokio::test]
959 async fn test_find_by_relayer_id_sorted_by_created_at_newest_first() {
960 let repo = InMemoryTransactionRepository::new();
961
962 let mut tx1 = create_test_transaction("test-1");
964 tx1.created_at = "2025-01-27T10:00:00.000000+00:00".to_string(); let mut tx2 = create_test_transaction("test-2");
967 tx2.created_at = "2025-01-27T12:00:00.000000+00:00".to_string(); let mut tx3 = create_test_transaction("test-3");
970 tx3.created_at = "2025-01-27T14:00:00.000000+00:00".to_string(); repo.create(tx2.clone()).await.unwrap(); repo.create(tx1.clone()).await.unwrap(); repo.create(tx3.clone()).await.unwrap(); let query = PaginationQuery {
978 page: 1,
979 per_page: 10,
980 };
981 let result = repo.find_by_relayer_id("relayer-1", query).await.unwrap();
982
983 assert_eq!(result.total, 3);
984 assert_eq!(result.items.len(), 3);
985
986 assert_eq!(
988 result.items[0].id, "test-3",
989 "First item should be newest (test-3)"
990 );
991 assert_eq!(
992 result.items[0].created_at,
993 "2025-01-27T14:00:00.000000+00:00"
994 );
995
996 assert_eq!(
997 result.items[1].id, "test-2",
998 "Second item should be middle (test-2)"
999 );
1000 assert_eq!(
1001 result.items[1].created_at,
1002 "2025-01-27T12:00:00.000000+00:00"
1003 );
1004
1005 assert_eq!(
1006 result.items[2].id, "test-1",
1007 "Third item should be oldest (test-1)"
1008 );
1009 assert_eq!(
1010 result.items[2].created_at,
1011 "2025-01-27T10:00:00.000000+00:00"
1012 );
1013 }
1014
1015 #[tokio::test]
1016 async fn test_find_by_status() {
1017 let repo = InMemoryTransactionRepository::new();
1018 let tx1 = create_test_transaction_pending_state("tx1");
1019 let mut tx2 = create_test_transaction_pending_state("tx2");
1020 tx2.status = TransactionStatus::Submitted;
1021 let mut tx3 = create_test_transaction_pending_state("tx3");
1022 tx3.relayer_id = "relayer-2".to_string();
1023 tx3.status = TransactionStatus::Pending;
1024
1025 repo.create(tx1.clone()).await.unwrap();
1026 repo.create(tx2.clone()).await.unwrap();
1027 repo.create(tx3.clone()).await.unwrap();
1028
1029 let pending_txs = repo
1031 .find_by_status("relayer-1", &[TransactionStatus::Pending])
1032 .await
1033 .unwrap();
1034 assert_eq!(pending_txs.len(), 1);
1035 assert_eq!(pending_txs[0].id, "tx1");
1036
1037 let submitted_txs = repo
1038 .find_by_status("relayer-1", &[TransactionStatus::Submitted])
1039 .await
1040 .unwrap();
1041 assert_eq!(submitted_txs.len(), 1);
1042 assert_eq!(submitted_txs[0].id, "tx2");
1043
1044 let multiple_status_txs = repo
1046 .find_by_status(
1047 "relayer-1",
1048 &[TransactionStatus::Pending, TransactionStatus::Submitted],
1049 )
1050 .await
1051 .unwrap();
1052 assert_eq!(multiple_status_txs.len(), 2);
1053
1054 let relayer2_pending = repo
1056 .find_by_status("relayer-2", &[TransactionStatus::Pending])
1057 .await
1058 .unwrap();
1059 assert_eq!(relayer2_pending.len(), 1);
1060 assert_eq!(relayer2_pending[0].id, "tx3");
1061
1062 let no_txs = repo
1064 .find_by_status("non-existent", &[TransactionStatus::Pending])
1065 .await
1066 .unwrap();
1067 assert_eq!(no_txs.len(), 0);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_find_by_status_sorted_by_created_at() {
1072 let repo = InMemoryTransactionRepository::new();
1073
1074 let create_tx_with_timestamp = |id: &str, timestamp: &str| -> TransactionRepoModel {
1076 let mut tx = create_test_transaction_pending_state(id);
1077 tx.created_at = timestamp.to_string();
1078 tx.status = TransactionStatus::Pending;
1079 tx
1080 };
1081
1082 let tx3 = create_tx_with_timestamp("tx3", "2025-01-27T17:00:00.000000+00:00"); let tx1 = create_tx_with_timestamp("tx1", "2025-01-27T15:00:00.000000+00:00"); let tx2 = create_tx_with_timestamp("tx2", "2025-01-27T16:00:00.000000+00:00"); repo.create(tx3.clone()).await.unwrap();
1089 repo.create(tx1.clone()).await.unwrap();
1090 repo.create(tx2.clone()).await.unwrap();
1091
1092 let result = repo
1094 .find_by_status("relayer-1", &[TransactionStatus::Pending])
1095 .await
1096 .unwrap();
1097
1098 assert_eq!(result.len(), 3);
1100 assert_eq!(result[0].id, "tx3"); assert_eq!(result[1].id, "tx2"); assert_eq!(result[2].id, "tx1"); assert_eq!(result[0].created_at, "2025-01-27T17:00:00.000000+00:00");
1106 assert_eq!(result[1].created_at, "2025-01-27T16:00:00.000000+00:00");
1107 assert_eq!(result[2].created_at, "2025-01-27T15:00:00.000000+00:00");
1108 }
1109
1110 #[tokio::test]
1111 async fn test_find_by_status_paginated() {
1112 let repo = InMemoryTransactionRepository::new();
1113
1114 let create_tx_with_timestamp =
1116 |id: &str, timestamp: &str, status: TransactionStatus| -> TransactionRepoModel {
1117 let mut tx = create_test_transaction_pending_state(id);
1118 tx.created_at = timestamp.to_string();
1119 tx.status = status;
1120 tx
1121 };
1122
1123 for i in 1..=5 {
1125 let tx = create_tx_with_timestamp(
1126 &format!("tx{i}"),
1127 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1128 TransactionStatus::Pending,
1129 );
1130 repo.create(tx).await.unwrap();
1131 }
1132
1133 for i in 6..=7 {
1135 let tx = create_tx_with_timestamp(
1136 &format!("tx{i}"),
1137 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1138 TransactionStatus::Confirmed,
1139 );
1140 repo.create(tx).await.unwrap();
1141 }
1142
1143 let query = PaginationQuery {
1145 page: 1,
1146 per_page: 2,
1147 };
1148 let result = repo
1149 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1150 .await
1151 .unwrap();
1152
1153 assert_eq!(result.total, 5);
1154 assert_eq!(result.items.len(), 2);
1155 assert_eq!(result.page, 1);
1156 assert_eq!(result.per_page, 2);
1157 assert_eq!(result.items[0].id, "tx5");
1159 assert_eq!(result.items[1].id, "tx4");
1160
1161 let query = PaginationQuery {
1163 page: 2,
1164 per_page: 2,
1165 };
1166 let result = repo
1167 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1168 .await
1169 .unwrap();
1170
1171 assert_eq!(result.total, 5);
1172 assert_eq!(result.items.len(), 2);
1173 assert_eq!(result.page, 2);
1174 assert_eq!(result.items[0].id, "tx3");
1176 assert_eq!(result.items[1].id, "tx2");
1177
1178 let query = PaginationQuery {
1180 page: 3,
1181 per_page: 2,
1182 };
1183 let result = repo
1184 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1185 .await
1186 .unwrap();
1187
1188 assert_eq!(result.total, 5);
1189 assert_eq!(result.items.len(), 1);
1190 assert_eq!(result.page, 3);
1191 assert_eq!(result.items[0].id, "tx1");
1192
1193 let query = PaginationQuery {
1195 page: 10,
1196 per_page: 2,
1197 };
1198 let result = repo
1199 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1200 .await
1201 .unwrap();
1202
1203 assert_eq!(result.total, 5);
1204 assert_eq!(result.items.len(), 0);
1205
1206 let query = PaginationQuery {
1208 page: 1,
1209 per_page: 10,
1210 };
1211 let result = repo
1212 .find_by_status_paginated(
1213 "relayer-1",
1214 &[TransactionStatus::Pending, TransactionStatus::Confirmed],
1215 query,
1216 false,
1217 )
1218 .await
1219 .unwrap();
1220
1221 assert_eq!(result.total, 7);
1222 assert_eq!(result.items.len(), 7);
1223
1224 let query = PaginationQuery {
1226 page: 1,
1227 per_page: 10,
1228 };
1229 let result = repo
1230 .find_by_status_paginated("relayer-1", &[TransactionStatus::Failed], query, false)
1231 .await
1232 .unwrap();
1233
1234 assert_eq!(result.total, 0);
1235 assert_eq!(result.items.len(), 0);
1236 }
1237
1238 #[tokio::test]
1239 async fn test_find_by_status_paginated_oldest_first() {
1240 let repo = InMemoryTransactionRepository::new();
1241
1242 let create_tx_with_timestamp =
1244 |id: &str, timestamp: &str, status: TransactionStatus| -> TransactionRepoModel {
1245 let mut tx = create_test_transaction_pending_state(id);
1246 tx.created_at = timestamp.to_string();
1247 tx.status = status;
1248 tx
1249 };
1250
1251 for i in 1..=5 {
1253 let tx = create_tx_with_timestamp(
1254 &format!("tx{i}"),
1255 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1256 TransactionStatus::Pending,
1257 );
1258 repo.create(tx).await.unwrap();
1259 }
1260
1261 let query = PaginationQuery {
1263 page: 1,
1264 per_page: 3,
1265 };
1266 let result = repo
1267 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, true)
1268 .await
1269 .unwrap();
1270
1271 assert_eq!(result.total, 5);
1272 assert_eq!(result.items.len(), 3);
1273 assert_eq!(
1275 result.items[0].id, "tx1",
1276 "First item should be oldest (tx1)"
1277 );
1278 assert_eq!(result.items[1].id, "tx2", "Second item should be tx2");
1279 assert_eq!(result.items[2].id, "tx3", "Third item should be tx3");
1280
1281 let query = PaginationQuery {
1283 page: 2,
1284 per_page: 3,
1285 };
1286 let result = repo
1287 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, true)
1288 .await
1289 .unwrap();
1290
1291 assert_eq!(result.total, 5);
1292 assert_eq!(result.items.len(), 2);
1293 assert_eq!(result.items[0].id, "tx4");
1295 assert_eq!(result.items[1].id, "tx5");
1296 }
1297
1298 #[tokio::test]
1299 async fn test_find_by_status_paginated_oldest_first_single_item() {
1300 let repo = InMemoryTransactionRepository::new();
1301
1302 let timestamps = [
1304 ("tx-oldest", "2025-01-27T08:00:00.000000+00:00"),
1305 ("tx-middle", "2025-01-27T10:00:00.000000+00:00"),
1306 ("tx-newest", "2025-01-27T12:00:00.000000+00:00"),
1307 ];
1308
1309 for (id, timestamp) in timestamps {
1310 let mut tx = create_test_transaction_pending_state(id);
1311 tx.created_at = timestamp.to_string();
1312 tx.status = TransactionStatus::Pending;
1313 repo.create(tx).await.unwrap();
1314 }
1315
1316 let query = PaginationQuery {
1318 page: 1,
1319 per_page: 1,
1320 };
1321 let result = repo
1322 .find_by_status_paginated(
1323 "relayer-1",
1324 &[TransactionStatus::Pending],
1325 query.clone(),
1326 true,
1327 )
1328 .await
1329 .unwrap();
1330
1331 assert_eq!(result.total, 3);
1332 assert_eq!(result.items.len(), 1);
1333 assert_eq!(
1334 result.items[0].id, "tx-oldest",
1335 "With oldest_first and per_page=1, should return the oldest transaction"
1336 );
1337
1338 let result = repo
1340 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1341 .await
1342 .unwrap();
1343
1344 assert_eq!(result.items.len(), 1);
1345 assert_eq!(
1346 result.items[0].id, "tx-newest",
1347 "With oldest_first=false and per_page=1, should return the newest transaction"
1348 );
1349 }
1350
1351 #[tokio::test]
1352 async fn test_find_by_status_paginated_multi_status_oldest_first() {
1353 let repo = InMemoryTransactionRepository::new();
1354
1355 let transactions = [
1357 (
1358 "tx-pending-old",
1359 "2025-01-27T08:00:00.000000+00:00",
1360 TransactionStatus::Pending,
1361 ),
1362 (
1363 "tx-sent-mid",
1364 "2025-01-27T10:00:00.000000+00:00",
1365 TransactionStatus::Sent,
1366 ),
1367 (
1368 "tx-pending-new",
1369 "2025-01-27T12:00:00.000000+00:00",
1370 TransactionStatus::Pending,
1371 ),
1372 (
1373 "tx-sent-old",
1374 "2025-01-27T07:00:00.000000+00:00",
1375 TransactionStatus::Sent,
1376 ),
1377 ];
1378
1379 for (id, timestamp, status) in transactions {
1380 let mut tx = create_test_transaction_pending_state(id);
1381 tx.created_at = timestamp.to_string();
1382 tx.status = status;
1383 repo.create(tx).await.unwrap();
1384 }
1385
1386 let query = PaginationQuery {
1388 page: 1,
1389 per_page: 10,
1390 };
1391 let result = repo
1392 .find_by_status_paginated(
1393 "relayer-1",
1394 &[TransactionStatus::Pending, TransactionStatus::Sent],
1395 query,
1396 true,
1397 )
1398 .await
1399 .unwrap();
1400
1401 assert_eq!(result.total, 4);
1402 assert_eq!(result.items.len(), 4);
1403 assert_eq!(result.items[0].id, "tx-sent-old", "Oldest should be first");
1405 assert_eq!(result.items[1].id, "tx-pending-old");
1406 assert_eq!(result.items[2].id, "tx-sent-mid");
1407 assert_eq!(
1408 result.items[3].id, "tx-pending-new",
1409 "Newest should be last"
1410 );
1411 }
1412
1413 #[tokio::test]
1414 async fn test_has_entries() {
1415 let repo = InMemoryTransactionRepository::new();
1416 assert!(!repo.has_entries().await.unwrap());
1417
1418 let tx = create_test_transaction("test");
1419 repo.create(tx.clone()).await.unwrap();
1420
1421 assert!(repo.has_entries().await.unwrap());
1422 }
1423
1424 #[tokio::test]
1425 async fn test_drop_all_entries() {
1426 let repo = InMemoryTransactionRepository::new();
1427 let tx = create_test_transaction("test");
1428 repo.create(tx.clone()).await.unwrap();
1429
1430 assert!(repo.has_entries().await.unwrap());
1431
1432 repo.drop_all_entries().await.unwrap();
1433 assert!(!repo.has_entries().await.unwrap());
1434 }
1435
1436 #[tokio::test]
1439 async fn test_update_status_sets_delete_at_for_final_statuses() {
1440 let _lock = ENV_MUTEX.lock().await;
1441
1442 use chrono::{DateTime, Duration, Utc};
1443 use std::env;
1444
1445 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
1447
1448 let repo = InMemoryTransactionRepository::new();
1449
1450 let final_statuses = [
1451 TransactionStatus::Canceled,
1452 TransactionStatus::Confirmed,
1453 TransactionStatus::Failed,
1454 TransactionStatus::Expired,
1455 ];
1456
1457 for (i, status) in final_statuses.iter().enumerate() {
1458 let tx_id = format!("test-final-{i}");
1459 let tx = create_test_transaction_pending_state(&tx_id);
1460
1461 assert!(tx.delete_at.is_none());
1463
1464 repo.create(tx).await.unwrap();
1465
1466 let before_update = Utc::now();
1467
1468 let updated = repo
1470 .update_status(tx_id.clone(), status.clone())
1471 .await
1472 .unwrap();
1473
1474 assert!(
1476 updated.delete_at.is_some(),
1477 "delete_at should be set for status: {status:?}"
1478 );
1479
1480 let delete_at_str = updated.delete_at.unwrap();
1482 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
1483 .expect("delete_at should be valid RFC3339")
1484 .with_timezone(&Utc);
1485
1486 let duration_from_before = delete_at.signed_duration_since(before_update);
1487 let expected_duration = Duration::hours(6);
1488 let tolerance = Duration::minutes(5);
1489
1490 assert!(
1491 duration_from_before >= expected_duration - tolerance &&
1492 duration_from_before <= expected_duration + tolerance,
1493 "delete_at should be approximately 6 hours from now for status: {status:?}. Duration: {duration_from_before:?}"
1494 );
1495 }
1496
1497 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1499 }
1500
1501 #[tokio::test]
1502 async fn test_update_status_does_not_set_delete_at_for_non_final_statuses() {
1503 let _lock = ENV_MUTEX.lock().await;
1504
1505 use std::env;
1506
1507 env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
1508
1509 let repo = InMemoryTransactionRepository::new();
1510
1511 let non_final_statuses = [
1512 TransactionStatus::Pending,
1513 TransactionStatus::Sent,
1514 TransactionStatus::Submitted,
1515 TransactionStatus::Mined,
1516 ];
1517
1518 for (i, status) in non_final_statuses.iter().enumerate() {
1519 let tx_id = format!("test-non-final-{i}");
1520 let tx = create_test_transaction_pending_state(&tx_id);
1521
1522 repo.create(tx).await.unwrap();
1523
1524 let updated = repo
1526 .update_status(tx_id.clone(), status.clone())
1527 .await
1528 .unwrap();
1529
1530 assert!(
1532 updated.delete_at.is_none(),
1533 "delete_at should NOT be set for status: {status:?}"
1534 );
1535 }
1536
1537 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1539 }
1540
1541 #[tokio::test]
1542 async fn test_partial_update_sets_delete_at_for_final_statuses() {
1543 let _lock = ENV_MUTEX.lock().await;
1544
1545 use chrono::{DateTime, Duration, Utc};
1546 use std::env;
1547
1548 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
1549
1550 let repo = InMemoryTransactionRepository::new();
1551 let tx = create_test_transaction_pending_state("test-partial-final");
1552
1553 repo.create(tx).await.unwrap();
1554
1555 let before_update = Utc::now();
1556
1557 let update = TransactionUpdateRequest {
1559 status: Some(TransactionStatus::Confirmed),
1560 status_reason: Some("Transaction completed".to_string()),
1561 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
1562 ..Default::default()
1563 };
1564
1565 let updated = repo
1566 .partial_update("test-partial-final".to_string(), update)
1567 .await
1568 .unwrap();
1569
1570 assert!(
1572 updated.delete_at.is_some(),
1573 "delete_at should be set when updating to Confirmed status"
1574 );
1575
1576 let delete_at_str = updated.delete_at.unwrap();
1578 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
1579 .expect("delete_at should be valid RFC3339")
1580 .with_timezone(&Utc);
1581
1582 let duration_from_before = delete_at.signed_duration_since(before_update);
1583 let expected_duration = Duration::hours(8);
1584 let tolerance = Duration::minutes(5);
1585
1586 assert!(
1587 duration_from_before >= expected_duration - tolerance
1588 && duration_from_before <= expected_duration + tolerance,
1589 "delete_at should be approximately 8 hours from now. Duration: {duration_from_before:?}"
1590 );
1591
1592 assert_eq!(updated.status, TransactionStatus::Confirmed);
1594 assert_eq!(
1595 updated.status_reason,
1596 Some("Transaction completed".to_string())
1597 );
1598 assert_eq!(
1599 updated.confirmed_at,
1600 Some("2023-01-01T12:05:00Z".to_string())
1601 );
1602
1603 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1605 }
1606
1607 #[tokio::test]
1608 async fn test_update_status_preserves_existing_delete_at() {
1609 let _lock = ENV_MUTEX.lock().await;
1610
1611 use std::env;
1612
1613 env::set_var("TRANSACTION_EXPIRATION_HOURS", "2");
1614
1615 let repo = InMemoryTransactionRepository::new();
1616 let mut tx = create_test_transaction_pending_state("test-preserve-delete-at");
1617
1618 let existing_delete_at = "2025-01-01T12:00:00Z".to_string();
1620 tx.delete_at = Some(existing_delete_at.clone());
1621
1622 repo.create(tx).await.unwrap();
1623
1624 let updated = repo
1626 .update_status(
1627 "test-preserve-delete-at".to_string(),
1628 TransactionStatus::Confirmed,
1629 )
1630 .await
1631 .unwrap();
1632
1633 assert_eq!(
1635 updated.delete_at,
1636 Some(existing_delete_at),
1637 "Existing delete_at should be preserved when updating to final status"
1638 );
1639
1640 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1642 }
1643
1644 #[tokio::test]
1645 async fn test_partial_update_without_status_change_preserves_delete_at() {
1646 let _lock = ENV_MUTEX.lock().await;
1647
1648 use std::env;
1649
1650 env::set_var("TRANSACTION_EXPIRATION_HOURS", "3");
1651
1652 let repo = InMemoryTransactionRepository::new();
1653 let tx = create_test_transaction_pending_state("test-preserve-no-status");
1654
1655 repo.create(tx).await.unwrap();
1656
1657 let updated1 = repo
1659 .update_status(
1660 "test-preserve-no-status".to_string(),
1661 TransactionStatus::Confirmed,
1662 )
1663 .await
1664 .unwrap();
1665
1666 assert!(updated1.delete_at.is_some());
1667 let original_delete_at = updated1.delete_at.clone();
1668
1669 let update = TransactionUpdateRequest {
1671 status: None, status_reason: Some("Updated reason".to_string()),
1673 confirmed_at: Some("2023-01-01T12:10:00Z".to_string()),
1674 ..Default::default()
1675 };
1676
1677 let updated2 = repo
1678 .partial_update("test-preserve-no-status".to_string(), update)
1679 .await
1680 .unwrap();
1681
1682 assert_eq!(
1684 updated2.delete_at, original_delete_at,
1685 "delete_at should be preserved when status is not updated"
1686 );
1687
1688 assert_eq!(updated2.status, TransactionStatus::Confirmed); assert_eq!(updated2.status_reason, Some("Updated reason".to_string()));
1691 assert_eq!(
1692 updated2.confirmed_at,
1693 Some("2023-01-01T12:10:00Z".to_string())
1694 );
1695
1696 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1698 }
1699
1700 #[tokio::test]
1701 async fn test_update_status_multiple_updates_idempotent() {
1702 let _lock = ENV_MUTEX.lock().await;
1703
1704 use std::env;
1705
1706 env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
1707
1708 let repo = InMemoryTransactionRepository::new();
1709 let tx = create_test_transaction_pending_state("test-idempotent");
1710
1711 repo.create(tx).await.unwrap();
1712
1713 let updated1 = repo
1715 .update_status("test-idempotent".to_string(), TransactionStatus::Confirmed)
1716 .await
1717 .unwrap();
1718
1719 assert!(updated1.delete_at.is_some());
1720 let first_delete_at = updated1.delete_at.clone();
1721
1722 let updated2 = repo
1724 .update_status("test-idempotent".to_string(), TransactionStatus::Failed)
1725 .await
1726 .unwrap();
1727
1728 assert_eq!(
1730 updated2.delete_at, first_delete_at,
1731 "delete_at should not change on subsequent final status updates"
1732 );
1733
1734 assert_eq!(updated2.status, TransactionStatus::Failed);
1736
1737 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1739 }
1740
1741 #[tokio::test]
1744 async fn test_delete_by_ids_empty_list() {
1745 let repo = InMemoryTransactionRepository::new();
1746
1747 let tx = create_test_transaction("test-1");
1749 repo.create(tx).await.unwrap();
1750
1751 let result = repo.delete_by_ids(vec![]).await.unwrap();
1753
1754 assert_eq!(result.deleted_count, 0);
1755 assert!(result.failed.is_empty());
1756
1757 assert!(repo.get_by_id("test-1".to_string()).await.is_ok());
1759 }
1760
1761 #[tokio::test]
1762 async fn test_delete_by_ids_single_transaction() {
1763 let repo = InMemoryTransactionRepository::new();
1764
1765 let tx = create_test_transaction("test-1");
1766 repo.create(tx).await.unwrap();
1767
1768 let result = repo
1769 .delete_by_ids(vec!["test-1".to_string()])
1770 .await
1771 .unwrap();
1772
1773 assert_eq!(result.deleted_count, 1);
1774 assert!(result.failed.is_empty());
1775
1776 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1778 }
1779
1780 #[tokio::test]
1781 async fn test_delete_by_ids_multiple_transactions() {
1782 let repo = InMemoryTransactionRepository::new();
1783
1784 for i in 1..=5 {
1786 let tx = create_test_transaction(&format!("test-{i}"));
1787 repo.create(tx).await.unwrap();
1788 }
1789
1790 assert_eq!(repo.count().await.unwrap(), 5);
1791
1792 let ids_to_delete = vec![
1794 "test-1".to_string(),
1795 "test-3".to_string(),
1796 "test-5".to_string(),
1797 ];
1798 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1799
1800 assert_eq!(result.deleted_count, 3);
1801 assert!(result.failed.is_empty());
1802
1803 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1805 assert!(repo.get_by_id("test-2".to_string()).await.is_ok()); assert!(repo.get_by_id("test-3".to_string()).await.is_err());
1807 assert!(repo.get_by_id("test-4".to_string()).await.is_ok()); assert!(repo.get_by_id("test-5".to_string()).await.is_err());
1809
1810 assert_eq!(repo.count().await.unwrap(), 2);
1811 }
1812
1813 #[tokio::test]
1814 async fn test_delete_by_ids_nonexistent_transactions() {
1815 let repo = InMemoryTransactionRepository::new();
1816
1817 let ids_to_delete = vec!["nonexistent-1".to_string(), "nonexistent-2".to_string()];
1819 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1820
1821 assert_eq!(result.deleted_count, 0);
1822 assert_eq!(result.failed.len(), 2);
1823
1824 assert!(result.failed.iter().any(|(id, _)| id == "nonexistent-1"));
1826 assert!(result.failed.iter().any(|(id, _)| id == "nonexistent-2"));
1827 }
1828
1829 #[tokio::test]
1830 async fn test_delete_by_ids_mixed_existing_and_nonexistent() {
1831 let repo = InMemoryTransactionRepository::new();
1832
1833 for i in 1..=3 {
1835 let tx = create_test_transaction(&format!("test-{i}"));
1836 repo.create(tx).await.unwrap();
1837 }
1838
1839 let ids_to_delete = vec![
1841 "test-1".to_string(), "nonexistent-1".to_string(), "test-2".to_string(), "nonexistent-2".to_string(), ];
1846 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1847
1848 assert_eq!(result.deleted_count, 2);
1849 assert_eq!(result.failed.len(), 2);
1850
1851 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1853 assert!(repo.get_by_id("test-2".to_string()).await.is_err());
1854
1855 assert!(repo.get_by_id("test-3".to_string()).await.is_ok());
1857
1858 let failed_ids: Vec<&String> = result.failed.iter().map(|(id, _)| id).collect();
1860 assert!(failed_ids.contains(&&"nonexistent-1".to_string()));
1861 assert!(failed_ids.contains(&&"nonexistent-2".to_string()));
1862 }
1863
1864 #[tokio::test]
1865 async fn test_delete_by_ids_all_transactions() {
1866 let repo = InMemoryTransactionRepository::new();
1867
1868 for i in 1..=10 {
1870 let tx = create_test_transaction(&format!("test-{i}"));
1871 repo.create(tx).await.unwrap();
1872 }
1873
1874 assert_eq!(repo.count().await.unwrap(), 10);
1875
1876 let ids_to_delete: Vec<String> = (1..=10).map(|i| format!("test-{i}")).collect();
1878 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1879
1880 assert_eq!(result.deleted_count, 10);
1881 assert!(result.failed.is_empty());
1882 assert_eq!(repo.count().await.unwrap(), 0);
1883 assert!(!repo.has_entries().await.unwrap());
1884 }
1885
1886 #[tokio::test]
1887 async fn test_delete_by_ids_duplicate_ids() {
1888 let repo = InMemoryTransactionRepository::new();
1889
1890 let tx = create_test_transaction("test-1");
1891 repo.create(tx).await.unwrap();
1892
1893 let ids_to_delete = vec![
1895 "test-1".to_string(),
1896 "test-1".to_string(), "test-1".to_string(), ];
1899 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1900
1901 assert_eq!(result.deleted_count, 1);
1903 assert_eq!(result.failed.len(), 2);
1904
1905 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1907 }
1908
1909 #[tokio::test]
1910 async fn test_delete_by_ids_preserves_other_relayer_transactions() {
1911 let repo = InMemoryTransactionRepository::new();
1912
1913 let mut tx1 = create_test_transaction("tx-relayer-1");
1915 tx1.relayer_id = "relayer-1".to_string();
1916
1917 let mut tx2 = create_test_transaction("tx-relayer-2");
1918 tx2.relayer_id = "relayer-2".to_string();
1919
1920 repo.create(tx1).await.unwrap();
1921 repo.create(tx2).await.unwrap();
1922
1923 let result = repo
1925 .delete_by_ids(vec!["tx-relayer-1".to_string()])
1926 .await
1927 .unwrap();
1928
1929 assert_eq!(result.deleted_count, 1);
1930
1931 let remaining = repo.get_by_id("tx-relayer-2".to_string()).await.unwrap();
1933 assert_eq!(remaining.relayer_id, "relayer-2");
1934 }
1935}