use alloc::vec::Vec;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
FeltSchema,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
Account,
AccountBuilder,
AccountCodeInterface,
AccountComponent,
AccountComponentName,
AccountProcedureRoot,
AccountStorage,
AccountType,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::TokenSymbol;
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Hasher, Word};
use super::{
Description,
ExternalLink,
LogoURI,
NonFungibleFaucetError,
TokenMetadata,
TokenMetadataError,
TokenName,
};
use crate::account::access::{AccessControl, Authority, Pausable, PausableManager};
use crate::account::account_component_code;
use crate::account::auth::{AuthSingleSig, NetworkAccount};
use crate::account::faucets::registers_owner_only_policy;
use crate::account::fees::FeePolicyManager;
use crate::account::policies::TokenPolicyManager;
use crate::note::{BurnNote, MintNote};
use crate::procedure_root;
#[cfg(test)]
mod tests;
pub(crate) static SYMBOL_SLOT: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::faucets::non_fungible::symbol")
.expect("storage slot name should be valid")
});
pub(crate) static ASSET_STATUS_SLOT: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::faucets::non_fungible::asset_status")
.expect("storage slot name should be valid")
});
const STATUS_ISSUED: u8 = 1;
const STATUS_BURNED: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetStatus {
NotIssued,
Issued,
Burned,
}
impl TryFrom<u8> for AssetStatus {
type Error = NonFungibleFaucetError;
fn try_from(status: u8) -> Result<Self, Self::Error> {
match status {
0 => Ok(Self::NotIssued),
STATUS_ISSUED => Ok(Self::Issued),
STATUS_BURNED => Ok(Self::Burned),
other => Err(NonFungibleFaucetError::InvalidAssetStatus { status: other.into() }),
}
}
}
account_component_code!(
NON_FUNGIBLE_FAUCET_CODE,
"miden-standards-faucets-non-fungible-faucet.masp"
);
const NON_FUNGIBLE_FAUCET_LIBRARY_PATH: &str =
"miden::standards::components::faucets::non_fungible_faucet";
procedure_root!(
NON_FUNGIBLE_FAUCET_MINT_AND_SEND,
NON_FUNGIBLE_FAUCET_LIBRARY_PATH,
NonFungibleFaucet::MINT_AND_SEND_PROC_NAME,
NonFungibleFaucet::code()
);
procedure_root!(
NON_FUNGIBLE_FAUCET_RECEIVE_AND_BURN,
NON_FUNGIBLE_FAUCET_LIBRARY_PATH,
NonFungibleFaucet::RECEIVE_AND_BURN_PROC_NAME,
NonFungibleFaucet::code()
);
procedure_root!(
NON_FUNGIBLE_FAUCET_SET_DESCRIPTION,
NON_FUNGIBLE_FAUCET_LIBRARY_PATH,
NonFungibleFaucet::SET_DESCRIPTION_PROC_NAME,
NonFungibleFaucet::code()
);
procedure_root!(
NON_FUNGIBLE_FAUCET_SET_LOGO_URI,
NON_FUNGIBLE_FAUCET_LIBRARY_PATH,
NonFungibleFaucet::SET_LOGO_URI_PROC_NAME,
NonFungibleFaucet::code()
);
procedure_root!(
NON_FUNGIBLE_FAUCET_SET_CONTRACT_URI,
NON_FUNGIBLE_FAUCET_LIBRARY_PATH,
NonFungibleFaucet::SET_CONTRACT_URI_PROC_NAME,
NonFungibleFaucet::code()
);
#[derive(Debug, Clone)]
pub struct NonFungibleFaucet {
symbol: TokenSymbol,
metadata: TokenMetadata,
}
#[bon::bon]
impl NonFungibleFaucet {
#[builder]
pub fn new(
name: TokenName,
symbol: TokenSymbol,
description: Option<Description>,
logo_uri: Option<LogoURI>,
contract_uri: Option<ExternalLink>,
#[builder(default)] is_description_mutable: bool,
#[builder(default)] is_logo_uri_mutable: bool,
#[builder(default)] is_contract_uri_mutable: bool,
) -> NonFungibleFaucet {
let mut metadata = TokenMetadata::new(name);
if let Some(desc) = description {
metadata = metadata.with_description(desc, is_description_mutable);
} else {
metadata = metadata.with_description_mutable(is_description_mutable);
}
if let Some(uri) = logo_uri {
metadata = metadata.with_logo_uri(uri, is_logo_uri_mutable);
} else {
metadata = metadata.with_logo_uri_mutable(is_logo_uri_mutable);
}
if let Some(link) = contract_uri {
metadata = metadata.with_external_link(link, is_contract_uri_mutable);
} else {
metadata = metadata.with_external_link_mutable(is_contract_uri_mutable);
}
Self { symbol, metadata }
}
}
impl NonFungibleFaucet {
pub const NAME: &'static str = "miden::standards::faucets::non_fungible";
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
const MINT_AND_SEND_PROC_NAME: &'static str = "mint_and_send";
const RECEIVE_AND_BURN_PROC_NAME: &'static str = "receive_and_burn";
const SET_DESCRIPTION_PROC_NAME: &'static str = "set_description";
const SET_LOGO_URI_PROC_NAME: &'static str = "set_logo_uri";
const SET_CONTRACT_URI_PROC_NAME: &'static str = "set_contract_uri";
pub fn code() -> &'static AccountComponentCode {
&NON_FUNGIBLE_FAUCET_CODE
}
pub fn mint_and_send_root() -> AccountProcedureRoot {
*NON_FUNGIBLE_FAUCET_MINT_AND_SEND
}
pub fn receive_and_burn_root() -> AccountProcedureRoot {
*NON_FUNGIBLE_FAUCET_RECEIVE_AND_BURN
}
pub fn set_description_root() -> AccountProcedureRoot {
*NON_FUNGIBLE_FAUCET_SET_DESCRIPTION
}
pub fn set_logo_uri_root() -> AccountProcedureRoot {
*NON_FUNGIBLE_FAUCET_SET_LOGO_URI
}
pub fn set_contract_uri_root() -> AccountProcedureRoot {
*NON_FUNGIBLE_FAUCET_SET_CONTRACT_URI
}
pub fn symbol_slot() -> &'static StorageSlotName {
&SYMBOL_SLOT
}
pub fn asset_status_slot() -> &'static StorageSlotName {
&ASSET_STATUS_SLOT
}
pub fn symbol(&self) -> &TokenSymbol {
&self.symbol
}
pub fn token_name(&self) -> &TokenName {
self.metadata.name()
}
pub fn description(&self) -> Option<&Description> {
self.metadata.description()
}
pub fn logo_uri(&self) -> Option<&LogoURI> {
self.metadata.logo_uri()
}
pub fn contract_uri(&self) -> Option<&ExternalLink> {
self.metadata.external_link()
}
pub fn compute_asset_commitment(user_data: &[u8], salt: Word) -> Word {
let data_digest = Hasher::hash(user_data);
Hasher::merge(&[data_digest, salt])
}
pub fn get_asset_status(
storage: &AccountStorage,
asset_commitment: Word,
) -> Result<AssetStatus, NonFungibleFaucetError> {
let key = StorageMapKey::new(Word::new([
asset_commitment[0],
asset_commitment[1],
Felt::ZERO,
Felt::ZERO,
]));
let status_word = storage.get_map_item(Self::asset_status_slot(), key).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: Self::asset_status_slot().clone(),
source: err,
}
})?;
let raw_status = status_word[0].as_canonical_u64();
let status_code = u8::try_from(raw_status)
.map_err(|_| NonFungibleFaucetError::InvalidAssetStatus { status: raw_status })?;
AssetStatus::try_from(status_code)
}
pub fn symbol_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::symbol_slot().clone(),
StorageSlotSchema::value(
"Token symbol",
[
FeltSchema::felt("symbol"),
FeltSchema::new_void(),
FeltSchema::new_void(),
FeltSchema::new_void(),
],
),
)
}
pub fn asset_status_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::asset_status_slot().clone(),
StorageSlotSchema::map(
"Asset status registry. Key is the token ID padded to a word \
`[token_id_suffix, token_id_prefix, 0, 0]`; value is the status (0 = not issued, \
1 = issued, 2 = burned) padded to a word `[status, 0, 0, 0]`.",
SchemaType::native_word(),
SchemaType::native_felt(),
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let mut schema_entries = vec![Self::symbol_slot_schema(), Self::asset_status_slot_schema()];
schema_entries.extend(TokenMetadata::storage_schema());
let storage_schema =
StorageSchema::new(schema_entries).expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(
"Non-fungible faucet component bundling minting, burning, status, and metadata",
)
.with_storage_schema(storage_schema)
}
pub fn into_storage_slots(self) -> Vec<StorageSlot> {
let mut slots: Vec<StorageSlot> = Vec::new();
slots.push(self.symbol_slot_value());
slots.push(StorageSlot::with_map(Self::asset_status_slot().clone(), StorageMap::default()));
slots.extend(self.metadata.into_storage_slots());
slots
}
pub fn symbol_slot_value(&self) -> StorageSlot {
let word = Word::new([self.symbol.clone().into(), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
StorageSlot::with_value(Self::symbol_slot().clone(), word)
}
fn try_from_interface(
interface: AccountCodeInterface,
storage: &AccountStorage,
) -> Result<Self, NonFungibleFaucetError> {
if !interface.contains(NonFungibleFaucet::code().procedure_roots()) {
return Err(NonFungibleFaucetError::NotANonFungibleFaucetAccount);
}
NonFungibleFaucet::try_from(storage)
}
pub(crate) fn from_symbol_word_and_token_metadata(
word: Word,
metadata: TokenMetadata,
) -> Result<Self, NonFungibleFaucetError> {
let [symbol, ..] = *word;
let symbol =
TokenSymbol::try_from(symbol).map_err(TokenMetadataError::InvalidTokenSymbol)?;
Ok(Self { symbol, metadata })
}
}
impl From<NonFungibleFaucet> for AccountComponent {
fn from(faucet: NonFungibleFaucet) -> Self {
let component_metadata = NonFungibleFaucet::component_metadata();
let storage_slots = faucet.into_storage_slots();
AccountComponent::new(NonFungibleFaucet::code().clone(), storage_slots, component_metadata)
.expect("non-fungible faucet component should satisfy the requirements of a valid account component")
}
}
impl TryFrom<&AccountStorage> for NonFungibleFaucet {
type Error = NonFungibleFaucetError;
fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
let symbol_word = storage.get_item(Self::symbol_slot()).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: Self::symbol_slot().clone(),
source: err,
}
})?;
let token_metadata = TokenMetadata::try_from_storage(storage)?;
Self::from_symbol_word_and_token_metadata(symbol_word, token_metadata)
}
}
impl TryFrom<&Account> for NonFungibleFaucet {
type Error = NonFungibleFaucetError;
fn try_from(account: &Account) -> Result<Self, Self::Error> {
NonFungibleFaucet::try_from_interface(account.code_interface(), account.storage())
}
}
pub fn create_user_non_fungible_faucet(
init_seed: [u8; 32],
faucet: NonFungibleFaucet,
auth_component: AuthSingleSig,
token_policy_manager: TokenPolicyManager,
account_type: AccountType,
) -> Result<Account, NonFungibleFaucetError> {
if registers_owner_only_policy(&token_policy_manager) {
return Err(NonFungibleFaucetError::OwnerOnlyPolicyWithoutOwnable2Step);
}
AccountBuilder::new(init_seed)
.account_type(account_type)
.with_component(auth_component)
.with_component(faucet)
.with_component(Authority::AuthControlled)
.with_components(token_policy_manager)
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(NonFungibleFaucetError::AccountCreationFailed)
}
pub fn create_network_non_fungible_faucet(
init_seed: [u8; 32],
faucet: NonFungibleFaucet,
access_control: AccessControl,
token_policy_manager: TokenPolicyManager,
fee_policy_manager: FeePolicyManager,
) -> Result<Account, NonFungibleFaucetError> {
if registers_owner_only_policy(&token_policy_manager)
&& !matches!(access_control, AccessControl::Ownable2Step { .. })
{
return Err(NonFungibleFaucetError::OwnerOnlyPolicyWithoutOwnable2Step);
}
let note_allowlist = [MintNote::script_root(), BurnNote::script_root()].into_iter().collect();
NetworkAccount::builder(init_seed, note_allowlist, fee_policy_manager)
.expect("MintNote + BurnNote allowlist is non-empty")
.with_component(faucet)
.with_components(access_control)
.with_components(token_policy_manager)
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(NonFungibleFaucetError::AccountCreationFailed)
}