use clap::Parser;
use comfy_table::{Cell, ContentArrangement, presets};
use miden_client::{
Client, ZERO,
account::{Account, AccountId, AccountType, StorageSlot},
asset::Asset,
};
use miden_objects::PrettyPrint;
use crate::{
CLIENT_BINARY_NAME,
config::CliConfig,
create_dynamic_table,
errors::CliError,
utils::{load_config_file, load_faucet_details_map, parse_account_id, update_config},
};
#[derive(Default, Debug, Clone, Parser)]
#[allow(clippy::option_option)]
pub struct AccountCmd {
#[clap(short, long, group = "action")]
list: bool,
#[clap(short, long, group = "action", value_name = "ID")]
show: Option<String>,
#[clap(long, requires = "show")]
with_code: bool,
#[clap(short, long, group = "action", value_name = "ID")]
default: Option<Option<String>>,
}
impl AccountCmd {
pub async fn execute(&self, client: Client) -> Result<(), CliError> {
let (cli_config, _) = load_config_file()?;
match self {
AccountCmd {
list: false,
show: Some(id),
default: None,
..
} => {
let account_id = parse_account_id(&client, id).await?;
show_account(client, account_id, &cli_config, self.with_code).await?;
},
AccountCmd {
list: false,
show: None,
default: Some(id),
..
} => {
match id {
None => {
display_default_account_id()?;
},
Some(id) => {
let default_account = if id == "none" {
None
} else {
let account_id: AccountId = parse_account_id(&client, id).await?;
let (account, _) = client.try_get_account_header(account_id).await?;
Some(account.id())
};
set_default_account(default_account)?;
if let Some(id) = default_account {
let id = id.to_hex();
println!("Setting default account to {id}...");
} else {
println!("Removing default account...");
}
},
}
},
_ => {
list_accounts(client, &cli_config).await?;
},
}
Ok(())
}
}
async fn list_accounts(client: Client, cli_config: &CliConfig) -> Result<(), CliError> {
let accounts = client.get_account_headers().await?;
let mut table =
create_dynamic_table(&["Address", "Account ID", "Type", "Storage Mode", "Nonce", "Status"]);
for (acc, _acc_seed) in &accounts {
let status = client
.get_account(acc.id())
.await?
.expect("Account should be in store")
.status()
.to_string();
table.add_row(vec![
acc.id().to_bech32(cli_config.rpc.endpoint.0.to_network_id()?),
acc.id().to_hex(),
account_type_display_name(&acc.id())?,
acc.id().storage_mode().to_string(),
acc.nonce().as_int().to_string(),
status,
]);
}
println!("{table}");
Ok(())
}
pub async fn show_account(
client: Client,
account_id: AccountId,
cli_config: &CliConfig,
with_code: bool,
) -> Result<(), CliError> {
let account: Account = client
.get_account(account_id)
.await?
.ok_or(CliError::Input(format!("Account with ID {account_id} not found")))?
.into();
print_summary_table(&account, cli_config)?;
{
let assets = account.vault().assets();
let faucet_details_map = load_faucet_details_map()?;
println!("Assets: ");
let mut table = create_dynamic_table(&["Asset Type", "Faucet", "Amount"]);
for asset in assets {
let (asset_type, faucet, amount) = match asset {
Asset::Fungible(fungible_asset) => {
let (faucet, amount) =
faucet_details_map.format_fungible_asset(&fungible_asset)?;
("Fungible Asset", faucet, amount)
},
Asset::NonFungible(non_fungible_asset) => {
(
"Non Fungible Asset",
non_fungible_asset.faucet_id_prefix().to_hex(),
1.0.to_string(),
)
},
};
table.add_row(vec![asset_type, &faucet, &amount.to_string()]);
}
println!("{table}\n");
}
{
let account_storage = account.storage();
println!("Storage: \n");
let mut table =
create_dynamic_table(&["Item Slot Index", "Item Slot Type", "Value/Commitment"]);
for (idx, entry) in account_storage.slots().iter().enumerate() {
let item = account_storage
.get_item(u8::try_from(idx).expect("there are no more than 256 slots"))
.map_err(|err| CliError::Account(err, "Index out of bounds".to_string()))?;
if matches!(entry, StorageSlot::Value { .. }) && item == [ZERO; 4].into() {
continue;
}
let slot_type = match entry {
StorageSlot::Value(..) => "Value",
StorageSlot::Map(..) => "Map",
};
table.add_row(vec![&idx.to_string(), slot_type, &item.to_hex()]);
}
println!("{table}\n");
}
if with_code {
println!("Code: \n");
let mut table = create_dynamic_table(&["Code"]);
table.add_row(vec![&account.code().to_pretty_string()]);
println!("{table}");
}
Ok(())
}
fn print_summary_table(account: &Account, cli_config: &CliConfig) -> Result<(), CliError> {
let mut table = create_dynamic_table(&["Account Information"]);
table
.load_preset(presets::UTF8_HORIZONTAL_ONLY)
.set_content_arrangement(ContentArrangement::DynamicFullWidth);
table.add_row(vec![
Cell::new("Address"),
Cell::new(account.id().to_bech32(cli_config.rpc.endpoint.0.to_network_id()?)),
]);
table.add_row(vec![Cell::new("Account ID (hex)"), Cell::new(account.id().to_string())]);
table.add_row(vec![
Cell::new("Account Commitment"),
Cell::new(account.commitment().to_string()),
]);
table.add_row(vec![Cell::new("Type"), Cell::new(account_type_display_name(&account.id())?)]);
table.add_row(vec![
Cell::new("Storage mode"),
Cell::new(account.id().storage_mode().to_string()),
]);
table.add_row(vec![
Cell::new("Code Commitment"),
Cell::new(account.code().commitment().to_string()),
]);
table.add_row(vec![
Cell::new("Vault Root"),
Cell::new(account.vault().asset_tree().root().to_string()),
]);
table.add_row(vec![
Cell::new("Storage Root"),
Cell::new(account.storage().commitment().to_string()),
]);
table.add_row(vec![Cell::new("Nonce"), Cell::new(account.nonce().as_int().to_string())]);
println!("{table}\n");
Ok(())
}
fn account_type_display_name(account_id: &AccountId) -> Result<String, CliError> {
Ok(match account_id.account_type() {
AccountType::FungibleFaucet => {
let faucet_details_map = load_faucet_details_map()?;
let token_symbol = faucet_details_map.get_token_symbol_or_default(account_id);
format!("Fungible faucet (token symbol: {token_symbol})")
},
AccountType::NonFungibleFaucet => "Non-fungible faucet".to_string(),
AccountType::RegularAccountImmutableCode => "Regular".to_string(),
AccountType::RegularAccountUpdatableCode => "Regular (updatable)".to_string(),
})
}
fn display_default_account_id() -> Result<(), CliError> {
let (cli_config, _) = load_config_file()?;
let default_account = cli_config.default_account_id.ok_or(CliError::Config(
"Default account".to_string().into(),
"No default account found in the configuration file".to_string(),
))?;
println!("Current default account ID: {default_account}");
Ok(())
}
pub(crate) fn set_default_account(account_id: Option<AccountId>) -> Result<(), CliError> {
let (mut current_config, config_path) = load_config_file()?;
current_config.default_account_id = account_id.map(AccountId::to_hex);
update_config(&config_path, ¤t_config)
}
pub(crate) fn maybe_set_default_account(
current_config: &mut CliConfig,
account_id: AccountId,
) -> Result<(), CliError> {
if current_config.default_account_id.is_some() {
return Ok(());
}
set_default_account(Some(account_id))?;
let account_id = account_id.to_bech32(current_config.rpc.endpoint.0.to_network_id()?);
println!("Setting account {account_id} as the default account ID.");
println!("You can unset it with `{CLIENT_BINARY_NAME} account --default none`.");
current_config.default_account_id = Some(account_id);
Ok(())
}