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