lssa/lez/wallet/src/cli/account.rs

543 lines
20 KiB
Rust
Raw Normal View History

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 _;
use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex};
use lee::{Account, PublicKey, program::Program};
use lee_core::Identifier;
use token_core::{TokenDefinition, TokenHolding};
2025-10-20 10:01:54 +03:00
use crate::{
WalletCore,
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,
/// 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")]
List {
2026-03-10 00:17:43 +03:00
/// Show detailed account information (like `account get`).
#[arg(short, long)]
long: bool,
},
2026-03-10 00:17:43 +03:00
/// Set a label for an account.
Label {
/// 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.
#[arg(short, long)]
label: Label,
},
/// 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>,
#[arg(short, long)]
2026-03-10 00:17:43 +03:00
/// Label to assign to the new account.
label: Option<Label>,
2025-11-10 16:29:33 +02: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.
Private {
#[arg(long)]
2026-05-07 22:48:32 +02:00
/// Chain index of a parent node.
cci: Option<ChainIndex>,
#[arg(short, long)]
/// Label to assign to the new account.
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.
group: Label,
2026-05-07 22:48:32 +02:00
#[arg(short, long)]
/// Label to assign to the new account.
label: Option<Label>,
#[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-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 } => {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
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
.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);
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Public(account_id))?;
}
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
wallet_core.store_persistent_data()?;
2025-10-20 10:01:54 +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 } => {
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
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(account_id))?;
2026-05-07 22:48:32 +02: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}"
);
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
2026-05-07 22:48:32 +02:00
println!(
"With vpk {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
2026-05-07 22:48:32 +02:00
);
wallet_core.store_persistent_data()?;
2026-05-07 22:48:32 +02:00
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
Self::PrivateGms {
group,
label,
pda,
seed,
program_id,
2026-05-12 01:29:24 -03:00
identifier,
} => {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
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-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"))?;
let pda_seed = lee_core::program::PdaSeed::new(seed_bytes);
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");
}
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-12 01:29:24 -03:00
wallet_core.create_shared_pda_account(
group.clone(),
2026-05-12 01:29:24 -03:00
pda_seed,
pid,
identifier.unwrap_or_else(rand::random),
)?
} else {
wallet_core.create_shared_regular_account(group.clone())?
2026-05-07 22:48:32 +02:00
};
2026-05-07 22:48:32 +02:00
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(info.account_id))?;
2026-05-07 22:48:32 +02: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));
wallet_core.store_persistent_data()?;
2026-05-07 22:48:32 +02:00
Ok(SubcommandReturnValue::RegisterAccount {
account_id: info.account_id,
})
}
2026-04-17 19:45:30 -03:00
Self::PrivateAccountsKey { cci } => {
let chain_index = wallet_core.create_private_accounts_key(cci);
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
println!("Generated new private key node at path {chain_index}");
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 {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
2025-10-20 10:01:54 +03:00
);
wallet_core.store_persistent_data()?;
2025-10-20 10:01:54 +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,
} => {
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
let account = wallet_core.get_account(resolved).await?;
2025-10-24 15:26:30 +03:00
// Helper closure to display keys for the account
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
match resolved {
AccountIdWithPrivacy::Public(account_id) => {
2026-01-16 11:03:01 +11:00
let private_key = wallet_core
.storage
.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()));
}
AccountIdWithPrivacy::Private(account_id) => {
let acc = wallet_core
2026-01-16 11:03:01 +11:00
.storage
.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
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
}
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();
println!("{account_hr}");
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 => {
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 } => {
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}]")
}
};
if !long {
let accounts = key_chain
.account_ids()
.map(|(id, idx)| format_with_label(id, idx))
.format("\n");
println!("{accounts}");
return Ok(SubcommandReturnValue::Empty);
}
// Detailed listing with --long flag
// Public key tree accounts
for (id, chain_index) in key_chain.public_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Public(id), chain_index)
);
match wallet_core.get_account_public(id).await {
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}"),
}
}
// Private key tree accounts
for (id, chain_index) in key_chain.private_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Private(id), chain_index)
);
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"),
}
}
Ok(SubcommandReturnValue::Empty)
}
Self::Label { account_id, label } => {
let account_id = account_id.resolve(wallet_core.storage())?;
wallet_core
.storage_mut()
.add_label(label.clone(), account_id)?;
wallet_core.store_persistent_data()?;
println!("Label '{label}' set for account {account_id}");
2025-11-27 04:22:49 +03:00
Ok(SubcommandReturnValue::Empty)
}
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)]
private_key: lee::PrivateKey,
},
/// 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 =
lee::AccountId::from(&lee::PublicKey::new_from_private_key(&private_key));
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
} => {
let key_chain: KeyChain = serde_json::from_str(&key_chain_json)
.map_err(|err| anyhow::anyhow!("Invalid key chain JSON: {err}"))?;
let account = lee::Account::from(account_state);
let account_id =
lee::AccountId::from((&key_chain.nullifier_public_key, identifier));
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
wallet_core.store_persistent_data()?;
2025-12-03 13:50:10 +02: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(),
)
}
}
}