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}