diff --git a/CHANGELOG.md b/CHANGELOG.md index 82bffbea64..57012da3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Made `NoteMetadataHeader` and `NoteMetadata::to_header()` public, added `NoteMetadata::from_header()` constructor, and exported `NoteMetadataHeader` from the `note` module ([#2561](https://github.com/0xMiden/protocol/pull/2561)). - Introduce NOTE_MAX_SIZE (32 KiB) and enforce it on individual output notes ([#2205](https://github.com/0xMiden/miden-base/pull/2205)) - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). +- Added `FungibleTokenMetadata` component with name, description, logo URI, and external link support, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) + - Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)). - Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)). - Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). @@ -45,6 +47,7 @@ - [BREAKING] The native hash function changed from RPO256 to Poseidon2 - see PR description ([#2508](https://github.com/0xMiden/miden-base/pull/2508)). - Introduced `StorageMapKey` and `StorageMapKeyHash` Word wrappers for type-safe storage map key handling ([#2431](https://github.com/0xMiden/miden-base/pull/2431)). - Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). +- `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. - Prefixed standard account component names with `miden::standards::components` ([#2400](https://github.com/0xMiden/miden-base/pull/2400)). - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 4151cc6086..fea82cd12e 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -233,12 +233,6 @@ proc get_raw_claim_amount mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 end -# Inputs: [U256[0], U256[1]] -# Outputs: [amount] -proc scale_down_amount - repeat.7 drop end -end - # Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] # Outputs: [] proc batch_pipe_double_words diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index b8870a89ef..720285efb7 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -24,7 +24,7 @@ use miden_protocol::asset::TokenSymbol; use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; +use miden_standards::account::faucets::{FungibleFaucetError, FungibleTokenMetadata, TokenName}; use miden_utils_sync::LazyLock; pub mod b2agg_note; @@ -313,14 +313,14 @@ static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| /// /// ## Storage Layout /// -/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::metadata_slot`]: Stores [`FungibleTokenMetadata`]. /// - [`Self::bridge_account_id_slot`]: Stores the AggLayer bridge account ID. /// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. /// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + /// origin network + scale. #[derive(Debug, Clone)] pub struct AggLayerFaucet { - metadata: TokenMetadata, + metadata: FungibleTokenMetadata, bridge_account_id: AccountId, origin_token_address: EthAddressFormat, origin_network: u32, @@ -332,7 +332,7 @@ impl AggLayerFaucet { /// /// # Errors /// Returns an error if: - /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. + /// - The decimals parameter exceeds maximum value of [`FungibleTokenMetadata::MAX_DECIMALS`]. /// - The max supply exceeds maximum possible amount for a fungible asset. /// - The token supply exceeds the max supply. pub fn new( @@ -345,7 +345,18 @@ impl AggLayerFaucet { origin_network: u32, scale: u8, ) -> Result { - let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; + // Use empty name for agglayer faucets (name is stored in Info component, not here). + let name = TokenName::default(); + let metadata = FungibleTokenMetadata::with_supply( + symbol, + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + )?; Ok(Self { metadata, bridge_account_id, @@ -364,9 +375,9 @@ impl AggLayerFaucet { Ok(self) } - /// Storage slot name for [`TokenMetadata`]. + /// Storage slot name for [`FungibleTokenMetadata`]. pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() + FungibleTokenMetadata::metadata_slot() } /// Storage slot name for the AggLayer bridge account ID. diff --git a/crates/miden-protocol/src/asset/token_symbol.rs b/crates/miden-protocol/src/asset/token_symbol.rs index 7189d6805b..34ed9d445b 100644 --- a/crates/miden-protocol/src/asset/token_symbol.rs +++ b/crates/miden-protocol/src/asset/token_symbol.rs @@ -144,6 +144,7 @@ impl TryFrom for TokenSymbol { if encoded_value < Self::MIN_ENCODED_VALUE { return Err(TokenSymbolError::ValueTooSmall(encoded_value)); } + if encoded_value > Self::MAX_ENCODED_VALUE { return Err(TokenSymbolError::ValueTooLarge(encoded_value)); } diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index 5d3b13a920..95e1a87433 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -1,6 +1,7 @@ # The MASM code of the Basic Fungible Faucet Account Component. # # See the `BasicFungibleFaucet` Rust type's documentation for more details. +# This component depends on `FungibleTokenMetadata` being present in the account. pub use ::miden::standards::faucets::basic_fungible::distribute pub use ::miden::standards::faucets::basic_fungible::burn diff --git a/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm new file mode 100644 index 0000000000..87d1aa532d --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm @@ -0,0 +1,20 @@ +# The MASM code of the Fungible Token Metadata Account Component. +# +# Re-exported from module miden::standards::components::fungible_token_metadata +# All metadata-related procedures are re-exported here. + +pub use ::miden::standards::metadata::fungible::get_name +pub use ::miden::standards::metadata::fungible::get_mutability_config +pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_mutable +pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable +pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_token_metadata +pub use ::miden::standards::metadata::fungible::get_max_supply +pub use ::miden::standards::metadata::fungible::get_decimals +pub use ::miden::standards::metadata::fungible::get_token_symbol +pub use ::miden::standards::metadata::fungible::get_token_supply +pub use ::miden::standards::metadata::fungible::set_description +pub use ::miden::standards::metadata::fungible::set_logo_uri +pub use ::miden::standards::metadata::fungible::set_external_link +pub use ::miden::standards::metadata::fungible::set_max_supply diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 604239c7fd..cbef972e9b 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -1,6 +1,7 @@ # The MASM code of the Network Fungible Faucet Account Component. # # See the `NetworkFungibleFaucet` Rust type's documentation for more details. +# This component depends on `FungibleTokenMetadata` being present in the account. pub use ::miden::standards::faucets::network_fungible::distribute pub use ::miden::standards::faucets::network_fungible::burn diff --git a/crates/miden-standards/asm/standards/access/ownable.masm b/crates/miden-standards/asm/standards/access/ownable.masm index b0591e71a5..1420c920b4 100644 --- a/crates/miden-standards/asm/standards/access/ownable.masm +++ b/crates/miden-standards/asm/standards/access/ownable.masm @@ -89,6 +89,10 @@ end #! Invocation: call pub proc get_owner exec.owner + # => [owner_suffix, owner_prefix, pad(16)] + + # truncate stack to 16 + movup.2 drop movup.2 drop # => [owner_suffix, owner_prefix, pad(14)] end diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index bee1948a2c..a6894c3f13 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -12,6 +12,7 @@ use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT const ASSET_PTR=0 const PRIVATE_NOTE=2 + # ERRORS # ================================================================================================= @@ -31,9 +32,9 @@ const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 no # The local memory address at which the metadata slot content is stored. const METADATA_SLOT_LOCAL=0 -# The standard slot where fungible faucet metadata like token symbol or decimals are stored. +# The standard slot where fungible faucet metadata is stored (canonical with metadata module). # Layout: [token_supply, max_supply, decimals, token_symbol] -const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") +const METADATA_SLOT=word("miden::standards::metadata::token_metadata") #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! @@ -68,8 +69,6 @@ pub proc distribute swap movup.2 drop movup.2 drop # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - # Assert that minting does not violate any supply constraints. - # # To make sure we cannot mint more than intended, we need to check: # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around # 2) amount + token_supply does not exceed max_supply diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm new file mode 100644 index 0000000000..5590c75cd5 --- /dev/null +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -0,0 +1,520 @@ +# miden::standards::metadata::fungible +# +# Metadata for fungible-style accounts: slots, getters (name, description, logo_uri, +# external_link, token_metadata), and owner-gated setters. +# Depends on ownable2step for ownership checks. + +use miden::protocol::active_account +use miden::protocol::native_account +use miden::standards::access::ownable2step + +# ================================================================================================= +# CONSTANTS — slot names +# ================================================================================================= + +const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") +const NAME_CHUNK_0_SLOT = word("miden::standards::metadata::name_0") +const NAME_CHUNK_1_SLOT = word("miden::standards::metadata::name_1") +const MUTABILITY_CONFIG_SLOT = word("miden::standards::metadata::mutability_config") + +const DESCRIPTION_0_SLOT = word("miden::standards::metadata::description_0") +const DESCRIPTION_1_SLOT = word("miden::standards::metadata::description_1") +const DESCRIPTION_2_SLOT = word("miden::standards::metadata::description_2") +const DESCRIPTION_3_SLOT = word("miden::standards::metadata::description_3") +const DESCRIPTION_4_SLOT = word("miden::standards::metadata::description_4") +const DESCRIPTION_5_SLOT = word("miden::standards::metadata::description_5") +const DESCRIPTION_6_SLOT = word("miden::standards::metadata::description_6") + +const LOGO_URI_0_SLOT = word("miden::standards::metadata::logo_uri_0") +const LOGO_URI_1_SLOT = word("miden::standards::metadata::logo_uri_1") +const LOGO_URI_2_SLOT = word("miden::standards::metadata::logo_uri_2") +const LOGO_URI_3_SLOT = word("miden::standards::metadata::logo_uri_3") +const LOGO_URI_4_SLOT = word("miden::standards::metadata::logo_uri_4") +const LOGO_URI_5_SLOT = word("miden::standards::metadata::logo_uri_5") +const LOGO_URI_6_SLOT = word("miden::standards::metadata::logo_uri_6") + +const EXTERNAL_LINK_0_SLOT = word("miden::standards::metadata::external_link_0") +const EXTERNAL_LINK_1_SLOT = word("miden::standards::metadata::external_link_1") +const EXTERNAL_LINK_2_SLOT = word("miden::standards::metadata::external_link_2") +const EXTERNAL_LINK_3_SLOT = word("miden::standards::metadata::external_link_3") +const EXTERNAL_LINK_4_SLOT = word("miden::standards::metadata::external_link_4") +const EXTERNAL_LINK_5_SLOT = word("miden::standards::metadata::external_link_5") +const EXTERNAL_LINK_6_SLOT = word("miden::standards::metadata::external_link_6") + +const CHUNK_1_LOC = 0 +const CHUNK_0_LOC = 4 +const FIELD_0_LOC = 0 +const FIELD_1_LOC = 4 +const FIELD_2_LOC = 8 +const FIELD_3_LOC = 12 +const FIELD_4_LOC = 16 +const FIELD_5_LOC = 20 +const FIELD_6_LOC = 24 + +const ERR_DESCRIPTION_NOT_MUTABLE = "description is not mutable" +const ERR_LOGO_URI_NOT_MUTABLE = "logo URI is not mutable" +const ERR_EXTERNAL_LINK_NOT_MUTABLE = "external link is not mutable" +const ERR_MAX_SUPPLY_IMMUTABLE = "max supply is immutable" +const ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY = "new max supply is less than current token supply" + +# ================================================================================================= +# PRIVATE HELPERS — single source of truth for slot access +# ================================================================================================= + +#! Loads token metadata word from storage. +#! Output: [token_supply, max_supply, decimals, token_symbol] (word[0] on top, little-endian). +proc get_token_metadata_word + push.TOKEN_METADATA_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads name chunk 0 (1 Word). Output: [NAME_CHUNK_0]. +proc get_name_chunk_0 + push.NAME_CHUNK_0_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads name chunk 1 (1 Word). Output: [NAME_CHUNK_1]. +proc get_name_chunk_1 + push.NAME_CHUNK_1_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads the mutability config word. +#! Output: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)]. +#! (word[0] on top after get_item, little-endian) +proc get_mutability_config_word + push.MUTABILITY_CONFIG_SLOT[0..2] + exec.active_account::get_item +end + +#! +#! Invocation: call +pub proc get_token_metadata + exec.get_token_metadata_word + swapw dropw + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] +end + +#! Returns the maximum supply. +#! Inputs: [pad(16)] Outputs: [max_supply, pad(15)] Invocation: call +pub proc get_max_supply + exec.get_token_metadata + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop movdn.2 drop drop + # => [max_supply, pad(15)] +end + +#! Returns decimals (single felt; e.g. 8). +#! Inputs: [pad(16)] Outputs: [decimals, pad(15)] Invocation: call +pub proc get_decimals + exec.get_token_metadata + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop drop swap drop + # => [decimals, pad(15)] +end + +#! Returns token_symbol (single felt). +#! Inputs: [pad(16)] Outputs: [token_symbol, pad(15)] Invocation: call +pub proc get_token_symbol + exec.get_token_metadata + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop drop drop + # => [token_symbol, pad(15)] +end + +#! Returns token_supply (single felt). +#! Inputs: [pad(16)] Outputs: [token_supply, pad(15)] Invocation: call +pub proc get_token_supply + exec.get_token_metadata + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + movdn.3 drop drop drop + # => [token_supply, pad(15)] +end + +# ================================================================================================= +# NAME (2 words) +# ================================================================================================= + +#! Returns the name (2 Words) padded to 16 elements: [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)]. +#! +#! Inputs: [pad(16)] +#! Outputs: [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] (16 felts; chunk_0 on top) +#! +#! Invocation: call +@locals(8) +pub proc get_name + exec.get_name_chunk_1 + loc_storew_le.CHUNK_1_LOC dropw + + exec.get_name_chunk_0 + loc_storew_le.CHUNK_0_LOC dropw + + loc_loadw_le.CHUNK_1_LOC + swapw + loc_loadw_le.CHUNK_0_LOC + # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] +end + +# ================================================================================================= +# MUTABILITY CONFIG — [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable] +# ================================================================================================= + +#! Returns the mutability config word. +#! +#! After get_item, stack has word[0] on top (little-endian): +#! [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] +#! +#! Inputs: [pad(16)] +#! Outputs: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] +#! +#! Invocation: call +pub proc get_mutability_config + exec.get_mutability_config_word + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] +end + +# ================================================================================================= +# MUTABILITY CHECKS — read mutability_config +# ================================================================================================= + +#! Returns whether max supply is mutable (single felt: 0 or 1). +#! +#! Reads mutability_config word felt[3] (max_supply_mutable). +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_max_supply_mutable + exec.get_mutability_config_word + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop drop drop + # => [max_supply_mutable, pad(15)] +end + +#! Returns whether description is mutable (flag == 1). +#! +#! Reads mutability_config word felt[0] (desc_mutable). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_description_mutable + exec.get_mutability_config_word + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + movdn.3 drop drop drop + # => [desc_mutable, pad(15)] + push.1 eq + # => [is_mutable, pad(15)] +end + +#! Returns whether logo URI is mutable (flag == 1). +#! +#! Reads mutability_config word felt[1] (logo_mutable). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_logo_uri_mutable + exec.get_mutability_config_word + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop movdn.2 drop drop + # => [logo_mutable, pad(15)] + push.1 eq + # => [is_mutable, pad(15)] +end + +#! Returns whether external link is mutable (flag == 1). +#! +#! Reads mutability_config word felt[2] (extlink_mutable). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_external_link_mutable + exec.get_mutability_config_word + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop drop swap drop + # => [extlink_mutable, pad(15)] + push.1 eq + # => [is_mutable, pad(15)] +end + +# ================================================================================================= +# SET DESCRIPTION (owner-only when desc_mutable == 1) +# ================================================================================================= + +#! Updates the description (7 Words) if the description mutability flag is 1 +#! and the note sender is the owner. +#! +#! The caller passes the hash of the new description on the stack and provides the +#! actual 7 Words in the advice map under that hash. The VM retrieves the data by +#! looking up the hash in the advice map. +#! +#! Inputs: +#! Stack: [DESCRIPTION_HASH, pad(12)] +#! Advice map: { +#! [DESCRIPTION_HASH] |-> [description_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] +#! +#! Panics if: +#! - the description mutability flag is not 1. +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +@locals(4) +pub proc set_description + # Save DESCRIPTION_HASH to locals, check mutability, verify owner, then restore hash + loc_storew_le.0 dropw + # => [pad(12)] + + # Read mutability config word + exec.get_mutability_config_word + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + movdn.3 drop drop drop + # => [desc_mutable, pad(15)] + push.1 eq + assert.err=ERR_DESCRIPTION_NOT_MUTABLE + # => [pad(16)] + + exec.ownable2step::assert_sender_is_owner + # => [pad(16)] + + # Restore hash and load 7 Words from advice map + loc_loadw_le.0 + # => [DESCRIPTION_HASH, pad(12)] + adv.push_mapval + dropw + # => [pad(16)] + + padw adv_loadw + push.DESCRIPTION_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_5_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_6_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# SET LOGO URI (owner-only when logo_mutable == 1) +# ================================================================================================= + +#! Updates the logo URI (7 Words) if the logo URI mutability flag is 1 +#! and the note sender is the owner. +#! +#! Inputs: +#! Stack: [LOGO_URI_HASH, pad(12)] +#! Advice map: { +#! [LOGO_URI_HASH] |-> [logo_uri_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] +#! +#! Panics if: +#! - the logo URI mutability flag is not 1. +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +@locals(4) +pub proc set_logo_uri + loc_storew_le.0 dropw + # => [pad(12)] + + exec.get_mutability_config_word + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop movdn.2 drop drop + # => [logo_mutable, pad(15)] + push.1 eq + assert.err=ERR_LOGO_URI_NOT_MUTABLE + # => [pad(16)] + + exec.ownable2step::assert_sender_is_owner + + loc_loadw_le.0 + adv.push_mapval + dropw + + padw adv_loadw + push.LOGO_URI_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_5_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_6_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# SET EXTERNAL LINK (owner-only when extlink_mutable == 1) +# ================================================================================================= + +#! Updates the external link (7 Words) if the external link mutability flag is 1 +#! and the note sender is the owner. +#! +#! Inputs: +#! Stack: [EXTERNAL_LINK_HASH, pad(12)] +#! Advice map: { +#! [EXTERNAL_LINK_HASH] |-> [external_link_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] +#! +#! Panics if: +#! - the external link mutability flag is not 1. +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +@locals(4) +pub proc set_external_link + loc_storew_le.0 dropw + # => [pad(12)] + + exec.get_mutability_config_word + swapw dropw + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop drop swap drop + # => [extlink_mutable, pad(15)] + push.1 eq + assert.err=ERR_EXTERNAL_LINK_NOT_MUTABLE + # => [pad(16)] + + exec.ownable2step::assert_sender_is_owner + + loc_loadw_le.0 + adv.push_mapval + dropw + + padw adv_loadw + push.EXTERNAL_LINK_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_5_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_6_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# SET MAX SUPPLY (owner-only when max_supply_mutable == 1) +# ================================================================================================= + +#! Updates the max supply if the max supply mutability flag is 1 +#! and the note sender is the owner. +#! +#! Inputs: [new_max_supply, pad(15)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the max supply mutability flag is not 1. +#! - the note sender is not the owner. +#! - the new max supply is less than the current token supply. +#! +#! Invocation: call (from note script context) +pub proc set_max_supply + # 1. Check max supply mutability + exec.get_mutability_config_word + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, new_max_supply, pad(15)] + # Note: unlike other optional_set_* procedures, we cannot use swapw dropw here because + # new_max_supply occupies position 4 and would be discarded. Instead, drop 3 elements + # directly; the assert then brings the stack back to depth 16. + drop drop drop + # => [max_supply_mutable, new_max_supply, pad(15)] + push.1 eq + assert.err=ERR_MAX_SUPPLY_IMMUTABLE + # => [new_max_supply, pad(15)] + + # 2. Verify note sender is the owner + exec.ownable2step::assert_sender_is_owner + # => [new_max_supply, pad(15)] + + # 3. Read current metadata word + exec.get_token_metadata_word + # => [token_supply, max_supply, decimals, token_symbol, new_max_supply, pad(15)] + + # 4. Validate: new_max_supply >= token_supply (i.e. token_supply <= new_max_supply) + dup # copy token_supply (index 0) to top + dup.5 # copy new_max_supply to top + # => [new_max_supply, token_supply, token_supply, max_supply, decimals, token_symbol, new_max_supply, ...] + lte # token_supply <= new_max_supply + assert.err=ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY + # => [token_supply, max_supply, decimals, token_symbol, new_max_supply, pad(15)] + + # 5. Replace max_supply (word[1]) with new_max_supply in the metadata word + movup.4 # bring new_max_supply to top + # => [new_max_supply, token_supply, max_supply, decimals, token_symbol, pad(15)] + swap # => [token_supply, new_max_supply, max_supply, decimals, token_symbol, pad(15)] + movup.2 # => [max_supply, token_supply, new_max_supply, decimals, token_symbol, pad(15)] + drop # discard old max_supply + # => [token_supply, new_max_supply, decimals, token_symbol, pad(15)] + + # 6. Write updated metadata word back to storage + push.TOKEN_METADATA_SLOT[0..2] + exec.native_account::set_item + # => [OLD_VALUE, pad(15)] + dropw + # => [pad(16)] +end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index b840ce7ac5..2805ba1d84 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -100,6 +100,20 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { // METADATA LIBRARIES // ================================================================================================ +// Initialize the Fungible Token Metadata library only once. +static FUNGIBLE_TOKEN_METADATA_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/fungible_token_metadata.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Fungible Token Metadata library is well-formed") +}); + +// Metadata Info component uses the standards library (get_name, set_description, etc. from +// metadata). +static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = + LazyLock::new(|| Library::from(crate::StandardsLib::default())); + // Initialize the Storage Schema library only once. static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( @@ -129,6 +143,19 @@ pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Fungible Token Metadata Library. +pub fn fungible_token_metadata_library() -> Library { + FUNGIBLE_TOKEN_METADATA_LIBRARY.clone() +} + +/// Returns the [`TokenMetadata`](crate::account::metadata::TokenMetadata) component library. +/// +/// Exposes `get_name`, `set_description`, `set_logo_uri`, `set_external_link`, and related +/// mutability-check procedures from `miden::standards::metadata::fungible`. +pub fn metadata_info_component_library() -> Library { + METADATA_INFO_COMPONENT_LIBRARY.clone() +} + /// Returns the Storage Schema Library. pub fn storage_schema_library() -> Library { STORAGE_SCHEMA_LIBRARY.clone() @@ -166,6 +193,7 @@ pub fn no_auth_library() -> Library { /// crate. pub enum StandardAccountComponent { BasicWallet, + FungibleTokenMetadata, BasicFungibleFaucet, NetworkFungibleFaucet, AuthSingleSig, @@ -180,6 +208,7 @@ impl StandardAccountComponent { pub fn procedure_digests(&self) -> impl Iterator { let library = match self { Self::BasicWallet => BASIC_WALLET_LIBRARY.as_ref(), + Self::FungibleTokenMetadata => FUNGIBLE_TOKEN_METADATA_LIBRARY.as_ref(), Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::AuthSingleSig => SINGLESIG_LIBRARY.as_ref(), @@ -223,6 +252,9 @@ impl StandardAccountComponent { Self::BasicWallet => { component_interface_vec.push(AccountComponentInterface::BasicWallet) }, + Self::FungibleTokenMetadata => { + component_interface_vec.push(AccountComponentInterface::FungibleTokenMetadata) + }, Self::BasicFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::BasicFungibleFaucet) }, @@ -255,6 +287,7 @@ impl StandardAccountComponent { component_interface_vec: &mut Vec, ) { Self::BasicWallet.extract_component(procedures_set, component_interface_vec); + Self::FungibleTokenMetadata.extract_component(procedures_set, component_interface_vec); Self::BasicFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSig.extract_component(procedures_set, component_interface_vec); diff --git a/crates/miden-standards/src/account/encoding/mod.rs b/crates/miden-standards/src/account/encoding/mod.rs new file mode 100644 index 0000000000..250d953f91 --- /dev/null +++ b/crates/miden-standards/src/account/encoding/mod.rs @@ -0,0 +1,275 @@ +//! Fixed-width UTF-8 string stored as N Words (7 bytes/felt, length-prefixed). +//! +//! [`FixedWidthString`] is the generic building block used by [`TokenName`], [`Description`], +//! [`LogoURI`], and [`ExternalLink`] to encode arbitrary UTF-8 strings into a fixed number of +//! storage words. +//! +//! ## Buffer layout (N × 4 × 7 bytes) +//! +//! ```text +//! Byte 0: string length (u8) +//! Bytes 1..1+len: UTF-8 content +//! Remaining: zero-padded +//! ``` +//! +//! Each 7-byte chunk is stored as a little-endian `u64` with the high byte always zero, so the +//! value is always < 2^56 and fits safely in a Goldilocks field element. +//! +//! [`TokenName`]: crate::account::faucets::TokenName +//! [`Description`]: crate::account::faucets::Description +//! [`LogoURI`]: crate::account::faucets::LogoURI +//! [`ExternalLink`]: crate::account::faucets::ExternalLink + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use miden_protocol::{Felt, Word}; + +// ENCODING CONSTANT +// ================================================================================================ + +/// Number of data bytes packed into each felt (7 bytes = 56 bits, always < Goldilocks prime). +pub(super) const BYTES_PER_FELT: usize = 7; + +// FIXED-WIDTH STRING +// ================================================================================================ + +/// A UTF-8 string stored in exactly `N` Words (N×4 felts, 7 bytes/felt, length-prefixed). +/// +/// The capacity (maximum storable bytes) is `N * 4 * 7 - 1`. Wrapper types such as +/// [`TokenName`](crate::account::faucets::TokenName) may impose a tighter limit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FixedWidthString(Box); + +impl Default for FixedWidthString { + fn default() -> Self { + Self("".into()) + } +} + +impl FixedWidthString { + /// Maximum bytes that can be stored (full capacity of the N words minus the length byte). + pub const CAPACITY: usize = N * 4 * BYTES_PER_FELT - 1; + + /// Creates a [`FixedWidthString`] from a UTF-8 string, validating it fits within capacity. + pub fn new(value: &str) -> Result { + if value.len() > Self::CAPACITY { + return Err(FixedWidthStringError::TooLong { + actual: value.len(), + max: Self::CAPACITY, + }); + } + Ok(Self(value.into())) + } + + /// Creates a [`FixedWidthString`] without checking the capacity limit. + /// + /// # Safety + /// The caller must ensure `value.len() <= Self::CAPACITY`. + pub(crate) fn from_str_unchecked(value: &str) -> Self { + Self(value.into()) + } + + /// Returns the string content. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Encodes the string into `N` Words (7 bytes/felt, length-prefixed, zero-padded). + pub fn to_words(&self) -> Vec { + let n_felts = N * 4; + let buf_len = n_felts * BYTES_PER_FELT; + let bytes = self.0.as_bytes(); + debug_assert!(bytes.len() < buf_len); + + let mut buf = alloc::vec![0u8; buf_len]; + buf[0] = bytes.len() as u8; + buf[1..1 + bytes.len()].copy_from_slice(bytes); + + (0..N) + .map(|i| { + let felts: [Felt; 4] = core::array::from_fn(|j| { + let start = (i * 4 + j) * BYTES_PER_FELT; + let mut le_bytes = [0u8; 8]; + le_bytes[..BYTES_PER_FELT].copy_from_slice(&buf[start..start + BYTES_PER_FELT]); + Felt::try_from(u64::from_le_bytes(le_bytes)) + .expect("7-byte LE value always fits in a Goldilocks felt") + }); + Word::from(felts) + }) + .collect() + } + + /// Decodes a [`FixedWidthString`] from a slice of exactly `N` Words. + pub fn try_from_words(words: &[Word]) -> Result { + if words.len() != N { + return Err(FixedWidthStringError::InvalidLength { expected: N, got: words.len() }); + } + let n_felts = N * 4; + let buf_len = n_felts * BYTES_PER_FELT; + let mut buf = alloc::vec![0u8; buf_len]; + + for (i, word) in words.iter().enumerate() { + for (j, felt) in word.as_slice().iter().enumerate() { + let v = felt.as_canonical_u64(); + let le = v.to_le_bytes(); + if le[BYTES_PER_FELT] != 0 { + return Err(FixedWidthStringError::InvalidUtf8); + } + let start = (i * 4 + j) * BYTES_PER_FELT; + buf[start..start + BYTES_PER_FELT].copy_from_slice(&le[..BYTES_PER_FELT]); + } + } + + let len = buf[0] as usize; + if len + 1 > buf_len { + return Err(FixedWidthStringError::InvalidUtf8); + } + String::from_utf8(buf[1..1 + len].to_vec()) + .map_err(|_| FixedWidthStringError::InvalidUtf8) + .map(|s| Self(s.into())) + } +} + +// ERROR TYPE +// ================================================================================================ + +/// Error type for [`FixedWidthString`] construction and decoding. +#[derive(Debug, Clone, thiserror::Error)] +pub enum FixedWidthStringError { + /// String exceeds the maximum capacity for this word width. + #[error("string must be at most {max} bytes, got {actual}")] + TooLong { actual: usize, max: usize }, + /// Decoded bytes are not valid UTF-8 (or a felt's high byte was non-zero). + #[error("string is not valid UTF-8")] + InvalidUtf8, + /// Slice length does not match the expected word count. + #[error("expected {expected} words, got {got}")] + InvalidLength { expected: usize, got: usize }, +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_string_roundtrip() { + let s: FixedWidthString<2> = FixedWidthString::new("").unwrap(); + let words = s.to_words(); + assert_eq!(words.len(), 2); + let decoded = FixedWidthString::<2>::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), ""); + } + + #[test] + fn ascii_roundtrip_2_words() { + let s = FixedWidthString::<2>::new("hello").unwrap(); + let decoded = FixedWidthString::<2>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), "hello"); + } + + #[test] + fn ascii_roundtrip_7_words() { + let text = "A longer description that spans many felts"; + let s = FixedWidthString::<7>::new(text).unwrap(); + let decoded = FixedWidthString::<7>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), text); + } + + #[test] + fn utf8_multibyte_roundtrip() { + // "café" — contains a 2-byte UTF-8 sequence + let s = FixedWidthString::<2>::new("café").unwrap(); + let decoded = FixedWidthString::<2>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), "café"); + } + + #[test] + fn exactly_at_capacity_accepted() { + let cap = FixedWidthString::<2>::CAPACITY; // 2*4*7 - 1 = 55 + let s = "a".repeat(cap); + assert!(FixedWidthString::<2>::new(&s).is_ok()); + } + + #[test] + fn one_over_capacity_rejected() { + let cap = FixedWidthString::<2>::CAPACITY; + let s = "a".repeat(cap + 1); + assert!(matches!( + FixedWidthString::<2>::new(&s), + Err(FixedWidthStringError::TooLong { .. }) + )); + } + + #[test] + fn capacity_7_words() { + // 7*4*7 - 1 = 195 + assert_eq!(FixedWidthString::<7>::CAPACITY, 195); + let s = "b".repeat(195); + let fw = FixedWidthString::<7>::new(&s).unwrap(); + let decoded = FixedWidthString::<7>::try_from_words(&fw.to_words()).unwrap(); + assert_eq!(decoded.as_str(), s); + } + + #[test] + fn to_words_returns_correct_count() { + let s = FixedWidthString::<7>::new("test").unwrap(); + assert_eq!(s.to_words().len(), 7); + } + + #[test] + fn wrong_word_count_returns_error() { + let s = FixedWidthString::<2>::new("hi").unwrap(); + let words = s.to_words(); + // pass only 1 word instead of 2 + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words[..1]), + Err(FixedWidthStringError::InvalidLength { expected: 2, got: 1 }) + )); + } + + #[test] + fn felt_with_high_byte_set_returns_invalid_utf8() { + // Construct a Word where one felt has its 8th byte non-zero, + // which violates the 7-bytes-per-felt invariant. + // A value with byte[7] != 0: 2^56 exceeds the Goldilocks prime so we need a + // different approach — set a byte in positions 0..7 that decodes to invalid UTF-8. + // The length byte will claim len=0xFF (255) which exceeds the buffer, triggering the error. + let overflow_len = Felt::try_from(0xff_u64).unwrap(); + let words = [ + Word::from([overflow_len, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + ]; + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words), + Err(FixedWidthStringError::InvalidUtf8) + )); + } + + #[test] + fn non_utf8_bytes_return_invalid_utf8() { + // Encode raw bytes that are not valid UTF-8 (e.g. 0xFF byte in content). + // Length byte = 1, content byte = 0xFF (invalid UTF-8 start byte). + // Pack into first felt: LE bytes [1, 0xFF, 0, 0, 0, 0, 0] → u64 = 0x0000_0000_00FF_01 + let raw: u64 = 0x0000_0000_00_ff_01; + let bad_felt = Felt::try_from(raw).unwrap(); + let words = [ + Word::from([bad_felt, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + ]; + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words), + Err(FixedWidthStringError::InvalidUtf8) + )); + } + + #[test] + fn default_is_empty_string() { + let s: FixedWidthString<2> = FixedWidthString::default(); + assert_eq!(s.as_str(), ""); + } +} diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 169d45eb69..6c77bf5908 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -1,29 +1,17 @@ -use miden_protocol::account::component::{ - AccountComponentMetadata, - FeltSchema, - SchemaType, - StorageSchema, - StorageSlotSchema, -}; +use miden_protocol::Word; +use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, - AccountStorage, AccountStorageMode, AccountType, - StorageSlotName, }; -use miden_protocol::asset::TokenSymbol; -use miden_protocol::{Felt, Word}; -use super::{FungibleFaucetError, TokenMetadata}; +use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::AuthMethod; use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; use crate::account::components::basic_fungible_faucet_library; - -/// The schema type for token symbols. -const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; @@ -62,14 +50,11 @@ procedure_digest!( /// /// This component supports accounts of type [`AccountType::FungibleFaucet`]. /// -/// ## Storage Layout -/// -/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// This component depends on [`FungibleTokenMetadata`] being present in the account for storage +/// of token metadata. It has no storage slots of its own. /// /// [builder]: crate::code_builder::CodeBuilder -pub struct BasicFungibleFaucet { - metadata: TokenMetadata, -} +pub struct BasicFungibleFaucet; impl BasicFungibleFaucet { // CONSTANTS @@ -78,123 +63,12 @@ impl BasicFungibleFaucet { /// The name of the component. pub const NAME: &'static str = "miden::standards::components::faucets::basic_fungible_faucet"; - /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; const BURN_PROC_NAME: &str = "burn"; - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata and with - /// an initial token supply of zero. - /// - /// # Errors - /// - /// Returns an error if: - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply parameter exceeds maximum possible amount for a fungible asset - /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - ) -> Result { - let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - Ok(Self { metadata }) - } - - /// Creates a new [`BasicFungibleFaucet`] component from the given [`TokenMetadata`]. - /// - /// This is a convenience constructor that allows creating a faucet from pre-validated - /// metadata. - pub fn from_metadata(metadata: TokenMetadata) -> Self { - Self { metadata } - } - - /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account - /// interface and storage. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided [`AccountInterface`] does not contain a - /// [`AccountComponentInterface::BasicFungibleFaucet`] component. - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. - /// - the token supply exceeds the max supply. - /// - the token symbol encoded value exceeds the maximum value of - /// [`TokenSymbol::MAX_ENCODED_VALUE`]. - fn try_from_interface( - interface: AccountInterface, - storage: &AccountStorage, - ) -> Result { - // Check that the procedures of the basic fungible faucet exist in the account. - if !interface.components().contains(&AccountComponentInterface::BasicFungibleFaucet) { - return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface); - } - - let metadata = TokenMetadata::try_from(storage)?; - Ok(Self { metadata }) - } - // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored. - pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() - } - - /// Returns the storage slot schema for the metadata slot. - pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); - ( - Self::metadata_slot().clone(), - StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ), - ) - } - - /// Returns the token metadata. - pub fn metadata(&self) -> &TokenMetadata { - &self.metadata - } - - /// Returns the symbol of the faucet. - pub fn symbol(&self) -> &TokenSymbol { - self.metadata.symbol() - } - - /// Returns the decimals of the faucet. - pub fn decimals(&self) -> u8 { - self.metadata.decimals() - } - - /// Returns the max supply (in base units) of the faucet. - /// - /// This is the highest amount of tokens that can be minted from this faucet. - pub fn max_supply(&self) -> Felt { - self.metadata.max_supply() - } - - /// Returns the token supply (in base units) of the faucet. - /// - /// This is the amount of tokens that were minted from the faucet so far. Its value can never - /// exceed [`Self::max_supply`]. - pub fn token_supply(&self) -> Felt { - self.metadata.token_supply() - } - /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE @@ -207,35 +81,28 @@ impl BasicFungibleFaucet { /// Returns the [`AccountComponentMetadata`] for this component. pub fn component_metadata() -> AccountComponentMetadata { - let storage_schema = StorageSchema::new([Self::metadata_slot_schema()]) - .expect("storage schema should be valid"); - AccountComponentMetadata::new(Self::NAME, [AccountType::FungibleFaucet]) .with_description("Basic fungible faucet component for minting and burning tokens") - .with_storage_schema(storage_schema) } - // MUTATORS - // -------------------------------------------------------------------------------------------- + /// Checks that the account contains the basic fungible faucet interface. + fn try_from_interface( + interface: AccountInterface, + _storage: &miden_protocol::account::AccountStorage, + ) -> Result { + if !interface.components().contains(&AccountComponentInterface::BasicFungibleFaucet) { + return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface); + } - /// Sets the token_supply (in base units) of the basic fungible faucet. - /// - /// # Errors - /// - /// Returns an error if: - /// - the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - self.metadata = self.metadata.with_token_supply(token_supply)?; - Ok(self) + Ok(BasicFungibleFaucet) } } impl From for AccountComponent { - fn from(faucet: BasicFungibleFaucet) -> Self { - let storage_slot = faucet.metadata.into(); + fn from(_faucet: BasicFungibleFaucet) -> Self { let metadata = BasicFungibleFaucet::component_metadata(); - AccountComponent::new(basic_fungible_faucet_library(), vec![storage_slot], metadata) + AccountComponent::new(basic_fungible_faucet_library(), vec![], metadata) .expect("basic fungible faucet component should satisfy the requirements of a valid account component") } } @@ -261,8 +128,7 @@ impl TryFrom<&Account> for BasicFungibleFaucet { } /// Creates a new faucet account with basic fungible faucet interface, -/// account storage type, specified authentication scheme, and provided meta data (token symbol, -/// decimals, max supply). +/// account storage type, specified authentication scheme, and provided metadata. /// /// The basic faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -274,13 +140,12 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// /// The storage layout of the faucet account is defined by the combination of the following /// components (see their docs for details): -/// - [`BasicFungibleFaucet`] +/// - [`FungibleTokenMetadata`] (token metadata, name, description, etc.) +/// - [`BasicFungibleFaucet`] (distribute and burn procedures) /// - [`AuthSingleSigAcl`] pub fn create_basic_fungible_faucet( init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, + metadata: FungibleTokenMetadata, account_storage_mode: AccountStorageMode, auth_method: AuthMethod, ) -> Result { @@ -318,7 +183,8 @@ pub fn create_basic_fungible_faucet( .account_type(AccountType::FungibleFaucet) .storage_mode(account_storage_mode) .with_auth_component(auth_component) - .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -331,8 +197,8 @@ pub fn create_basic_fungible_faucet( #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_protocol::Word; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; + use miden_protocol::{Felt, Word}; use super::{ AccountBuilder, @@ -340,12 +206,13 @@ mod tests { AccountType, AuthMethod, BasicFungibleFaucet, - Felt, FungibleFaucetError, - TokenSymbol, + FungibleTokenMetadata, create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; + use crate::account::faucets::{Description, TokenName}; + use crate::account::metadata::TokenMetadata; use crate::account::wallets::BasicWallet; #[test] @@ -363,20 +230,27 @@ mod tests { let max_supply = Felt::new(123); let token_symbol_string = "POL"; - let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); + let token_symbol = + miden_protocol::asset::TokenSymbol::try_from(token_symbol_string).unwrap(); + let token_name_string = "polygon"; + let description_string = "A polygon token"; let decimals = 2u8; let storage_mode = AccountStorageMode::Private; - let token_symbol_felt = token_symbol.as_element(); - let faucet_account = create_basic_fungible_faucet( - init_seed, + let token_name = TokenName::new(token_name_string).unwrap(); + let description = Description::new(description_string).unwrap(); + let metadata = FungibleTokenMetadata::new( token_symbol.clone(), decimals, max_supply, - storage_mode, - auth_method, + token_name, + Some(description), + None, + None, ) .unwrap(); + let faucet_account = + create_basic_fungible_faucet(init_seed, metadata, storage_mode, auth_method).unwrap(); // The falcon auth component's public key should be present. assert_eq!( @@ -410,20 +284,31 @@ mod tests { // Check that faucet metadata was initialized to the given values. // Storage layout: [token_supply, max_supply, decimals, symbol] assert_eq!( - faucet_account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(), - [Felt::ZERO, Felt::new(123), Felt::new(2), token_symbol_felt].into() + faucet_account + .storage() + .get_item(FungibleTokenMetadata::metadata_slot()) + .unwrap(), + [Felt::ZERO, Felt::new(123), Felt::new(2), token_symbol.into()].into() ); + // Check that name was stored + let name_0 = faucet_account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = faucet_account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); + let decoded_name = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded_name.as_str(), token_name_string); + let expected_desc_words = Description::new(description_string).unwrap().to_words(); + for (i, expected) in expected_desc_words.iter().enumerate() { + let chunk = + faucet_account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + assert!(faucet_account.is_faucet()); assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); - // Verify the faucet can be extracted and has correct metadata - let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap(); - assert_eq!(faucet_component.symbol(), &token_symbol); - assert_eq!(faucet_component.decimals(), decimals); - assert_eq!(faucet_component.max_supply(), max_supply); - assert_eq!(faucet_component.token_supply(), Felt::ZERO); + // Verify the faucet component can be extracted + let _faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap(); } #[test] @@ -434,13 +319,23 @@ mod tests { let mock_seed = mock_word.as_bytes(); // valid account - let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol"); + let token_symbol = + miden_protocol::asset::TokenSymbol::new("POL").expect("invalid token symbol"); + let metadata = FungibleTokenMetadata::new( + token_symbol, + 10, + Felt::new(100), + TokenName::new("POL").unwrap(), + None, + None, + None, + ) + .expect("failed to create token metadata"); + let faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) - .with_component( - BasicFungibleFaucet::new(token_symbol.clone(), 10, Felt::new(100)) - .expect("failed to create a fungible faucet component"), - ) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .with_auth_component(AuthSingleSig::new( mock_public_key, AuthScheme::Falcon512Poseidon2, @@ -448,17 +343,16 @@ mod tests { .build_existing() .expect("failed to create wallet account"); - let basic_ff = BasicFungibleFaucet::try_from(faucet_account) + let _basic_ff = BasicFungibleFaucet::try_from(faucet_account) .expect("basic fungible faucet creation failed"); - assert_eq!(basic_ff.symbol(), &token_symbol); - assert_eq!(basic_ff.decimals(), 10); - assert_eq!(basic_ff.max_supply(), Felt::new(100)); - assert_eq!(basic_ff.token_supply(), Felt::ZERO); // invalid account: basic fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) - .with_auth_component(AuthSingleSig::new(mock_public_key, AuthScheme::Falcon512Poseidon2)) + .with_auth_component(AuthSingleSig::new( + mock_public_key, + AuthScheme::Falcon512Poseidon2, + )) // we need to add some other component so the builder doesn't fail .with_component(BasicWallet) .build_existing() diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index db654c10fe..abea6bc485 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -12,7 +12,9 @@ mod token_metadata; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; -pub use token_metadata::TokenMetadata; +pub use token_metadata::{Description, ExternalLink, FungibleTokenMetadata, LogoURI, TokenName}; + +pub use crate::account::encoding::{FixedWidthString, FixedWidthStringError}; // FUNGIBLE FAUCET ERROR // ================================================================================================ @@ -54,4 +56,6 @@ pub enum FungibleFaucetError { NotAFungibleFaucetAccount, #[error("failed to read ownership data from storage")] OwnershipError(#[source] Ownable2StepError), + #[error("network faucet ownership has been renounced (no owner)")] + OwnershipRenounced, } diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 74c1e5298a..47d7897a8f 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -1,32 +1,20 @@ -use miden_protocol::account::component::{ - AccountComponentMetadata, - FeltSchema, - SchemaType, - StorageSchema, - StorageSlotSchema, -}; +use miden_protocol::Word; +use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, - AccountStorage, AccountStorageMode, AccountType, - StorageSlotName, }; -use miden_protocol::asset::TokenSymbol; -use miden_protocol::{Felt, Word}; -use super::{FungibleFaucetError, TokenMetadata}; +use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; -/// The schema type for token symbols. -const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; - // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -63,14 +51,11 @@ procedure_digest!( /// `distribute`. When building an account with this component, /// [`crate::account::access::Ownable2Step`] must also be included. /// -/// ## Storage Layout -/// -/// - [`Self::metadata_slot`]: Fungible faucet metadata. +/// This component depends on [`FungibleTokenMetadata`] being present in the account for storage +/// of token metadata. It has no storage slots of its own. /// /// [builder]: crate::code_builder::CodeBuilder -pub struct NetworkFungibleFaucet { - metadata: TokenMetadata, -} +pub struct NetworkFungibleFaucet; impl NetworkFungibleFaucet { // CONSTANTS @@ -79,57 +64,27 @@ impl NetworkFungibleFaucet { /// The name of the component. pub const NAME: &'static str = "miden::standards::components::faucets::network_fungible_faucet"; - /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; const BURN_PROC_NAME: &str = "burn"; - // CONSTRUCTORS + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata. - /// - /// # Errors: - /// Returns an error if: - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply parameter exceeds maximum possible amount for a fungible asset - /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - ) -> Result { - let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - Ok(Self { metadata }) + /// Returns the digest of the `distribute` account procedure. + pub fn distribute_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE } - /// Creates a new [`NetworkFungibleFaucet`] component from the given [`TokenMetadata`]. - /// - /// This is a convenience constructor that allows creating a faucet from pre-validated - /// metadata. - pub fn from_metadata(metadata: TokenMetadata) -> Self { - Self { metadata } + /// Returns the digest of the `burn` account procedure. + pub fn burn_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_BURN } - /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account - /// interface and storage. - /// - /// # Errors: - /// Returns an error if: - /// - the provided [`AccountInterface`] does not contain a - /// [`AccountComponentInterface::NetworkFungibleFaucet`] component. - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. - /// - the token supply exceeds the max supply. - /// - the token symbol encoded value exceeds the maximum value of - /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + /// Checks that the account contains the network fungible faucet interface. fn try_from_interface( interface: AccountInterface, - storage: &AccountStorage, + _storage: &miden_protocol::account::AccountStorage, ) -> Result { - // Check that the procedures of the network fungible faucet exist in the account. if !interface .components() .contains(&AccountComponentInterface::NetworkFungibleFaucet) @@ -137,113 +92,22 @@ impl NetworkFungibleFaucet { return Err(FungibleFaucetError::MissingNetworkFungibleFaucetInterface); } - // Read token metadata from storage - let metadata = TokenMetadata::try_from(storage)?; - - Ok(Self { metadata }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored. - pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() - } - - /// Returns the storage slot schema for the metadata slot. - pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); - ( - Self::metadata_slot().clone(), - StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ), - ) - } - - /// Returns the token metadata. - pub fn metadata(&self) -> &TokenMetadata { - &self.metadata - } - - /// Returns the symbol of the faucet. - pub fn symbol(&self) -> &TokenSymbol { - self.metadata.symbol() - } - - /// Returns the decimals of the faucet. - pub fn decimals(&self) -> u8 { - self.metadata.decimals() - } - - /// Returns the max supply (in base units) of the faucet. - /// - /// This is the highest amount of tokens that can be minted from this faucet. - pub fn max_supply(&self) -> Felt { - self.metadata.max_supply() - } - - /// Returns the token supply (in base units) of the faucet. - /// - /// This is the amount of tokens that were minted from the faucet so far. Its value can never - /// exceed [`Self::max_supply`]. - pub fn token_supply(&self) -> Felt { - self.metadata.token_supply() - } - - /// Returns the digest of the `distribute` account procedure. - pub fn distribute_digest() -> Word { - *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE - } - - /// Returns the digest of the `burn` account procedure. - pub fn burn_digest() -> Word { - *NETWORK_FUNGIBLE_FAUCET_BURN - } - - // MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Sets the token_supply (in base units) of the network fungible faucet. - /// - /// # Errors - /// - /// Returns an error if: - /// - the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - self.metadata = self.metadata.with_token_supply(token_supply)?; - Ok(self) + Ok(NetworkFungibleFaucet) } /// Returns the [`AccountComponentMetadata`] for this component. pub fn component_metadata() -> AccountComponentMetadata { - let storage_schema = StorageSchema::new([Self::metadata_slot_schema()]) - .expect("storage schema should be valid"); - AccountComponentMetadata::new(Self::NAME, [AccountType::FungibleFaucet]) .with_description("Network fungible faucet component for minting and burning tokens") - .with_storage_schema(storage_schema) } } impl From for AccountComponent { - fn from(network_faucet: NetworkFungibleFaucet) -> Self { - let metadata_slot = network_faucet.metadata.into(); + fn from(_network_faucet: NetworkFungibleFaucet) -> Self { let metadata = NetworkFungibleFaucet::component_metadata(); - AccountComponent::new( - network_fungible_faucet_library(), - vec![metadata_slot], - metadata, - ) - .expect("network fungible faucet component should satisfy the requirements of a valid account component") + AccountComponent::new(network_fungible_faucet_library(), vec![], metadata) + .expect("network fungible faucet component should satisfy the requirements of a valid account component") } } @@ -268,7 +132,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { } /// Creates a new faucet account with network fungible faucet interface and provided metadata -/// (token symbol, decimals, max supply) and access control. +/// and access control. /// /// The network faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -282,14 +146,12 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// - [`AccountStorageMode::Network`] for storage /// - [`NoAuth`] for authentication /// -/// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] and +/// The storage layout of the faucet account is documented on the [`FungibleTokenMetadata`] and /// [`crate::account::access::Ownable2Step`] types, and contains no additional storage slots for /// its auth ([`NoAuth`]). pub fn create_network_fungible_faucet( init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, + metadata: FungibleTokenMetadata, access_control: AccessControl, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); @@ -298,7 +160,8 @@ pub fn create_network_fungible_faucet( .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(auth_component) - .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply)?) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) .with_component(access_control) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -311,17 +174,17 @@ pub fn create_network_fungible_faucet( #[cfg(test)] mod tests { + use miden_protocol::Felt; use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; + use miden_protocol::asset::TokenSymbol; use super::*; use crate::account::access::Ownable2Step; + use crate::account::faucets::{FungibleTokenMetadata, TokenName}; #[test] fn test_create_network_fungible_faucet() { let init_seed = [7u8; 32]; - let symbol = TokenSymbol::new("NET").expect("token symbol should be valid"); - let decimals = 8u8; - let max_supply = Felt::new(1_000); let owner = AccountId::dummy( [1u8; 15], @@ -330,11 +193,20 @@ mod tests { AccountStorageMode::Private, ); + let metadata = FungibleTokenMetadata::new( + TokenSymbol::new("NET").expect("valid symbol"), + 8u8, + Felt::new(1_000), + TokenName::new("NET").expect("valid name"), + None, + None, + None, + ) + .expect("valid metadata"); + let account = create_network_fungible_faucet( init_seed, - symbol.clone(), - decimals, - max_supply, + metadata, AccessControl::Ownable2Step { owner }, ) .expect("network faucet creation should succeed"); @@ -345,11 +217,7 @@ mod tests { expected_owner_word ); - let faucet = NetworkFungibleFaucet::try_from(&account) + let _faucet = NetworkFungibleFaucet::try_from(&account) .expect("network fungible faucet should be extractable from account"); - assert_eq!(faucet.symbol(), &symbol); - assert_eq!(faucet.decimals(), decimals); - assert_eq!(faucet.max_supply(), max_supply); - assert_eq!(faucet.token_supply(), Felt::ZERO); } } diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index bdca915fa5..835d01659a 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,17 +1,180 @@ -use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; +use alloc::vec::Vec; + +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaType, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + AccountComponent, + AccountStorage, + AccountType, + StorageSlot, + StorageSlotName, +}; use miden_protocol::asset::{FungibleAsset, TokenSymbol}; -use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; +use crate::account::components::fungible_token_metadata_library; +use crate::account::encoding::{FixedWidthString, FixedWidthStringError}; +use crate::account::metadata::{self, FieldBytesError, NameUtf8Error, TokenMetadata}; -// CONSTANTS +// TOKEN NAME // ================================================================================================ -static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::fungible_faucets::metadata") - .expect("storage slot name should be valid") -}); +/// Token display name (max 32 bytes UTF-8), stored in 2 Words. +/// +/// The maximum is intentionally capped at 32 bytes even though the 2-Word encoding could +/// hold up to 55 bytes. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TokenName(FixedWidthString<2>); + +impl TokenName { + /// Maximum byte length for a token name (capped at 32, below the 55-byte capacity). + pub const MAX_BYTES: usize = metadata::NAME_UTF8_MAX_BYTES; + + /// Creates a token name from a UTF-8 string (at most 32 bytes). + pub fn new(s: &str) -> Result { + if s.len() > Self::MAX_BYTES { + return Err(NameUtf8Error::TooLong(s.len())); + } + Ok(Self(FixedWidthString::from_str_unchecked(s))) + } + + /// Returns the name as a string slice. + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Encodes the name into 2 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() + } + + /// Decodes a token name from a 2-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + let inner = + FixedWidthString::<2>::try_from_words(words).map_err(|_| NameUtf8Error::InvalidUtf8)?; + if inner.as_str().len() > Self::MAX_BYTES { + return Err(NameUtf8Error::TooLong(inner.as_str().len())); + } + Ok(Self(inner)) + } +} + +// DESCRIPTION +// ================================================================================================ + +/// Token description (max 195 bytes UTF-8), stored in 7 Words. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Description(FixedWidthString<7>); + +impl Description { + /// Maximum byte length for a description (7 Words × 4 felts × 7 bytes − 1 length byte). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates a description from a UTF-8 string. + pub fn new(s: &str) -> Result { + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) + } + + /// Returns the description as a string slice. + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Encodes the description into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() + } + + /// Decodes a description from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) + } +} + +// LOGO URI +// ================================================================================================ + +/// Token logo URI (max 195 bytes UTF-8), stored in 7 Words. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogoURI(FixedWidthString<7>); + +impl LogoURI { + /// Maximum byte length for a logo URI (7 Words × 4 felts × 7 bytes − 1 length byte). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates a logo URI from a UTF-8 string. + pub fn new(s: &str) -> Result { + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) + } + + /// Returns the logo URI as a string slice. + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Encodes the logo URI into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() + } + + /// Decodes a logo URI from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) + } +} + +// EXTERNAL LINK +// ================================================================================================ + +/// Token external link (max 195 bytes UTF-8), stored in 7 Words. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExternalLink(FixedWidthString<7>); + +impl ExternalLink { + /// Maximum byte length for an external link (7 Words × 4 felts × 7 bytes − 1 length byte). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates an external link from a UTF-8 string. + pub fn new(s: &str) -> Result { + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) + } + + /// Returns the external link as a string slice. + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Encodes the external link into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() + } + + /// Decodes an external link from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) + } +} // TOKEN METADATA // ================================================================================================ @@ -26,15 +189,24 @@ static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { /// /// The metadata is stored in a single storage slot as: /// `[token_supply, max_supply, decimals, symbol]` +/// +/// `name` and optional `description`/`logo_uri`/`external_link` are stored in separate +/// storage slots (slots 2–25). All fields are serialized into the component's storage +/// via [`storage_slots`](Self::storage_slots) when converting to an [`AccountComponent`]. +/// The schema type for token symbols. +const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; + #[derive(Debug, Clone)] -pub struct TokenMetadata { +pub struct FungibleTokenMetadata { token_supply: Felt, max_supply: Felt, decimals: u8, symbol: TokenSymbol, + /// Embeds name, optional fields, and mutability flags. + metadata: TokenMetadata, } -impl TokenMetadata { +impl FungibleTokenMetadata { // CONSTANTS // -------------------------------------------------------------------------------------------- @@ -44,7 +216,7 @@ impl TokenMetadata { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`TokenMetadata`] with the specified metadata and zero token supply. + /// Creates a new [`FungibleTokenMetadata`] with the specified metadata and zero token supply. /// /// # Errors /// Returns an error if: @@ -54,11 +226,24 @@ impl TokenMetadata { symbol: TokenSymbol, decimals: u8, max_supply: Felt, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { - Self::with_supply(symbol, decimals, max_supply, Felt::ZERO) + Self::with_supply( + symbol, + decimals, + max_supply, + Felt::ZERO, + name, + description, + logo_uri, + external_link, + ) } - /// Creates a new [`TokenMetadata`] with the specified metadata and token supply. + /// Creates a new [`FungibleTokenMetadata`] with the specified metadata and token supply. /// /// # Errors /// Returns an error if: @@ -70,6 +255,10 @@ impl TokenMetadata { decimals: u8, max_supply: Felt, token_supply: Felt, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { if decimals > Self::MAX_DECIMALS { return Err(FungibleFaucetError::TooManyDecimals { @@ -92,20 +281,33 @@ impl TokenMetadata { }); } + let mut token_metadata = TokenMetadata::new().with_name(name); + if let Some(desc) = description { + token_metadata = token_metadata.with_description(desc, false); + } + if let Some(uri) = logo_uri { + token_metadata = token_metadata.with_logo_uri(uri, false); + } + if let Some(link) = external_link { + token_metadata = token_metadata.with_external_link(link, false); + } + Ok(Self { token_supply, max_supply, decimals, symbol, + metadata: token_metadata, }) } // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the token metadata is stored. + /// Returns the [`StorageSlotName`] where the token metadata is stored (canonical slot shared + /// with the metadata module). pub fn metadata_slot() -> &'static StorageSlotName { - &METADATA_SLOT_NAME + metadata::token_metadata_slot() } /// Returns the current token supply (amount issued). @@ -128,6 +330,63 @@ impl TokenMetadata { &self.symbol } + /// Returns the token name. + pub fn name(&self) -> &TokenName { + self.metadata.name().expect("FungibleTokenMetadata always has a name") + } + + /// Returns the optional description. + pub fn description(&self) -> Option<&Description> { + self.metadata.description() + } + + /// Returns the optional logo URI. + pub fn logo_uri(&self) -> Option<&LogoURI> { + self.metadata.logo_uri() + } + + /// Returns the optional external link. + pub fn external_link(&self) -> Option<&ExternalLink> { + self.metadata.external_link() + } + + /// Returns the storage slot schema for the metadata slot. + pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); + ( + Self::metadata_slot().clone(), + StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ), + ) + } + + /// Returns all the storage slots for this component (metadata word + name + config + + /// description + logo_uri + external_link). + pub fn storage_slots(&self) -> Vec { + let mut slots: Vec = Vec::new(); + + // Slot 0: metadata word [token_supply, max_supply, decimals, symbol] + let metadata_word = Word::new([ + self.token_supply, + self.max_supply, + Felt::from(self.decimals), + self.symbol.clone().into(), + ]); + slots.push(StorageSlot::with_value(Self::metadata_slot().clone(), metadata_word)); + + // Slots 1-24: name, mutability config, description, logo_uri, external_link + slots.extend(self.metadata.storage_slots()); + + slots + } + // MUTATORS // -------------------------------------------------------------------------------------------- @@ -149,17 +408,46 @@ impl TokenMetadata { Ok(self) } + + /// Sets whether the description can be updated by the owner. + pub fn with_description_mutable(mut self, mutable: bool) -> Self { + self.metadata = self.metadata.with_description_mutable(mutable); + self + } + + /// Sets whether the logo URI can be updated by the owner. + pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self { + self.metadata = self.metadata.with_logo_uri_mutable(mutable); + self + } + + /// Sets whether the external link can be updated by the owner. + pub fn with_external_link_mutable(mut self, mutable: bool) -> Self { + self.metadata = self.metadata.with_external_link_mutable(mutable); + self + } + + /// Sets whether the max supply can be updated by the owner. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.metadata = self.metadata.with_max_supply_mutable(mutable); + self + } } // TRAIT IMPLEMENTATIONS // ================================================================================================ -impl TryFrom for TokenMetadata { +impl TryFrom for FungibleTokenMetadata { type Error = FungibleFaucetError; /// Parses token metadata from a Word. /// - /// The Word is expected to be in the format: `[token_supply, max_supply, decimals, symbol]` + /// The Word is expected to be in the format: `[token_supply, max_supply, decimals, symbol]`. + /// + /// **Note:** The name is set to an empty string and optional fields (description, + /// logo_uri, external_link) are `None`, because these are stored in separate + /// storage slots (via [`TokenMetadata`](crate::account::metadata::TokenMetadata)), + /// not in the metadata Word itself. fn try_from(word: Word) -> Result { let [token_supply, max_supply, decimals, token_symbol] = *word; @@ -173,32 +461,56 @@ impl TryFrom for TokenMetadata { } })?; - Self::with_supply(symbol, decimals, max_supply, token_supply) + Self::with_supply( + symbol, + decimals, + max_supply, + token_supply, + TokenName::default(), + None, + None, + None, + ) + } +} + +impl From for Word { + fn from(m: FungibleTokenMetadata) -> Self { + Word::new([m.token_supply, m.max_supply, Felt::from(m.decimals), m.symbol.into()]) } } -impl From for Word { - fn from(metadata: TokenMetadata) -> Self { - // Storage layout: [token_supply, max_supply, decimals, symbol] - Word::new([ - metadata.token_supply, - metadata.max_supply, - Felt::from(metadata.decimals), - metadata.symbol.as_element(), - ]) +impl From for StorageSlot { + fn from(metadata: FungibleTokenMetadata) -> Self { + StorageSlot::with_value(FungibleTokenMetadata::metadata_slot().clone(), metadata.into()) } } -impl From for StorageSlot { - fn from(metadata: TokenMetadata) -> Self { - StorageSlot::with_value(TokenMetadata::metadata_slot().clone(), metadata.into()) +impl From for AccountComponent { + fn from(metadata: FungibleTokenMetadata) -> Self { + let storage_schema = StorageSchema::new([FungibleTokenMetadata::metadata_slot_schema()]) + .expect("storage schema should be valid"); + + let component_metadata = AccountComponentMetadata::new( + "miden::standards::components::faucets::fungible_token_metadata", + [AccountType::FungibleFaucet], + ) + .with_description("Fungible token metadata component storing token metadata, name, mutability config, description, logo URI, and external link") + .with_storage_schema(storage_schema); + + AccountComponent::new( + fungible_token_metadata_library(), + metadata.storage_slots(), + component_metadata, + ) + .expect("fungible token metadata component should satisfy the requirements of a valid account component") } } -impl TryFrom<&StorageSlot> for TokenMetadata { +impl TryFrom<&StorageSlot> for FungibleTokenMetadata { type Error = FungibleFaucetError; - /// Tries to create [`TokenMetadata`] from a storage slot. + /// Tries to create [`FungibleTokenMetadata`] from a storage slot. /// /// # Errors /// Returns an error if: @@ -211,23 +523,24 @@ impl TryFrom<&StorageSlot> for TokenMetadata { actual: slot.name().clone(), }); } - TokenMetadata::try_from(slot.value()) + FungibleTokenMetadata::try_from(slot.value()) } } -impl TryFrom<&AccountStorage> for TokenMetadata { +impl TryFrom<&AccountStorage> for FungibleTokenMetadata { type Error = FungibleFaucetError; - /// Tries to create [`TokenMetadata`] from account storage. + /// Tries to create [`FungibleTokenMetadata`] from account storage. fn try_from(storage: &AccountStorage) -> Result { - let metadata_word = storage.get_item(TokenMetadata::metadata_slot()).map_err(|err| { - FungibleFaucetError::StorageLookupFailed { - slot_name: TokenMetadata::metadata_slot().clone(), - source: err, - } - })?; - - TokenMetadata::try_from(metadata_word) + let metadata_word = + storage.get_item(FungibleTokenMetadata::metadata_slot()).map_err(|err| { + FungibleFaucetError::StorageLookupFailed { + slot_name: FungibleTokenMetadata::metadata_slot().clone(), + source: err, + } + })?; + + FungibleTokenMetadata::try_from(metadata_word) } } @@ -246,13 +559,27 @@ mod tests { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 8u8; let max_supply = Felt::new(1_000_000); + let name = TokenName::new("TEST").unwrap(); - let metadata = TokenMetadata::new(symbol.clone(), decimals, max_supply).unwrap(); + let metadata = FungibleTokenMetadata::new( + symbol.clone(), + decimals, + max_supply, + name.clone(), + None, + None, + None, + ) + .unwrap(); assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.decimals(), decimals); assert_eq!(metadata.max_supply(), max_supply); assert_eq!(metadata.token_supply(), Felt::ZERO); + assert_eq!(metadata.name(), &name); + assert!(metadata.description().is_none()); + assert!(metadata.logo_uri().is_none()); + assert!(metadata.external_link().is_none()); } #[test] @@ -261,9 +588,19 @@ mod tests { let decimals = 8u8; let max_supply = Felt::new(1_000_000); let token_supply = Felt::new(500_000); + let name = TokenName::new("TEST").unwrap(); - let metadata = - TokenMetadata::with_supply(symbol.clone(), decimals, max_supply, token_supply).unwrap(); + let metadata = FungibleTokenMetadata::with_supply( + symbol.clone(), + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + ) + .unwrap(); assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.decimals(), decimals); @@ -271,13 +608,96 @@ mod tests { assert_eq!(metadata.token_supply(), token_supply); } + #[test] + fn token_metadata_with_name_and_description() { + let symbol = TokenSymbol::new("POL").unwrap(); + let decimals = 2u8; + let max_supply = Felt::new(123); + let name = TokenName::new("polygon").unwrap(); + let description = Description::new("A polygon token").unwrap(); + + let metadata = FungibleTokenMetadata::new( + symbol.clone(), + decimals, + max_supply, + name.clone(), + Some(description.clone()), + None, + None, + ) + .unwrap(); + + assert_eq!(metadata.symbol(), &symbol); + assert_eq!(metadata.name(), &name); + assert_eq!(metadata.description(), Some(&description)); + let word: Word = metadata.into(); + let restored = FungibleTokenMetadata::try_from(word).unwrap(); + assert_eq!(restored.symbol(), &symbol); + assert!(restored.description().is_none()); + } + + #[test] + fn token_name_roundtrip() { + let name = TokenName::new("polygon").unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), "polygon"); + } + + #[test] + fn token_name_as_str() { + let name = TokenName::new("my_token").unwrap(); + assert_eq!(name.as_str(), "my_token"); + } + + #[test] + fn token_name_too_long() { + let s = "a".repeat(33); + assert!(TokenName::new(&s).is_err()); + } + + #[test] + fn description_roundtrip() { + let text = "A short description"; + let desc = Description::new(text).unwrap(); + let words = desc.to_words(); + let decoded = Description::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), text); + } + + #[test] + fn description_too_long() { + let s = "a".repeat(Description::MAX_BYTES + 1); + assert!(Description::new(&s).is_err()); + } + + #[test] + fn logo_uri_roundtrip() { + let url = "https://example.com/logo.png"; + let uri = LogoURI::new(url).unwrap(); + let words = uri.to_words(); + let decoded = LogoURI::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), url); + } + + #[test] + fn external_link_roundtrip() { + let url = "https://example.com"; + let link = ExternalLink::new(url).unwrap(); + let words = link.to_words(); + let decoded = ExternalLink::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), url); + } + #[test] fn token_metadata_too_many_decimals() { let symbol = TokenSymbol::new("TEST").unwrap(); - let decimals = 13u8; // exceeds MAX_DECIMALS + let decimals = 13u8; let max_supply = Felt::new(1_000_000); + let name = TokenName::new("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply); + let result = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. }))); } @@ -287,10 +707,11 @@ mod tests { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 8u8; - // FungibleAsset::MAX_AMOUNT is 2^63 - 1, so we use MAX_AMOUNT + 1 to exceed it let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1); + let name = TokenName::new("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply); + let result = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. }))); } @@ -300,12 +721,14 @@ mod tests { let symbol_felt = symbol.as_element(); let decimals = 2u8; let max_supply = Felt::new(123); + let name = TokenName::new("POL").unwrap(); - let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap(); + let metadata = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None) + .unwrap(); let word: Word = metadata.into(); - // Storage layout: [token_supply, max_supply, decimals, symbol] - assert_eq!(word[0], Felt::ZERO); // token_supply + assert_eq!(word[0], Felt::ZERO); assert_eq!(word[1], max_supply); assert_eq!(word[2], Felt::from(decimals)); assert_eq!(word[3], symbol_felt); @@ -316,11 +739,21 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); + let name = TokenName::new("POL").unwrap(); - let original = TokenMetadata::new(symbol.clone(), decimals, max_supply).unwrap(); + let original = FungibleTokenMetadata::new( + symbol.clone(), + decimals, + max_supply, + name, + None, + None, + None, + ) + .unwrap(); let slot: StorageSlot = original.into(); - let restored = TokenMetadata::try_from(&slot).unwrap(); + let restored = FungibleTokenMetadata::try_from(&slot).unwrap(); assert_eq!(restored.symbol(), &symbol); assert_eq!(restored.decimals(), decimals); @@ -334,15 +767,243 @@ mod tests { let decimals = 2u8; let max_supply = Felt::new(1000); let token_supply = Felt::new(500); + let name = TokenName::new("POL").unwrap(); - let original = - TokenMetadata::with_supply(symbol.clone(), decimals, max_supply, token_supply).unwrap(); + let original = FungibleTokenMetadata::with_supply( + symbol.clone(), + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + ) + .unwrap(); let word: Word = original.into(); - let restored = TokenMetadata::try_from(word).unwrap(); + let restored = FungibleTokenMetadata::try_from(word).unwrap(); assert_eq!(restored.symbol(), &symbol); assert_eq!(restored.decimals(), decimals); assert_eq!(restored.max_supply(), max_supply); assert_eq!(restored.token_supply(), token_supply); } + + #[test] + fn mutability_builders() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) + .unwrap() + .with_description_mutable(true) + .with_logo_uri_mutable(true) + .with_external_link_mutable(false) + .with_max_supply_mutable(true); + + let slots = metadata.storage_slots(); + + // Slot layout (no owner slot): [0]=metadata, [1]=name_0, [2]=name_1, [3]=mutability_config + let config_slot = &slots[3]; + let config_word = config_slot.value(); + assert_eq!(config_word[0], Felt::from(1u32), "desc_mutable"); + assert_eq!(config_word[1], Felt::from(1u32), "logo_mutable"); + assert_eq!(config_word[2], Felt::from(0u32), "extlink_mutable"); + assert_eq!(config_word[3], Felt::from(1u32), "max_supply_mutable"); + } + + #[test] + fn mutability_defaults_to_false() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) + .unwrap(); + + let slots = metadata.storage_slots(); + let config_word = slots[3].value(); + assert_eq!(config_word[0], Felt::ZERO, "desc_mutable default"); + assert_eq!(config_word[1], Felt::ZERO, "logo_mutable default"); + assert_eq!(config_word[2], Felt::ZERO, "extlink_mutable default"); + assert_eq!(config_word[3], Felt::ZERO, "max_supply_mutable default"); + } + + #[test] + fn storage_slots_includes_metadata_word() { + let symbol = TokenSymbol::new("POL").unwrap(); + let name = TokenName::new("polygon").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol.clone(), 2, Felt::new(123), name, None, None, None) + .unwrap(); + let slots = metadata.storage_slots(); + + // First slot is the metadata word [token_supply, max_supply, decimals, symbol] + let metadata_word = slots[0].value(); + assert_eq!(metadata_word[0], Felt::ZERO); // token_supply + assert_eq!(metadata_word[1], Felt::new(123)); // max_supply + assert_eq!(metadata_word[2], Felt::from(2u32)); // decimals + assert_eq!(metadata_word[3], Felt::from(symbol)); // symbol + } + + #[test] + fn storage_slots_includes_name() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("my token").unwrap(); + let expected_words = name.to_words(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let slots = metadata.storage_slots(); + + // Slot layout: [0]=metadata, [1]=name_0, [2]=name_1 + assert_eq!(slots[1].value(), expected_words[0]); + assert_eq!(slots[2].value(), expected_words[1]); + } + + #[test] + fn storage_slots_includes_description() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let description = Description::new("A cool token").unwrap(); + let expected_words = description.to_words(); + + let metadata = FungibleTokenMetadata::new( + symbol, + 2, + Felt::new(100), + name, + Some(description), + None, + None, + ) + .unwrap(); + let slots = metadata.storage_slots(); + + // Slots 4..11 are description (7 words): after metadata(1) + name(2) + config(1) + for (i, expected) in expected_words.iter().enumerate() { + assert_eq!(slots[4 + i].value(), *expected, "description word {i}"); + } + } + + #[test] + fn storage_slots_total_count() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let slots = metadata.storage_slots(); + + // 1 metadata + 2 name + 1 config + 7 description + 7 logo + 7 external_link = 25 + assert_eq!(slots.len(), 25); + } + + #[test] + fn into_account_component() { + use miden_protocol::account::{AccountBuilder, AccountType}; + + use crate::account::auth::NoAuth; + use crate::account::faucets::basic_fungible::BasicFungibleFaucet; + + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("test token").unwrap(); + let description = Description::new("A test").unwrap(); + + let metadata = FungibleTokenMetadata::new( + symbol, + 4, + Felt::new(10_000), + name, + Some(description), + None, + None, + ) + .unwrap() + .with_max_supply_mutable(true); + + // Should build an account successfully with FungibleTokenMetadata as a component + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .expect("account build should succeed"); + + // Verify metadata slot is accessible + let md_word = account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(md_word[1], Felt::new(10_000)); // max_supply + assert_eq!(md_word[2], Felt::from(4u32)); // decimals + + // Verify mutability config + let config = account.storage().get_item(metadata::mutability_config_slot()).unwrap(); + assert_eq!(config[3], Felt::from(1u32), "max_supply_mutable"); + } + + #[test] + fn logo_uri_too_long() { + let s = "a".repeat(LogoURI::MAX_BYTES + 1); + assert!(LogoURI::new(&s).is_err()); + } + + #[test] + fn external_link_too_long() { + let s = "a".repeat(ExternalLink::MAX_BYTES + 1); + assert!(ExternalLink::new(&s).is_err()); + } + + #[test] + fn token_supply_exceeds_max_supply() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let max_supply = Felt::new(100); + let token_supply = Felt::new(101); + + let result = FungibleTokenMetadata::with_supply( + symbol, + 2, + max_supply, + token_supply, + name, + None, + None, + None, + ); + assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); + } + + #[test] + fn with_token_supply_exceeds_max_supply() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + + let result = metadata.with_token_supply(Felt::new(101)); + assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); + } + + #[test] + fn slot_name_mismatch() { + use miden_protocol::account::StorageSlotName; + + let wrong_slot_name = StorageSlotName::new("wrong::slot::name").expect("valid slot name"); + let slot = StorageSlot::with_value(wrong_slot_name, Word::default()); + + let result = FungibleTokenMetadata::try_from(&slot); + assert!(matches!(result, Err(FungibleFaucetError::SlotNameMismatch { .. }))); + } + + #[test] + fn invalid_token_symbol_in_word() { + // TokenSymbol::try_from(Felt) fails when the value exceeds MAX_ENCODED_VALUE. + // The Word layout is [token_supply, max_supply, decimals, token_symbol] — symbol is [3]. + let bad_symbol = Felt::new(TokenSymbol::MAX_ENCODED_VALUE + 1); + let bad_word = Word::from([Felt::ZERO, Felt::new(100), Felt::new(2), bad_symbol]); + let result = FungibleTokenMetadata::try_from(bad_word); + assert!(matches!(result, Err(FungibleFaucetError::InvalidTokenSymbol(_)))); + } } diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 51615b7151..65e236685b 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -19,6 +19,9 @@ pub enum AccountComponentInterface { /// Exposes procedures from the [`BasicWallet`][crate::account::wallets::BasicWallet] module. BasicWallet, /// Exposes procedures from the + /// [`FungibleTokenMetadata`][crate::account::faucets::FungibleTokenMetadata] module. + FungibleTokenMetadata, + /// Exposes procedures from the /// [`BasicFungibleFaucet`][crate::account::faucets::BasicFungibleFaucet] module. BasicFungibleFaucet, /// Exposes procedures from the @@ -57,6 +60,9 @@ impl AccountComponentInterface { pub fn name(&self) -> String { match self { AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(), + AccountComponentInterface::FungibleTokenMetadata => { + "Fungible Token Metadata".to_string() + }, AccountComponentInterface::BasicFungibleFaucet => "Basic Fungible Faucet".to_string(), AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index f23b1414a7..cf8b783080 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -13,6 +13,7 @@ use crate::account::components::{ StandardAccountComponent, basic_fungible_faucet_library, basic_wallet_library, + fungible_token_metadata_library, multisig_library, multisig_psm_library, network_fungible_faucet_library, @@ -92,6 +93,11 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(basic_wallet_library().mast_forest().procedure_digests()); }, + AccountComponentInterface::FungibleTokenMetadata => { + component_proc_digests.extend( + fungible_token_metadata_library().mast_forest().procedure_digests(), + ); + }, AccountComponentInterface::BasicFungibleFaucet => { component_proc_digests .extend(basic_fungible_faucet_library().mast_forest().procedure_digests()); diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index d6cbcc7b51..374908aad9 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -55,13 +55,18 @@ fn test_basic_wallet_default_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - BasicFungibleFaucet::new( + crate::account::faucets::FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), + crate::account::faucets::TokenName::new("POL").unwrap(), + None, + None, + None, ) - .expect("failed to create a fungible faucet component"), + .expect("failed to create token metadata"), ) + .with_component(BasicFungibleFaucet) .build_existing() .expect("failed to create wallet account"); let faucet_account_interface = AccountInterface::from_account(&faucet_account); @@ -321,13 +326,18 @@ fn test_basic_fungible_faucet_custom_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - BasicFungibleFaucet::new( + crate::account::faucets::FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), + crate::account::faucets::TokenName::new("POL").unwrap(), + None, + None, + None, ) - .expect("failed to create a fungible faucet component"), + .expect("failed to create token metadata"), ) + .with_component(BasicFungibleFaucet) .build_existing() .expect("failed to create wallet account"); let faucet_account_interface = AccountInterface::from_account(&faucet_account); diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 69def3da9f..0f9c1a8155 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,258 +1,345 @@ -use alloc::collections::BTreeMap; - -use miden_protocol::Word; -use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; -use miden_protocol::account::{ - Account, - AccountBuilder, - AccountComponent, - AccountType, - StorageSlot, - StorageSlotName, -}; -use miden_protocol::errors::{AccountError, ComponentMetadataError}; +//! Account / contract / faucet metadata +//! +//! All of the following are metadata of the account (or faucet): token_symbol, decimals, +//! max_supply, name, mutability_config, description, logo URI, and external link. +//! Ownership is handled by the `Ownable2Step` component separately. +//! +//! ## Storage layout +//! +//! | Slot name | Contents | +//! |-----------|----------| +//! | `metadata::token_metadata` | `[token_supply, max_supply, decimals, token_symbol]` | +//! | `metadata::name_0` | first 4 felts of name | +//! | `metadata::name_1` | last 4 felts of name | +//! | `metadata::mutability_config` | `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` | +//! | `metadata::description_0..6` | description (7 Words, max 195 bytes) | +//! | `metadata::logo_uri_0..6` | logo URI (7 Words, max 195 bytes) | +//! | `metadata::external_link_0..6` | external link (7 Words, max 195 bytes) | +//! +//! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. +//! Any change to slot indices or names must be applied in both Rust and MASM. +//! +//! ## Config Word +//! +//! A single config Word stores per-field boolean flags: +//! +//! **mutability_config**: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +//! - Each flag is 0 (immutable) or 1 (mutable / owner can update). +//! +//! Whether a field is *present* is determined by whether its storage words are all zero +//! (absent) or not (present). No separate `initialized_config` is needed. +//! +//! ## MASM modules +//! +//! All metadata procedures (getters, setters) live in `miden::standards::metadata::fungible`, +//! which depends on `ownable2step` for ownership checks. For mutable fields, accounts must +//! also include the `Ownable2Step` component (e.g. network fungible faucet). +//! +//! ## String encoding (UTF-8) +//! +//! All string fields use **7-bytes-per-felt, length-prefixed** encoding. The N felts are +//! serialized into a flat buffer of N × 7 bytes; byte 0 is the string length, followed by UTF-8 +//! content, zero-padded. Each 7-byte chunk is stored as a LE u64 with the high byte always zero, +//! so it always fits in a Goldilocks field element. +//! +//! The name slots hold 2 Words (8 felts, capacity 55 bytes, capped at 32). +//! +//! # Example +//! +//! ```ignore +//! use miden_standards::account::metadata::TokenMetadata; +//! use miden_standards::account::faucets::{TokenName, Description, LogoURI}; +//! +//! let info = TokenMetadata::new() +//! .with_name(TokenName::new("My Token").unwrap()) +//! .with_description(Description::new("A cool token").unwrap(), true) +//! .with_logo_uri(LogoURI::new("https://example.com/logo.png").unwrap(), false); +//! +//! let metadata = FungibleTokenMetadata::new(/* ... */).unwrap(); +//! let account = AccountBuilder::new(seed) +//! .with_component(metadata) +//! .with_component(BasicFungibleFaucet) +//! .build()?; +//! ``` + +mod schema_commitment; +mod token_metadata; + +use miden_protocol::account::StorageSlotName; use miden_protocol::utils::sync::LazyLock; +pub use schema_commitment::{ + AccountBuilderSchemaCommitmentExt, + AccountSchemaCommitment, + SCHEMA_COMMITMENT_SLOT_NAME, +}; +use thiserror::Error; +pub use token_metadata::TokenMetadata; -use crate::account::components::storage_schema_library; +// CONSTANTS — canonical layout: slots 0–22 +// ================================================================================================ -pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::metadata::storage_schema") +/// Token metadata: `[token_supply, max_supply, decimals, token_symbol]`. +pub(crate) static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::token_metadata") .expect("storage slot name should be valid") }); -/// An [`AccountComponent`] exposing the account storage schema commitment. -/// -/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], -/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`] -/// slot. +/// Token name (2 Words = 8 felts), split across 2 slots. /// -/// It reexports the `get_schema_commitment` procedure from -/// `miden::standards::metadata::storage_schema`. -/// -/// ## Storage Layout -/// -/// - [`Self::schema_commitment_slot`]: Storage schema commitment. -pub struct AccountSchemaCommitment { - schema_commitment: Word, +/// The encoding is not specified; the value is opaque word data. For human-readable names, +/// use [`TokenName::new`](crate::account::faucets::TokenName::new) / +/// [`TokenName::to_words`](crate::account::faucets::TokenName::to_words) / +/// [`TokenName::try_from_words`](crate::account::faucets::TokenName::try_from_words). +pub(crate) static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::name_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::name_1").expect("valid slot name"), + ] +}); + +/// Maximum length of a name in bytes when using the UTF-8 encoding (2 Words = 8 felts × 7 bytes +/// = 56 byte buffer − 1 length byte = 55 capacity, capped at 32). +pub(crate) const NAME_UTF8_MAX_BYTES: usize = 32; + +/// Errors when encoding or decoding the metadata name as UTF-8. +#[derive(Debug, Clone, Error)] +pub enum NameUtf8Error { + /// Name exceeds the maximum of 32 UTF-8 bytes. + #[error("name must be at most 32 UTF-8 bytes, got {0}")] + TooLong(usize), + /// Decoded bytes are not valid UTF-8. + #[error("name is not valid UTF-8")] + InvalidUtf8, } -impl AccountSchemaCommitment { - /// Creates a new [`AccountSchemaCommitment`] component from storage schemas. - /// - /// The input schemas are merged into a single schema before the final commitment is computed. - /// - /// # Errors - /// - /// Returns an error if the schemas contain conflicting definitions for the same slot name. - pub fn new<'a>( - schemas: impl IntoIterator, - ) -> Result { - Ok(Self { - schema_commitment: compute_schema_commitment(schemas)?, - }) - } +/// Mutability config slot: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]`. +/// +/// Each flag is 0 (immutable) or 1 (mutable / owner can update). +pub(crate) static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::mutability_config") + .expect("storage slot name should be valid") +}); - /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`]. - pub fn from_schema(storage_schema: &StorageSchema) -> Result { - Self::new(core::slice::from_ref(storage_schema)) - } +/// Maximum length of a metadata field (description, logo_uri, external_link) in bytes. +/// 7 Words = 28 felts × 7 bytes = 196 byte buffer − 1 length byte = 195 bytes. +pub(crate) const FIELD_MAX_BYTES: usize = 195; + +/// Errors when encoding or decoding metadata fields. +#[derive(Debug, Clone, Error)] +pub enum FieldBytesError { + /// Field exceeds the maximum of 195 bytes. + #[error("field must be at most 195 bytes, got {0}")] + TooLong(usize), + /// Decoded bytes are not valid UTF-8. + #[error("field is not valid UTF-8")] + InvalidUtf8, +} - /// Returns the [`StorageSlotName`] where the schema commitment is stored. - pub fn schema_commitment_slot() -> &'static StorageSlotName { - &SCHEMA_COMMITMENT_SLOT_NAME - } +/// Description (7 Words = 28 felts), split across 7 slots. +pub(crate) static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::description_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_1").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_2").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_3").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_4").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_5").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_6").expect("valid slot name"), + ] +}); - /// Returns the [`AccountComponentMetadata`] for this component. - pub fn component_metadata() -> AccountComponentMetadata { - AccountComponentMetadata::new("miden::metadata::schema_commitment", AccountType::all()) - .with_description("Component exposing the account storage schema commitment") - } -} +/// Logo URI (7 Words = 28 felts), split across 7 slots. +pub(crate) static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::logo_uri_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_1").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_2").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_3").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_4").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_5").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_6").expect("valid slot name"), + ] +}); -impl From for AccountComponent { - fn from(schema_commitment: AccountSchemaCommitment) -> Self { - let metadata = AccountSchemaCommitment::component_metadata(); - - AccountComponent::new( - storage_schema_library(), - vec![StorageSlot::with_value( - AccountSchemaCommitment::schema_commitment_slot().clone(), - schema_commitment.schema_commitment, - )], - metadata, - ) - .expect( - "AccountSchemaCommitment component should satisfy the requirements of a valid account component", - ) - } -} +/// External link (7 Words = 28 felts), split across 7 slots. +pub(crate) static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::external_link_0") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_1") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_2") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_3") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_4") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_5") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_6") + .expect("valid slot name"), + ] +}); -// ACCOUNT BUILDER EXTENSION +// SLOT ACCESSORS // ================================================================================================ -/// An extension trait for [`AccountBuilder`] that provides a convenience method for building an -/// account with an [`AccountSchemaCommitment`] component. -pub trait AccountBuilderSchemaCommitmentExt { - /// Builds an [`Account`] out of the configured builder after computing the storage schema - /// commitment from all components currently in the builder and adding an - /// [`AccountSchemaCommitment`] component. - /// - /// # Errors - /// - /// Returns an error if: - /// - The components' storage schemas contain conflicting definitions for the same slot name. - /// - [`AccountBuilder::build`] fails. - fn build_with_schema_commitment(self) -> Result; +/// Returns the [`StorageSlotName`] for token metadata (slot 0). +pub(crate) fn token_metadata_slot() -> &'static StorageSlotName { + &TOKEN_METADATA_SLOT } -impl AccountBuilderSchemaCommitmentExt for AccountBuilder { - fn build_with_schema_commitment(self) -> Result { - let schema_commitment = - AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| { - AccountError::other_with_source("failed to compute account schema commitment", err) - })?; - - self.with_component(schema_commitment).build() - } +/// Returns the [`StorageSlotName`] for the mutability config Word. +pub(crate) fn mutability_config_slot() -> &'static StorageSlotName { + &MUTABILITY_CONFIG_SLOT } -// HELPERS +// TESTS // ================================================================================================ -/// Computes the schema commitment. -/// -/// The account schema commitment is computed from the merged schema commitment. -/// If the passed list of schemas is empty, [`Word::empty()`] is returned. -fn compute_schema_commitment<'a>( - schemas: impl IntoIterator, -) -> Result { - let mut schemas = schemas.into_iter().peekable(); - if schemas.peek().is_none() { - return Ok(Word::empty()); +#[cfg(test)] +mod tests { + use miden_protocol::account::AccountBuilder; + + use super::{TokenMetadata as InfoType, mutability_config_slot}; + use crate::account::auth::NoAuth; + use crate::account::faucets::{ + BasicFungibleFaucet, + Description, + FungibleTokenMetadata, + TokenName, + }; + + fn build_faucet_metadata( + name: TokenName, + description: Option, + ) -> FungibleTokenMetadata { + FungibleTokenMetadata::new( + miden_protocol::asset::TokenSymbol::new("TST").unwrap(), + 2, + miden_protocol::Felt::new(1_000), + name, + description, + None, + None, + ) + .unwrap() } - let mut merged_slots = BTreeMap::new(); - - for schema in schemas { - for (slot_name, slot_schema) in schema.iter() { - match merged_slots.get(slot_name) { - None => { - merged_slots.insert(slot_name.clone(), slot_schema.clone()); - }, - // Slot exists, check if the schema is the same before erroring - Some(existing) => { - if existing != slot_schema { - return Err(ComponentMetadataError::InvalidSchema(format!( - "conflicting definitions for storage slot `{slot_name}`", - ))); - } - }, - } - } + fn build_account_with_metadata( + metadata: FungibleTokenMetadata, + ) -> miden_protocol::account::Account { + AccountBuilder::new([1u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap() } - let merged_schema = StorageSchema::new(merged_slots)?; + #[test] + fn metadata_info_can_store_name_and_description() { + let name = TokenName::new("test_name").unwrap(); + let description = Description::new("test description").unwrap(); - Ok(merged_schema.commitment()) -} + let name_words = name.to_words(); + let desc_words = description.to_words(); -// TESTS -// ================================================================================================ + let metadata = build_faucet_metadata(name, Some(description)); + let account = build_account_with_metadata(metadata); -#[cfg(test)] -mod tests { - use miden_protocol::Word; - use miden_protocol::account::AccountBuilder; - use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; - use miden_protocol::account::component::AccountComponentMetadata; + let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); + + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(InfoType::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + } - use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment}; - use crate::account::auth::{AuthSingleSig, NoAuth}; + #[test] + fn metadata_info_empty_works() { + let name = TokenName::new("T").unwrap(); + let metadata = build_faucet_metadata(name, None); + let _account = build_account_with_metadata(metadata); + } #[test] - fn storage_schema_commitment_is_order_independent() { - let toml_a = r#" - name = "Component A" - description = "Component A schema" - version = "0.1.0" - supported-types = [] - - [[storage.slots]] - name = "test::slot_a" - type = "word" - "#; - - let toml_b = r#" - name = "Component B" - description = "Component B schema" - version = "0.1.0" - supported-types = [] - - [[storage.slots]] - name = "test::slot_b" - description = "description is committed to" - type = "word" - "#; - - let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); - let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); - - let schema_a = metadata_a.storage_schema().clone(); - let schema_b = metadata_b.storage_schema().clone(); - - // Create one component for each of two different accounts, but switch orderings - let component_a = - AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap(); - let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap(); - - let account_a = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(component_a) - .build() - .unwrap(); + fn config_slots_set_correctly() { + use miden_protocol::Felt; + + let name = TokenName::new("T").unwrap(); + let metadata = build_faucet_metadata(name, Some(Description::new("test").unwrap())) + .with_description_mutable(true) + .with_max_supply_mutable(true); + let account = build_account_with_metadata(metadata); + + let mut_word = account.storage().get_item(mutability_config_slot()).unwrap(); + assert_eq!(mut_word[0], Felt::from(1u32), "desc_mutable should be 1"); + assert_eq!(mut_word[1], Felt::from(0u32), "logo_mutable should be 0"); + assert_eq!(mut_word[2], Felt::from(0u32), "extlink_mutable should be 0"); + assert_eq!(mut_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); + + let name_default = TokenName::new("T").unwrap(); + let metadata_default = build_faucet_metadata(name_default, None); + let account_default = build_account_with_metadata(metadata_default); + let mut_default = account_default.storage().get_item(mutability_config_slot()).unwrap(); + assert_eq!(mut_default[0], Felt::from(0u32), "desc_mutable should be 0 by default"); + assert_eq!(mut_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); + } - let account_b = AccountBuilder::new([2u8; 32]) - .with_auth_component(NoAuth) - .with_component(component_b) - .build() - .unwrap(); + #[test] + fn name_too_long_rejected() { + let long_name = "a".repeat(TokenName::MAX_BYTES + 1); + assert!(TokenName::new(&long_name).is_err()); + } - let slot_name = AccountSchemaCommitment::schema_commitment_slot(); - let commitment_a = account_a.storage().get_item(slot_name).unwrap(); - let commitment_b = account_b.storage().get_item(slot_name).unwrap(); + #[test] + fn description_too_long_rejected() { + let long_desc = "a".repeat(Description::MAX_BYTES + 1); + assert!(Description::new(&long_desc).is_err()); + } - assert_eq!(commitment_a, commitment_b); + #[test] + fn name_roundtrip() { + let s = "POL Faucet"; + let name = TokenName::new(s).unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), s); } #[test] - fn storage_schema_commitment_is_empty_for_no_schemas() { - let component = AccountSchemaCommitment::new(&[]).unwrap(); + fn name_max_32_bytes_accepted() { + let s = "a".repeat(TokenName::MAX_BYTES); + assert_eq!(s.len(), 32); + let name = TokenName::new(&s).unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), s); + } - assert_eq!(component.schema_commitment, Word::empty()); + #[test] + fn description_max_bytes_accepted() { + let s = "a".repeat(Description::MAX_BYTES); + let desc = Description::new(&s).unwrap(); + assert_eq!(desc.to_words().len(), 7); } #[test] - fn build_with_schema_commitment_adds_schema_commitment_component() { - let auth_component = AuthSingleSig::new( - PublicKeyCommitment::from(Word::empty()), - AuthScheme::EcdsaK256Keccak, - ); - - let account = AccountBuilder::new([1u8; 32]) - .with_auth_component(auth_component) - .build_with_schema_commitment() - .unwrap(); - - // The auth component has 2 slots (public key and scheme ID) and the schema commitment adds - // 1 more. - assert_eq!(account.storage().num_slots(), 3); - - // The auth component's public key slot should be accessible. - assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok()); - - // The schema commitment slot should be non-empty since we have a component with a schema. - let slot_name = AccountSchemaCommitment::schema_commitment_slot(); - let commitment = account.storage().get_item(slot_name).unwrap(); - assert_ne!(commitment, Word::empty()); + fn metadata_info_with_name() { + let name = TokenName::new("My Token").unwrap(); + let name_words = name.to_words(); + let metadata = build_faucet_metadata(name, None); + let account = build_account_with_metadata(metadata); + let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); + let decoded = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded.as_str(), "My Token"); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); } } diff --git a/crates/miden-standards/src/account/metadata/schema_commitment.rs b/crates/miden-standards/src/account/metadata/schema_commitment.rs new file mode 100644 index 0000000000..642d5d3d93 --- /dev/null +++ b/crates/miden-standards/src/account/metadata/schema_commitment.rs @@ -0,0 +1,273 @@ +//! Account storage schema commitment component. +//! +//! [`AccountSchemaCommitment`] computes a commitment over the merged storage schemas of all +//! account components and stores the result in a dedicated slot. The companion +//! [`AccountBuilderSchemaCommitmentExt`] trait adds a convenience method to +//! [`AccountBuilder`](miden_protocol::account::AccountBuilder) for building accounts with an +//! automatically computed schema commitment. + +use alloc::collections::BTreeMap; + +use miden_protocol::Word; +use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::errors::{AccountError, ComponentMetadataError}; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::storage_schema_library; + +// CONSTANTS +// ================================================================================================ + +/// Schema commitment slot name. +pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::metadata::schema_commitment") + .expect("storage slot name should be valid") +}); + +// SCHEMA COMMITMENT COMPONENT +// ================================================================================================ + +/// An [`AccountComponent`] exposing the account storage schema commitment. +/// +/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], +/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`] +/// slot. +/// +/// It reexports the `get_schema_commitment` procedure from +/// `miden::standards::metadata::storage_schema`. +/// +/// ## Storage Layout +/// +/// - [`Self::schema_commitment_slot`]: Storage schema commitment. +pub struct AccountSchemaCommitment { + schema_commitment: Word, +} + +impl AccountSchemaCommitment { + /// Creates a new [`AccountSchemaCommitment`] component from storage schemas. + /// + /// The input schemas are merged into a single schema before the final commitment is computed. + /// + /// # Errors + /// + /// Returns an error if the schemas contain conflicting definitions for the same slot name. + pub fn new<'a>( + schemas: impl IntoIterator, + ) -> Result { + Ok(Self { + schema_commitment: compute_schema_commitment(schemas)?, + }) + } + + /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`]. + pub fn from_schema(storage_schema: &StorageSchema) -> Result { + Self::new(core::slice::from_ref(storage_schema)) + } + + /// Returns the [`StorageSlotName`] where the schema commitment is stored. + pub fn schema_commitment_slot() -> &'static StorageSlotName { + &SCHEMA_COMMITMENT_SLOT_NAME + } + + /// Returns the [`AccountComponentMetadata`] for this component. + pub fn component_metadata() -> AccountComponentMetadata { + AccountComponentMetadata::new("miden::metadata::schema_commitment", AccountType::all()) + .with_description("Component exposing the account storage schema commitment") + } +} + +impl From for AccountComponent { + fn from(schema_commitment: AccountSchemaCommitment) -> Self { + let metadata = AccountSchemaCommitment::component_metadata(); + + AccountComponent::new( + storage_schema_library(), + vec![StorageSlot::with_value( + AccountSchemaCommitment::schema_commitment_slot().clone(), + schema_commitment.schema_commitment, + )], + metadata, + ) + .expect( + "AccountSchemaCommitment component should satisfy the requirements of a valid account component", + ) + } +} + +// ACCOUNT BUILDER EXTENSION +// ================================================================================================ + +/// An extension trait for [`AccountBuilder`] that provides a convenience method for building an +/// account with an [`AccountSchemaCommitment`] component. +pub trait AccountBuilderSchemaCommitmentExt { + /// Builds an [`Account`] out of the configured builder after computing the storage schema + /// commitment from all components currently in the builder and adding an + /// [`AccountSchemaCommitment`] component. + /// + /// # Errors + /// + /// Returns an error if: + /// - The components' storage schemas contain conflicting definitions for the same slot name. + /// - [`AccountBuilder::build`] fails. + fn build_with_schema_commitment(self) -> Result; +} + +impl AccountBuilderSchemaCommitmentExt for AccountBuilder { + fn build_with_schema_commitment(self) -> Result { + let schema_commitment = + AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| { + AccountError::other_with_source("failed to compute account schema commitment", err) + })?; + + self.with_component(schema_commitment).build() + } +} + +// HELPERS +// ================================================================================================ + +/// Computes the schema commitment. +/// +/// The account schema commitment is computed from the merged schema commitment. +/// If the passed list of schemas is empty, [`Word::empty()`] is returned. +fn compute_schema_commitment<'a>( + schemas: impl IntoIterator, +) -> Result { + let mut schemas = schemas.into_iter().peekable(); + if schemas.peek().is_none() { + return Ok(Word::empty()); + } + + let mut merged_slots = BTreeMap::new(); + + for schema in schemas { + for (slot_name, slot_schema) in schema.iter() { + match merged_slots.get(slot_name) { + None => { + merged_slots.insert(slot_name.clone(), slot_schema.clone()); + }, + // Slot exists, check if the schema is the same before erroring + Some(existing) => { + if existing != slot_schema { + return Err(ComponentMetadataError::InvalidSchema(format!( + "conflicting definitions for storage slot `{slot_name}`", + ))); + } + }, + } + } + } + + let merged_schema = StorageSchema::new(merged_slots)?; + + Ok(merged_schema.commitment()) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; + use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; + use miden_protocol::account::component::AccountComponentMetadata; + + use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment}; + use crate::account::auth::{AuthSingleSig, NoAuth}; + + #[test] + fn storage_schema_commitment_is_order_independent() { + let toml_a = r#" + name = "Component A" + description = "Component A schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_a" + type = "word" + "#; + + let toml_b = r#" + name = "Component B" + description = "Component B schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_b" + description = "description is committed to" + type = "word" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); + + let schema_a = metadata_a.storage_schema().clone(); + let schema_b = metadata_b.storage_schema().clone(); + + // Create one component for each of two different accounts, but switch orderings + let component_a = + AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap(); + let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap(); + + let account_a = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_a) + .build() + .unwrap(); + + let account_b = AccountBuilder::new([2u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_b) + .build() + .unwrap(); + + let slot_name = AccountSchemaCommitment::schema_commitment_slot(); + let commitment_a = account_a.storage().get_item(slot_name).unwrap(); + let commitment_b = account_b.storage().get_item(slot_name).unwrap(); + + assert_eq!(commitment_a, commitment_b); + } + + #[test] + fn storage_schema_commitment_is_empty_for_no_schemas() { + let component = AccountSchemaCommitment::new(&[]).unwrap(); + + assert_eq!(component.schema_commitment, Word::empty()); + } + + #[test] + fn build_with_schema_commitment_adds_schema_commitment_component() { + let auth_component = AuthSingleSig::new( + PublicKeyCommitment::from(Word::empty()), + AuthScheme::EcdsaK256Keccak, + ); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(auth_component) + .build_with_schema_commitment() + .unwrap(); + + // The auth component has 2 slots (public key and scheme ID) and the schema commitment adds + // 1 more. + assert_eq!(account.storage().num_slots(), 3); + + // The auth component's public key slot should be accessible. + assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok()); + + // The schema commitment slot should be non-empty since we have a component with a schema. + let slot_name = AccountSchemaCommitment::schema_commitment_slot(); + let commitment = account.storage().get_item(slot_name).unwrap(); + assert_ne!(commitment, Word::empty()); + } +} diff --git a/crates/miden-standards/src/account/metadata/token_metadata.rs b/crates/miden-standards/src/account/metadata/token_metadata.rs new file mode 100644 index 0000000000..17502e8ff6 --- /dev/null +++ b/crates/miden-standards/src/account/metadata/token_metadata.rs @@ -0,0 +1,271 @@ +//! Generic token metadata helper. +//! +//! [`TokenMetadata`] is a builder-pattern struct used to manage name and optional fields +//! (description, logo_uri, external_link) with their mutability flags in fixed value slots. +//! It is intended to be embedded inside [`FungibleTokenMetadata`] rather than used as a +//! standalone [`AccountComponent`]. +//! +//! Ownership is handled by the `Ownable2Step` component. + +use alloc::vec::Vec; + +use miden_protocol::Word; +use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; + +use super::{ + DESCRIPTION_SLOTS, + EXTERNAL_LINK_SLOTS, + LOGO_URI_SLOTS, + NAME_SLOTS, + mutability_config_slot, +}; +use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; + +// TOKEN METADATA +// ================================================================================================ + +/// A helper that stores name, mutability config, and optional fields in fixed value slots. +/// +/// Designed to be embedded in [`FungibleTokenMetadata`] to avoid duplication. Slot names are +/// shared via the static accessors (e.g. [`name_chunk_0_slot`]). +/// +/// ## Storage Layout +/// +/// - Slot 0–1: name (2 Words = 8 felts) +/// - Slot 2: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +/// - Slot 3–9: description (7 Words) +/// - Slot 10–16: logo_uri (7 Words) +/// - Slot 17–23: external_link (7 Words) +/// +/// [`FungibleTokenMetadata`]: crate::account::faucets::FungibleTokenMetadata +/// [`name_chunk_0_slot`]: TokenMetadata::name_chunk_0_slot +#[derive(Debug, Clone, Default)] +pub struct TokenMetadata { + name: Option, + description: Option, + logo_uri: Option, + external_link: Option, + description_mutable: bool, + logo_uri_mutable: bool, + external_link_mutable: bool, + max_supply_mutable: bool, +} + +impl TokenMetadata { + /// Creates a new empty token metadata (all fields absent, all flags false). + pub fn new() -> Self { + Self::default() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Sets the token name. + pub fn with_name(mut self, name: TokenName) -> Self { + self.name = Some(name); + self + } + + /// Sets the description and its mutability flag together. + pub fn with_description(mut self, description: Description, mutable: bool) -> Self { + self.description = Some(description); + self.description_mutable = mutable; + self + } + + /// Sets whether the description can be updated by the owner. + pub fn with_description_mutable(mut self, mutable: bool) -> Self { + self.description_mutable = mutable; + self + } + + /// Sets the logo URI and its mutability flag together. + pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self { + self.logo_uri = Some(logo_uri); + self.logo_uri_mutable = mutable; + self + } + + /// Sets whether the logo URI can be updated by the owner. + pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self { + self.logo_uri_mutable = mutable; + self + } + + /// Sets the external link and its mutability flag together. + pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self { + self.external_link = Some(external_link); + self.external_link_mutable = mutable; + self + } + + /// Sets whether the external link can be updated by the owner. + pub fn with_external_link_mutable(mut self, mutable: bool) -> Self { + self.external_link_mutable = mutable; + self + } + + /// Sets whether the max supply can be updated by the owner. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the token name if set. + pub fn name(&self) -> Option<&TokenName> { + self.name.as_ref() + } + + /// Returns the description if set. + pub fn description(&self) -> Option<&Description> { + self.description.as_ref() + } + + /// Returns the logo URI if set. + pub fn logo_uri(&self) -> Option<&LogoURI> { + self.logo_uri.as_ref() + } + + /// Returns the external link if set. + pub fn external_link(&self) -> Option<&ExternalLink> { + self.external_link.as_ref() + } + + // STATIC SLOT NAME ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] for name chunk 0. + pub fn name_chunk_0_slot() -> &'static StorageSlotName { + &NAME_SLOTS[0] + } + + /// Returns the [`StorageSlotName`] for name chunk 1. + pub fn name_chunk_1_slot() -> &'static StorageSlotName { + &NAME_SLOTS[1] + } + + /// Returns the [`StorageSlotName`] for a description chunk by index (0..7). + pub fn description_slot(index: usize) -> &'static StorageSlotName { + &DESCRIPTION_SLOTS[index] + } + + /// Returns the [`StorageSlotName`] for a logo URI chunk by index (0..7). + pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { + &LOGO_URI_SLOTS[index] + } + + /// Returns the [`StorageSlotName`] for an external link chunk by index (0..7). + pub fn external_link_slot(index: usize) -> &'static StorageSlotName { + &EXTERNAL_LINK_SLOTS[index] + } + + // STORAGE + // -------------------------------------------------------------------------------------------- + + /// Reads the name and optional metadata fields from account storage. + /// + /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if + /// at least one word is non-zero. Decoding errors cause the field to be returned as `None`. + pub fn read_metadata_from_storage( + storage: &AccountStorage, + ) -> (Option, Option, Option, Option) { + let name = if let (Ok(chunk_0), Ok(chunk_1)) = ( + storage.get_item(TokenMetadata::name_chunk_0_slot()), + storage.get_item(TokenMetadata::name_chunk_1_slot()), + ) { + let words: [Word; 2] = [chunk_0, chunk_1]; + if words != [Word::default(); 2] { + TokenName::try_from_words(&words).ok() + } else { + None + } + } else { + None + }; + + let read_field = |slots: &[StorageSlotName; 7]| -> Option<[Word; 7]> { + let mut field = [Word::default(); 7]; + let mut any_set = false; + for (i, slot) in field.iter_mut().enumerate() { + if let Ok(chunk) = storage.get_item(&slots[i]) { + *slot = chunk; + if chunk != Word::default() { + any_set = true; + } + } + } + if any_set { Some(field) } else { None } + }; + + let description = + read_field(&DESCRIPTION_SLOTS).and_then(|w| Description::try_from_words(&w).ok()); + let logo_uri = read_field(&LOGO_URI_SLOTS).and_then(|w| LogoURI::try_from_words(&w).ok()); + let external_link = + read_field(&EXTERNAL_LINK_SLOTS).and_then(|w| ExternalLink::try_from_words(&w).ok()); + + (name, description, logo_uri, external_link) + } + + /// Returns the storage slots for this metadata (name, mutability config, and all fields). + pub fn storage_slots(&self) -> Vec { + let mut slots: Vec = Vec::new(); + + let name_words = self + .name + .as_ref() + .map(|n| n.to_words()) + .unwrap_or_else(|| (0..2).map(|_| Word::default()).collect()); + 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], + )); + + let mutability_config_word = Word::from([ + miden_protocol::Felt::from(self.description_mutable as u32), + miden_protocol::Felt::from(self.logo_uri_mutable as u32), + miden_protocol::Felt::from(self.external_link_mutable as u32), + miden_protocol::Felt::from(self.max_supply_mutable as u32), + ]); + slots.push(StorageSlot::with_value( + mutability_config_slot().clone(), + mutability_config_word, + )); + + let desc_words: Vec = self + .description + .as_ref() + .map(|d| d.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); + for (i, word) in desc_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); + } + + let logo_words: Vec = self + .logo_uri + .as_ref() + .map(|l| l.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); + for (i, word) in logo_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); + } + + let link_words: Vec = self + .external_link + .as_ref() + .map(|e| e.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); + for (i, word) in link_words.iter().enumerate() { + slots + .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word)); + } + + slots + } +} diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index ad7b67f1f9..93a896f2db 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -3,6 +3,7 @@ use super::auth_method::AuthMethod; pub mod access; pub mod auth; pub mod components; +pub mod encoding; pub mod faucets; pub mod interface; pub mod metadata; diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 84dc5ba520..93c70424c4 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -47,7 +47,15 @@ use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel}; use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; use miden_standards::account::access::Ownable2Step; -use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + Description, + ExternalLink, + FungibleTokenMetadata, + LogoURI, + NetworkFungibleFaucet, + TokenName, +}; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; use miden_standards::testing::account_component::MockAccountComponent; @@ -322,17 +330,26 @@ impl MockChainBuilder { token_symbol: &str, max_supply: u64, ) -> anyhow::Result { + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; - let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt) - .context("failed to create BasicFungibleFaucet")?; + let metadata = FungibleTokenMetadata::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply_felt, + name, + None, + None, + None, + ) + .context("failed to create FungibleTokenMetadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) .account_type(AccountType::FungibleFaucet) - .with_component(basic_faucet); + .with_component(metadata) + .with_component(BasicFungibleFaucet); self.add_account_from_builder(auth_method, account_builder, AccountState::New) } @@ -350,17 +367,25 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - - let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create basic fungible faucet")?; + let metadata = FungibleTokenMetadata::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply, + name, + None, + None, + None, + ) + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) - .with_component(basic_faucet) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .account_type(AccountType::FungibleFaucet); self.add_account_from_builder(auth_method, account_builder, AccountState::Exists) @@ -378,17 +403,26 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let network_faucet = - NetworkFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")?; + let metadata = FungibleTokenMetadata::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply, + name, + None, + None, + None, + ) + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) - .with_component(network_faucet) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) .with_component(Ownable2Step::new(owner_account_id)) .account_type(AccountType::FungibleFaucet); @@ -396,6 +430,65 @@ impl MockChainBuilder { self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) } + /// Adds an existing network fungible faucet account with a metadata Info component + /// (for testing metadata::fungible procedures: owner can update description / logo_uri / + /// external_link / max supply when mutable). + #[allow(clippy::too_many_arguments)] + pub fn add_existing_network_faucet_with_metadata_info( + &mut self, + token_symbol: &str, + max_supply: u64, + owner_account_id: AccountId, + token_supply: Option, + max_supply_mutable: bool, + description: Option<([Word; 7], bool)>, + logo_uri: Option<([Word; 7], bool)>, + external_link: Option<([Word; 7], bool)>, + ) -> anyhow::Result { + let max_supply = Felt::try_from(max_supply)?; + let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); + let token_symbol = + TokenSymbol::new(token_symbol).context("failed to create token symbol")?; + + let mut metadata = FungibleTokenMetadata::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply, + name, + description.map(|(words, _)| { + Description::try_from_words(&words).expect("valid description words") + }), + logo_uri + .map(|(words, _)| LogoURI::try_from_words(&words).expect("valid logo_uri words")), + external_link.map(|(words, _)| { + ExternalLink::try_from_words(&words).expect("valid external_link words") + }), + ) + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")? + .with_max_supply_mutable(max_supply_mutable); + + if let Some((_, mutable)) = description { + metadata = metadata.with_description_mutable(mutable); + } + if let Some((_, mutable)) = logo_uri { + metadata = metadata.with_logo_uri_mutable(mutable); + } + if let Some((_, mutable)) = external_link { + metadata = metadata.with_external_link_mutable(mutable); + } + + let account_builder = AccountBuilder::new(self.rng.random()) + .storage_mode(AccountStorageMode::Network) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) + .with_component(Ownable2Step::new(owner_account_id)) + .account_type(AccountType::FungibleFaucet); + + self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) + } + /// Creates a new public account with an [`MockAccountComponent`] and registers the /// authenticator (if any). pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result { diff --git a/crates/miden-testing/src/standards/mod.rs b/crates/miden-testing/src/standards/mod.rs index 76cf06a85d..b0f8c808a7 100644 --- a/crates/miden-testing/src/standards/mod.rs +++ b/crates/miden-testing/src/standards/mod.rs @@ -1,2 +1,3 @@ mod network_account_target; mod note_tag; +mod token_metadata; diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs new file mode 100644 index 0000000000..5c9af4fedf --- /dev/null +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -0,0 +1,1257 @@ +//! Integration tests for the Token Metadata standard (`FungibleTokenMetadata`). + +extern crate alloc; + +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_crypto::hash::rpo::Rpo256; +use miden_crypto::rand::RpoRandomCoin; +use miden_protocol::account::{ + AccountBuilder, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, +}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::errors::MasmError; +use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::access::Ownable2Step; +use miden_standards::account::auth::NoAuth; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + Description, + ExternalLink, + FungibleTokenMetadata, + LogoURI, + NetworkFungibleFaucet, + TokenName, +}; +use miden_standards::account::metadata::TokenMetadata; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_DESCRIPTION_NOT_MUTABLE, + ERR_EXTERNAL_LINK_NOT_MUTABLE, + ERR_LOGO_URI_NOT_MUTABLE, + ERR_MAX_SUPPLY_IMMUTABLE, + ERR_SENDER_NOT_OWNER, +}; +use miden_standards::testing::note::NoteBuilder; + +use crate::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; + +// SHARED HELPERS +// ================================================================================================ + +fn initial_field_data() -> [Word; 7] { + [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), + ] +} + +fn new_field_data() -> [Word; 7] { + [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ] +} + +fn owner_account_id() -> AccountId { + AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ) +} + +fn non_owner_account_id() -> AccountId { + AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ) +} + +/// Build a minimal faucet metadata (no optional fields). +fn build_faucet_metadata() -> FungibleTokenMetadata { + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + None, + ) + .unwrap() +} + +/// Build a standard POL faucet metadata (used by scalar getter tests). +fn build_pol_faucet_metadata() -> FungibleTokenMetadata { + FungibleTokenMetadata::new( + TokenSymbol::new("POL").unwrap(), + 8, + Felt::new(1_000_000), + TokenName::new("POL").unwrap(), + None, + None, + None, + ) + .unwrap() +} + +/// Build a basic faucet account with POL metadata. +fn build_pol_faucet_account() -> miden_protocol::account::Account { + AccountBuilder::new([4u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(build_pol_faucet_metadata()) + .with_component(BasicFungibleFaucet) + .build() + .unwrap() +} + +/// Flatten `[Word; 7]` into `Vec` for advice map values. +fn field_advice_map_value(field: &[Word; 7]) -> Vec { + let mut value = Vec::with_capacity(28); + for word in field.iter() { + value.extend(word.iter()); + } + value +} + +/// Compute the Rpo256 hash of the field data (used as the advice map key). +fn compute_field_hash(data: &[Word; 7]) -> Word { + let felts = field_advice_map_value(data); + Word::from(*Rpo256::hash_elements(&felts)) +} + +/// Execute a tx script against the given account and assert success. +async fn execute_tx_script( + account: miden_protocol::account::Account, + tx_script_code: impl AsRef, +) -> anyhow::Result<()> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(tx_script_code.as_ref())?; + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + tx_context.execute().await?; + Ok(()) +} + +// ================================================================================================= +// GETTER TESTS – name +// ================================================================================================= + +#[tokio::test] +async fn get_name_from_masm() -> anyhow::Result<()> { + let token_name = TokenName::new("test name").unwrap(); + let name = token_name.to_words(); + + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + token_name, + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="name chunk 0 does not match" + push.{n1} + assert_eqw.err="name chunk 1 does not match" + end + "#, + n0 = name[0], + n1 = name[1], + ), + ) + .await +} + +#[tokio::test] +async fn get_name_zeros_returns_empty() -> anyhow::Result<()> { + // Build a faucet with an empty name to verify get_name returns zero words. + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::default(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + r#" + begin + call.::miden::standards::metadata::fungible::get_name + padw assert_eqw.err="name chunk 0 should be empty" + padw assert_eqw.err="name chunk 1 should be empty" + end + "#, + ) + .await +} + +// ================================================================================================= +// GETTER TESTS – scalar fields +// ================================================================================================= + +#[tokio::test] +async fn faucet_get_decimals() -> anyhow::Result<()> { + let expected = Felt::from(8u8).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_decimals + push.{expected} assert_eq.err="decimals does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_symbol() -> anyhow::Result<()> { + let expected = Felt::from(TokenSymbol::new("POL").unwrap()).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_symbol + push.{expected} assert_eq.err="token_symbol does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_supply() -> anyhow::Result<()> { + execute_tx_script( + build_pol_faucet_account(), + r#" + begin + call.::miden::standards::metadata::fungible::get_token_supply + push.0 assert_eq.err="token_supply does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "#, + ) + .await +} + +#[tokio::test] +async fn faucet_get_max_supply() -> anyhow::Result<()> { + let expected = Felt::new(1_000_000).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_max_supply + push.{expected} assert_eq.err="max_supply does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_metadata() -> anyhow::Result<()> { + let symbol = TokenSymbol::new("POL").unwrap(); + let expected_symbol = Felt::from(symbol).as_canonical_u64(); + let expected_decimals = Felt::from(8u8).as_canonical_u64(); + let expected_max_supply = Felt::new(1_000_000).as_canonical_u64(); + + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_metadata + push.0 assert_eq.err="token_supply does not match" + push.{expected_max_supply} assert_eq.err="max_supply does not match" + push.{expected_decimals} assert_eq.err="decimals does not match" + push.{expected_symbol} assert_eq.err="token_symbol does not match" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_decimals_symbol_and_max_supply() -> anyhow::Result<()> { + let symbol = TokenSymbol::new("POL").unwrap(); + let expected_decimals = Felt::from(8u8).as_canonical_u64(); + let expected_symbol = Felt::from(symbol).as_canonical_u64(); + let expected_max_supply = Felt::new(1_000_000).as_canonical_u64(); + + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_decimals + push.{expected_decimals} assert_eq.err="decimals does not match" + push.0 + call.::miden::standards::metadata::fungible::get_token_symbol + push.{expected_symbol} assert_eq.err="token_symbol does not match" + push.0 + call.::miden::standards::metadata::fungible::get_max_supply + push.{expected_max_supply} assert_eq.err="max_supply does not match" + end + "# + ), + ) + .await +} + +// ================================================================================================= +// GETTER TESTS – mutability config +// ================================================================================================= + +#[tokio::test] +async fn get_mutability_config() -> anyhow::Result<()> { + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(Description::new("test").unwrap()), + None, + None, + ) + .unwrap() + .with_description_mutable(true) + .with_max_supply_mutable(true); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + r#" + begin + call.::miden::standards::metadata::fungible::get_mutability_config + push.1 assert_eq.err="desc_mutable should be 1" + push.0 assert_eq.err="logo_mutable should be 0" + push.0 assert_eq.err="extlink_mutable should be 0" + push.1 assert_eq.err="max_supply_mutable should be 1" + end + "#, + ) + .await +} + +/// Tests all `is_*_mutable` procedures with flag=0 and flag=1. +#[tokio::test] +async fn is_field_mutable_checks() -> anyhow::Result<()> { + let desc = Description::new("test").unwrap(); + let logo = LogoURI::new("https://example.com/logo").unwrap(); + let link = ExternalLink::new("https://example.com").unwrap(); + + // (metadata_builder, proc_name, expected_value) + let cases: Vec<(FungibleTokenMetadata, &str, u8)> = vec![ + ( + build_faucet_metadata().with_max_supply_mutable(true), + "is_max_supply_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(desc.clone()), + None, + None, + ) + .unwrap() + .with_description_mutable(true), + "is_description_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(desc), + None, + None, + ) + .unwrap(), + "is_description_mutable", + 0, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + Some(logo.clone()), + None, + ) + .unwrap() + .with_logo_uri_mutable(true), + "is_logo_uri_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + Some(logo), + None, + ) + .unwrap(), + "is_logo_uri_mutable", + 0, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + Some(link.clone()), + ) + .unwrap() + .with_external_link_mutable(true), + "is_external_link_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + Some(link), + ) + .unwrap(), + "is_external_link_mutable", + 0, + ), + ]; + + for (metadata, proc_name, expected) in cases { + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + "begin + call.::miden::standards::metadata::fungible::{proc_name} + push.{expected} + assert_eq.err=\"{proc_name} returned unexpected value\" + end" + ), + ) + .await?; + } + + Ok(()) +} + +// ================================================================================================= +// STORAGE LAYOUT TESTS +// ================================================================================================= + +#[test] +fn faucet_with_metadata_storage_layout() { + let token_name = TokenName::new("test faucet name").unwrap(); + let desc_text = "faucet description text for testing"; + let description = Description::new(desc_text).unwrap(); + let desc_words = description.to_words(); + + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 8, + Felt::new(1_000_000), + token_name, + Some(description), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + + // Verify faucet metadata + let faucet_metadata = + account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[0], Felt::new(0)); + assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); + assert_eq!(faucet_metadata[2], Felt::new(8)); + + // Verify description + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +#[test] +fn name_32_bytes_accepted() { + let max_name = "a".repeat(TokenName::MAX_BYTES); + let token_name = TokenName::new(&max_name).unwrap(); + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + token_name, + None, + None, + None, + ) + .unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); + let decoded = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded.as_str(), max_name); +} + +#[test] +fn name_33_bytes_rejected() { + let result = TokenName::new(&"a".repeat(33)); + assert!(matches!( + result, + Err(miden_standards::account::metadata::NameUtf8Error::TooLong(33)) + )); +} + +#[test] +fn description_7_words_full_capacity() { + let desc_text = "a".repeat(Description::MAX_BYTES); + let description = Description::new(&desc_text).unwrap(); + let desc_words = description.to_words(); + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(description), + None, + None, + ) + .unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +// ================================================================================================= +// FAUCET INITIALIZATION – basic + network with max name/description +// ================================================================================================= + +fn verify_faucet_with_max_name_and_description( + seed: [u8; 32], + symbol: &str, + max_supply: u64, + storage_mode: AccountStorageMode, + extra_components: Vec, +) { + let max_name = "a".repeat(TokenName::MAX_BYTES); + let desc_text = "a".repeat(Description::MAX_BYTES); + let description = Description::new(&desc_text).unwrap(); + let desc_words = description.to_words(); + + let faucet_metadata = FungibleTokenMetadata::new( + symbol.try_into().unwrap(), + 6, + Felt::new(max_supply), + TokenName::new(&max_name).unwrap(), + Some(description), + None, + None, + ) + .unwrap(); + + let mut builder = AccountBuilder::new(seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(storage_mode) + .with_auth_component(NoAuth) + .with_component(faucet_metadata); + + for comp in extra_components { + builder = builder.with_component(comp); + } + + let account = builder.build().unwrap(); + + let name_words = TokenName::new(&max_name).unwrap().to_words(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + let faucet_metadata_val = + account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata_val[1], Felt::new(max_supply)); +} + +#[test] +fn basic_faucet_with_max_name_and_full_description() { + verify_faucet_with_max_name_and_description( + [5u8; 32], + "MAX", + 1_000_000, + AccountStorageMode::Public, + vec![BasicFungibleFaucet.into()], + ); +} + +#[test] +fn network_faucet_with_max_name_and_full_description() { + verify_faucet_with_max_name_and_description( + [6u8; 32], + "NET", + 2_000_000, + AccountStorageMode::Network, + vec![NetworkFungibleFaucet.into(), Ownable2Step::new(owner_account_id()).into()], + ); +} + +// ================================================================================================= +// MASM NAME READBACK – basic + network faucets +// ================================================================================================= + +#[tokio::test] +async fn basic_faucet_name_readable_from_masm() -> anyhow::Result<()> { + let token_name = TokenName::new("readable name").unwrap(); + let name = token_name.to_words(); + + let faucet_metadata = FungibleTokenMetadata::new( + "MAS".try_into().unwrap(), + 10, + Felt::new(999_999), + token_name, + Some(Description::new("readable description").unwrap()), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([3u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet_metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="faucet name chunk 0 does not match" + push.{n1} + assert_eqw.err="faucet name chunk 1 does not match" + end + "#, + n0 = name[0], + n1 = name[1], + ), + ) + .await +} + +#[tokio::test] +async fn network_faucet_name_readable_from_masm() -> anyhow::Result<()> { + let max_name = "b".repeat(TokenName::MAX_BYTES); + let name_words = TokenName::new(&max_name).unwrap().to_words(); + + let network_faucet_metadata = FungibleTokenMetadata::new( + "MAS".try_into().unwrap(), + 6, + Felt::new(1_000_000), + TokenName::new(&max_name).unwrap(), + Some(Description::new("network faucet description").unwrap()), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([7u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_auth_component(NoAuth) + .with_component(network_faucet_metadata) + .with_component(NetworkFungibleFaucet) + .with_component(Ownable2Step::new(owner_account_id())) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="network faucet name chunk 0 does not match" + push.{n1} + assert_eqw.err="network faucet name chunk 1 does not match" + end + "#, + n0 = name_words[0], + n1 = name_words[1], + ), + ) + .await +} + +// ================================================================================================= +// SETTER TESTS – set_description, set_logo_uri, set_external_link (parameterised) +// ================================================================================================= + +struct FieldSetterFaucetArgs { + description: Option<([Word; 7], bool)>, + logo_uri: Option<([Word; 7], bool)>, + external_link: Option<([Word; 7], bool)>, +} + +fn description_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: Some((data, mutable)), + logo_uri: None, + external_link: None, + } +} + +fn logo_uri_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: None, + logo_uri: Some((data, mutable)), + external_link: None, + } +} + +fn external_link_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: None, + logo_uri: None, + external_link: Some((data, mutable)), + } +} + +async fn test_field_setter_immutable_fails( + proc_name: &str, + immutable_error: MasmError, + args: FieldSetterFaucetArgs, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let tx_script_code = format!( + r#" + begin + call.::miden::standards::metadata::fungible::{proc_name} + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(&tx_script_code)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, immutable_error); + + Ok(()) +} + +async fn test_field_setter_owner_succeeds( + proc_name: &str, + args: FieldSetterFaucetArgs, + slot_fn: fn(usize) -> &'static miden_protocol::account::StorageSlotName, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let new_data = new_field_data(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let hash = compute_field_hash(&new_data); + + let note_script_code = format!( + r#" + begin + push.{h0}.{h1}.{h2}.{h3} + call.::miden::standards::metadata::fungible::{proc_name} + end +"#, + h0 = hash[0].as_canonical_u64(), + h1 = hash[1].as_canonical_u64(), + h2 = hash[2].as_canonical_u64(), + h3 = hash[3].as_canonical_u64(), + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let note = NoteBuilder::new(owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([7, 8, 9, 10u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .extend_advice_map([(hash, field_advice_map_value(&new_data))]) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + for (i, expected) in new_data.iter().enumerate() { + let chunk = updated_faucet.storage().get_item(slot_fn(i))?; + assert_eq!(chunk, *expected, "field chunk {i} should be updated"); + } + + Ok(()) +} + +async fn test_field_setter_non_owner_fails( + proc_name: &str, + args: FieldSetterFaucetArgs, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let non_owner = non_owner_account_id(); + let new_data = new_field_data(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let hash = compute_field_hash(&new_data); + + let note_script_code = format!( + r#" + begin + push.{h0}.{h1}.{h2}.{h3} + call.::miden::standards::metadata::fungible::{proc_name} + end +"#, + h0 = hash[0].as_canonical_u64(), + h1 = hash[1].as_canonical_u64(), + h2 = hash[2].as_canonical_u64(), + h3 = hash[3].as_canonical_u64(), + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let note = NoteBuilder::new(non_owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([11, 12, 13, 14u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .extend_advice_map([(hash, field_advice_map_value(&new_data))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} + +// --- set_description --- + +#[tokio::test] +async fn set_description_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_description", + ERR_DESCRIPTION_NOT_MUTABLE, + description_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_description", + description_config(initial_field_data(), true), + TokenMetadata::description_slot, + ) + .await +} + +#[tokio::test] +async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails( + "set_description", + description_config(initial_field_data(), true), + ) + .await +} + +// --- set_logo_uri --- + +#[tokio::test] +async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_logo_uri", + ERR_LOGO_URI_NOT_MUTABLE, + logo_uri_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_logo_uri", + logo_uri_config(initial_field_data(), true), + TokenMetadata::logo_uri_slot, + ) + .await +} + +#[tokio::test] +async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails("set_logo_uri", logo_uri_config(initial_field_data(), true)) + .await +} + +// --- set_external_link --- + +#[tokio::test] +async fn set_external_link_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_external_link", + ERR_EXTERNAL_LINK_NOT_MUTABLE, + external_link_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_external_link", + external_link_config(initial_field_data(), true), + TokenMetadata::external_link_slot, + ) + .await +} + +#[tokio::test] +async fn set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails( + "set_external_link", + external_link_config(initial_field_data(), true), + ) + .await +} + +// ================================================================================================= +// SETTER TESTS – set_max_supply +// ================================================================================================= + +#[tokio::test] +async fn set_max_supply_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + false, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let tx_script_code = r#" + begin + push.2000 + call.::miden::standards::metadata::fungible::set_max_supply + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(tx_script_code)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_MAX_SUPPLY_IMMUTABLE); + + Ok(()) +} + +#[tokio::test] +async fn set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let new_max_supply: u64 = 2000; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + true, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let note = NoteBuilder::new(owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([20, 21, 22, 23u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + let metadata_word = updated_faucet.storage().get_item(FungibleTokenMetadata::metadata_slot())?; + assert_eq!(metadata_word[1], Felt::new(new_max_supply), "max_supply should be updated"); + assert_eq!(metadata_word[0], Felt::new(0), "token_supply should remain unchanged"); + + Ok(()) +} + +#[tokio::test] +async fn set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let non_owner = non_owner_account_id(); + let new_max_supply: u64 = 2000; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + true, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let note = NoteBuilder::new(non_owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([30, 31, 32, 33u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index ffb8b3ab19..bd9246108a 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -23,7 +23,7 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{NoteAssets, NoteScript, NoteType}; use miden_protocol::transaction::RawOutputNote; -use miden_standards::account::faucets::TokenMetadata; +use miden_standards::account::faucets::FungibleTokenMetadata; use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use miden_tx::utils::hex_to_bytes; @@ -264,7 +264,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { // STEP 3: CONSUME ALL BURN NOTES WITH THE AGGLAYER FAUCET // -------------------------------------------------------------------------------------------- - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( initial_token_supply, Felt::new(total_burned), @@ -288,7 +288,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { mock_chain.prove_next_block()?; } - let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let final_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( final_token_supply, Felt::new(initial_token_supply.as_canonical_u64() - total_burned), diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index dd80e4f73e..5b4d7a619b 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -31,8 +31,8 @@ use miden_protocol::{Felt, Word}; use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{ BasicFungibleFaucet, + FungibleTokenMetadata, NetworkFungibleFaucet, - TokenMetadata, }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ @@ -285,7 +285,7 @@ async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::R builder.add_output_note(RawOutputNote::Full(note.clone())); let mock_chain = builder.build()?; - let token_metadata = TokenMetadata::try_from(faucet.storage())?; + let token_metadata = FungibleTokenMetadata::try_from(faucet.storage())?; // Check that max_supply at the word's index 0 is 200. The remainder of the word is initialized // with the metadata of the faucet which we don't need to check. @@ -566,7 +566,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { let mut target_account = builder.add_existing_wallet(Auth::IncrNonce)?; // Check the Network Fungible Faucet's max supply. - let actual_max_supply = TokenMetadata::try_from(faucet.storage())?.max_supply(); + let actual_max_supply = FungibleTokenMetadata::try_from(faucet.storage())?.max_supply(); assert_eq!(actual_max_supply.as_canonical_u64(), max_supply); // Check that the creator account ID is stored in the ownership slot. @@ -582,7 +582,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { // Check that the faucet's token supply has been correctly initialized. // The already issued amount should be 50. - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!(initial_token_supply.as_canonical_u64(), token_supply); // CREATE MINT NOTE USING STANDARD NOTE @@ -1168,7 +1168,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { mock_chain.prove_next_block()?; // Check the initial token issuance before burning - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!(initial_token_supply, Felt::new(100)); // EXECUTE BURN NOTE AGAINST NETWORK FAUCET @@ -1185,7 +1185,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { // Apply the delta to the faucet account and verify the token issuance decreased faucet.apply_delta(executed_transaction.account_delta())?; - let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let final_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( final_token_supply, Felt::new(initial_token_supply.as_canonical_u64() - burn_amount)