miden_standards/note/
faucet_metadata_config.rs1use 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
29const FAUCET_METADATA_CONFIG_SCRIPT_PATH: &str =
34 "::miden::standards::notes::faucet_metadata_config::main";
35
36static 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
44const STRING_NUM_ELEMENTS: usize = 28;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum FaucetMetadataConfig {
70 SetMaxSupply { max_supply: AssetAmount },
73 SetDescription { description: Description },
75 SetLogoUri { logo_uri: LogoURI },
77 SetExternalLink { external_link: ExternalLink },
79}
80
81impl FaucetMetadataConfig {
82 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 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 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
128fn 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#[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 #[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 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 pub const MAX_NUM_STORAGE_ITEMS: usize = 4 + STRING_NUM_ELEMENTS;
232
233 pub fn script() -> NoteScript {
238 FAUCET_METADATA_CONFIG_SCRIPT.clone()
239 }
240
241 pub fn script_root() -> NoteScriptRoot {
243 FAUCET_METADATA_CONFIG_SCRIPT.root()
244 }
245
246 pub fn sender(&self) -> AccountId {
248 self.sender
249 }
250
251 pub fn target(&self) -> AccountId {
253 self.target
254 }
255
256 pub fn config(&self) -> &FaucetMetadataConfig {
258 &self.config
259 }
260
261 pub fn serial_number(&self) -> Word {
263 self.serial_number
264 }
265
266 pub fn attachments(&self) -> &NoteAttachments {
268 &self.attachments
269 }
270}
271
272impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S> {
276 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
278 self.attachments.push(attachment.into());
279 self
280 }
281
282 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 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
306impl From<FaucetMetadataConfigNote> for Note {
310 fn from(note: FaucetMetadataConfigNote) -> Self {
311 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#[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 #[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 #[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 #[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 #[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}