use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
use miden_protocol::account::{
Account,
AccountComponent,
AccountId,
AccountType,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::{AssetAmount, AssetId, TokenSymbol};
use miden_protocol::block::FeeParameters;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
use miden_protocol::errors::StorageMapError;
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::vm::Package;
use miden_protocol::{Felt, Word};
use miden_standards::account::faucets::{FungibleFaucet, TokenName};
use miden_standards::account::fees::FeePolicyManager;
use super::{
USDCX_DECIMALS,
USDCX_TOKEN_SYMBOL,
XReserveStablecoinBuilder,
XReserveStablecoinBuilderError,
};
use crate::xreserve::encoding::{CircleDomain, DepositNonce};
const XRESERVE_COMPONENT_LABEL: &str = "xusdc-xreserve";
const BURN_POLICY_COMPONENT_LABEL: &str = "xusdc-burn-policy";
const ATTESTER_ENABLED_MARKER: [u32; 4] = [1, 0, 0, 0];
const NONCE_USED_MARKER: [u32; 4] = [1, 0, 0, 0];
static FAUCET_EXTENSION_CODE: LazyLock<AccountComponentCode> = LazyLock::new(|| {
AccountComponentCode::from(
Package::read_from_bytes_trusted(include_bytes!(concat!(
env!("OUT_DIR"),
"/assets/components/xreserve-faucet-extension.masp"
)))
.expect("the shipped account-component package deserializes"),
)
});
static BURN_POLICY_CODE: LazyLock<AccountComponentCode> = LazyLock::new(|| {
AccountComponentCode::from(
Package::read_from_bytes_trusted(include_bytes!(concat!(
env!("OUT_DIR"),
"/assets/components/xreserve-faucet-burn-policy.masp"
)))
.expect("the shipped burn-policy package deserializes"),
)
});
static DOMAIN_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("xusdc::xreserve::domain_config::domain")
.expect("storage slot name should be valid")
});
static USED_NONCES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("xusdc::xreserve::nonce_registry::used_nonces")
.expect("storage slot name should be valid")
});
static XRESERVE_ATTESTERS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("xusdc::xreserve::attester_admin::xreserve_attesters")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone)]
pub struct XReserveFaucetExtension {
domain: CircleDomain,
attesters: StorageMap,
}
impl XReserveFaucetExtension {
pub fn new(domain: CircleDomain, attesters: &[PublicKey]) -> Result<Self, StorageMapError> {
let attesters = StorageMap::with_entries(attesters.iter().map(|key| {
(StorageMapKey::new(key.to_commitment()), Word::from(ATTESTER_ENABLED_MARKER))
}))?;
Ok(Self { domain, attesters })
}
pub fn code() -> &'static AccountComponentCode {
&FAUCET_EXTENSION_CODE
}
pub fn domain_config_slot() -> &'static StorageSlotName {
&DOMAIN_CONFIG_SLOT_NAME
}
pub fn used_nonces_slot() -> &'static StorageSlotName {
&USED_NONCES_SLOT_NAME
}
pub fn xreserve_attesters_slot() -> &'static StorageSlotName {
&XRESERVE_ATTESTERS_SLOT_NAME
}
}
impl From<XReserveFaucetExtension> for AccountComponent {
fn from(faucet_ext: XReserveFaucetExtension) -> Self {
AccountComponent::new(
FAUCET_EXTENSION_CODE.clone(),
vec![
StorageSlot::with_value(
XReserveFaucetExtension::domain_config_slot().clone(),
Word::from([faucet_ext.domain.as_u32(), 0, 0, 0]),
),
StorageSlot::with_empty_map(XReserveFaucetExtension::used_nonces_slot().clone()),
StorageSlot::with_map(
XReserveFaucetExtension::xreserve_attesters_slot().clone(),
faucet_ext.attesters,
),
],
AccountComponentMetadata::new(XRESERVE_COMPONENT_LABEL),
)
.expect("the faucet extension binds with its three declared slots")
}
}
impl XReserveStablecoinBuilder {
pub fn burn_policy_component() -> AccountComponent {
AccountComponent::new(
BURN_POLICY_CODE.clone(),
vec![],
AccountComponentMetadata::new(BURN_POLICY_COMPONENT_LABEL),
)
.expect("the burn policy binds with no storage slots")
}
pub fn build_account(
&self,
init_seed: [u8; 32],
) -> Result<Account, XReserveStablecoinBuilderError> {
let components = self.build_components()?;
let mut builder = Account::builder(init_seed).account_type(AccountType::Public);
for component in components {
builder = builder.with_component(component);
}
builder = builder
.with_components(Self::auth_component(self.fee_parameters.clone(), self.fee_asset_id)?);
builder.build().map_err(XReserveStablecoinBuilderError::AccountComposition)
}
pub fn build_genesis_account(
&self,
init_seed: [u8; 32],
) -> Result<Account, XReserveStablecoinBuilderError> {
let account = self.build_account(init_seed)?;
let fee_asset_id = AssetId::new_fungible(account.id());
let (id, vault, mut storage, code, _nonce, _seed) = account.into_parts();
storage
.set_item(FeePolicyManager::fee_asset_id_slot(), fee_asset_id.to_word())
.map_err(XReserveStablecoinBuilderError::AccountComposition)?;
Account::new(id, vault, storage, code, Felt::ONE, None)
.map_err(XReserveStablecoinBuilderError::AccountComposition)
}
}
pub fn record_used_nonces(
account: Account,
used_nonces: &[DepositNonce],
) -> Result<Account, XReserveStablecoinBuilderError> {
let (id, vault, mut storage, code, _nonce, _seed) = account.into_parts();
for nonce in used_nonces {
storage
.set_map_item(
XReserveFaucetExtension::used_nonces_slot(),
nonce.to_storage_map_key(),
Word::from(NONCE_USED_MARKER),
)
.map_err(XReserveStablecoinBuilderError::AccountComposition)?;
}
Account::new(id, vault, storage, code, Felt::ONE, None)
.map_err(XReserveStablecoinBuilderError::AccountComposition)
}
#[allow(clippy::too_many_arguments)]
pub fn build_faucet_account(
init_seed: [u8; 32],
token_supply: AssetAmount,
owner: AccountId,
attest_admin_holders: Vec<AccountId>,
pauser_holders: Vec<AccountId>,
unpauser_holders: Vec<AccountId>,
blocklist_manager_holders: Vec<AccountId>,
fee_parameters: FeeParameters,
fee_asset_id: AssetId,
domain: CircleDomain,
) -> Result<Account, XReserveStablecoinBuilderError> {
XReserveStablecoinBuilder::builder()
.token_supply(token_supply)
.owner(owner)
.attest_admin_holders(attest_admin_holders)
.pauser_holders(pauser_holders)
.unpauser_holders(unpauser_holders)
.blocklist_manager_holders(blocklist_manager_holders)
.fee_parameters(fee_parameters)
.fee_asset_id(fee_asset_id)
.domain(domain)
.build()?
.build_account(init_seed)
}
pub(super) fn build_usdcx_faucet(
token_supply: AssetAmount,
) -> Result<FungibleFaucet, XReserveStablecoinBuilderError> {
FungibleFaucet::builder()
.name(TokenName::new("USDCx").expect("USDCx is a valid token name"))
.symbol(TokenSymbol::new(USDCX_TOKEN_SYMBOL).expect("the USDCX symbol constant is valid"))
.decimals(USDCX_DECIMALS)
.max_supply(AssetAmount::MAX)
.token_supply(token_supply)
.is_max_supply_mutable(true)
.build()
.map_err(XReserveStablecoinBuilderError::FaucetComposition)
}