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. It offers methods to query
5//! input and output notes from the store, check their consumability, compile note scripts, and
6//! 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::config::NetworkAccountConfigNote;
110pub use miden_standards::note::costs::{NoteConsumptionCost, NoteCost};
111pub use miden_standards::note::{
112    FeeSponsorshipNote,
113    MintNote,
114    MintNoteStorage,
115    NetworkAccountTarget,
116    NoteConsumptionStatus,
117    NoteExecutionHint,
118    NoteFile,
119    NoteSyncHint,
120    P2idNote,
121    P2idNoteStorage,
122    P2ideNote,
123    P2ideNoteStorage,
124    PswapNote,
125    StandardNote,
126    SwapNote,
127    TxFeeNote,
128};
129pub use miden_tx::{FailedNote, NoteConsumptionInfo};
130pub use note_reader::InputNoteReader;
131pub use note_screener::{NoteConsumability, NoteScreener, NoteScreenerError};
132pub use note_update_tracker::{
133    InputNoteUpdate,
134    NoteConsumption,
135    NoteUpdateTracker,
136    NoteUpdateType,
137    OutputNoteUpdate,
138};
139
140/// Note retrieval methods.
141impl<AUTH> Client<AUTH>
142where
143    AUTH: TransactionAuthenticator + Sync,
144{
145    // INPUT NOTE DATA RETRIEVAL
146    // --------------------------------------------------------------------------------------------
147
148    /// Retrieves the input notes managed by the client from the store.
149    ///
150    /// # Errors
151    ///
152    /// Returns a [`ClientError::StoreError`] if the filter is [`NoteFilter::Unique`] and there is
153    /// no Note with the provided ID.
154    pub async fn get_input_notes(
155        &self,
156        filter: NoteFilter,
157    ) -> Result<Vec<InputNoteRecord>, ClientError> {
158        self.store.get_input_notes(filter).await.map_err(Into::into)
159    }
160
161    /// Returns the input notes and their consumability. Assuming the notes will be consumed by a
162    /// normal consume transaction. If `account_id` is None then all consumable input notes are
163    /// returned.
164    ///
165    /// The note screener runs a series of checks to determine whether the note can be executed as
166    /// part of a transaction for a specific account. If the specific account ID can consume it (ie,
167    /// if it's compatible with the account), it will be returned as part of the result list.
168    ///
169    /// # Performance
170    ///
171    /// This call screens every committed note tracked by the client on each invocation, without
172    /// retaining verdicts between calls. When `account_id` is `None` the notes are screened against
173    /// every account tracked by the client; when it is `Some`, only against that account. For notes
174    /// whose consumability cannot be determined statically, the screener runs one trial transaction
175    /// in the VM per `(account, note)` pair, so the cost grows with the number of screened accounts
176    /// multiplied by the number of committed notes.
177    ///
178    /// Consider cheaper alternatives when calling this function for accounts that accumulate
179    /// committed-unconsumed notes, especially when used in polling loops:
180    ///
181    /// - Query and filter the notes directly with [`Self::get_input_notes`] and
182    ///   [`NoteFilter::Committed`] if note consumability verdict is not needed.
183    /// - Wait for a specific note to commit with [`Self::get_input_note`] and
184    ///   [`InputNoteRecord::is_committed`], instead of polling for it in the screened results.
185    /// - Screen a narrower set of notes with [`NoteScreener::get_batch_consumability`] or
186    ///   [`NoteScreener::get_batch_consumability_for_account`], reached through
187    ///   [`Self::note_screener`].
188    pub async fn get_consumable_notes(
189        &self,
190        account_id: Option<AccountId>,
191    ) -> Result<Vec<(InputNoteRecord, Vec<NoteConsumability>)>, ClientError> {
192        let committed_notes = self.store.get_input_notes(NoteFilter::Committed).await?;
193        let notes = committed_notes
194            .iter()
195            .cloned()
196            .map(TryInto::try_into)
197            .collect::<Result<Vec<Note>, _>>()?;
198
199        let note_screener = self.note_screener();
200        let mut note_relevances = match account_id {
201            Some(account_id) => {
202                note_screener.get_batch_consumability_for_account(account_id, &notes).await?
203            },
204            None => note_screener.get_batch_consumability(&notes).await?,
205        };
206
207        let mut relevant_notes = Vec::new();
208        for input_note in committed_notes {
209            // Committed notes always have metadata, so id() is `Some`.
210            let Some(note_id) = input_note.id() else { continue };
211            // A note is in the map only when at least one screened account can consume it, so its
212            // relevance list is never empty.
213            let Some(account_relevance) = note_relevances.remove(&note_id) else {
214                continue;
215            };
216
217            relevant_notes.push((input_note, account_relevance));
218        }
219
220        Ok(relevant_notes)
221    }
222
223    /// Returns the consumability conditions for the provided note.
224    ///
225    /// The note screener runs a series of checks to determine whether the note can be executed as
226    /// part of a transaction for a specific account. If the specific account ID can consume it (ie,
227    /// if it's compatible with the account), it will be returned as part of the result list.
228    pub async fn get_note_consumability(
229        &self,
230        note: InputNoteRecord,
231    ) -> Result<Vec<NoteConsumability>, ClientError> {
232        self.note_screener()
233            .get_consumability(&note.try_into()?)
234            .await
235            .map_err(Into::into)
236    }
237
238    /// Retrieves the input note given a [`NoteId`]. Returns `None` if the note is not found.
239    pub async fn get_input_note(
240        &self,
241        note_id: NoteId,
242    ) -> Result<Option<InputNoteRecord>, ClientError> {
243        Ok(self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.pop())
244    }
245
246    // OUTPUT NOTE DATA RETRIEVAL
247    // --------------------------------------------------------------------------------------------
248
249    /// Returns output notes managed by this client.
250    pub async fn get_output_notes(
251        &self,
252        filter: NoteFilter,
253    ) -> Result<Vec<OutputNoteRecord>, ClientError> {
254        self.store.get_output_notes(filter).await.map_err(Into::into)
255    }
256
257    /// Retrieves the output note given a [`NoteId`]. Returns `None` if the note is not found.
258    pub async fn get_output_note(
259        &self,
260        note_id: NoteId,
261    ) -> Result<Option<OutputNoteRecord>, ClientError> {
262        Ok(self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.pop())
263    }
264
265    /// Returns an [`InputNoteReader`] that lazily iterates over consumed input notes for the given
266    /// consumer account.
267    ///
268    /// The consumer is required because ordering is only guaranteed among notes consumed by the
269    /// same account.
270    ///
271    /// # Example
272    ///
273    /// ```rust,ignore
274    /// let mut reader = client.input_note_reader(account_id);
275    ///
276    /// while let Some(note) = reader.next().await? {
277    ///     process(note);
278    /// }
279    /// ```
280    pub fn input_note_reader(&self, consumer: AccountId) -> InputNoteReader {
281        InputNoteReader::new(self.store.clone(), consumer)
282    }
283}
284
285/// Returns the client input note whose ID starts with `note_id_prefix`.
286///
287/// # Errors
288///
289/// - Returns [`IdPrefixFetchError::NoMatch`] if we were unable to find any note where
290///   `note_id_prefix` is a prefix of its ID.
291/// - Returns [`IdPrefixFetchError::MultipleMatches`] if there were more than one note found where
292///   `note_id_prefix` is a prefix of its ID.
293pub async fn get_input_note_with_id_prefix<AUTH>(
294    client: &Client<AUTH>,
295    note_id_prefix: &str,
296) -> Result<InputNoteRecord, IdPrefixFetchError>
297where
298    AUTH: TransactionAuthenticator + Sync,
299{
300    let mut input_note_records = client
301        .get_input_notes(NoteFilter::All)
302        .await
303        .map_err(|err| {
304            tracing::error!("Error when fetching all notes from the store: {err}");
305            IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}"))
306        })?
307        .into_iter()
308        .filter(|note_record| {
309            note_record.id().is_some_and(|id| id.to_hex().starts_with(note_id_prefix))
310        })
311        .collect::<Vec<_>>();
312
313    if input_note_records.is_empty() {
314        return Err(IdPrefixFetchError::NoMatch(format!("note ID prefix {note_id_prefix}")));
315    }
316    if input_note_records.len() > 1 {
317        let input_note_record_ids =
318            input_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
319        tracing::error!(
320            "Multiple notes found for the prefix {}: {:?}",
321            note_id_prefix,
322            input_note_record_ids
323        );
324        return Err(IdPrefixFetchError::MultipleMatches(format!(
325            "note ID prefix {note_id_prefix}"
326        )));
327    }
328
329    Ok(input_note_records
330        .pop()
331        .expect("input_note_records should always have one element"))
332}