miden_usdcx/account/xreserve/builder/
construction.rs1use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
9use miden_protocol::account::{
10 Account,
11 AccountCode,
12 AccountComponent,
13 AccountId,
14 AccountStorage,
15 AccountType,
16 StorageMap,
17 StorageMapKey,
18 StorageSlot,
19 StorageSlotName,
20};
21use miden_protocol::asset::{AssetAmount, AssetCallbacks, AssetId, TokenSymbol};
22use miden_protocol::block::FeeParameters;
23use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
24use miden_protocol::errors::{AccountError, StorageMapError};
25use miden_protocol::utils::sync::LazyLock;
26use miden_protocol::vm::Package;
27use miden_protocol::{Felt, Word};
28use miden_standards::account::faucets::{FungibleFaucet, TokenName};
29use miden_standards::account::fees::FeePolicyManager;
30use miden_standards::account::policies::{TokenPolicyManager, TokenPolicyManagerV2};
31
32use super::{
33 PolicyManagerVersion,
34 USDCX_DECIMALS,
35 USDCX_TOKEN_SYMBOL,
36 XReserveStablecoinBuilder,
37 XReserveStablecoinBuilderError,
38};
39use crate::xreserve::encoding::{CircleDomain, DepositNonce};
40
41const XRESERVE_COMPONENT_LABEL: &str = "xusdc-xreserve";
48const BURN_POLICY_COMPONENT_LABEL: &str = "xusdc-burn-policy";
49
50const ATTESTER_ENABLED_MARKER: [u32; 4] = [1, 0, 0, 0];
52
53const NONCE_USED_MARKER: [u32; 4] = [1, 0, 0, 0];
55
56static FAUCET_EXTENSION_CODE: LazyLock<AccountComponentCode> = LazyLock::new(|| {
60 AccountComponentCode::from(
61 Package::read_from_bytes_trusted(include_bytes!(concat!(
62 env!("OUT_DIR"),
63 "/assets/components/xreserve-faucet-extension.masp"
64 )))
65 .expect("the shipped account-component package deserializes"),
66 )
67});
68
69static BURN_POLICY_CODE: LazyLock<AccountComponentCode> = LazyLock::new(|| {
70 AccountComponentCode::from(
71 Package::read_from_bytes_trusted(include_bytes!(concat!(
72 env!("OUT_DIR"),
73 "/assets/components/xreserve-faucet-burn-policy.masp"
74 )))
75 .expect("the shipped burn-policy package deserializes"),
76 )
77});
78
79static DOMAIN_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
80 StorageSlotName::new("xusdc::xreserve::domain_config::domain")
81 .expect("storage slot name should be valid")
82});
83
84static USED_NONCES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
86 StorageSlotName::new("xusdc::xreserve::nonce_registry::used_nonces")
87 .expect("storage slot name should be valid")
88});
89
90static XRESERVE_ATTESTERS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
93 StorageSlotName::new("xusdc::xreserve::attester_admin::xreserve_attesters")
94 .expect("storage slot name should be valid")
95});
96
97#[derive(Debug, Clone)]
101pub struct XReserveFaucetExtension {
102 domain: CircleDomain,
103 attesters: StorageMap,
104}
105
106impl XReserveFaucetExtension {
107 pub fn new(domain: CircleDomain, attesters: &[PublicKey]) -> Result<Self, StorageMapError> {
115 let attesters = StorageMap::with_entries(attesters.iter().map(|key| {
116 (StorageMapKey::new(key.to_commitment()), Word::from(ATTESTER_ENABLED_MARKER))
117 }))?;
118 Ok(Self { domain, attesters })
119 }
120
121 pub fn code() -> &'static AccountComponentCode {
126 &FAUCET_EXTENSION_CODE
127 }
128
129 pub fn domain_config_slot() -> &'static StorageSlotName {
131 &DOMAIN_CONFIG_SLOT_NAME
132 }
133
134 pub fn used_nonces_slot() -> &'static StorageSlotName {
136 &USED_NONCES_SLOT_NAME
137 }
138
139 pub fn xreserve_attesters_slot() -> &'static StorageSlotName {
141 &XRESERVE_ATTESTERS_SLOT_NAME
142 }
143}
144
145impl From<XReserveFaucetExtension> for AccountComponent {
146 fn from(faucet_ext: XReserveFaucetExtension) -> Self {
147 AccountComponent::new(
148 FAUCET_EXTENSION_CODE.clone(),
149 vec![
150 StorageSlot::with_value(
151 XReserveFaucetExtension::domain_config_slot().clone(),
152 Word::from([faucet_ext.domain.as_u32(), 0, 0, 0]),
153 ),
154 StorageSlot::with_empty_map(XReserveFaucetExtension::used_nonces_slot().clone()),
155 StorageSlot::with_map(
156 XReserveFaucetExtension::xreserve_attesters_slot().clone(),
157 faucet_ext.attesters,
158 ),
159 ],
160 AccountComponentMetadata::new(XRESERVE_COMPONENT_LABEL),
161 )
162 .expect("the faucet extension binds with its three declared slots")
163 }
164}
165
166impl XReserveStablecoinBuilder {
167 pub fn burn_policy_component() -> AccountComponent {
170 AccountComponent::new(
171 BURN_POLICY_CODE.clone(),
172 vec![],
173 AccountComponentMetadata::new(BURN_POLICY_COMPONENT_LABEL),
174 )
175 .expect("the burn policy binds with no storage slots")
176 }
177
178 pub fn build_account(
187 &self,
188 init_seed: [u8; 32],
189 ) -> Result<Account, XReserveStablecoinBuilderError> {
190 let components = self.build_components()?;
191 let mut builder = Account::builder(init_seed).account_type(AccountType::Public);
192 for component in components {
193 builder = builder.with_component(component);
194 }
195 builder = builder
196 .with_components(Self::auth_component(self.fee_parameters.clone(), self.fee_asset_id)?);
197 builder.build().map_err(XReserveStablecoinBuilderError::AccountComposition)
198 }
199
200 pub fn build_genesis_account(
209 &self,
210 init_seed: [u8; 32],
211 ) -> Result<Account, XReserveStablecoinBuilderError> {
212 let account = self.build_account(init_seed)?;
213 let fee_asset_id = AssetId::new_fungible(account.id());
214 let (id, vault, mut storage, code, _nonce, _seed) = account.into_parts();
215 storage
216 .set_item(FeePolicyManager::fee_asset_id_slot(), fee_asset_id.to_word())
217 .map_err(XReserveStablecoinBuilderError::AccountComposition)?;
218 Account::new(id, vault, storage, code, Felt::ONE, None)
219 .map_err(XReserveStablecoinBuilderError::AccountComposition)
220 }
221}
222
223pub fn record_used_nonces(
236 account: Account,
237 used_nonces: &[DepositNonce],
238) -> Result<Account, XReserveStablecoinBuilderError> {
239 let (id, vault, mut storage, code, _nonce, _seed) = account.into_parts();
240 for nonce in used_nonces {
241 storage
242 .set_map_item(
243 XReserveFaucetExtension::used_nonces_slot(),
244 nonce.to_storage_map_key(),
245 Word::from(NONCE_USED_MARKER),
246 )
247 .map_err(XReserveStablecoinBuilderError::AccountComposition)?;
248 }
249 Account::new(id, vault, storage, code, Felt::ONE, None)
250 .map_err(XReserveStablecoinBuilderError::AccountComposition)
251}
252
253pub fn upgrade_to_token_policy_manager_v2(
268 account: Account,
269) -> Result<Account, XReserveStablecoinBuilderError> {
270 let has_v1_manager = [
271 TokenPolicyManager::invoke_send_policy_root(),
272 TokenPolicyManager::invoke_receive_policy_root(),
273 ]
274 .into_iter()
275 .all(|procedure_root| account.code().has_procedure(procedure_root.as_word()));
276 if !has_v1_manager {
277 return Err(XReserveStablecoinBuilderError::AccountComposition(AccountError::other(
278 "the account is not a faucet with the V1 token policy manager",
279 )));
280 }
281 let v2_code = faucet_code(account.id(), PolicyManagerVersion::V2)?;
282
283 let (id, vault, mut storage, _code, nonce, seed) = account.into_parts();
284 set_v2_callback_slots(&mut storage)
285 .map_err(XReserveStablecoinBuilderError::AccountComposition)?;
286 Account::new(id, vault, storage, v2_code, nonce, seed)
287 .map_err(XReserveStablecoinBuilderError::AccountComposition)
288}
289
290fn set_v2_callback_slots(storage: &mut AccountStorage) -> Result<(), AccountError> {
292 let callback_mappings = [
293 (
294 AssetCallbacks::on_before_asset_added_to_note_slot(),
295 TokenPolicyManagerV2::invoke_send_policy_root(),
296 ),
297 (
298 AssetCallbacks::on_before_asset_added_to_account_slot(),
299 TokenPolicyManagerV2::invoke_receive_policy_root(),
300 ),
301 ];
302
303 for (slot_name, callback_root) in callback_mappings {
304 storage.set_item(slot_name, callback_root.as_word())?;
305 }
306
307 Ok(())
308}
309
310fn faucet_code(
315 faucet_id: AccountId,
316 version: PolicyManagerVersion,
317) -> Result<AccountCode, XReserveStablecoinBuilderError> {
318 let account = XReserveStablecoinBuilder::builder()
319 .token_supply(AssetAmount::ZERO)
320 .owner(faucet_id)
321 .attest_admin_holders(Vec::new())
322 .pauser_holders(Vec::new())
323 .unpauser_holders(Vec::new())
324 .blocklist_manager_holders(Vec::new())
325 .fee_parameters(FeeParameters::new(0))
326 .fee_asset_id(AssetId::new_fungible(faucet_id))
327 .domain(CircleDomain::new(0))
328 .policy_manager(version)
329 .build()?
330 .build_account([0; 32])?;
331 Ok(account.code().clone())
332}
333
334#[allow(clippy::too_many_arguments)]
343pub fn build_faucet_account(
344 init_seed: [u8; 32],
345 token_supply: AssetAmount,
346 owner: AccountId,
347 attest_admin_holders: Vec<AccountId>,
348 pauser_holders: Vec<AccountId>,
349 unpauser_holders: Vec<AccountId>,
350 blocklist_manager_holders: Vec<AccountId>,
351 fee_parameters: FeeParameters,
352 fee_asset_id: AssetId,
353 domain: CircleDomain,
354) -> Result<Account, XReserveStablecoinBuilderError> {
355 XReserveStablecoinBuilder::builder()
356 .token_supply(token_supply)
357 .owner(owner)
358 .attest_admin_holders(attest_admin_holders)
359 .pauser_holders(pauser_holders)
360 .unpauser_holders(unpauser_holders)
361 .blocklist_manager_holders(blocklist_manager_holders)
362 .fee_parameters(fee_parameters)
363 .fee_asset_id(fee_asset_id)
364 .domain(domain)
365 .build()?
366 .build_account(init_seed)
367}
368
369pub(super) fn build_usdcx_faucet(
376 token_supply: AssetAmount,
377) -> Result<FungibleFaucet, XReserveStablecoinBuilderError> {
378 FungibleFaucet::builder()
379 .name(TokenName::new("USDCx").expect("USDCx is a valid token name"))
380 .symbol(TokenSymbol::new(USDCX_TOKEN_SYMBOL).expect("the USDCX symbol constant is valid"))
381 .decimals(USDCX_DECIMALS)
382 .max_supply(AssetAmount::MAX)
383 .token_supply(token_supply)
384 .is_max_supply_mutable(true)
385 .build()
386 .map_err(XReserveStablecoinBuilderError::FaucetComposition)
387}