openzeppelin_relayer/repositories/
transaction.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/// A trait defining transaction repository operations
20#[async_trait]
21pub trait TransactionRepository: Repository<TransactionRepoModel, String> {
22    /// Find transactions by relayer ID with pagination
23    async fn find_by_relayer_id(
24        &self,
25        relayer_id: &str,
26        query: PaginationQuery,
27    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
28
29    /// Find transactions by relayer ID and status(es)
30    async fn find_by_status(
31        &self,
32        relayer_id: &str,
33        statuses: &[TransactionStatus],
34    ) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
35
36    /// Find a transaction by relayer ID and nonce
37    async fn find_by_nonce(
38        &self,
39        relayer_id: &str,
40        nonce: u64,
41    ) -> Result<Option<TransactionRepoModel>, RepositoryError>;
42
43    /// Update the status of a transaction
44    async fn update_status(
45        &self,
46        tx_id: String,
47        status: TransactionStatus,
48    ) -> Result<TransactionRepoModel, RepositoryError>;
49
50    /// Partially update a transaction
51    async fn partial_update(
52        &self,
53        tx_id: String,
54        update: TransactionUpdateRequest,
55    ) -> Result<TransactionRepoModel, RepositoryError>;
56
57    /// Update the network data of a transaction
58    async fn update_network_data(
59        &self,
60        tx_id: String,
61        network_data: NetworkTransactionData,
62    ) -> Result<TransactionRepoModel, RepositoryError>;
63
64    /// Set the sent_at timestamp of a transaction
65    async fn set_sent_at(
66        &self,
67        tx_id: String,
68        sent_at: String,
69    ) -> Result<TransactionRepoModel, RepositoryError>;
70
71    /// Set the confirmed_at timestamp of a transaction
72    async fn set_confirmed_at(
73        &self,
74        tx_id: String,
75        confirmed_at: String,
76    ) -> Result<TransactionRepoModel, RepositoryError>;
77}
78
79#[cfg(test)]
80mockall::mock! {
81    pub TransactionRepository {}
82
83    #[async_trait]
84    impl Repository<TransactionRepoModel, String> for TransactionRepository {
85        async fn create(&self, entity: TransactionRepoModel) -> Result<TransactionRepoModel, RepositoryError>;
86        async fn get_by_id(&self, id: String) -> Result<TransactionRepoModel, RepositoryError>;
87        async fn list_all(&self) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
88        async fn list_paginated(&self, query: PaginationQuery) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
89        async fn update(&self, id: String, entity: TransactionRepoModel) -> Result<TransactionRepoModel, RepositoryError>;
90        async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError>;
91        async fn count(&self) -> Result<usize, RepositoryError>;
92    }
93
94    #[async_trait]
95    impl TransactionRepository for TransactionRepository {
96        async fn find_by_relayer_id(&self, relayer_id: &str, query: PaginationQuery) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
97        async fn find_by_status(&self, relayer_id: &str, statuses: &[TransactionStatus]) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
98        async fn find_by_nonce(&self, relayer_id: &str, nonce: u64) -> Result<Option<TransactionRepoModel>, RepositoryError>;
99        async fn update_status(&self, tx_id: String, status: TransactionStatus) -> Result<TransactionRepoModel, RepositoryError>;
100        async fn partial_update(&self, tx_id: String, update: TransactionUpdateRequest) -> Result<TransactionRepoModel, RepositoryError>;
101        async fn update_network_data(&self, tx_id: String, network_data: NetworkTransactionData) -> Result<TransactionRepoModel, RepositoryError>;
102        async fn set_sent_at(&self, tx_id: String, sent_at: String) -> Result<TransactionRepoModel, RepositoryError>;
103        async fn set_confirmed_at(&self, tx_id: String, confirmed_at: String) -> Result<TransactionRepoModel, RepositoryError>;
104
105    }
106}
107
108#[derive(Debug)]
109pub struct InMemoryTransactionRepository {
110    store: Mutex<HashMap<String, TransactionRepoModel>>,
111}
112
113impl InMemoryTransactionRepository {
114    pub fn new() -> Self {
115        Self {
116            store: Mutex::new(HashMap::new()),
117        }
118    }
119
120    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
121        Ok(lock.lock().await)
122    }
123}
124
125// Implement both traits for InMemoryTransactionRepository
126
127#[async_trait]
128impl Repository<TransactionRepoModel, String> for InMemoryTransactionRepository {
129    async fn create(
130        &self,
131        tx: TransactionRepoModel,
132    ) -> Result<TransactionRepoModel, RepositoryError> {
133        let mut store = Self::acquire_lock(&self.store).await?;
134        if store.contains_key(&tx.id) {
135            return Err(RepositoryError::ConstraintViolation(format!(
136                "Transaction with ID {} already exists",
137                tx.id
138            )));
139        }
140        store.insert(tx.id.clone(), tx.clone());
141        Ok(tx)
142    }
143
144    async fn get_by_id(&self, id: String) -> Result<TransactionRepoModel, RepositoryError> {
145        let store = Self::acquire_lock(&self.store).await?;
146        store.get(&id).cloned().ok_or_else(|| {
147            RepositoryError::NotFound(format!("Transaction with ID {} not found", id))
148        })
149    }
150
151    #[allow(clippy::map_entry)]
152    async fn update(
153        &self,
154        id: String,
155        tx: TransactionRepoModel,
156    ) -> Result<TransactionRepoModel, RepositoryError> {
157        let mut store = Self::acquire_lock(&self.store).await?;
158        if store.contains_key(&id) {
159            let mut updated_tx = tx;
160            updated_tx.id = id.clone();
161            store.insert(id, updated_tx.clone());
162            Ok(updated_tx)
163        } else {
164            Err(RepositoryError::NotFound(format!(
165                "Transaction with ID {} not found",
166                id
167            )))
168        }
169    }
170
171    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
172        let mut store = Self::acquire_lock(&self.store).await?;
173        if store.remove(&id).is_some() {
174            Ok(())
175        } else {
176            Err(RepositoryError::NotFound(format!(
177                "Transaction with ID {} not found",
178                id
179            )))
180        }
181    }
182
183    async fn list_all(&self) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
184        let store = Self::acquire_lock(&self.store).await?;
185        Ok(store.values().cloned().collect())
186    }
187
188    async fn list_paginated(
189        &self,
190        query: PaginationQuery,
191    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
192        let total = self.count().await?;
193        let start = ((query.page - 1) * query.per_page) as usize;
194        let store = Self::acquire_lock(&self.store).await?;
195        let items: Vec<TransactionRepoModel> = store
196            .values()
197            .skip(start)
198            .take(query.per_page as usize)
199            .cloned()
200            .collect();
201
202        Ok(PaginatedResult {
203            items,
204            total: total as u64,
205            page: query.page,
206            per_page: query.per_page,
207        })
208    }
209
210    async fn count(&self) -> Result<usize, RepositoryError> {
211        let store = Self::acquire_lock(&self.store).await?;
212        Ok(store.len())
213    }
214}
215
216#[async_trait]
217impl TransactionRepository for InMemoryTransactionRepository {
218    async fn find_by_relayer_id(
219        &self,
220        relayer_id: &str,
221        query: PaginationQuery,
222    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
223        let store = Self::acquire_lock(&self.store).await?;
224        let filtered: Vec<TransactionRepoModel> = store
225            .values()
226            .filter(|tx| tx.relayer_id == relayer_id)
227            .cloned()
228            .collect();
229
230        let total = filtered.len() as u64;
231
232        if total == 0 {
233            return Ok(PaginatedResult::<TransactionRepoModel> {
234                items: vec![],
235                total: 0,
236                page: query.page,
237                per_page: query.per_page,
238            });
239        }
240
241        let start = ((query.page - 1) * query.per_page) as usize;
242
243        // Sort and paginate
244        let items = filtered
245            .into_iter()
246            .sorted_by(|a, b| a.created_at.cmp(&b.created_at)) // Sort by created_at
247            .skip(start)
248            .take(query.per_page as usize)
249            .collect();
250
251        Ok(PaginatedResult {
252            items,
253            total,
254            page: query.page,
255            per_page: query.per_page,
256        })
257    }
258
259    async fn find_by_status(
260        &self,
261        relayer_id: &str,
262        statuses: &[TransactionStatus],
263    ) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
264        let store = Self::acquire_lock(&self.store).await?;
265        let filtered: Vec<TransactionRepoModel> = store
266            .values()
267            .filter(|tx| tx.relayer_id == relayer_id && statuses.contains(&tx.status))
268            .cloned()
269            .collect();
270
271        // Sort by created_at (oldest first)
272        let sorted = filtered
273            .into_iter()
274            .sorted_by_key(|tx| tx.created_at.clone())
275            .collect();
276
277        Ok(sorted)
278    }
279
280    async fn find_by_nonce(
281        &self,
282        relayer_id: &str,
283        nonce: u64,
284    ) -> Result<Option<TransactionRepoModel>, RepositoryError> {
285        let store = Self::acquire_lock(&self.store).await?;
286        let filtered: Vec<TransactionRepoModel> = store
287            .values()
288            .filter(|tx| {
289                tx.relayer_id == relayer_id
290                    && match &tx.network_data {
291                        NetworkTransactionData::Evm(data) => data.nonce == Some(nonce),
292                        _ => false,
293                    }
294            })
295            .cloned()
296            .collect();
297
298        Ok(filtered.into_iter().next())
299    }
300
301    async fn update_status(
302        &self,
303        tx_id: String,
304        status: TransactionStatus,
305    ) -> Result<TransactionRepoModel, RepositoryError> {
306        let mut tx = self.get_by_id(tx_id.clone()).await?;
307        tx.status = status;
308        self.update(tx_id, tx).await
309    }
310
311    async fn partial_update(
312        &self,
313        tx_id: String,
314        update: TransactionUpdateRequest,
315    ) -> Result<TransactionRepoModel, RepositoryError> {
316        let mut store = Self::acquire_lock(&self.store).await?;
317
318        if let Some(tx) = store.get_mut(&tx_id) {
319            if let Some(status) = update.status {
320                tx.status = status;
321            }
322            if let Some(status_reason) = update.status_reason {
323                tx.status_reason = Some(status_reason);
324            }
325            if let Some(sent_at) = update.sent_at {
326                tx.sent_at = Some(sent_at);
327            }
328            if let Some(confirmed_at) = update.confirmed_at {
329                tx.confirmed_at = Some(confirmed_at);
330            }
331            if let Some(network_data) = update.network_data {
332                tx.network_data = network_data;
333            }
334            if let Some(hashes) = update.hashes {
335                tx.hashes = hashes;
336            }
337            if let Some(is_canceled) = update.is_canceled {
338                tx.is_canceled = Some(is_canceled);
339            }
340            Ok(tx.clone())
341        } else {
342            Err(RepositoryError::NotFound(format!(
343                "Transaction with ID {} not found",
344                tx_id
345            )))
346        }
347    }
348
349    async fn update_network_data(
350        &self,
351        tx_id: String,
352        network_data: NetworkTransactionData,
353    ) -> Result<TransactionRepoModel, RepositoryError> {
354        let mut tx = self.get_by_id(tx_id.clone()).await?;
355        tx.network_data = network_data;
356        self.update(tx_id, tx).await
357    }
358
359    async fn set_sent_at(
360        &self,
361        tx_id: String,
362        sent_at: String,
363    ) -> Result<TransactionRepoModel, RepositoryError> {
364        let mut tx = self.get_by_id(tx_id.clone()).await?;
365        tx.sent_at = Some(sent_at);
366        self.update(tx_id, tx).await
367    }
368
369    async fn set_confirmed_at(
370        &self,
371        tx_id: String,
372        confirmed_at: String,
373    ) -> Result<TransactionRepoModel, RepositoryError> {
374        let mut tx = self.get_by_id(tx_id.clone()).await?;
375        tx.confirmed_at = Some(confirmed_at);
376        self.update(tx_id, tx).await
377    }
378}
379
380impl Default for InMemoryTransactionRepository {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use crate::models::{evm::Speed, EvmTransactionData, NetworkType};
389    use std::str::FromStr;
390
391    use crate::models::U256;
392
393    use super::*;
394
395    // Helper function to create test transactions
396    fn create_test_transaction(id: &str) -> TransactionRepoModel {
397        TransactionRepoModel {
398            id: id.to_string(),
399            relayer_id: "relayer-1".to_string(),
400            status: TransactionStatus::Pending,
401            status_reason: None,
402            created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
403            sent_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
404            confirmed_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
405            valid_until: None,
406            network_type: NetworkType::Evm,
407            priced_at: None,
408            hashes: vec![],
409            network_data: NetworkTransactionData::Evm(EvmTransactionData {
410                gas_price: Some(1000000000),
411                gas_limit: 21000,
412                nonce: Some(1),
413                value: U256::from_str("1000000000000000000").unwrap(),
414                data: Some("0x".to_string()),
415                from: "0xSender".to_string(),
416                to: Some("0xRecipient".to_string()),
417                chain_id: 1,
418                signature: None,
419                hash: Some(format!("0x{}", id)),
420                speed: Some(Speed::Fast),
421                max_fee_per_gas: None,
422                max_priority_fee_per_gas: None,
423                raw: None,
424            }),
425            noop_count: None,
426            is_canceled: Some(false),
427        }
428    }
429
430    fn create_test_transaction_pending_state(id: &str) -> TransactionRepoModel {
431        TransactionRepoModel {
432            id: id.to_string(),
433            relayer_id: "relayer-1".to_string(),
434            status: TransactionStatus::Pending,
435            status_reason: None,
436            created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
437            sent_at: None,
438            confirmed_at: None,
439            valid_until: None,
440            network_type: NetworkType::Evm,
441            priced_at: None,
442            hashes: vec![],
443            network_data: NetworkTransactionData::Evm(EvmTransactionData {
444                gas_price: Some(1000000000),
445                gas_limit: 21000,
446                nonce: Some(1),
447                value: U256::from_str("1000000000000000000").unwrap(),
448                data: Some("0x".to_string()),
449                from: "0xSender".to_string(),
450                to: Some("0xRecipient".to_string()),
451                chain_id: 1,
452                signature: None,
453                hash: Some(format!("0x{}", id)),
454                speed: Some(Speed::Fast),
455                max_fee_per_gas: None,
456                max_priority_fee_per_gas: None,
457                raw: None,
458            }),
459            noop_count: None,
460            is_canceled: Some(false),
461        }
462    }
463
464    #[tokio::test]
465    async fn test_create_transaction() {
466        let repo = InMemoryTransactionRepository::new();
467        let tx = create_test_transaction("test-1");
468
469        let result = repo.create(tx.clone()).await.unwrap();
470        assert_eq!(result.id, tx.id);
471        assert_eq!(repo.count().await.unwrap(), 1);
472    }
473
474    #[tokio::test]
475    async fn test_get_transaction() {
476        let repo = InMemoryTransactionRepository::new();
477        let tx = create_test_transaction("test-1");
478
479        repo.create(tx.clone()).await.unwrap();
480        let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
481        if let NetworkTransactionData::Evm(stored_data) = &stored.network_data {
482            if let NetworkTransactionData::Evm(tx_data) = &tx.network_data {
483                assert_eq!(stored_data.hash, tx_data.hash);
484            }
485        }
486    }
487
488    #[tokio::test]
489    async fn test_update_transaction() {
490        let repo = InMemoryTransactionRepository::new();
491        let mut tx = create_test_transaction("test-1");
492
493        repo.create(tx.clone()).await.unwrap();
494        tx.status = TransactionStatus::Confirmed;
495
496        let updated = repo.update("test-1".to_string(), tx).await.unwrap();
497        assert!(matches!(updated.status, TransactionStatus::Confirmed));
498    }
499
500    #[tokio::test]
501    async fn test_delete_transaction() {
502        let repo = InMemoryTransactionRepository::new();
503        let tx = create_test_transaction("test-1");
504
505        repo.create(tx).await.unwrap();
506        repo.delete_by_id("test-1".to_string()).await.unwrap();
507
508        let result = repo.get_by_id("test-1".to_string()).await;
509        assert!(result.is_err());
510    }
511
512    #[tokio::test]
513    async fn test_list_all_transactions() {
514        let repo = InMemoryTransactionRepository::new();
515        let tx1 = create_test_transaction("test-1");
516        let tx2 = create_test_transaction("test-2");
517
518        repo.create(tx1).await.unwrap();
519        repo.create(tx2).await.unwrap();
520
521        let transactions = repo.list_all().await.unwrap();
522        assert_eq!(transactions.len(), 2);
523    }
524
525    #[tokio::test]
526    async fn test_count_transactions() {
527        let repo = InMemoryTransactionRepository::new();
528        let tx = create_test_transaction("test-1");
529
530        assert_eq!(repo.count().await.unwrap(), 0);
531        repo.create(tx).await.unwrap();
532        assert_eq!(repo.count().await.unwrap(), 1);
533    }
534
535    #[tokio::test]
536    async fn test_get_nonexistent_transaction() {
537        let repo = InMemoryTransactionRepository::new();
538        let result = repo.get_by_id("nonexistent".to_string()).await;
539        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
540    }
541
542    #[tokio::test]
543    async fn test_duplicate_transaction_creation() {
544        let repo = InMemoryTransactionRepository::new();
545        let tx = create_test_transaction("test-1");
546
547        repo.create(tx.clone()).await.unwrap();
548        let result = repo.create(tx).await;
549
550        assert!(matches!(
551            result,
552            Err(RepositoryError::ConstraintViolation(_))
553        ));
554    }
555
556    #[tokio::test]
557    async fn test_update_nonexistent_transaction() {
558        let repo = InMemoryTransactionRepository::new();
559        let tx = create_test_transaction("test-1");
560
561        let result = repo.update("nonexistent".to_string(), tx).await;
562        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
563    }
564
565    #[tokio::test]
566    async fn test_partial_update() {
567        let repo = InMemoryTransactionRepository::new();
568        let tx = create_test_transaction_pending_state("test-tx-id");
569        repo.create(tx.clone()).await.unwrap();
570
571        // Test updating only status
572        let update1 = TransactionUpdateRequest {
573            status: Some(TransactionStatus::Sent),
574            status_reason: None,
575            sent_at: None,
576            confirmed_at: None,
577            network_data: None,
578            hashes: None,
579            priced_at: None,
580            noop_count: None,
581            is_canceled: None,
582        };
583        let updated_tx1 = repo
584            .partial_update("test-tx-id".to_string(), update1)
585            .await
586            .unwrap();
587        assert_eq!(updated_tx1.status, TransactionStatus::Sent);
588        assert_eq!(updated_tx1.sent_at, None);
589
590        // Test updating multiple fields
591        let update2 = TransactionUpdateRequest {
592            status: Some(TransactionStatus::Confirmed),
593            status_reason: None,
594            sent_at: Some("2023-01-01T12:00:00Z".to_string()),
595            confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
596            network_data: None,
597            hashes: None,
598            priced_at: None,
599            noop_count: None,
600            is_canceled: None,
601        };
602        let updated_tx2 = repo
603            .partial_update("test-tx-id".to_string(), update2)
604            .await
605            .unwrap();
606        assert_eq!(updated_tx2.status, TransactionStatus::Confirmed);
607        assert_eq!(
608            updated_tx2.sent_at,
609            Some("2023-01-01T12:00:00Z".to_string())
610        );
611        assert_eq!(
612            updated_tx2.confirmed_at,
613            Some("2023-01-01T12:05:00Z".to_string())
614        );
615
616        // Test updating non-existent transaction
617        let update3 = TransactionUpdateRequest {
618            status: Some(TransactionStatus::Failed),
619            status_reason: None,
620            sent_at: None,
621            confirmed_at: None,
622            network_data: None,
623            hashes: None,
624            priced_at: None,
625            noop_count: None,
626            is_canceled: None,
627        };
628        let result = repo
629            .partial_update("non-existent-id".to_string(), update3)
630            .await;
631        assert!(result.is_err());
632        assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
633    }
634
635    #[tokio::test]
636    async fn test_update_status() {
637        let repo = InMemoryTransactionRepository::new();
638        let tx = create_test_transaction("test-1");
639
640        repo.create(tx).await.unwrap();
641
642        // Update status to Confirmed
643        let updated = repo
644            .update_status("test-1".to_string(), TransactionStatus::Confirmed)
645            .await
646            .unwrap();
647
648        // Verify the status was updated in the returned transaction
649        assert_eq!(updated.status, TransactionStatus::Confirmed);
650
651        // Also verify by getting the transaction directly
652        let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
653        assert_eq!(stored.status, TransactionStatus::Confirmed);
654
655        // Update status to Failed
656        let updated = repo
657            .update_status("test-1".to_string(), TransactionStatus::Failed)
658            .await
659            .unwrap();
660
661        // Verify the status was updated
662        assert_eq!(updated.status, TransactionStatus::Failed);
663
664        // Verify updating a non-existent transaction
665        let result = repo
666            .update_status("non-existent".to_string(), TransactionStatus::Confirmed)
667            .await;
668        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
669    }
670
671    #[tokio::test]
672    async fn test_list_paginated() {
673        let repo = InMemoryTransactionRepository::new();
674
675        // Create multiple transactions
676        for i in 1..=10 {
677            let tx = create_test_transaction(&format!("test-{}", i));
678            repo.create(tx).await.unwrap();
679        }
680
681        // Test first page with 3 items per page
682        let query = PaginationQuery {
683            page: 1,
684            per_page: 3,
685        };
686        let result = repo.list_paginated(query).await.unwrap();
687        assert_eq!(result.items.len(), 3);
688        assert_eq!(result.total, 10);
689        assert_eq!(result.page, 1);
690        assert_eq!(result.per_page, 3);
691
692        // Test second page with 3 items per page
693        let query = PaginationQuery {
694            page: 2,
695            per_page: 3,
696        };
697        let result = repo.list_paginated(query).await.unwrap();
698        assert_eq!(result.items.len(), 3);
699        assert_eq!(result.total, 10);
700        assert_eq!(result.page, 2);
701        assert_eq!(result.per_page, 3);
702
703        // Test page with fewer items than per_page
704        let query = PaginationQuery {
705            page: 4,
706            per_page: 3,
707        };
708        let result = repo.list_paginated(query).await.unwrap();
709        assert_eq!(result.items.len(), 1);
710        assert_eq!(result.total, 10);
711        assert_eq!(result.page, 4);
712        assert_eq!(result.per_page, 3);
713
714        // Test empty page (beyond total items)
715        let query = PaginationQuery {
716            page: 5,
717            per_page: 3,
718        };
719        let result = repo.list_paginated(query).await.unwrap();
720        assert_eq!(result.items.len(), 0);
721        assert_eq!(result.total, 10);
722    }
723
724    #[tokio::test]
725    async fn test_find_by_nonce() {
726        let repo = InMemoryTransactionRepository::new();
727
728        // Create transactions with different nonces
729        let tx1 = create_test_transaction("test-1");
730
731        let mut tx2 = create_test_transaction("test-2");
732        if let NetworkTransactionData::Evm(ref mut data) = tx2.network_data {
733            data.nonce = Some(2);
734        }
735
736        let mut tx3 = create_test_transaction("test-3");
737        tx3.relayer_id = "relayer-2".to_string();
738        if let NetworkTransactionData::Evm(ref mut data) = tx3.network_data {
739            data.nonce = Some(1);
740        }
741
742        repo.create(tx1).await.unwrap();
743        repo.create(tx2).await.unwrap();
744        repo.create(tx3).await.unwrap();
745
746        // Test finding transaction with specific relayer_id and nonce
747        let result = repo.find_by_nonce("relayer-1", 1).await.unwrap();
748        assert!(result.is_some());
749        assert_eq!(result.as_ref().unwrap().id, "test-1");
750
751        // Test finding transaction with a different nonce
752        let result = repo.find_by_nonce("relayer-1", 2).await.unwrap();
753        assert!(result.is_some());
754        assert_eq!(result.as_ref().unwrap().id, "test-2");
755
756        // Test finding transaction from a different relayer
757        let result = repo.find_by_nonce("relayer-2", 1).await.unwrap();
758        assert!(result.is_some());
759        assert_eq!(result.as_ref().unwrap().id, "test-3");
760
761        // Test finding transaction that doesn't exist
762        let result = repo.find_by_nonce("relayer-1", 99).await.unwrap();
763        assert!(result.is_none());
764    }
765
766    #[tokio::test]
767    async fn test_update_network_data() {
768        let repo = InMemoryTransactionRepository::new();
769        let tx = create_test_transaction("test-1");
770
771        repo.create(tx.clone()).await.unwrap();
772
773        // Create new network data with updated values
774        let updated_network_data = NetworkTransactionData::Evm(EvmTransactionData {
775            gas_price: Some(2000000000),
776            gas_limit: 30000,
777            nonce: Some(2),
778            value: U256::from_str("2000000000000000000").unwrap(),
779            data: Some("0xUpdated".to_string()),
780            from: "0xSender".to_string(),
781            to: Some("0xRecipient".to_string()),
782            chain_id: 1,
783            signature: None,
784            hash: Some("0xUpdated".to_string()),
785            raw: None,
786            speed: None,
787            max_fee_per_gas: None,
788            max_priority_fee_per_gas: None,
789        });
790
791        let updated = repo
792            .update_network_data("test-1".to_string(), updated_network_data)
793            .await
794            .unwrap();
795
796        // Verify the network data was updated
797        if let NetworkTransactionData::Evm(data) = &updated.network_data {
798            assert_eq!(data.gas_price, Some(2000000000));
799            assert_eq!(data.gas_limit, 30000);
800            assert_eq!(data.nonce, Some(2));
801            assert_eq!(data.hash, Some("0xUpdated".to_string()));
802            assert_eq!(data.data, Some("0xUpdated".to_string()));
803        } else {
804            panic!("Expected EVM network data");
805        }
806    }
807
808    #[tokio::test]
809    async fn test_set_sent_at() {
810        let repo = InMemoryTransactionRepository::new();
811        let tx = create_test_transaction("test-1");
812
813        repo.create(tx).await.unwrap();
814
815        // Updated sent_at timestamp
816        let new_sent_at = "2025-02-01T10:00:00.000000+00:00".to_string();
817
818        let updated = repo
819            .set_sent_at("test-1".to_string(), new_sent_at.clone())
820            .await
821            .unwrap();
822
823        // Verify the sent_at timestamp was updated
824        assert_eq!(updated.sent_at, Some(new_sent_at.clone()));
825
826        // Also verify by getting the transaction directly
827        let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
828        assert_eq!(stored.sent_at, Some(new_sent_at.clone()));
829    }
830
831    #[tokio::test]
832    async fn test_set_confirmed_at() {
833        let repo = InMemoryTransactionRepository::new();
834        let tx = create_test_transaction("test-1");
835
836        repo.create(tx).await.unwrap();
837
838        // Updated confirmed_at timestamp
839        let new_confirmed_at = "2025-02-01T11:30:45.123456+00:00".to_string();
840
841        let updated = repo
842            .set_confirmed_at("test-1".to_string(), new_confirmed_at.clone())
843            .await
844            .unwrap();
845
846        // Verify the confirmed_at timestamp was updated
847        assert_eq!(updated.confirmed_at, Some(new_confirmed_at.clone()));
848
849        // Also verify by getting the transaction directly
850        let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
851        assert_eq!(stored.confirmed_at, Some(new_confirmed_at.clone()));
852    }
853
854    #[tokio::test]
855    async fn test_find_by_relayer_id() {
856        let repo = InMemoryTransactionRepository::new();
857        let tx1 = create_test_transaction("test-1");
858        let tx2 = create_test_transaction("test-2");
859
860        // Create a transaction with a different relayer_id
861        let mut tx3 = create_test_transaction("test-3");
862        tx3.relayer_id = "relayer-2".to_string();
863
864        repo.create(tx1).await.unwrap();
865        repo.create(tx2).await.unwrap();
866        repo.create(tx3).await.unwrap();
867
868        // Test finding transactions for relayer-1
869        let query = PaginationQuery {
870            page: 1,
871            per_page: 10,
872        };
873        let result = repo
874            .find_by_relayer_id("relayer-1", query.clone())
875            .await
876            .unwrap();
877        assert_eq!(result.total, 2);
878        assert_eq!(result.items.len(), 2);
879        assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-1"));
880
881        // Test finding transactions for relayer-2
882        let result = repo
883            .find_by_relayer_id("relayer-2", query.clone())
884            .await
885            .unwrap();
886        assert_eq!(result.total, 1);
887        assert_eq!(result.items.len(), 1);
888        assert!(result.items.iter().all(|tx| tx.relayer_id == "relayer-2"));
889
890        // Test finding transactions for non-existent relayer
891        let result = repo
892            .find_by_relayer_id("non-existent", query.clone())
893            .await
894            .unwrap();
895        assert_eq!(result.total, 0);
896        assert_eq!(result.items.len(), 0);
897    }
898
899    #[tokio::test]
900    async fn test_find_by_status() {
901        let repo = InMemoryTransactionRepository::new();
902        let tx1 = create_test_transaction_pending_state("tx1");
903        let mut tx2 = create_test_transaction_pending_state("tx2");
904        tx2.status = TransactionStatus::Submitted;
905        let mut tx3 = create_test_transaction_pending_state("tx3");
906        tx3.relayer_id = "relayer-2".to_string();
907        tx3.status = TransactionStatus::Pending;
908
909        repo.create(tx1.clone()).await.unwrap();
910        repo.create(tx2.clone()).await.unwrap();
911        repo.create(tx3.clone()).await.unwrap();
912
913        // Test finding by single status
914        let pending_txs = repo
915            .find_by_status("relayer-1", &[TransactionStatus::Pending])
916            .await
917            .unwrap();
918        assert_eq!(pending_txs.len(), 1);
919        assert_eq!(pending_txs[0].id, "tx1");
920
921        let submitted_txs = repo
922            .find_by_status("relayer-1", &[TransactionStatus::Submitted])
923            .await
924            .unwrap();
925        assert_eq!(submitted_txs.len(), 1);
926        assert_eq!(submitted_txs[0].id, "tx2");
927
928        // Test finding by multiple statuses
929        let multiple_status_txs = repo
930            .find_by_status(
931                "relayer-1",
932                &[TransactionStatus::Pending, TransactionStatus::Submitted],
933            )
934            .await
935            .unwrap();
936        assert_eq!(multiple_status_txs.len(), 2);
937
938        // Test finding for different relayer
939        let relayer2_pending = repo
940            .find_by_status("relayer-2", &[TransactionStatus::Pending])
941            .await
942            .unwrap();
943        assert_eq!(relayer2_pending.len(), 1);
944        assert_eq!(relayer2_pending[0].id, "tx3");
945
946        // Test finding for non-existent relayer
947        let no_txs = repo
948            .find_by_status("non-existent", &[TransactionStatus::Pending])
949            .await
950            .unwrap();
951        assert_eq!(no_txs.len(), 0);
952    }
953
954    #[tokio::test]
955    async fn test_find_by_status_sorted_by_created_at() {
956        let repo = InMemoryTransactionRepository::new();
957
958        // Helper function to create transaction with custom created_at timestamp
959        let create_tx_with_timestamp = |id: &str, timestamp: &str| -> TransactionRepoModel {
960            let mut tx = create_test_transaction_pending_state(id);
961            tx.created_at = timestamp.to_string();
962            tx.status = TransactionStatus::Pending;
963            tx
964        };
965
966        // Create transactions with different timestamps (out of chronological order)
967        let tx3 = create_tx_with_timestamp("tx3", "2025-01-27T17:00:00.000000+00:00"); // Latest
968        let tx1 = create_tx_with_timestamp("tx1", "2025-01-27T15:00:00.000000+00:00"); // Earliest
969        let tx2 = create_tx_with_timestamp("tx2", "2025-01-27T16:00:00.000000+00:00"); // Middle
970
971        // Create them in reverse chronological order to test sorting
972        repo.create(tx3.clone()).await.unwrap();
973        repo.create(tx1.clone()).await.unwrap();
974        repo.create(tx2.clone()).await.unwrap();
975
976        // Find by status
977        let result = repo
978            .find_by_status("relayer-1", &[TransactionStatus::Pending])
979            .await
980            .unwrap();
981
982        // Verify they are sorted by created_at (oldest first)
983        assert_eq!(result.len(), 3);
984        assert_eq!(result[0].id, "tx1"); // Earliest
985        assert_eq!(result[1].id, "tx2"); // Middle
986        assert_eq!(result[2].id, "tx3"); // Latest
987
988        // Verify the timestamps are in ascending order
989        assert_eq!(result[0].created_at, "2025-01-27T15:00:00.000000+00:00");
990        assert_eq!(result[1].created_at, "2025-01-27T16:00:00.000000+00:00");
991        assert_eq!(result[2].created_at, "2025-01-27T17:00:00.000000+00:00");
992    }
993}