Skip to main content

nectar_postage/
store.rs

1//! Batch storage traits for persisting batch data.
2
3use crate::{Batch, BatchId, PostageContext};
4
5/// A trait for storing and retrieving batches.
6///
7/// Implementations may persist batches in memory, on disk, or retrieve
8/// them from a remote source such as a blockchain node.
9///
10/// # Synchronous Design
11///
12/// The methods are synchronous. The known backends (in memory, redb) are
13/// themselves synchronous, so there is no genuinely async work to drive here;
14/// any async behaviour belongs at the true edges (a gRPC service, an FFI
15/// boundary) where it is added by the edge, not by the store. Keeping the core
16/// synchronous avoids colouring every caller with `async`, keeps the futures
17/// `Send`-free on the wasm path, and makes this trait naturally object-safe (it
18/// has an associated `Error` and no generic methods), a property the previous
19/// async-in-trait shape did not have.
20pub trait BatchStore {
21    /// The error type returned by store operations.
22    type Error: std::error::Error;
23
24    /// Retrieves a batch by its ID.
25    ///
26    /// Returns `None` if the batch doesn't exist.
27    fn get(&self, id: &BatchId) -> Result<Option<Batch>, Self::Error>;
28
29    /// Stores or updates a batch.
30    fn put(&self, batch: Batch) -> Result<(), Self::Error>;
31
32    /// Removes a batch from the store.
33    ///
34    /// Returns `true` if the batch existed and was removed.
35    fn remove(&self, id: &BatchId) -> Result<bool, Self::Error>;
36
37    /// Checks if a batch exists in the store.
38    fn contains(&self, id: &BatchId) -> Result<bool, Self::Error>;
39
40    /// Returns the current postage context.
41    fn context(&self) -> Result<PostageContext, Self::Error>;
42
43    /// Updates the postage context.
44    fn set_context(&self, state: PostageContext) -> Result<(), Self::Error>;
45
46    /// Returns all batch IDs in the store.
47    fn batch_ids(&self) -> Result<Vec<BatchId>, Self::Error>;
48
49    /// Returns the number of batches in the store.
50    fn count(&self) -> Result<usize, Self::Error>;
51}
52
53/// Extension methods for [`BatchStore`].
54///
55/// This is a plain synchronous extension trait; it carries no `Sync` bound,
56/// because the methods are no longer `async` and therefore never need their
57/// futures to be `Send` (which previously forced `Self: Sync`).
58pub trait BatchStoreExt: BatchStore {
59    /// Gets a batch and verifies it's usable.
60    ///
61    /// Returns an error if the batch doesn't exist, isn't usable yet,
62    /// or has expired.
63    fn get_usable(
64        &self,
65        id: &BatchId,
66        confirmation_threshold: u64,
67    ) -> Result<Batch, BatchStoreError<Self::Error>> {
68        let batch = self
69            .get(id)
70            .map_err(BatchStoreError::Store)?
71            .ok_or(BatchStoreError::NotFound(*id))?;
72
73        let state = self.context().map_err(BatchStoreError::Store)?;
74
75        if !batch.is_usable(state.block(), confirmation_threshold) {
76            return Err(BatchStoreError::NotUsable {
77                batch_id: *id,
78                created: batch.start(),
79                current: state.block(),
80                threshold: confirmation_threshold,
81            });
82        }
83
84        if batch.is_expired(state.total_amount()) {
85            return Err(BatchStoreError::Expired {
86                batch_id: *id,
87                value: batch.value(),
88                total_amount: state.total_amount(),
89            });
90        }
91
92        Ok(batch)
93    }
94}
95
96// Blanket implementation
97impl<T: BatchStore> BatchStoreExt for T {}
98
99/// Errors that can occur when working with a batch store.
100#[non_exhaustive]
101#[derive(Debug, thiserror::Error)]
102pub enum BatchStoreError<E: std::error::Error> {
103    /// The batch was not found in the store.
104    #[error("batch not found: {0}")]
105    NotFound(BatchId),
106    /// The batch is not yet usable (needs more confirmations).
107    #[error(
108        "batch {batch_id} not usable: created at block {created}, current block {current}, need {threshold} confirmations"
109    )]
110    NotUsable {
111        /// The batch ID.
112        batch_id: BatchId,
113        /// Block when batch was created.
114        created: u64,
115        /// Current block number.
116        current: u64,
117        /// Required confirmations.
118        threshold: u64,
119    },
120    /// The batch has expired.
121    #[error("batch {batch_id} expired: value {value} <= total_amount {total_amount}")]
122    Expired {
123        /// The batch ID.
124        batch_id: BatchId,
125        /// Current batch value.
126        value: u128,
127        /// Total amount consumed.
128        total_amount: u128,
129    },
130    /// An error from the underlying store.
131    #[error("store error: {0}")]
132    Store(#[from] E),
133}