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