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