use alloc::vec::Vec;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
FeltSchema,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
Account,
AccountBuilder,
AccountComponent,
AccountComponentName,
AccountProcedureRoot,
AccountStorage,
AccountType,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::{AssetAmount, TokenSymbol};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use super::{
Description,
ExternalLink,
FungibleFaucetError,
LogoURI,
TokenMetadata,
TokenMetadataError,
TokenName,
};
use crate::account::access::{AccessControl, PausableManager};
use crate::account::account_component_code;
use crate::account::auth::{AuthNetworkAccount, AuthSingleSigAcl, AuthSingleSigAclConfig, NoAuth};
use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt};
use crate::account::policies::TokenPolicyManager;
use crate::{AuthMethod, procedure_root};
#[cfg(test)]
mod tests;
pub(crate) static TOKEN_CONFIG_SLOT: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::faucets::fungible::token_config")
.expect("storage slot name should be valid")
});
const TOKEN_SYMBOL_TYPE: &str = "miden::standards::faucets::fungible::token_symbol";
account_component_code!(FUNGIBLE_FAUCET_CODE, "faucets/fungible_faucet.masl");
procedure_root!(
FUNGIBLE_FAUCET_MINT_AND_SEND,
FungibleFaucet::NAME,
FungibleFaucet::MINT_PROC_NAME,
FungibleFaucet::code()
);
procedure_root!(
FUNGIBLE_FAUCET_RECEIVE_AND_BURN,
FungibleFaucet::NAME,
FungibleFaucet::RECEIVE_AND_BURN_PROC_NAME,
FungibleFaucet::code()
);
procedure_root!(
FUNGIBLE_FAUCET_SET_MAX_SUPPLY,
FungibleFaucet::NAME,
FungibleFaucet::SET_MAX_SUPPLY_PROC_NAME,
FungibleFaucet::code()
);
procedure_root!(
FUNGIBLE_FAUCET_SET_DESCRIPTION,
FungibleFaucet::NAME,
FungibleFaucet::SET_DESCRIPTION_PROC_NAME,
FungibleFaucet::code()
);
procedure_root!(
FUNGIBLE_FAUCET_SET_LOGO_URI,
FungibleFaucet::NAME,
FungibleFaucet::SET_LOGO_URI_PROC_NAME,
FungibleFaucet::code()
);
procedure_root!(
FUNGIBLE_FAUCET_SET_EXTERNAL_LINK,
FungibleFaucet::NAME,
FungibleFaucet::SET_EXTERNAL_LINK_PROC_NAME,
FungibleFaucet::code()
);
#[derive(Debug, Clone)]
pub struct FungibleFaucet {
token_supply: AssetAmount,
max_supply: AssetAmount,
decimals: u8,
symbol: TokenSymbol,
metadata: TokenMetadata,
}
#[bon::bon]
impl FungibleFaucet {
#[builder]
pub fn new(
name: TokenName,
symbol: TokenSymbol,
decimals: u8,
max_supply: AssetAmount,
#[builder(default)] token_supply: AssetAmount,
description: Option<Description>,
logo_uri: Option<LogoURI>,
external_link: Option<ExternalLink>,
#[builder(default)] is_description_mutable: bool,
#[builder(default)] is_logo_uri_mutable: bool,
#[builder(default)] is_external_link_mutable: bool,
#[builder(default)] is_max_supply_mutable: bool,
) -> Result<FungibleFaucet, FungibleFaucetError> {
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) = external_link {
metadata = metadata.with_external_link(link, is_external_link_mutable);
} else {
metadata = metadata.with_external_link_mutable(is_external_link_mutable);
}
metadata = metadata.with_max_supply_mutable(is_max_supply_mutable);
Self::new_validated(symbol, decimals, max_supply, token_supply, metadata)
}
}
impl FungibleFaucet {
pub const NAME: &'static str = "miden::standards::components::faucets::fungible_faucet";
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub const MAX_DECIMALS: u8 = 12;
const MINT_PROC_NAME: &'static str = "mint_and_send";
const RECEIVE_AND_BURN_PROC_NAME: &'static str = "receive_and_burn";
const SET_MAX_SUPPLY_PROC_NAME: &'static str = "set_max_supply";
const SET_DESCRIPTION_PROC_NAME: &'static str = "set_description";
const SET_LOGO_URI_PROC_NAME: &'static str = "set_logo_uri";
const SET_EXTERNAL_LINK_PROC_NAME: &'static str = "set_external_link";
pub(crate) fn new_validated(
symbol: TokenSymbol,
decimals: u8,
max_supply: AssetAmount,
token_supply: AssetAmount,
metadata: TokenMetadata,
) -> Result<Self, FungibleFaucetError> {
if decimals > Self::MAX_DECIMALS {
return Err(FungibleFaucetError::TooManyDecimals {
actual: decimals as u64,
max: Self::MAX_DECIMALS,
});
}
if token_supply > max_supply {
return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
token_supply: token_supply.as_u64(),
max_supply: max_supply.as_u64(),
});
}
Ok(Self {
token_supply,
max_supply,
decimals,
symbol,
metadata,
})
}
pub fn code() -> &'static AccountComponentCode {
&FUNGIBLE_FAUCET_CODE
}
pub fn mint_and_send_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_MINT_AND_SEND
}
pub fn receive_and_burn_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_RECEIVE_AND_BURN
}
pub fn set_max_supply_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_MAX_SUPPLY
}
pub fn set_description_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_DESCRIPTION
}
pub fn set_logo_uri_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_LOGO_URI
}
pub fn set_external_link_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_EXTERNAL_LINK
}
pub fn token_config_slot() -> &'static StorageSlotName {
&TOKEN_CONFIG_SLOT
}
pub fn token_supply(&self) -> AssetAmount {
self.token_supply
}
pub fn max_supply(&self) -> AssetAmount {
self.max_supply
}
pub fn decimals(&self) -> u8 {
self.decimals
}
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 external_link(&self) -> Option<&ExternalLink> {
self.metadata.external_link()
}
pub fn token_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type");
(
Self::token_config_slot().clone(),
StorageSlotSchema::value(
"Token config",
[
FeltSchema::felt("token_supply").with_default(Felt::ZERO),
FeltSchema::felt("max_supply"),
FeltSchema::u8("decimals"),
FeltSchema::new_typed(token_symbol_type, "symbol"),
],
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let mut schema_entries = vec![Self::token_config_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(
"Fungible faucet component bundling minting, burning, and token metadata",
)
.with_storage_schema(storage_schema)
}
pub fn into_storage_slots(self) -> Vec<StorageSlot> {
let mut slots: Vec<StorageSlot> = Vec::new();
slots.push(self.token_config_slot_value());
slots.extend(self.metadata.into_storage_slots());
slots.push(crate::account::access::pausable::PausableStorage::default().into_slot());
slots
}
pub fn token_config_slot_value(&self) -> StorageSlot {
let word = Word::new([
self.token_supply.into(),
self.max_supply.into(),
Felt::from(self.decimals),
self.symbol.clone().into(),
]);
StorageSlot::with_value(Self::token_config_slot().clone(), word)
}
pub fn with_token_supply(
mut self,
token_supply: AssetAmount,
) -> Result<Self, FungibleFaucetError> {
if token_supply > self.max_supply {
return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
token_supply: token_supply.as_u64(),
max_supply: self.max_supply.as_u64(),
});
}
self.token_supply = token_supply;
Ok(self)
}
pub fn with_description_mutable(mut self, mutable: bool) -> Self {
self.metadata = self.metadata.with_description_mutable(mutable);
self
}
pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self {
self.metadata = self.metadata.with_logo_uri_mutable(mutable);
self
}
pub fn with_external_link_mutable(mut self, mutable: bool) -> Self {
self.metadata = self.metadata.with_external_link_mutable(mutable);
self
}
pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self {
self.metadata = self.metadata.with_max_supply_mutable(mutable);
self
}
fn try_from_interface(
interface: AccountInterface,
storage: &AccountStorage,
) -> Result<Self, FungibleFaucetError> {
if !interface.components().contains(&AccountComponentInterface::FungibleFaucet) {
return Err(FungibleFaucetError::MissingFungibleFaucetInterface);
}
FungibleFaucet::try_from(storage)
}
pub(crate) fn from_token_config_word_and_token_metadata(
word: Word,
metadata: TokenMetadata,
) -> Result<Self, FungibleFaucetError> {
let [token_supply, max_supply, decimals_felt, token_symbol] = *word;
let symbol =
TokenSymbol::try_from(token_symbol).map_err(TokenMetadataError::InvalidTokenSymbol)?;
let decimals: u8 = decimals_felt.as_canonical_u64().try_into().map_err(|_| {
FungibleFaucetError::TooManyDecimals {
actual: decimals_felt.as_canonical_u64(),
max: Self::MAX_DECIMALS,
}
})?;
let max_supply = AssetAmount::try_from(max_supply).map_err(|_| {
FungibleFaucetError::MaxSupplyTooLarge {
actual: max_supply.as_canonical_u64(),
max: AssetAmount::MAX.as_u64(),
}
})?;
let token_supply = AssetAmount::try_from(token_supply).map_err(|_| {
FungibleFaucetError::MaxSupplyTooLarge {
actual: token_supply.as_canonical_u64(),
max: AssetAmount::MAX.as_u64(),
}
})?;
Self::new_validated(symbol, decimals, max_supply, token_supply, metadata)
}
}
impl From<FungibleFaucet> for AccountComponent {
fn from(faucet: FungibleFaucet) -> Self {
let component_metadata = FungibleFaucet::component_metadata();
let storage_slots = faucet.into_storage_slots();
AccountComponent::new(FungibleFaucet::code().clone(), storage_slots, component_metadata)
.expect("fungible faucet component should satisfy the requirements of a valid account component")
}
}
impl TryFrom<&AccountStorage> for FungibleFaucet {
type Error = FungibleFaucetError;
fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
let token_config_word = storage.get_item(Self::token_config_slot()).map_err(|err| {
TokenMetadataError::StorageLookupFailed {
slot_name: Self::token_config_slot().clone(),
source: err,
}
})?;
let token_metadata = TokenMetadata::try_from_storage(storage)?;
Self::from_token_config_word_and_token_metadata(token_config_word, token_metadata)
}
}
impl TryFrom<Account> for FungibleFaucet {
type Error = FungibleFaucetError;
fn try_from(account: Account) -> Result<Self, Self::Error> {
let account_interface = AccountInterface::from_account(&account);
FungibleFaucet::try_from_interface(account_interface, account.storage())
}
}
impl TryFrom<&Account> for FungibleFaucet {
type Error = FungibleFaucetError;
fn try_from(account: &Account) -> Result<Self, Self::Error> {
let account_interface = AccountInterface::from_account(account);
FungibleFaucet::try_from_interface(account_interface, account.storage())
}
}
fn all_authority_gated_setter_roots() -> Vec<AccountProcedureRoot> {
vec![
FungibleFaucet::mint_and_send_root(),
FungibleFaucet::set_max_supply_root(),
FungibleFaucet::set_description_root(),
FungibleFaucet::set_logo_uri_root(),
FungibleFaucet::set_external_link_root(),
TokenPolicyManager::set_mint_policy_root(),
TokenPolicyManager::set_burn_policy_root(),
TokenPolicyManager::set_send_policy_root(),
TokenPolicyManager::set_receive_policy_root(),
PausableManager::pause_root(),
PausableManager::unpause_root(),
]
}
pub fn create_fungible_faucet(
init_seed: [u8; 32],
faucet: FungibleFaucet,
account_type: AccountType,
auth_method: AuthMethod,
access_control: AccessControl,
token_policy_manager: TokenPolicyManager,
) -> Result<Account, FungibleFaucetError> {
let auth_component = build_auth_component(&access_control, auth_method)?;
let account = AccountBuilder::new(init_seed)
.account_type(account_type)
.with_auth_component(auth_component)
.with_component(faucet)
.with_components(access_control)
.with_components(token_policy_manager)
.with_component(PausableManager)
.build()
.map_err(FungibleFaucetError::AccountError)?;
Ok(account)
}
fn build_auth_component(
access_control: &AccessControl,
auth_method: AuthMethod,
) -> Result<AccountComponent, FungibleFaucetError> {
match (access_control, auth_method) {
(
AccessControl::AuthControlled,
AuthMethod::SingleSig { approver: (pub_key, auth_scheme) },
) => Ok(AuthSingleSigAcl::new(
pub_key,
auth_scheme,
AuthSingleSigAclConfig::new()
.with_auth_trigger_procedures(all_authority_gated_setter_roots())
.with_allow_unauthorized_input_notes(true),
)
.map_err(FungibleFaucetError::AccountError)?
.into()),
(AccessControl::AuthControlled, AuthMethod::NetworkAccount { .. }) => {
Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination(
"NetworkAccount is only supported with AccessControl::Ownable2Step or \
AccessControl::Rbac (network-style faucets)"
.into(),
))
},
(AccessControl::AuthControlled, AuthMethod::NoAuth) => {
Err(FungibleFaucetError::IncompatibleAuthControlledAuth(
"NoAuth cannot authenticate authority-gated setters".into(),
))
},
(
AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. },
AuthMethod::NetworkAccount { allowed_script_roots },
) => Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots)
.map_err(|err| {
FungibleFaucetError::UnsupportedAuthMethod(alloc::format!(
"invalid network account allowlist: {err}"
))
})?
.into()),
(AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, AuthMethod::NoAuth) => {
Ok(NoAuth::new().into())
},
(
AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. },
AuthMethod::SingleSig { .. },
) => Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination(
"SingleSig is only supported with AccessControl::AuthControlled; pair \
Ownable2Step / Rbac with NetworkAccount or NoAuth instead"
.into(),
)),
(_, AuthMethod::Multisig { .. }) => Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets do not support Multisig authentication".into(),
)),
(_, AuthMethod::Unknown) => Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets cannot be created with Unknown authentication method".into(),
)),
}
}