Skip to main content

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}