2026-03-09 18:27:56 +03:00
|
|
|
use anyhow::{Context as _, Result};
|
2025-10-20 10:01:54 +03:00
|
|
|
use clap::Subcommand;
|
2025-11-27 04:22:49 +03:00
|
|
|
use itertools::Itertools as _;
|
2026-04-10 20:23:25 +03:00
|
|
|
use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex};
|
2026-06-01 17:10:46 -03:00
|
|
|
use lee::{Account, PublicKey, program::Program};
|
|
|
|
|
use lee_core::Identifier;
|
2026-01-16 04:11:33 +03:00
|
|
|
use token_core::{TokenDefinition, TokenHolding};
|
2025-10-20 10:01:54 +03:00
|
|
|
|
|
|
|
|
use crate::{
|
2026-01-16 04:11:33 +03:00
|
|
|
WalletCore,
|
2026-04-10 20:23:25 +03:00
|
|
|
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
|
|
|
|
|
cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand},
|
2025-10-20 10:01:54 +03:00
|
|
|
};
|
|
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Represents generic chain CLI subcommand.
|
2025-10-20 10:01:54 +03:00
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
|
|
|
pub enum AccountSubcommand {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Get account data.
|
2025-10-24 15:26:30 +03:00
|
|
|
Get {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Flag to get raw account data.
|
2025-10-28 16:53:39 +02:00
|
|
|
#[arg(short, long)]
|
2025-10-24 15:26:30 +03:00
|
|
|
raw: bool,
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Display keys (pk for public accounts, npk/vpk for private accounts).
|
2026-01-07 10:07:44 +11:00
|
|
|
#[arg(short, long)]
|
|
|
|
|
keys: bool,
|
2026-04-10 20:23:25 +03:00
|
|
|
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
account_id: CliAccountMention,
|
2025-10-24 15:26:30 +03:00
|
|
|
},
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Produce new public or private account.
|
2025-10-20 10:01:54 +03:00
|
|
|
#[command(subcommand)]
|
2025-10-23 17:33:25 +03:00
|
|
|
New(NewSubcommand),
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Sync private accounts.
|
2026-03-04 18:42:33 +03:00
|
|
|
SyncPrivate,
|
2026-03-10 00:17:43 +03:00
|
|
|
/// List all accounts owned by the wallet.
|
2025-11-27 04:22:49 +03:00
|
|
|
#[command(visible_alias = "ls")]
|
2026-01-07 16:10:38 +11:00
|
|
|
List {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Show detailed account information (like `account get`).
|
2026-01-07 16:10:38 +11:00
|
|
|
#[arg(short, long)]
|
|
|
|
|
long: bool,
|
|
|
|
|
},
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Set a label for an account.
|
2026-01-07 13:13:14 +11:00
|
|
|
Label {
|
2026-04-10 20:23:25 +03:00
|
|
|
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
account_id: CliAccountMention,
|
2026-03-10 00:17:43 +03:00
|
|
|
/// The label to assign to the account.
|
2026-01-07 13:13:14 +11:00
|
|
|
#[arg(short, long)]
|
2026-04-10 20:23:25 +03:00
|
|
|
label: Label,
|
2026-01-07 13:13:14 +11:00
|
|
|
},
|
2026-04-10 20:23:25 +03:00
|
|
|
/// Import external account.
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
Import(ImportSubcommand),
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Represents generic register CLI subcommand.
|
2025-10-20 10:01:54 +03:00
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
2025-10-23 17:33:25 +03:00
|
|
|
pub enum NewSubcommand {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Register new public account.
|
2025-11-11 12:15:20 +02:00
|
|
|
Public {
|
2025-11-10 16:29:33 +02:00
|
|
|
#[arg(long)]
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Chain index of a parent node.
|
2025-12-03 13:10:07 +02:00
|
|
|
cci: Option<ChainIndex>,
|
2026-01-19 13:04:43 +01:00
|
|
|
#[arg(short, long)]
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Label to assign to the new account.
|
2026-04-10 20:23:25 +03:00
|
|
|
label: Option<Label>,
|
2025-11-10 16:29:33 +02:00
|
|
|
},
|
2026-04-29 12:27:43 -03:00
|
|
|
/// Single-account convenience: creates a key node and auto-registers one account with a random
|
2026-05-07 22:48:32 +02:00
|
|
|
/// identifier.
|
2026-04-21 02:09:30 -03:00
|
|
|
Private {
|
|
|
|
|
#[arg(long)]
|
2026-05-07 22:48:32 +02:00
|
|
|
/// Chain index of a parent node.
|
2026-04-21 02:09:30 -03:00
|
|
|
cci: Option<ChainIndex>,
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
/// Label to assign to the new account.
|
2026-04-10 20:23:25 +03:00
|
|
|
label: Option<Label>,
|
2026-05-07 22:48:32 +02:00
|
|
|
},
|
|
|
|
|
/// Create a shared private account from a group's GMS.
|
|
|
|
|
PrivateGms {
|
|
|
|
|
/// Group name to derive keys from.
|
2026-04-10 20:23:25 +03:00
|
|
|
group: Label,
|
2026-05-07 22:48:32 +02:00
|
|
|
#[arg(short, long)]
|
|
|
|
|
/// Label to assign to the new account.
|
2026-04-10 20:23:25 +03:00
|
|
|
label: Option<Label>,
|
2026-05-05 18:55:51 +02:00
|
|
|
#[arg(long)]
|
|
|
|
|
/// Create a PDA account (requires --seed and --program-id).
|
|
|
|
|
pda: bool,
|
|
|
|
|
#[arg(long, requires = "pda")]
|
|
|
|
|
/// PDA seed as 64-character hex string.
|
|
|
|
|
seed: Option<String>,
|
|
|
|
|
#[arg(long, requires = "pda")]
|
|
|
|
|
/// Program ID as hex string.
|
|
|
|
|
program_id: Option<String>,
|
2026-05-12 01:29:24 -03:00
|
|
|
#[arg(long, requires = "pda")]
|
2026-05-12 13:55:30 -03:00
|
|
|
/// Identifier that diversifies this PDA within the (`program_id`, seed, npk) family.
|
2026-05-12 01:29:24 -03:00
|
|
|
/// Defaults to a random value if not specified.
|
|
|
|
|
identifier: Option<u128>,
|
2026-04-21 02:09:30 -03:00
|
|
|
},
|
2026-04-28 00:18:57 -03:00
|
|
|
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
|
|
|
|
|
/// registering any account.
|
2026-04-17 19:45:30 -03:00
|
|
|
PrivateAccountsKey {
|
2025-11-10 16:29:33 +02:00
|
|
|
#[arg(long)]
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Chain index of a parent node.
|
2025-12-03 13:10:07 +02:00
|
|
|
cci: Option<ChainIndex>,
|
2025-11-10 16:29:33 +02:00
|
|
|
},
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-23 17:33:25 +03:00
|
|
|
impl WalletSubcommand for NewSubcommand {
|
2025-10-20 10:01:54 +03:00
|
|
|
async fn handle_subcommand(
|
|
|
|
|
self,
|
|
|
|
|
wallet_core: &mut WalletCore,
|
|
|
|
|
) -> Result<SubcommandReturnValue> {
|
|
|
|
|
match self {
|
2026-03-09 18:27:56 +03:00
|
|
|
Self::Public { cci, label } => {
|
2026-04-10 20:23:25 +03:00
|
|
|
if let Some(label) = &label {
|
|
|
|
|
wallet_core.storage().check_label_availability(label)?;
|
2026-01-19 13:04:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 13:10:07 +02:00
|
|
|
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
|
2025-10-20 10:01:54 +03:00
|
|
|
|
2026-01-16 11:03:01 +11:00
|
|
|
let private_key = wallet_core
|
|
|
|
|
.storage
|
2026-04-10 20:23:25 +03:00
|
|
|
.key_chain()
|
|
|
|
|
.pub_account_signing_key(account_id)
|
2026-01-16 11:03:01 +11:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let public_key = PublicKey::new_from_private_key(private_key);
|
|
|
|
|
|
2026-01-19 13:04:43 +01:00
|
|
|
if let Some(label) = label {
|
|
|
|
|
wallet_core
|
2026-04-10 20:23:25 +03:00
|
|
|
.storage_mut()
|
|
|
|
|
.add_label(label, AccountIdWithPrivacy::Public(account_id))?;
|
2026-01-19 13:04:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 13:10:07 +02:00
|
|
|
println!(
|
|
|
|
|
"Generated new account with account_id Public/{account_id} at path {chain_index}"
|
|
|
|
|
);
|
2026-01-16 11:03:01 +11:00
|
|
|
println!("With pk {}", hex::encode(public_key.value()));
|
2025-10-20 10:01:54 +03:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
2025-10-20 10:01:54 +03:00
|
|
|
|
2025-11-24 17:09:30 +03:00
|
|
|
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
2026-05-07 22:48:32 +02:00
|
|
|
Self::Private { cci, label } => {
|
2026-04-10 20:23:25 +03:00
|
|
|
if let Some(label) = &label {
|
|
|
|
|
wallet_core.storage().check_label_availability(label)?;
|
2026-05-07 22:48:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
|
|
|
|
|
|
|
|
|
|
if let Some(label) = label {
|
|
|
|
|
wallet_core
|
2026-04-10 20:23:25 +03:00
|
|
|
.storage_mut()
|
|
|
|
|
.add_label(label, AccountIdWithPrivacy::Private(account_id))?;
|
2026-05-07 22:48:32 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
let found_acc = wallet_core
|
|
|
|
|
.storage()
|
|
|
|
|
.key_chain()
|
|
|
|
|
.private_account(account_id)
|
|
|
|
|
.expect("Account should exist after creation");
|
|
|
|
|
let key_chain = found_acc.key_chain;
|
|
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
println!(
|
|
|
|
|
"Generated new account with account_id Private/{account_id} at path {chain_index}"
|
|
|
|
|
);
|
2026-04-10 20:23:25 +03:00
|
|
|
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
|
2026-05-07 22:48:32 +02:00
|
|
|
println!(
|
|
|
|
|
"With vpk {}",
|
2026-04-10 20:23:25 +03:00
|
|
|
hex::encode(key_chain.viewing_public_key.to_bytes())
|
2026-05-07 22:48:32 +02:00
|
|
|
);
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
|
|
|
|
}
|
|
|
|
|
Self::PrivateGms {
|
|
|
|
|
group,
|
2026-05-05 18:55:51 +02:00
|
|
|
label,
|
|
|
|
|
pda,
|
|
|
|
|
seed,
|
|
|
|
|
program_id,
|
2026-05-12 01:29:24 -03:00
|
|
|
identifier,
|
2026-05-05 18:55:51 +02:00
|
|
|
} => {
|
2026-04-10 20:23:25 +03:00
|
|
|
if let Some(label) = &label {
|
|
|
|
|
wallet_core.storage().check_label_availability(label)?;
|
2026-04-21 02:09:30 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
let info = if pda {
|
|
|
|
|
let seed_hex = seed.context("--seed is required for PDA accounts")?;
|
|
|
|
|
let pid_hex =
|
|
|
|
|
program_id.context("--program-id is required for PDA accounts")?;
|
2026-04-21 02:09:30 -03:00
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
|
|
|
|
|
.context("Invalid seed hex")?
|
|
|
|
|
.try_into()
|
|
|
|
|
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
|
2026-06-01 17:10:46 -03:00
|
|
|
let pda_seed = lee_core::program::PdaSeed::new(seed_bytes);
|
2026-05-05 18:55:51 +02:00
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
|
|
|
|
|
if pid_bytes.len() != 32 {
|
|
|
|
|
anyhow::bail!("Program ID must be exactly 32 bytes");
|
|
|
|
|
}
|
2026-06-01 17:10:46 -03:00
|
|
|
let mut pid: lee_core::program::ProgramId = [0; 8];
|
2026-05-07 22:48:32 +02:00
|
|
|
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
|
|
|
|
|
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
|
2026-05-07 17:35:51 +02:00
|
|
|
}
|
2026-05-05 18:55:51 +02:00
|
|
|
|
2026-05-12 01:29:24 -03:00
|
|
|
wallet_core.create_shared_pda_account(
|
2026-04-10 20:23:25 +03:00
|
|
|
group.clone(),
|
2026-05-12 01:29:24 -03:00
|
|
|
pda_seed,
|
|
|
|
|
pid,
|
|
|
|
|
identifier.unwrap_or_else(rand::random),
|
|
|
|
|
)?
|
2026-05-05 18:55:51 +02:00
|
|
|
} else {
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.create_shared_regular_account(group.clone())?
|
2026-05-07 22:48:32 +02:00
|
|
|
};
|
2026-05-05 18:55:51 +02:00
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
if let Some(label) = label {
|
|
|
|
|
wallet_core
|
2026-04-10 20:23:25 +03:00
|
|
|
.storage_mut()
|
|
|
|
|
.add_label(label, AccountIdWithPrivacy::Private(info.account_id))?;
|
2026-05-07 22:48:32 +02:00
|
|
|
}
|
2026-04-21 02:09:30 -03:00
|
|
|
|
2026-05-07 22:48:32 +02:00
|
|
|
println!("Shared account from group '{group}'");
|
|
|
|
|
println!("AccountId: Private/{}", info.account_id);
|
|
|
|
|
println!("NPK: {}", hex::encode(info.npk.0));
|
|
|
|
|
println!("VPK: {}", hex::encode(&info.vpk.0));
|
2026-04-21 02:09:30 -03:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
2026-05-07 22:48:32 +02:00
|
|
|
Ok(SubcommandReturnValue::RegisterAccount {
|
|
|
|
|
account_id: info.account_id,
|
|
|
|
|
})
|
2026-04-21 02:09:30 -03:00
|
|
|
}
|
2026-04-17 19:45:30 -03:00
|
|
|
Self::PrivateAccountsKey { cci } => {
|
|
|
|
|
let chain_index = wallet_core.create_private_accounts_key(cci);
|
2026-04-10 20:23:25 +03:00
|
|
|
let key_chain = wallet_core
|
|
|
|
|
.storage()
|
|
|
|
|
.key_chain()
|
|
|
|
|
.private_account_key_chain_by_index(&chain_index)
|
|
|
|
|
.expect("Key chain should exist after creation");
|
2025-10-20 10:01:54 +03:00
|
|
|
|
2026-04-17 14:26:50 -03:00
|
|
|
println!("Generated new private key node at path {chain_index}");
|
2026-04-10 20:23:25 +03:00
|
|
|
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
|
2025-10-20 10:01:54 +03:00
|
|
|
println!(
|
2026-01-21 17:27:23 -05:00
|
|
|
"With vpk {}",
|
2026-04-10 20:23:25 +03:00
|
|
|
hex::encode(key_chain.viewing_public_key.to_bytes())
|
2025-10-20 10:01:54 +03:00
|
|
|
);
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
2025-10-20 10:01:54 +03:00
|
|
|
|
2026-04-17 14:26:50 -03:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WalletSubcommand for AccountSubcommand {
|
|
|
|
|
async fn handle_subcommand(
|
|
|
|
|
self,
|
|
|
|
|
wallet_core: &mut WalletCore,
|
|
|
|
|
) -> Result<SubcommandReturnValue> {
|
|
|
|
|
match self {
|
2026-03-09 18:27:56 +03:00
|
|
|
Self::Get {
|
2026-01-07 10:07:44 +11:00
|
|
|
raw,
|
|
|
|
|
keys,
|
|
|
|
|
account_id,
|
|
|
|
|
} => {
|
2026-04-10 20:23:25 +03:00
|
|
|
let resolved = account_id.resolve(wallet_core.storage())?;
|
|
|
|
|
wallet_core
|
|
|
|
|
.storage()
|
|
|
|
|
.labels_for_account(resolved)
|
|
|
|
|
.for_each(|label| {
|
|
|
|
|
println!("Label: {label}");
|
|
|
|
|
});
|
2025-10-28 16:02:30 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
let account = wallet_core.get_account(resolved).await?;
|
2025-10-24 15:26:30 +03:00
|
|
|
|
2026-01-16 11:12:57 +11:00
|
|
|
// Helper closure to display keys for the account
|
|
|
|
|
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
|
2026-04-10 20:23:25 +03:00
|
|
|
match resolved {
|
|
|
|
|
AccountIdWithPrivacy::Public(account_id) => {
|
2026-01-16 11:03:01 +11:00
|
|
|
let private_key = wallet_core
|
|
|
|
|
.storage
|
2026-04-10 20:23:25 +03:00
|
|
|
.key_chain()
|
|
|
|
|
.pub_account_signing_key(account_id)
|
2026-03-09 18:27:56 +03:00
|
|
|
.context("Public account not found in storage")?;
|
2026-01-16 11:03:01 +11:00
|
|
|
|
|
|
|
|
let public_key = PublicKey::new_from_private_key(private_key);
|
|
|
|
|
println!("pk {}", hex::encode(public_key.value()));
|
|
|
|
|
}
|
2026-04-10 20:23:25 +03:00
|
|
|
AccountIdWithPrivacy::Private(account_id) => {
|
|
|
|
|
let acc = wallet_core
|
2026-01-16 11:03:01 +11:00
|
|
|
.storage
|
2026-04-10 20:23:25 +03:00
|
|
|
.key_chain()
|
|
|
|
|
.private_account(account_id)
|
2026-03-09 18:27:56 +03:00
|
|
|
.context("Private account not found in storage")?;
|
2026-01-16 11:03:01 +11:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
println!("npk {}", hex::encode(acc.key_chain.nullifier_public_key.0));
|
|
|
|
|
println!(
|
|
|
|
|
"vpk {}",
|
|
|
|
|
hex::encode(acc.key_chain.viewing_public_key.to_bytes())
|
|
|
|
|
);
|
2026-01-16 11:03:01 +11:00
|
|
|
}
|
2026-01-07 10:07:44 +11:00
|
|
|
}
|
2026-01-16 11:12:57 +11:00
|
|
|
Ok(())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if account == Account::default() {
|
|
|
|
|
println!("Account is Uninitialized");
|
|
|
|
|
|
|
|
|
|
if keys {
|
|
|
|
|
display_keys(wallet_core)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if raw {
|
2026-03-09 18:27:56 +03:00
|
|
|
let account_hr: HumanReadableAccount = account.into();
|
2026-04-10 20:23:25 +03:00
|
|
|
println!("{account_hr}");
|
2026-01-16 11:12:57 +11:00
|
|
|
|
|
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (description, json_view) = format_account_details(&account);
|
|
|
|
|
println!("{description}");
|
|
|
|
|
println!("{json_view}");
|
|
|
|
|
|
|
|
|
|
if keys {
|
|
|
|
|
display_keys(wallet_core)?;
|
2026-01-07 10:07:44 +11:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:26:30 +03:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
2026-03-09 18:27:56 +03:00
|
|
|
Self::New(new_subcommand) => new_subcommand.handle_subcommand(wallet_core).await,
|
|
|
|
|
Self::SyncPrivate => {
|
2026-04-10 20:23:25 +03:00
|
|
|
let curr_last_block = wallet_core.sync_to_latest_block().await?;
|
2025-10-28 16:02:30 +02:00
|
|
|
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
|
2025-10-27 14:32:28 +02:00
|
|
|
}
|
2026-03-09 18:27:56 +03:00
|
|
|
Self::List { long } => {
|
2026-04-10 20:23:25 +03:00
|
|
|
let key_chain = &wallet_core.storage.key_chain();
|
|
|
|
|
let storage = wallet_core.storage();
|
|
|
|
|
|
|
|
|
|
let format_with_label =
|
|
|
|
|
|id: AccountIdWithPrivacy, chain_index: Option<&ChainIndex>| {
|
|
|
|
|
let id_str =
|
|
|
|
|
chain_index.map_or_else(|| id.to_string(), |cci| format!("{cci} {id}"));
|
|
|
|
|
|
|
|
|
|
let labels = storage.labels_for_account(id).format(", ").to_string();
|
|
|
|
|
if labels.is_empty() {
|
|
|
|
|
id_str
|
|
|
|
|
} else {
|
|
|
|
|
format!("{id_str} [{labels}]")
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-07 16:10:38 +11:00
|
|
|
|
|
|
|
|
if !long {
|
2026-04-10 20:23:25 +03:00
|
|
|
let accounts = key_chain
|
|
|
|
|
.account_ids()
|
|
|
|
|
.map(|(id, idx)| format_with_label(id, idx))
|
|
|
|
|
.format("\n");
|
2026-01-07 16:10:38 +11:00
|
|
|
println!("{accounts}");
|
2026-04-10 20:23:25 +03:00
|
|
|
|
2026-01-07 16:10:38 +11:00
|
|
|
return Ok(SubcommandReturnValue::Empty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detailed listing with --long flag
|
2026-04-10 20:23:25 +03:00
|
|
|
|
|
|
|
|
// Public key tree accounts
|
|
|
|
|
for (id, chain_index) in key_chain.public_account_ids() {
|
2026-01-07 13:13:14 +11:00
|
|
|
println!(
|
|
|
|
|
"{}",
|
2026-04-10 20:23:25 +03:00
|
|
|
format_with_label(AccountIdWithPrivacy::Public(id), chain_index)
|
2026-01-07 13:13:14 +11:00
|
|
|
);
|
2026-01-29 22:20:42 +03:00
|
|
|
match wallet_core.get_account_public(id).await {
|
2026-01-07 16:10:38 +11:00
|
|
|
Ok(account) if account != Account::default() => {
|
|
|
|
|
let (description, json_view) = format_account_details(&account);
|
|
|
|
|
println!(" {description}");
|
|
|
|
|
println!(" {json_view}");
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => println!(" Uninitialized"),
|
|
|
|
|
Err(e) => println!(" Error fetching account: {e}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
// Private key tree accounts
|
|
|
|
|
for (id, chain_index) in key_chain.private_account_ids() {
|
2026-01-07 13:13:14 +11:00
|
|
|
println!(
|
|
|
|
|
"{}",
|
2026-04-10 20:23:25 +03:00
|
|
|
format_with_label(AccountIdWithPrivacy::Private(id), chain_index)
|
2026-01-07 13:13:14 +11:00
|
|
|
);
|
2026-01-07 16:10:38 +11:00
|
|
|
match wallet_core.get_account_private(id) {
|
|
|
|
|
Some(account) if account != Account::default() => {
|
|
|
|
|
let (description, json_view) = format_account_details(&account);
|
|
|
|
|
println!(" {description}");
|
|
|
|
|
println!(" {json_view}");
|
|
|
|
|
}
|
|
|
|
|
Some(_) => println!(" Uninitialized"),
|
|
|
|
|
None => println!(" Not found in local storage"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
|
|
|
|
Self::Label { account_id, label } => {
|
|
|
|
|
let account_id = account_id.resolve(wallet_core.storage())?;
|
2026-01-07 16:10:38 +11:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core
|
|
|
|
|
.storage_mut()
|
|
|
|
|
.add_label(label.clone(), account_id)?;
|
|
|
|
|
|
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
|
|
|
|
|
|
println!("Label '{label}' set for account {account_id}");
|
2026-01-07 16:10:38 +11:00
|
|
|
|
2025-11-27 04:22:49 +03:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
2026-04-10 20:23:25 +03:00
|
|
|
Self::Import(import_subcommand) => {
|
|
|
|
|
import_subcommand.handle_subcommand(wallet_core).await
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
|
|
|
pub enum ImportSubcommand {
|
|
|
|
|
/// Import a public account signing key.
|
|
|
|
|
Public {
|
|
|
|
|
/// Private key in hex format.
|
|
|
|
|
#[arg(long)]
|
2026-06-01 17:10:46 -03:00
|
|
|
private_key: lee::PrivateKey,
|
2026-04-10 20:23:25 +03:00
|
|
|
},
|
|
|
|
|
/// Import a private account keychain and account state.
|
|
|
|
|
Private {
|
|
|
|
|
/// Private account keychain JSON.
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
key_chain_json: String,
|
|
|
|
|
/// Private account state JSON (`HumanReadableAccount`).
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
account_state: HumanReadableAccount,
|
|
|
|
|
/// Chain index.
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
chain_index: Option<ChainIndex>,
|
|
|
|
|
/// Identifier.
|
|
|
|
|
#[arg(long, default_value = "0")]
|
|
|
|
|
identifier: Identifier,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WalletSubcommand for ImportSubcommand {
|
|
|
|
|
async fn handle_subcommand(
|
|
|
|
|
self,
|
|
|
|
|
wallet_core: &mut WalletCore,
|
|
|
|
|
) -> Result<SubcommandReturnValue> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Public { private_key } => {
|
|
|
|
|
let account_id =
|
2026-06-01 17:10:46 -03:00
|
|
|
lee::AccountId::from(&lee::PublicKey::new_from_private_key(&private_key));
|
2026-04-10 20:23:25 +03:00
|
|
|
|
|
|
|
|
wallet_core
|
|
|
|
|
.storage_mut()
|
|
|
|
|
.key_chain_mut()
|
|
|
|
|
.add_imported_public_account(private_key);
|
|
|
|
|
|
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
|
|
|
|
|
|
println!("Imported public account Public/{account_id}");
|
|
|
|
|
|
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
|
|
|
|
Self::Private {
|
|
|
|
|
key_chain_json,
|
|
|
|
|
account_state,
|
|
|
|
|
chain_index,
|
|
|
|
|
identifier,
|
feat: add --account-label as alternative to --account-id across all wallet subcommands
Allow users to identify accounts by their human-readable label instead of the
full `Privacy/base58` account ID. This makes the CLI much more ergonomic for
users who have labeled their accounts.
- [x] Add `resolve_account_label()` in `helperfunctions.rs` that looks up a label,
determines account privacy (public/private), and returns the full `Privacy/id` string
- [x] Add `--account-label` (or `--from-label`, `--to-label`, `--definition-label`,
`--holder-label`, `--user-holding-*-label`) as mutually exclusive alternative to
every `--account-id`-style flag across all subcommands:
- `account get`, `account label`
- `auth-transfer init`, `auth-transfer send`
- `token new`, `token send`, `token burn`, `token mint`
- `pinata claim`
- `amm new`, `amm swap`, `amm add-liquidity`, `amm remove-liquidity`
- [x] Update zsh completion script with `_wallet_account_labels()` helper
- [x] Add bash completion script with `_wallet_get_account_labels()` helper
1. Start a local sequencer
2. Create accounts and label them: `wallet account new public --label alice`
3. Use labels in commands: `wallet account get --account-label alice`
4. Verify mutual exclusivity: `wallet account get --account-id <id> --account-label alice` should error
5. Test shell completions: `wallet account get --account-label <TAB>` should list labels
None
None
- [x] Complete PR description
- [x] Implement the core functionality
- [ ] Add/update tests
- [x] Add/update documentation and inline comments
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:33:51 +11:00
|
|
|
} => {
|
2026-04-10 20:23:25 +03:00
|
|
|
let key_chain: KeyChain = serde_json::from_str(&key_chain_json)
|
|
|
|
|
.map_err(|err| anyhow::anyhow!("Invalid key chain JSON: {err}"))?;
|
2026-06-01 17:10:46 -03:00
|
|
|
let account = lee::Account::from(account_state);
|
2026-04-10 20:23:25 +03:00
|
|
|
let account_id =
|
2026-06-01 17:10:46 -03:00
|
|
|
lee::AccountId::from((&key_chain.nullifier_public_key, identifier));
|
2026-01-19 13:04:43 +01:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core
|
|
|
|
|
.storage_mut()
|
|
|
|
|
.key_chain_mut()
|
|
|
|
|
.add_imported_private_account(key_chain, chain_index, identifier, account);
|
2025-12-03 13:50:10 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
2025-12-03 13:50:10 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
println!("Imported private account Private/{account_id}");
|
2025-12-03 13:50:10 +02:00
|
|
|
|
2025-11-27 04:22:49 +03:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
2025-10-20 10:01:54 +03:00
|
|
|
}
|
2025-12-03 13:50:10 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-04 18:42:33 +03:00
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Formats account details for display, returning (description, `json_view`).
|
2026-03-04 18:42:33 +03:00
|
|
|
fn format_account_details(account: &Account) -> (String, String) {
|
|
|
|
|
let auth_tr_prog_id = Program::authenticated_transfer_program().id();
|
|
|
|
|
let token_prog_id = Program::token().id();
|
|
|
|
|
|
|
|
|
|
match &account.program_owner {
|
|
|
|
|
o if *o == auth_tr_prog_id => {
|
|
|
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
|
|
|
(
|
2026-03-09 18:27:56 +03:00
|
|
|
"Account owned by authenticated transfer program".to_owned(),
|
2026-03-04 18:42:33 +03:00
|
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-03-09 18:27:56 +03:00
|
|
|
o if *o == token_prog_id => TokenDefinition::try_from(&account.data)
|
|
|
|
|
.map(|token_def| {
|
2026-03-04 18:42:33 +03:00
|
|
|
(
|
|
|
|
|
"Definition account owned by token program".to_owned(),
|
|
|
|
|
serde_json::to_string(&token_def).unwrap(),
|
|
|
|
|
)
|
2026-03-09 18:27:56 +03:00
|
|
|
})
|
|
|
|
|
.or_else(|_| {
|
|
|
|
|
TokenHolding::try_from(&account.data).map(|token_hold| {
|
|
|
|
|
(
|
|
|
|
|
"Holding account owned by token program".to_owned(),
|
|
|
|
|
serde_json::to_string(&token_hold).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|_| {
|
2026-03-04 18:42:33 +03:00
|
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
|
|
|
(
|
|
|
|
|
"Unknown token program account".to_owned(),
|
|
|
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
|
|
|
)
|
2026-03-09 18:27:56 +03:00
|
|
|
}),
|
2026-03-04 18:42:33 +03:00
|
|
|
_ => {
|
|
|
|
|
let account_hr: HumanReadableAccount = account.clone().into();
|
|
|
|
|
(
|
|
|
|
|
"Account".to_owned(),
|
|
|
|
|
serde_json::to_string(&account_hr).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|