Skip to main content

miden_standards/note/
burn.rs

1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::AccountId;
5use miden_protocol::assembly::Path;
6use miden_protocol::asset::Asset;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10    Note,
11    NoteAssets,
12    NoteAttachment,
13    NoteAttachments,
14    NoteRecipient,
15    NoteScript,
16    NoteScriptRoot,
17    NoteStorage,
18    NoteTag,
19    NoteType,
20    PartialNoteMetadata,
21};
22use miden_protocol::utils::sync::LazyLock;
23
24use crate::StandardsLib;
25
26// NOTE SCRIPT
27// ================================================================================================
28
29/// Path to the BURN note script procedure in the standards library.
30const BURN_SCRIPT_PATH: &str = "::miden::standards::notes::burn::main";
31
32// Initialize the BURN note script only once
33static BURN_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
34    let standards_lib = StandardsLib::default();
35    let path = Path::new(BURN_SCRIPT_PATH);
36    NoteScript::from_library_reference(standards_lib.as_ref(), path)
37        .expect("Standards library contains BURN note script procedure")
38});
39
40// BURN NOTE
41// ================================================================================================
42
43/// A BURN note: instructs a faucet to burn the asset carried by the note.
44///
45/// When consumed by the faucet that issued the asset, the note's asset is destroyed via the
46/// faucet's `receive_and_burn` procedure. The single BURN script works against both fungible and
47/// non-fungible faucets: it detects the faucet kind by reflection (via the `CodeInspection`
48/// component) and calls the matching `receive_and_burn`. BURN notes are always public so they can
49/// be executed by the network.
50///
51/// Construct one with the [builder](BurnNote::builder); convert it into a protocol [`Note`]
52/// infallibly via `Note::from`.
53#[derive(Debug, Clone)]
54pub struct BurnNote {
55    sender: AccountId,
56    serial_number: Word,
57    asset: Asset,
58    attachments: NoteAttachments,
59}
60
61#[bon::bon]
62impl BurnNote {
63    /// Builds a new [`BurnNote`] that burns `asset` against the faucet that issued it.
64    ///
65    /// The target faucet is the asset's own issuing faucet; the note is tagged for it.
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(into)] asset: Asset,
76        serial_number: Word,
77    ) -> Result<Self, NoteError> {
78        let attachments = NoteAttachments::new(attachments)?;
79
80        Ok(Self {
81            sender,
82            serial_number,
83            asset,
84            attachments,
85        })
86    }
87}
88
89impl BurnNote {
90    // CONSTANTS
91    // --------------------------------------------------------------------------------------------
92
93    /// Expected number of storage items of the BURN note.
94    pub const NUM_STORAGE_ITEMS: usize = 0;
95
96    // PUBLIC ACCESSORS
97    // --------------------------------------------------------------------------------------------
98
99    /// Returns the script of the BURN note.
100    pub fn script() -> NoteScript {
101        BURN_SCRIPT.clone()
102    }
103
104    /// Returns the BURN note script root.
105    pub fn script_root() -> NoteScriptRoot {
106        BURN_SCRIPT.root()
107    }
108
109    /// Returns the account ID of the note's sender.
110    pub fn sender(&self) -> AccountId {
111        self.sender
112    }
113
114    /// Returns the account ID of the faucet that will burn the asset (the asset's own faucet).
115    pub fn faucet_id(&self) -> AccountId {
116        self.asset.faucet_id()
117    }
118
119    /// Returns the note's serial number.
120    pub fn serial_number(&self) -> Word {
121        self.serial_number
122    }
123
124    /// Returns the asset carried by the note (the asset to be burned).
125    pub fn asset(&self) -> Asset {
126        self.asset
127    }
128
129    /// Returns the attachments carried by the note.
130    pub fn attachments(&self) -> &NoteAttachments {
131        &self.attachments
132    }
133}
134
135// BUILDER EXTENSIONS
136// ================================================================================================
137
138impl<S: burn_note_builder::State> BurnNoteBuilder<S> {
139    /// Adds a single attachment to the note.
140    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
141        self.attachments.push(attachment.into());
142        self
143    }
144
145    /// Adds multiple attachments to the note.
146    pub fn attachments(
147        mut self,
148        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
149    ) -> Self {
150        self.attachments.extend(attachments.into_iter().map(Into::into));
151        self
152    }
153}
154
155impl<S: burn_note_builder::State> BurnNoteBuilder<S>
156where
157    S::SerialNumber: burn_note_builder::IsUnset,
158{
159    /// Draws a serial number from `rng` and sets it on the builder.
160    pub fn generate_serial_number(
161        self,
162        rng: &mut impl FeltRng,
163    ) -> BurnNoteBuilder<burn_note_builder::SetSerialNumber<S>> {
164        self.serial_number(rng.draw_word())
165    }
166}
167
168// CONVERSIONS
169// ================================================================================================
170
171impl From<BurnNote> for Note {
172    fn from(note: BurnNote) -> Self {
173        // BURN notes are always public for network execution and carry no storage. The tag routes
174        // the note to the asset's issuing faucet.
175        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
176            .with_tag(NoteTag::with_account_target(note.asset.faucet_id()));
177        let recipient =
178            NoteRecipient::new(note.serial_number, BurnNote::script(), NoteStorage::default());
179
180        let assets = NoteAssets::new(vec![note.asset])
181            .expect("a single asset never exceeds the note asset limit");
182        Note::with_attachments(assets, metadata, recipient, note.attachments)
183    }
184}
185
186// TESTS
187// ================================================================================================
188
189#[cfg(test)]
190mod tests {
191    use miden_protocol::account::AccountType;
192    use miden_protocol::asset::FungibleAsset;
193    use miden_protocol::crypto::rand::RandomCoin;
194
195    use super::*;
196
197    fn sender() -> AccountId {
198        AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
199    }
200
201    fn faucet() -> AccountId {
202        AccountId::builder().account_type(AccountType::Public).build_with_seed([2; 32])
203    }
204
205    /// The builder produces a public note, tagged for the faucet, carrying the asset to burn.
206    #[test]
207    fn builder_builds_public_burn_note() {
208        let mut rng = RandomCoin::new(Word::empty());
209        let asset = FungibleAsset::new(faucet(), 100).unwrap();
210
211        let burn_note = BurnNote::builder()
212            .sender(sender())
213            .asset(asset)
214            .generate_serial_number(&mut rng)
215            .build()
216            .unwrap();
217
218        assert_eq!(burn_note.sender(), sender());
219        assert_eq!(burn_note.faucet_id(), faucet());
220        assert_eq!(burn_note.asset(), asset.into());
221        assert_ne!(burn_note.serial_number(), Word::empty());
222
223        let note = Note::from(burn_note);
224        assert_eq!(note.metadata().note_type(), NoteType::Public);
225        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet()));
226        assert_eq!(note.assets().num_assets(), 1);
227    }
228}