1use crate::{
8 models::{
9 NetworkTransactionData, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
10 },
11 repositories::*,
12};
13use async_trait::async_trait;
14use eyre::Result;
15use itertools::Itertools;
16use std::collections::HashMap;
17use tokio::sync::{Mutex, MutexGuard};
18
19#[async_trait]
21pub trait TransactionRepository: Repository<TransactionRepoModel, String> {
22 async fn find_by_relayer_id(
24 &self,
25 relayer_id: &str,
26 query: PaginationQuery,
27 ) -> Result<PaginatedResult<TransactionRepoModel>, RepositoryError>;
28
29 async fn find_by_status(
31 &self,
32 relayer_id: &str,
33 statuses: &[TransactionStatus],
34 ) -> Result<Vec<TransactionRepoModel>, RepositoryError>;
35
36 async fn find_by_nonce(
38 &self,
39 relayer_id: &str,
40 nonce: u64,
41 ) -> Result<Option<TransactionRepoModel>, RepositoryError>;
42
43 async fn update_status(
45 &self,
46 tx_id: String,
47 status: TransactionStatus,
48 ) -> Result<TransactionRepoModel, RepositoryError>;
49
50 async fn partial_update(
52 &self,
53 tx_id: String,
54 update: TransactionUpdateRequest,
55 ) -> Result<TransactionRepoModel, RepositoryError>;
56
57 async fn update_network_data(
59 &self,
60 tx_id: String,
61 network_data: NetworkTransactionData,
62 ) -> Result<TransactionRepoModel, RepositoryError>;
63
64 async fn set_sent_at(
66 &self,
67 tx_id: String,
68 sent_at: String,
69 ) -> Result<TransactionRepoModel, RepositoryError>;
70
71 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#[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 let items = filtered
245 .into_iter()
246 .sorted_by(|a, b| a.created_at.cmp(&b.created_at)) .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 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 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 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 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 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 let updated = repo
644 .update_status("test-1".to_string(), TransactionStatus::Confirmed)
645 .await
646 .unwrap();
647
648 assert_eq!(updated.status, TransactionStatus::Confirmed);
650
651 let stored = repo.get_by_id("test-1".to_string()).await.unwrap();
653 assert_eq!(stored.status, TransactionStatus::Confirmed);
654
655 let updated = repo
657 .update_status("test-1".to_string(), TransactionStatus::Failed)
658 .await
659 .unwrap();
660
661 assert_eq!(updated.status, TransactionStatus::Failed);
663
664 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 for i in 1..=10 {
677 let tx = create_test_transaction(&format!("test-{}", i));
678 repo.create(tx).await.unwrap();
679 }
680
681 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(updated.sent_at, Some(new_sent_at.clone()));
825
826 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 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 assert_eq!(updated.confirmed_at, Some(new_confirmed_at.clone()));
848
849 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 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 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 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 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 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 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 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 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 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 let tx3 = create_tx_with_timestamp("tx3", "2025-01-27T17:00:00.000000+00:00"); let tx1 = create_tx_with_timestamp("tx1", "2025-01-27T15:00:00.000000+00:00"); let tx2 = create_tx_with_timestamp("tx2", "2025-01-27T16:00:00.000000+00:00"); repo.create(tx3.clone()).await.unwrap();
973 repo.create(tx1.clone()).await.unwrap();
974 repo.create(tx2.clone()).await.unwrap();
975
976 let result = repo
978 .find_by_status("relayer-1", &[TransactionStatus::Pending])
979 .await
980 .unwrap();
981
982 assert_eq!(result.len(), 3);
984 assert_eq!(result[0].id, "tx1"); assert_eq!(result[1].id, "tx2"); assert_eq!(result[2].id, "tx3"); 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}