use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use miden_client::Client;
use miden_client::account::component::{
AccountComponent,
AccountComponentMetadata,
BurnPolicyConfig,
FungibleFaucet,
InitStorageData,
MIDEN_PACKAGE_EXTENSION,
MintPolicyConfig,
PolicyRegistration,
StorageSlotSchema,
TokenName,
TokenPolicyManager,
};
use miden_client::account::{
Account,
AccountBuilder,
AccountBuilderSchemaCommitmentExt,
AccountType,
};
use miden_client::asset::{AssetAmount, TokenSymbol};
use miden_client::auth::{AuthSchemeId, AuthSecretKey, AuthSingleSig};
use miden_client::keystore::Keystore;
use miden_client::transaction::TransactionRequestBuilder;
use miden_client::utils::Deserializable;
use miden_client::vm::{Package, SectionId};
use rand::RngCore;
use serde::Deserialize;
use tracing::debug;
use crate::commands::account::set_default_account_if_unset;
use crate::config::CliConfig;
use crate::errors::CliError;
use crate::{CliKeyStore, client_binary_name};
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum CliAccountType {
Private,
Public,
}
impl From<CliAccountType> for AccountType {
fn from(cli_account_type: CliAccountType) -> Self {
match cli_account_type {
CliAccountType::Private => AccountType::Private,
CliAccountType::Public => AccountType::Public,
}
}
}
#[derive(Debug, Parser, Clone)]
pub struct NewWalletCmd {
#[arg(value_enum, short = 't', long = "account-type", default_value_t = CliAccountType::Private)]
pub account_type: CliAccountType,
#[arg(short, long)]
pub extra_packages: Vec<PathBuf>,
#[arg(short, long)]
pub init_storage_data_path: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub deploy: bool,
#[cfg_attr(
feature = "testing",
arg(long, default_value_t = false, conflicts_with = "deploy")
)]
#[cfg_attr(not(feature = "testing"), arg(skip = false))]
pub offline: bool,
}
impl NewWalletCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
keystore: CliKeyStore,
) -> Result<(), CliError> {
let package_paths: Vec<PathBuf> = [PathBuf::from("basic-wallet")]
.into_iter()
.chain(self.extra_packages.clone())
.collect();
let new_account = create_client_account(
&mut client,
&keystore,
self.account_type.into(),
&package_paths,
self.init_storage_data_path.clone(),
self.deploy,
self.offline,
)
.await?;
println!("Successfully created new wallet.");
println!(
"To view account details execute {} account -s {}",
client_binary_name().display(),
new_account.id().to_hex()
);
set_default_account_if_unset(&mut client, new_account.id()).await?;
Ok(())
}
}
#[derive(Debug, Parser, Clone)]
pub struct NewAccountCmd {
#[arg(value_enum, short = 't', long = "account-type", default_value_t = CliAccountType::Private)]
pub account_type: CliAccountType,
#[arg(short, long)]
pub packages: Vec<PathBuf>,
#[arg(short, long)]
pub init_storage_data_path: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub deploy: bool,
#[cfg_attr(
feature = "testing",
arg(long, default_value_t = false, conflicts_with = "deploy")
)]
#[cfg_attr(not(feature = "testing"), arg(skip = false))]
pub offline: bool,
}
impl NewAccountCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
keystore: CliKeyStore,
) -> Result<(), CliError> {
let new_account = create_client_account(
&mut client,
&keystore,
self.account_type.into(),
&self.packages,
self.init_storage_data_path.clone(),
self.deploy,
self.offline,
)
.await?;
println!("Successfully created new account.");
println!(
"To view account details execute {} account -s {}",
client_binary_name().display(),
new_account.id().to_hex()
);
Ok(())
}
}
fn load_packages(
cli_config: &CliConfig,
package_paths: &[PathBuf],
) -> Result<Vec<Package>, CliError> {
let mut packages = Vec::with_capacity(package_paths.len());
let packages_dir = &cli_config.package_directory;
for path in package_paths {
let path = match path.extension() {
None => {
let path = path.with_extension(MIDEN_PACKAGE_EXTENSION);
Ok(packages_dir.join(path))
},
Some(extension) => {
if extension == OsStr::new(MIDEN_PACKAGE_EXTENSION) {
Ok(path.clone())
} else {
let error = std::io::Error::new(
std::io::ErrorKind::InvalidFilename,
format!(
"{} has an invalid file extension: '{}'. \
Expected: {MIDEN_PACKAGE_EXTENSION}",
path.display(),
extension.display()
),
);
Err(CliError::AccountComponentError(
Box::new(error),
format!("refuesed to read {}", path.display()),
))
}
},
}?;
let bytes = fs::read(&path).map_err(|e| {
CliError::AccountComponentError(
Box::new(e),
format!("failed to read Package file from {}", path.display()),
)
})?;
let package = Package::read_from_bytes(&bytes).map_err(|e| {
CliError::AccountComponentError(
Box::new(e),
format!("failed to deserialize Package in {}", path.display()),
)
})?;
packages.push(package);
}
Ok(packages)
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct FungibleFaucetMetadata {
symbol: String,
decimals: u8,
max_supply: u64,
#[serde(default)]
name: String,
}
fn build_fungible_faucet_component(
metadata: &FungibleFaucetMetadata,
) -> Result<AccountComponent, CliError> {
let symbol = TokenSymbol::new(&metadata.symbol).map_err(|err| {
CliError::InvalidArgument(format!("invalid token symbol `{}`: {err}", metadata.symbol))
})?;
let name_input = if metadata.name.is_empty() {
metadata.symbol.as_str()
} else {
metadata.name.as_str()
};
let name = TokenName::new(name_input).map_err(|err| {
CliError::InvalidArgument(format!("invalid token name `{name_input}`: {err}"))
})?;
let max_supply = AssetAmount::new(metadata.max_supply).map_err(|err| {
CliError::InvalidArgument(format!("invalid max_supply `{}`: {err}", metadata.max_supply))
})?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(metadata.decimals)
.max_supply(max_supply)
.build()
.map_err(|err| {
CliError::InvalidArgument(format!("failed to build fungible faucet metadata: {err}"))
})?;
Ok(faucet.into())
}
fn drop_basic_fungible_faucet_packages(packages: &mut Vec<Package>) -> bool {
let before = packages.len();
packages.retain(|pkg| pkg.name.to_string() != FungibleFaucet::NAME);
packages.len() != before
}
fn load_init_storage_data(
path: Option<&PathBuf>,
) -> Result<(InitStorageData, Option<FungibleFaucetMetadata>), CliError> {
let Some(path) = path else {
return Ok((InitStorageData::default(), None));
};
let mut contents = String::new();
File::open(path)
.and_then(|mut f| f.read_to_string(&mut contents))
.map_err(|err| {
CliError::InitDataError(
Box::new(err),
format!("Failed to open init data file {}", path.display()),
)
})?;
let mut table: toml::Table = toml::from_str(&contents).map_err(|err| {
CliError::InitDataError(
Box::new(err),
format!("Failed to parse init data file {} as TOML", path.display()),
)
})?;
let faucet_metadata = table
.remove("fungible-faucet-metadata")
.map(FungibleFaucetMetadata::deserialize)
.transpose()
.map_err(|err| {
CliError::InitDataError(
Box::new(err),
format!("Invalid `fungible-faucet-metadata` in init data file {}", path.display()),
)
})?;
let stripped = toml::to_string(&table).map_err(|err| {
CliError::InitDataError(
Box::new(err),
format!("Failed to re-serialize init data from file {}", path.display()),
)
})?;
let init = InitStorageData::from_toml(&stripped).map_err(|err| {
CliError::InitDataError(
Box::new(err),
format!("Failed to deserialize init data from file {}", path.display()),
)
})?;
Ok((init, faucet_metadata))
}
fn separate_auth_components(
components: Vec<AccountComponent>,
) -> Result<(Option<AccountComponent>, Vec<AccountComponent>), CliError> {
let mut auth_component: Option<AccountComponent> = None;
let mut regular_components = Vec::new();
for component in components {
let auth_proc_count = component.procedures().filter(|(_, is_auth)| *is_auth).count();
match auth_proc_count {
0 => regular_components.push(component),
1 => {
if auth_component.is_some() {
return Err(CliError::InvalidArgument(
"Multiple auth components found in packages. Only one auth component is allowed per account.".to_string()
));
}
auth_component = Some(component);
},
_ => {
return Err(CliError::InvalidArgument(
"Component has multiple auth procedures. Only one auth procedure is allowed per component.".to_string()
));
},
}
}
Ok((auth_component, regular_components))
}
fn should_add_implicit_token_policy_manager(regular_components: &[AccountComponent]) -> bool {
let has_basic_fungible_faucet = regular_components
.iter()
.any(|component| component.metadata().name() == FungibleFaucet::NAME);
let has_token_policy_manager = regular_components
.iter()
.any(|component| component.metadata().name() == TokenPolicyManager::NAME);
has_basic_fungible_faucet && !has_token_policy_manager
}
async fn create_client_account<AUTH: Keystore + Sync + 'static>(
client: &mut Client<AUTH>,
keystore: &CliKeyStore,
account_type: AccountType,
package_paths: &[PathBuf],
init_storage_data_path: Option<PathBuf>,
deploy: bool,
offline: bool,
) -> Result<Account, CliError> {
if package_paths.is_empty() {
return Err(CliError::InvalidArgument(format!(
"Account must contain at least one component. To provide one, pass a package with the -p flag, like so:
{} -p <package_name>
", client_binary_name().display())));
}
let cli_config = CliConfig::load()?;
debug!("Loading packages...");
let packages = load_packages(&cli_config, package_paths)?;
debug!("Loaded {} packages", packages.len());
debug!("Loading initialization storage data...");
let (init_storage_data, faucet_metadata) =
load_init_storage_data(init_storage_data_path.as_ref())?;
debug!("Loaded initialization storage data");
let mut packages = packages;
let injected_fungible_faucet = if let Some(metadata) = faucet_metadata.as_ref() {
if drop_basic_fungible_faucet_packages(&mut packages) {
debug!("Building FungibleFaucet component from fungible-faucet-metadata block");
Some(build_fungible_faucet_component(metadata)?)
} else {
None
}
} else {
None
};
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);
let mut builder = AccountBuilder::new(init_seed).account_type(account_type);
let account_components = process_packages(packages, &init_storage_data)?;
let (auth_component, mut regular_components) = separate_auth_components(account_components)?;
if let Some(component) = injected_fungible_faucet {
regular_components.push(component);
}
if should_add_implicit_token_policy_manager(®ular_components) {
debug!("Adding implicit TokenPolicyManager component for fungible faucet");
let policy_manager = TokenPolicyManager::new()
.with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)
.map_err(|err| {
CliError::Faucet(err.into(), "Failed to register mint policy".to_string())
})?
.with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)
.map_err(|err| {
CliError::Faucet(err.into(), "Failed to register burn policy".to_string())
})?;
regular_components.extend(policy_manager);
}
let key_pair = if let Some(auth_component) = auth_component {
debug!("Adding auth component from package");
builder = builder.with_auth_component(auth_component);
None
} else {
debug!("Adding default Falcon auth component");
let kp = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
builder = builder.with_auth_component(AuthSingleSig::new(
kp.public_key().to_commitment(),
AuthSchemeId::Falcon512Poseidon2,
));
Some(kp)
};
for component in regular_components {
builder = builder.with_component(component);
}
let account = builder
.build_with_schema_commitment()
.map_err(|err| CliError::Account(err, "failed to build account".into()))?;
if let Some(key_pair) = key_pair {
keystore.add_key(&key_pair, account.id()).await.map_err(CliError::KeyStore)?;
println!("Generated and stored Falcon512 authentication key in keystore.");
} else {
println!("Using custom authentication component from package (no key generated).");
}
let _ = offline;
#[cfg(feature = "testing")]
if offline {
client.prepare_offline_bootstrap().await?;
println!("Offline mode enabled for local account creation.");
}
client.add_account(&account, false).await?;
if deploy {
deploy_account(client, &account).await?;
}
Ok(account)
}
async fn deploy_account<AUTH: Keystore + Sync + 'static>(
client: &mut Client<AUTH>,
account: &Account,
) -> Result<(), CliError> {
let tx_request = TransactionRequestBuilder::new().build().map_err(|err| {
CliError::Transaction(err.into(), "Failed to build deploy transaction".to_string())
})?;
client.submit_new_transaction(account.id(), tx_request).await?;
Ok(())
}
fn process_packages(
packages: Vec<Package>,
init_storage_data: &InitStorageData,
) -> Result<Vec<AccountComponent>, CliError> {
let mut account_components = Vec::with_capacity(packages.len());
for package in packages {
let mut value_entries = init_storage_data.values().clone();
let mut map_entries = BTreeMap::new();
let Some(component_metadata_section) = package.sections.iter().find(|section| {
section.id.as_str() == (SectionId::ACCOUNT_COMPONENT_METADATA).as_str()
}) else {
continue;
};
let component_metadata = AccountComponentMetadata::read_from_bytes(
&component_metadata_section.data,
)
.map_err(|err| {
CliError::AccountComponentError(
Box::new(err),
format!(
"Failed to deserialize Account Component Metadata from package {}",
package.name
),
)
})?;
for (slot_name, schema) in component_metadata.storage_schema().iter() {
if matches!(schema, StorageSlotSchema::Map(_))
&& let Some(entries) = init_storage_data.map_entries(slot_name)
{
map_entries.insert(slot_name.clone(), entries.clone());
}
}
for (value_name, requirement) in component_metadata.schema_requirements() {
if value_entries.contains_key(&value_name) {
continue;
}
if let Some(default_value) = &requirement.default_value {
value_entries.insert(value_name, default_value.clone().into());
continue;
}
let description = requirement.description.unwrap_or("[No description]".into());
println!(
"Enter value for '{value_name}' - {description} (type: {}): ",
requirement.r#type
);
std::io::stdout().flush()?;
let mut input_value = String::new();
std::io::stdin().read_line(&mut input_value)?;
let input_value = input_value.trim();
value_entries.insert(value_name, input_value.to_string().into());
}
let init_data = InitStorageData::new(value_entries, map_entries).map_err(|e| {
CliError::AccountComponentError(
Box::new(e),
format!("error creating InitStorageData for Package {}", package.name),
)
})?;
let account_component =
AccountComponent::from_package(&package, &init_data).map_err(|e| {
CliError::Account(
e,
format!("error instantiating component from Package {}", package.name),
)
})?;
account_components.push(account_component);
}
Ok(account_components)
}
#[cfg(test)]
mod tests {
use miden_client::account::component::{BasicWallet, TokenName};
use miden_client::asset::{AssetAmount, TokenSymbol};
use super::*;
fn test_fungible_faucet_component() -> AccountComponent {
FungibleFaucet::builder()
.name(TokenName::new("TST").unwrap())
.symbol(TokenSymbol::new("TST").unwrap())
.decimals(8)
.max_supply(AssetAmount::new(1_000_000).unwrap())
.build()
.unwrap()
.into()
}
#[test]
fn implicit_token_policy_manager_is_added_for_basic_faucet_accounts() {
let regular_components = vec![test_fungible_faucet_component()];
assert!(should_add_implicit_token_policy_manager(®ular_components));
}
#[test]
fn implicit_token_policy_manager_is_skipped_when_component_already_present() {
let mut regular_components: Vec<AccountComponent> = vec![test_fungible_faucet_component()];
let policy_manager = TokenPolicyManager::new()
.with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap()
.with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap();
regular_components.extend(policy_manager);
assert!(!should_add_implicit_token_policy_manager(®ular_components));
}
#[test]
fn implicit_token_policy_manager_is_not_added_for_non_faucet_accounts() {
let regular_components = vec![AccountComponent::from(BasicWallet)];
assert!(!should_add_implicit_token_policy_manager(®ular_components));
}
}