miden_agglayer/faucet.rs
1extern crate alloc;
2
3use alloc::collections::BTreeSet;
4use alloc::string::ToString;
5use alloc::vec;
6use alloc::vec::Vec;
7
8use miden_core::{Felt, Word};
9use miden_protocol::account::component::AccountComponentMetadata;
10use miden_protocol::account::{Account, AccountComponent, AccountId, StorageSlot, StorageSlotName};
11use miden_protocol::asset::{AssetAmount, TokenSymbol};
12use miden_protocol::errors::AccountIdError;
13use miden_protocol::note::NoteScriptRoot;
14use miden_standards::account::access::{Authority, Ownable2Step};
15use miden_standards::account::faucets::{FungibleFaucet, FungibleFaucetError, TokenName};
16use miden_standards::account::policies::TokenPolicyManager;
17use miden_standards::note::{BurnNote, MintNote};
18use thiserror::Error;
19
20use super::agglayer_faucet_component_library;
21pub use crate::{
22 AggLayerBridge,
23 B2AggNote,
24 ClaimNoteStorage,
25 ConfigAggBridgeNote,
26 EthAddress,
27 EthAmount,
28 EthAmountError,
29 EthEmbeddedAccountId,
30 ExitRoot,
31 GlobalIndex,
32 GlobalIndexError,
33 LeafData,
34 MetadataHash,
35 ProofData,
36 SmtNode,
37 UpdateGerNote,
38};
39
40// CONSTANTS
41// ================================================================================================
42// Include the generated agglayer constants
43include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs"));
44
45// AGGLAYER FAUCET STRUCT
46// ================================================================================================
47
48/// An [`AccountComponent`] implementing the AggLayer Faucet.
49///
50/// It re-exports `mint_and_send` and `receive_and_burn` from the agglayer faucet library.
51/// Conversion metadata (origin address, origin network, scale, metadata hash) is held by the
52/// bridge, not the faucet — see
53/// [`AggLayerBridge`] and the `faucet_metadata_map` populated on registration.
54///
55/// ## Storage Layout
56///
57/// - All [`FungibleFaucet`] storage slots (token config + name + mutability + description + logo
58/// URI + external link). Conversion metadata is no longer stored on the faucet; the bridge holds
59/// it in `faucet_metadata_map`.
60///
61/// ## Required Companion Components
62///
63/// This component re-exports `fungible::mint_and_send`, which requires:
64/// - [`Ownable2Step`]: Provides ownership data (bridge account ID as owner).
65/// - [`miden_standards::account::policies::TokenPolicyManager`]: Provides mint and burn policy
66/// management.
67///
68/// These must be added as separate components when building the faucet account.
69#[derive(Debug, Clone)]
70pub struct AggLayerFaucet {
71 faucet: FungibleFaucet,
72}
73
74impl AggLayerFaucet {
75 // CONSTRUCTORS
76 // --------------------------------------------------------------------------------------------
77
78 /// Creates a new AggLayer faucet component from the given configuration.
79 ///
80 /// The faucet's display name is derived from the symbol (an AggLayer faucet is identified by
81 /// its symbol; the human-readable name is not used in the bridge protocol).
82 ///
83 /// # Errors
84 /// Returns an error if:
85 /// - The decimals parameter exceeds maximum value of [`FungibleFaucet::MAX_DECIMALS`].
86 /// - The max supply exceeds maximum possible amount for a fungible asset.
87 /// - The token supply exceeds the max supply.
88 pub fn new(
89 symbol: TokenSymbol,
90 decimals: u8,
91 max_supply: Felt,
92 token_supply: Felt,
93 ) -> Result<Self, FungibleFaucetError> {
94 // Use the symbol as the display name; AggLayer faucets do not use a separate token name.
95 let name = TokenName::new(symbol.to_string().as_str())
96 .expect("symbol fits within token name capacity");
97 let max_supply_amount = AssetAmount::try_from(max_supply).map_err(|_| {
98 FungibleFaucetError::MaxSupplyTooLarge {
99 actual: max_supply.as_canonical_u64(),
100 max: AssetAmount::MAX.as_u64(),
101 }
102 })?;
103 let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| {
104 FungibleFaucetError::MaxSupplyTooLarge {
105 actual: token_supply.as_canonical_u64(),
106 max: AssetAmount::MAX.as_u64(),
107 }
108 })?;
109 let faucet = FungibleFaucet::builder()
110 .name(name)
111 .symbol(symbol)
112 .decimals(decimals)
113 .max_supply(max_supply_amount)
114 .token_supply(token_supply_amount)
115 .build()?;
116 Ok(Self { faucet })
117 }
118
119 /// Sets the token supply for an existing faucet (e.g. for testing scenarios).
120 ///
121 /// # Errors
122 /// Returns an error if the token supply exceeds the max supply.
123 pub fn with_token_supply(mut self, token_supply: Felt) -> Result<Self, FungibleFaucetError> {
124 let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| {
125 FungibleFaucetError::MaxSupplyTooLarge {
126 actual: token_supply.as_canonical_u64(),
127 max: AssetAmount::MAX.as_u64(),
128 }
129 })?;
130 self.faucet = self.faucet.with_token_supply(token_supply_amount)?;
131 Ok(self)
132 }
133
134 // PUBLIC ACCESSORS
135 // --------------------------------------------------------------------------------------------
136
137 /// Storage slot name for the token config word
138 /// `[token_supply, max_supply, decimals, token_symbol]`.
139 pub fn token_config_slot() -> &'static StorageSlotName {
140 FungibleFaucet::token_config_slot()
141 }
142
143 /// Storage slot name for the owner account ID (bridge), provided by the
144 /// [`Ownable2Step`] companion component.
145 pub fn owner_config_slot() -> &'static StorageSlotName {
146 Ownable2Step::slot_name()
147 }
148
149 // ALLOWED NOTES
150 // --------------------------------------------------------------------------------------------
151
152 /// Returns the set of input-note script roots that AggLayer faucet accounts accept.
153 ///
154 /// The faucet's [`AuthNetworkAccount`] component is initialized with this allowlist so only
155 /// MINT and BURN notes can drive the faucet.
156 ///
157 /// [`AuthNetworkAccount`]: miden_standards::account::auth::AuthNetworkAccount
158 pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
159 BTreeSet::from([MintNote::script_root(), BurnNote::script_root()])
160 }
161
162 /// Extracts the underlying [`FungibleFaucet`] component (which holds the token metadata)
163 /// from the storage slots of the provided account.
164 ///
165 /// # Errors
166 ///
167 /// Returns an error if:
168 /// - the provided account is not an [`AggLayerFaucet`] account.
169 pub fn try_faucet_from_account(
170 faucet_account: &Account,
171 ) -> Result<FungibleFaucet, AgglayerFaucetError> {
172 // check that the provided account is a faucet account
173 Self::assert_faucet_account(faucet_account)?;
174
175 FungibleFaucet::try_from(faucet_account.storage())
176 .map_err(AgglayerFaucetError::FungibleFaucetError)
177 }
178
179 /// Extracts the bridge account ID from the [`Ownable2Step`] owner config storage slot
180 /// of the provided account.
181 ///
182 /// # Errors
183 ///
184 /// Returns an error if:
185 /// - the provided account is not an [`AggLayerFaucet`] account.
186 pub fn owner_account_id(faucet_account: &Account) -> Result<AccountId, AgglayerFaucetError> {
187 // check that the provided account is a faucet account
188 Self::assert_faucet_account(faucet_account)?;
189
190 let ownership = Ownable2Step::try_from_storage(faucet_account.storage())
191 .map_err(AgglayerFaucetError::Ownable2StepError)?;
192 ownership.owner().ok_or(AgglayerFaucetError::OwnershipRenounced)
193 }
194
195 // HELPER FUNCTIONS
196 // --------------------------------------------------------------------------------------------
197
198 /// Checks that the provided account is an [`AggLayerFaucet`] account.
199 ///
200 /// # Errors
201 ///
202 /// Returns an error if:
203 /// - the provided account does not have all AggLayer Faucet specific storage slots.
204 /// - the provided account does not have all AggLayer Faucet specific procedures.
205 fn assert_faucet_account(account: &Account) -> Result<(), AgglayerFaucetError> {
206 // check that the storage slots are as expected
207 Self::assert_storage_slots(account)?;
208
209 // check that the procedure roots are as expected
210 Self::assert_code_commitment(account)?;
211
212 Ok(())
213 }
214
215 /// Checks that the provided account has all storage slots required for the [`AggLayerFaucet`].
216 ///
217 /// # Errors
218 ///
219 /// Returns an error if:
220 /// - provided account does not have all AggLayer Faucet specific storage slots).
221 fn assert_storage_slots(account: &Account) -> Result<(), AgglayerFaucetError> {
222 // get the storage slot names of the provided account
223 let account_storage_slot_names: Vec<&StorageSlotName> = account
224 .storage()
225 .slots()
226 .iter()
227 .map(|storage_slot| storage_slot.name())
228 .collect::<Vec<&StorageSlotName>>();
229
230 // check that all bridge specific storage slots are presented in the provided account
231 let are_slots_present = Self::slot_names()
232 .iter()
233 .all(|slot_name| account_storage_slot_names.contains(slot_name));
234 if !are_slots_present {
235 return Err(AgglayerFaucetError::StorageSlotsMismatch);
236 }
237
238 Ok(())
239 }
240
241 /// Checks that the code commitment of the provided account matches the code commitment of the
242 /// [`AggLayerFaucet`].
243 ///
244 /// # Errors
245 ///
246 /// Returns an error if:
247 /// - the code commitment of the provided account does not match the code commitment of the
248 /// [`AggLayerFaucet`].
249 fn assert_code_commitment(account: &Account) -> Result<(), AgglayerFaucetError> {
250 if FAUCET_CODE_COMMITMENT != account.code().commitment() {
251 return Err(AgglayerFaucetError::CodeCommitmentMismatch);
252 }
253
254 Ok(())
255 }
256
257 /// Returns a vector of all [`AggLayerFaucet`] storage slot names.
258 fn slot_names() -> Vec<&'static StorageSlotName> {
259 vec![
260 FungibleFaucet::token_config_slot(),
261 Ownable2Step::slot_name(),
262 Authority::authority_slot(),
263 TokenPolicyManager::active_mint_policy_slot(),
264 TokenPolicyManager::active_burn_policy_slot(),
265 TokenPolicyManager::allowed_mint_policies_slot(),
266 TokenPolicyManager::allowed_burn_policies_slot(),
267 TokenPolicyManager::allowed_send_policies_slot(),
268 TokenPolicyManager::allowed_receive_policies_slot(),
269 ]
270 }
271}
272
273impl From<AggLayerFaucet> for AccountComponent {
274 fn from(agglayer_faucet: AggLayerFaucet) -> Self {
275 // Bring in all of the FungibleFaucet's storage slots (token config + name +
276 // mutability + description + logo URI + external link).
277 agglayer_faucet_component(agglayer_faucet.faucet.into_storage_slots())
278 }
279}
280
281// AGGLAYER FAUCET ERROR
282// ================================================================================================
283
284/// AggLayer Faucet related errors.
285#[derive(Debug, Error)]
286pub enum AgglayerFaucetError {
287 #[error(
288 "provided account does not have storage slots required for the AggLayer Faucet account"
289 )]
290 StorageSlotsMismatch,
291 #[error("provided account does not have procedures required for the AggLayer Faucet account")]
292 CodeCommitmentMismatch,
293 #[error("fungible faucet error")]
294 FungibleFaucetError(#[source] FungibleFaucetError),
295 #[error("account ID error")]
296 AccountIdError(#[source] AccountIdError),
297 #[error("ownable2step error")]
298 Ownable2StepError(#[source] miden_standards::account::access::Ownable2StepError),
299 #[error("faucet ownership has been renounced")]
300 OwnershipRenounced,
301}
302
303// HELPER FUNCTIONS
304// ================================================================================================
305
306/// Creates an Agglayer Faucet component with the specified storage slots.
307fn agglayer_faucet_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
308 let library = agglayer_faucet_component_library();
309 let metadata = AccountComponentMetadata::new("agglayer::faucet")
310 .with_description("AggLayer faucet component");
311
312 AccountComponent::new(library, storage_slots, metadata).expect(
313 "agglayer_faucet component should satisfy the requirements of a valid account component",
314 )
315}