use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
use miden_protocol::account::{
Account,
AccountCode,
AccountComponent,
AccountId,
AccountStorage,
AccountType,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::{AssetAmount, AssetCallbacks, AssetId, TokenSymbol};
use miden_protocol::block::FeeParameters;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
use miden_protocol::errors::{AccountError, 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 miden_standards::account::policies::{TokenPolicyManager, TokenPolicyManagerV2};
use super::{
PolicyManagerVersion,
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)
}
pub fn upgrade_to_token_policy_manager_v2(
account: Account,
) -> Result<Account, XReserveStablecoinBuilderError> {
let has_v1_manager = [
TokenPolicyManager::invoke_send_policy_root(),
TokenPolicyManager::invoke_receive_policy_root(),
]
.into_iter()
.all(|procedure_root| account.code().has_procedure(procedure_root.as_word()));
if !has_v1_manager {
return Err(XReserveStablecoinBuilderError::AccountComposition(AccountError::other(
"the account is not a faucet with the V1 token policy manager",
)));
}
let v2_code = faucet_code(account.id(), PolicyManagerVersion::V2)?;
let (id, vault, mut storage, _code, nonce, seed) = account.into_parts();
set_v2_callback_slots(&mut storage)
.map_err(XReserveStablecoinBuilderError::AccountComposition)?;
Account::new(id, vault, storage, v2_code, nonce, seed)
.map_err(XReserveStablecoinBuilderError::AccountComposition)
}
fn set_v2_callback_slots(storage: &mut AccountStorage) -> Result<(), AccountError> {
let callback_mappings = [
(
AssetCallbacks::on_before_asset_added_to_note_slot(),
TokenPolicyManagerV2::invoke_send_policy_root(),
),
(
AssetCallbacks::on_before_asset_added_to_account_slot(),
TokenPolicyManagerV2::invoke_receive_policy_root(),
),
];
for (slot_name, callback_root) in callback_mappings {
storage.set_item(slot_name, callback_root.as_word())?;
}
Ok(())
}
fn faucet_code(
faucet_id: AccountId,
version: PolicyManagerVersion,
) -> Result<AccountCode, XReserveStablecoinBuilderError> {
let account = XReserveStablecoinBuilder::builder()
.token_supply(AssetAmount::ZERO)
.owner(faucet_id)
.attest_admin_holders(Vec::new())
.pauser_holders(Vec::new())
.unpauser_holders(Vec::new())
.blocklist_manager_holders(Vec::new())
.fee_parameters(FeeParameters::new(0))
.fee_asset_id(AssetId::new_fungible(faucet_id))
.domain(CircleDomain::new(0))
.policy_manager(version)
.build()?
.build_account([0; 32])?;
Ok(account.code().clone())
}
#[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)
}