Skip to main content

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
14/// next matching 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
19/// per-account transaction order within the block. Notes consumed by the same transaction
20/// are returned in a 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
30    /// for the given consumer account.
31    ///
32    /// The consumer is required because ordering is only guaranteed among notes
33    /// consumed by the 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
56    /// returned.
57    ///
58    /// Each call executes a single store query.
59    pub async fn next(&mut self) -> Result<Option<InputNoteRecord>, ClientError> {
60        let (block_start, block_end) = match self.block_range {
61            Some((from, to)) => (Some(from), Some(to)),
62            None => (None, None),
63        };
64
65        // TODO: The note filter should be configurable instead of hardcoding `NoteFilter::Consumed`
66        let note = self
67            .store
68            .get_input_note_after(
69                NoteFilter::Consumed,
70                self.consumer,
71                block_start,
72                block_end,
73                self.cursor,
74            )
75            .await
76            .map_err(ClientError::StoreError)?;
77
78        if let Some(note) = &note {
79            // A note with no position cannot move the cursor forward, so silently keeping or
80            // clearing it would either return this same note forever or restart the walk.
81            let cursor = InputNoteCursor::from_record(note).ok_or_else(|| {
82                ClientError::MissingNoteConsumptionPosition(note.details_commitment().as_word())
83            })?;
84            self.cursor = Some(cursor);
85        }
86        Ok(note)
87    }
88}