miden_lib/account/faucets/
mod.rs1use 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
18pub struct BasicFungibleFaucet {
38 symbol: TokenSymbol,
39 decimals: u8,
40 max_supply: Felt,
41}
42
43impl BasicFungibleFaucet {
44 pub const MAX_DECIMALS: u8 = 12;
49
50 const DISTRIBUTE_PROC_NAME: &str = "distribute";
51 const BURN_PROC_NAME: &str = "burn";
52
53 pub fn new(
64 symbol: TokenSymbol,
65 decimals: u8,
66 max_supply: Felt,
67 ) -> Result<Self, FungibleFaucetError> {
68 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 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 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 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 pub fn symbol(&self) -> TokenSymbol {
131 self.symbol
132 }
133
134 pub fn decimals(&self) -> u8 {
136 self.decimals
137 }
138
139 pub fn max_supply(&self) -> Felt {
141 self.max_supply
142 }
143
144 pub fn distribute_digest() -> Digest {
146 Self::get_procedure_digest_by_name(Self::DISTRIBUTE_PROC_NAME)
147 }
148
149 pub fn burn_digest() -> Digest {
151 Self::get_procedure_digest_by_name(Self::BURN_PROC_NAME)
152 }
153
154 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 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
204pub 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#[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#[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 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 assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::default().into());
320
321 assert_eq!(faucet_account.storage().get_item(1).unwrap(), Word::from(pub_key).into());
324
325 assert_eq!(
327 faucet_account.storage().get_item(2).unwrap(),
328 [Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
329 );
330
331 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 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 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 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 let invalid_faucet_account = AccountBuilder::new(mock_seed)
376 .account_type(AccountType::FungibleFaucet)
377 .with_auth_component(RpoFalcon512::new(mock_public_key))
378 .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}