1use alloc::string::String;
2
3use miden_objects::account::{
4 Account,
5 AccountBuilder,
6 AccountComponent,
7 AccountStorage,
8 AccountStorageMode,
9 AccountType,
10 StorageSlot,
11};
12use miden_objects::assembly::{ProcedureName, QualifiedProcedureName};
13use miden_objects::asset::{FungibleAsset, TokenSymbol};
14use miden_objects::utils::sync::LazyLock;
15use miden_objects::{AccountError, Felt, FieldElement, TokenSymbolError, Word};
16use thiserror::Error;
17
18use super::AuthScheme;
19use super::interface::{AccountComponentInterface, AccountInterface};
20use crate::account::auth::{
21 AuthRpoFalcon512Acl,
22 AuthRpoFalcon512AclConfig,
23 AuthRpoFalcon512Multisig,
24};
25use crate::account::components::basic_fungible_faucet_library;
26use crate::transaction::memory::FAUCET_STORAGE_DATA_SLOT;
27
28static BASIC_FUNGIBLE_FAUCET_DISTRIBUTE: LazyLock<Word> = LazyLock::new(|| {
33 let distribute_proc_name = QualifiedProcedureName::new(
34 Default::default(),
35 ProcedureName::new(BasicFungibleFaucet::DISTRIBUTE_PROC_NAME)
36 .expect("failed to create name for 'distribute' procedure"),
37 );
38 basic_fungible_faucet_library()
39 .get_procedure_root_by_name(distribute_proc_name)
40 .expect("Basic Fungible Faucet should contain 'distribute' procedure")
41});
42
43static BASIC_FUNGIBLE_FAUCET_BURN: LazyLock<Word> = LazyLock::new(|| {
45 let burn_proc_name = QualifiedProcedureName::new(
46 Default::default(),
47 ProcedureName::new(BasicFungibleFaucet::BURN_PROC_NAME)
48 .expect("failed to create name for 'burn' procedure"),
49 );
50 basic_fungible_faucet_library()
51 .get_procedure_root_by_name(burn_proc_name)
52 .expect("Basic Fungible Faucet should contain 'burn' procedure")
53});
54
55pub struct BasicFungibleFaucet {
72 symbol: TokenSymbol,
73 decimals: u8,
74 max_supply: Felt,
75}
76
77impl BasicFungibleFaucet {
78 pub const MAX_DECIMALS: u8 = 12;
83
84 const DISTRIBUTE_PROC_NAME: &str = "distribute";
85 const BURN_PROC_NAME: &str = "burn";
86
87 pub fn new(
98 symbol: TokenSymbol,
99 decimals: u8,
100 max_supply: Felt,
101 ) -> Result<Self, FungibleFaucetError> {
102 if decimals > Self::MAX_DECIMALS {
104 return Err(FungibleFaucetError::TooManyDecimals {
105 actual: decimals as u64,
106 max: Self::MAX_DECIMALS,
107 });
108 } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
109 return Err(FungibleFaucetError::MaxSupplyTooLarge {
110 actual: max_supply.as_int(),
111 max: FungibleAsset::MAX_AMOUNT,
112 });
113 }
114
115 Ok(Self { symbol, decimals, max_supply })
116 }
117
118 fn try_from_interface(
131 interface: AccountInterface,
132 storage: &AccountStorage,
133 ) -> Result<Self, FungibleFaucetError> {
134 for component in interface.components().iter() {
135 if let AccountComponentInterface::BasicFungibleFaucet(offset) = component {
136 let faucet_metadata = storage
139 .get_item(*offset)
140 .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
141 let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
142
143 let token_symbol = TokenSymbol::try_from(token_symbol)
145 .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
146 let decimals = decimals.as_int().try_into().map_err(|_| {
147 FungibleFaucetError::TooManyDecimals {
148 actual: decimals.as_int(),
149 max: Self::MAX_DECIMALS,
150 }
151 })?;
152
153 return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
154 }
155 }
156
157 Err(FungibleFaucetError::NoAvailableInterface)
158 }
159
160 pub fn symbol(&self) -> TokenSymbol {
165 self.symbol
166 }
167
168 pub fn decimals(&self) -> u8 {
170 self.decimals
171 }
172
173 pub fn max_supply(&self) -> Felt {
175 self.max_supply
176 }
177
178 pub fn distribute_digest() -> Word {
180 *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE
181 }
182
183 pub fn burn_digest() -> Word {
185 *BASIC_FUNGIBLE_FAUCET_BURN
186 }
187}
188
189impl From<BasicFungibleFaucet> for AccountComponent {
190 fn from(faucet: BasicFungibleFaucet) -> Self {
191 let metadata = Word::new([
194 faucet.max_supply,
195 Felt::from(faucet.decimals),
196 faucet.symbol.into(),
197 Felt::ZERO,
198 ]);
199
200 AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
201 .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
202 .with_supported_type(AccountType::FungibleFaucet)
203 }
204}
205
206impl TryFrom<Account> for BasicFungibleFaucet {
207 type Error = FungibleFaucetError;
208
209 fn try_from(account: Account) -> Result<Self, Self::Error> {
210 let account_interface = AccountInterface::from(&account);
211
212 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
213 }
214}
215
216impl TryFrom<&Account> for BasicFungibleFaucet {
217 type Error = FungibleFaucetError;
218
219 fn try_from(account: &Account) -> Result<Self, Self::Error> {
220 let account_interface = AccountInterface::from(account);
221
222 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
223 }
224}
225
226pub trait FungibleFaucetExt {
232 const ISSUANCE_ELEMENT_INDEX: usize;
233 const ISSUANCE_STORAGE_SLOT: u8;
234
235 fn get_token_issuance(&self) -> Result<Felt, FungibleFaucetError>;
240}
241
242impl FungibleFaucetExt for Account {
243 const ISSUANCE_ELEMENT_INDEX: usize = 3;
244 const ISSUANCE_STORAGE_SLOT: u8 = FAUCET_STORAGE_DATA_SLOT;
245
246 fn get_token_issuance(&self) -> Result<Felt, FungibleFaucetError> {
247 if self.account_type() != AccountType::FungibleFaucet {
248 return Err(FungibleFaucetError::NotAFungibleFaucetAccount);
249 }
250
251 let slot = self
252 .storage()
253 .get_item(Self::ISSUANCE_STORAGE_SLOT)
254 .map_err(|_| FungibleFaucetError::InvalidStorageOffset(Self::ISSUANCE_STORAGE_SLOT))?;
255 Ok(slot[Self::ISSUANCE_ELEMENT_INDEX])
256 }
257}
258
259pub fn create_basic_fungible_faucet(
278 init_seed: [u8; 32],
279 symbol: TokenSymbol,
280 decimals: u8,
281 max_supply: Felt,
282 account_storage_mode: AccountStorageMode,
283 auth_scheme: AuthScheme,
284) -> Result<(Account, Word), FungibleFaucetError> {
285 let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
286
287 let auth_component: AccountComponent = match auth_scheme {
288 AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new(
289 pub_key,
290 AuthRpoFalcon512AclConfig::new()
291 .with_auth_trigger_procedures(vec![distribute_proc_root])
292 .with_allow_unauthorized_input_notes(true),
293 )
294 .map_err(FungibleFaucetError::AccountError)?
295 .into(),
296 AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } => {
297 AuthRpoFalcon512Multisig::new(threshold, pub_keys)
298 .map_err(FungibleFaucetError::AccountError)?
299 .into()
300 },
301 AuthScheme::NoAuth => {
302 return Err(FungibleFaucetError::UnsupportedAuthScheme(
303 "basic fungible faucets cannot be created with NoAuth authentication scheme".into(),
304 ));
305 },
306 AuthScheme::Unknown => {
307 return Err(FungibleFaucetError::UnsupportedAuthScheme(
308 "basic fungible faucets cannot be created with Unknown authentication scheme"
309 .into(),
310 ));
311 },
312 };
313
314 let (account, account_seed) = AccountBuilder::new(init_seed)
315 .account_type(AccountType::FungibleFaucet)
316 .storage_mode(account_storage_mode)
317 .with_auth_component(auth_component)
318 .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
319 .build()
320 .map_err(FungibleFaucetError::AccountError)?;
321
322 Ok((account, account_seed))
323}
324
325#[derive(Debug, Error)]
330pub enum FungibleFaucetError {
331 #[error("faucet metadata decimals is {actual} which exceeds max value of {max}")]
332 TooManyDecimals { actual: u64, max: u8 },
333 #[error("faucet metadata max supply is {actual} which exceeds max value of {max}")]
334 MaxSupplyTooLarge { actual: u64, max: u64 },
335 #[error(
336 "account interface provided for faucet creation does not have basic fungible faucet component"
337 )]
338 NoAvailableInterface,
339 #[error("storage offset `{0}` is invalid")]
340 InvalidStorageOffset(u8),
341 #[error("invalid token symbol")]
342 InvalidTokenSymbol(#[source] TokenSymbolError),
343 #[error("unsupported authentication scheme: {0}")]
344 UnsupportedAuthScheme(String),
345 #[error("account creation failed")]
346 AccountError(#[source] AccountError),
347 #[error("account is not a fungible faucet account")]
348 NotAFungibleFaucetAccount,
349}
350
351#[cfg(test)]
355mod tests {
356 use assert_matches::assert_matches;
357 use miden_objects::crypto::dsa::rpo_falcon512::{self, PublicKey};
358 use miden_objects::{FieldElement, ONE, Word};
359
360 use super::{
361 AccountBuilder,
362 AccountStorageMode,
363 AccountType,
364 AuthScheme,
365 BasicFungibleFaucet,
366 Felt,
367 FungibleFaucetError,
368 TokenSymbol,
369 create_basic_fungible_faucet,
370 };
371 use crate::account::auth::AuthRpoFalcon512;
372 use crate::account::wallets::BasicWallet;
373
374 #[test]
375 fn faucet_contract_creation() {
376 let pub_key = rpo_falcon512::PublicKey::new(Word::new([ONE; 4]));
377 let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key };
378
379 let init_seed: [u8; 32] = [
381 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
382 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
383 ];
384
385 let max_supply = Felt::new(123);
386 let token_symbol_string = "POL";
387 let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
388 let decimals = 2u8;
389 let storage_mode = AccountStorageMode::Private;
390
391 let (faucet_account, _) = create_basic_fungible_faucet(
392 init_seed,
393 token_symbol,
394 decimals,
395 max_supply,
396 storage_mode,
397 auth_scheme,
398 )
399 .unwrap();
400
401 assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::empty());
403
404 assert_eq!(faucet_account.storage().get_item(1).unwrap(), Word::from(pub_key));
407
408 assert_eq!(
413 faucet_account.storage().get_item(2).unwrap(),
414 [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
415 );
416
417 let distribute_root = BasicFungibleFaucet::distribute_digest();
419 assert_eq!(
420 faucet_account
421 .storage()
422 .get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into())
423 .unwrap(),
424 distribute_root
425 );
426
427 assert_eq!(
430 faucet_account.storage().get_item(4).unwrap(),
431 [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
432 );
433
434 assert!(faucet_account.is_faucet());
435 }
436
437 #[test]
438 fn faucet_create_from_account() {
439 let mock_public_key = PublicKey::new(Word::from([0, 1, 2, 3u32]));
441 let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes();
442
443 let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
445 let faucet_account = AccountBuilder::new(mock_seed)
446 .account_type(AccountType::FungibleFaucet)
447 .with_component(
448 BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
449 .expect("failed to create a fungible faucet component"),
450 )
451 .with_auth_component(AuthRpoFalcon512::new(mock_public_key))
452 .build_existing()
453 .expect("failed to create wallet account");
454
455 let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
456 .expect("basic fungible faucet creation failed");
457 assert_eq!(basic_ff.symbol, token_symbol);
458 assert_eq!(basic_ff.decimals, 10);
459 assert_eq!(basic_ff.max_supply, Felt::new(100));
460
461 let invalid_faucet_account = AccountBuilder::new(mock_seed)
463 .account_type(AccountType::FungibleFaucet)
464 .with_auth_component(AuthRpoFalcon512::new(mock_public_key))
465 .with_component(BasicWallet)
467 .build_existing()
468 .expect("failed to create wallet account");
469
470 let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
471 .err()
472 .expect("basic fungible faucet creation should fail");
473 assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
474 }
475
476 #[test]
478 fn get_faucet_procedures() {
479 let _distribute_digest = BasicFungibleFaucet::distribute_digest();
480 let _burn_digest = BasicFungibleFaucet::burn_digest();
481 }
482}