openzeppelin_relayer/repositories/transaction/
transaction_in_memory.rs

1//! This module defines an in-memory transaction repository for managing
2//! transaction data. It provides asynchronous methods for creating, retrieving,
3//! updating, and deleting transactions, as well as querying transactions by
4//! various criteria such as relayer ID, status, and nonce. The repository
5//! is implemented using a `Mutex`-protected `HashMap` to store transaction
6//! data, ensuring thread-safe access in an asynchronous context.
7use 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        // Try to get the current data, or use empty HashMap if lock fails
27        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    /// Get the sort key for a transaction based on its status.
51    /// - For Confirmed status: use confirmed_at (on-chain confirmation order)
52    /// - For all other statuses: use created_at (queue/processing order)
53    ///
54    /// Returns a tuple (timestamp_string, is_confirmed) for consistent sorting.
55    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            // Fallback to created_at if confirmed_at not set (shouldn't happen)
61        }
62        (&tx.created_at, false)
63    }
64
65    /// Compare two transactions for sorting (newest first).
66    /// Uses the same logic as Redis implementation: confirmed_at for Confirmed, created_at for others.
67    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) // Descending (newest first)
72            .then_with(|| b.id.cmp(&a.id)) // Tie-breaker: sort by ID for deterministic ordering
73    }
74}
75
76// Implement both traits for InMemoryTransactionRepository
77
78#[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        // Sort and paginate (newest first)
205        let items = filtered
206            .into_iter()
207            .sorted_by(|a, b| b.created_at.cmp(&a.created_at)) // Sort by created_at descending (newest first)
208            .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        // Sort by created_at (newest first)
233        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        // Filter by relayer_id and statuses
251        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        // Sort using status-aware ordering: confirmed_at for Confirmed, created_at for others
261        // oldest_first: ascending order, otherwise descending (newest first)
262        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) // Ascending (oldest first)
270                        .then_with(|| a.id.cmp(&b.id)) // Tie-breaker: sort by ID for deterministic ordering
271                })
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) // Descending (newest first)
279                .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            // Apply partial updates using the model's business logic
334            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        // For in-memory storage, we only need the IDs (no separate indexes to clean up)
418        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    // Helper function to create test transactions
445    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        // Test updating only status
625        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        // Test updating multiple fields
646        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        // Test updating non-existent transaction
674        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        // Update status to Confirmed
702        let updated = repo
703            .update_status("test-1".to_string(), TransactionStatus::Confirmed)
704            .await
705            .unwrap();
706
707        // Verify the status was updated in the returned transaction
708        assert_eq!(updated.status, TransactionStatus::Confirmed);
709
710        // Also verify by getting the transaction directly
711        let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
712        assert_eq!(stored.status, TransactionStatus::Confirmed);
713
714        // Update status to Failed
715        let updated = repo
716            .update_status("test-1".to_string(), TransactionStatus::Failed)
717            .await
718            .unwrap();
719
720        // Verify the status was updated
721        assert_eq!(updated.status, TransactionStatus::Failed);
722
723        // Verify updating a non-existent transaction
724        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        // Create multiple transactions
735        for i in 1..=10 {
736            let tx = create_test_transaction(&format!("test-{i}"));
737            repo.create(tx).await.unwrap();
738        }
739
740        // Test first page with 3 items per page
741        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        // Test second page with 3 items per page
752        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        // Test page with fewer items than per_page
763        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        // Test empty page (beyond total items)
774        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        // Create transactions with different nonces
788        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        // Test finding transaction with specific relayer_id and nonce
806        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        // Test finding transaction with a different nonce
811        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        // Test finding transaction from a different relayer
816        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        // Test finding transaction that doesn't exist
821        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        // Create new network data with updated values
833        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        // Verify the network data was updated
856        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        // Updated sent_at timestamp
875        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        // Verify the sent_at timestamp was updated
883        assert_eq!(updated.sent_at, Some(new_sent_at.clone()));
884
885        // Also verify by getting the transaction directly
886        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        // Updated confirmed_at timestamp
898        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        // Verify the confirmed_at timestamp was updated
906        assert_eq!(updated.confirmed_at, Some(new_confirmed_at.clone()));
907
908        // Also verify by getting the transaction directly
909        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        // Create a transaction with a different relayer_id
920        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        // Test finding transactions for relayer-1
928        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        // Test finding transactions for relayer-2
941        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        // Test finding transactions for non-existent relayer
950        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        // Create transactions with different created_at timestamps
963        let mut tx1 = create_test_transaction("test-1");
964        tx1.created_at = "2025-01-27T10:00:00.000000+00:00".to_string(); // Oldest
965
966        let mut tx2 = create_test_transaction("test-2");
967        tx2.created_at = "2025-01-27T12:00:00.000000+00:00".to_string(); // Middle
968
969        let mut tx3 = create_test_transaction("test-3");
970        tx3.created_at = "2025-01-27T14:00:00.000000+00:00".to_string(); // Newest
971
972        // Create transactions in non-chronological order to ensure sorting works
973        repo.create(tx2.clone()).await.unwrap(); // Middle first
974        repo.create(tx1.clone()).await.unwrap(); // Oldest second
975        repo.create(tx3.clone()).await.unwrap(); // Newest last
976
977        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        // Verify transactions are sorted by created_at descending (newest first)
987        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        // Test finding by single status
1030        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        // Test finding by multiple statuses
1045        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        // Test finding for different relayer
1055        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        // Test finding for non-existent relayer
1063        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        // Helper function to create transaction with custom created_at timestamp
1075        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        // Create transactions with different timestamps (out of chronological order)
1083        let tx3 = create_tx_with_timestamp("tx3", "2025-01-27T17:00:00.000000+00:00"); // Latest
1084        let tx1 = create_tx_with_timestamp("tx1", "2025-01-27T15:00:00.000000+00:00"); // Earliest
1085        let tx2 = create_tx_with_timestamp("tx2", "2025-01-27T16:00:00.000000+00:00"); // Middle
1086
1087        // Create them in reverse chronological order to test sorting
1088        repo.create(tx3.clone()).await.unwrap();
1089        repo.create(tx1.clone()).await.unwrap();
1090        repo.create(tx2.clone()).await.unwrap();
1091
1092        // Find by status
1093        let result = repo
1094            .find_by_status("relayer-1", &[TransactionStatus::Pending])
1095            .await
1096            .unwrap();
1097
1098        // Verify they are sorted by created_at (newest first) for Pending status
1099        assert_eq!(result.len(), 3);
1100        assert_eq!(result[0].id, "tx3"); // Latest
1101        assert_eq!(result[1].id, "tx2"); // Middle
1102        assert_eq!(result[2].id, "tx1"); // Earliest
1103
1104        // Verify the timestamps are in descending order
1105        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        // Helper function to create transaction with custom created_at timestamp
1115        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        // Create 5 pending transactions
1124        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        // Create 2 confirmed transactions
1134        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        // Test first page (2 items per page)
1144        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        // Should be newest first (tx5, tx4)
1158        assert_eq!(result.items[0].id, "tx5");
1159        assert_eq!(result.items[1].id, "tx4");
1160
1161        // Test second page
1162        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        // Should be tx3, tx2
1175        assert_eq!(result.items[0].id, "tx3");
1176        assert_eq!(result.items[1].id, "tx2");
1177
1178        // Test last page (partial)
1179        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        // Test beyond last page
1194        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        // Test multiple statuses
1207        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        // Test empty result
1225        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        // Helper function to create transaction with custom created_at timestamp
1243        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        // Create 5 pending transactions with ascending timestamps
1252        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        // Test oldest_first: true - should return tx1, tx2, tx3... (ascending order)
1262        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        // Should be oldest first (tx1, tx2, tx3)
1274        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        // Test second page with oldest_first
1282        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        // Should be tx4, tx5
1294        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        // Create 3 pending transactions with different timestamps
1303        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        // Request just 1 item with oldest_first: true - should get the oldest
1317        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        // Contrast with oldest_first: false - should get the newest
1339        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        // Create transactions with different statuses and timestamps
1356        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        // Query multiple statuses with oldest_first: true
1387        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        // Should be sorted by created_at ascending (oldest first)
1404        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    // Tests for delete_at field setting on final status updates
1437
1438    #[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        // Use a unique test environment variable to avoid conflicts
1446        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            // Ensure transaction has no delete_at initially
1462            assert!(tx.delete_at.is_none());
1463
1464            repo.create(tx).await.unwrap();
1465
1466            let before_update = Utc::now();
1467
1468            // Update to final status
1469            let updated = repo
1470                .update_status(tx_id.clone(), status.clone())
1471                .await
1472                .unwrap();
1473
1474            // Should have delete_at set
1475            assert!(
1476                updated.delete_at.is_some(),
1477                "delete_at should be set for status: {status:?}"
1478            );
1479
1480            // Verify the timestamp is reasonable (approximately 6 hours from now)
1481            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        // Cleanup
1498        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            // Update to non-final status
1525            let updated = repo
1526                .update_status(tx_id.clone(), status.clone())
1527                .await
1528                .unwrap();
1529
1530            // Should NOT have delete_at set
1531            assert!(
1532                updated.delete_at.is_none(),
1533                "delete_at should NOT be set for status: {status:?}"
1534            );
1535        }
1536
1537        // Cleanup
1538        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        // Use partial_update to set status to Confirmed (final status)
1558        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        // Should have delete_at set
1571        assert!(
1572            updated.delete_at.is_some(),
1573            "delete_at should be set when updating to Confirmed status"
1574        );
1575
1576        // Verify the timestamp is reasonable (approximately 8 hours from now)
1577        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        // Also verify other fields were updated
1593        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        // Cleanup
1604        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        // Set an existing delete_at value
1619        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        // Update to final status
1625        let updated = repo
1626            .update_status(
1627                "test-preserve-delete-at".to_string(),
1628                TransactionStatus::Confirmed,
1629            )
1630            .await
1631            .unwrap();
1632
1633        // Should preserve the existing delete_at value
1634        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        // Cleanup
1641        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        // First, update to final status to set delete_at
1658        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        // Now update other fields without changing status
1670        let update = TransactionUpdateRequest {
1671            status: None, // No status change
1672            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        // delete_at should be preserved
1683        assert_eq!(
1684            updated2.delete_at, original_delete_at,
1685            "delete_at should be preserved when status is not updated"
1686        );
1687
1688        // Other fields should be updated
1689        assert_eq!(updated2.status, TransactionStatus::Confirmed); // Unchanged
1690        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        // Cleanup
1697        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        // First update to final status
1714        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        // Second update to another final status
1723        let updated2 = repo
1724            .update_status("test-idempotent".to_string(), TransactionStatus::Failed)
1725            .await
1726            .unwrap();
1727
1728        // delete_at should remain the same (idempotent)
1729        assert_eq!(
1730            updated2.delete_at, first_delete_at,
1731            "delete_at should not change on subsequent final status updates"
1732        );
1733
1734        // Status should be updated
1735        assert_eq!(updated2.status, TransactionStatus::Failed);
1736
1737        // Cleanup
1738        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1739    }
1740
1741    // Tests for delete_by_ids batch delete functionality
1742
1743    #[tokio::test]
1744    async fn test_delete_by_ids_empty_list() {
1745        let repo = InMemoryTransactionRepository::new();
1746
1747        // Create a transaction to ensure repo is not empty
1748        let tx = create_test_transaction("test-1");
1749        repo.create(tx).await.unwrap();
1750
1751        // Delete with empty list should succeed and not affect existing data
1752        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        // Original transaction should still exist
1758        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        // Verify transaction was deleted
1777        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        // Create multiple transactions
1785        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        // Delete 3 of them
1793        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        // Verify correct transactions were deleted
1804        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()); // Not deleted
1806        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()); // Not deleted
1808        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        // Try to delete transactions that don't exist
1818        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        // Verify error messages contain the IDs
1825        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        // Create some transactions
1834        for i in 1..=3 {
1835            let tx = create_test_transaction(&format!("test-{i}"));
1836            repo.create(tx).await.unwrap();
1837        }
1838
1839        // Try to delete mix of existing and non-existing
1840        let ids_to_delete = vec![
1841            "test-1".to_string(),        // exists
1842            "nonexistent-1".to_string(), // doesn't exist
1843            "test-2".to_string(),        // exists
1844            "nonexistent-2".to_string(), // doesn't exist
1845        ];
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        // Verify existing transactions were deleted
1852        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        // Verify remaining transaction still exists
1856        assert!(repo.get_by_id("test-3".to_string()).await.is_ok());
1857
1858        // Verify failed IDs are reported
1859        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        // Create transactions
1869        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        // Delete all
1877        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        // Try to delete same ID multiple times in one call
1894        let ids_to_delete = vec![
1895            "test-1".to_string(),
1896            "test-1".to_string(), // duplicate
1897            "test-1".to_string(), // duplicate
1898        ];
1899        let result = repo.delete_by_ids(ids_to_delete).await.unwrap();
1900
1901        // First delete succeeds, subsequent ones fail (already deleted)
1902        assert_eq!(result.deleted_count, 1);
1903        assert_eq!(result.failed.len(), 2);
1904
1905        // Verify transaction was deleted
1906        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        // Create transactions for different relayers
1914        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        // Delete only relayer-1's transaction
1924        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        // relayer-2's transaction should still exist
1932        let remaining = repo.get_by_id("tx-relayer-2".to_string()).await.unwrap();
1933        assert_eq!(remaining.relayer_id, "relayer-2");
1934    }
1935}