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 }
478 }
479
480 fn create_test_transaction_pending_state(id: &str) -> TransactionRepoModel {
481 TransactionRepoModel {
482 id: id.to_string(),
483 relayer_id: "relayer-1".to_string(),
484 status: TransactionStatus::Pending,
485 status_reason: None,
486 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
487 sent_at: None,
488 confirmed_at: None,
489 valid_until: None,
490 delete_at: None,
491 network_type: NetworkType::Evm,
492 priced_at: None,
493 hashes: vec![],
494 network_data: NetworkTransactionData::Evm(EvmTransactionData {
495 gas_price: Some(1000000000),
496 gas_limit: Some(21000),
497 nonce: Some(1),
498 value: U256::from_str("1000000000000000000").unwrap(),
499 data: Some("0x".to_string()),
500 from: "0xSender".to_string(),
501 to: Some("0xRecipient".to_string()),
502 chain_id: 1,
503 signature: None,
504 hash: Some(format!("0x{}", id)),
505 speed: Some(Speed::Fast),
506 max_fee_per_gas: None,
507 max_priority_fee_per_gas: None,
508 raw: None,
509 }),
510 noop_count: None,
511 is_canceled: Some(false),
512 }
513 }
514
515 #[tokio::test]
516 async fn test_create_transaction() {
517 let repo = InMemoryTransactionRepository::new();
518 let tx = create_test_transaction("test-1");
519
520 let result = repo.create(tx.clone()).await.unwrap();
521 assert_eq!(result.id, tx.id);
522 assert_eq!(repo.count().await.unwrap(), 1);
523 }
524
525 #[tokio::test]
526 async fn test_get_transaction() {
527 let repo = InMemoryTransactionRepository::new();
528 let tx = create_test_transaction("test-1");
529
530 repo.create(tx.clone()).await.unwrap();
531 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
532 if let NetworkTransactionData::Evm(stored_data) = &stored.network_data {
533 if let NetworkTransactionData::Evm(tx_data) = &tx.network_data {
534 assert_eq!(stored_data.hash, tx_data.hash);
535 }
536 }
537 }
538
539 #[tokio::test]
540 async fn test_update_transaction() {
541 let repo = InMemoryTransactionRepository::new();
542 let mut tx = create_test_transaction("test-1");
543
544 repo.create(tx.clone()).await.unwrap();
545 tx.status = TransactionStatus::Confirmed;
546
547 let updated = repo.update("test-1".to_string(), tx).await.unwrap();
548 assert!(matches!(updated.status, TransactionStatus::Confirmed));
549 }
550
551 #[tokio::test]
552 async fn test_delete_transaction() {
553 let repo = InMemoryTransactionRepository::new();
554 let tx = create_test_transaction("test-1");
555
556 repo.create(tx).await.unwrap();
557 repo.delete_by_id("test-1".to_string()).await.unwrap();
558
559 let result = repo.get_by_id("test-1".to_string()).await;
560 assert!(result.is_err());
561 }
562
563 #[tokio::test]
564 async fn test_list_all_transactions() {
565 let repo = InMemoryTransactionRepository::new();
566 let tx1 = create_test_transaction("test-1");
567 let tx2 = create_test_transaction("test-2");
568
569 repo.create(tx1).await.unwrap();
570 repo.create(tx2).await.unwrap();
571
572 let transactions = repo.list_all().await.unwrap();
573 assert_eq!(transactions.len(), 2);
574 }
575
576 #[tokio::test]
577 async fn test_count_transactions() {
578 let repo = InMemoryTransactionRepository::new();
579 let tx = create_test_transaction("test-1");
580
581 assert_eq!(repo.count().await.unwrap(), 0);
582 repo.create(tx).await.unwrap();
583 assert_eq!(repo.count().await.unwrap(), 1);
584 }
585
586 #[tokio::test]
587 async fn test_get_nonexistent_transaction() {
588 let repo = InMemoryTransactionRepository::new();
589 let result = repo.get_by_id("nonexistent".to_string()).await;
590 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
591 }
592
593 #[tokio::test]
594 async fn test_duplicate_transaction_creation() {
595 let repo = InMemoryTransactionRepository::new();
596 let tx = create_test_transaction("test-1");
597
598 repo.create(tx.clone()).await.unwrap();
599 let result = repo.create(tx).await;
600
601 assert!(matches!(
602 result,
603 Err(RepositoryError::ConstraintViolation(_))
604 ));
605 }
606
607 #[tokio::test]
608 async fn test_update_nonexistent_transaction() {
609 let repo = InMemoryTransactionRepository::new();
610 let tx = create_test_transaction("test-1");
611
612 let result = repo.update("nonexistent".to_string(), tx).await;
613 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
614 }
615
616 #[tokio::test]
617 async fn test_partial_update() {
618 let repo = InMemoryTransactionRepository::new();
619 let tx = create_test_transaction_pending_state("test-tx-id");
620 repo.create(tx.clone()).await.unwrap();
621
622 let update1 = TransactionUpdateRequest {
624 status: Some(TransactionStatus::Sent),
625 status_reason: None,
626 sent_at: None,
627 confirmed_at: None,
628 network_data: None,
629 hashes: None,
630 priced_at: None,
631 noop_count: None,
632 is_canceled: None,
633 delete_at: None,
634 };
635 let updated_tx1 = repo
636 .partial_update("test-tx-id".to_string(), update1)
637 .await
638 .unwrap();
639 assert_eq!(updated_tx1.status, TransactionStatus::Sent);
640 assert_eq!(updated_tx1.sent_at, None);
641
642 let update2 = TransactionUpdateRequest {
644 status: Some(TransactionStatus::Confirmed),
645 status_reason: None,
646 sent_at: Some("2023-01-01T12:00:00Z".to_string()),
647 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
648 network_data: None,
649 hashes: None,
650 priced_at: None,
651 noop_count: None,
652 is_canceled: None,
653 delete_at: None,
654 };
655 let updated_tx2 = repo
656 .partial_update("test-tx-id".to_string(), update2)
657 .await
658 .unwrap();
659 assert_eq!(updated_tx2.status, TransactionStatus::Confirmed);
660 assert_eq!(
661 updated_tx2.sent_at,
662 Some("2023-01-01T12:00:00Z".to_string())
663 );
664 assert_eq!(
665 updated_tx2.confirmed_at,
666 Some("2023-01-01T12:05:00Z".to_string())
667 );
668
669 let update3 = TransactionUpdateRequest {
671 status: Some(TransactionStatus::Failed),
672 status_reason: None,
673 sent_at: None,
674 confirmed_at: None,
675 network_data: None,
676 hashes: None,
677 priced_at: None,
678 noop_count: None,
679 is_canceled: None,
680 delete_at: None,
681 };
682 let result = repo
683 .partial_update("non-existent-id".to_string(), update3)
684 .await;
685 assert!(result.is_err());
686 assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
687 }
688
689 #[tokio::test]
690 async fn test_update_status() {
691 let repo = InMemoryTransactionRepository::new();
692 let tx = create_test_transaction("test-1");
693
694 repo.create(tx).await.unwrap();
695
696 let updated = repo
698 .update_status("test-1".to_string(), TransactionStatus::Confirmed)
699 .await
700 .unwrap();
701
702 assert_eq!(updated.status, TransactionStatus::Confirmed);
704
705 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
707 assert_eq!(stored.status, TransactionStatus::Confirmed);
708
709 let updated = repo
711 .update_status("test-1".to_string(), TransactionStatus::Failed)
712 .await
713 .unwrap();
714
715 assert_eq!(updated.status, TransactionStatus::Failed);
717
718 let result = repo
720 .update_status("non-existent".to_string(), TransactionStatus::Confirmed)
721 .await;
722 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
723 }
724
725 #[tokio::test]
726 async fn test_list_paginated() {
727 let repo = InMemoryTransactionRepository::new();
728
729 for i in 1..=10 {
731 let tx = create_test_transaction(&format!("test-{}", i));
732 repo.create(tx).await.unwrap();
733 }
734
735 let query = PaginationQuery {
737 page: 1,
738 per_page: 3,
739 };
740 let result = repo.list_paginated(query).await.unwrap();
741 assert_eq!(result.items.len(), 3);
742 assert_eq!(result.total, 10);
743 assert_eq!(result.page, 1);
744 assert_eq!(result.per_page, 3);
745
746 let query = PaginationQuery {
748 page: 2,
749 per_page: 3,
750 };
751 let result = repo.list_paginated(query).await.unwrap();
752 assert_eq!(result.items.len(), 3);
753 assert_eq!(result.total, 10);
754 assert_eq!(result.page, 2);
755 assert_eq!(result.per_page, 3);
756
757 let query = PaginationQuery {
759 page: 4,
760 per_page: 3,
761 };
762 let result = repo.list_paginated(query).await.unwrap();
763 assert_eq!(result.items.len(), 1);
764 assert_eq!(result.total, 10);
765 assert_eq!(result.page, 4);
766 assert_eq!(result.per_page, 3);
767
768 let query = PaginationQuery {
770 page: 5,
771 per_page: 3,
772 };
773 let result = repo.list_paginated(query).await.unwrap();
774 assert_eq!(result.items.len(), 0);
775 assert_eq!(result.total, 10);
776 }
777
778 #[tokio::test]
779 async fn test_find_by_nonce() {
780 let repo = InMemoryTransactionRepository::new();
781
782 let tx1 = create_test_transaction("test-1");
784
785 let mut tx2 = create_test_transaction("test-2");
786 if let NetworkTransactionData::Evm(ref mut data) = tx2.network_data {
787 data.nonce = Some(2);
788 }
789
790 let mut tx3 = create_test_transaction("test-3");
791 tx3.relayer_id = "relayer-2".to_string();
792 if let NetworkTransactionData::Evm(ref mut data) = tx3.network_data {
793 data.nonce = Some(1);
794 }
795
796 repo.create(tx1).await.unwrap();
797 repo.create(tx2).await.unwrap();
798 repo.create(tx3).await.unwrap();
799
800 let result = repo.find_by_nonce("relayer-1", 1).await.unwrap();
802 assert!(result.is_some());
803 assert_eq!(result.as_ref().unwrap().id, "test-1");
804
805 let result = repo.find_by_nonce("relayer-1", 2).await.unwrap();
807 assert!(result.is_some());
808 assert_eq!(result.as_ref().unwrap().id, "test-2");
809
810 let result = repo.find_by_nonce("relayer-2", 1).await.unwrap();
812 assert!(result.is_some());
813 assert_eq!(result.as_ref().unwrap().id, "test-3");
814
815 let result = repo.find_by_nonce("relayer-1", 99).await.unwrap();
817 assert!(result.is_none());
818 }
819
820 #[tokio::test]
821 async fn test_update_network_data() {
822 let repo = InMemoryTransactionRepository::new();
823 let tx = create_test_transaction("test-1");
824
825 repo.create(tx.clone()).await.unwrap();
826
827 let updated_network_data = NetworkTransactionData::Evm(EvmTransactionData {
829 gas_price: Some(2000000000),
830 gas_limit: Some(30000),
831 nonce: Some(2),
832 value: U256::from_str("2000000000000000000").unwrap(),
833 data: Some("0xUpdated".to_string()),
834 from: "0xSender".to_string(),
835 to: Some("0xRecipient".to_string()),
836 chain_id: 1,
837 signature: None,
838 hash: Some("0xUpdated".to_string()),
839 raw: None,
840 speed: None,
841 max_fee_per_gas: None,
842 max_priority_fee_per_gas: None,
843 });
844
845 let updated = repo
846 .update_network_data("test-1".to_string(), updated_network_data)
847 .await
848 .unwrap();
849
850 if let NetworkTransactionData::Evm(data) = &updated.network_data {
852 assert_eq!(data.gas_price, Some(2000000000));
853 assert_eq!(data.gas_limit, Some(30000));
854 assert_eq!(data.nonce, Some(2));
855 assert_eq!(data.hash, Some("0xUpdated".to_string()));
856 assert_eq!(data.data, Some("0xUpdated".to_string()));
857 } else {
858 panic!("Expected EVM network data");
859 }
860 }
861
862 #[tokio::test]
863 async fn test_set_sent_at() {
864 let repo = InMemoryTransactionRepository::new();
865 let tx = create_test_transaction("test-1");
866
867 repo.create(tx).await.unwrap();
868
869 let new_sent_at = "2025-02-01T10:00:00.000000+00:00".to_string();
871
872 let updated = repo
873 .set_sent_at("test-1".to_string(), new_sent_at.clone())
874 .await
875 .unwrap();
876
877 assert_eq!(updated.sent_at, Some(new_sent_at.clone()));
879
880 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
882 assert_eq!(stored.sent_at, Some(new_sent_at.clone()));
883 }
884
885 #[tokio::test]
886 async fn test_set_confirmed_at() {
887 let repo = InMemoryTransactionRepository::new();
888 let tx = create_test_transaction("test-1");
889
890 repo.create(tx).await.unwrap();
891
892 let new_confirmed_at = "2025-02-01T11:30:45.123456+00:00".to_string();
894
895 let updated = repo
896 .set_confirmed_at("test-1".to_string(), new_confirmed_at.clone())
897 .await
898 .unwrap();
899
900 assert_eq!(updated.confirmed_at, Some(new_confirmed_at.clone()));
902
903 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
905 assert_eq!(stored.confirmed_at, Some(new_confirmed_at.clone()));
906 }
907
908 #[tokio::test]
909 async fn test_find_by_relayer_id() {
910 let repo = InMemoryTransactionRepository::new();
911 let tx1 = create_test_transaction("test-1");
912 let tx2 = create_test_transaction("test-2");
913
914 let mut tx3 = create_test_transaction("test-3");
916 tx3.relayer_id = "relayer-2".to_string();
917
918 repo.create(tx1).await.unwrap();
919 repo.create(tx2).await.unwrap();
920 repo.create(tx3).await.unwrap();
921
922 let query = PaginationQuery {
924 page: 1,
925 per_page: 10,
926 };
927 let result = repo
928 .find_by_relayer_id("relayer-1", query.clone())
929 .await
930 .unwrap();
931 assert_eq!(result.total, 2);
932 assert_eq!(result.items.len(), 2);
933 assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-1"));
934
935 let result = repo
937 .find_by_relayer_id("relayer-2", query.clone())
938 .await
939 .unwrap();
940 assert_eq!(result.total, 1);
941 assert_eq!(result.items.len(), 1);
942 assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-2"));
943
944 let result = repo
946 .find_by_relayer_id("non-existent", query.clone())
947 .await
948 .unwrap();
949 assert_eq!(result.total, 0);
950 assert_eq!(result.items.len(), 0);
951 }
952
953 #[tokio::test]
954 async fn test_find_by_relayer_id_sorted_by_created_at_newest_first() {
955 let repo = InMemoryTransactionRepository::new();
956
957 let mut tx1 = create_test_transaction("test-1");
959 tx1.created_at = "2025-01-27T10:00:00.000000+00:00".to_string(); let mut tx2 = create_test_transaction("test-2");
962 tx2.created_at = "2025-01-27T12:00:00.000000+00:00".to_string(); let mut tx3 = create_test_transaction("test-3");
965 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 {
973 page: 1,
974 per_page: 10,
975 };
976 let result = repo.find_by_relayer_id("relayer-1", query).await.unwrap();
977
978 assert_eq!(result.total, 3);
979 assert_eq!(result.items.len(), 3);
980
981 assert_eq!(
983 result.items[0].id, "test-3",
984 "First item should be newest (test-3)"
985 );
986 assert_eq!(
987 result.items[0].created_at,
988 "2025-01-27T14:00:00.000000+00:00"
989 );
990
991 assert_eq!(
992 result.items[1].id, "test-2",
993 "Second item should be middle (test-2)"
994 );
995 assert_eq!(
996 result.items[1].created_at,
997 "2025-01-27T12:00:00.000000+00:00"
998 );
999
1000 assert_eq!(
1001 result.items[2].id, "test-1",
1002 "Third item should be oldest (test-1)"
1003 );
1004 assert_eq!(
1005 result.items[2].created_at,
1006 "2025-01-27T10:00:00.000000+00:00"
1007 );
1008 }
1009
1010 #[tokio::test]
1011 async fn test_find_by_status() {
1012 let repo = InMemoryTransactionRepository::new();
1013 let tx1 = create_test_transaction_pending_state("tx1");
1014 let mut tx2 = create_test_transaction_pending_state("tx2");
1015 tx2.status = TransactionStatus::Submitted;
1016 let mut tx3 = create_test_transaction_pending_state("tx3");
1017 tx3.relayer_id = "relayer-2".to_string();
1018 tx3.status = TransactionStatus::Pending;
1019
1020 repo.create(tx1.clone()).await.unwrap();
1021 repo.create(tx2.clone()).await.unwrap();
1022 repo.create(tx3.clone()).await.unwrap();
1023
1024 let pending_txs = repo
1026 .find_by_status("relayer-1", &[TransactionStatus::Pending])
1027 .await
1028 .unwrap();
1029 assert_eq!(pending_txs.len(), 1);
1030 assert_eq!(pending_txs[0].id, "tx1");
1031
1032 let submitted_txs = repo
1033 .find_by_status("relayer-1", &[TransactionStatus::Submitted])
1034 .await
1035 .unwrap();
1036 assert_eq!(submitted_txs.len(), 1);
1037 assert_eq!(submitted_txs[0].id, "tx2");
1038
1039 let multiple_status_txs = repo
1041 .find_by_status(
1042 "relayer-1",
1043 &[TransactionStatus::Pending, TransactionStatus::Submitted],
1044 )
1045 .await
1046 .unwrap();
1047 assert_eq!(multiple_status_txs.len(), 2);
1048
1049 let relayer2_pending = repo
1051 .find_by_status("relayer-2", &[TransactionStatus::Pending])
1052 .await
1053 .unwrap();
1054 assert_eq!(relayer2_pending.len(), 1);
1055 assert_eq!(relayer2_pending[0].id, "tx3");
1056
1057 let no_txs = repo
1059 .find_by_status("non-existent", &[TransactionStatus::Pending])
1060 .await
1061 .unwrap();
1062 assert_eq!(no_txs.len(), 0);
1063 }
1064
1065 #[tokio::test]
1066 async fn test_find_by_status_sorted_by_created_at() {
1067 let repo = InMemoryTransactionRepository::new();
1068
1069 let create_tx_with_timestamp = |id: &str, timestamp: &str| -> TransactionRepoModel {
1071 let mut tx = create_test_transaction_pending_state(id);
1072 tx.created_at = timestamp.to_string();
1073 tx.status = TransactionStatus::Pending;
1074 tx
1075 };
1076
1077 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();
1084 repo.create(tx1.clone()).await.unwrap();
1085 repo.create(tx2.clone()).await.unwrap();
1086
1087 let result = repo
1089 .find_by_status("relayer-1", &[TransactionStatus::Pending])
1090 .await
1091 .unwrap();
1092
1093 assert_eq!(result.len(), 3);
1095 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");
1101 assert_eq!(result[1].created_at, "2025-01-27T16:00:00.000000+00:00");
1102 assert_eq!(result[2].created_at, "2025-01-27T15:00:00.000000+00:00");
1103 }
1104
1105 #[tokio::test]
1106 async fn test_find_by_status_paginated() {
1107 let repo = InMemoryTransactionRepository::new();
1108
1109 let create_tx_with_timestamp =
1111 |id: &str, timestamp: &str, status: TransactionStatus| -> TransactionRepoModel {
1112 let mut tx = create_test_transaction_pending_state(id);
1113 tx.created_at = timestamp.to_string();
1114 tx.status = status;
1115 tx
1116 };
1117
1118 for i in 1..=5 {
1120 let tx = create_tx_with_timestamp(
1121 &format!("tx{}", i),
1122 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1123 TransactionStatus::Pending,
1124 );
1125 repo.create(tx).await.unwrap();
1126 }
1127
1128 for i in 6..=7 {
1130 let tx = create_tx_with_timestamp(
1131 &format!("tx{}", i),
1132 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1133 TransactionStatus::Confirmed,
1134 );
1135 repo.create(tx).await.unwrap();
1136 }
1137
1138 let query = PaginationQuery {
1140 page: 1,
1141 per_page: 2,
1142 };
1143 let result = repo
1144 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1145 .await
1146 .unwrap();
1147
1148 assert_eq!(result.total, 5);
1149 assert_eq!(result.items.len(), 2);
1150 assert_eq!(result.page, 1);
1151 assert_eq!(result.per_page, 2);
1152 assert_eq!(result.items[0].id, "tx5");
1154 assert_eq!(result.items[1].id, "tx4");
1155
1156 let query = PaginationQuery {
1158 page: 2,
1159 per_page: 2,
1160 };
1161 let result = repo
1162 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1163 .await
1164 .unwrap();
1165
1166 assert_eq!(result.total, 5);
1167 assert_eq!(result.items.len(), 2);
1168 assert_eq!(result.page, 2);
1169 assert_eq!(result.items[0].id, "tx3");
1171 assert_eq!(result.items[1].id, "tx2");
1172
1173 let query = PaginationQuery {
1175 page: 3,
1176 per_page: 2,
1177 };
1178 let result = repo
1179 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1180 .await
1181 .unwrap();
1182
1183 assert_eq!(result.total, 5);
1184 assert_eq!(result.items.len(), 1);
1185 assert_eq!(result.page, 3);
1186 assert_eq!(result.items[0].id, "tx1");
1187
1188 let query = PaginationQuery {
1190 page: 10,
1191 per_page: 2,
1192 };
1193 let result = repo
1194 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1195 .await
1196 .unwrap();
1197
1198 assert_eq!(result.total, 5);
1199 assert_eq!(result.items.len(), 0);
1200
1201 let query = PaginationQuery {
1203 page: 1,
1204 per_page: 10,
1205 };
1206 let result = repo
1207 .find_by_status_paginated(
1208 "relayer-1",
1209 &[TransactionStatus::Pending, TransactionStatus::Confirmed],
1210 query,
1211 false,
1212 )
1213 .await
1214 .unwrap();
1215
1216 assert_eq!(result.total, 7);
1217 assert_eq!(result.items.len(), 7);
1218
1219 let query = PaginationQuery {
1221 page: 1,
1222 per_page: 10,
1223 };
1224 let result = repo
1225 .find_by_status_paginated("relayer-1", &[TransactionStatus::Failed], query, false)
1226 .await
1227 .unwrap();
1228
1229 assert_eq!(result.total, 0);
1230 assert_eq!(result.items.len(), 0);
1231 }
1232
1233 #[tokio::test]
1234 async fn test_find_by_status_paginated_oldest_first() {
1235 let repo = InMemoryTransactionRepository::new();
1236
1237 let create_tx_with_timestamp =
1239 |id: &str, timestamp: &str, status: TransactionStatus| -> TransactionRepoModel {
1240 let mut tx = create_test_transaction_pending_state(id);
1241 tx.created_at = timestamp.to_string();
1242 tx.status = status;
1243 tx
1244 };
1245
1246 for i in 1..=5 {
1248 let tx = create_tx_with_timestamp(
1249 &format!("tx{}", i),
1250 &format!("2025-01-27T{:02}:00:00.000000+00:00", 10 + i),
1251 TransactionStatus::Pending,
1252 );
1253 repo.create(tx).await.unwrap();
1254 }
1255
1256 let query = PaginationQuery {
1258 page: 1,
1259 per_page: 3,
1260 };
1261 let result = repo
1262 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, true)
1263 .await
1264 .unwrap();
1265
1266 assert_eq!(result.total, 5);
1267 assert_eq!(result.items.len(), 3);
1268 assert_eq!(
1270 result.items[0].id, "tx1",
1271 "First item should be oldest (tx1)"
1272 );
1273 assert_eq!(result.items[1].id, "tx2", "Second item should be tx2");
1274 assert_eq!(result.items[2].id, "tx3", "Third item should be tx3");
1275
1276 let query = PaginationQuery {
1278 page: 2,
1279 per_page: 3,
1280 };
1281 let result = repo
1282 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, true)
1283 .await
1284 .unwrap();
1285
1286 assert_eq!(result.total, 5);
1287 assert_eq!(result.items.len(), 2);
1288 assert_eq!(result.items[0].id, "tx4");
1290 assert_eq!(result.items[1].id, "tx5");
1291 }
1292
1293 #[tokio::test]
1294 async fn test_find_by_status_paginated_oldest_first_single_item() {
1295 let repo = InMemoryTransactionRepository::new();
1296
1297 let timestamps = [
1299 ("tx-oldest", "2025-01-27T08:00:00.000000+00:00"),
1300 ("tx-middle", "2025-01-27T10:00:00.000000+00:00"),
1301 ("tx-newest", "2025-01-27T12:00:00.000000+00:00"),
1302 ];
1303
1304 for (id, timestamp) in timestamps {
1305 let mut tx = create_test_transaction_pending_state(id);
1306 tx.created_at = timestamp.to_string();
1307 tx.status = TransactionStatus::Pending;
1308 repo.create(tx).await.unwrap();
1309 }
1310
1311 let query = PaginationQuery {
1313 page: 1,
1314 per_page: 1,
1315 };
1316 let result = repo
1317 .find_by_status_paginated(
1318 "relayer-1",
1319 &[TransactionStatus::Pending],
1320 query.clone(),
1321 true,
1322 )
1323 .await
1324 .unwrap();
1325
1326 assert_eq!(result.total, 3);
1327 assert_eq!(result.items.len(), 1);
1328 assert_eq!(
1329 result.items[0].id, "tx-oldest",
1330 "With oldest_first and per_page=1, should return the oldest transaction"
1331 );
1332
1333 let result = repo
1335 .find_by_status_paginated("relayer-1", &[TransactionStatus::Pending], query, false)
1336 .await
1337 .unwrap();
1338
1339 assert_eq!(result.items.len(), 1);
1340 assert_eq!(
1341 result.items[0].id, "tx-newest",
1342 "With oldest_first=false and per_page=1, should return the newest transaction"
1343 );
1344 }
1345
1346 #[tokio::test]
1347 async fn test_find_by_status_paginated_multi_status_oldest_first() {
1348 let repo = InMemoryTransactionRepository::new();
1349
1350 let transactions = [
1352 (
1353 "tx-pending-old",
1354 "2025-01-27T08:00:00.000000+00:00",
1355 TransactionStatus::Pending,
1356 ),
1357 (
1358 "tx-sent-mid",
1359 "2025-01-27T10:00:00.000000+00:00",
1360 TransactionStatus::Sent,
1361 ),
1362 (
1363 "tx-pending-new",
1364 "2025-01-27T12:00:00.000000+00:00",
1365 TransactionStatus::Pending,
1366 ),
1367 (
1368 "tx-sent-old",
1369 "2025-01-27T07:00:00.000000+00:00",
1370 TransactionStatus::Sent,
1371 ),
1372 ];
1373
1374 for (id, timestamp, status) in transactions {
1375 let mut tx = create_test_transaction_pending_state(id);
1376 tx.created_at = timestamp.to_string();
1377 tx.status = status;
1378 repo.create(tx).await.unwrap();
1379 }
1380
1381 let query = PaginationQuery {
1383 page: 1,
1384 per_page: 10,
1385 };
1386 let result = repo
1387 .find_by_status_paginated(
1388 "relayer-1",
1389 &[TransactionStatus::Pending, TransactionStatus::Sent],
1390 query,
1391 true,
1392 )
1393 .await
1394 .unwrap();
1395
1396 assert_eq!(result.total, 4);
1397 assert_eq!(result.items.len(), 4);
1398 assert_eq!(result.items[0].id, "tx-sent-old", "Oldest should be first");
1400 assert_eq!(result.items[1].id, "tx-pending-old");
1401 assert_eq!(result.items[2].id, "tx-sent-mid");
1402 assert_eq!(
1403 result.items[3].id, "tx-pending-new",
1404 "Newest should be last"
1405 );
1406 }
1407
1408 #[tokio::test]
1409 async fn test_has_entries() {
1410 let repo = InMemoryTransactionRepository::new();
1411 assert!(!repo.has_entries().await.unwrap());
1412
1413 let tx = create_test_transaction("test");
1414 repo.create(tx.clone()).await.unwrap();
1415
1416 assert!(repo.has_entries().await.unwrap());
1417 }
1418
1419 #[tokio::test]
1420 async fn test_drop_all_entries() {
1421 let repo = InMemoryTransactionRepository::new();
1422 let tx = create_test_transaction("test");
1423 repo.create(tx.clone()).await.unwrap();
1424
1425 assert!(repo.has_entries().await.unwrap());
1426
1427 repo.drop_all_entries().await.unwrap();
1428 assert!(!repo.has_entries().await.unwrap());
1429 }
1430
1431 #[tokio::test]
1434 async fn test_update_status_sets_delete_at_for_final_statuses() {
1435 let _lock = ENV_MUTEX.lock().await;
1436
1437 use chrono::{DateTime, Duration, Utc};
1438 use std::env;
1439
1440 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
1442
1443 let repo = InMemoryTransactionRepository::new();
1444
1445 let final_statuses = [
1446 TransactionStatus::Canceled,
1447 TransactionStatus::Confirmed,
1448 TransactionStatus::Failed,
1449 TransactionStatus::Expired,
1450 ];
1451
1452 for (i, status) in final_statuses.iter().enumerate() {
1453 let tx_id = format!("test-final-{}", i);
1454 let tx = create_test_transaction_pending_state(&tx_id);
1455
1456 assert!(tx.delete_at.is_none());
1458
1459 repo.create(tx).await.unwrap();
1460
1461 let before_update = Utc::now();
1462
1463 let updated = repo
1465 .update_status(tx_id.clone(), status.clone())
1466 .await
1467 .unwrap();
1468
1469 assert!(
1471 updated.delete_at.is_some(),
1472 "delete_at should be set for status: {:?}",
1473 status
1474 );
1475
1476 let delete_at_str = updated.delete_at.unwrap();
1478 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
1479 .expect("delete_at should be valid RFC3339")
1480 .with_timezone(&Utc);
1481
1482 let duration_from_before = delete_at.signed_duration_since(before_update);
1483 let expected_duration = Duration::hours(6);
1484 let tolerance = Duration::minutes(5);
1485
1486 assert!(
1487 duration_from_before >= expected_duration - tolerance &&
1488 duration_from_before <= expected_duration + tolerance,
1489 "delete_at should be approximately 6 hours from now for status: {:?}. Duration: {:?}",
1490 status, duration_from_before
1491 );
1492 }
1493
1494 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1496 }
1497
1498 #[tokio::test]
1499 async fn test_update_status_does_not_set_delete_at_for_non_final_statuses() {
1500 let _lock = ENV_MUTEX.lock().await;
1501
1502 use std::env;
1503
1504 env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
1505
1506 let repo = InMemoryTransactionRepository::new();
1507
1508 let non_final_statuses = [
1509 TransactionStatus::Pending,
1510 TransactionStatus::Sent,
1511 TransactionStatus::Submitted,
1512 TransactionStatus::Mined,
1513 ];
1514
1515 for (i, status) in non_final_statuses.iter().enumerate() {
1516 let tx_id = format!("test-non-final-{}", i);
1517 let tx = create_test_transaction_pending_state(&tx_id);
1518
1519 repo.create(tx).await.unwrap();
1520
1521 let updated = repo
1523 .update_status(tx_id.clone(), status.clone())
1524 .await
1525 .unwrap();
1526
1527 assert!(
1529 updated.delete_at.is_none(),
1530 "delete_at should NOT be set for status: {:?}",
1531 status
1532 );
1533 }
1534
1535 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1537 }
1538
1539 #[tokio::test]
1540 async fn test_partial_update_sets_delete_at_for_final_statuses() {
1541 let _lock = ENV_MUTEX.lock().await;
1542
1543 use chrono::{DateTime, Duration, Utc};
1544 use std::env;
1545
1546 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
1547
1548 let repo = InMemoryTransactionRepository::new();
1549 let tx = create_test_transaction_pending_state("test-partial-final");
1550
1551 repo.create(tx).await.unwrap();
1552
1553 let before_update = Utc::now();
1554
1555 let update = TransactionUpdateRequest {
1557 status: Some(TransactionStatus::Confirmed),
1558 status_reason: Some("Transaction completed".to_string()),
1559 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
1560 ..Default::default()
1561 };
1562
1563 let updated = repo
1564 .partial_update("test-partial-final".to_string(), update)
1565 .await
1566 .unwrap();
1567
1568 assert!(
1570 updated.delete_at.is_some(),
1571 "delete_at should be set when updating to Confirmed status"
1572 );
1573
1574 let delete_at_str = updated.delete_at.unwrap();
1576 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
1577 .expect("delete_at should be valid RFC3339")
1578 .with_timezone(&Utc);
1579
1580 let duration_from_before = delete_at.signed_duration_since(before_update);
1581 let expected_duration = Duration::hours(8);
1582 let tolerance = Duration::minutes(5);
1583
1584 assert!(
1585 duration_from_before >= expected_duration - tolerance
1586 && duration_from_before <= expected_duration + tolerance,
1587 "delete_at should be approximately 8 hours from now. Duration: {:?}",
1588 duration_from_before
1589 );
1590
1591 assert_eq!(updated.status, TransactionStatus::Confirmed);
1593 assert_eq!(
1594 updated.status_reason,
1595 Some("Transaction completed".to_string())
1596 );
1597 assert_eq!(
1598 updated.confirmed_at,
1599 Some("2023-01-01T12:05:00Z".to_string())
1600 );
1601
1602 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1604 }
1605
1606 #[tokio::test]
1607 async fn test_update_status_preserves_existing_delete_at() {
1608 let _lock = ENV_MUTEX.lock().await;
1609
1610 use std::env;
1611
1612 env::set_var("TRANSACTION_EXPIRATION_HOURS", "2");
1613
1614 let repo = InMemoryTransactionRepository::new();
1615 let mut tx = create_test_transaction_pending_state("test-preserve-delete-at");
1616
1617 let existing_delete_at = "2025-01-01T12:00:00Z".to_string();
1619 tx.delete_at = Some(existing_delete_at.clone());
1620
1621 repo.create(tx).await.unwrap();
1622
1623 let updated = repo
1625 .update_status(
1626 "test-preserve-delete-at".to_string(),
1627 TransactionStatus::Confirmed,
1628 )
1629 .await
1630 .unwrap();
1631
1632 assert_eq!(
1634 updated.delete_at,
1635 Some(existing_delete_at),
1636 "Existing delete_at should be preserved when updating to final status"
1637 );
1638
1639 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1641 }
1642
1643 #[tokio::test]
1644 async fn test_partial_update_without_status_change_preserves_delete_at() {
1645 let _lock = ENV_MUTEX.lock().await;
1646
1647 use std::env;
1648
1649 env::set_var("TRANSACTION_EXPIRATION_HOURS", "3");
1650
1651 let repo = InMemoryTransactionRepository::new();
1652 let tx = create_test_transaction_pending_state("test-preserve-no-status");
1653
1654 repo.create(tx).await.unwrap();
1655
1656 let updated1 = repo
1658 .update_status(
1659 "test-preserve-no-status".to_string(),
1660 TransactionStatus::Confirmed,
1661 )
1662 .await
1663 .unwrap();
1664
1665 assert!(updated1.delete_at.is_some());
1666 let original_delete_at = updated1.delete_at.clone();
1667
1668 let update = TransactionUpdateRequest {
1670 status: None, status_reason: Some("Updated reason".to_string()),
1672 confirmed_at: Some("2023-01-01T12:10:00Z".to_string()),
1673 ..Default::default()
1674 };
1675
1676 let updated2 = repo
1677 .partial_update("test-preserve-no-status".to_string(), update)
1678 .await
1679 .unwrap();
1680
1681 assert_eq!(
1683 updated2.delete_at, original_delete_at,
1684 "delete_at should be preserved when status is not updated"
1685 );
1686
1687 assert_eq!(updated2.status, TransactionStatus::Confirmed); assert_eq!(updated2.status_reason, Some("Updated reason".to_string()));
1690 assert_eq!(
1691 updated2.confirmed_at,
1692 Some("2023-01-01T12:10:00Z".to_string())
1693 );
1694
1695 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1697 }
1698
1699 #[tokio::test]
1700 async fn test_update_status_multiple_updates_idempotent() {
1701 let _lock = ENV_MUTEX.lock().await;
1702
1703 use std::env;
1704
1705 env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
1706
1707 let repo = InMemoryTransactionRepository::new();
1708 let tx = create_test_transaction_pending_state("test-idempotent");
1709
1710 repo.create(tx).await.unwrap();
1711
1712 let updated1 = repo
1714 .update_status("test-idempotent".to_string(), TransactionStatus::Confirmed)
1715 .await
1716 .unwrap();
1717
1718 assert!(updated1.delete_at.is_some());
1719 let first_delete_at = updated1.delete_at.clone();
1720
1721 let updated2 = repo
1723 .update_status("test-idempotent".to_string(), TransactionStatus::Failed)
1724 .await
1725 .unwrap();
1726
1727 assert_eq!(
1729 updated2.delete_at, first_delete_at,
1730 "delete_at should not change on subsequent final status updates"
1731 );
1732
1733 assert_eq!(updated2.status, TransactionStatus::Failed);
1735
1736 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1738 }
1739
1740 #[tokio::test]
1743 async fn test_delete_by_ids_empty_list() {
1744 let repo = InMemoryTransactionRepository::new();
1745
1746 let tx = create_test_transaction("test-1");
1748 repo.create(tx).await.unwrap();
1749
1750 let result = repo.delete_by_ids(vec![]).await.unwrap();
1752
1753 assert_eq!(result.deleted_count, 0);
1754 assert!(result.failed.is_empty());
1755
1756 assert!(repo.get_by_id("test-1".to_string()).await.is_ok());
1758 }
1759
1760 #[tokio::test]
1761 async fn test_delete_by_ids_single_transaction() {
1762 let repo = InMemoryTransactionRepository::new();
1763
1764 let tx = create_test_transaction("test-1");
1765 repo.create(tx).await.unwrap();
1766
1767 let result = repo
1768 .delete_by_ids(vec!["test-1".to_string()])
1769 .await
1770 .unwrap();
1771
1772 assert_eq!(result.deleted_count, 1);
1773 assert!(result.failed.is_empty());
1774
1775 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1777 }
1778
1779 #[tokio::test]
1780 async fn test_delete_by_ids_multiple_transactions() {
1781 let repo = InMemoryTransactionRepository::new();
1782
1783 for i in 1..=5 {
1785 let tx = create_test_transaction(&format!("test-{}", i));
1786 repo.create(tx).await.unwrap();
1787 }
1788
1789 assert_eq!(repo.count().await.unwrap(), 5);
1790
1791 let ids_to_delete = vec![
1793 "test-1".to_string(),
1794 "test-3".to_string(),
1795 "test-5".to_string(),
1796 ];
1797 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1798
1799 assert_eq!(result.deleted_count, 3);
1800 assert!(result.failed.is_empty());
1801
1802 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1804 assert!(repo.get_by_id("test-2".to_string()).await.is_ok()); assert!(repo.get_by_id("test-3".to_string()).await.is_err());
1806 assert!(repo.get_by_id("test-4".to_string()).await.is_ok()); assert!(repo.get_by_id("test-5".to_string()).await.is_err());
1808
1809 assert_eq!(repo.count().await.unwrap(), 2);
1810 }
1811
1812 #[tokio::test]
1813 async fn test_delete_by_ids_nonexistent_transactions() {
1814 let repo = InMemoryTransactionRepository::new();
1815
1816 let ids_to_delete = vec!["nonexistent-1".to_string(), "nonexistent-2".to_string()];
1818 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1819
1820 assert_eq!(result.deleted_count, 0);
1821 assert_eq!(result.failed.len(), 2);
1822
1823 assert!(result.failed.iter().any(|(id, _)| id == "nonexistent-1"));
1825 assert!(result.failed.iter().any(|(id, _)| id == "nonexistent-2"));
1826 }
1827
1828 #[tokio::test]
1829 async fn test_delete_by_ids_mixed_existing_and_nonexistent() {
1830 let repo = InMemoryTransactionRepository::new();
1831
1832 for i in 1..=3 {
1834 let tx = create_test_transaction(&format!("test-{}", i));
1835 repo.create(tx).await.unwrap();
1836 }
1837
1838 let ids_to_delete = vec![
1840 "test-1".to_string(), "nonexistent-1".to_string(), "test-2".to_string(), "nonexistent-2".to_string(), ];
1845 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1846
1847 assert_eq!(result.deleted_count, 2);
1848 assert_eq!(result.failed.len(), 2);
1849
1850 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1852 assert!(repo.get_by_id("test-2".to_string()).await.is_err());
1853
1854 assert!(repo.get_by_id("test-3".to_string()).await.is_ok());
1856
1857 let failed_ids: Vec<&String> = result.failed.iter().map(|(id, _)| id).collect();
1859 assert!(failed_ids.contains(&&"nonexistent-1".to_string()));
1860 assert!(failed_ids.contains(&&"nonexistent-2".to_string()));
1861 }
1862
1863 #[tokio::test]
1864 async fn test_delete_by_ids_all_transactions() {
1865 let repo = InMemoryTransactionRepository::new();
1866
1867 for i in 1..=10 {
1869 let tx = create_test_transaction(&format!("test-{}", i));
1870 repo.create(tx).await.unwrap();
1871 }
1872
1873 assert_eq!(repo.count().await.unwrap(), 10);
1874
1875 let ids_to_delete: Vec<String> = (1..=10).map(|i| format!("test-{}", i)).collect();
1877 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1878
1879 assert_eq!(result.deleted_count, 10);
1880 assert!(result.failed.is_empty());
1881 assert_eq!(repo.count().await.unwrap(), 0);
1882 assert!(!repo.has_entries().await.unwrap());
1883 }
1884
1885 #[tokio::test]
1886 async fn test_delete_by_ids_duplicate_ids() {
1887 let repo = InMemoryTransactionRepository::new();
1888
1889 let tx = create_test_transaction("test-1");
1890 repo.create(tx).await.unwrap();
1891
1892 let ids_to_delete = vec![
1894 "test-1".to_string(),
1895 "test-1".to_string(), "test-1".to_string(), ];
1898 let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1899
1900 assert_eq!(result.deleted_count, 1);
1902 assert_eq!(result.failed.len(), 2);
1903
1904 assert!(repo.get_by_id("test-1".to_string()).await.is_err());
1906 }
1907
1908 #[tokio::test]
1909 async fn test_delete_by_ids_preserves_other_relayer_transactions() {
1910 let repo = InMemoryTransactionRepository::new();
1911
1912 let mut tx1 = create_test_transaction("tx-relayer-1");
1914 tx1.relayer_id = "relayer-1".to_string();
1915
1916 let mut tx2 = create_test_transaction("tx-relayer-2");
1917 tx2.relayer_id = "relayer-2".to_string();
1918
1919 repo.create(tx1).await.unwrap();
1920 repo.create(tx2).await.unwrap();
1921
1922 let result = repo
1924 .delete_by_ids(vec!["tx-relayer-1".to_string()])
1925 .await
1926 .unwrap();
1927
1928 assert_eq!(result.deleted_count, 1);
1929
1930 let remaining = repo.get_by_id("tx-relayer-2".to_string()).await.unwrap();
1932 assert_eq!(remaining.relayer_id, "relayer-2");
1933 }
1934}