1use 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::{
17 auth::{NoAuth, RpoFalcon512ProcedureAcl},
18 components::basic_fungible_faucet_library,
19};
20
21pub struct BasicFungibleFaucet {
41 symbol: TokenSymbol,
42 decimals: u8,
43 max_supply: Felt,
44}
45
46impl BasicFungibleFaucet {
47 pub const MAX_DECIMALS: u8 = 12;
52
53 const DISTRIBUTE_PROC_NAME: &str = "distribute";
54 const BURN_PROC_NAME: &str = "burn";
55
56 pub fn new(
67 symbol: TokenSymbol,
68 decimals: u8,
69 max_supply: Felt,
70 ) -> Result<Self, FungibleFaucetError> {
71 if decimals > Self::MAX_DECIMALS {
73 return Err(FungibleFaucetError::TooManyDecimals {
74 actual: decimals as u64,
75 max: Self::MAX_DECIMALS,
76 });
77 } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
78 return Err(FungibleFaucetError::MaxSupplyTooLarge {
79 actual: max_supply.as_int(),
80 max: FungibleAsset::MAX_AMOUNT,
81 });
82 }
83
84 Ok(Self { symbol, decimals, max_supply })
85 }
86
87 fn try_from_interface(
100 interface: AccountInterface,
101 storage: &AccountStorage,
102 ) -> Result<Self, FungibleFaucetError> {
103 for component in interface.components().iter() {
104 if let AccountComponentInterface::BasicFungibleFaucet(offset) = component {
105 let faucet_metadata = storage
108 .get_item(*offset)
109 .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
110 let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
111
112 let token_symbol = TokenSymbol::try_from(token_symbol)
114 .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
115 let decimals = decimals.as_int().try_into().map_err(|_| {
116 FungibleFaucetError::TooManyDecimals {
117 actual: decimals.as_int(),
118 max: Self::MAX_DECIMALS,
119 }
120 })?;
121
122 return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
123 }
124 }
125
126 Err(FungibleFaucetError::NoAvailableInterface)
127 }
128
129 pub fn symbol(&self) -> TokenSymbol {
134 self.symbol
135 }
136
137 pub fn decimals(&self) -> u8 {
139 self.decimals
140 }
141
142 pub fn max_supply(&self) -> Felt {
144 self.max_supply
145 }
146
147 pub fn distribute_digest() -> Digest {
149 Self::get_procedure_digest_by_name(Self::DISTRIBUTE_PROC_NAME)
150 }
151
152 pub fn burn_digest() -> Digest {
154 Self::get_procedure_digest_by_name(Self::BURN_PROC_NAME)
155 }
156
157 fn get_procedure_digest_by_name(procedure_name: &str) -> Digest {
163 let proc_name = ProcedureName::new(procedure_name).expect("procedure name should be valid");
164 let module = basic_fungible_faucet_library()
165 .module_infos()
166 .next()
167 .expect("basic_fungible_faucet_library should have exactly one module");
168 module.get_procedure_digest_by_name(&proc_name).unwrap_or_else(|| {
169 panic!("basic_fungible_faucet_library should contain the '{proc_name}' procedure")
170 })
171 }
172}
173
174impl From<BasicFungibleFaucet> for AccountComponent {
175 fn from(faucet: BasicFungibleFaucet) -> Self {
176 let metadata =
179 [faucet.max_supply, Felt::from(faucet.decimals), faucet.symbol.into(), Felt::ZERO];
180
181 AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
182 .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
183 .with_supported_type(AccountType::FungibleFaucet)
184 }
185}
186
187impl TryFrom<Account> for BasicFungibleFaucet {
188 type Error = FungibleFaucetError;
189
190 fn try_from(account: Account) -> Result<Self, Self::Error> {
191 let account_interface = AccountInterface::from(&account);
192
193 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
194 }
195}
196
197impl TryFrom<&Account> for BasicFungibleFaucet {
198 type Error = FungibleFaucetError;
199
200 fn try_from(account: &Account) -> Result<Self, Self::Error> {
201 let account_interface = AccountInterface::from(account);
202
203 BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
204 }
205}
206
207pub fn create_basic_fungible_faucet(
228 init_seed: [u8; 32],
229 symbol: TokenSymbol,
230 decimals: u8,
231 max_supply: Felt,
232 account_storage_mode: AccountStorageMode,
233 auth_scheme: AuthScheme,
234) -> Result<(Account, Word), FungibleFaucetError> {
235 let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
236
237 let auth_component: AccountComponent = match auth_scheme {
238 AuthScheme::RpoFalcon512 { pub_key } => {
239 RpoFalcon512ProcedureAcl::new(pub_key, vec![distribute_proc_root])
240 .map_err(FungibleFaucetError::AccountError)?
241 .into()
242 },
243 AuthScheme::NoAuth => NoAuth::new().into(),
244 };
245
246 let (account, account_seed) = AccountBuilder::new(init_seed)
247 .account_type(AccountType::FungibleFaucet)
248 .storage_mode(account_storage_mode)
249 .with_auth_component(auth_component)
250 .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
251 .build()
252 .map_err(FungibleFaucetError::AccountError)?;
253
254 Ok((account, account_seed))
255}
256
257#[derive(Debug, Error)]
262pub enum FungibleFaucetError {
263 #[error("faucet metadata decimals is {actual} which exceeds max value of {max}")]
264 TooManyDecimals { actual: u64, max: u8 },
265 #[error("faucet metadata max supply is {actual} which exceeds max value of {max}")]
266 MaxSupplyTooLarge { actual: u64, max: u64 },
267 #[error(
268 "account interface provided for faucet creation does not have basic fungible faucet component"
269 )]
270 NoAvailableInterface,
271 #[error("storage offset `{0}` is invalid")]
272 InvalidStorageOffset(u8),
273 #[error("invalid token symbol")]
274 InvalidTokenSymbol(#[source] TokenSymbolError),
275 #[error("account creation failed")]
276 AccountError(#[source] AccountError),
277}
278
279#[cfg(test)]
283mod tests {
284 use assert_matches::assert_matches;
285 use miden_objects::{
286 Digest, FieldElement, ONE, Word, ZERO,
287 crypto::dsa::rpo_falcon512::{self, PublicKey},
288 };
289
290 use super::{
291 AccountBuilder, AccountStorageMode, AccountType, AuthScheme, BasicFungibleFaucet, Felt,
292 FungibleFaucetError, TokenSymbol, create_basic_fungible_faucet,
293 };
294 use crate::account::{auth::RpoFalcon512, wallets::BasicWallet};
295
296 #[test]
297 fn faucet_contract_creation() {
298 let pub_key = rpo_falcon512::PublicKey::new([ONE; 4]);
299 let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key };
300
301 let init_seed: [u8; 32] = [
303 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
304 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
305 ];
306
307 let max_supply = Felt::new(123);
308 let token_symbol_string = "POL";
309 let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
310 let decimals = 2u8;
311 let storage_mode = AccountStorageMode::Private;
312
313 let (faucet_account, _) = create_basic_fungible_faucet(
314 init_seed,
315 token_symbol,
316 decimals,
317 max_supply,
318 storage_mode,
319 auth_scheme,
320 )
321 .unwrap();
322
323 assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::default().into());
325
326 assert_eq!(faucet_account.storage().get_item(1).unwrap(), Word::from(pub_key).into());
329
330 assert_eq!(
332 faucet_account.storage().get_item(2).unwrap(),
333 [Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
334 );
335
336 assert_eq!(
338 faucet_account
339 .storage()
340 .get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO])
341 .unwrap(),
342 Word::from(BasicFungibleFaucet::distribute_digest())
343 );
344
345 assert_eq!(
348 faucet_account.storage().get_item(4).unwrap(),
349 [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
350 );
351
352 assert!(faucet_account.is_faucet());
353 }
354
355 #[test]
356 fn faucet_create_from_account() {
357 let mock_public_key = PublicKey::new([ZERO, ONE, Felt::new(2), Felt::new(3)]);
359 let mock_seed = Digest::from([ZERO, ONE, Felt::new(2), Felt::new(3)]).as_bytes();
360
361 let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
363 let faucet_account = AccountBuilder::new(mock_seed)
364 .account_type(AccountType::FungibleFaucet)
365 .with_component(
366 BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
367 .expect("failed to create a fungible faucet component"),
368 )
369 .with_auth_component(RpoFalcon512::new(mock_public_key))
370 .build_existing()
371 .expect("failed to create wallet account");
372
373 let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
374 .expect("basic fungible faucet creation failed");
375 assert_eq!(basic_ff.symbol, token_symbol);
376 assert_eq!(basic_ff.decimals, 10);
377 assert_eq!(basic_ff.max_supply, Felt::new(100));
378
379 let invalid_faucet_account = AccountBuilder::new(mock_seed)
381 .account_type(AccountType::FungibleFaucet)
382 .with_auth_component(RpoFalcon512::new(mock_public_key))
383 .with_component(BasicWallet)
385 .build_existing()
386 .expect("failed to create wallet account");
387
388 let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
389 .err()
390 .expect("basic fungible faucet creation should fail");
391 assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
392 }
393}