miden_lib/account/faucets/
network_fungible.rs

1use miden_objects::account::{
2    Account,
3    AccountBuilder,
4    AccountComponent,
5    AccountId,
6    AccountStorage,
7    AccountStorageMode,
8    AccountType,
9    StorageSlot,
10};
11use miden_objects::asset::TokenSymbol;
12use miden_objects::{Felt, FieldElement, Word};
13
14use super::{BasicFungibleFaucet, FungibleFaucetError};
15use crate::account::auth::NoAuth;
16use crate::account::components::network_fungible_faucet_library;
17use crate::account::interface::{AccountComponentInterface, AccountInterface};
18use crate::procedure_digest;
19
20// NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT
21// ================================================================================================
22
23// Initialize the digest of the `distribute` procedure of the Network Fungible Faucet only once.
24procedure_digest!(
25    NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE,
26    NetworkFungibleFaucet::DISTRIBUTE_PROC_NAME,
27    network_fungible_faucet_library
28);
29
30// Initialize the digest of the `burn` procedure of the Network Fungible Faucet only once.
31procedure_digest!(
32    NETWORK_FUNGIBLE_FAUCET_BURN,
33    NetworkFungibleFaucet::BURN_PROC_NAME,
34    network_fungible_faucet_library
35);
36
37/// An [`AccountComponent`] implementing a network fungible faucet.
38///
39/// It reexports the procedures from `miden::contracts::faucets::network_fungible`. When linking
40/// against this component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be
41/// available to the assembler which is the case when using
42/// [`TransactionKernel::assembler()`][kasm]. The procedures of this component are:
43/// - `distribute`, which mints an assets and create a note for the provided recipient.
44/// - `burn`, which burns the provided asset.
45///
46/// Both `distribute` and `burn` can only be called from note scripts. `distribute` requires
47/// authentication while `burn` does not require authentication and can be called by anyone.
48/// Thus, this component must be combined with a component providing authentication.
49///
50/// This component supports accounts of type [`AccountType::FungibleFaucet`].
51///
52/// Unlike [`super::BasicFungibleFaucet`], this component uses two storage slots:
53/// - First slot: Token metadata `[max_supply, decimals, token_symbol, 0]`
54/// - Second slot: Owner account ID as a single Word
55///
56/// [kasm]: crate::transaction::TransactionKernel::assembler
57pub struct NetworkFungibleFaucet {
58    faucet: BasicFungibleFaucet,
59    owner_account_id: AccountId,
60}
61
62impl NetworkFungibleFaucet {
63    // CONSTANTS
64    // --------------------------------------------------------------------------------------------
65
66    /// The maximum number of decimals supported by the component.
67    pub const MAX_DECIMALS: u8 = 12;
68
69    const DISTRIBUTE_PROC_NAME: &str = "distribute";
70    const BURN_PROC_NAME: &str = "burn";
71
72    // CONSTRUCTORS
73    // --------------------------------------------------------------------------------------------
74
75    /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata.
76    ///
77    /// # Errors:
78    /// Returns an error if:
79    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
80    /// - the max supply parameter exceeds maximum possible amount for a fungible asset
81    ///   ([`miden_objects::asset::FungibleAsset::MAX_AMOUNT`])
82    pub fn new(
83        symbol: TokenSymbol,
84        decimals: u8,
85        max_supply: Felt,
86        owner_account_id: AccountId,
87    ) -> Result<Self, FungibleFaucetError> {
88        // Create the basic fungible faucet (this validates the metadata)
89        let faucet = BasicFungibleFaucet::new(symbol, decimals, max_supply)?;
90
91        Ok(Self { faucet, owner_account_id })
92    }
93
94    /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account
95    /// interface and storage.
96    ///
97    /// # Errors:
98    /// Returns an error if:
99    /// - the provided [`AccountInterface`] does not contain a
100    ///   [`AccountComponentInterface::NetworkFungibleFaucet`] component.
101    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
102    /// - the max supply value exceeds maximum possible amount for a fungible asset of
103    ///   [`miden_objects::asset::FungibleAsset::MAX_AMOUNT`].
104    /// - the token symbol encoded value exceeds the maximum value of
105    ///   [`TokenSymbol::MAX_ENCODED_VALUE`].
106    fn try_from_interface(
107        interface: AccountInterface,
108        storage: &AccountStorage,
109    ) -> Result<Self, FungibleFaucetError> {
110        for component in interface.components().iter() {
111            if let AccountComponentInterface::NetworkFungibleFaucet(offset) = component {
112                // obtain metadata from storage using offset provided by NetworkFungibleFaucet
113                // interface
114                let faucet_metadata = storage
115                    .get_item(*offset)
116                    .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
117                let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
118
119                // obtain owner account ID from the next storage slot
120                let owner_account_id_word: Word = storage
121                    .get_item(*offset + 1)
122                    .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset + 1))?;
123
124                // Convert Word back to AccountId
125                // Storage format: [0, 0, suffix, prefix]
126                let prefix = owner_account_id_word[3];
127                let suffix = owner_account_id_word[2];
128                let owner_account_id = AccountId::new_unchecked([prefix, suffix]);
129
130                // verify metadata values and create BasicFungibleFaucet
131                let token_symbol = TokenSymbol::try_from(token_symbol)
132                    .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
133                let decimals = decimals.as_int().try_into().map_err(|_| {
134                    FungibleFaucetError::TooManyDecimals {
135                        actual: decimals.as_int(),
136                        max: Self::MAX_DECIMALS,
137                    }
138                })?;
139
140                let faucet = BasicFungibleFaucet::new(token_symbol, decimals, max_supply)?;
141
142                return Ok(Self { faucet, owner_account_id });
143            }
144        }
145
146        Err(FungibleFaucetError::NoAvailableInterface)
147    }
148
149    // PUBLIC ACCESSORS
150    // --------------------------------------------------------------------------------------------
151
152    /// Returns the symbol of the faucet.
153    pub fn symbol(&self) -> TokenSymbol {
154        self.faucet.symbol()
155    }
156
157    /// Returns the decimals of the faucet.
158    pub fn decimals(&self) -> u8 {
159        self.faucet.decimals()
160    }
161
162    /// Returns the max supply of the faucet.
163    pub fn max_supply(&self) -> Felt {
164        self.faucet.max_supply()
165    }
166
167    /// Returns the owner account ID of the faucet.
168    pub fn owner_account_id(&self) -> AccountId {
169        self.owner_account_id
170    }
171
172    /// Returns the digest of the `distribute` account procedure.
173    pub fn distribute_digest() -> Word {
174        *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE
175    }
176
177    /// Returns the digest of the `burn` account procedure.
178    pub fn burn_digest() -> Word {
179        *NETWORK_FUNGIBLE_FAUCET_BURN
180    }
181}
182
183impl From<NetworkFungibleFaucet> for AccountComponent {
184    fn from(network_faucet: NetworkFungibleFaucet) -> Self {
185        // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as
186        // [a3, a2, a1, a0, ...]
187        let metadata = Word::new([
188            network_faucet.faucet.max_supply(),
189            Felt::from(network_faucet.faucet.decimals()),
190            network_faucet.faucet.symbol().into(),
191            Felt::ZERO,
192        ]);
193
194        // Convert AccountId to Word representation for storage
195        let owner_account_id_word: Word = [
196            Felt::new(0),
197            Felt::new(0),
198            network_faucet.owner_account_id.suffix(),
199            network_faucet.owner_account_id.prefix().as_felt(),
200        ]
201        .into();
202
203        // Second storage slot stores the owner account ID
204        let owner_slot = StorageSlot::Value(owner_account_id_word);
205
206        AccountComponent::new(
207            network_fungible_faucet_library(),
208            vec![StorageSlot::Value(metadata), owner_slot]
209        )
210            .expect("network fungible faucet component should satisfy the requirements of a valid account component")
211            .with_supported_type(AccountType::FungibleFaucet)
212    }
213}
214
215impl TryFrom<Account> for NetworkFungibleFaucet {
216    type Error = FungibleFaucetError;
217
218    fn try_from(account: Account) -> Result<Self, Self::Error> {
219        let account_interface = AccountInterface::from(&account);
220
221        NetworkFungibleFaucet::try_from_interface(account_interface, account.storage())
222    }
223}
224
225impl TryFrom<&Account> for NetworkFungibleFaucet {
226    type Error = FungibleFaucetError;
227
228    fn try_from(account: &Account) -> Result<Self, Self::Error> {
229        let account_interface = AccountInterface::from(account);
230
231        NetworkFungibleFaucet::try_from_interface(account_interface, account.storage())
232    }
233}
234
235/// Creates a new faucet account with network fungible faucet interface and provided metadata
236/// (token symbol, decimals, max supply, owner account ID).
237///
238/// The network faucet interface exposes two procedures:
239/// - `distribute`, which mints an assets and create a note for the provided recipient.
240/// - `burn`, which burns the provided asset.
241///
242/// Both `distribute` and `burn` can only be called from note scripts. `distribute` requires
243/// authentication using the NoAuth scheme. `burn` does not require authentication and can be
244/// called by anyone.
245///
246/// Network fungible faucets always use:
247/// - [`AccountStorageMode::Network`] for storage
248/// - [`NoAuth`] for authentication
249///
250/// The storage layout of the network faucet account is:
251/// - Slot 0: Reserved slot for faucets.
252/// - Slot 1: Public Key of the authentication component.
253/// - Slot 2: [num_tracked_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes,
254///   0].
255/// - Slot 3: A map with tracked procedure roots.
256/// - Slot 4: Token metadata of the faucet.
257/// - Slot 5: Owner account ID.
258pub fn create_network_fungible_faucet(
259    init_seed: [u8; 32],
260    symbol: TokenSymbol,
261    decimals: u8,
262    max_supply: Felt,
263    owner_account_id: AccountId,
264) -> Result<Account, FungibleFaucetError> {
265    let auth_component: AccountComponent = NoAuth::new().into();
266
267    let account = AccountBuilder::new(init_seed)
268        .account_type(AccountType::FungibleFaucet)
269        .storage_mode(AccountStorageMode::Network)
270        .with_auth_component(auth_component)
271        .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply, owner_account_id)?)
272        .build()
273        .map_err(FungibleFaucetError::AccountError)?;
274
275    Ok(account)
276}