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::Felt;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{Note, NoteScript, NoteScriptRoot};
9
10pub mod costs;
11
12mod allowlist_config;
13pub use allowlist_config::{AllowlistConfig, AllowlistConfigNote};
14
15mod blocklist_config;
16pub use blocklist_config::{BlocklistConfig, BlocklistConfigNote};
17
18mod burn;
19pub use burn::BurnNote;
20
21mod constant_fee_policy_config;
22pub use constant_fee_policy_config::ConstantFeePolicyConfigNote;
23
24mod faucet_metadata_config;
25pub use faucet_metadata_config::{FaucetMetadataConfig, FaucetMetadataConfigNote};
26
27mod faucet_policy_config;
28pub use faucet_policy_config::{FaucetPolicyConfig, FaucetPolicyConfigNote};
29
30mod fee_sponsorship;
31pub use fee_sponsorship::{FeeSponsorshipNote, FeeSponsorshipNoteStorage};
32
33mod execution_hint;
34pub use execution_hint::NoteExecutionHint;
35
36mod file;
37pub use file::{NoteFile, NoteSyncHint};
38
39mod min_burn_amount_config;
40pub use min_burn_amount_config::MinBurnAmountConfigNote;
41
42mod mint;
43pub use mint::{MintNote, MintNoteStorage};
44
45mod network_account_config;
46pub use network_account_config::{NetworkAccountConfig, NetworkAccountConfigNote};
47
48mod owner_config;
49pub use owner_config::{OwnerConfig, OwnerConfigNote};
50
51mod p2id;
52pub use p2id::{P2idNote, P2idNoteStorage};
53
54mod p2ide;
55pub use p2ide::{P2ideNote, P2ideNoteStorage};
56
57mod pause_config;
58pub use pause_config::{PauseConfig, PauseConfigNote};
59
60mod pswap;
61pub use pswap::{PswapNote, PswapNoteAttachment, PswapNoteStorage};
62
63mod rbac_config;
64pub use rbac_config::{RbacConfig, RbacConfigNote};
65
66mod swap;
67pub use swap::{SwapNote, SwapNoteStorage, SwapPayback, payback_serial_from_swap};
68
69mod tx_fee;
70pub use tx_fee::TxFeeNote;
71
72mod network_account_target;
73pub use network_account_target::{NetworkAccountTarget, NetworkAccountTargetError};
74
75mod network_note;
76pub use network_note::{AccountTargetNetworkNote, NetworkNoteExt};
77
78mod standard_note_attachment;
79use miden_protocol::errors::NoteError;
80pub use standard_note_attachment::StandardNoteAttachment;
81// STANDARD NOTE
82// ================================================================================================
83
84/// The enum holding the types of standard notes provided by `miden-standards`.
85#[allow(non_camel_case_types)]
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum StandardNote {
88    P2ID,
89    P2IDE,
90    SWAP,
91    PSWAP,
92    MINT,
93    BURN,
94    CONSTANT_FEE_POLICY_CONFIG,
95    FAUCET_POLICY_CONFIG,
96    FAUCET_METADATA_CONFIG,
97    MIN_BURN_AMOUNT_CONFIG,
98    ALLOWLIST_CONFIG,
99    BLOCKLIST_CONFIG,
100    PAUSE_CONFIG,
101    OWNER_CONFIG,
102    RBAC_CONFIG,
103    NETWORK_ACCOUNT_CONFIG,
104    FEE_SPONSORSHIP,
105    TX_FEE,
106}
107
108impl StandardNote {
109    // CONSTRUCTOR
110    // --------------------------------------------------------------------------------------------
111
112    /// Returns a [`StandardNote`] instance based on the provided [`NoteScript`]. Returns `None`
113    /// if the provided script does not match any standard note script.
114    pub fn from_script(script: &NoteScript) -> Option<Self> {
115        Self::from_script_root(script.root())
116    }
117
118    /// Returns a [`StandardNote`] instance based on the provided script root. Returns `None` if
119    /// the provided root does not match any standard note script.
120    pub fn from_script_root(root: NoteScriptRoot) -> Option<Self> {
121        if root == P2idNote::script_root() {
122            return Some(Self::P2ID);
123        }
124        if root == P2ideNote::script_root() {
125            return Some(Self::P2IDE);
126        }
127        if root == SwapNote::script_root() {
128            return Some(Self::SWAP);
129        }
130        if root == PswapNote::script_root() {
131            return Some(Self::PSWAP);
132        }
133        if root == MintNote::script_root() {
134            return Some(Self::MINT);
135        }
136        if root == BurnNote::script_root() {
137            return Some(Self::BURN);
138        }
139        if root == ConstantFeePolicyConfigNote::script_root() {
140            return Some(Self::CONSTANT_FEE_POLICY_CONFIG);
141        }
142        if root == FaucetPolicyConfigNote::script_root() {
143            return Some(Self::FAUCET_POLICY_CONFIG);
144        }
145        if root == FaucetMetadataConfigNote::script_root() {
146            return Some(Self::FAUCET_METADATA_CONFIG);
147        }
148        if root == MinBurnAmountConfigNote::script_root() {
149            return Some(Self::MIN_BURN_AMOUNT_CONFIG);
150        }
151        if root == AllowlistConfigNote::script_root() {
152            return Some(Self::ALLOWLIST_CONFIG);
153        }
154        if root == BlocklistConfigNote::script_root() {
155            return Some(Self::BLOCKLIST_CONFIG);
156        }
157        if root == PauseConfigNote::script_root() {
158            return Some(Self::PAUSE_CONFIG);
159        }
160        if root == OwnerConfigNote::script_root() {
161            return Some(Self::OWNER_CONFIG);
162        }
163        if root == RbacConfigNote::script_root() {
164            return Some(Self::RBAC_CONFIG);
165        }
166        if root == NetworkAccountConfigNote::script_root() {
167            return Some(Self::NETWORK_ACCOUNT_CONFIG);
168        }
169        if root == FeeSponsorshipNote::script_root() {
170            return Some(Self::FEE_SPONSORSHIP);
171        }
172        if root == TxFeeNote::script_root() {
173            return Some(Self::TX_FEE);
174        }
175
176        None
177    }
178
179    // PUBLIC ACCESSORS
180    // --------------------------------------------------------------------------------------------
181
182    /// Returns the name of this [`StandardNote`] variant as a string.
183    pub fn name(&self) -> &'static str {
184        match self {
185            Self::P2ID => "P2ID",
186            Self::P2IDE => "P2IDE",
187            Self::SWAP => "SWAP",
188            Self::PSWAP => "PSWAP",
189            Self::MINT => "MINT",
190            Self::BURN => "BURN",
191            Self::CONSTANT_FEE_POLICY_CONFIG => "CONSTANT_FEE_POLICY_CONFIG",
192            Self::FAUCET_POLICY_CONFIG => "FAUCET_POLICY_CONFIG",
193            Self::FAUCET_METADATA_CONFIG => "FAUCET_METADATA_CONFIG",
194            Self::MIN_BURN_AMOUNT_CONFIG => "MIN_BURN_AMOUNT_CONFIG",
195            Self::ALLOWLIST_CONFIG => "ALLOWLIST_CONFIG",
196            Self::BLOCKLIST_CONFIG => "BLOCKLIST_CONFIG",
197            Self::PAUSE_CONFIG => "PAUSE_CONFIG",
198            Self::OWNER_CONFIG => "OWNER_CONFIG",
199            Self::RBAC_CONFIG => "RBAC_CONFIG",
200            Self::NETWORK_ACCOUNT_CONFIG => "NETWORK_ACCOUNT_CONFIG",
201            Self::FEE_SPONSORSHIP => "FEE_SPONSORSHIP",
202            Self::TX_FEE => "TX_FEE",
203        }
204    }
205
206    /// Returns the expected number of storage items of the active note.
207    pub fn expected_num_storage_items(&self) -> usize {
208        match self {
209            Self::P2ID => P2idNote::NUM_STORAGE_ITEMS,
210            Self::P2IDE => P2ideNote::NUM_STORAGE_ITEMS,
211            Self::SWAP => SwapNote::NUM_STORAGE_ITEMS,
212            Self::PSWAP => PswapNote::NUM_STORAGE_ITEMS,
213            Self::MINT => MintNote::NUM_STORAGE_ITEMS_PRIVATE,
214            Self::BURN => BurnNote::NUM_STORAGE_ITEMS,
215            Self::CONSTANT_FEE_POLICY_CONFIG => ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS,
216            Self::FAUCET_POLICY_CONFIG => FaucetPolicyConfigNote::NUM_STORAGE_ITEMS,
217            // FaucetMetadataConfig storage is variable per action; this returns the upper bound.
218            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS,
219            Self::MIN_BURN_AMOUNT_CONFIG => MinBurnAmountConfigNote::NUM_STORAGE_ITEMS,
220            Self::ALLOWLIST_CONFIG => AllowlistConfigNote::NUM_STORAGE_ITEMS,
221            Self::BLOCKLIST_CONFIG => BlocklistConfigNote::NUM_STORAGE_ITEMS,
222            Self::PAUSE_CONFIG => PauseConfigNote::NUM_STORAGE_ITEMS,
223            // OwnerConfig storage is variable per action; this returns the upper bound.
224            Self::OWNER_CONFIG => OwnerConfigNote::MAX_NUM_STORAGE_ITEMS,
225            // RbacConfig storage is variable per action; this returns the upper bound.
226            Self::RBAC_CONFIG => RbacConfigNote::MAX_NUM_STORAGE_ITEMS,
227            Self::NETWORK_ACCOUNT_CONFIG => NetworkAccountConfigNote::NUM_STORAGE_ITEMS,
228            Self::FEE_SPONSORSHIP => FeeSponsorshipNote::NUM_STORAGE_ITEMS,
229            Self::TX_FEE => TxFeeNote::NUM_STORAGE_ITEMS,
230        }
231    }
232
233    /// Returns the note script of the current [StandardNote] instance.
234    pub fn script(&self) -> NoteScript {
235        match self {
236            Self::P2ID => P2idNote::script(),
237            Self::P2IDE => P2ideNote::script(),
238            Self::SWAP => SwapNote::script(),
239            Self::PSWAP => PswapNote::script(),
240            Self::MINT => MintNote::script(),
241            Self::BURN => BurnNote::script(),
242            Self::CONSTANT_FEE_POLICY_CONFIG => ConstantFeePolicyConfigNote::script(),
243            Self::FAUCET_POLICY_CONFIG => FaucetPolicyConfigNote::script(),
244            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::script(),
245            Self::MIN_BURN_AMOUNT_CONFIG => MinBurnAmountConfigNote::script(),
246            Self::ALLOWLIST_CONFIG => AllowlistConfigNote::script(),
247            Self::BLOCKLIST_CONFIG => BlocklistConfigNote::script(),
248            Self::PAUSE_CONFIG => PauseConfigNote::script(),
249            Self::OWNER_CONFIG => OwnerConfigNote::script(),
250            Self::RBAC_CONFIG => RbacConfigNote::script(),
251            Self::NETWORK_ACCOUNT_CONFIG => NetworkAccountConfigNote::script(),
252            Self::FEE_SPONSORSHIP => FeeSponsorshipNote::script(),
253            Self::TX_FEE => TxFeeNote::script(),
254        }
255    }
256
257    /// Returns the script root of the current [StandardNote] instance.
258    pub fn script_root(&self) -> NoteScriptRoot {
259        match self {
260            Self::P2ID => P2idNote::script_root(),
261            Self::P2IDE => P2ideNote::script_root(),
262            Self::SWAP => SwapNote::script_root(),
263            Self::PSWAP => PswapNote::script_root(),
264            Self::MINT => MintNote::script_root(),
265            Self::BURN => BurnNote::script_root(),
266            Self::CONSTANT_FEE_POLICY_CONFIG => ConstantFeePolicyConfigNote::script_root(),
267            Self::FAUCET_POLICY_CONFIG => FaucetPolicyConfigNote::script_root(),
268            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::script_root(),
269            Self::MIN_BURN_AMOUNT_CONFIG => MinBurnAmountConfigNote::script_root(),
270            Self::ALLOWLIST_CONFIG => AllowlistConfigNote::script_root(),
271            Self::BLOCKLIST_CONFIG => BlocklistConfigNote::script_root(),
272            Self::PAUSE_CONFIG => PauseConfigNote::script_root(),
273            Self::OWNER_CONFIG => OwnerConfigNote::script_root(),
274            Self::RBAC_CONFIG => RbacConfigNote::script_root(),
275            Self::NETWORK_ACCOUNT_CONFIG => NetworkAccountConfigNote::script_root(),
276            Self::FEE_SPONSORSHIP => FeeSponsorshipNote::script_root(),
277            Self::TX_FEE => TxFeeNote::script_root(),
278        }
279    }
280
281    /// Performs the inputs check of the provided standard note against the target account and the
282    /// block number.
283    ///
284    /// This function returns:
285    /// - `Some` if we can definitively determine whether the note can be consumed not by the target
286    ///   account.
287    /// - `None` if the consumption status of the note cannot be determined conclusively and further
288    ///   checks are necessary.
289    pub fn is_consumable(
290        &self,
291        note: &Note,
292        target_account_id: AccountId,
293        block_ref: BlockNumber,
294    ) -> Option<NoteConsumptionStatus> {
295        match self.is_consumable_inner(note, target_account_id, block_ref) {
296            Ok(status) => status,
297            Err(err) => {
298                let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
299                Some(NoteConsumptionStatus::NeverConsumable(err))
300            },
301        }
302    }
303
304    /// Performs the inputs check of the provided note against the target account and the block
305    /// number.
306    ///
307    /// It performs:
308    /// - for `P2ID` note:
309    ///     - check that note storage has correct number of values.
310    ///     - assertion that the account ID provided by the note storage is equal to the target
311    ///       account ID.
312    /// - for `P2IDE` note:
313    ///     - check that note storage has correct number of values.
314    ///     - check that the target account is either the receiver account or the reclaimer account.
315    ///     - check that depending on whether the target account is reclaimer or receiver, it could
316    ///       be either consumed, or consumed after timelock height, or consumed after reclaim
317    ///       height.
318    /// - for `TX_FEE` note:
319    ///     - check that note storage is empty; the note is otherwise consumable by any account.
320    fn is_consumable_inner(
321        &self,
322        note: &Note,
323        target_account_id: AccountId,
324        block_ref: BlockNumber,
325    ) -> Result<Option<NoteConsumptionStatus>, NoteError> {
326        match self {
327            StandardNote::P2ID => {
328                let input_account_id = P2idNoteStorage::try_from(note.storage().items())
329                    .map_err(|e| NoteError::other_with_source("invalid P2ID note storage", e))?;
330
331                if input_account_id.target() == target_account_id {
332                    Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
333                } else {
334                    Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note storage doesn't match the target account ID".into())))
335                }
336            },
337            StandardNote::P2IDE => {
338                let storage = P2ideNoteStorage::try_from(note.storage().items())
339                    .map_err(|e| NoteError::other_with_source("invalid P2IDE note storage", e))?;
340
341                let reclaimer_account_id = storage.reclaimer();
342                let receiver_account_id = storage.target();
343
344                let current_block_height = block_ref.as_u32();
345                let reclaim_height = storage.reclaim_height().unwrap_or_default().as_u32();
346                let timelock_height = storage.timelock_height().unwrap_or_default().as_u32();
347
348                // block height after which the reclaimer account can consume the note
349                let consumable_after = reclaim_height.max(timelock_height);
350
351                // handle the case when the target account of the transaction is the reclaimer
352                if target_account_id == reclaimer_account_id {
353                    // For the reclaimer, the current block height needs to have reached both
354                    // reclaim and timelock height to be consumable.
355                    if current_block_height >= consumable_after {
356                        Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
357                    } else {
358                        Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
359                            consumable_after,
360                        ))))
361                    }
362                // handle the case when the target account of the transaction is receiver
363                } else if target_account_id == receiver_account_id {
364                    // For the receiver, the current block height needs to have reached only the
365                    // timelock height to be consumable: we can ignore the reclaim height in this
366                    // case
367                    if current_block_height >= timelock_height {
368                        Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
369                    } else {
370                        Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
371                            timelock_height,
372                        ))))
373                    }
374                // if the target account is neither the reclaimer nor the receiver (from the
375                // note's storage), then this account cannot consume the note
376                } else {
377                    Ok(Some(NoteConsumptionStatus::NeverConsumable(
378            "target account of the transaction does not match neither the receiver account specified by the P2IDE storage, nor the reclaimer account".into()
379        )))
380                }
381            },
382
383            // TX_FEE notes carry no target restriction: any account can consume them, as long as
384            // the note carries no storage items (the note script rejects any other
385            // storage shape).
386            StandardNote::TX_FEE => {
387                if usize::from(note.storage().num_items()) != TxFeeNote::NUM_STORAGE_ITEMS {
388                    Ok(Some(NoteConsumptionStatus::NeverConsumable(
389                        "TX_FEE note carries unexpected storage items".into(),
390                    )))
391                } else {
392                    Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
393                }
394            },
395
396            // the consumption status of any other note cannot be determined by the static analysis,
397            // further checks are necessary.
398            _ => Ok(None),
399        }
400    }
401}
402
403// HELPER FUNCTIONS
404// ================================================================================================
405
406/// Decodes an optional block height stored as a single storage item, where zero encodes `None`.
407///
408/// `error_msg` names the field being decoded so that a caller can tell the heights apart.
409pub(crate) fn decode_optional_block_height(
410    item: Felt,
411    error_msg: &'static str,
412) -> Result<Option<BlockNumber>, NoteError> {
413    if item == Felt::ZERO {
414        return Ok(None);
415    }
416
417    let height: u32 = item
418        .as_canonical_u64()
419        .try_into()
420        .map_err(|e| NoteError::other_with_source(error_msg, e))?;
421
422    Ok(Some(BlockNumber::from(height)))
423}
424
425// HELPER STRUCTURES
426// ================================================================================================
427
428/// Describes if a note could be consumed under a specific conditions: target account state
429/// and block height.
430///
431/// The status does not account for any authorization that may be required to consume the
432/// note, nor does it indicate whether the account has sufficient fees to consume it.
433#[derive(Debug)]
434pub enum NoteConsumptionStatus {
435    /// The note can be consumed by the account at the specified block height.
436    Consumable,
437    /// The note can be consumed by the account after the required block height is achieved.
438    ConsumableAfter(BlockNumber),
439    /// The note can be consumed by the account if proper authorization is provided.
440    ConsumableWithAuthorization,
441    /// The note cannot be consumed by the account at the specified conditions (i.e., block
442    /// height and account state).
443    UnconsumableConditions,
444    /// The note cannot be consumed by the specified account under any conditions.
445    NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
446}
447
448impl Clone for NoteConsumptionStatus {
449    fn clone(&self) -> Self {
450        match self {
451            NoteConsumptionStatus::Consumable => NoteConsumptionStatus::Consumable,
452            NoteConsumptionStatus::ConsumableAfter(block_height) => {
453                NoteConsumptionStatus::ConsumableAfter(*block_height)
454            },
455            NoteConsumptionStatus::ConsumableWithAuthorization => {
456                NoteConsumptionStatus::ConsumableWithAuthorization
457            },
458            NoteConsumptionStatus::UnconsumableConditions => {
459                NoteConsumptionStatus::UnconsumableConditions
460            },
461            NoteConsumptionStatus::NeverConsumable(error) => {
462                let err = error.to_string();
463                NoteConsumptionStatus::NeverConsumable(err.into())
464            },
465        }
466    }
467}