miden_lib/account/faucets/
mod.rs

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