Skip to main content

miden_standards/note/config/
faucet_metadata_config.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::AssetAmount;
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, Word};
23
24use crate::StandardsLib;
25use crate::account::faucets::{Description, ExternalLink, LogoURI};
26use crate::note::costs::{FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27use crate::note::{NetworkAccountTarget, NumStorageItems};
28
29// NOTE SCRIPT
30// ================================================================================================
31
32/// Path to the FAUCET_METADATA_CONFIG note script procedure in the standards library.
33const FAUCET_METADATA_CONFIG_SCRIPT_PATH: &str =
34    "::miden::standards::notes::faucet_metadata_config::main";
35
36// Initialize the FAUCET_METADATA_CONFIG note script only once.
37static FAUCET_METADATA_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
38    let standards_lib = StandardsLib::default();
39    let path = Path::new(FAUCET_METADATA_CONFIG_SCRIPT_PATH);
40    NoteScript::from_package_reference(standards_lib.as_ref(), path)
41        .expect("Standards library contains FAUCET_METADATA_CONFIG note script procedure")
42});
43
44// FUNGIBLE FAUCET CONFIG
45// ================================================================================================
46
47/// Number of felts encoding a metadata string: 7 Words. Keep in sync with
48/// `faucet_metadata_config.masm`.
49const STRING_NUM_ELEMENTS: usize = 28;
50
51/// A token metadata management action that a [`FaucetMetadataConfigNote`] triggers on the faucet
52/// that consumes it.
53///
54/// The action, together with its arguments, is encoded into the note's storage (see [`NoteStorage`]
55/// conversion below) and is fixed at note creation, bound into the note commitment. The consuming
56/// faucet's metadata setters authorize the action through the account-wide
57/// [`Authority`](crate::account::access::Authority) component.
58///
59/// The three string actions apply to both faucet kinds, since
60/// [`FungibleFaucet`](crate::account::faucets::FungibleFaucet) and
61/// [`NonFungibleFaucet`](crate::account::faucets::NonFungibleFaucet) re-export the same setters
62/// from the shared `miden::standards::faucets` module. [`Self::SetMaxSupply`] is fungible-only, and
63/// aborts on a non-fungible faucet, which does not expose `set_max_supply`.
64///
65/// The three string actions carry their new value as the 28 felts the faucet stores it in. The note
66/// script commits to those felts and publishes them in the advice map, which is how the called
67/// setter receives them — nothing outside the note has to supply advice inputs.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum FaucetMetadataConfig {
70    /// Set the faucet's maximum supply. Fungible faucets only. Requires the max supply to be
71    /// configured as mutable, and the new cap to be at least the current token supply.
72    SetMaxSupply { max_supply: AssetAmount },
73    /// Set the token description. Requires the description to be configured as mutable.
74    SetDescription { description: Description },
75    /// Set the token logo URI. Requires the logo URI to be configured as mutable.
76    SetLogoUri { logo_uri: LogoURI },
77    /// Set the token external link. Requires the external link to be configured as mutable.
78    SetExternalLink { external_link: ExternalLink },
79}
80
81impl FaucetMetadataConfig {
82    // VARIANTS
83    // --------------------------------------------------------------------------------------------
84
85    // Config note variants stored in the first storage item. Keep in sync with
86    // `faucet_metadata_config.masm`.
87    const VARIANT_SET_MAX_SUPPLY: u8 = 0;
88    const VARIANT_SET_DESCRIPTION: u8 = 1;
89    const VARIANT_SET_LOGO_URI: u8 = 2;
90    const VARIANT_SET_EXTERNAL_LINK: u8 = 3;
91
92    /// Returns the variant encoding this action.
93    const fn variant(&self) -> u8 {
94        match self {
95            FaucetMetadataConfig::SetMaxSupply { .. } => Self::VARIANT_SET_MAX_SUPPLY,
96            FaucetMetadataConfig::SetDescription { .. } => Self::VARIANT_SET_DESCRIPTION,
97            FaucetMetadataConfig::SetLogoUri { .. } => Self::VARIANT_SET_LOGO_URI,
98            FaucetMetadataConfig::SetExternalLink { .. } => Self::VARIANT_SET_EXTERNAL_LINK,
99        }
100    }
101
102    /// Returns the note storage values encoding this action.
103    ///
104    /// `SetMaxSupply` lays out as `[variant, new_max_supply]`. The string actions lay out as
105    /// `[variant, 0, 0, 0, value(28)]`: the variant is padded out to a full word with three
106    /// zeros, so the payload starts word-aligned, as the note script's `poseidon2::hash_elements`
107    /// call requires.
108    fn to_storage_values(&self) -> Vec<Felt> {
109        let variant = Felt::from(self.variant());
110
111        match self {
112            FaucetMetadataConfig::SetMaxSupply { max_supply } => {
113                vec![variant, Felt::from(*max_supply)]
114            },
115            FaucetMetadataConfig::SetDescription { description } => {
116                string_storage_values(variant, &description.to_words())
117            },
118            FaucetMetadataConfig::SetLogoUri { logo_uri } => {
119                string_storage_values(variant, &logo_uri.to_words())
120            },
121            FaucetMetadataConfig::SetExternalLink { external_link } => {
122                string_storage_values(variant, &external_link.to_words())
123            },
124        }
125    }
126}
127
128/// Lays out a string action as `[variant, 0, 0, 0, value(28)]`.
129fn string_storage_values(variant: Felt, value: &[Word]) -> Vec<Felt> {
130    let mut items = Vec::with_capacity(FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
131    items.push(variant);
132    items.extend([Felt::ZERO; 3]);
133    items.extend(value.iter().flat_map(Word::as_elements).copied());
134
135    debug_assert_eq!(items.len(), 4 + STRING_NUM_ELEMENTS);
136
137    items
138}
139
140impl From<FaucetMetadataConfig> for NoteStorage {
141    fn from(config: FaucetMetadataConfig) -> Self {
142        NoteStorage::new(config.to_storage_values())
143            .expect("number of storage items should not exceed max storage items")
144    }
145}
146
147// FUNGIBLE FAUCET CONFIG NOTE
148// ================================================================================================
149
150/// A FaucetMetadataConfig note: triggers a token metadata admin action on the faucet that consumes
151/// it.
152///
153/// A single note script dispatches on the note variant in its storage to one of the faucet's
154/// metadata setters (`set_max_supply`, `set_description`, `set_logo_uri`, `set_external_link`).
155/// Authorization is enforced by those procedures through the account-wide
156/// [`Authority`](crate::account::access::Authority) component, so the note carries no assets.
157///
158/// See [`FaucetMetadataConfig`] for which actions apply to which faucet kind.
159///
160/// The note is always public and tagged for `target` — the faucet whose metadata is being managed.
161///
162/// The note is bound to `target` by a
163/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts
164/// that the consuming account matches that target before dispatching, so the note cannot be
165/// consumed by a third-party account that merely accepts its sender.
166///
167/// The note must be public: the script rejects a non-public note. See
168/// [the module docs](crate::note::config#note-type) for the layers that enforce it.
169///
170/// Construct one with the [builder](FaucetMetadataConfigNote::builder); convert it into a protocol
171/// [`Note`] infallibly via `Note::from`.
172#[derive(Debug, Clone)]
173pub struct FaucetMetadataConfigNote {
174    sender: AccountId,
175    target: AccountId,
176    config: FaucetMetadataConfig,
177    serial_number: Word,
178    attachments: NoteAttachments,
179}
180
181#[bon::bon]
182impl FaucetMetadataConfigNote {
183    /// Builds a new [`FaucetMetadataConfigNote`] that applies `config` to `target`.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if:
188    /// - `target` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
189    ///   which requires a public target).
190    /// - the attachments carry a `NetworkAccountTarget` for an account other than `target`.
191    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
192    ///   attachment occupies one of the available slots when the caller does not supply it.
193    #[builder]
194    pub fn new(
195        #[builder(field)] mut attachments: Vec<NoteAttachment>,
196        sender: AccountId,
197        target: AccountId,
198        config: FaucetMetadataConfig,
199        serial_number: Word,
200    ) -> Result<Self, NoteError> {
201        // The note script asserts that the consuming account matches this target before
202        // dispatching.
203        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
204            NoteError::other_with_source(
205                "failed to bind the FaucetMetadataConfig note to its target account",
206                err,
207            )
208        })?;
209
210        let attachments = NoteAttachments::new(attachments)?;
211
212        Ok(Self {
213            sender,
214            target,
215            config,
216            serial_number,
217            attachments,
218        })
219    }
220}
221
222impl FaucetMetadataConfigNote {
223    // CONSTANTS
224    // --------------------------------------------------------------------------------------------
225
226    /// Upper bound on the number of storage items of a FaucetMetadataConfig note.
227    ///
228    /// The layout is variable: `SetMaxSupply` uses 2 items (`[variant, new_max_supply]`), while
229    /// the three string actions use 32 (`[variant, 0, 0, 0, value(28)]`).
230    pub const MAX_NUM_STORAGE_ITEMS: usize = 4 + STRING_NUM_ELEMENTS;
231
232    /// The numbers of storage items the FaucetMetadataConfig note script accepts.
233    ///
234    /// `SetMaxSupply` uses 2 items, the three string actions use
235    /// [`Self::MAX_NUM_STORAGE_ITEMS`], and no size in between is valid. Keep in sync with the
236    /// `NUM_ITEMS_*` constants in `faucet_metadata_config.masm`.
237    pub const NUM_STORAGE_ITEMS: NumStorageItems = NumStorageItems::AnyOf(&[
238        NumStorageItems::Exact(2),
239        NumStorageItems::Exact(Self::MAX_NUM_STORAGE_ITEMS),
240    ]);
241
242    // PUBLIC ACCESSORS
243    // --------------------------------------------------------------------------------------------
244
245    /// Returns the script of the FaucetMetadataConfig note.
246    pub fn script() -> NoteScript {
247        FAUCET_METADATA_CONFIG_SCRIPT.clone()
248    }
249
250    /// Returns the FaucetMetadataConfig note script root.
251    pub fn script_root() -> NoteScriptRoot {
252        FAUCET_METADATA_CONFIG_SCRIPT.root()
253    }
254
255    /// Returns the account ID of the note's sender (the authorizing party under an owner- or
256    /// role-controlled `Authority`).
257    pub fn sender(&self) -> AccountId {
258        self.sender
259    }
260
261    /// Returns the account ID of the managed faucet (the account the note is tagged for).
262    pub fn target(&self) -> AccountId {
263        self.target
264    }
265
266    /// Returns the metadata action carried by the note.
267    pub fn config(&self) -> &FaucetMetadataConfig {
268        &self.config
269    }
270
271    /// Returns the note's serial number.
272    pub fn serial_number(&self) -> Word {
273        self.serial_number
274    }
275
276    /// Returns the attachments carried by the note.
277    pub fn attachments(&self) -> &NoteAttachments {
278        &self.attachments
279    }
280}
281
282// BUILDER EXTENSIONS
283// ================================================================================================
284
285impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S> {
286    /// Adds a single attachment to the note.
287    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
288        self.attachments.push(attachment.into());
289        self
290    }
291
292    /// Adds multiple attachments to the note.
293    pub fn attachments(
294        mut self,
295        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
296    ) -> Self {
297        self.attachments.extend(attachments.into_iter().map(Into::into));
298        self
299    }
300}
301
302impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S>
303where
304    S::SerialNumber: faucet_metadata_config_note_builder::IsUnset,
305{
306    /// Draws a serial number from `rng` and sets it on the builder.
307    pub fn generate_serial_number(
308        self,
309        rng: &mut impl FeltRng,
310    ) -> FaucetMetadataConfigNoteBuilder<faucet_metadata_config_note_builder::SetSerialNumber<S>>
311    {
312        self.serial_number(rng.draw_word())
313    }
314}
315
316// CONVERSIONS
317// ================================================================================================
318
319impl From<FaucetMetadataConfigNote> for Note {
320    fn from(note: FaucetMetadataConfigNote) -> Self {
321        // FaucetMetadataConfig notes carry no assets and are always public; the action and its
322        // arguments live in the note storage.
323        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
324            .with_tag(NoteTag::with_account_target(note.target));
325        let recipient = NoteRecipient::new(
326            note.serial_number,
327            FaucetMetadataConfigNote::script(),
328            NoteStorage::from(note.config),
329        );
330
331        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
332    }
333}
334
335impl NoteConsumptionCost for FaucetMetadataConfigNote {
336    fn consumption_cycles() -> u32 {
337        FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES
338    }
339}
340
341// TESTS
342// ================================================================================================
343
344#[cfg(test)]
345mod tests {
346    use miden_protocol::account::AccountType;
347    use miden_protocol::crypto::rand::RandomCoin;
348
349    use super::*;
350
351    fn account_id(seed: u8) -> AccountId {
352        AccountId::builder()
353            .account_type(AccountType::Public)
354            .build_with_seed([seed; 32])
355    }
356
357    fn description() -> Description {
358        Description::new("A described token").expect("description should be valid")
359    }
360
361    /// The builder produces a public, asset-less note tagged for the managed faucet.
362    #[test]
363    fn builder_builds_faucet_metadata_config_note() {
364        let mut rng = RandomCoin::new(Word::empty());
365        let faucet = account_id(1);
366        let owner = account_id(2);
367
368        let note = FaucetMetadataConfigNote::builder()
369            .sender(owner)
370            .target(faucet)
371            .config(FaucetMetadataConfig::SetDescription { description: description() })
372            .generate_serial_number(&mut rng)
373            .build()
374            .unwrap();
375
376        assert_eq!(note.sender(), owner);
377        assert_eq!(note.target(), faucet);
378
379        let note = Note::from(note);
380        assert_eq!(note.metadata().note_type(), NoteType::Public);
381        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
382        assert_eq!(note.assets().num_assets(), 0);
383    }
384
385    /// `SetMaxSupply` storage is `[variant, new_max_supply]`.
386    #[test]
387    fn set_max_supply_storage_layout() {
388        let max_supply = AssetAmount::new(1_000).unwrap();
389        let storage = NoteStorage::from(FaucetMetadataConfig::SetMaxSupply { max_supply });
390
391        assert_eq!(
392            storage.items(),
393            &[Felt::from(FaucetMetadataConfig::VARIANT_SET_MAX_SUPPLY), Felt::from(max_supply),]
394        );
395    }
396
397    /// A string action reserves the first storage word for the variant so the 7-Word payload that
398    /// follows starts word-aligned.
399    #[test]
400    fn set_description_storage_layout() {
401        let description = description();
402        let storage = NoteStorage::from(FaucetMetadataConfig::SetDescription {
403            description: description.clone(),
404        });
405
406        let items = storage.items();
407        assert_eq!(items.len(), FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
408        assert_eq!(items[0], Felt::from(FaucetMetadataConfig::VARIANT_SET_DESCRIPTION));
409        assert_eq!(&items[1..4], &[Felt::ZERO; 3]);
410
411        let payload: Vec<Felt> =
412            description.to_words().iter().flat_map(Word::as_elements).copied().collect();
413        assert_eq!(&items[4..], payload.as_slice());
414    }
415
416    /// Every string action carries the same layout, differing only in the variant.
417    #[test]
418    fn string_action_variants() {
419        let logo_uri = LogoURI::new("https://example.com/logo.png").unwrap();
420        let storage = NoteStorage::from(FaucetMetadataConfig::SetLogoUri { logo_uri });
421        assert_eq!(storage.items()[0], Felt::from(FaucetMetadataConfig::VARIANT_SET_LOGO_URI));
422
423        let external_link = ExternalLink::new("https://example.com").unwrap();
424        let storage = NoteStorage::from(FaucetMetadataConfig::SetExternalLink { external_link });
425        assert_eq!(storage.items()[0], Felt::from(FaucetMetadataConfig::VARIANT_SET_EXTERNAL_LINK));
426    }
427}