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