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, FungibleAsset, NonFungibleAsset};
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::P2idNote;
26use crate::note::costs::{MINT_CONSUMPTION_CYCLES, NoteConsumptionCost};
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 (a fungible asset, or a non-fungible commitment)
50/// directly from the note's storage. For fungible faucets the embedded `ASSET_ID` binds the note
51/// to one faucet, so a MINT note bound to faucet A cannot be redirected to faucet B. MINT notes are
52/// always public (for network execution) and carry no assets; the output note minted on
53/// consumption can be private or public depending on the [`MintNoteStorage`] variant.
54///
55/// Construct one with the [builder](MintNote::builder); convert it into a protocol [`Note`]
56/// infallibly via `Note::from`.
57#[derive(Debug, Clone)]
58pub struct MintNote {
59    sender: AccountId,
60    storage: MintNoteStorage,
61    serial_number: Word,
62    attachments: NoteAttachments,
63}
64
65#[bon::bon]
66impl MintNote {
67    /// Builds a new [`MintNote`] that mints the asset embedded in `mint_storage`.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the attachments exceed their protocol limit (see
72    /// [`NoteAttachments::new`]).
73    #[builder]
74    pub fn new(
75        #[builder(field)] attachments: Vec<NoteAttachment>,
76        sender: AccountId,
77        #[builder(name = mint_storage)] storage: MintNoteStorage,
78        serial_number: Word,
79    ) -> Result<Self, NoteError> {
80        let attachments = NoteAttachments::new(attachments)?;
81
82        Ok(Self {
83            sender,
84            storage,
85            serial_number,
86            attachments,
87        })
88    }
89}
90
91impl MintNote {
92    // CONSTANTS
93    // --------------------------------------------------------------------------------------------
94
95    /// Expected number of storage items of a fungible MINT note (private mode).
96    ///
97    /// Layout: RECIPIENT(4) + ASSET_ID(4) + ASSET_VALUE(4) + tag(1).
98    pub const NUM_STORAGE_ITEMS_PRIVATE: usize = 13;
99
100    /// Minimum number of storage items of a fungible MINT note (public mode).
101    ///
102    /// Layout: SCRIPT_ROOT(4) + SERIAL_NUM(4) + ASSET_ID(4) + ASSET_VALUE(4) + tag(1) +
103    /// padding(3) + variable output-note storage. The variable portion starts at offset 20
104    /// (word-aligned) and may contain zero or more items.
105    pub const MIN_NUM_STORAGE_ITEMS_PUBLIC: usize = 20;
106
107    /// Expected number of storage items of a non-fungible MINT note (private mode).
108    ///
109    /// Layout: RECIPIENT(4) + COMMITMENT(4) + tag(1).
110    pub const NON_FUNGIBLE_NUM_STORAGE_ITEMS_PRIVATE: usize = 9;
111
112    /// Minimum number of storage items of a non-fungible MINT note (public mode).
113    ///
114    /// Layout: SCRIPT_ROOT(4) + SERIAL_NUM(4) + COMMITMENT(4) + tag(1) + padding(3) + variable
115    /// output-note storage. The variable portion starts at offset 16 (word-aligned).
116    pub const NON_FUNGIBLE_MIN_NUM_STORAGE_ITEMS_PUBLIC: usize = 16;
117
118    // PUBLIC ACCESSORS
119    // --------------------------------------------------------------------------------------------
120
121    /// Returns the script of the MINT note.
122    pub fn script() -> NoteScript {
123        MINT_SCRIPT.clone()
124    }
125
126    /// Returns the MINT note script root.
127    pub fn script_root() -> NoteScriptRoot {
128        MINT_SCRIPT.root()
129    }
130
131    /// Returns the account ID of the faucet that will mint the asset.
132    pub fn faucet_id(&self) -> AccountId {
133        self.storage.faucet_id()
134    }
135
136    /// Returns the account ID of the note's sender (the faucet owner).
137    pub fn sender(&self) -> AccountId {
138        self.sender
139    }
140
141    /// Returns the note's storage configuration.
142    pub fn storage(&self) -> &MintNoteStorage {
143        &self.storage
144    }
145
146    /// Returns the note's serial number.
147    pub fn serial_number(&self) -> Word {
148        self.serial_number
149    }
150
151    /// Returns the attachments carried by the note.
152    pub fn attachments(&self) -> &NoteAttachments {
153        &self.attachments
154    }
155}
156
157// BUILDER EXTENSIONS
158// ================================================================================================
159
160impl<S: mint_note_builder::State> MintNoteBuilder<S> {
161    /// Adds a single attachment to the note.
162    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
163        self.attachments.push(attachment.into());
164        self
165    }
166
167    /// Adds multiple attachments to the note.
168    pub fn attachments(
169        mut self,
170        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
171    ) -> Self {
172        self.attachments.extend(attachments.into_iter().map(Into::into));
173        self
174    }
175}
176
177impl<S: mint_note_builder::State> MintNoteBuilder<S>
178where
179    S::SerialNumber: mint_note_builder::IsUnset,
180{
181    /// Draws a serial number from `rng` and sets it on the builder.
182    pub fn generate_serial_number(
183        self,
184        rng: &mut impl FeltRng,
185    ) -> MintNoteBuilder<mint_note_builder::SetSerialNumber<S>> {
186        self.serial_number(rng.draw_word())
187    }
188}
189
190// CONVERSIONS
191// ================================================================================================
192
193impl From<MintNote> for Note {
194    fn from(note: MintNote) -> Self {
195        // MINT notes are always public for network execution and carry no assets; the asset to mint
196        // lives in the note's storage.
197        let faucet_id = note.storage.faucet_id();
198        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
199            .with_tag(NoteTag::with_account_target(faucet_id));
200        let recipient = NoteRecipient::new(
201            note.serial_number,
202            MintNote::script(),
203            NoteStorage::from(note.storage),
204        );
205
206        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
207    }
208}
209
210// MINT NOTE STORAGE
211// ================================================================================================
212
213/// Represents the different storage formats for MINT notes.
214///
215/// The MINT note serves both fungible and non-fungible faucets. The fungible variants embed a
216/// [`FungibleAsset`] (`ASSET_ID` + `ASSET_VALUE`, 8 felts) so the faucet executing the note can be
217/// checked against the asset's faucet ID at mint time. The non-fungible variants embed a
218/// [`NonFungibleAsset`], whose value word (`ASSET_VALUE`, 4 felts) is the asset commitment and
219/// whose faucet ID routes the note.
220///
221/// - Fungible private (13 items): RECIPIENT + ASSET_ID + ASSET_VALUE + tag.
222/// - Fungible public (20+ items): SCRIPT_ROOT + SERIAL_NUM + ASSET_ID + ASSET_VALUE + tag +
223///   padding(3) + variable output-note storage (word-aligned at offset 20).
224/// - Non-fungible private (9 items): RECIPIENT + COMMITMENT + tag.
225/// - Non-fungible public (16+ items): SCRIPT_ROOT + SERIAL_NUM + COMMITMENT + tag + padding(3) +
226///   variable output-note storage (word-aligned at offset 16).
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub enum MintNoteStorage {
229    FungiblePrivate {
230        recipient_digest: Word,
231        asset: FungibleAsset,
232        tag: NoteTag,
233    },
234    FungiblePublic {
235        recipient: NoteRecipient,
236        asset: FungibleAsset,
237        tag: NoteTag,
238    },
239    NonFungiblePrivate {
240        recipient_digest: Word,
241        asset: NonFungibleAsset,
242        tag: NoteTag,
243    },
244    NonFungiblePublic {
245        recipient: NoteRecipient,
246        asset: NonFungibleAsset,
247        tag: NoteTag,
248    },
249}
250
251impl MintNoteStorage {
252    /// Builds fungible private-mode storage (creates a private output note).
253    pub fn new_fungible_private(
254        recipient_digest: Word,
255        asset: FungibleAsset,
256        tag: NoteTag,
257    ) -> Self {
258        Self::FungiblePrivate { recipient_digest, asset, tag }
259    }
260
261    /// Builds fungible public-mode storage (creates a public output note).
262    pub fn new_fungible_public(
263        recipient: NoteRecipient,
264        asset: FungibleAsset,
265        tag: NoteTag,
266    ) -> Result<Self, NoteError> {
267        let total_storage_items =
268            MintNote::MIN_NUM_STORAGE_ITEMS_PUBLIC + recipient.storage().num_items() as usize;
269
270        if total_storage_items > MAX_NOTE_STORAGE_ITEMS {
271            return Err(NoteError::TooManyStorageItems(total_storage_items));
272        }
273
274        Ok(Self::FungiblePublic { recipient, asset, tag })
275    }
276
277    /// Builds non-fungible private-mode storage (creates a private output note).
278    pub fn new_non_fungible_private(
279        recipient_digest: Word,
280        asset: NonFungibleAsset,
281        tag: NoteTag,
282    ) -> Self {
283        Self::NonFungiblePrivate { recipient_digest, asset, tag }
284    }
285
286    /// Builds non-fungible public-mode storage (creates a public output note).
287    pub fn new_non_fungible_public(
288        recipient: NoteRecipient,
289        asset: NonFungibleAsset,
290        tag: NoteTag,
291    ) -> Result<Self, NoteError> {
292        let total_storage_items = MintNote::NON_FUNGIBLE_MIN_NUM_STORAGE_ITEMS_PUBLIC
293            + recipient.storage().num_items() as usize;
294
295        if total_storage_items > MAX_NOTE_STORAGE_ITEMS {
296            return Err(NoteError::TooManyStorageItems(total_storage_items));
297        }
298
299        Ok(Self::NonFungiblePublic { recipient, asset, tag })
300    }
301
302    /// Returns the account ID of the faucet that will mint the asset.
303    pub fn faucet_id(&self) -> AccountId {
304        match self {
305            Self::FungiblePrivate { asset, .. } | Self::FungiblePublic { asset, .. } => {
306                asset.faucet_id()
307            },
308            Self::NonFungiblePrivate { asset, .. } | Self::NonFungiblePublic { asset, .. } => {
309                asset.faucet_id()
310            },
311        }
312    }
313}
314
315impl From<MintNoteStorage> for NoteStorage {
316    fn from(mint_storage: MintNoteStorage) -> Self {
317        match mint_storage {
318            MintNoteStorage::FungiblePrivate { recipient_digest, asset, tag } => {
319                let mut storage_values = Vec::with_capacity(MintNote::NUM_STORAGE_ITEMS_PRIVATE);
320                storage_values.extend_from_slice(recipient_digest.as_elements());
321                storage_values.extend_from_slice(&Asset::from(asset).as_elements());
322                storage_values.push(tag.into());
323                NoteStorage::new(storage_values)
324                    .expect("number of storage items should not exceed max storage items")
325            },
326            MintNoteStorage::FungiblePublic { recipient, asset, tag } => {
327                let mut storage_values = Vec::new();
328                storage_values.extend_from_slice(recipient.script().root().as_elements());
329                storage_values.extend_from_slice(recipient.serial_num().as_elements());
330                storage_values.extend_from_slice(&Asset::from(asset).as_elements());
331                // tag followed by 3 padding felts so the variable storage that follows starts at
332                // a word-aligned offset (20).
333                storage_values.extend_from_slice(&[tag.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
334                storage_values.extend_from_slice(recipient.storage().items());
335                NoteStorage::new(storage_values)
336                    .expect("number of storage items should not exceed max storage items")
337            },
338            MintNoteStorage::NonFungiblePrivate { recipient_digest, asset, tag } => {
339                let mut storage_values =
340                    Vec::with_capacity(MintNote::NON_FUNGIBLE_NUM_STORAGE_ITEMS_PRIVATE);
341                storage_values.extend_from_slice(recipient_digest.as_elements());
342                storage_values.extend_from_slice(asset.to_value_word().as_elements());
343                storage_values.push(tag.into());
344                NoteStorage::new(storage_values)
345                    .expect("number of storage items should not exceed max storage items")
346            },
347            MintNoteStorage::NonFungiblePublic { recipient, asset, tag } => {
348                let mut storage_values = Vec::new();
349                storage_values.extend_from_slice(recipient.script().root().as_elements());
350                storage_values.extend_from_slice(recipient.serial_num().as_elements());
351                storage_values.extend_from_slice(asset.to_value_word().as_elements());
352                // tag followed by 3 padding felts so the variable storage that follows starts at
353                // a word-aligned offset (16).
354                storage_values.extend_from_slice(&[tag.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
355                storage_values.extend_from_slice(recipient.storage().items());
356                NoteStorage::new(storage_values)
357                    .expect("number of storage items should not exceed max storage items")
358            },
359        }
360    }
361}
362
363// NOTE CONSUMPTION COST
364// ================================================================================================
365
366impl NoteConsumptionCost for MintNote {
367    fn consumption_cycles() -> u32 {
368        MINT_CONSUMPTION_CYCLES
369    }
370
371    /// Consuming a MINT note typically creates the P2ID note delivering the minted asset
372    /// (the recipient digest may encode any script; P2ID is the standard flow).
373    fn created_notes() -> Vec<NoteScriptRoot> {
374        vec![P2idNote::script_root()]
375    }
376}
377
378// TESTS
379// ================================================================================================
380
381#[cfg(test)]
382mod tests {
383    use miden_protocol::account::AccountType;
384    use miden_protocol::crypto::rand::RandomCoin;
385
386    use super::*;
387
388    fn faucet() -> AccountId {
389        AccountId::builder().account_type(AccountType::Public).build_with_seed([1; 32])
390    }
391
392    fn owner() -> AccountId {
393        AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32])
394    }
395
396    /// The builder produces a public, asset-less note tagged for the faucet.
397    #[test]
398    fn builder_builds_public_mint_note() {
399        let mut rng = RandomCoin::new(Word::empty());
400        let asset = FungibleAsset::new(faucet(), 50).unwrap();
401        let mint_storage =
402            MintNoteStorage::new_fungible_private(Word::empty(), asset, NoteTag::default());
403        let mint_note = MintNote::builder()
404            .sender(owner())
405            .mint_storage(mint_storage)
406            .generate_serial_number(&mut rng)
407            .build()
408            .unwrap();
409
410        assert_eq!(mint_note.faucet_id(), faucet());
411        assert_eq!(mint_note.sender(), owner());
412
413        let note = Note::from(mint_note);
414        assert_eq!(note.metadata().note_type(), NoteType::Public);
415        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet()));
416        assert_eq!(note.assets().num_assets(), 0);
417    }
418}