Skip to main content

miden_standards/note/
mint.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::crypto::rand::FeltRng;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9    Note,
10    NoteAssets,
11    NoteAttachment,
12    NoteAttachments,
13    NoteRecipient,
14    NoteScript,
15    NoteScriptRoot,
16    NoteStorage,
17    NoteTag,
18    NoteType,
19    PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
23
24use crate::StandardsLib;
25use crate::note::costs::{MINT_CONSUMPTION_CYCLES, NoteConsumptionCost};
26use crate::note::{NetworkAccountTarget, NumStorageItems, P2idNote};
27
28// NOTE SCRIPT
29// ================================================================================================
30
31/// Path to the MINT note script procedure in the standards library.
32const MINT_SCRIPT_PATH: &str = "::miden::standards::notes::mint::main";
33
34// Initialize the MINT note script only once
35static MINT_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
36    let standards_lib = StandardsLib::default();
37    let path = Path::new(MINT_SCRIPT_PATH);
38    NoteScript::from_package_reference(standards_lib.as_ref(), path)
39        .expect("Standards library contains MINT note script procedure")
40});
41
42// MINT NOTE
43// ================================================================================================
44
45/// A MINT note: instructs a network faucet to mint the asset embedded in its storage.
46///
47/// The single MINT script works against both fungible and non-fungible faucets: it detects the
48/// faucet kind by reflection (via the `CodeInspection` component) and calls the matching
49/// `mint_and_send`. The script reads the asset directly from the note's storage, in the same layout
50/// for both faucet kinds. MINT notes are always public (for network execution) and carry no assets;
51/// the output note minted on consumption can be private or public depending on the
52/// [`MintNoteStorage`] variant.
53///
54/// A MINT note for a public faucet is tagged for that faucet and carries a
55/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment naming it, both derived
56/// from the asset in the note's storage, so the network can route the note to it. A private faucet
57/// can never be a network account, so a note for one is only tagged and carries no such
58/// attachment.
59///
60/// Construct one with the [builder](MintNote::builder); convert it into a protocol [`Note`]
61/// infallibly via `Note::from`.
62#[derive(Debug, Clone)]
63pub struct MintNote {
64    sender: AccountId,
65    storage: MintNoteStorage,
66    serial_number: Word,
67    attachments: NoteAttachments,
68}
69
70#[bon::bon]
71impl MintNote {
72    /// Builds a new [`MintNote`] that mints the asset embedded in `mint_storage`.
73    ///
74    /// The faucet the note is bound to comes from [`MintNoteStorage`].
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if:
79    /// - the attachments carry a `NetworkAccountTarget` for an account other than the faucet.
80    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]).
81    #[builder]
82    pub fn new(
83        #[builder(field)] mut attachments: Vec<NoteAttachment>,
84        sender: AccountId,
85        #[builder(name = mint_storage)] storage: MintNoteStorage,
86        serial_number: Word,
87    ) -> Result<Self, NoteError> {
88        // The network routes the note on this attachment; the stored ASSET_ID is what binds the
89        // script to the same faucet on consumption.
90        NetworkAccountTarget::ensure_presence_if_public(&mut attachments, storage.faucet_id())
91            .map_err(|err| {
92                NoteError::other_with_source("failed to target the MINT note at its faucet", err)
93            })?;
94
95        let attachments = NoteAttachments::new(attachments)?;
96
97        Ok(Self {
98            sender,
99            storage,
100            serial_number,
101            attachments,
102        })
103    }
104}
105
106impl MintNote {
107    // CONSTANTS
108    // --------------------------------------------------------------------------------------------
109
110    /// Expected number of storage items of a MINT note (private mode).
111    ///
112    /// Layout: RECIPIENT(4) + ASSET_ID(4) + ASSET_VALUE(4) + tag(1).
113    pub const NUM_STORAGE_ITEMS_PRIVATE: usize = 13;
114
115    /// Minimum number of storage items of a MINT note (public mode).
116    ///
117    /// Layout: SCRIPT_ROOT(4) + SERIAL_NUM(4) + ASSET_ID(4) + ASSET_VALUE(4) + tag(1) +
118    /// padding(3) + variable output-note storage. The variable portion starts at offset 20
119    /// (word-aligned) and may contain zero or more items.
120    pub const MIN_NUM_STORAGE_ITEMS_PUBLIC: usize = 20;
121
122    /// The numbers of storage items the MINT note script accepts.
123    ///
124    /// A note creating a private output note holds exactly [`Self::NUM_STORAGE_ITEMS_PRIVATE`]
125    /// items, while one creating a public output note holds at least
126    /// [`Self::MIN_NUM_STORAGE_ITEMS_PUBLIC`] and grows with the storage of the output note
127    /// recipient.
128    pub const NUM_STORAGE_ITEMS: NumStorageItems = NumStorageItems::AnyOf(&[
129        NumStorageItems::Exact(Self::NUM_STORAGE_ITEMS_PRIVATE),
130        NumStorageItems::Range {
131            min: Self::MIN_NUM_STORAGE_ITEMS_PUBLIC,
132            max: MAX_NOTE_STORAGE_ITEMS,
133        },
134    ]);
135
136    // PUBLIC ACCESSORS
137    // --------------------------------------------------------------------------------------------
138
139    /// Returns the script of the MINT note.
140    pub fn script() -> NoteScript {
141        MINT_SCRIPT.clone()
142    }
143
144    /// Returns the MINT note script root.
145    pub fn script_root() -> NoteScriptRoot {
146        MINT_SCRIPT.root()
147    }
148
149    /// Returns the account ID of the faucet that will mint the asset.
150    pub fn faucet_id(&self) -> AccountId {
151        self.storage.faucet_id()
152    }
153
154    /// Returns the account ID of the note's sender (the faucet owner).
155    pub fn sender(&self) -> AccountId {
156        self.sender
157    }
158
159    /// Returns the note's storage configuration.
160    pub fn storage(&self) -> &MintNoteStorage {
161        &self.storage
162    }
163
164    /// Returns the note's serial number.
165    pub fn serial_number(&self) -> Word {
166        self.serial_number
167    }
168
169    /// Returns the attachments carried by the note.
170    pub fn attachments(&self) -> &NoteAttachments {
171        &self.attachments
172    }
173}
174
175// BUILDER EXTENSIONS
176// ================================================================================================
177
178impl<S: mint_note_builder::State> MintNoteBuilder<S> {
179    /// Adds a single attachment to the note.
180    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
181        self.attachments.push(attachment.into());
182        self
183    }
184
185    /// Adds multiple attachments to the note.
186    pub fn attachments(
187        mut self,
188        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
189    ) -> Self {
190        self.attachments.extend(attachments.into_iter().map(Into::into));
191        self
192    }
193}
194
195impl<S: mint_note_builder::State> MintNoteBuilder<S>
196where
197    S::SerialNumber: mint_note_builder::IsUnset,
198{
199    /// Draws a serial number from `rng` and sets it on the builder.
200    pub fn generate_serial_number(
201        self,
202        rng: &mut impl FeltRng,
203    ) -> MintNoteBuilder<mint_note_builder::SetSerialNumber<S>> {
204        self.serial_number(rng.draw_word())
205    }
206}
207
208// CONVERSIONS
209// ================================================================================================
210
211impl From<MintNote> for Note {
212    fn from(note: MintNote) -> Self {
213        // MINT notes are always public for network execution and carry no assets; the asset to mint
214        // lives in the note's storage.
215        let faucet_id = note.storage.faucet_id();
216        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
217            .with_tag(NoteTag::with_account_target(faucet_id));
218        let recipient = NoteRecipient::new(
219            note.serial_number,
220            MintNote::script(),
221            NoteStorage::from(note.storage),
222        );
223
224        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
225    }
226}
227
228// MINT NOTE STORAGE
229// ================================================================================================
230
231/// Represents the different storage formats for MINT notes.
232///
233/// The MINT note serves both fungible and non-fungible faucets, and both use the same layout: the
234/// note embeds the full [`Asset`] (`ASSET_ID` + `ASSET_VALUE`, 8 felts). The `ASSET_ID` is what
235/// binds the note to one faucet - the faucet's `mint_and_send` derives the asset for the active
236/// account and asserts it equals the stored `ASSET_ID`, so a note created for one faucet cannot be
237/// minted by another. This works for non-fungible assets too, since a non-fungible `ASSET_ID` is
238/// `f(faucet_id, ASSET_VALUE)` and is therefore known when the note is built.
239///
240/// - Private (13 items): RECIPIENT + ASSET_ID + ASSET_VALUE + tag.
241/// - Public (20+ items): SCRIPT_ROOT + SERIAL_NUM + ASSET_ID + ASSET_VALUE + tag + padding(3) +
242///   variable output-note storage (word-aligned at offset 20).
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum MintNoteStorage {
245    Private {
246        recipient_digest: Word,
247        asset: Asset,
248        tag: NoteTag,
249    },
250    Public {
251        recipient: NoteRecipient,
252        asset: Asset,
253        tag: NoteTag,
254    },
255}
256
257impl MintNoteStorage {
258    /// Builds private-mode storage (creates a private output note).
259    pub fn new_private(recipient_digest: Word, asset: impl Into<Asset>, tag: NoteTag) -> Self {
260        Self::Private {
261            recipient_digest,
262            asset: asset.into(),
263            tag,
264        }
265    }
266
267    /// Builds public-mode storage (creates a public output note).
268    pub fn new_public(
269        recipient: NoteRecipient,
270        asset: impl Into<Asset>,
271        tag: NoteTag,
272    ) -> Result<Self, NoteError> {
273        let total_storage_items =
274            MintNote::MIN_NUM_STORAGE_ITEMS_PUBLIC + recipient.storage().num_items() as usize;
275
276        if total_storage_items > MAX_NOTE_STORAGE_ITEMS {
277            return Err(NoteError::TooManyStorageItems(total_storage_items));
278        }
279
280        Ok(Self::Public { recipient, asset: asset.into(), tag })
281    }
282
283    /// Returns the asset that will be minted.
284    pub fn asset(&self) -> Asset {
285        match self {
286            Self::Private { asset, .. } | Self::Public { asset, .. } => *asset,
287        }
288    }
289
290    /// Returns the account ID of the faucet that will mint the asset.
291    pub fn faucet_id(&self) -> AccountId {
292        self.asset().faucet_id()
293    }
294}
295
296impl From<MintNoteStorage> for NoteStorage {
297    fn from(mint_storage: MintNoteStorage) -> Self {
298        match mint_storage {
299            MintNoteStorage::Private { recipient_digest, asset, tag } => {
300                let mut storage_values = Vec::with_capacity(MintNote::NUM_STORAGE_ITEMS_PRIVATE);
301                storage_values.extend_from_slice(recipient_digest.as_elements());
302                storage_values.extend_from_slice(&asset.as_elements());
303                storage_values.push(tag.into());
304                NoteStorage::new(storage_values)
305                    .expect("number of storage items should not exceed max storage items")
306            },
307            MintNoteStorage::Public { recipient, asset, tag } => {
308                let mut storage_values = Vec::new();
309                storage_values.extend_from_slice(recipient.script().root().as_elements());
310                storage_values.extend_from_slice(recipient.serial_num().as_elements());
311                storage_values.extend_from_slice(&asset.as_elements());
312                // tag followed by 3 padding felts so the variable storage that follows starts at
313                // a word-aligned offset (20).
314                storage_values.extend_from_slice(&[tag.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
315                storage_values.extend_from_slice(recipient.storage().items());
316                NoteStorage::new(storage_values)
317                    .expect("number of storage items should not exceed max storage items")
318            },
319        }
320    }
321}
322
323// NOTE CONSUMPTION COST
324// ================================================================================================
325
326impl NoteConsumptionCost for MintNote {
327    fn consumption_cycles() -> u32 {
328        MINT_CONSUMPTION_CYCLES
329    }
330
331    /// Consuming a MINT note typically creates the P2ID note delivering the minted asset
332    /// (the recipient digest may encode any script; P2ID is the standard flow).
333    fn created_notes() -> Vec<NoteScriptRoot> {
334        vec![P2idNote::script_root()]
335    }
336}
337
338// TESTS
339// ================================================================================================
340
341#[cfg(test)]
342mod tests {
343    use miden_protocol::account::AccountType;
344    use miden_protocol::asset::FungibleAsset;
345    use miden_protocol::crypto::rand::RandomCoin;
346
347    use super::*;
348    use crate::note::{NetworkNoteExt, NoteExecutionHint};
349
350    fn faucet() -> AccountId {
351        AccountId::builder().account_type(AccountType::Public).build_with_seed([1; 32])
352    }
353
354    fn private_faucet() -> AccountId {
355        AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
356    }
357
358    fn owner() -> AccountId {
359        AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32])
360    }
361
362    fn build_mint_note(faucet_id: AccountId) -> MintNote {
363        let asset = FungibleAsset::new(faucet_id, 50).unwrap();
364        let mut rng = RandomCoin::new(Word::empty());
365        MintNote::builder()
366            .sender(owner())
367            .mint_storage(MintNoteStorage::new_private(Word::empty(), asset, NoteTag::default()))
368            .generate_serial_number(&mut rng)
369            .build()
370            .unwrap()
371    }
372
373    /// The builder produces a public, asset-less note tagged for the faucet and routed to it by a
374    /// derived network target. How that target treats caller-supplied attachments is covered by the
375    /// `network_account_target` tests.
376    #[test]
377    fn builder_builds_public_mint_note() {
378        let mint_note = build_mint_note(faucet());
379
380        assert_eq!(mint_note.faucet_id(), faucet());
381        assert_eq!(mint_note.sender(), owner());
382        assert_eq!(mint_note.attachments().num_attachments(), 1);
383
384        let note = Note::from(mint_note);
385        assert_eq!(note.metadata().note_type(), NoteType::Public);
386        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet()));
387        assert_eq!(note.assets().num_assets(), 0);
388        assert!(note.is_network_note());
389
390        let target = NetworkAccountTarget::try_from(note.attachments()).unwrap();
391        assert_eq!(target.target_id(), faucet());
392        assert_eq!(target.execution_hint(), NoteExecutionHint::Always);
393    }
394
395    /// A private faucet is never a network account, so no target is derived for it. The note is
396    /// still tagged for the faucet and remains consumable by it.
397    #[test]
398    fn builder_omits_network_target_for_private_faucet() {
399        let mint_note = build_mint_note(private_faucet());
400
401        assert_eq!(mint_note.attachments().num_attachments(), 0);
402
403        let note = Note::from(mint_note);
404        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(private_faucet()));
405        assert!(!note.is_network_note());
406    }
407}