miden_client/note/note_reader.rs
1//! Provides a lazy iterator over consumed input notes.
2
3use alloc::sync::Arc;
4
5use miden_protocol::account::AccountId;
6use miden_protocol::block::BlockNumber;
7
8use crate::ClientError;
9use crate::store::{InputNoteCursor, InputNoteRecord, NoteFilter, Store};
10
11/// A lazy iterator over consumed input notes for a specific consumer account.
12///
13/// Each call to [`InputNoteReader::next`] executes a store query and returns the next matching
14/// note. Use builder methods to configure filters before iterating.
15///
16/// # Ordering
17///
18/// Notes are returned in on-chain consumption order: first by block number, then by per-account
19/// transaction order within the block. Notes consumed by the same transaction are returned in a
20/// deterministic order that is consistent across calls.
21pub struct InputNoteReader {
22 store: Arc<dyn Store>,
23 consumer: AccountId,
24 block_range: Option<(BlockNumber, BlockNumber)>,
25 cursor: Option<InputNoteCursor>,
26}
27
28impl InputNoteReader {
29 /// Creates a new `InputNoteReader` that iterates over consumed input notes for the given
30 /// consumer account.
31 ///
32 /// The consumer is required because ordering is only guaranteed among notes consumed by the
33 /// same account.
34 pub fn new(store: Arc<dyn Store>, consumer: AccountId) -> Self {
35 Self {
36 store,
37 consumer,
38 block_range: None,
39 cursor: None,
40 }
41 }
42
43 /// Restricts iteration to notes consumed within the given block range (inclusive).
44 #[must_use]
45 pub fn in_block_range(mut self, from: BlockNumber, to: BlockNumber) -> Self {
46 self.block_range = Some((from, to));
47 self
48 }
49
50 /// Resets the iterator to the beginning.
51 pub fn reset(&mut self) {
52 self.cursor = None;
53 }
54
55 /// Returns the next consumed input note, or `None` when all matching notes have been returned.
56 ///
57 /// Each call executes a single store query.
58 pub async fn next(&mut self) -> Result<Option<InputNoteRecord>, ClientError> {
59 let (block_start, block_end) = match self.block_range {
60 Some((from, to)) => (Some(from), Some(to)),
61 None => (None, None),
62 };
63
64 // TODO: The note filter should be configurable instead of hardcoding `NoteFilter::Consumed`
65 let note = self
66 .store
67 .get_input_note_after(
68 NoteFilter::Consumed,
69 self.consumer,
70 block_start,
71 block_end,
72 self.cursor,
73 )
74 .await
75 .map_err(ClientError::StoreError)?;
76
77 if let Some(note) = ¬e {
78 // A note with no position cannot move the cursor forward, so silently keeping or
79 // clearing it would either return this same note forever or restart the walk.
80 let cursor = InputNoteCursor::from_record(note).ok_or_else(|| {
81 ClientError::MissingNoteConsumptionPosition(note.details_commitment().as_word())
82 })?;
83 self.cursor = Some(cursor);
84 }
85 Ok(note)
86 }
87}