Skip to main content

miden_standards/note/
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::NetworkAccountTarget;
27use crate::note::costs::{FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
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). Because the storage is fixed at note creation and bound into the note
56/// commitment, the authorized party is the note sender: the consuming faucet's metadata setters
57/// authorize the sender through the account-wide `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    // SELECTORS
83    // --------------------------------------------------------------------------------------------
84
85    // Config note selectors stored in the first storage item. Keep in sync with
86    // `faucet_metadata_config.masm`.
87    const SELECTOR_SET_MAX_SUPPLY: u8 = 0;
88    const SELECTOR_SET_DESCRIPTION: u8 = 1;
89    const SELECTOR_SET_LOGO_URI: u8 = 2;
90    const SELECTOR_SET_EXTERNAL_LINK: u8 = 3;
91
92    /// Returns the selector encoding this action in the first storage item.
93    const fn selector(&self) -> u8 {
94        match self {
95            FaucetMetadataConfig::SetMaxSupply { .. } => Self::SELECTOR_SET_MAX_SUPPLY,
96            FaucetMetadataConfig::SetDescription { .. } => Self::SELECTOR_SET_DESCRIPTION,
97            FaucetMetadataConfig::SetLogoUri { .. } => Self::SELECTOR_SET_LOGO_URI,
98            FaucetMetadataConfig::SetExternalLink { .. } => Self::SELECTOR_SET_EXTERNAL_LINK,
99        }
100    }
101
102    /// Returns the note storage values encoding this action.
103    ///
104    /// `SetMaxSupply` lays out as `[selector, new_max_supply]`. The string actions lay out as
105    /// `[selector, 0, 0, 0, value(28)]`: the selector 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 selector = Felt::from(self.selector());
110
111        match self {
112            FaucetMetadataConfig::SetMaxSupply { max_supply } => {
113                vec![selector, Felt::from(*max_supply)]
114            },
115            FaucetMetadataConfig::SetDescription { description } => {
116                string_storage_values(selector, &description.to_words())
117            },
118            FaucetMetadataConfig::SetLogoUri { logo_uri } => {
119                string_storage_values(selector, &logo_uri.to_words())
120            },
121            FaucetMetadataConfig::SetExternalLink { external_link } => {
122                string_storage_values(selector, &external_link.to_words())
123            },
124        }
125    }
126}
127
128/// Lays out a string action as `[selector, 0, 0, 0, value(28)]`.
129fn string_storage_values(selector: Felt, value: &[Word]) -> Vec<Felt> {
130    let mut items = Vec::with_capacity(FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
131    items.push(selector);
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 a selector in the note's 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 `Authority` component
156/// against the note sender, so the note carries no assets and its authorization is bound to
157/// `sender` at creation time.
158///
159/// See [`FaucetMetadataConfig`] for which actions apply to which faucet kind.
160///
161/// The note is always public and tagged for `target` — the faucet whose metadata is being managed.
162/// The `sender` is the account authorized for the action per the target's `Authority` configuration
163/// (the owner under `Authority::OwnerControlled`, or a role member under
164/// `Authority::RbacControlled`).
165///
166/// The note is bound to `target` by a
167/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts
168/// that the consuming account matches that target before dispatching, so the note cannot be
169/// consumed by a third-party account that merely accepts its sender.
170///
171/// Construct one with the [builder](FaucetMetadataConfigNote::builder); convert it into a protocol
172/// [`Note`] infallibly via `Note::from`.
173#[derive(Debug, Clone)]
174pub struct FaucetMetadataConfigNote {
175    sender: AccountId,
176    target: AccountId,
177    config: FaucetMetadataConfig,
178    serial_number: Word,
179    attachments: NoteAttachments,
180}
181
182#[bon::bon]
183impl FaucetMetadataConfigNote {
184    /// Builds a new [`FaucetMetadataConfigNote`] that applies `config` to `target`.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if:
189    /// - `target` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
190    ///   which requires a public target).
191    /// - the attachments carry a `NetworkAccountTarget` for an account other than `target`.
192    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
193    ///   attachment occupies one of the available slots when the caller does not supply it.
194    #[builder]
195    pub fn new(
196        #[builder(field)] mut attachments: Vec<NoteAttachment>,
197        sender: AccountId,
198        target: AccountId,
199        config: FaucetMetadataConfig,
200        serial_number: Word,
201    ) -> Result<Self, NoteError> {
202        // The note script asserts that the consuming account matches this target before
203        // dispatching.
204        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
205            NoteError::other_with_source(
206                "failed to bind the FaucetMetadataConfig note to its target account",
207                err,
208            )
209        })?;
210
211        let attachments = NoteAttachments::new(attachments)?;
212
213        Ok(Self {
214            sender,
215            target,
216            config,
217            serial_number,
218            attachments,
219        })
220    }
221}
222
223impl FaucetMetadataConfigNote {
224    // CONSTANTS
225    // --------------------------------------------------------------------------------------------
226
227    /// Upper bound on the number of storage items of a FaucetMetadataConfig note.
228    ///
229    /// The layout is variable: `SetMaxSupply` uses 2 items (`[selector, new_max_supply]`), while
230    /// the three string actions use 32 (`[selector, 0, 0, 0, value(28)]`).
231    pub const MAX_NUM_STORAGE_ITEMS: usize = 4 + STRING_NUM_ELEMENTS;
232
233    // PUBLIC ACCESSORS
234    // --------------------------------------------------------------------------------------------
235
236    /// Returns the script of the FaucetMetadataConfig note.
237    pub fn script() -> NoteScript {
238        FAUCET_METADATA_CONFIG_SCRIPT.clone()
239    }
240
241    /// Returns the FaucetMetadataConfig note script root.
242    pub fn script_root() -> NoteScriptRoot {
243        FAUCET_METADATA_CONFIG_SCRIPT.root()
244    }
245
246    /// Returns the account ID of the note's sender (the account authorized for the action).
247    pub fn sender(&self) -> AccountId {
248        self.sender
249    }
250
251    /// Returns the account ID of the managed faucet (the account the note is tagged for).
252    pub fn target(&self) -> AccountId {
253        self.target
254    }
255
256    /// Returns the metadata action carried by the note.
257    pub fn config(&self) -> &FaucetMetadataConfig {
258        &self.config
259    }
260
261    /// Returns the note's serial number.
262    pub fn serial_number(&self) -> Word {
263        self.serial_number
264    }
265
266    /// Returns the attachments carried by the note.
267    pub fn attachments(&self) -> &NoteAttachments {
268        &self.attachments
269    }
270}
271
272// BUILDER EXTENSIONS
273// ================================================================================================
274
275impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S> {
276    /// Adds a single attachment to the note.
277    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
278        self.attachments.push(attachment.into());
279        self
280    }
281
282    /// Adds multiple attachments to the note.
283    pub fn attachments(
284        mut self,
285        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
286    ) -> Self {
287        self.attachments.extend(attachments.into_iter().map(Into::into));
288        self
289    }
290}
291
292impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S>
293where
294    S::SerialNumber: faucet_metadata_config_note_builder::IsUnset,
295{
296    /// Draws a serial number from `rng` and sets it on the builder.
297    pub fn generate_serial_number(
298        self,
299        rng: &mut impl FeltRng,
300    ) -> FaucetMetadataConfigNoteBuilder<faucet_metadata_config_note_builder::SetSerialNumber<S>>
301    {
302        self.serial_number(rng.draw_word())
303    }
304}
305
306// CONVERSIONS
307// ================================================================================================
308
309impl From<FaucetMetadataConfigNote> for Note {
310    fn from(note: FaucetMetadataConfigNote) -> Self {
311        // FaucetMetadataConfig notes carry no assets and are always public; the action and its
312        // arguments live in the note storage.
313        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
314            .with_tag(NoteTag::with_account_target(note.target));
315        let recipient = NoteRecipient::new(
316            note.serial_number,
317            FaucetMetadataConfigNote::script(),
318            NoteStorage::from(note.config),
319        );
320
321        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
322    }
323}
324
325impl NoteConsumptionCost for FaucetMetadataConfigNote {
326    fn consumption_cycles() -> u32 {
327        FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES
328    }
329}
330
331// TESTS
332// ================================================================================================
333
334#[cfg(test)]
335mod tests {
336    use miden_protocol::account::AccountType;
337    use miden_protocol::crypto::rand::RandomCoin;
338
339    use super::*;
340
341    fn account_id(seed: u8) -> AccountId {
342        AccountId::builder()
343            .account_type(AccountType::Public)
344            .build_with_seed([seed; 32])
345    }
346
347    fn description() -> Description {
348        Description::new("A described token").expect("description should be valid")
349    }
350
351    /// The builder produces a public, asset-less note tagged for the managed faucet.
352    #[test]
353    fn builder_builds_faucet_metadata_config_note() {
354        let mut rng = RandomCoin::new(Word::empty());
355        let faucet = account_id(1);
356        let owner = account_id(2);
357
358        let note = FaucetMetadataConfigNote::builder()
359            .sender(owner)
360            .target(faucet)
361            .config(FaucetMetadataConfig::SetDescription { description: description() })
362            .generate_serial_number(&mut rng)
363            .build()
364            .unwrap();
365
366        assert_eq!(note.sender(), owner);
367        assert_eq!(note.target(), faucet);
368
369        let note = Note::from(note);
370        assert_eq!(note.metadata().note_type(), NoteType::Public);
371        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
372        assert_eq!(note.assets().num_assets(), 0);
373    }
374
375    /// `SetMaxSupply` storage is `[selector, new_max_supply]`.
376    #[test]
377    fn set_max_supply_storage_layout() {
378        let max_supply = AssetAmount::new(1_000).unwrap();
379        let storage = NoteStorage::from(FaucetMetadataConfig::SetMaxSupply { max_supply });
380
381        assert_eq!(
382            storage.items(),
383            &[
384                Felt::from(FaucetMetadataConfig::SELECTOR_SET_MAX_SUPPLY),
385                Felt::from(max_supply),
386            ]
387        );
388    }
389
390    /// A string action reserves the first storage word for the selector so the 7-Word payload that
391    /// follows starts word-aligned.
392    #[test]
393    fn set_description_storage_layout() {
394        let description = description();
395        let storage = NoteStorage::from(FaucetMetadataConfig::SetDescription {
396            description: description.clone(),
397        });
398
399        let items = storage.items();
400        assert_eq!(items.len(), FaucetMetadataConfigNote::MAX_NUM_STORAGE_ITEMS);
401        assert_eq!(items[0], Felt::from(FaucetMetadataConfig::SELECTOR_SET_DESCRIPTION));
402        assert_eq!(&items[1..4], &[Felt::ZERO; 3]);
403
404        let payload: Vec<Felt> =
405            description.to_words().iter().flat_map(Word::as_elements).copied().collect();
406        assert_eq!(&items[4..], payload.as_slice());
407    }
408
409    /// Every string action carries the same layout, differing only in the selector.
410    #[test]
411    fn string_action_selectors() {
412        let logo_uri = LogoURI::new("https://example.com/logo.png").unwrap();
413        let storage = NoteStorage::from(FaucetMetadataConfig::SetLogoUri { logo_uri });
414        assert_eq!(storage.items()[0], Felt::from(FaucetMetadataConfig::SELECTOR_SET_LOGO_URI));
415
416        let external_link = ExternalLink::new("https://example.com").unwrap();
417        let storage = NoteStorage::from(FaucetMetadataConfig::SetExternalLink { external_link });
418        assert_eq!(
419            storage.items()[0],
420            Felt::from(FaucetMetadataConfig::SELECTOR_SET_EXTERNAL_LINK)
421        );
422    }
423}