Skip to main content

miden_standards/account/faucets/
basic_fungible.rs

1use miden_protocol::account::component::{
2    AccountComponentMetadata,
3    FeltSchema,
4    SchemaType,
5    StorageSchema,
6    StorageSlotSchema,
7};
8use miden_protocol::account::{
9    Account,
10    AccountBuilder,
11    AccountComponent,
12    AccountStorage,
13    AccountStorageMode,
14    AccountType,
15    StorageSlotName,
16};
17use miden_protocol::asset::TokenSymbol;
18use miden_protocol::{Felt, Word};
19
20use super::{FungibleFaucetError, TokenMetadata};
21use crate::account::AuthMethod;
22use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig};
23use crate::account::components::basic_fungible_faucet_library;
24use crate::account::mint_policies::AuthControlled;
25
26/// The schema type for token symbols.
27const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol";
28use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt};
29use crate::procedure_digest;
30
31// BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT
32// ================================================================================================
33
34// Initialize the digest of the `mint_and_send` procedure of the Basic Fungible Faucet only once.
35procedure_digest!(
36    BASIC_FUNGIBLE_FAUCET_MINT_AND_SEND,
37    BasicFungibleFaucet::NAME,
38    BasicFungibleFaucet::MINT_PROC_NAME,
39    basic_fungible_faucet_library
40);
41
42// Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once.
43procedure_digest!(
44    BASIC_FUNGIBLE_FAUCET_BURN,
45    BasicFungibleFaucet::NAME,
46    BasicFungibleFaucet::BURN_PROC_NAME,
47    basic_fungible_faucet_library
48);
49
50/// An [`AccountComponent`] implementing a basic fungible faucet.
51///
52/// It reexports the procedures from `miden::standards::faucets::basic_fungible`. When linking
53/// against this component, the `miden` library (i.e.
54/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the
55/// case when using [`CodeBuilder`][builder]. The procedures of this component are:
56/// - `mint_and_send`, which mints an assets and create a note for the provided recipient.
57/// - `burn`, which burns the provided asset.
58///
59/// The `mint_and_send` procedure can be called from a transaction script and requires
60/// authentication via the authentication component. The `burn` procedure can only be called from a
61/// note script and requires the calling note to contain the asset to be burned.
62/// This component must be combined with an authentication component.
63///
64/// This component supports accounts of type [`AccountType::FungibleFaucet`].
65///
66/// ## Storage Layout
67///
68/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`].
69///
70/// [builder]: crate::code_builder::CodeBuilder
71pub struct BasicFungibleFaucet {
72    metadata: TokenMetadata,
73}
74
75impl BasicFungibleFaucet {
76    // CONSTANTS
77    // --------------------------------------------------------------------------------------------
78
79    /// The name of the component.
80    pub const NAME: &'static str = "miden::standards::components::faucets::basic_fungible_faucet";
81
82    /// The maximum number of decimals supported by the component.
83    pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS;
84
85    const MINT_PROC_NAME: &str = "mint_and_send";
86    const BURN_PROC_NAME: &str = "burn";
87
88    // CONSTRUCTORS
89    // --------------------------------------------------------------------------------------------
90
91    /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata and with
92    /// an initial token supply of zero.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if:
97    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
98    /// - the max supply parameter exceeds maximum possible amount for a fungible asset
99    ///   ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`])
100    pub fn new(
101        symbol: TokenSymbol,
102        decimals: u8,
103        max_supply: Felt,
104    ) -> Result<Self, FungibleFaucetError> {
105        let metadata = TokenMetadata::new(symbol, decimals, max_supply)?;
106        Ok(Self { metadata })
107    }
108
109    /// Creates a new [`BasicFungibleFaucet`] component from the given [`TokenMetadata`].
110    ///
111    /// This is a convenience constructor that allows creating a faucet from pre-validated
112    /// metadata.
113    pub fn from_metadata(metadata: TokenMetadata) -> Self {
114        Self { metadata }
115    }
116
117    /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account
118    /// interface and storage.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if:
123    /// - the provided [`AccountInterface`] does not contain a
124    ///   [`AccountComponentInterface::BasicFungibleFaucet`] component.
125    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
126    /// - the max supply value exceeds maximum possible amount for a fungible asset of
127    ///   [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`].
128    /// - the token supply exceeds the max supply.
129    /// - the token symbol encoded value exceeds the maximum value of
130    ///   [`TokenSymbol::MAX_ENCODED_VALUE`].
131    fn try_from_interface(
132        interface: AccountInterface,
133        storage: &AccountStorage,
134    ) -> Result<Self, FungibleFaucetError> {
135        // Check that the procedures of the basic fungible faucet exist in the account.
136        if !interface.components().contains(&AccountComponentInterface::BasicFungibleFaucet) {
137            return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface);
138        }
139
140        let metadata = TokenMetadata::try_from(storage)?;
141        Ok(Self { metadata })
142    }
143
144    // PUBLIC ACCESSORS
145    // --------------------------------------------------------------------------------------------
146
147    /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored.
148    pub fn metadata_slot() -> &'static StorageSlotName {
149        TokenMetadata::metadata_slot()
150    }
151
152    /// Returns the storage slot schema for the metadata slot.
153    pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
154        let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type");
155        (
156            Self::metadata_slot().clone(),
157            StorageSlotSchema::value(
158                "Token metadata",
159                [
160                    FeltSchema::felt("token_supply").with_default(Felt::new(0)),
161                    FeltSchema::felt("max_supply"),
162                    FeltSchema::u8("decimals"),
163                    FeltSchema::new_typed(token_symbol_type, "symbol"),
164                ],
165            ),
166        )
167    }
168
169    /// Returns the token metadata.
170    pub fn metadata(&self) -> &TokenMetadata {
171        &self.metadata
172    }
173
174    /// Returns the symbol of the faucet.
175    pub fn symbol(&self) -> &TokenSymbol {
176        self.metadata.symbol()
177    }
178
179    /// Returns the decimals of the faucet.
180    pub fn decimals(&self) -> u8 {
181        self.metadata.decimals()
182    }
183
184    /// Returns the max supply (in base units) of the faucet.
185    ///
186    /// This is the highest amount of tokens that can be minted from this faucet.
187    pub fn max_supply(&self) -> Felt {
188        self.metadata.max_supply()
189    }
190
191    /// Returns the token supply (in base units) of the faucet.
192    ///
193    /// This is the amount of tokens that were minted from the faucet so far. Its value can never
194    /// exceed [`Self::max_supply`].
195    pub fn token_supply(&self) -> Felt {
196        self.metadata.token_supply()
197    }
198
199    /// Returns the digest of the `mint_and_send` account procedure.
200    pub fn mint_and_send_digest() -> Word {
201        *BASIC_FUNGIBLE_FAUCET_MINT_AND_SEND
202    }
203
204    /// Returns the digest of the `burn` account procedure.
205    pub fn burn_digest() -> Word {
206        *BASIC_FUNGIBLE_FAUCET_BURN
207    }
208
209    /// Returns the [`AccountComponentMetadata`] for this component.
210    pub fn component_metadata() -> AccountComponentMetadata {
211        let storage_schema = StorageSchema::new([Self::metadata_slot_schema()])
212            .expect("storage schema should be valid");
213
214        AccountComponentMetadata::new(Self::NAME, [AccountType::FungibleFaucet])
215            .with_description("Basic fungible faucet component for minting and burning tokens")
216            .with_storage_schema(storage_schema)
217    }
218
219    // MUTATORS
220    // --------------------------------------------------------------------------------------------
221
222    /// Sets the token_supply (in base units) of the basic fungible faucet.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if:
227    /// - the token supply exceeds the max supply.
228    pub fn with_token_supply(mut self, token_supply: Felt) -> Result<Self, FungibleFaucetError> {
229        self.metadata = self.metadata.with_token_supply(token_supply)?;
230        Ok(self)
231    }
232}
233
234impl From<BasicFungibleFaucet> for AccountComponent {
235    fn from(faucet: BasicFungibleFaucet) -> Self {
236        let storage_slot = faucet.metadata.into();
237        let metadata = BasicFungibleFaucet::component_metadata();
238
239        AccountComponent::new(basic_fungible_faucet_library(), vec![storage_slot], metadata)
240            .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
241    }
242}
243
244impl TryFrom<Account> for BasicFungibleFaucet {
245    type Error = FungibleFaucetError;
246
247    fn try_from(account: Account) -> Result<Self, Self::Error> {
248        let account_interface = AccountInterface::from_account(&account);
249
250        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
251    }
252}
253
254impl TryFrom<&Account> for BasicFungibleFaucet {
255    type Error = FungibleFaucetError;
256
257    fn try_from(account: &Account) -> Result<Self, Self::Error> {
258        let account_interface = AccountInterface::from_account(account);
259
260        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
261    }
262}
263
264/// Creates a new faucet account with basic fungible faucet interface,
265/// account storage type, specified authentication scheme, and provided meta data (token symbol,
266/// decimals, max supply).
267///
268/// The basic faucet interface exposes two procedures:
269/// - `mint_and_send`, which mints an assets and create a note for the provided recipient.
270/// - `burn`, which burns the provided asset.
271///
272/// The `mint_and_send` procedure can be called from a transaction script and requires
273/// authentication via the specified authentication scheme. The `burn` procedure can only be called
274/// from a note script and requires the calling note to contain the asset to be burned.
275///
276/// The storage layout of the faucet account is defined by the combination of the following
277/// components (see their docs for details):
278/// - [`BasicFungibleFaucet`]
279/// - [`AuthSingleSigAcl`]
280/// - [`AuthControlled`]
281pub fn create_basic_fungible_faucet(
282    init_seed: [u8; 32],
283    symbol: TokenSymbol,
284    decimals: u8,
285    max_supply: Felt,
286    account_storage_mode: AccountStorageMode,
287    auth_method: AuthMethod,
288) -> Result<Account, FungibleFaucetError> {
289    let mint_proc_root = BasicFungibleFaucet::mint_and_send_digest();
290
291    let auth_component: AccountComponent = match auth_method {
292        AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => AuthSingleSigAcl::new(
293            pub_key,
294            auth_scheme,
295            AuthSingleSigAclConfig::new()
296                .with_auth_trigger_procedures(vec![mint_proc_root])
297                .with_allow_unauthorized_input_notes(true),
298        )
299        .map_err(FungibleFaucetError::AccountError)?
300        .into(),
301        AuthMethod::NoAuth => {
302            return Err(FungibleFaucetError::UnsupportedAuthMethod(
303                "basic fungible faucets cannot be created with NoAuth authentication method".into(),
304            ));
305        },
306        AuthMethod::Unknown => {
307            return Err(FungibleFaucetError::UnsupportedAuthMethod(
308                "basic fungible faucets cannot be created with Unknown authentication method"
309                    .into(),
310            ));
311        },
312        AuthMethod::Multisig { .. } => {
313            return Err(FungibleFaucetError::UnsupportedAuthMethod(
314                "basic fungible faucets do not support Multisig authentication".into(),
315            ));
316        },
317    };
318
319    let account = AccountBuilder::new(init_seed)
320        .account_type(AccountType::FungibleFaucet)
321        .storage_mode(account_storage_mode)
322        .with_auth_component(auth_component)
323        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
324        .with_component(AuthControlled::allow_all())
325        .build()
326        .map_err(FungibleFaucetError::AccountError)?;
327
328    Ok(account)
329}
330
331// TESTS
332// ================================================================================================
333
334#[cfg(test)]
335mod tests {
336    use assert_matches::assert_matches;
337    use miden_protocol::Word;
338    use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
339
340    use super::{
341        AccountBuilder,
342        AccountStorageMode,
343        AccountType,
344        AuthMethod,
345        BasicFungibleFaucet,
346        Felt,
347        FungibleFaucetError,
348        TokenSymbol,
349        create_basic_fungible_faucet,
350    };
351    use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl};
352    use crate::account::wallets::BasicWallet;
353
354    #[test]
355    fn faucet_contract_creation() {
356        let pub_key_word = Word::new([Felt::ONE; 4]);
357        let auth_method: AuthMethod = AuthMethod::SingleSig {
358            approver: (pub_key_word.into(), AuthScheme::Falcon512Poseidon2),
359        };
360
361        // we need to use an initial seed to create the wallet account
362        let init_seed: [u8; 32] = [
363            90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
364            183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
365        ];
366
367        let max_supply = Felt::new(123);
368        let token_symbol_string = "POL";
369        let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
370        let decimals = 2u8;
371        let storage_mode = AccountStorageMode::Private;
372
373        let token_symbol_felt = token_symbol.as_element();
374        let faucet_account = create_basic_fungible_faucet(
375            init_seed,
376            token_symbol.clone(),
377            decimals,
378            max_supply,
379            storage_mode,
380            auth_method,
381        )
382        .unwrap();
383
384        // The falcon auth component's public key should be present.
385        assert_eq!(
386            faucet_account.storage().get_item(AuthSingleSigAcl::public_key_slot()).unwrap(),
387            pub_key_word
388        );
389
390        // The config slot of the auth component stores:
391        // [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, 0].
392        //
393        // With 1 trigger procedure (mint_and_send), allow_unauthorized_output_notes=false, and
394        // allow_unauthorized_input_notes=true, this should be [1, 0, 1, 0].
395        assert_eq!(
396            faucet_account.storage().get_item(AuthSingleSigAcl::config_slot()).unwrap(),
397            [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
398        );
399
400        // The procedure root map should contain the mint_and_send procedure root.
401        let mint_root = BasicFungibleFaucet::mint_and_send_digest();
402        assert_eq!(
403            faucet_account
404                .storage()
405                .get_map_item(
406                    AuthSingleSigAcl::trigger_procedure_roots_slot(),
407                    [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
408                )
409                .unwrap(),
410            mint_root
411        );
412
413        // Check that faucet metadata was initialized to the given values.
414        // Storage layout: [token_supply, max_supply, decimals, symbol]
415        assert_eq!(
416            faucet_account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(),
417            [Felt::ZERO, Felt::new(123), Felt::new(2), token_symbol_felt].into()
418        );
419
420        assert!(faucet_account.is_faucet());
421
422        assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet);
423
424        // Verify the faucet can be extracted and has correct metadata
425        let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap();
426        assert_eq!(faucet_component.symbol(), &token_symbol);
427        assert_eq!(faucet_component.decimals(), decimals);
428        assert_eq!(faucet_component.max_supply(), max_supply);
429        assert_eq!(faucet_component.token_supply(), Felt::ZERO);
430    }
431
432    #[test]
433    fn faucet_create_from_account() {
434        // prepare the test data
435        let mock_word = Word::from([0, 1, 2, 3u32]);
436        let mock_public_key = PublicKeyCommitment::from(mock_word);
437        let mock_seed = mock_word.as_bytes();
438
439        // valid account
440        let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
441        let faucet_account = AccountBuilder::new(mock_seed)
442            .account_type(AccountType::FungibleFaucet)
443            .with_component(
444                BasicFungibleFaucet::new(token_symbol.clone(), 10, Felt::new(100))
445                    .expect("failed to create a fungible faucet component"),
446            )
447            .with_auth_component(AuthSingleSig::new(
448                mock_public_key,
449                AuthScheme::Falcon512Poseidon2,
450            ))
451            .build_existing()
452            .expect("failed to create wallet account");
453
454        let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
455            .expect("basic fungible faucet creation failed");
456        assert_eq!(basic_ff.symbol(), &token_symbol);
457        assert_eq!(basic_ff.decimals(), 10);
458        assert_eq!(basic_ff.max_supply(), Felt::new(100));
459        assert_eq!(basic_ff.token_supply(), Felt::ZERO);
460
461        // invalid account: basic fungible faucet component is missing
462        let invalid_faucet_account = AccountBuilder::new(mock_seed)
463            .account_type(AccountType::FungibleFaucet)
464            .with_auth_component(AuthSingleSig::new(mock_public_key, AuthScheme::Falcon512Poseidon2))
465            // we need to add some other component so the builder doesn't fail
466            .with_component(BasicWallet)
467            .build_existing()
468            .expect("failed to create wallet account");
469
470        let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
471            .err()
472            .expect("basic fungible faucet creation should fail");
473        assert_matches!(err, FungibleFaucetError::MissingBasicFungibleFaucetInterface);
474    }
475
476    /// Check that the obtaining of the basic fungible faucet procedure digests does not panic.
477    #[test]
478    fn get_faucet_procedures() {
479        let _mint_and_send_digest = BasicFungibleFaucet::mint_and_send_digest();
480        let _burn_digest = BasicFungibleFaucet::burn_digest();
481    }
482}