Skip to main content

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}