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