miden_standards/note/config/
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::costs::{FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27use crate::note::{NetworkAccountTarget, NumStorageItems};
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 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 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 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
128fn 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#[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 #[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 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 pub const MAX_NUM_STORAGE_ITEMS: usize = 4 + STRING_NUM_ELEMENTS;
231
232 pub const NUM_STORAGE_ITEMS: NumStorageItems = NumStorageItems::AnyOf(&[
238 NumStorageItems::Exact(2),
239 NumStorageItems::Exact(Self::MAX_NUM_STORAGE_ITEMS),
240 ]);
241
242 pub fn script() -> NoteScript {
247 FAUCET_METADATA_CONFIG_SCRIPT.clone()
248 }
249
250 pub fn script_root() -> NoteScriptRoot {
252 FAUCET_METADATA_CONFIG_SCRIPT.root()
253 }
254
255 pub fn sender(&self) -> AccountId {
258 self.sender
259 }
260
261 pub fn target(&self) -> AccountId {
263 self.target
264 }
265
266 pub fn config(&self) -> &FaucetMetadataConfig {
268 &self.config
269 }
270
271 pub fn serial_number(&self) -> Word {
273 self.serial_number
274 }
275
276 pub fn attachments(&self) -> &NoteAttachments {
278 &self.attachments
279 }
280}
281
282impl<S: faucet_metadata_config_note_builder::State> FaucetMetadataConfigNoteBuilder<S> {
286 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
288 self.attachments.push(attachment.into());
289 self
290 }
291
292 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 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
316impl From<FaucetMetadataConfigNote> for Note {
320 fn from(note: FaucetMetadataConfigNote) -> Self {
321 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#[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 #[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 #[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 #[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 #[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}