use miden_objects::account::{
Account,
AccountBuilder,
AccountComponent,
AccountStorage,
AccountStorageMode,
AccountType,
StorageSlot,
};
use miden_objects::asset::{FungibleAsset, TokenSymbol};
use miden_objects::{Felt, FieldElement, Word};
use super::FungibleFaucetError;
use crate::account::AuthScheme;
use crate::account::auth::{
AuthEcdsaK256KeccakAcl,
AuthEcdsaK256KeccakAclConfig,
AuthRpoFalcon512Acl,
AuthRpoFalcon512AclConfig,
};
use crate::account::components::basic_fungible_faucet_library;
use crate::account::interface::{AccountComponentInterface, AccountInterface};
use crate::procedure_digest;
procedure_digest!(
BASIC_FUNGIBLE_FAUCET_DISTRIBUTE,
BasicFungibleFaucet::DISTRIBUTE_PROC_NAME,
basic_fungible_faucet_library
);
procedure_digest!(
BASIC_FUNGIBLE_FAUCET_BURN,
BasicFungibleFaucet::BURN_PROC_NAME,
basic_fungible_faucet_library
);
pub struct BasicFungibleFaucet {
symbol: TokenSymbol,
decimals: u8,
max_supply: Felt,
}
impl BasicFungibleFaucet {
pub const MAX_DECIMALS: u8 = 12;
const DISTRIBUTE_PROC_NAME: &str = "distribute";
const BURN_PROC_NAME: &str = "burn";
pub fn new(
symbol: TokenSymbol,
decimals: u8,
max_supply: Felt,
) -> Result<Self, FungibleFaucetError> {
if decimals > Self::MAX_DECIMALS {
return Err(FungibleFaucetError::TooManyDecimals {
actual: decimals as u64,
max: Self::MAX_DECIMALS,
});
} else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
return Err(FungibleFaucetError::MaxSupplyTooLarge {
actual: max_supply.as_int(),
max: FungibleAsset::MAX_AMOUNT,
});
}
Ok(Self { symbol, decimals, max_supply })
}
fn try_from_interface(
interface: AccountInterface,
storage: &AccountStorage,
) -> Result<Self, FungibleFaucetError> {
for component in interface.components().iter() {
if let AccountComponentInterface::BasicFungibleFaucet(offset) = component {
let faucet_metadata = storage
.get_item(*offset)
.map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
let token_symbol = TokenSymbol::try_from(token_symbol)
.map_err(FungibleFaucetError::InvalidTokenSymbol)?;
let decimals = decimals.as_int().try_into().map_err(|_| {
FungibleFaucetError::TooManyDecimals {
actual: decimals.as_int(),
max: Self::MAX_DECIMALS,
}
})?;
return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
}
}
Err(FungibleFaucetError::NoAvailableInterface)
}
pub fn symbol(&self) -> TokenSymbol {
self.symbol
}
pub fn decimals(&self) -> u8 {
self.decimals
}
pub fn max_supply(&self) -> Felt {
self.max_supply
}
pub fn distribute_digest() -> Word {
*BASIC_FUNGIBLE_FAUCET_DISTRIBUTE
}
pub fn burn_digest() -> Word {
*BASIC_FUNGIBLE_FAUCET_BURN
}
}
impl From<BasicFungibleFaucet> for AccountComponent {
fn from(faucet: BasicFungibleFaucet) -> Self {
let metadata = Word::new([
faucet.max_supply,
Felt::from(faucet.decimals),
faucet.symbol.into(),
Felt::ZERO,
]);
AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
.expect("basic fungible faucet component should satisfy the requirements of a valid account component")
.with_supported_type(AccountType::FungibleFaucet)
}
}
impl TryFrom<Account> for BasicFungibleFaucet {
type Error = FungibleFaucetError;
fn try_from(account: Account) -> Result<Self, Self::Error> {
let account_interface = AccountInterface::from(&account);
BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
}
}
impl TryFrom<&Account> for BasicFungibleFaucet {
type Error = FungibleFaucetError;
fn try_from(account: &Account) -> Result<Self, Self::Error> {
let account_interface = AccountInterface::from(account);
BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
}
}
pub fn create_basic_fungible_faucet(
init_seed: [u8; 32],
symbol: TokenSymbol,
decimals: u8,
max_supply: Felt,
account_storage_mode: AccountStorageMode,
auth_scheme: AuthScheme,
) -> Result<Account, FungibleFaucetError> {
let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
let auth_component: AccountComponent = match auth_scheme {
AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new(
pub_key,
AuthRpoFalcon512AclConfig::new()
.with_auth_trigger_procedures(vec![distribute_proc_root])
.with_allow_unauthorized_input_notes(true),
)
.map_err(FungibleFaucetError::AccountError)?
.into(),
AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new(
pub_key,
AuthEcdsaK256KeccakAclConfig::new()
.with_auth_trigger_procedures(vec![distribute_proc_root])
.with_allow_unauthorized_input_notes(true),
)
.map_err(FungibleFaucetError::AccountError)?
.into(),
AuthScheme::NoAuth => {
return Err(FungibleFaucetError::UnsupportedAuthScheme(
"basic fungible faucets cannot be created with NoAuth authentication scheme".into(),
));
},
AuthScheme::RpoFalcon512Multisig { threshold: _, pub_keys: _ } => {
return Err(FungibleFaucetError::UnsupportedAuthScheme(
"basic fungible faucets do not support multisig authentication".into(),
));
},
AuthScheme::Unknown => {
return Err(FungibleFaucetError::UnsupportedAuthScheme(
"basic fungible faucets cannot be created with Unknown authentication scheme"
.into(),
));
},
AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => {
return Err(FungibleFaucetError::UnsupportedAuthScheme(
"basic fungible faucets do not support EcdsaK256KeccakMultisig authentication"
.into(),
));
},
};
let account = AccountBuilder::new(init_seed)
.account_type(AccountType::FungibleFaucet)
.storage_mode(account_storage_mode)
.with_auth_component(auth_component)
.with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
.build()
.map_err(FungibleFaucetError::AccountError)?;
Ok(account)
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_objects::account::auth::PublicKeyCommitment;
use miden_objects::{FieldElement, ONE, Word};
use super::{
AccountBuilder,
AccountStorageMode,
AccountType,
AuthScheme,
BasicFungibleFaucet,
Felt,
FungibleFaucetError,
TokenSymbol,
create_basic_fungible_faucet,
};
use crate::account::auth::AuthRpoFalcon512;
use crate::account::wallets::BasicWallet;
#[test]
fn faucet_contract_creation() {
let pub_key_word = Word::new([ONE; 4]);
let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key: pub_key_word.into() };
let init_seed: [u8; 32] = [
90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
];
let max_supply = Felt::new(123);
let token_symbol_string = "POL";
let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
let decimals = 2u8;
let storage_mode = AccountStorageMode::Private;
let faucet_account = create_basic_fungible_faucet(
init_seed,
token_symbol,
decimals,
max_supply,
storage_mode,
auth_scheme,
)
.unwrap();
assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::empty());
assert_eq!(faucet_account.storage().get_item(1).unwrap(), pub_key_word);
assert_eq!(
faucet_account.storage().get_item(2).unwrap(),
[Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
);
let distribute_root = BasicFungibleFaucet::distribute_digest();
assert_eq!(
faucet_account
.storage()
.get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into())
.unwrap(),
distribute_root
);
assert_eq!(
faucet_account.storage().get_item(4).unwrap(),
[Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
);
assert!(faucet_account.is_faucet());
assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet);
let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap();
assert_eq!(faucet_component.symbol(), token_symbol);
assert_eq!(faucet_component.decimals(), decimals);
assert_eq!(faucet_component.max_supply(), max_supply);
}
#[test]
fn faucet_create_from_account() {
let mock_word = Word::from([0, 1, 2, 3u32]);
let mock_public_key = PublicKeyCommitment::from(mock_word);
let mock_seed = mock_word.as_bytes();
let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
let faucet_account = AccountBuilder::new(mock_seed)
.account_type(AccountType::FungibleFaucet)
.with_component(
BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
.expect("failed to create a fungible faucet component"),
)
.with_auth_component(AuthRpoFalcon512::new(mock_public_key))
.build_existing()
.expect("failed to create wallet account");
let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
.expect("basic fungible faucet creation failed");
assert_eq!(basic_ff.symbol(), token_symbol);
assert_eq!(basic_ff.decimals(), 10);
assert_eq!(basic_ff.max_supply(), Felt::new(100));
let invalid_faucet_account = AccountBuilder::new(mock_seed)
.account_type(AccountType::FungibleFaucet)
.with_auth_component(AuthRpoFalcon512::new(mock_public_key))
.with_component(BasicWallet)
.build_existing()
.expect("failed to create wallet account");
let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
.err()
.expect("basic fungible faucet creation should fail");
assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
}
#[test]
fn get_faucet_procedures() {
let _distribute_digest = BasicFungibleFaucet::distribute_digest();
let _burn_digest = BasicFungibleFaucet::burn_digest();
}
}