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 FeeSponsorshipNote,
114 MintNote,
115 MintNoteStorage,
116 NetworkAccountTarget,
117 NoteConsumptionStatus,
118 NoteExecutionHint,
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 for the given
265 /// consumer account.
266 ///
267 /// The consumer is required because ordering is only guaranteed among notes consumed by the
268 /// 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}