openzeppelin_relayer/repositories/transaction/
mod.rs

1//! Transaction Repository Module
2//!
3//! This module provides the transaction repository layer for the OpenZeppelin Relayer service.
4//! It implements the Repository pattern to abstract transaction data persistence operations,
5//! supporting both in-memory and Redis-backed storage implementations.
6//!
7//! ## Features
8//!
9//! - **CRUD Operations**: Create, read, update, and delete transactions
10//! - **Specialized Queries**: Find transactions by relayer ID, status, and nonce
11//! - **Pagination Support**: Efficient paginated listing of transactions
12//! - **Status Management**: Update transaction status and timestamps
13//! - **Partial Updates**: Support for partial transaction updates
14//! - **Network Data**: Manage transaction network-specific data
15//!
16//! ## Repository Implementations
17//!
18//! - [`InMemoryTransactionRepository`]: Fast in-memory storage for testing/development
19//! - [`RedisTransactionRepository`]: Redis-backed storage for production environments
20//!
21mod transaction_in_memory;
22mod transaction_redis;
23
24use redis::aio::ConnectionManager;
25pub use transaction_in_memory::*;
26pub use transaction_redis::*;
27
28use crate::{
29    models::{
30        NetworkTransactionData, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
31    },
32    repositories::*,
33};
34use async_trait::async_trait;
35use eyre::Result;
36use std::sync::Arc;
37
38/// A trait defining transaction repository operations
39#[async_trait]
40pub trait TransactionRepository: Repository<TransactionRepoModel, String> {
41    /// Find transactions by relayer ID with pagination
42    async fn find_by_relayer_id(
43        &self,
44        relayer_id: &str,
45        query: PaginationQuery,
46    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
47
48    /// Find transactions by relayer ID and status(es)
49    async fn find_by_status(
50        &self,
51        relayer_id: &str,
52        statuses: &[TransactionStatus],
53    ) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
54
55    /// Find a transaction by relayer ID and nonce
56    async fn find_by_nonce(
57        &self,
58        relayer_id: &str,
59        nonce: u64,
60    ) -> Result<Option<TransactionRepoModel>, RepositoryError>;
61
62    /// Update the status of a transaction
63    async fn update_status(
64        &self,
65        tx_id: String,
66        status: TransactionStatus,
67    ) -> Result<TransactionRepoModel, RepositoryError>;
68
69    /// Partially update a transaction
70    async fn partial_update(
71        &self,
72        tx_id: String,
73        update: TransactionUpdateRequest,
74    ) -> Result<TransactionRepoModel, RepositoryError>;
75
76    /// Update the network data of a transaction
77    async fn update_network_data(
78        &self,
79        tx_id: String,
80        network_data: NetworkTransactionData,
81    ) -> Result<TransactionRepoModel, RepositoryError>;
82
83    /// Set the sent_at timestamp of a transaction
84    async fn set_sent_at(
85        &self,
86        tx_id: String,
87        sent_at: String,
88    ) -> Result<TransactionRepoModel, RepositoryError>;
89
90    /// Set the confirmed_at timestamp of a transaction
91    async fn set_confirmed_at(
92        &self,
93        tx_id: String,
94        confirmed_at: String,
95    ) -> Result<TransactionRepoModel, RepositoryError>;
96}
97
98#[cfg(test)]
99mockall::mock! {
100  pub TransactionRepository {}
101
102  #[async_trait]
103  impl Repository<TransactionRepoModel, String> for TransactionRepository {
104      async fn create(&self, entity: TransactionRepoModel) -> Result<TransactionRepoModel, RepositoryError>;
105      async fn get_by_id(&self, id: String) -> Result<TransactionRepoModel, RepositoryError>;
106      async fn list_all(&self) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
107      async fn list_paginated(&self, query: PaginationQuery) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
108      async fn update(&self, id: String, entity: TransactionRepoModel) -> Result<TransactionRepoModel, RepositoryError>;
109      async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError>;
110      async fn count(&self) -> Result<usize, RepositoryError>;
111      async fn has_entries(&self) -> Result<bool, RepositoryError>;
112      async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
113  }
114
115  #[async_trait]
116  impl TransactionRepository for TransactionRepository {
117      async fn find_by_relayer_id(&self, relayer_id: &str, query: PaginationQuery) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
118      async fn find_by_status(&self, relayer_id: &str, statuses: &[TransactionStatus]) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
119      async fn find_by_nonce(&self, relayer_id: &str, nonce: u64) -> Result<Option<TransactionRepoModel>, RepositoryError>;
120      async fn update_status(&self, tx_id: String, status: TransactionStatus) -> Result<TransactionRepoModel, RepositoryError>;
121      async fn partial_update(&self, tx_id: String, update: TransactionUpdateRequest) -> Result<TransactionRepoModel, RepositoryError>;
122      async fn update_network_data(&self, tx_id: String, network_data: NetworkTransactionData) -> Result<TransactionRepoModel, RepositoryError>;
123      async fn set_sent_at(&self, tx_id: String, sent_at: String) -> Result<TransactionRepoModel, RepositoryError>;
124      async fn set_confirmed_at(&self, tx_id: String, confirmed_at: String) -> Result<TransactionRepoModel, RepositoryError>;
125
126  }
127}
128
129/// Enum wrapper for different transaction repository implementations
130#[derive(Debug, Clone)]
131pub enum TransactionRepositoryStorage {
132    InMemory(InMemoryTransactionRepository),
133    Redis(RedisTransactionRepository),
134}
135
136impl TransactionRepositoryStorage {
137    pub fn new_in_memory() -> Self {
138        Self::InMemory(InMemoryTransactionRepository::new())
139    }
140    pub fn new_redis(
141        connection_manager: Arc<ConnectionManager>,
142        key_prefix: String,
143    ) -> Result<Self, RepositoryError> {
144        Ok(Self::Redis(RedisTransactionRepository::new(
145            connection_manager,
146            key_prefix,
147        )?))
148    }
149}
150
151#[async_trait]
152impl TransactionRepository for TransactionRepositoryStorage {
153    async fn find_by_relayer_id(
154        &self,
155        relayer_id: &str,
156        query: PaginationQuery,
157    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
158        match self {
159            TransactionRepositoryStorage::InMemory(repo) => {
160                repo.find_by_relayer_id(relayer_id, query).await
161            }
162            TransactionRepositoryStorage::Redis(repo) => {
163                repo.find_by_relayer_id(relayer_id, query).await
164            }
165        }
166    }
167
168    async fn find_by_status(
169        &self,
170        relayer_id: &str,
171        statuses: &[TransactionStatus],
172    ) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
173        match self {
174            TransactionRepositoryStorage::InMemory(repo) => {
175                repo.find_by_status(relayer_id, statuses).await
176            }
177            TransactionRepositoryStorage::Redis(repo) => {
178                repo.find_by_status(relayer_id, statuses).await
179            }
180        }
181    }
182
183    async fn find_by_nonce(
184        &self,
185        relayer_id: &str,
186        nonce: u64,
187    ) -> Result<Option<TransactionRepoModel>, RepositoryError> {
188        match self {
189            TransactionRepositoryStorage::InMemory(repo) => {
190                repo.find_by_nonce(relayer_id, nonce).await
191            }
192            TransactionRepositoryStorage::Redis(repo) => {
193                repo.find_by_nonce(relayer_id, nonce).await
194            }
195        }
196    }
197
198    async fn update_status(
199        &self,
200        tx_id: String,
201        status: TransactionStatus,
202    ) -> Result<TransactionRepoModel, RepositoryError> {
203        match self {
204            TransactionRepositoryStorage::InMemory(repo) => repo.update_status(tx_id, status).await,
205            TransactionRepositoryStorage::Redis(repo) => repo.update_status(tx_id, status).await,
206        }
207    }
208
209    async fn partial_update(
210        &self,
211        tx_id: String,
212        update: TransactionUpdateRequest,
213    ) -> Result<TransactionRepoModel, RepositoryError> {
214        match self {
215            TransactionRepositoryStorage::InMemory(repo) => {
216                repo.partial_update(tx_id, update).await
217            }
218            TransactionRepositoryStorage::Redis(repo) => repo.partial_update(tx_id, update).await,
219        }
220    }
221
222    async fn update_network_data(
223        &self,
224        tx_id: String,
225        network_data: NetworkTransactionData,
226    ) -> Result<TransactionRepoModel, RepositoryError> {
227        match self {
228            TransactionRepositoryStorage::InMemory(repo) => {
229                repo.update_network_data(tx_id, network_data).await
230            }
231            TransactionRepositoryStorage::Redis(repo) => {
232                repo.update_network_data(tx_id, network_data).await
233            }
234        }
235    }
236
237    async fn set_sent_at(
238        &self,
239        tx_id: String,
240        sent_at: String,
241    ) -> Result<TransactionRepoModel, RepositoryError> {
242        match self {
243            TransactionRepositoryStorage::InMemory(repo) => repo.set_sent_at(tx_id, sent_at).await,
244            TransactionRepositoryStorage::Redis(repo) => repo.set_sent_at(tx_id, sent_at).await,
245        }
246    }
247
248    async fn set_confirmed_at(
249        &self,
250        tx_id: String,
251        confirmed_at: String,
252    ) -> Result<TransactionRepoModel, RepositoryError> {
253        match self {
254            TransactionRepositoryStorage::InMemory(repo) => {
255                repo.set_confirmed_at(tx_id, confirmed_at).await
256            }
257            TransactionRepositoryStorage::Redis(repo) => {
258                repo.set_confirmed_at(tx_id, confirmed_at).await
259            }
260        }
261    }
262}
263
264#[async_trait]
265impl Repository<TransactionRepoModel, String> for TransactionRepositoryStorage {
266    async fn create(
267        &self,
268        entity: TransactionRepoModel,
269    ) -> Result<TransactionRepoModel, RepositoryError> {
270        match self {
271            TransactionRepositoryStorage::InMemory(repo) => repo.create(entity).await,
272            TransactionRepositoryStorage::Redis(repo) => repo.create(entity).await,
273        }
274    }
275
276    async fn get_by_id(&self, id: String) -> Result<TransactionRepoModel, RepositoryError> {
277        match self {
278            TransactionRepositoryStorage::InMemory(repo) => repo.get_by_id(id).await,
279            TransactionRepositoryStorage::Redis(repo) => repo.get_by_id(id).await,
280        }
281    }
282
283    async fn list_all(&self) -> Result<Vec<TransactionRepoModel>, RepositoryError> {
284        match self {
285            TransactionRepositoryStorage::InMemory(repo) => repo.list_all().await,
286            TransactionRepositoryStorage::Redis(repo) => repo.list_all().await,
287        }
288    }
289
290    async fn list_paginated(
291        &self,
292        query: PaginationQuery,
293    ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError> {
294        match self {
295            TransactionRepositoryStorage::InMemory(repo) => repo.list_paginated(query).await,
296            TransactionRepositoryStorage::Redis(repo) => repo.list_paginated(query).await,
297        }
298    }
299
300    async fn update(
301        &self,
302        id: String,
303        entity: TransactionRepoModel,
304    ) -> Result<TransactionRepoModel, RepositoryError> {
305        match self {
306            TransactionRepositoryStorage::InMemory(repo) => repo.update(id, entity).await,
307            TransactionRepositoryStorage::Redis(repo) => repo.update(id, entity).await,
308        }
309    }
310
311    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
312        match self {
313            TransactionRepositoryStorage::InMemory(repo) => repo.delete_by_id(id).await,
314            TransactionRepositoryStorage::Redis(repo) => repo.delete_by_id(id).await,
315        }
316    }
317
318    async fn count(&self) -> Result<usize, RepositoryError> {
319        match self {
320            TransactionRepositoryStorage::InMemory(repo) => repo.count().await,
321            TransactionRepositoryStorage::Redis(repo) => repo.count().await,
322        }
323    }
324
325    async fn has_entries(&self) -> Result<bool, RepositoryError> {
326        match self {
327            TransactionRepositoryStorage::InMemory(repo) => repo.has_entries().await,
328            TransactionRepositoryStorage::Redis(repo) => repo.has_entries().await,
329        }
330    }
331
332    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
333        match self {
334            TransactionRepositoryStorage::InMemory(repo) => repo.drop_all_entries().await,
335            TransactionRepositoryStorage::Redis(repo) => repo.drop_all_entries().await,
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::models::{
344        EvmTransactionData, NetworkTransactionData, TransactionStatus, TransactionUpdateRequest,
345    };
346    use crate::repositories::PaginationQuery;
347    use crate::utils::mocks::mockutils::create_mock_transaction;
348    use chrono::Utc;
349    use color_eyre::Result;
350
351    fn create_test_transaction(id: &str, relayer_id: &str) -> TransactionRepoModel {
352        let mut transaction = create_mock_transaction();
353        transaction.id = id.to_string();
354        transaction.relayer_id = relayer_id.to_string();
355        transaction
356    }
357
358    fn create_test_transaction_with_status(
359        id: &str,
360        relayer_id: &str,
361        status: TransactionStatus,
362    ) -> TransactionRepoModel {
363        let mut transaction = create_test_transaction(id, relayer_id);
364        transaction.status = status;
365        transaction
366    }
367
368    fn create_test_transaction_with_nonce(
369        id: &str,
370        relayer_id: &str,
371        nonce: u64,
372    ) -> TransactionRepoModel {
373        let mut transaction = create_test_transaction(id, relayer_id);
374        if let NetworkTransactionData::Evm(ref mut evm_data) = transaction.network_data {
375            evm_data.nonce = Some(nonce);
376        }
377        transaction
378    }
379
380    fn create_test_update_request() -> TransactionUpdateRequest {
381        TransactionUpdateRequest {
382            status: Some(TransactionStatus::Sent),
383            status_reason: Some("Test reason".to_string()),
384            sent_at: Some(Utc::now().to_string()),
385            confirmed_at: None,
386            network_data: None,
387            priced_at: None,
388            hashes: Some(vec!["test_hash".to_string()]),
389            noop_count: None,
390            is_canceled: None,
391            delete_at: None,
392        }
393    }
394
395    #[tokio::test]
396    async fn test_new_in_memory() {
397        let storage = TransactionRepositoryStorage::new_in_memory();
398
399        match storage {
400            TransactionRepositoryStorage::InMemory(_) => {
401                // Success - verify it's the InMemory variant
402            }
403            TransactionRepositoryStorage::Redis(_) => {
404                panic!("Expected InMemory variant, got Redis");
405            }
406        }
407    }
408
409    #[tokio::test]
410    async fn test_create_in_memory() -> Result<()> {
411        let storage = TransactionRepositoryStorage::new_in_memory();
412        let transaction = create_test_transaction("test-tx", "test-relayer");
413
414        let created = storage.create(transaction.clone()).await?;
415        assert_eq!(created.id, transaction.id);
416        assert_eq!(created.relayer_id, transaction.relayer_id);
417        assert_eq!(created.status, transaction.status);
418
419        Ok(())
420    }
421
422    #[tokio::test]
423    async fn test_get_by_id_in_memory() -> Result<()> {
424        let storage = TransactionRepositoryStorage::new_in_memory();
425        let transaction = create_test_transaction("test-tx", "test-relayer");
426
427        // Create transaction first
428        storage.create(transaction.clone()).await?;
429
430        // Get by ID
431        let retrieved = storage.get_by_id("test-tx".to_string()).await?;
432        assert_eq!(retrieved.id, transaction.id);
433        assert_eq!(retrieved.relayer_id, transaction.relayer_id);
434        assert_eq!(retrieved.status, transaction.status);
435
436        Ok(())
437    }
438
439    #[tokio::test]
440    async fn test_get_by_id_not_found_in_memory() -> Result<()> {
441        let storage = TransactionRepositoryStorage::new_in_memory();
442
443        let result = storage.get_by_id("non-existent".to_string()).await;
444        assert!(result.is_err());
445
446        Ok(())
447    }
448
449    #[tokio::test]
450    async fn test_list_all_in_memory() -> Result<()> {
451        let storage = TransactionRepositoryStorage::new_in_memory();
452
453        // Initially empty
454        let transactions = storage.list_all().await?;
455        assert!(transactions.is_empty());
456
457        // Add transactions
458        let tx1 = create_test_transaction("tx-1", "relayer-1");
459        let tx2 = create_test_transaction("tx-2", "relayer-2");
460
461        storage.create(tx1.clone()).await?;
462        storage.create(tx2.clone()).await?;
463
464        let all_transactions = storage.list_all().await?;
465        assert_eq!(all_transactions.len(), 2);
466
467        let ids: Vec<&str> = all_transactions.iter().map(|t| t.id.as_str()).collect();
468        assert!(ids.contains(&"tx-1"));
469        assert!(ids.contains(&"tx-2"));
470
471        Ok(())
472    }
473
474    #[tokio::test]
475    async fn test_list_paginated_in_memory() -> Result<()> {
476        let storage = TransactionRepositoryStorage::new_in_memory();
477
478        // Add test transactions
479        for i in 1..=5 {
480            let tx = create_test_transaction(&format!("tx-{}", i), "test-relayer");
481            storage.create(tx).await?;
482        }
483
484        // Test pagination
485        let query = PaginationQuery {
486            page: 1,
487            per_page: 2,
488        };
489        let page = storage.list_paginated(query).await?;
490
491        assert_eq!(page.items.len(), 2);
492        assert_eq!(page.total, 5);
493        assert_eq!(page.page, 1);
494        assert_eq!(page.per_page, 2);
495
496        // Test second page
497        let query2 = PaginationQuery {
498            page: 2,
499            per_page: 2,
500        };
501        let page2 = storage.list_paginated(query2).await?;
502
503        assert_eq!(page2.items.len(), 2);
504        assert_eq!(page2.total, 5);
505        assert_eq!(page2.page, 2);
506        assert_eq!(page2.per_page, 2);
507
508        Ok(())
509    }
510
511    #[tokio::test]
512    async fn test_update_in_memory() -> Result<()> {
513        let storage = TransactionRepositoryStorage::new_in_memory();
514        let transaction = create_test_transaction("test-tx", "test-relayer");
515
516        // Create transaction first
517        storage.create(transaction.clone()).await?;
518
519        // Update it
520        let mut updated_transaction = transaction.clone();
521        updated_transaction.status = TransactionStatus::Sent;
522        updated_transaction.status_reason = Some("Updated reason".to_string());
523
524        let result = storage
525            .update("test-tx".to_string(), updated_transaction.clone())
526            .await?;
527        assert_eq!(result.id, "test-tx");
528        assert_eq!(result.status, TransactionStatus::Sent);
529        assert_eq!(result.status_reason, Some("Updated reason".to_string()));
530
531        // Verify the update persisted
532        let retrieved = storage.get_by_id("test-tx".to_string()).await?;
533        assert_eq!(retrieved.status, TransactionStatus::Sent);
534        assert_eq!(retrieved.status_reason, Some("Updated reason".to_string()));
535
536        Ok(())
537    }
538
539    #[tokio::test]
540    async fn test_update_not_found_in_memory() -> Result<()> {
541        let storage = TransactionRepositoryStorage::new_in_memory();
542        let transaction = create_test_transaction("non-existent", "test-relayer");
543
544        let result = storage
545            .update("non-existent".to_string(), transaction)
546            .await;
547        assert!(result.is_err());
548
549        Ok(())
550    }
551
552    #[tokio::test]
553    async fn test_delete_by_id_in_memory() -> Result<()> {
554        let storage = TransactionRepositoryStorage::new_in_memory();
555        let transaction = create_test_transaction("test-tx", "test-relayer");
556
557        // Create transaction first
558        storage.create(transaction.clone()).await?;
559
560        // Verify it exists
561        let retrieved = storage.get_by_id("test-tx".to_string()).await?;
562        assert_eq!(retrieved.id, "test-tx");
563
564        // Delete it
565        storage.delete_by_id("test-tx".to_string()).await?;
566
567        // Verify it's gone
568        let result = storage.get_by_id("test-tx".to_string()).await;
569        assert!(result.is_err());
570
571        Ok(())
572    }
573
574    #[tokio::test]
575    async fn test_delete_by_id_not_found_in_memory() -> Result<()> {
576        let storage = TransactionRepositoryStorage::new_in_memory();
577
578        let result = storage.delete_by_id("non-existent".to_string()).await;
579        assert!(result.is_err());
580
581        Ok(())
582    }
583
584    #[tokio::test]
585    async fn test_count_in_memory() -> Result<()> {
586        let storage = TransactionRepositoryStorage::new_in_memory();
587
588        // Initially empty
589        let count = storage.count().await?;
590        assert_eq!(count, 0);
591
592        // Add transactions
593        let tx1 = create_test_transaction("tx-1", "relayer-1");
594        let tx2 = create_test_transaction("tx-2", "relayer-2");
595
596        storage.create(tx1).await?;
597        let count_after_one = storage.count().await?;
598        assert_eq!(count_after_one, 1);
599
600        storage.create(tx2).await?;
601        let count_after_two = storage.count().await?;
602        assert_eq!(count_after_two, 2);
603
604        // Delete one
605        storage.delete_by_id("tx-1".to_string()).await?;
606        let count_after_delete = storage.count().await?;
607        assert_eq!(count_after_delete, 1);
608
609        Ok(())
610    }
611
612    #[tokio::test]
613    async fn test_has_entries_in_memory() -> Result<()> {
614        let storage = TransactionRepositoryStorage::new_in_memory();
615
616        // Initially empty
617        let has_entries = storage.has_entries().await?;
618        assert!(!has_entries);
619
620        // Add transaction
621        let transaction = create_test_transaction("test-tx", "test-relayer");
622        storage.create(transaction).await?;
623
624        let has_entries_after_create = storage.has_entries().await?;
625        assert!(has_entries_after_create);
626
627        // Delete transaction
628        storage.delete_by_id("test-tx".to_string()).await?;
629
630        let has_entries_after_delete = storage.has_entries().await?;
631        assert!(!has_entries_after_delete);
632
633        Ok(())
634    }
635
636    #[tokio::test]
637    async fn test_drop_all_entries_in_memory() -> Result<()> {
638        let storage = TransactionRepositoryStorage::new_in_memory();
639
640        // Add multiple transactions
641        for i in 1..=5 {
642            let tx = create_test_transaction(&format!("tx-{}", i), "test-relayer");
643            storage.create(tx).await?;
644        }
645
646        // Verify they exist
647        let count_before = storage.count().await?;
648        assert_eq!(count_before, 5);
649
650        let has_entries_before = storage.has_entries().await?;
651        assert!(has_entries_before);
652
653        // Drop all entries
654        storage.drop_all_entries().await?;
655
656        // Verify they're gone
657        let count_after = storage.count().await?;
658        assert_eq!(count_after, 0);
659
660        let has_entries_after = storage.has_entries().await?;
661        assert!(!has_entries_after);
662
663        let all_transactions = storage.list_all().await?;
664        assert!(all_transactions.is_empty());
665
666        Ok(())
667    }
668
669    #[tokio::test]
670    async fn test_find_by_relayer_id_in_memory() -> Result<()> {
671        let storage = TransactionRepositoryStorage::new_in_memory();
672
673        // Add transactions for different relayers
674        let tx1 = create_test_transaction("tx-1", "relayer-1");
675        let tx2 = create_test_transaction("tx-2", "relayer-1");
676        let tx3 = create_test_transaction("tx-3", "relayer-2");
677
678        storage.create(tx1).await?;
679        storage.create(tx2).await?;
680        storage.create(tx3).await?;
681
682        // Find by relayer ID
683        let query = PaginationQuery {
684            page: 1,
685            per_page: 10,
686        };
687        let result = storage.find_by_relayer_id("relayer-1", query).await?;
688
689        assert_eq!(result.items.len(), 2);
690        assert_eq!(result.total, 2);
691
692        // Verify all transactions belong to relayer-1
693        for tx in result.items {
694            assert_eq!(tx.relayer_id, "relayer-1");
695        }
696
697        Ok(())
698    }
699
700    #[tokio::test]
701    async fn test_find_by_status_in_memory() -> Result<()> {
702        let storage = TransactionRepositoryStorage::new_in_memory();
703
704        // Add transactions with different statuses
705        let tx1 =
706            create_test_transaction_with_status("tx-1", "relayer-1", TransactionStatus::Pending);
707        let tx2 = create_test_transaction_with_status("tx-2", "relayer-1", TransactionStatus::Sent);
708        let tx3 =
709            create_test_transaction_with_status("tx-3", "relayer-1", TransactionStatus::Pending);
710        let tx4 =
711            create_test_transaction_with_status("tx-4", "relayer-2", TransactionStatus::Pending);
712
713        storage.create(tx1).await?;
714        storage.create(tx2).await?;
715        storage.create(tx3).await?;
716        storage.create(tx4).await?;
717
718        // Find by status
719        let statuses = vec![TransactionStatus::Pending];
720        let result = storage.find_by_status("relayer-1", &statuses).await?;
721
722        assert_eq!(result.len(), 2);
723
724        // Verify all transactions have Pending status and belong to relayer-1
725        for tx in result {
726            assert_eq!(tx.status, TransactionStatus::Pending);
727            assert_eq!(tx.relayer_id, "relayer-1");
728        }
729
730        Ok(())
731    }
732
733    #[tokio::test]
734    async fn test_find_by_nonce_in_memory() -> Result<()> {
735        let storage = TransactionRepositoryStorage::new_in_memory();
736
737        // Add transactions with different nonces
738        let tx1 = create_test_transaction_with_nonce("tx-1", "relayer-1", 10);
739        let tx2 = create_test_transaction_with_nonce("tx-2", "relayer-1", 20);
740        let tx3 = create_test_transaction_with_nonce("tx-3", "relayer-2", 10);
741
742        storage.create(tx1).await?;
743        storage.create(tx2).await?;
744        storage.create(tx3).await?;
745
746        // Find by nonce
747        let result = storage.find_by_nonce("relayer-1", 10).await?;
748
749        assert!(result.is_some());
750        let found_tx = result.unwrap();
751        assert_eq!(found_tx.id, "tx-1");
752        assert_eq!(found_tx.relayer_id, "relayer-1");
753
754        // Check EVM nonce
755        if let NetworkTransactionData::Evm(evm_data) = found_tx.network_data {
756            assert_eq!(evm_data.nonce, Some(10));
757        }
758
759        // Test not found
760        let not_found = storage.find_by_nonce("relayer-1", 99).await?;
761        assert!(not_found.is_none());
762
763        Ok(())
764    }
765
766    #[tokio::test]
767    async fn test_update_status_in_memory() -> Result<()> {
768        let storage = TransactionRepositoryStorage::new_in_memory();
769        let transaction = create_test_transaction("test-tx", "test-relayer");
770
771        // Create transaction first
772        storage.create(transaction).await?;
773
774        // Update status
775        let updated = storage
776            .update_status("test-tx".to_string(), TransactionStatus::Sent)
777            .await?;
778
779        assert_eq!(updated.id, "test-tx");
780        assert_eq!(updated.status, TransactionStatus::Sent);
781
782        // Verify the update persisted
783        let retrieved = storage.get_by_id("test-tx".to_string()).await?;
784        assert_eq!(retrieved.status, TransactionStatus::Sent);
785
786        Ok(())
787    }
788
789    #[tokio::test]
790    async fn test_partial_update_in_memory() -> Result<()> {
791        let storage = TransactionRepositoryStorage::new_in_memory();
792        let transaction = create_test_transaction("test-tx", "test-relayer");
793
794        // Create transaction first
795        storage.create(transaction).await?;
796
797        // Partial update
798        let update_request = create_test_update_request();
799        let updated = storage
800            .partial_update("test-tx".to_string(), update_request)
801            .await?;
802
803        assert_eq!(updated.id, "test-tx");
804        assert_eq!(updated.status, TransactionStatus::Sent);
805        assert_eq!(updated.status_reason, Some("Test reason".to_string()));
806        assert!(updated.sent_at.is_some());
807        assert_eq!(updated.hashes, vec!["test_hash".to_string()]);
808
809        Ok(())
810    }
811
812    #[tokio::test]
813    async fn test_update_network_data_in_memory() -> Result<()> {
814        let storage = TransactionRepositoryStorage::new_in_memory();
815        let transaction = create_test_transaction("test-tx", "test-relayer");
816
817        // Create transaction first
818        storage.create(transaction).await?;
819
820        // Update network data
821        let new_evm_data = EvmTransactionData {
822            nonce: Some(42),
823            gas_limit: Some(21000),
824            ..Default::default()
825        };
826        let new_network_data = NetworkTransactionData::Evm(new_evm_data);
827
828        let updated = storage
829            .update_network_data("test-tx".to_string(), new_network_data)
830            .await?;
831
832        assert_eq!(updated.id, "test-tx");
833        if let NetworkTransactionData::Evm(evm_data) = updated.network_data {
834            assert_eq!(evm_data.nonce, Some(42));
835            assert_eq!(evm_data.gas_limit, Some(21000));
836        } else {
837            panic!("Expected EVM network data");
838        }
839
840        Ok(())
841    }
842
843    #[tokio::test]
844    async fn test_set_sent_at_in_memory() -> Result<()> {
845        let storage = TransactionRepositoryStorage::new_in_memory();
846        let transaction = create_test_transaction("test-tx", "test-relayer");
847
848        // Create transaction first
849        storage.create(transaction).await?;
850
851        // Set sent_at
852        let sent_at = Utc::now().to_string();
853        let updated = storage
854            .set_sent_at("test-tx".to_string(), sent_at.clone())
855            .await?;
856
857        assert_eq!(updated.id, "test-tx");
858        assert_eq!(updated.sent_at, Some(sent_at));
859
860        Ok(())
861    }
862
863    #[tokio::test]
864    async fn test_set_confirmed_at_in_memory() -> Result<()> {
865        let storage = TransactionRepositoryStorage::new_in_memory();
866        let transaction = create_test_transaction("test-tx", "test-relayer");
867
868        // Create transaction first
869        storage.create(transaction).await?;
870
871        // Set confirmed_at
872        let confirmed_at = Utc::now().to_string();
873        let updated = storage
874            .set_confirmed_at("test-tx".to_string(), confirmed_at.clone())
875            .await?;
876
877        assert_eq!(updated.id, "test-tx");
878        assert_eq!(updated.confirmed_at, Some(confirmed_at));
879
880        Ok(())
881    }
882
883    #[tokio::test]
884    async fn test_create_duplicate_id_in_memory() -> Result<()> {
885        let storage = TransactionRepositoryStorage::new_in_memory();
886        let transaction = create_test_transaction("duplicate-id", "test-relayer");
887
888        // Create first transaction
889        storage.create(transaction.clone()).await?;
890
891        // Try to create another with same ID - should fail
892        let result = storage.create(transaction.clone()).await;
893        assert!(result.is_err());
894
895        Ok(())
896    }
897
898    #[tokio::test]
899    async fn test_workflow_in_memory() -> Result<()> {
900        let storage = TransactionRepositoryStorage::new_in_memory();
901
902        // 1. Start with empty storage
903        assert!(!storage.has_entries().await?);
904        assert_eq!(storage.count().await?, 0);
905
906        // 2. Create transaction
907        let transaction = create_test_transaction("workflow-test", "test-relayer");
908        let created = storage.create(transaction.clone()).await?;
909        assert_eq!(created.id, "workflow-test");
910
911        // 3. Verify it exists
912        assert!(storage.has_entries().await?);
913        assert_eq!(storage.count().await?, 1);
914
915        // 4. Retrieve it
916        let retrieved = storage.get_by_id("workflow-test".to_string()).await?;
917        assert_eq!(retrieved.id, "workflow-test");
918
919        // 5. Update status
920        let updated = storage
921            .update_status("workflow-test".to_string(), TransactionStatus::Sent)
922            .await?;
923        assert_eq!(updated.status, TransactionStatus::Sent);
924
925        // 6. Verify update
926        let retrieved_updated = storage.get_by_id("workflow-test".to_string()).await?;
927        assert_eq!(retrieved_updated.status, TransactionStatus::Sent);
928
929        // 7. Delete it
930        storage.delete_by_id("workflow-test".to_string()).await?;
931
932        // 8. Verify it's gone
933        assert!(!storage.has_entries().await?);
934        assert_eq!(storage.count().await?, 0);
935
936        let result = storage.get_by_id("workflow-test".to_string()).await;
937        assert!(result.is_err());
938
939        Ok(())
940    }
941
942    #[tokio::test]
943    async fn test_multiple_relayers_workflow() -> Result<()> {
944        let storage = TransactionRepositoryStorage::new_in_memory();
945
946        // Add transactions for multiple relayers
947        let tx1 =
948            create_test_transaction_with_status("tx-1", "relayer-1", TransactionStatus::Pending);
949        let tx2 = create_test_transaction_with_status("tx-2", "relayer-1", TransactionStatus::Sent);
950        let tx3 =
951            create_test_transaction_with_status("tx-3", "relayer-2", TransactionStatus::Pending);
952
953        storage.create(tx1).await?;
954        storage.create(tx2).await?;
955        storage.create(tx3).await?;
956
957        // Test find_by_relayer_id
958        let query = PaginationQuery {
959            page: 1,
960            per_page: 10,
961        };
962        let relayer1_txs = storage.find_by_relayer_id("relayer-1", query).await?;
963        assert_eq!(relayer1_txs.items.len(), 2);
964
965        // Test find_by_status
966        let pending_txs = storage
967            .find_by_status("relayer-1", &[TransactionStatus::Pending])
968            .await?;
969        assert_eq!(pending_txs.len(), 1);
970        assert_eq!(pending_txs[0].id, "tx-1");
971
972        // Test count remains accurate
973        assert_eq!(storage.count().await?, 3);
974
975        Ok(())
976    }
977
978    #[tokio::test]
979    async fn test_pagination_edge_cases_in_memory() -> Result<()> {
980        let storage = TransactionRepositoryStorage::new_in_memory();
981
982        // Test pagination with empty storage
983        let query = PaginationQuery {
984            page: 1,
985            per_page: 10,
986        };
987        let page = storage.list_paginated(query).await?;
988        assert_eq!(page.items.len(), 0);
989        assert_eq!(page.total, 0);
990        assert_eq!(page.page, 1);
991        assert_eq!(page.per_page, 10);
992
993        // Add one transaction
994        let transaction = create_test_transaction("single-tx", "test-relayer");
995        storage.create(transaction).await?;
996
997        // Test pagination with single item
998        let query = PaginationQuery {
999            page: 1,
1000            per_page: 10,
1001        };
1002        let page = storage.list_paginated(query).await?;
1003        assert_eq!(page.items.len(), 1);
1004        assert_eq!(page.total, 1);
1005        assert_eq!(page.page, 1);
1006        assert_eq!(page.per_page, 10);
1007
1008        // Test pagination with page beyond total
1009        let query = PaginationQuery {
1010            page: 3,
1011            per_page: 10,
1012        };
1013        let page = storage.list_paginated(query).await?;
1014        assert_eq!(page.items.len(), 0);
1015        assert_eq!(page.total, 1);
1016        assert_eq!(page.page, 3);
1017        assert_eq!(page.per_page, 10);
1018
1019        Ok(())
1020    }
1021
1022    #[tokio::test]
1023    async fn test_find_by_relayer_id_pagination() -> Result<()> {
1024        let storage = TransactionRepositoryStorage::new_in_memory();
1025
1026        // Add many transactions for one relayer
1027        for i in 1..=10 {
1028            let tx = create_test_transaction(&format!("tx-{}", i), "test-relayer");
1029            storage.create(tx).await?;
1030        }
1031
1032        // Test first page
1033        let query = PaginationQuery {
1034            page: 1,
1035            per_page: 3,
1036        };
1037        let page1 = storage.find_by_relayer_id("test-relayer", query).await?;
1038        assert_eq!(page1.items.len(), 3);
1039        assert_eq!(page1.total, 10);
1040        assert_eq!(page1.page, 1);
1041        assert_eq!(page1.per_page, 3);
1042
1043        // Test second page
1044        let query = PaginationQuery {
1045            page: 2,
1046            per_page: 3,
1047        };
1048        let page2 = storage.find_by_relayer_id("test-relayer", query).await?;
1049        assert_eq!(page2.items.len(), 3);
1050        assert_eq!(page2.total, 10);
1051        assert_eq!(page2.page, 2);
1052        assert_eq!(page2.per_page, 3);
1053
1054        Ok(())
1055    }
1056
1057    #[tokio::test]
1058    async fn test_find_by_multiple_statuses() -> Result<()> {
1059        let storage = TransactionRepositoryStorage::new_in_memory();
1060
1061        // Add transactions with different statuses
1062        let tx1 =
1063            create_test_transaction_with_status("tx-1", "test-relayer", TransactionStatus::Pending);
1064        let tx2 =
1065            create_test_transaction_with_status("tx-2", "test-relayer", TransactionStatus::Sent);
1066        let tx3 = create_test_transaction_with_status(
1067            "tx-3",
1068            "test-relayer",
1069            TransactionStatus::Confirmed,
1070        );
1071        let tx4 =
1072            create_test_transaction_with_status("tx-4", "test-relayer", TransactionStatus::Failed);
1073
1074        storage.create(tx1).await?;
1075        storage.create(tx2).await?;
1076        storage.create(tx3).await?;
1077        storage.create(tx4).await?;
1078
1079        // Find by multiple statuses
1080        let statuses = vec![TransactionStatus::Pending, TransactionStatus::Sent];
1081        let result = storage.find_by_status("test-relayer", &statuses).await?;
1082
1083        assert_eq!(result.len(), 2);
1084
1085        // Verify all transactions have the correct statuses
1086        let found_statuses: Vec<TransactionStatus> =
1087            result.iter().map(|tx| tx.status.clone()).collect();
1088        assert!(found_statuses.contains(&TransactionStatus::Pending));
1089        assert!(found_statuses.contains(&TransactionStatus::Sent));
1090
1091        Ok(())
1092    }
1093}