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