miden_standards/account/faucets/
token_metadata.rs1use alloc::vec::Vec;
37
38use miden_protocol::account::component::{FeltSchema, StorageSlotSchema};
39use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName};
40use miden_protocol::utils::sync::LazyLock;
41use miden_protocol::{Felt, Word};
42
43use crate::account::faucets::TokenMetadataError;
44use crate::utils::{FixedWidthString, FixedWidthStringError};
45
46static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| {
51 [
52 StorageSlotName::new("miden::standards::faucets::token_name_0").expect("valid slot name"),
53 StorageSlotName::new("miden::standards::faucets::token_name_1").expect("valid slot name"),
54 ]
55});
56
57static MUTABILITY_CONFIG_SLOT: LazyLock<StorageSlotName> = LazyLock::new(|| {
60 StorageSlotName::new("miden::standards::faucets::mutability_config")
61 .expect("storage slot name should be valid")
62});
63
64static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
66 [
67 StorageSlotName::new("miden::standards::faucets::token_description_0")
68 .expect("valid slot name"),
69 StorageSlotName::new("miden::standards::faucets::token_description_1")
70 .expect("valid slot name"),
71 StorageSlotName::new("miden::standards::faucets::token_description_2")
72 .expect("valid slot name"),
73 StorageSlotName::new("miden::standards::faucets::token_description_3")
74 .expect("valid slot name"),
75 StorageSlotName::new("miden::standards::faucets::token_description_4")
76 .expect("valid slot name"),
77 StorageSlotName::new("miden::standards::faucets::token_description_5")
78 .expect("valid slot name"),
79 StorageSlotName::new("miden::standards::faucets::token_description_6")
80 .expect("valid slot name"),
81 ]
82});
83
84static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
86 [
87 StorageSlotName::new("miden::standards::faucets::logo_uri_0").expect("valid slot name"),
88 StorageSlotName::new("miden::standards::faucets::logo_uri_1").expect("valid slot name"),
89 StorageSlotName::new("miden::standards::faucets::logo_uri_2").expect("valid slot name"),
90 StorageSlotName::new("miden::standards::faucets::logo_uri_3").expect("valid slot name"),
91 StorageSlotName::new("miden::standards::faucets::logo_uri_4").expect("valid slot name"),
92 StorageSlotName::new("miden::standards::faucets::logo_uri_5").expect("valid slot name"),
93 StorageSlotName::new("miden::standards::faucets::logo_uri_6").expect("valid slot name"),
94 ]
95});
96
97static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
99 [
100 StorageSlotName::new("miden::standards::faucets::external_link_0")
101 .expect("valid slot name"),
102 StorageSlotName::new("miden::standards::faucets::external_link_1")
103 .expect("valid slot name"),
104 StorageSlotName::new("miden::standards::faucets::external_link_2")
105 .expect("valid slot name"),
106 StorageSlotName::new("miden::standards::faucets::external_link_3")
107 .expect("valid slot name"),
108 StorageSlotName::new("miden::standards::faucets::external_link_4")
109 .expect("valid slot name"),
110 StorageSlotName::new("miden::standards::faucets::external_link_5")
111 .expect("valid slot name"),
112 StorageSlotName::new("miden::standards::faucets::external_link_6")
113 .expect("valid slot name"),
114 ]
115});
116
117pub(crate) fn mutability_config_slot() -> &'static StorageSlotName {
119 &MUTABILITY_CONFIG_SLOT
120}
121
122pub(crate) const NAME_UTF8_MAX_BYTES: usize = 32;
124
125#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct TokenName(FixedWidthString<2>);
134
135impl TokenName {
136 pub const MAX_BYTES: usize = NAME_UTF8_MAX_BYTES;
138
139 pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
141 if s.len() > Self::MAX_BYTES {
142 return Err(FixedWidthStringError::TooLong { max: Self::MAX_BYTES, actual: s.len() });
143 }
144 Ok(Self(FixedWidthString::new(s).expect("length already validated above")))
145 }
146
147 pub fn as_str(&self) -> &str {
149 self.0.as_str()
150 }
151
152 pub fn to_words(&self) -> Vec<Word> {
154 self.0.to_words()
155 }
156
157 pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
159 let inner = FixedWidthString::<2>::try_from_words(words)?;
160 if inner.as_str().len() > Self::MAX_BYTES {
161 return Err(FixedWidthStringError::TooLong {
162 max: Self::MAX_BYTES,
163 actual: inner.as_str().len(),
164 });
165 }
166 Ok(Self(inner))
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct Description(FixedWidthString<7>);
176
177impl Description {
178 pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
180
181 pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
183 FixedWidthString::<7>::new(s).map(Self)
184 }
185
186 pub fn as_str(&self) -> &str {
188 self.0.as_str()
189 }
190
191 pub fn to_words(&self) -> Vec<Word> {
193 self.0.to_words()
194 }
195
196 pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
198 FixedWidthString::<7>::try_from_words(words).map(Self)
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct LogoURI(FixedWidthString<7>);
205
206impl LogoURI {
207 pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
209
210 pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
212 FixedWidthString::<7>::new(s).map(Self)
213 }
214
215 pub fn as_str(&self) -> &str {
217 self.0.as_str()
218 }
219
220 pub fn to_words(&self) -> Vec<Word> {
222 self.0.to_words()
223 }
224
225 pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
227 FixedWidthString::<7>::try_from_words(words).map(Self)
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ExternalLink(FixedWidthString<7>);
234
235impl ExternalLink {
236 pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
238
239 pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
241 FixedWidthString::<7>::new(s).map(Self)
242 }
243
244 pub fn as_str(&self) -> &str {
246 self.0.as_str()
247 }
248
249 pub fn to_words(&self) -> Vec<Word> {
251 self.0.to_words()
252 }
253
254 pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
256 FixedWidthString::<7>::try_from_words(words).map(Self)
257 }
258}
259
260#[derive(Debug, Clone)]
270pub struct TokenMetadata {
271 name: TokenName,
272 description: Option<Description>,
273 logo_uri: Option<LogoURI>,
274 external_link: Option<ExternalLink>,
275 is_description_mutable: bool,
276 is_logo_uri_mutable: bool,
277 is_external_link_mutable: bool,
278 is_max_supply_mutable: bool,
279}
280
281impl TokenMetadata {
282 pub fn new(name: TokenName) -> Self {
285 Self {
286 name,
287 description: None,
288 logo_uri: None,
289 external_link: None,
290 is_description_mutable: false,
291 is_logo_uri_mutable: false,
292 is_external_link_mutable: false,
293 is_max_supply_mutable: false,
294 }
295 }
296
297 pub fn with_description(mut self, description: Description, mutable: bool) -> Self {
302 self.description = Some(description);
303 self.is_description_mutable = mutable;
304 self
305 }
306
307 pub fn with_description_mutable(mut self, mutable: bool) -> Self {
309 self.is_description_mutable = mutable;
310 self
311 }
312
313 pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self {
315 self.logo_uri = Some(logo_uri);
316 self.is_logo_uri_mutable = mutable;
317 self
318 }
319
320 pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self {
322 self.is_logo_uri_mutable = mutable;
323 self
324 }
325
326 pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self {
328 self.external_link = Some(external_link);
329 self.is_external_link_mutable = mutable;
330 self
331 }
332
333 pub fn with_external_link_mutable(mut self, mutable: bool) -> Self {
335 self.is_external_link_mutable = mutable;
336 self
337 }
338
339 pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self {
341 self.is_max_supply_mutable = mutable;
342 self
343 }
344
345 pub fn name(&self) -> &TokenName {
350 &self.name
351 }
352
353 pub fn description(&self) -> Option<&Description> {
355 self.description.as_ref()
356 }
357
358 pub fn logo_uri(&self) -> Option<&LogoURI> {
360 self.logo_uri.as_ref()
361 }
362
363 pub fn external_link(&self) -> Option<&ExternalLink> {
365 self.external_link.as_ref()
366 }
367
368 pub fn is_max_supply_mutable(&self) -> bool {
370 self.is_max_supply_mutable
371 }
372
373 pub fn name_chunk_0_slot() -> &'static StorageSlotName {
378 &NAME_SLOTS[0]
379 }
380
381 pub fn name_chunk_1_slot() -> &'static StorageSlotName {
383 &NAME_SLOTS[1]
384 }
385
386 pub fn mutability_config_slot() -> &'static StorageSlotName {
388 mutability_config_slot()
389 }
390
391 pub fn description_slot(index: usize) -> &'static StorageSlotName {
393 &DESCRIPTION_SLOTS[index]
394 }
395
396 pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName {
398 &LOGO_URI_SLOTS[index]
399 }
400
401 pub fn external_link_slot(index: usize) -> &'static StorageSlotName {
403 &EXTERNAL_LINK_SLOTS[index]
404 }
405
406 pub fn storage_schema() -> Vec<(StorageSlotName, StorageSlotSchema)> {
411 let mut entries: Vec<(StorageSlotName, StorageSlotSchema)> = Vec::new();
412
413 for (i, slot) in NAME_SLOTS.iter().enumerate() {
414 entries.push((
415 slot.clone(),
416 StorageSlotSchema::value(
417 alloc::format!("Name chunk {i}"),
418 core::array::from_fn(|j| FeltSchema::felt(alloc::format!("data_{j}"))),
419 ),
420 ));
421 }
422
423 entries.push((
424 MUTABILITY_CONFIG_SLOT.clone(),
425 StorageSlotSchema::value(
426 "Mutability config",
427 [
428 FeltSchema::bool("is_description_mutable"),
429 FeltSchema::bool("is_logo_uri_mutable"),
430 FeltSchema::bool("is_external_link_mutable"),
431 FeltSchema::bool("is_max_supply_mutable"),
432 ],
433 ),
434 ));
435
436 for (label, slots) in [
437 ("Description", DESCRIPTION_SLOTS.as_slice()),
438 ("Logo URI", LOGO_URI_SLOTS.as_slice()),
439 ("External link", EXTERNAL_LINK_SLOTS.as_slice()),
440 ] {
441 for (i, slot) in slots.iter().enumerate() {
442 entries.push((
443 slot.clone(),
444 StorageSlotSchema::value(
445 alloc::format!("{label} chunk {i}"),
446 core::array::from_fn(|j| FeltSchema::felt(alloc::format!("data_{j}"))),
447 ),
448 ));
449 }
450 }
451
452 entries
453 }
454
455 fn felt_to_bool(felt: Felt, index: usize) -> Result<bool, TokenMetadataError> {
462 match felt.as_canonical_u64() {
463 0 => Ok(false),
464 1 => Ok(true),
465 value => Err(TokenMetadataError::InvalidMutabilityFlag { index, value }),
466 }
467 }
468
469 fn mutability_flags_from_word(
478 word: Word,
479 ) -> Result<(bool, bool, bool, bool), TokenMetadataError> {
480 Ok((
481 Self::felt_to_bool(word[0], 0)?,
482 Self::felt_to_bool(word[1], 1)?,
483 Self::felt_to_bool(word[2], 2)?,
484 Self::felt_to_bool(word[3], 3)?,
485 ))
486 }
487
488 fn mutability_config_word(&self) -> Word {
490 Word::from([
491 Felt::from(self.is_description_mutable as u32),
492 Felt::from(self.is_logo_uri_mutable as u32),
493 Felt::from(self.is_external_link_mutable as u32),
494 Felt::from(self.is_max_supply_mutable as u32),
495 ])
496 }
497
498 pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, TokenMetadataError> {
506 let chunk_0 = storage.get_item(TokenMetadata::name_chunk_0_slot()).map_err(|err| {
507 TokenMetadataError::StorageLookupFailed {
508 slot_name: TokenMetadata::name_chunk_0_slot().clone(),
509 source: err,
510 }
511 })?;
512 let chunk_1 = storage.get_item(TokenMetadata::name_chunk_1_slot()).map_err(|err| {
513 TokenMetadataError::StorageLookupFailed {
514 slot_name: TokenMetadata::name_chunk_1_slot().clone(),
515 source: err,
516 }
517 })?;
518 let name_words: [Word; 2] = [chunk_0, chunk_1];
519 let name = TokenName::try_from_words(&name_words)
520 .map_err(|err| TokenMetadataError::InvalidStringField { field: "name", source: err })?;
521
522 let read_slots = |slots: &[StorageSlotName; 7]| -> Result<[Word; 7], TokenMetadataError> {
523 let mut field = [Word::default(); 7];
524 for (i, slot) in slots.iter().enumerate() {
525 field[i] = storage.get_item(slot).map_err(|err| {
526 TokenMetadataError::StorageLookupFailed { slot_name: slot.clone(), source: err }
527 })?;
528 }
529 Ok(field)
530 };
531
532 let description_words = read_slots(&DESCRIPTION_SLOTS)?;
533 let description = Description::try_from_words(&description_words).map_err(|err| {
534 TokenMetadataError::InvalidStringField { field: "description", source: err }
535 })?;
536 let description = if description.as_str().is_empty() {
537 None
538 } else {
539 Some(description)
540 };
541
542 let logo_words = read_slots(&LOGO_URI_SLOTS)?;
543 let logo_uri = LogoURI::try_from_words(&logo_words).map_err(|err| {
544 TokenMetadataError::InvalidStringField { field: "logo_uri", source: err }
545 })?;
546 let logo_uri = if logo_uri.as_str().is_empty() {
547 None
548 } else {
549 Some(logo_uri)
550 };
551
552 let link_words = read_slots(&EXTERNAL_LINK_SLOTS)?;
553 let external_link = ExternalLink::try_from_words(&link_words).map_err(|err| {
554 TokenMetadataError::InvalidStringField { field: "external_link", source: err }
555 })?;
556 let external_link = if external_link.as_str().is_empty() {
557 None
558 } else {
559 Some(external_link)
560 };
561
562 let mutability_word = storage.get_item(mutability_config_slot()).map_err(|err| {
563 TokenMetadataError::StorageLookupFailed {
564 slot_name: mutability_config_slot().clone(),
565 source: err,
566 }
567 })?;
568 let (is_desc_mutable, is_logo_mutable, is_extlink_mutable, is_max_supply_mutable) =
569 TokenMetadata::mutability_flags_from_word(mutability_word)?;
570
571 let mut meta = TokenMetadata::new(name);
572 if let Some(d) = description {
573 meta = meta.with_description(d, is_desc_mutable);
574 }
575 meta = meta.with_description_mutable(is_desc_mutable);
576 if let Some(l) = logo_uri {
577 meta = meta.with_logo_uri(l, is_logo_mutable);
578 }
579 meta = meta.with_logo_uri_mutable(is_logo_mutable);
580 if let Some(e) = external_link {
581 meta = meta.with_external_link(e, is_extlink_mutable);
582 }
583 meta = meta.with_external_link_mutable(is_extlink_mutable);
584 meta = meta.with_max_supply_mutable(is_max_supply_mutable);
585
586 Ok(meta)
587 }
588
589 pub fn into_storage_slots(self) -> Vec<StorageSlot> {
592 let mut slots: Vec<StorageSlot> = Vec::new();
593
594 let name_words = self.name.to_words();
595 slots.push(StorageSlot::with_value(
596 TokenMetadata::name_chunk_0_slot().clone(),
597 name_words[0],
598 ));
599 slots.push(StorageSlot::with_value(
600 TokenMetadata::name_chunk_1_slot().clone(),
601 name_words[1],
602 ));
603
604 slots.push(StorageSlot::with_value(
605 mutability_config_slot().clone(),
606 self.mutability_config_word(),
607 ));
608
609 let description = self
610 .description
611 .unwrap_or_else(|| Description::new("").expect("empty description should be valid"));
612 for (i, word) in description.to_words().iter().enumerate() {
613 slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word));
614 }
615
616 let logo_uri = self
617 .logo_uri
618 .unwrap_or_else(|| LogoURI::new("").expect("empty logo URI should be valid"));
619 for (i, word) in logo_uri.to_words().iter().enumerate() {
620 slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word));
621 }
622
623 let external_link = self
624 .external_link
625 .unwrap_or_else(|| ExternalLink::new("").expect("empty external link should be valid"));
626 for (i, word) in external_link.to_words().iter().enumerate() {
627 slots
628 .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word));
629 }
630
631 slots
632 }
633}