miden_standards/note/mod.rs
1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::error::Error;
4
5use miden_protocol::Word;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{Note, NoteScript};
9
10use crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
11use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt};
12use crate::account::wallets::BasicWallet;
13
14mod burn;
15pub use burn::BurnNote;
16
17mod execution_hint;
18pub use execution_hint::NoteExecutionHint;
19
20mod mint;
21pub use mint::{MintNote, MintNoteStorage};
22
23mod p2id;
24pub use p2id::{P2idNote, P2idNoteStorage};
25
26mod p2ide;
27pub use p2ide::{P2ideNote, P2ideNoteStorage};
28
29mod swap;
30pub use swap::SwapNote;
31
32mod network_account_target;
33pub use network_account_target::{NetworkAccountTarget, NetworkAccountTargetError};
34
35mod network_note;
36pub use network_note::{AccountTargetNetworkNote, NetworkNoteExt};
37
38mod standard_note_attachment;
39use miden_protocol::errors::NoteError;
40pub use standard_note_attachment::StandardNoteAttachment;
41// STANDARD NOTE
42// ================================================================================================
43
44/// The enum holding the types of standard notes provided by `miden-standards`.
45pub enum StandardNote {
46 P2ID,
47 P2IDE,
48 SWAP,
49 MINT,
50 BURN,
51}
52
53impl StandardNote {
54 // CONSTRUCTOR
55 // --------------------------------------------------------------------------------------------
56
57 /// Returns a [`StandardNote`] instance based on the provided [`NoteScript`]. Returns `None`
58 /// if the provided script does not match any standard note script.
59 pub fn from_script(script: &NoteScript) -> Option<Self> {
60 Self::from_script_root(script.root())
61 }
62
63 /// Returns a [`StandardNote`] instance based on the provided script root. Returns `None` if
64 /// the provided root does not match any standard note script.
65 pub fn from_script_root(root: Word) -> Option<Self> {
66 if root == P2idNote::script_root() {
67 return Some(Self::P2ID);
68 }
69 if root == P2ideNote::script_root() {
70 return Some(Self::P2IDE);
71 }
72 if root == SwapNote::script_root() {
73 return Some(Self::SWAP);
74 }
75 if root == MintNote::script_root() {
76 return Some(Self::MINT);
77 }
78 if root == BurnNote::script_root() {
79 return Some(Self::BURN);
80 }
81
82 None
83 }
84
85 // PUBLIC ACCESSORS
86 // --------------------------------------------------------------------------------------------
87
88 /// Returns the name of this [`StandardNote`] variant as a string.
89 pub fn name(&self) -> &'static str {
90 match self {
91 Self::P2ID => "P2ID",
92 Self::P2IDE => "P2IDE",
93 Self::SWAP => "SWAP",
94 Self::MINT => "MINT",
95 Self::BURN => "BURN",
96 }
97 }
98
99 /// Returns the expected number of storage items of the active note.
100 pub fn expected_num_storage_items(&self) -> usize {
101 match self {
102 Self::P2ID => P2idNote::NUM_STORAGE_ITEMS,
103 Self::P2IDE => P2ideNote::NUM_STORAGE_ITEMS,
104 Self::SWAP => SwapNote::NUM_STORAGE_ITEMS,
105 Self::MINT => MintNote::NUM_STORAGE_ITEMS_PRIVATE,
106 Self::BURN => BurnNote::NUM_STORAGE_ITEMS,
107 }
108 }
109
110 /// Returns the note script of the current [StandardNote] instance.
111 pub fn script(&self) -> NoteScript {
112 match self {
113 Self::P2ID => P2idNote::script(),
114 Self::P2IDE => P2ideNote::script(),
115 Self::SWAP => SwapNote::script(),
116 Self::MINT => MintNote::script(),
117 Self::BURN => BurnNote::script(),
118 }
119 }
120
121 /// Returns the script root of the current [StandardNote] instance.
122 pub fn script_root(&self) -> Word {
123 match self {
124 Self::P2ID => P2idNote::script_root(),
125 Self::P2IDE => P2ideNote::script_root(),
126 Self::SWAP => SwapNote::script_root(),
127 Self::MINT => MintNote::script_root(),
128 Self::BURN => BurnNote::script_root(),
129 }
130 }
131
132 /// Returns a boolean value indicating whether this [StandardNote] is compatible with the
133 /// provided [AccountInterface].
134 pub fn is_compatible_with(&self, account_interface: &AccountInterface) -> bool {
135 if account_interface.components().contains(&AccountComponentInterface::BasicWallet) {
136 return true;
137 }
138
139 let interface_proc_digests = account_interface.get_procedure_digests();
140 match self {
141 Self::P2ID | &Self::P2IDE => {
142 // To consume P2ID and P2IDE notes, the `receive_asset` procedure must be present in
143 // the provided account interface.
144 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
145 },
146 Self::SWAP => {
147 // To consume SWAP note, the `receive_asset` and `move_asset_to_note` procedures
148 // must be present in the provided account interface.
149 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
150 && interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest())
151 },
152 Self::MINT => {
153 // MINT notes work only with network fungible faucets. The network faucet uses
154 // note-based authentication (checking if the note sender equals the faucet owner)
155 // to authorize minting, while basic faucets have different mint procedures that
156 // are not compatible with MINT notes.
157 interface_proc_digests.contains(&NetworkFungibleFaucet::distribute_digest())
158 },
159 Self::BURN => {
160 // BURN notes work with both basic and network fungible faucets because both
161 // faucet types export the same `burn` procedure with identical MAST roots.
162 // This allows a single BURN note script to work with either faucet type.
163 interface_proc_digests.contains(&BasicFungibleFaucet::burn_digest())
164 || interface_proc_digests.contains(&NetworkFungibleFaucet::burn_digest())
165 },
166 }
167 }
168
169 /// Performs the inputs check of the provided standard note against the target account and the
170 /// block number.
171 ///
172 /// This function returns:
173 /// - `Some` if we can definitively determine whether the note can be consumed not by the target
174 /// account.
175 /// - `None` if the consumption status of the note cannot be determined conclusively and further
176 /// checks are necessary.
177 pub fn is_consumable(
178 &self,
179 note: &Note,
180 target_account_id: AccountId,
181 block_ref: BlockNumber,
182 ) -> Option<NoteConsumptionStatus> {
183 match self.is_consumable_inner(note, target_account_id, block_ref) {
184 Ok(status) => status,
185 Err(err) => {
186 let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
187 Some(NoteConsumptionStatus::NeverConsumable(err))
188 },
189 }
190 }
191
192 /// Performs the inputs check of the provided note against the target account and the block
193 /// number.
194 ///
195 /// It performs:
196 /// - for `P2ID` note:
197 /// - check that note storage has correct number of values.
198 /// - assertion that the account ID provided by the note storage is equal to the target
199 /// account ID.
200 /// - for `P2IDE` note:
201 /// - check that note storage has correct number of values.
202 /// - check that the target account is either the receiver account or the sender account.
203 /// - check that depending on whether the target account is sender or receiver, it could be
204 /// either consumed, or consumed after timelock height, or consumed after reclaim height.
205 fn is_consumable_inner(
206 &self,
207 note: &Note,
208 target_account_id: AccountId,
209 block_ref: BlockNumber,
210 ) -> Result<Option<NoteConsumptionStatus>, NoteError> {
211 match self {
212 StandardNote::P2ID => {
213 let input_account_id = P2idNoteStorage::try_from(note.storage().items())
214 .map_err(|e| NoteError::other_with_source("invalid P2ID note storage", e))?;
215
216 if input_account_id.target() == target_account_id {
217 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
218 } else {
219 Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note storage doesn't match the target account ID".into())))
220 }
221 },
222 StandardNote::P2IDE => {
223 let P2ideNoteStorage {
224 target: receiver_account_id,
225 reclaim_height,
226 timelock_height,
227 } = P2ideNoteStorage::try_from(note.storage().items())
228 .map_err(|e| NoteError::other_with_source("invalid P2IDE note storage", e))?;
229
230 let current_block_height = block_ref.as_u32();
231 let reclaim_height = reclaim_height.unwrap_or_default().as_u32();
232 let timelock_height = timelock_height.unwrap_or_default().as_u32();
233
234 // block height after which sender account can consume the note
235 let consumable_after = reclaim_height.max(timelock_height);
236
237 // handle the case when the target account of the transaction is sender
238 if target_account_id == note.metadata().sender() {
239 // For the sender, the current block height needs to have reached both reclaim
240 // and timelock height to be consumable.
241 if current_block_height >= consumable_after {
242 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
243 } else {
244 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
245 consumable_after,
246 ))))
247 }
248 // handle the case when the target account of the transaction is receiver
249 } else if target_account_id == receiver_account_id {
250 // For the receiver, the current block height needs to have reached only the
251 // timelock height to be consumable: we can ignore the reclaim height in this
252 // case
253 if current_block_height >= timelock_height {
254 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
255 } else {
256 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
257 timelock_height,
258 ))))
259 }
260 // if the target account is neither the sender nor the receiver (from the note's
261 // storage), then this account cannot consume the note
262 } else {
263 Ok(Some(NoteConsumptionStatus::NeverConsumable(
264 "target account of the transaction does not match neither the receiver account specified by the P2IDE storage, nor the sender account".into()
265 )))
266 }
267 },
268
269 // the consumption status of any other note cannot be determined by the static analysis,
270 // further checks are necessary.
271 _ => Ok(None),
272 }
273 }
274}
275
276// HELPER FUNCTIONS
277// ================================================================================================
278
279// HELPER STRUCTURES
280// ================================================================================================
281
282/// Describes if a note could be consumed under a specific conditions: target account state
283/// and block height.
284///
285/// The status does not account for any authorization that may be required to consume the
286/// note, nor does it indicate whether the account has sufficient fees to consume it.
287#[derive(Debug)]
288pub enum NoteConsumptionStatus {
289 /// The note can be consumed by the account at the specified block height.
290 Consumable,
291 /// The note can be consumed by the account after the required block height is achieved.
292 ConsumableAfter(BlockNumber),
293 /// The note can be consumed by the account if proper authorization is provided.
294 ConsumableWithAuthorization,
295 /// The note cannot be consumed by the account at the specified conditions (i.e., block
296 /// height and account state).
297 UnconsumableConditions,
298 /// The note cannot be consumed by the specified account under any conditions.
299 NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
300}
301
302impl Clone for NoteConsumptionStatus {
303 fn clone(&self) -> Self {
304 match self {
305 NoteConsumptionStatus::Consumable => NoteConsumptionStatus::Consumable,
306 NoteConsumptionStatus::ConsumableAfter(block_height) => {
307 NoteConsumptionStatus::ConsumableAfter(*block_height)
308 },
309 NoteConsumptionStatus::ConsumableWithAuthorization => {
310 NoteConsumptionStatus::ConsumableWithAuthorization
311 },
312 NoteConsumptionStatus::UnconsumableConditions => {
313 NoteConsumptionStatus::UnconsumableConditions
314 },
315 NoteConsumptionStatus::NeverConsumable(error) => {
316 let err = error.to_string();
317 NoteConsumptionStatus::NeverConsumable(err.into())
318 },
319 }
320 }
321}