miden_lib/account/faucets/
mod.rs

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