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