Skip to main content

miden_standards/account/faucets/
token_metadata.rs

1//! Generic token metadata helper.
2//!
3//! [`TokenMetadata`] is a builder-pattern struct used to manage the token name and optional
4//! fields (description, logo_uri, external_link) along with their mutability flags. It is meant
5//! to be embedded inside a token-bearing component such as
6//! [`FungibleFaucet`][crate::account::faucets::FungibleFaucet], not used as a
7//! standalone account component.
8//!
9//! Owner-gated mutators (`set_description`, `set_logo_uri`, `set_external_link`,
10//! `set_max_supply`) are exposed through the embedding component's MASM library and rely on
11//! the `Ownable2Step` access component for ownership checks.
12//!
13//! ## Storage layout (per-component, see embedding component for absolute slot order)
14//!
15//! | Slot name | Contents |
16//! |-----------|----------|
17//! | `faucets::token_name_0` | first 4 felts of name |
18//! | `faucets::token_name_1` | last 4 felts of name |
19//! | `faucets::mutability_config` | `[is_desc_mutable, is_logo_mutable, is_extlink_mutable, is_max_supply_mutable]` |
20//! | `faucets::token_description_0..=6` | description (7 Words, max 195 bytes) |
21//! | `faucets::logo_uri_0..=6` | logo URI (7 Words, max 195 bytes) |
22//! | `faucets::external_link_0..=6` | external link (7 Words, max 195 bytes) |
23//!
24//! Layout sync: the same layout is defined in MASM at `asm/standards/faucets/mod.masm`.
25//! Any change to slot names must be applied in both Rust and MASM.
26//!
27//! ## String encoding (UTF-8)
28//!
29//! All string fields use **7-bytes-per-felt, length-prefixed** encoding. The N felts are
30//! serialized into a flat buffer of N × 7 bytes; byte 0 is the string length, followed by
31//! UTF-8 content, zero-padded. Each 7-byte chunk is stored as a LE u64 with the high byte
32//! always zero, so it always fits in a Goldilocks field element.
33//!
34//! The name slots hold 2 Words (8 felts, capacity 55 bytes, capped at 32).
35
36use 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
46// SLOT NAMES — canonical layout (sync with asm/standards/faucets/fungible.masm)
47// ================================================================================================
48
49/// Token name (2 Words = 8 felts), split across 2 slots.
50static 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
57/// Mutability config slot: `[is_desc_mutable, is_logo_mutable, is_extlink_mutable,
58/// is_max_supply_mutable]`.
59static 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
64/// Description (7 Words), split across 7 slots.
65static 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
84/// Logo URI (7 Words), split across 7 slots.
85static 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
97/// External link (7 Words), split across 7 slots.
98static 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
117/// Returns the [`StorageSlotName`] for the mutability config Word.
118pub(crate) fn mutability_config_slot() -> &'static StorageSlotName {
119    &MUTABILITY_CONFIG_SLOT
120}
121
122/// Maximum length of a name in bytes when using the UTF-8 encoding (capped at 32).
123pub(crate) const NAME_UTF8_MAX_BYTES: usize = 32;
124
125// TOKEN NAME
126// ================================================================================================
127
128/// Token display name (max 32 bytes UTF-8), stored in 2 Words.
129///
130/// The maximum is intentionally capped at 32 bytes even though the 2-Word encoding could
131/// hold up to 55 bytes.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct TokenName(FixedWidthString<2>);
134
135impl TokenName {
136    /// Maximum byte length for a token name (capped at 32, below the 55-byte capacity).
137    pub const MAX_BYTES: usize = NAME_UTF8_MAX_BYTES;
138
139    /// Creates a token name from a UTF-8 string (at most 32 bytes).
140    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    /// Returns the name as a string slice.
148    pub fn as_str(&self) -> &str {
149        self.0.as_str()
150    }
151
152    /// Encodes the name into 2 Words for storage.
153    pub fn to_words(&self) -> Vec<Word> {
154        self.0.to_words()
155    }
156
157    /// Decodes a token name from a 2-Word slice.
158    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// FIELD TYPES
171// ================================================================================================
172
173/// Token description (max 195 bytes UTF-8), stored in 7 Words.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct Description(FixedWidthString<7>);
176
177impl Description {
178    /// Maximum byte length for a description (7 Words × 4 felts × 7 bytes − 1 length byte).
179    pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
180
181    /// Creates a description from a UTF-8 string.
182    pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
183        FixedWidthString::<7>::new(s).map(Self)
184    }
185
186    /// Returns the description as a string slice.
187    pub fn as_str(&self) -> &str {
188        self.0.as_str()
189    }
190
191    /// Encodes the description into 7 Words for storage.
192    pub fn to_words(&self) -> Vec<Word> {
193        self.0.to_words()
194    }
195
196    /// Decodes a description from a 7-Word slice.
197    pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
198        FixedWidthString::<7>::try_from_words(words).map(Self)
199    }
200}
201
202/// Token logo URI (max 195 bytes UTF-8), stored in 7 Words.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct LogoURI(FixedWidthString<7>);
205
206impl LogoURI {
207    /// Maximum byte length for a logo URI (7 Words × 4 felts × 7 bytes − 1 length byte).
208    pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
209
210    /// Creates a logo URI from a UTF-8 string.
211    pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
212        FixedWidthString::<7>::new(s).map(Self)
213    }
214
215    /// Returns the logo URI as a string slice.
216    pub fn as_str(&self) -> &str {
217        self.0.as_str()
218    }
219
220    /// Encodes the logo URI into 7 Words for storage.
221    pub fn to_words(&self) -> Vec<Word> {
222        self.0.to_words()
223    }
224
225    /// Decodes a logo URI from a 7-Word slice.
226    pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
227        FixedWidthString::<7>::try_from_words(words).map(Self)
228    }
229}
230
231/// Token external link (max 195 bytes UTF-8), stored in 7 Words.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ExternalLink(FixedWidthString<7>);
234
235impl ExternalLink {
236    /// Maximum byte length for an external link (7 Words × 4 felts × 7 bytes − 1 length byte).
237    pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
238
239    /// Creates an external link from a UTF-8 string.
240    pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
241        FixedWidthString::<7>::new(s).map(Self)
242    }
243
244    /// Returns the external link as a string slice.
245    pub fn as_str(&self) -> &str {
246        self.0.as_str()
247    }
248
249    /// Encodes the external link into 7 Words for storage.
250    pub fn to_words(&self) -> Vec<Word> {
251        self.0.to_words()
252    }
253
254    /// Decodes an external link from a 7-Word slice.
255    pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
256        FixedWidthString::<7>::try_from_words(words).map(Self)
257    }
258}
259
260// TOKEN METADATA
261// ================================================================================================
262
263/// A helper that stores name, mutability config, and optional fields in fixed value slots.
264///
265/// Designed to be embedded in
266/// [`FungibleFaucet`][crate::account::faucets::FungibleFaucet] (or other token-bearing
267/// account components) to avoid duplication. Slot names are referenced via
268/// [`TokenMetadata::name_chunk_0_slot`] and friends.
269#[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    /// Creates a new token metadata with the given name (all optional fields absent, all flags
283    /// false).
284    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    // BUILDERS
298    // --------------------------------------------------------------------------------------------
299
300    /// Sets the description and its mutability flag together.
301    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    /// Sets whether the description can be updated by the owner.
308    pub fn with_description_mutable(mut self, mutable: bool) -> Self {
309        self.is_description_mutable = mutable;
310        self
311    }
312
313    /// Sets the logo URI and its mutability flag together.
314    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    /// Sets whether the logo URI can be updated by the owner.
321    pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self {
322        self.is_logo_uri_mutable = mutable;
323        self
324    }
325
326    /// Sets the external link and its mutability flag together.
327    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    /// Sets whether the external link can be updated by the owner.
334    pub fn with_external_link_mutable(mut self, mutable: bool) -> Self {
335        self.is_external_link_mutable = mutable;
336        self
337    }
338
339    /// Sets whether the max supply can be updated by the owner.
340    pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self {
341        self.is_max_supply_mutable = mutable;
342        self
343    }
344
345    // ACCESSORS
346    // --------------------------------------------------------------------------------------------
347
348    /// Returns the token name.
349    pub fn name(&self) -> &TokenName {
350        &self.name
351    }
352
353    /// Returns the description if set.
354    pub fn description(&self) -> Option<&Description> {
355        self.description.as_ref()
356    }
357
358    /// Returns the logo URI if set.
359    pub fn logo_uri(&self) -> Option<&LogoURI> {
360        self.logo_uri.as_ref()
361    }
362
363    /// Returns the external link if set.
364    pub fn external_link(&self) -> Option<&ExternalLink> {
365        self.external_link.as_ref()
366    }
367
368    /// Returns whether the max supply is configured as mutable.
369    pub fn is_max_supply_mutable(&self) -> bool {
370        self.is_max_supply_mutable
371    }
372
373    // STATIC SLOT NAME ACCESSORS
374    // --------------------------------------------------------------------------------------------
375
376    /// Returns the [`StorageSlotName`] for name chunk 0.
377    pub fn name_chunk_0_slot() -> &'static StorageSlotName {
378        &NAME_SLOTS[0]
379    }
380
381    /// Returns the [`StorageSlotName`] for name chunk 1.
382    pub fn name_chunk_1_slot() -> &'static StorageSlotName {
383        &NAME_SLOTS[1]
384    }
385
386    /// Returns the [`StorageSlotName`] for the mutability config Word.
387    pub fn mutability_config_slot() -> &'static StorageSlotName {
388        mutability_config_slot()
389    }
390
391    /// Returns the [`StorageSlotName`] for a description chunk by index (0..=6).
392    pub fn description_slot(index: usize) -> &'static StorageSlotName {
393        &DESCRIPTION_SLOTS[index]
394    }
395
396    /// Returns the [`StorageSlotName`] for a logo URI chunk by index (0..=6).
397    pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName {
398        &LOGO_URI_SLOTS[index]
399    }
400
401    /// Returns the [`StorageSlotName`] for an external link chunk by index (0..=6).
402    pub fn external_link_slot(index: usize) -> &'static StorageSlotName {
403        &EXTERNAL_LINK_SLOTS[index]
404    }
405
406    /// Returns the storage slot schema entries describing the token metadata layout
407    /// (name chunks, mutability config, description, logo URI, external link).
408    ///
409    /// Embedding components should call this and extend their own schema with the result.
410    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    // STORAGE
456    // --------------------------------------------------------------------------------------------
457
458    /// Converts a single [`Felt`] at the given `index` in the mutability config word to a `bool`.
459    ///
460    /// Returns `Err` if the value is neither `0` nor `1`.
461    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    /// Decodes the mutability config [`Word`] into its four boolean flags.
470    ///
471    /// The word layout is `[is_desc_mutable, is_logo_mutable, is_extlink_mutable,
472    /// is_max_supply_mutable]`. Each element must be exactly `0` or `1`.
473    ///
474    /// # Errors
475    ///
476    /// Returns [`TokenMetadataError::InvalidMutabilityFlag`] if any element is not `0` or `1`.
477    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    /// Returns the mutability config word for this metadata.
489    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    /// Constructs a [`TokenMetadata`] by reading all relevant name, optional-field, and
499    /// mutability config slots from account storage.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`TokenMetadataError`] if any storage lookup fails, a mutability flag is invalid,
504    /// or a string field cannot be decoded.
505    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    /// Consumes `self` and returns the storage slots for this metadata (name, mutability config,
590    /// and all fields). Absent optional fields are encoded as empty strings (all-zero words).
591    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}