miden_standards/account/faucets/
basic_fungible.rs1use miden_protocol::account::{
2 Account,
3 AccountBuilder,
4 AccountComponent,
5 AccountStorage,
6 AccountStorageMode,
7 AccountType,
8 StorageSlot,
9 StorageSlotName,
10};
11use miden_protocol::asset::{FungibleAsset, TokenSymbol};
12use miden_protocol::{Felt, FieldElement, Word};
13
14use super::FungibleFaucetError;
15use crate::account::AuthScheme;
16use crate::account::auth::{
17 AuthEcdsaK256KeccakAcl,
18 AuthEcdsaK256KeccakAclConfig,
19 AuthFalcon512RpoAcl,
20 AuthFalcon512RpoAclConfig,
21};
22use crate::account::components::basic_fungible_faucet_library;
23use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt};
24use crate::procedure_digest;
25
26procedure_digest!(
31 BASIC_FUNGIBLE_FAUCET_DISTRIBUTE,
32 BasicFungibleFaucet::DISTRIBUTE_PROC_NAME,
33 basic_fungible_faucet_library
34);
35
36procedure_digest!(
38 BASIC_FUNGIBLE_FAUCET_BURN,
39 BasicFungibleFaucet::BURN_PROC_NAME,
40 basic_fungible_faucet_library
41);
42
43pub struct BasicFungibleFaucet {
65 symbol: TokenSymbol,
66 decimals: u8,
67 max_supply: Felt,
68}
69
70impl BasicFungibleFaucet {
71 pub const MAX_DECIMALS: u8 = 12;
76
77 const DISTRIBUTE_PROC_NAME: &str = "basic_fungible_faucet::distribute";
78 const BURN_PROC_NAME: &str = "basic_fungible_faucet::burn";
79
80 pub fn new(
91 symbol: TokenSymbol,
92 decimals: u8,
93 max_supply: Felt,
94 ) -> Result<Self, FungibleFaucetError> {
95 if decimals > Self::MAX_DECIMALS {
97 return Err(FungibleFaucetError::TooManyDecimals {
98 actual: decimals as u64,
99 max: Self::MAX_DECIMALS,
100 });
101 } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
102 return Err(FungibleFaucetError::MaxSupplyTooLarge {
103 actual: max_supply.as_int(),
104 max: FungibleAsset::MAX_AMOUNT,
105 });
106 }
107
108 Ok(Self { symbol, decimals, max_supply })
109 }
110
111 fn try_from_interface(
124 interface: AccountInterface,
125 storage: &AccountStorage,
126 ) -> Result<Self, FungibleFaucetError> {
127 for component in interface.components().iter() {
128 if let AccountComponentInterface::BasicFungibleFaucet = component {
129 let faucet_metadata = storage
130 .get_item(BasicFungibleFaucet::metadata_slot())
131 .map_err(|err| FungibleFaucetError::StorageLookupFailed {
132 slot_name: BasicFungibleFaucet::metadata_slot().clone(),
133 source: err,
134 })?;
135 let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
136
137 let token_symbol = TokenSymbol::try_from(token_symbol)
139 .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
140 let decimals = decimals.as_int().try_into().map_err(|_| {
141 FungibleFaucetError::TooManyDecimals {
142 actual: decimals.as_int(),
143 max: Self::MAX_DECIMALS,
144 }
145 })?;
146
147 return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
148 }
149 }
150
151 Err(FungibleFaucetError::NoAvailableInterface)
152 }
153
154 pub fn metadata_slot() -> &'static StorageSlotName {
159 &super::METADATA_SLOT_NAME
160 }
161
162 pub fn symbol(&self) -> TokenSymbol {
164 self.symbol
165 }
166
167 pub fn decimals(&self) -> u8 {
169 self.decimals
170 }
171
172 pub fn max_supply(&self) -> Felt {
174 self.max_supply
175 }
176
177 pub fn distribute_digest() -> Word {
179 *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE
180 }
181
182 pub fn burn_digest() -> Word {
184 *BASIC_FUNGIBLE_FAUCET_BURN
185 }
186}
187
188impl From<BasicFungibleFaucet> for AccountComponent {
189 fn from(faucet: BasicFungibleFaucet) -> Self {
190 let metadata = Word::new([
193 faucet.max_supply,
194 Felt::from(faucet.decimals),
195 faucet.symbol.into(),
196 Felt::ZERO,
197 ]);
198 let storage_slot =
199 StorageSlot::with_value(BasicFungibleFaucet::metadata_slot().clone(), metadata);
200
201 AccountComponent::new(basic_fungible_faucet_library(), vec![storage_slot])
202 .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
203 .with_supported_type(AccountType::FungibleFaucet)
204 }
205}
206
207impl TryFrom<Account> for BasicFungibleFaucet {
208 type Error = FungibleFaucetError;
209
210 fn try_from(account: Account) -> Result<Self, Self::Error> {
211 let account_interface = AccountInterface::from_account(&account);
212
213 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
214 }
215}
216
217impl TryFrom<&Account> for BasicFungibleFaucet {
218 type Error = FungibleFaucetError;
219
220 fn try_from(account: &Account) -> Result<Self, Self::Error> {
221 let account_interface = AccountInterface::from_account(account);
222
223 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
224 }
225}
226
227pub fn create_basic_fungible_faucet(
247 init_seed: [u8; 32],
248 symbol: TokenSymbol,
249 decimals: u8,
250 max_supply: Felt,
251 account_storage_mode: AccountStorageMode,
252 auth_scheme: AuthScheme,
253) -> Result<Account, FungibleFaucetError> {
254 let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
255
256 let auth_component: AccountComponent = match auth_scheme {
257 AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new(
258 pub_key,
259 AuthFalcon512RpoAclConfig::new()
260 .with_auth_trigger_procedures(vec![distribute_proc_root])
261 .with_allow_unauthorized_input_notes(true),
262 )
263 .map_err(FungibleFaucetError::AccountError)?
264 .into(),
265 AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new(
266 pub_key,
267 AuthEcdsaK256KeccakAclConfig::new()
268 .with_auth_trigger_procedures(vec![distribute_proc_root])
269 .with_allow_unauthorized_input_notes(true),
270 )
271 .map_err(FungibleFaucetError::AccountError)?
272 .into(),
273 AuthScheme::NoAuth => {
274 return Err(FungibleFaucetError::UnsupportedAuthScheme(
275 "basic fungible faucets cannot be created with NoAuth authentication scheme".into(),
276 ));
277 },
278 AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => {
279 return Err(FungibleFaucetError::UnsupportedAuthScheme(
280 "basic fungible faucets do not support multisig authentication".into(),
281 ));
282 },
283 AuthScheme::Unknown => {
284 return Err(FungibleFaucetError::UnsupportedAuthScheme(
285 "basic fungible faucets cannot be created with Unknown authentication scheme"
286 .into(),
287 ));
288 },
289 AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => {
290 return Err(FungibleFaucetError::UnsupportedAuthScheme(
291 "basic fungible faucets do not support EcdsaK256KeccakMultisig authentication"
292 .into(),
293 ));
294 },
295 };
296
297 let account = AccountBuilder::new(init_seed)
298 .account_type(AccountType::FungibleFaucet)
299 .storage_mode(account_storage_mode)
300 .with_auth_component(auth_component)
301 .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
302 .build()
303 .map_err(FungibleFaucetError::AccountError)?;
304
305 Ok(account)
306}
307
308#[cfg(test)]
312mod tests {
313 use assert_matches::assert_matches;
314 use miden_protocol::account::AccountStorage;
315 use miden_protocol::account::auth::PublicKeyCommitment;
316 use miden_protocol::{FieldElement, ONE, Word};
317
318 use super::{
319 AccountBuilder,
320 AccountStorageMode,
321 AccountType,
322 AuthScheme,
323 BasicFungibleFaucet,
324 Felt,
325 FungibleFaucetError,
326 TokenSymbol,
327 create_basic_fungible_faucet,
328 };
329 use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl};
330 use crate::account::wallets::BasicWallet;
331
332 #[test]
333 fn faucet_contract_creation() {
334 let pub_key_word = Word::new([ONE; 4]);
335 let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() };
336
337 let init_seed: [u8; 32] = [
339 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
340 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
341 ];
342
343 let max_supply = Felt::new(123);
344 let token_symbol_string = "POL";
345 let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
346 let decimals = 2u8;
347 let storage_mode = AccountStorageMode::Private;
348
349 let faucet_account = create_basic_fungible_faucet(
350 init_seed,
351 token_symbol,
352 decimals,
353 max_supply,
354 storage_mode,
355 auth_scheme,
356 )
357 .unwrap();
358
359 assert_eq!(
361 faucet_account
362 .storage()
363 .get_item(AccountStorage::faucet_sysdata_slot())
364 .unwrap(),
365 Word::empty()
366 );
367
368 assert_eq!(
370 faucet_account
371 .storage()
372 .get_item(AuthFalcon512RpoAcl::public_key_slot())
373 .unwrap(),
374 pub_key_word
375 );
376
377 assert_eq!(
383 faucet_account.storage().get_item(AuthFalcon512RpoAcl::config_slot()).unwrap(),
384 [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
385 );
386
387 let distribute_root = BasicFungibleFaucet::distribute_digest();
389 assert_eq!(
390 faucet_account
391 .storage()
392 .get_map_item(
393 AuthFalcon512RpoAcl::trigger_procedure_roots_slot(),
394 [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
395 )
396 .unwrap(),
397 distribute_root
398 );
399
400 assert_eq!(
402 faucet_account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(),
403 [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
404 );
405
406 assert!(faucet_account.is_faucet());
407
408 assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet);
409
410 let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap();
412 assert_eq!(faucet_component.symbol(), token_symbol);
413 assert_eq!(faucet_component.decimals(), decimals);
414 assert_eq!(faucet_component.max_supply(), max_supply);
415 }
416
417 #[test]
418 fn faucet_create_from_account() {
419 let mock_word = Word::from([0, 1, 2, 3u32]);
421 let mock_public_key = PublicKeyCommitment::from(mock_word);
422 let mock_seed = mock_word.as_bytes();
423
424 let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
426 let faucet_account = AccountBuilder::new(mock_seed)
427 .account_type(AccountType::FungibleFaucet)
428 .with_component(
429 BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
430 .expect("failed to create a fungible faucet component"),
431 )
432 .with_auth_component(AuthFalcon512Rpo::new(mock_public_key))
433 .build_existing()
434 .expect("failed to create wallet account");
435
436 let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
437 .expect("basic fungible faucet creation failed");
438 assert_eq!(basic_ff.symbol, token_symbol);
439 assert_eq!(basic_ff.decimals, 10);
440 assert_eq!(basic_ff.max_supply, Felt::new(100));
441
442 let invalid_faucet_account = AccountBuilder::new(mock_seed)
444 .account_type(AccountType::FungibleFaucet)
445 .with_auth_component(AuthFalcon512Rpo::new(mock_public_key))
446 .with_component(BasicWallet)
448 .build_existing()
449 .expect("failed to create wallet account");
450
451 let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
452 .err()
453 .expect("basic fungible faucet creation should fail");
454 assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
455 }
456
457 #[test]
459 fn get_faucet_procedures() {
460 let _distribute_digest = BasicFungibleFaucet::distribute_digest();
461 let _burn_digest = BasicFungibleFaucet::burn_digest();
462 }
463}