miden_lib/account/faucets/
mod.rs

1use alloc::string::String;
2
3use miden_objects::account::{
4    Account,
5    AccountBuilder,
6    AccountComponent,
7    AccountStorage,
8    AccountStorageMode,
9    AccountType,
10    StorageSlot,
11};
12use miden_objects::assembly::{ProcedureName, QualifiedProcedureName};
13use miden_objects::asset::{FungibleAsset, TokenSymbol};
14use miden_objects::utils::sync::LazyLock;
15use miden_objects::{AccountError, Felt, FieldElement, TokenSymbolError, Word};
16use thiserror::Error;
17
18use super::AuthScheme;
19use super::interface::{AccountComponentInterface, AccountInterface};
20use crate::account::auth::{
21    AuthRpoFalcon512Acl,
22    AuthRpoFalcon512AclConfig,
23    AuthRpoFalcon512Multisig,
24};
25use crate::account::components::basic_fungible_faucet_library;
26use crate::transaction::memory::FAUCET_STORAGE_DATA_SLOT;
27
28// BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT
29// ================================================================================================
30
31// Initialize the digest of the `distribute` procedure of the Basic Fungible Faucet only once.
32static BASIC_FUNGIBLE_FAUCET_DISTRIBUTE: LazyLock<Word> = LazyLock::new(|| {
33    let distribute_proc_name = QualifiedProcedureName::new(
34        Default::default(),
35        ProcedureName::new(BasicFungibleFaucet::DISTRIBUTE_PROC_NAME)
36            .expect("failed to create name for 'distribute' procedure"),
37    );
38    basic_fungible_faucet_library()
39        .get_procedure_root_by_name(distribute_proc_name)
40        .expect("Basic Fungible Faucet should contain 'distribute' procedure")
41});
42
43// Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once.
44static BASIC_FUNGIBLE_FAUCET_BURN: LazyLock<Word> = LazyLock::new(|| {
45    let burn_proc_name = QualifiedProcedureName::new(
46        Default::default(),
47        ProcedureName::new(BasicFungibleFaucet::BURN_PROC_NAME)
48            .expect("failed to create name for 'burn' procedure"),
49    );
50    basic_fungible_faucet_library()
51        .get_procedure_root_by_name(burn_proc_name)
52        .expect("Basic Fungible Faucet should contain 'burn' procedure")
53});
54
55/// An [`AccountComponent`] implementing a basic fungible faucet.
56///
57/// It reexports the procedures from `miden::contracts::faucets::basic_fungible`. When linking
58/// against this component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be
59/// available to the assembler which is the case when using
60/// [`TransactionKernel::assembler()`][kasm]. The procedures of this component are:
61/// - `distribute`, which mints an assets and create a note for the provided recipient.
62/// - `burn`, which burns the provided asset.
63///
64/// `distribute` requires authentication while `burn` does not require authentication and can be
65/// called by anyone. Thus, this component must be combined with a component providing
66/// authentication.
67///
68/// This component supports accounts of type [`AccountType::FungibleFaucet`].
69///
70/// [kasm]: crate::transaction::TransactionKernel::assembler
71pub struct BasicFungibleFaucet {
72    symbol: TokenSymbol,
73    decimals: u8,
74    max_supply: Felt,
75}
76
77impl BasicFungibleFaucet {
78    // CONSTANTS
79    // --------------------------------------------------------------------------------------------
80
81    /// The maximum number of decimals supported by the component.
82    pub const MAX_DECIMALS: u8 = 12;
83
84    const DISTRIBUTE_PROC_NAME: &str = "distribute";
85    const BURN_PROC_NAME: &str = "burn";
86
87    // CONSTRUCTORS
88    // --------------------------------------------------------------------------------------------
89
90    /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata.
91    ///
92    /// # Errors:
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    ///   ([`FungibleAsset::MAX_AMOUNT`])
97    pub fn new(
98        symbol: TokenSymbol,
99        decimals: u8,
100        max_supply: Felt,
101    ) -> Result<Self, FungibleFaucetError> {
102        // First check that the metadata is valid.
103        if decimals > Self::MAX_DECIMALS {
104            return Err(FungibleFaucetError::TooManyDecimals {
105                actual: decimals as u64,
106                max: Self::MAX_DECIMALS,
107            });
108        } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
109            return Err(FungibleFaucetError::MaxSupplyTooLarge {
110                actual: max_supply.as_int(),
111                max: FungibleAsset::MAX_AMOUNT,
112            });
113        }
114
115        Ok(Self { symbol, decimals, max_supply })
116    }
117
118    /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account
119    /// interface and storage.
120    ///
121    /// # Errors:
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    ///   [`FungibleAsset::MAX_AMOUNT`].
128    /// - the token symbol encoded value exceeds the maximum value of
129    ///   [`TokenSymbol::MAX_ENCODED_VALUE`].
130    fn try_from_interface(
131        interface: AccountInterface,
132        storage: &AccountStorage,
133    ) -> Result<Self, FungibleFaucetError> {
134        for component in interface.components().iter() {
135            if let AccountComponentInterface::BasicFungibleFaucet(offset) = component {
136                // obtain metadata from storage using offset provided by BasicFungibleFaucet
137                // interface
138                let faucet_metadata = storage
139                    .get_item(*offset)
140                    .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
141                let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
142
143                // verify metadata values
144                let token_symbol = TokenSymbol::try_from(token_symbol)
145                    .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
146                let decimals = decimals.as_int().try_into().map_err(|_| {
147                    FungibleFaucetError::TooManyDecimals {
148                        actual: decimals.as_int(),
149                        max: Self::MAX_DECIMALS,
150                    }
151                })?;
152
153                return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
154            }
155        }
156
157        Err(FungibleFaucetError::NoAvailableInterface)
158    }
159
160    // PUBLIC ACCESSORS
161    // --------------------------------------------------------------------------------------------
162
163    /// Returns the symbol of the faucet.
164    pub fn symbol(&self) -> TokenSymbol {
165        self.symbol
166    }
167
168    /// Returns the decimals of the faucet.
169    pub fn decimals(&self) -> u8 {
170        self.decimals
171    }
172
173    /// Returns the max supply of the faucet.
174    pub fn max_supply(&self) -> Felt {
175        self.max_supply
176    }
177
178    /// Returns the digest of the `distribute` account procedure.
179    pub fn distribute_digest() -> Word {
180        *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE
181    }
182
183    /// Returns the digest of the `burn` account procedure.
184    pub fn burn_digest() -> Word {
185        *BASIC_FUNGIBLE_FAUCET_BURN
186    }
187}
188
189impl From<BasicFungibleFaucet> for AccountComponent {
190    fn from(faucet: BasicFungibleFaucet) -> Self {
191        // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as
192        // [a3, a2, a1, a0, ...]
193        let metadata = Word::new([
194            faucet.max_supply,
195            Felt::from(faucet.decimals),
196            faucet.symbol.into(),
197            Felt::ZERO,
198        ]);
199
200        AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
201            .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
202            .with_supported_type(AccountType::FungibleFaucet)
203    }
204}
205
206impl TryFrom<Account> for BasicFungibleFaucet {
207    type Error = FungibleFaucetError;
208
209    fn try_from(account: Account) -> Result<Self, Self::Error> {
210        let account_interface = AccountInterface::from(&account);
211
212        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
213    }
214}
215
216impl TryFrom<&Account> for BasicFungibleFaucet {
217    type Error = FungibleFaucetError;
218
219    fn try_from(account: &Account) -> Result<Self, Self::Error> {
220        let account_interface = AccountInterface::from(account);
221
222        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
223    }
224}
225
226// FUNGIBLE FAUCET
227// ================================================================================================
228
229/// Extension trait for fungible faucet accounts. Provides methods to access the fungible faucet
230/// account's reserved storage slot.
231pub trait FungibleFaucetExt {
232    const ISSUANCE_ELEMENT_INDEX: usize;
233    const ISSUANCE_STORAGE_SLOT: u8;
234
235    /// Returns the amount of tokens (in base units) issued from this fungible faucet.
236    ///
237    /// # Errors
238    /// Returns an error if the account is not a fungible faucet account.
239    fn get_token_issuance(&self) -> Result<Felt, FungibleFaucetError>;
240}
241
242impl FungibleFaucetExt for Account {
243    const ISSUANCE_ELEMENT_INDEX: usize = 3;
244    const ISSUANCE_STORAGE_SLOT: u8 = FAUCET_STORAGE_DATA_SLOT;
245
246    fn get_token_issuance(&self) -> Result<Felt, FungibleFaucetError> {
247        if self.account_type() != AccountType::FungibleFaucet {
248            return Err(FungibleFaucetError::NotAFungibleFaucetAccount);
249        }
250
251        let slot = self
252            .storage()
253            .get_item(Self::ISSUANCE_STORAGE_SLOT)
254            .map_err(|_| FungibleFaucetError::InvalidStorageOffset(Self::ISSUANCE_STORAGE_SLOT))?;
255        Ok(slot[Self::ISSUANCE_ELEMENT_INDEX])
256    }
257}
258
259/// Creates a new faucet account with basic fungible faucet interface,
260/// account storage type, specified authentication scheme, and provided meta data (token symbol,
261/// decimals, max supply).
262///
263/// The basic faucet interface exposes two procedures:
264/// - `distribute`, which mints an assets and create a note for the provided recipient.
265/// - `burn`, which burns the provided asset.
266///
267/// `distribute` requires authentication. The authentication procedure is defined by the specified
268/// authentication scheme. `burn` does not require authentication and can be called by anyone.
269///
270/// The storage layout of the faucet account is:
271/// - Slot 0: Reserved slot for faucets.
272/// - Slot 1: Public Key of the authentication component.
273/// - Slot 2: [num_tracked_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes,
274///   0].
275/// - Slot 3: A map with tracked procedure roots.
276/// - Slot 4: Token metadata of the faucet.
277pub fn create_basic_fungible_faucet(
278    init_seed: [u8; 32],
279    symbol: TokenSymbol,
280    decimals: u8,
281    max_supply: Felt,
282    account_storage_mode: AccountStorageMode,
283    auth_scheme: AuthScheme,
284) -> Result<(Account, Word), FungibleFaucetError> {
285    let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
286
287    let auth_component: AccountComponent = match auth_scheme {
288        AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new(
289            pub_key,
290            AuthRpoFalcon512AclConfig::new()
291                .with_auth_trigger_procedures(vec![distribute_proc_root])
292                .with_allow_unauthorized_input_notes(true),
293        )
294        .map_err(FungibleFaucetError::AccountError)?
295        .into(),
296        AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } => {
297            AuthRpoFalcon512Multisig::new(threshold, pub_keys)
298                .map_err(FungibleFaucetError::AccountError)?
299                .into()
300        },
301        AuthScheme::NoAuth => {
302            return Err(FungibleFaucetError::UnsupportedAuthScheme(
303                "basic fungible faucets cannot be created with NoAuth authentication scheme".into(),
304            ));
305        },
306        AuthScheme::Unknown => {
307            return Err(FungibleFaucetError::UnsupportedAuthScheme(
308                "basic fungible faucets cannot be created with Unknown authentication scheme"
309                    .into(),
310            ));
311        },
312    };
313
314    let (account, account_seed) = AccountBuilder::new(init_seed)
315        .account_type(AccountType::FungibleFaucet)
316        .storage_mode(account_storage_mode)
317        .with_auth_component(auth_component)
318        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
319        .build()
320        .map_err(FungibleFaucetError::AccountError)?;
321
322    Ok((account, account_seed))
323}
324
325// FUNGIBLE FAUCET ERROR
326// ================================================================================================
327
328/// Basic fungible faucet related errors.
329#[derive(Debug, Error)]
330pub enum FungibleFaucetError {
331    #[error("faucet metadata decimals is {actual} which exceeds max value of {max}")]
332    TooManyDecimals { actual: u64, max: u8 },
333    #[error("faucet metadata max supply is {actual} which exceeds max value of {max}")]
334    MaxSupplyTooLarge { actual: u64, max: u64 },
335    #[error(
336        "account interface provided for faucet creation does not have basic fungible faucet component"
337    )]
338    NoAvailableInterface,
339    #[error("storage offset `{0}` is invalid")]
340    InvalidStorageOffset(u8),
341    #[error("invalid token symbol")]
342    InvalidTokenSymbol(#[source] TokenSymbolError),
343    #[error("unsupported authentication scheme: {0}")]
344    UnsupportedAuthScheme(String),
345    #[error("account creation failed")]
346    AccountError(#[source] AccountError),
347    #[error("account is not a fungible faucet account")]
348    NotAFungibleFaucetAccount,
349}
350
351// TESTS
352// ================================================================================================
353
354#[cfg(test)]
355mod tests {
356    use assert_matches::assert_matches;
357    use miden_objects::crypto::dsa::rpo_falcon512::{self, PublicKey};
358    use miden_objects::{FieldElement, ONE, Word};
359
360    use super::{
361        AccountBuilder,
362        AccountStorageMode,
363        AccountType,
364        AuthScheme,
365        BasicFungibleFaucet,
366        Felt,
367        FungibleFaucetError,
368        TokenSymbol,
369        create_basic_fungible_faucet,
370    };
371    use crate::account::auth::AuthRpoFalcon512;
372    use crate::account::wallets::BasicWallet;
373
374    #[test]
375    fn faucet_contract_creation() {
376        let pub_key = rpo_falcon512::PublicKey::new(Word::new([ONE; 4]));
377        let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key };
378
379        // we need to use an initial seed to create the wallet account
380        let init_seed: [u8; 32] = [
381            90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
382            183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
383        ];
384
385        let max_supply = Felt::new(123);
386        let token_symbol_string = "POL";
387        let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
388        let decimals = 2u8;
389        let storage_mode = AccountStorageMode::Private;
390
391        let (faucet_account, _) = create_basic_fungible_faucet(
392            init_seed,
393            token_symbol,
394            decimals,
395            max_supply,
396            storage_mode,
397            auth_scheme,
398        )
399        .unwrap();
400
401        // The reserved faucet slot should be initialized to an empty word.
402        assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::empty());
403
404        // The falcon auth component is added first so its assigned storage slot for the public key
405        // will be 1.
406        assert_eq!(faucet_account.storage().get_item(1).unwrap(), Word::from(pub_key));
407
408        // Slot 2 stores [num_tracked_procs, allow_unauthorized_output_notes,
409        // allow_unauthorized_input_notes, 0]. With 1 tracked procedure (distribute),
410        // allow_unauthorized_output_notes=false, and allow_unauthorized_input_notes=true,
411        // this should be [1, 0, 1, 0].
412        assert_eq!(
413            faucet_account.storage().get_item(2).unwrap(),
414            [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
415        );
416
417        // The procedure root map in slot 3 should contain the distribute procedure root.
418        let distribute_root = BasicFungibleFaucet::distribute_digest();
419        assert_eq!(
420            faucet_account
421                .storage()
422                .get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into())
423                .unwrap(),
424            distribute_root
425        );
426
427        // Check that faucet metadata was initialized to the given values. The faucet component is
428        // added second, so its assigned storage slot for the metadata will be 2.
429        assert_eq!(
430            faucet_account.storage().get_item(4).unwrap(),
431            [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
432        );
433
434        assert!(faucet_account.is_faucet());
435    }
436
437    #[test]
438    fn faucet_create_from_account() {
439        // prepare the test data
440        let mock_public_key = PublicKey::new(Word::from([0, 1, 2, 3u32]));
441        let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes();
442
443        // valid account
444        let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
445        let faucet_account = AccountBuilder::new(mock_seed)
446            .account_type(AccountType::FungibleFaucet)
447            .with_component(
448                BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
449                    .expect("failed to create a fungible faucet component"),
450            )
451            .with_auth_component(AuthRpoFalcon512::new(mock_public_key))
452            .build_existing()
453            .expect("failed to create wallet account");
454
455        let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
456            .expect("basic fungible faucet creation failed");
457        assert_eq!(basic_ff.symbol, token_symbol);
458        assert_eq!(basic_ff.decimals, 10);
459        assert_eq!(basic_ff.max_supply, Felt::new(100));
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(AuthRpoFalcon512::new(mock_public_key))
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::NoAvailableInterface);
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 _distribute_digest = BasicFungibleFaucet::distribute_digest();
480        let _burn_digest = BasicFungibleFaucet::burn_digest();
481    }
482}