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, ¬es).await?
202 },
203 None => note_screener.get_batch_consumability(¬es).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(¬e_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(¬e.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}