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