openzeppelin_relayer/services/transaction_counter/
mod.rs

1//! This module provides a service for managing transaction counters.
2//!
3//! The `TransactionCounterService` struct offers methods to get, increment,
4//! decrement, and set transaction counts associated with a specific relayer
5//! and address. It uses an in-memory store to keep track of these counts.
6use std::sync::Arc;
7
8use crate::repositories::{TransactionCounterError, TransactionCounterTrait};
9use async_trait::async_trait;
10
11#[cfg(test)]
12use mockall::automock;
13
14#[derive(Clone, Debug)]
15pub struct TransactionCounterService<T> {
16    relayer_id: String,
17    address: String,
18    store: Arc<T>,
19}
20
21impl<T> TransactionCounterService<T> {
22    pub fn new(relayer_id: String, address: String, store: Arc<T>) -> Self {
23        Self {
24            relayer_id,
25            address,
26            store,
27        }
28    }
29}
30
31#[async_trait]
32#[cfg_attr(test, automock)]
33pub trait TransactionCounterServiceTrait: Send + Sync {
34    async fn get(&self) -> Result<Option<u64>, TransactionCounterError>;
35    async fn get_and_increment(&self) -> Result<u64, TransactionCounterError>;
36    async fn decrement(&self) -> Result<u64, TransactionCounterError>;
37    async fn set(&self, value: u64) -> Result<(), TransactionCounterError>;
38
39    /// Monotonically raise the counter to `floor` only if the current value is lower.
40    ///
41    /// Used for race-safe sync with the live chain sequence: the chain value is treated as a
42    /// floor, never as the authoritative next assignment, so concurrent allocations that have
43    /// already advanced the counter beyond `floor` are never rewound. Returns the effective
44    /// value after the operation (always `>= floor`).
45    async fn sync_floor(&self, floor: u64) -> Result<u64, TransactionCounterError>;
46}
47
48#[async_trait]
49#[allow(dead_code)]
50impl<T> TransactionCounterServiceTrait for TransactionCounterService<T>
51where
52    T: TransactionCounterTrait + Send + Sync,
53{
54    async fn get(&self) -> Result<Option<u64>, TransactionCounterError> {
55        self.store
56            .get(&self.relayer_id, &self.address)
57            .await
58            .map_err(|e| TransactionCounterError::NotFound(e.to_string()))
59    }
60
61    async fn get_and_increment(&self) -> Result<u64, TransactionCounterError> {
62        self.store
63            .get_and_increment(&self.relayer_id, &self.address)
64            .await
65            .map_err(|e| TransactionCounterError::NotFound(e.to_string()))
66    }
67
68    async fn decrement(&self) -> Result<u64, TransactionCounterError> {
69        self.store
70            .decrement(&self.relayer_id, &self.address)
71            .await
72            .map_err(|e| TransactionCounterError::NotFound(e.to_string()))
73    }
74
75    async fn set(&self, value: u64) -> Result<(), TransactionCounterError> {
76        self.store
77            .set(&self.relayer_id, &self.address, value)
78            .await
79            .map_err(|e| TransactionCounterError::NotFound(e.to_string()))
80    }
81
82    async fn sync_floor(&self, floor: u64) -> Result<u64, TransactionCounterError> {
83        self.store
84            .sync_floor(&self.relayer_id, &self.address, floor)
85            .await
86            .map_err(|e| TransactionCounterError::NotFound(e.to_string()))
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::repositories::InMemoryTransactionCounter;
94
95    #[tokio::test]
96    async fn test_transaction_counter() {
97        let store = Arc::new(InMemoryTransactionCounter::default());
98        let service =
99            TransactionCounterService::new("relayer_id".to_string(), "address".to_string(), store);
100
101        assert_eq!(service.get().await.unwrap(), None);
102        assert_eq!(service.get_and_increment().await.unwrap(), 0);
103        assert_eq!(service.get_and_increment().await.unwrap(), 1);
104        assert_eq!(service.decrement().await.unwrap(), 1);
105        assert!(service.set(10).await.is_ok());
106        assert_eq!(service.get().await.unwrap(), Some(10));
107    }
108
109    #[tokio::test]
110    async fn test_sync_floor_does_not_rewind() {
111        let store = Arc::new(InMemoryTransactionCounter::default());
112        let service =
113            TransactionCounterService::new("relayer_id".to_string(), "address".to_string(), store);
114
115        // Counter already advanced past the chain floor by concurrent allocations.
116        service.set(15).await.unwrap();
117
118        // A stale/lower chain floor must not rewind the counter.
119        let effective = service.sync_floor(6).await.unwrap();
120        assert_eq!(effective, 15);
121        assert_eq!(service.get().await.unwrap(), Some(15));
122
123        // A genuinely-ahead floor does advance the counter.
124        let effective = service.sync_floor(20).await.unwrap();
125        assert_eq!(effective, 20);
126        assert_eq!(service.get().await.unwrap(), Some(20));
127    }
128}