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
122impl From<BasicFungibleFaucet> for AccountComponent {
123    fn from(faucet: BasicFungibleFaucet) -> Self {
124        // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as
125        // [a3, a2, a1, a0, ...]
126        let metadata =
127            [faucet.max_supply, Felt::from(faucet.decimals), faucet.symbol.into(), Felt::ZERO];
128
129        AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
130            .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
131            .with_supported_type(AccountType::FungibleFaucet)
132    }
133}
134
135impl TryFrom<Account> for BasicFungibleFaucet {
136    type Error = FungibleFaucetError;
137
138    fn try_from(account: Account) -> Result<Self, Self::Error> {
139        let account_interface = AccountInterface::from(&account);
140
141        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
142    }
143}
144
145// FUNGIBLE FAUCET
146// ================================================================================================
147
148/// Creates a new faucet account with basic fungible faucet interface,
149/// account storage type, specified authentication scheme, and provided meta data (token symbol,
150/// decimals, max supply).
151///
152/// The basic faucet interface exposes two procedures:
153/// - `distribute`, which mints an assets and create a note for the provided recipient.
154/// - `burn`, which burns the provided asset.
155///
156/// `distribute` requires authentication. The authentication procedure is defined by the specified
157/// authentication scheme. `burn` does not require authentication and can be called by anyone.
158///
159/// The storage layout of the faucet account is:
160/// - Slot 0: Reserved slot for faucets.
161/// - Slot 1: Public Key of the authentication component.
162/// - Slot 2: Token metadata of the faucet.
163pub fn create_basic_fungible_faucet(
164    init_seed: [u8; 32],
165    id_anchor: AccountIdAnchor,
166    symbol: TokenSymbol,
167    decimals: u8,
168    max_supply: Felt,
169    account_storage_mode: AccountStorageMode,
170    auth_scheme: AuthScheme,
171) -> Result<(Account, Word), AccountError> {
172    // Atm we only have RpoFalcon512 as authentication scheme and this is also the default in the
173    // faucet contract.
174    let auth_component: RpoFalcon512 = match auth_scheme {
175        AuthScheme::RpoFalcon512 { pub_key } => RpoFalcon512::new(pub_key),
176    };
177
178    let (account, account_seed) = AccountBuilder::new(init_seed)
179        .anchor(id_anchor)
180        .account_type(AccountType::FungibleFaucet)
181        .storage_mode(account_storage_mode)
182        .with_component(auth_component)
183        .with_component(
184            BasicFungibleFaucet::new(symbol, decimals, max_supply)
185                .map_err(AccountError::FungibleFaucetError)?,
186        )
187        .build()?;
188
189    Ok((account, account_seed))
190}
191
192// TESTS
193// ================================================================================================
194
195#[cfg(test)]
196mod tests {
197    use assert_matches::assert_matches;
198    use miden_objects::{
199        Digest, FieldElement, FungibleFaucetError, ONE, Word, ZERO,
200        block::BlockHeader,
201        crypto::dsa::rpo_falcon512::{self, PublicKey},
202        digest,
203    };
204
205    use super::{
206        AccountBuilder, AccountStorageMode, AccountType, AuthScheme, BasicFungibleFaucet, Felt,
207        TokenSymbol, create_basic_fungible_faucet,
208    };
209    use crate::account::auth::RpoFalcon512;
210
211    #[test]
212    fn faucet_contract_creation() {
213        let pub_key = rpo_falcon512::PublicKey::new([ONE; 4]);
214        let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key };
215
216        // we need to use an initial seed to create the wallet account
217        let init_seed: [u8; 32] = [
218            90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
219            183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
220        ];
221
222        let max_supply = Felt::new(123);
223        let token_symbol_string = "POL";
224        let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
225        let decimals = 2u8;
226        let storage_mode = AccountStorageMode::Private;
227
228        let anchor_block_header_mock = BlockHeader::mock(
229            0,
230            Some(digest!("0xaa")),
231            Some(digest!("0xbb")),
232            &[],
233            digest!("0xcc"),
234        );
235
236        let (faucet_account, _) = create_basic_fungible_faucet(
237            init_seed,
238            (&anchor_block_header_mock).try_into().unwrap(),
239            token_symbol,
240            decimals,
241            max_supply,
242            storage_mode,
243            auth_scheme,
244        )
245        .unwrap();
246
247        // The reserved faucet slot should be initialized to an empty word.
248        assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::default().into());
249
250        // The falcon auth component is added first so its assigned storage slot for the public key
251        // will be 1.
252        assert_eq!(faucet_account.storage().get_item(1).unwrap(), Word::from(pub_key).into());
253
254        // Check that faucet metadata was initialized to the given values. The faucet component is
255        // added second, so its assigned storage slot for the metadata will be 2.
256        assert_eq!(
257            faucet_account.storage().get_item(2).unwrap(),
258            [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
259        );
260
261        assert!(faucet_account.is_faucet());
262    }
263
264    #[test]
265    fn faucet_create_from_account() {
266        // prepare the test data
267        let mock_public_key = PublicKey::new([ZERO, ONE, Felt::new(2), Felt::new(3)]);
268        let mock_seed = Digest::from([ZERO, ONE, Felt::new(2), Felt::new(3)]).as_bytes();
269
270        // valid account
271        let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
272        let faucet_account = AccountBuilder::new(mock_seed)
273            .account_type(AccountType::FungibleFaucet)
274            .with_component(
275                BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
276                    .expect("failed to create a fungible faucet component"),
277            )
278            .with_component(RpoFalcon512::new(mock_public_key))
279            .build_existing()
280            .expect("failed to create wallet account");
281
282        let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
283            .expect("basic fungible faucet creation failed");
284        assert_eq!(basic_ff.symbol, token_symbol);
285        assert_eq!(basic_ff.decimals, 10);
286        assert_eq!(basic_ff.max_supply, Felt::new(100));
287
288        // invalid account: basic fungible faucet component is missing
289        let invalid_faucet_account = AccountBuilder::new(mock_seed)
290            .account_type(AccountType::FungibleFaucet)
291            .with_component(RpoFalcon512::new(mock_public_key))
292            .build_existing()
293            .expect("failed to create wallet account");
294
295        let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
296            .err()
297            .expect("basic fungible faucet creation should fail");
298        assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
299    }
300}