Skip to main content

nectar_postage_issuer/
factory.rs

1//! Batch factory traits for creating batches.
2
3use nectar_postage::{Batch, BatchId, BatchParams};
4
5/// The result of creating a batch.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct CreateResult {
8    /// The created batch.
9    pub batch: Batch,
10    /// The transaction hash (if created on-chain).
11    pub tx_hash: Option<alloy_primitives::B256>,
12}
13
14/// A trait for creating postage batches.
15///
16/// Implementations may create batches on-chain (by sending transactions
17/// to the postage stamp contract) or in-memory for testing.
18pub trait BatchFactory {
19    /// The error type returned by factory operations.
20    type Error: std::error::Error;
21
22    /// Creates a new batch with the given parameters.
23    ///
24    /// For on-chain implementations, this sends a transaction to the
25    /// postage stamp contract and waits for confirmation.
26    ///
27    /// # Arguments
28    ///
29    /// * `params` - The batch parameters (owner, depth, bucket_depth, etc.)
30    ///
31    /// # Returns
32    ///
33    /// A `CreateResult` containing the created batch and optional transaction hash.
34    fn create(
35        &self,
36        params: BatchParams,
37    ) -> impl std::future::Future<Output = Result<CreateResult, Self::Error>> + Send;
38
39    /// Tops up a batch with additional funds.
40    ///
41    /// # Arguments
42    ///
43    /// * `batch_id` - The ID of the batch to top up
44    /// * `amount` - The amount to add to the batch
45    ///
46    /// # Returns
47    ///
48    /// The new normalized balance of the batch.
49    fn top_up(
50        &self,
51        batch_id: BatchId,
52        amount: u128,
53    ) -> impl std::future::Future<Output = Result<u128, Self::Error>> + Send;
54
55    /// Dilutes a batch by increasing its depth.
56    ///
57    /// This doubles the capacity of the batch for each depth increase,
58    /// but halves the remaining TTL.
59    ///
60    /// # Arguments
61    ///
62    /// * `batch_id` - The ID of the batch to dilute
63    /// * `new_depth` - The new depth (must be greater than current depth)
64    fn dilute(
65        &self,
66        batch_id: BatchId,
67        new_depth: u8,
68    ) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
69}
70
71/// An in-memory batch factory for testing.
72///
73/// This implementation creates batches in memory without any blockchain
74/// interaction. Useful for unit tests and local development.
75#[derive(Debug)]
76pub struct MemoryBatchFactory {
77    /// Counter for generating unique batch IDs.
78    next_id: std::sync::atomic::AtomicU64,
79    /// The current block number (for start block).
80    current_block: u64,
81}
82
83impl MemoryBatchFactory {
84    /// Creates a new memory batch factory.
85    pub const fn new(current_block: u64) -> Self {
86        Self {
87            next_id: std::sync::atomic::AtomicU64::new(0),
88            current_block,
89        }
90    }
91
92    /// Sets the current block number.
93    pub const fn set_current_block(&mut self, block: u64) {
94        self.current_block = block;
95    }
96
97    fn generate_batch_id(&self) -> BatchId {
98        use alloy_primitives::B256;
99
100        let id = self
101            .next_id
102            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
103        let mut bytes = [0u8; 32];
104        bytes[24..32].copy_from_slice(&id.to_be_bytes());
105        B256::from(bytes)
106    }
107}
108
109impl Default for MemoryBatchFactory {
110    fn default() -> Self {
111        Self::new(0)
112    }
113}
114
115/// Error type for memory batch factory operations.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum MemoryBatchError {
118    /// The batch was not found.
119    NotFound(BatchId),
120    /// The batch is immutable and cannot be diluted.
121    Immutable(BatchId),
122    /// Invalid depth for dilution.
123    InvalidDepth {
124        /// The batch ID.
125        batch_id: BatchId,
126        /// Current depth.
127        current: u8,
128        /// Requested depth.
129        requested: u8,
130    },
131}
132
133impl std::fmt::Display for MemoryBatchError {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::NotFound(id) => write!(f, "batch not found: {}", id),
137            Self::Immutable(id) => write!(f, "batch is immutable: {}", id),
138            Self::InvalidDepth {
139                batch_id,
140                current,
141                requested,
142            } => write!(
143                f,
144                "invalid depth for batch {}: current {}, requested {}",
145                batch_id, current, requested
146            ),
147        }
148    }
149}
150
151impl std::error::Error for MemoryBatchError {}
152
153impl BatchFactory for MemoryBatchFactory {
154    type Error = std::convert::Infallible;
155
156    async fn create(&self, params: BatchParams) -> Result<CreateResult, Self::Error> {
157        let batch_id = self.generate_batch_id();
158
159        let batch = Batch::new(
160            batch_id,
161            params.amount,
162            self.current_block,
163            params.owner,
164            params.depth,
165            params.bucket_depth,
166            params.immutable,
167        );
168
169        Ok(CreateResult {
170            batch,
171            tx_hash: None,
172        })
173    }
174
175    async fn top_up(&self, _batch_id: BatchId, _amount: u128) -> Result<u128, Self::Error> {
176        // Memory factory doesn't track batches after creation
177        Ok(0)
178    }
179
180    async fn dilute(&self, _batch_id: BatchId, _new_depth: u8) -> Result<(), Self::Error> {
181        // Memory factory doesn't track batches after creation
182        Ok(())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use alloy_primitives::Address;
190
191    #[tokio::test]
192    async fn test_memory_factory_create() {
193        let factory = MemoryBatchFactory::new(100);
194
195        let params = BatchParams::new(Address::ZERO, 20, 16, 1000);
196        let result = factory.create(params).await.unwrap();
197
198        assert_eq!(result.batch.owner(), Address::ZERO);
199        assert_eq!(result.batch.depth(), 20);
200        assert_eq!(result.batch.bucket_depth(), 16);
201        assert_eq!(result.batch.value(), 1000);
202        assert_eq!(result.batch.start(), 100);
203        assert!(result.tx_hash.is_none());
204    }
205
206    #[tokio::test]
207    async fn test_memory_factory_unique_ids() {
208        let factory = MemoryBatchFactory::new(0);
209
210        let params = BatchParams::new(Address::ZERO, 20, 16, 1000);
211
212        let r1 = factory.create(params.clone()).await.unwrap();
213        let r2 = factory.create(params.clone()).await.unwrap();
214        let r3 = factory.create(params).await.unwrap();
215
216        assert_ne!(r1.batch.id(), r2.batch.id());
217        assert_ne!(r2.batch.id(), r3.batch.id());
218    }
219
220    #[tokio::test]
221    async fn test_memory_factory_immutable() {
222        let factory = MemoryBatchFactory::new(0);
223
224        let params = BatchParams::new(Address::ZERO, 20, 16, 1000).immutable(true);
225        let result = factory.create(params).await.unwrap();
226
227        assert!(result.batch.immutable());
228    }
229}