use alloc::vec::Vec;
use miden_protocol::account::component::{FeltSchema, StorageSlotSchema};
use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::account::faucets::TokenMetadataError;
use crate::utils::{FixedWidthString, FixedWidthStringError};
static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| {
[
StorageSlotName::new("miden::standards::faucets::token_name_0").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_name_1").expect("valid slot name"),
]
});
static MUTABILITY_CONFIG_SLOT: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::faucets::mutability_config")
.expect("storage slot name should be valid")
});
static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
[
StorageSlotName::new("miden::standards::faucets::token_description_0")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_1")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_2")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_3")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_4")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_5")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::token_description_6")
.expect("valid slot name"),
]
});
static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
[
StorageSlotName::new("miden::standards::faucets::logo_uri_0").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_1").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_2").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_3").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_4").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_5").expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::logo_uri_6").expect("valid slot name"),
]
});
static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| {
[
StorageSlotName::new("miden::standards::faucets::external_link_0")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_1")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_2")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_3")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_4")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_5")
.expect("valid slot name"),
StorageSlotName::new("miden::standards::faucets::external_link_6")
.expect("valid slot name"),
]
});
pub(crate) fn mutability_config_slot() -> &'static StorageSlotName {
&MUTABILITY_CONFIG_SLOT
}
pub(crate) const NAME_UTF8_MAX_BYTES: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenName(FixedWidthString<2>);
impl TokenName {
pub const MAX_BYTES: usize = NAME_UTF8_MAX_BYTES;
pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
if s.len() > Self::MAX_BYTES {
return Err(FixedWidthStringError::TooLong { max: Self::MAX_BYTES, actual: s.len() });
}
Ok(Self(FixedWidthString::new(s).expect("length already validated above")))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_words(&self) -> Vec<Word> {
self.0.to_words()
}
pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
let inner = FixedWidthString::<2>::try_from_words(words)?;
if inner.as_str().len() > Self::MAX_BYTES {
return Err(FixedWidthStringError::TooLong {
max: Self::MAX_BYTES,
actual: inner.as_str().len(),
});
}
Ok(Self(inner))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Description(FixedWidthString<7>);
impl Description {
pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::new(s).map(Self)
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_words(&self) -> Vec<Word> {
self.0.to_words()
}
pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::try_from_words(words).map(Self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoURI(FixedWidthString<7>);
impl LogoURI {
pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::new(s).map(Self)
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_words(&self) -> Vec<Word> {
self.0.to_words()
}
pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::try_from_words(words).map(Self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalLink(FixedWidthString<7>);
impl ExternalLink {
pub const MAX_BYTES: usize = FixedWidthString::<7>::CAPACITY;
pub fn new(s: &str) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::new(s).map(Self)
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_words(&self) -> Vec<Word> {
self.0.to_words()
}
pub fn try_from_words(words: &[Word]) -> Result<Self, FixedWidthStringError> {
FixedWidthString::<7>::try_from_words(words).map(Self)
}
}
#[derive(Debug, Clone)]
pub struct TokenMetadata {
name: TokenName,
description: Option<Description>,
logo_uri: Option<LogoURI>,
external_link: Option<ExternalLink>,
is_description_mutable: bool,
is_logo_uri_mutable: bool,
is_external_link_mutable: bool,
is_max_supply_mutable: bool,
}
impl TokenMetadata {
pub fn new(name: TokenName) -> Self {
Self {
name,
description: None,
logo_uri: None,
external_link: None,
is_description_mutable: false,
is_logo_uri_mutable: false,
is_external_link_mutable: false,
is_max_supply_mutable: false,
}
}
pub fn with_description(mut self, description: Description, mutable: bool) -> Self {
self.description = Some(description);
self.is_description_mutable = mutable;
self
}
pub fn with_description_mutable(mut self, mutable: bool) -> Self {
self.is_description_mutable = mutable;
self
}
pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self {
self.logo_uri = Some(logo_uri);
self.is_logo_uri_mutable = mutable;
self
}
pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self {
self.is_logo_uri_mutable = mutable;
self
}
pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self {
self.external_link = Some(external_link);
self.is_external_link_mutable = mutable;
self
}
pub fn with_external_link_mutable(mut self, mutable: bool) -> Self {
self.is_external_link_mutable = mutable;
self
}
pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self {
self.is_max_supply_mutable = mutable;
self
}
pub fn name(&self) -> &TokenName {
&self.name
}
pub fn description(&self) -> Option<&Description> {
self.description.as_ref()
}
pub fn logo_uri(&self) -> Option<&LogoURI> {
self.logo_uri.as_ref()
}
pub fn external_link(&self) -> Option<&ExternalLink> {
self.external_link.as_ref()
}
pub fn is_max_supply_mutable(&self) -> bool {
self.is_max_supply_mutable
}
pub fn name_chunk_0_slot() -> &'static StorageSlotName {
&NAME_SLOTS[0]
}
pub fn name_chunk_1_slot() -> &'static StorageSlotName {
&NAME_SLOTS[1]
}
pub fn mutability_config_slot() -> &'static StorageSlotName {
mutability_config_slot()
}
pub fn description_slot(index: usize) -> &'static StorageSlotName {
&DESCRIPTION_SLOTS[index]
}
pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName {
&LOGO_URI_SLOTS[index]
}
pub fn external_link_slot(index: usize) -> &'static StorageSlotName {
&EXTERNAL_LINK_SLOTS[index]
}
pub fn storage_schema() -> Vec<(StorageSlotName, StorageSlotSchema)> {
let mut entries: Vec<(StorageSlotName, StorageSlotSchema)> = Vec::new();
for (i, slot) in NAME_SLOTS.iter().enumerate() {
entries.push((
slot.clone(),
StorageSlotSchema::value(
alloc::format!("Name chunk {i}"),
core::array::from_fn(|j| FeltSchema::felt(alloc::format!("data_{j}"))),
),
));
}
entries.push((
MUTABILITY_CONFIG_SLOT.clone(),
StorageSlotSchema::value(
"Mutability config",
[
FeltSchema::bool("is_description_mutable"),
FeltSchema::bool("is_logo_uri_mutable"),
FeltSchema::bool("is_external_link_mutable"),
FeltSchema::bool("is_max_supply_mutable"),
],
),
));
for (label, slots) in [
("Description", DESCRIPTION_SLOTS.as_slice()),
("Logo URI", LOGO_URI_SLOTS.as_slice()),
("External link", EXTERNAL_LINK_SLOTS.as_slice()),
] {
for (i, slot) in slots.iter().enumerate() {
entries.push((
slot.clone(),
StorageSlotSchema::value(
alloc::format!("{label} chunk {i}"),
core::array::from_fn(|j| FeltSchema::felt(alloc::format!("data_{j}"))),
),
));
}
}
entries
}
fn felt_to_bool(felt: Felt, index: usize) -> Result<bool, TokenMetadataError> {
match felt.as_canonical_u64() {
0 => Ok(false),
1 => Ok(true),
value => Err(TokenMetadataError::InvalidMutabilityFlag { index, value }),
}
}
fn mutability_flags_from_word(
word: Word,
) -> Result<(bool, bool, bool, bool), TokenMetadataError> {
Ok((
Self::felt_to_bool(word[0], 0)?,
Self::felt_to_bool(word[1], 1)?,
Self::felt_to_bool(word[2], 2)?,
Self::felt_to_bool(word[3], 3)?,
))
}
fn mutability_config_word(&self) -> Word {
Word::from([
Felt::from(self.is_description_mutable as u32),
Felt::from(self.is_logo_uri_mutable as u32),
Felt::from(self.is_external_link_mutable as u32),
Felt::from(self.is_max_supply_mutable as u32),
])
}
pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, TokenMetadataError> {
let chunk_0 = storage.get_item(TokenMetadata::name_chunk_0_slot()).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: TokenMetadata::name_chunk_0_slot().clone(),
source: err,
}
})?;
let chunk_1 = storage.get_item(TokenMetadata::name_chunk_1_slot()).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: TokenMetadata::name_chunk_1_slot().clone(),
source: err,
}
})?;
let name_words: [Word; 2] = [chunk_0, chunk_1];
let name = TokenName::try_from_words(&name_words)
.map_err(|err| TokenMetadataError::InvalidStringField { field: "name", source: err })?;
let read_slots = |slots: &[StorageSlotName; 7]| -> Result<[Word; 7], TokenMetadataError> {
let mut field = [Word::default(); 7];
for (i, slot) in slots.iter().enumerate() {
field[i] = storage.get_item(slot).map_err(|err| {
TokenMetadataError::StorageLookupFailed { slot_name: slot.clone(), source: err }
})?;
}
Ok(field)
};
let description_words = read_slots(&DESCRIPTION_SLOTS)?;
let description = Description::try_from_words(&description_words).map_err(|err| {
TokenMetadataError::InvalidStringField { field: "description", source: err }
})?;
let description = if description.as_str().is_empty() {
None
} else {
Some(description)
};
let logo_words = read_slots(&LOGO_URI_SLOTS)?;
let logo_uri = LogoURI::try_from_words(&logo_words).map_err(|err| {
TokenMetadataError::InvalidStringField { field: "logo_uri", source: err }
})?;
let logo_uri = if logo_uri.as_str().is_empty() {
None
} else {
Some(logo_uri)
};
let link_words = read_slots(&EXTERNAL_LINK_SLOTS)?;
let external_link = ExternalLink::try_from_words(&link_words).map_err(|err| {
TokenMetadataError::InvalidStringField { field: "external_link", source: err }
})?;
let external_link = if external_link.as_str().is_empty() {
None
} else {
Some(external_link)
};
let mutability_word = storage.get_item(mutability_config_slot()).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: mutability_config_slot().clone(),
source: err,
}
})?;
let (is_desc_mutable, is_logo_mutable, is_extlink_mutable, is_max_supply_mutable) =
TokenMetadata::mutability_flags_from_word(mutability_word)?;
let mut meta = TokenMetadata::new(name);
if let Some(d) = description {
meta = meta.with_description(d, is_desc_mutable);
}
meta = meta.with_description_mutable(is_desc_mutable);
if let Some(l) = logo_uri {
meta = meta.with_logo_uri(l, is_logo_mutable);
}
meta = meta.with_logo_uri_mutable(is_logo_mutable);
if let Some(e) = external_link {
meta = meta.with_external_link(e, is_extlink_mutable);
}
meta = meta.with_external_link_mutable(is_extlink_mutable);
meta = meta.with_max_supply_mutable(is_max_supply_mutable);
Ok(meta)
}
pub fn into_storage_slots(self) -> Vec<StorageSlot> {
let mut slots: Vec<StorageSlot> = Vec::new();
let name_words = self.name.to_words();
slots.push(StorageSlot::with_value(
TokenMetadata::name_chunk_0_slot().clone(),
name_words[0],
));
slots.push(StorageSlot::with_value(
TokenMetadata::name_chunk_1_slot().clone(),
name_words[1],
));
slots.push(StorageSlot::with_value(
mutability_config_slot().clone(),
self.mutability_config_word(),
));
let description = self
.description
.unwrap_or_else(|| Description::new("").expect("empty description should be valid"));
for (i, word) in description.to_words().iter().enumerate() {
slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word));
}
let logo_uri = self
.logo_uri
.unwrap_or_else(|| LogoURI::new("").expect("empty logo URI should be valid"));
for (i, word) in logo_uri.to_words().iter().enumerate() {
slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word));
}
let external_link = self
.external_link
.unwrap_or_else(|| ExternalLink::new("").expect("empty external link should be valid"));
for (i, word) in external_link.to_words().iter().enumerate() {
slots
.push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word));
}
slots
}
}