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::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}