Skip to main content

miden_client/note/
mod.rs

1//! Contains the Client APIs related to notes. Notes can contain assets and scripts that are
2//! executed as part of transactions.
3//!
4//! This module enables the tracking, retrieval, and processing of notes.
5//! It offers methods to query input and output notes from the store, check their consumability,
6//! compile note scripts, and retrieve notes based on partial ID matching.
7//!
8//! ## Overview
9//!
10//! The module exposes APIs to:
11//!
12//! - Retrieve input notes and output notes.
13//! - Determine the consumability of notes using the [`NoteScreener`].
14//! - Compile note scripts from source code with `compile_note_script`.
15//! - Retrieve an input note by a prefix of its ID using the helper function
16//!   [`get_input_note_with_id_prefix`].
17//!
18//! ## Example
19//!
20//! ```rust
21//! use miden_client::{
22//!     auth::TransactionAuthenticator,
23//!     Client,
24//!     crypto::FeltRng,
25//!     note::{NoteScreener, get_input_note_with_id_prefix},
26//!     store::NoteFilter,
27//! };
28//! use miden_protocol::account::AccountId;
29//!
30//! # async fn example<AUTH: TransactionAuthenticator + Sync>(client: &Client<AUTH>) -> Result<(), Box<dyn std::error::Error>> {
31//! // Retrieve all committed input notes
32//! let input_notes = client.get_input_notes(NoteFilter::Committed).await?;
33//! println!("Found {} committed input notes.", input_notes.len());
34//!
35//! // Check consumability for a specific note
36//! if let Some(note) = input_notes.first() {
37//!     let consumability = client.get_note_consumability(note.clone()).await?;
38//!     println!("Note consumability: {:?}", consumability);
39//! }
40//!
41//! // Retrieve an input note by a partial ID match
42//! let note_prefix = "0x70b7ec";
43//! match get_input_note_with_id_prefix(client, note_prefix).await {
44//!     Ok(note) => println!(
45//!         "Found note with matching prefix: {}",
46//!         note.id().expect("note matched by ID prefix has an ID").to_hex()
47//!     ),
48//!     Err(err) => println!("Error retrieving note: {err:?}"),
49//! }
50//!
51//! // Compile the note script
52//! let script_src = "@note_script\npub proc main\n    push.9 push.12 add\nend";
53//! let note_script = client.code_builder().compile_note_script(script_src)?;
54//! println!("Compiled note script successfully.");
55//!
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! For more details on the API and error handling, see the documentation for the specific functions
61//! and types in this module.
62
63use alloc::vec::Vec;
64
65use miden_protocol::account::AccountId;
66use miden_tx::auth::TransactionAuthenticator;
67
68use crate::store::{InputNoteRecord, NoteFilter, OutputNoteRecord};
69use crate::{Client, ClientError, IdPrefixFetchError};
70
71mod import;
72mod note_reader;
73mod note_screener;
74mod note_update_tracker;
75
76// RE-EXPORTS
77// ================================================================================================
78
79pub use miden_protocol::block::BlockNumber;
80pub use miden_protocol::errors::NoteError;
81pub use miden_protocol::note::{
82    Note,
83    NoteAssets,
84    NoteAttachment,
85    NoteAttachmentContent,
86    NoteAttachmentHeader,
87    NoteAttachmentScheme,
88    NoteAttachments,
89    NoteDetails,
90    NoteDetailsCommitment,
91    NoteHeader,
92    NoteId,
93    NoteInclusionProof,
94    NoteLocation,
95    NoteMetadata,
96    NoteRecipient,
97    NoteScript,
98    NoteScriptRoot,
99    NoteStorage,
100    NoteTag,
101    NoteType,
102    Nullifier,
103    PartialNote,
104    PartialNoteMetadata,
105};
106pub use miden_protocol::transaction::ToInputNoteCommitments;
107/// Raw access to `miden-standards` note modules for items not curated by `miden-client`.
108pub use miden_standards::note as standards;
109pub use miden_standards::note::{
110    FeeSponsorshipNote,
111    MintNote,
112    MintNoteStorage,
113    NetworkAccountConfigNote,
114    NetworkAccountTarget,
115    NoteConsumptionStatus,
116    NoteExecutionHint,
117    NoteFile,
118    NoteSyncHint,
119    P2idNote,
120    P2idNoteStorage,
121    P2ideNote,
122    P2ideNoteStorage,
123    PswapNote,
124    StandardNote,
125    SwapNote,
126    TxFeeNote,
127};
128pub use miden_tx::{FailedNote, NoteConsumptionInfo};
129pub use note_reader::InputNoteReader;
130pub use note_screener::{NoteConsumability, NoteScreener, NoteScreenerError};
131pub use note_update_tracker::{
132    InputNoteUpdate,
133    NoteConsumption,
134    NoteUpdateTracker,
135    NoteUpdateType,
136    OutputNoteUpdate,
137};
138
139/// Note retrieval methods.
140impl<AUTH> Client<AUTH>
141where
142    AUTH: TransactionAuthenticator + Sync,
143{
144    // INPUT NOTE DATA RETRIEVAL
145    // --------------------------------------------------------------------------------------------
146
147    /// Retrieves the input notes managed by the client from the store.
148    ///
149    /// # Errors
150    ///
151    /// Returns a [`ClientError::StoreError`] if the filter is [`NoteFilter::Unique`] and there is
152    /// no Note with the provided ID.
153    pub async fn get_input_notes(
154        &self,
155        filter: NoteFilter,
156    ) -> Result<Vec<InputNoteRecord>, ClientError> {
157        self.store.get_input_notes(filter).await.map_err(Into::into)
158    }
159
160    /// Returns the input notes and their consumability. Assuming the notes will be consumed by a
161    /// normal consume transaction. If `account_id` is None then all consumable input notes are
162    /// returned.
163    ///
164    /// The note screener runs a series of checks to determine whether the note can be executed as
165    /// part of a transaction for a specific account. If the specific account ID can consume it (ie,
166    /// if it's compatible with the account), it will be returned as part of the result list.
167    ///
168    /// # Performance
169    ///
170    /// This call screens every committed note tracked by the client on each invocation, without
171    /// retaining verdicts between calls. When `account_id` is `None` the notes are screened against
172    /// every account tracked by the client; when it is `Some`, only against that account. For notes
173    /// whose consumability cannot be determined statically, the screener runs one trial transaction
174    /// in the VM per `(account, note)` pair, so the cost grows with the number of screened accounts
175    /// multiplied by the number of committed notes.
176    ///
177    /// Consider cheaper alternatives when calling this function for accounts that accumulate
178    /// committed-unconsumed notes, especially when used in polling loops:
179    ///
180    /// - Query and filter the notes directly with [`Self::get_input_notes`] and
181    ///   [`NoteFilter::Committed`] if note consumability verdict is not needed.
182    /// - Wait for a specific note to commit with [`Self::get_input_note`] and
183    ///   [`InputNoteRecord::is_committed`], instead of polling for it in the screened results.
184    /// - Screen a narrower set of notes with [`NoteScreener::get_batch_consumability`] or
185    ///   [`NoteScreener::get_batch_consumability_for_account`], reached through
186    ///   [`Self::note_screener`].
187    pub async fn get_consumable_notes(
188        &self,
189        account_id: Option<AccountId>,
190    ) -> Result<Vec<(InputNoteRecord, Vec<NoteConsumability>)>, ClientError> {
191        let committed_notes = self.store.get_input_notes(NoteFilter::Committed).await?;
192        let notes = committed_notes
193            .iter()
194            .cloned()
195            .map(TryInto::try_into)
196            .collect::<Result<Vec<Note>, _>>()?;
197
198        let note_screener = self.note_screener();
199        let mut note_relevances = match account_id {
200            Some(account_id) => {
201                note_screener.get_batch_consumability_for_account(account_id, &notes).await?
202            },
203            None => note_screener.get_batch_consumability(&notes).await?,
204        };
205
206        let mut relevant_notes = Vec::new();
207        for input_note in committed_notes {
208            // Committed notes always have metadata, so id() is `Some`.
209            let Some(note_id) = input_note.id() else { continue };
210            // A note is in the map only when at least one screened account can consume it, so its
211            // relevance list is never empty.
212            let Some(account_relevance) = note_relevances.remove(&note_id) else {
213                continue;
214            };
215
216            relevant_notes.push((input_note, account_relevance));
217        }
218
219        Ok(relevant_notes)
220    }
221
222    /// Returns the consumability conditions for the provided note.
223    ///
224    /// The note screener runs a series of checks to determine whether the note can be executed as
225    /// part of a transaction for a specific account. If the specific account ID can consume it (ie,
226    /// if it's compatible with the account), it will be returned as part of the result list.
227    pub async fn get_note_consumability(
228        &self,
229        note: InputNoteRecord,
230    ) -> Result<Vec<NoteConsumability>, ClientError> {
231        self.note_screener()
232            .get_consumability(&note.try_into()?)
233            .await
234            .map_err(Into::into)
235    }
236
237    /// Retrieves the input note given a [`NoteId`]. Returns `None` if the note is not found.
238    pub async fn get_input_note(
239        &self,
240        note_id: NoteId,
241    ) -> Result<Option<InputNoteRecord>, ClientError> {
242        Ok(self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.pop())
243    }
244
245    // OUTPUT NOTE DATA RETRIEVAL
246    // --------------------------------------------------------------------------------------------
247
248    /// Returns output notes managed by this client.
249    pub async fn get_output_notes(
250        &self,
251        filter: NoteFilter,
252    ) -> Result<Vec<OutputNoteRecord>, ClientError> {
253        self.store.get_output_notes(filter).await.map_err(Into::into)
254    }
255
256    /// Retrieves the output note given a [`NoteId`]. Returns `None` if the note is not found.
257    pub async fn get_output_note(
258        &self,
259        note_id: NoteId,
260    ) -> Result<Option<OutputNoteRecord>, ClientError> {
261        Ok(self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.pop())
262    }
263
264    /// Returns an [`InputNoteReader`] that lazily iterates over consumed input notes
265    /// for the given consumer account.
266    ///
267    /// The consumer is required because ordering is only guaranteed among notes
268    /// consumed by the same account.
269    ///
270    /// # Example
271    ///
272    /// ```rust,ignore
273    /// let mut reader = client.input_note_reader(account_id);
274    ///
275    /// while let Some(note) = reader.next().await? {
276    ///     process(note);
277    /// }
278    /// ```
279    pub fn input_note_reader(&self, consumer: AccountId) -> InputNoteReader {
280        InputNoteReader::new(self.store.clone(), consumer)
281    }
282}
283
284/// Returns the client input note whose ID starts with `note_id_prefix`.
285///
286/// # Errors
287///
288/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any note where
289///   `note_id_prefix` is a prefix of its ID.
290/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one note found where
291///   `note_id_prefix` is a prefix of its ID.
292pub async fn get_input_note_with_id_prefix<AUTH>(
293    client: &Client<AUTH>,
294    note_id_prefix: &str,
295) -> Result<InputNoteRecord, IdPrefixFetchError>
296where
297    AUTH: TransactionAuthenticator + Sync,
298{
299    let mut input_note_records = client
300        .get_input_notes(NoteFilter::All)
301        .await
302        .map_err(|err| {
303            tracing::error!("Error when fetching all notes from the store: {err}");
304            IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}"))
305        })?
306        .into_iter()
307        .filter(|note_record| {
308            note_record.id().is_some_and(|id| id.to_hex().starts_with(note_id_prefix))
309        })
310        .collect::<Vec<_>>();
311
312    if input_note_records.is_empty() {
313        return Err(IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}")));
314    }
315    if input_note_records.len() > 1 {
316        let input_note_record_ids =
317            input_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
318        tracing::error!(
319            "Multiple notes found for the prefix {}: {:?}",
320            note_id_prefix,
321            input_note_record_ids
322        );
323        return Err(IdPrefixFetchError::MultipleMatches(format!(
324            "note ID prefix {note_id_prefix}"
325        )));
326    }
327
328    Ok(input_note_records
329        .pop()
330        .expect("input_note_records should always have one element"))
331}