456 lines
17 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 _;
2025-11-28 09:49:05 +02:00
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
2026-01-16 11:03:01 +11:00
use nssa::{Account, PublicKey, program::Program};
use token_core::{TokenDefinition, TokenHolding};
2025-10-20 10:01:54 +03:00
use crate::{
WalletCore,
cli::{SubcommandReturnValue, WalletSubcommand},
config::Label,
2025-12-03 00:17:12 +03:00
helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix},
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-03-10 00:17:43 +03:00
/// Valid 32 byte base58 string with privacy prefix.
2025-10-24 15:26:30 +03:00
#[arg(short, long)]
account_id: String,
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 {
2026-03-10 00:17:43 +03:00
/// Valid 32 byte base58 string with privacy prefix.
#[arg(short, long)]
account_id: String,
2026-03-10 00:17:43 +03:00
/// The label to assign to the account.
#[arg(short, long)]
label: String,
},
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<String>,
2025-11-10 16:29:33 +02:00
},
2026-03-10 00:17:43 +03:00
/// Register new private account.
2025-11-10 16:29:33 +02:00
Private {
#[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<String>,
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-03-04 18:42:33 +03:00
if let Some(label) = &label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
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
.user_data
.get_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
.labels
.insert(account_id.to_string(), Label::new(label));
}
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().await?;
2025-10-20 10:01:54 +03:00
Ok(SubcommandReturnValue::RegisterAccount { account_id })
2025-10-20 10:01:54 +03:00
}
2026-03-09 18:27:56 +03:00
Self::Private { cci, label } => {
2026-03-04 18:42:33 +03:00
if let Some(label) = &label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
2025-12-03 13:10:07 +02:00
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
2025-10-20 10:01:54 +03:00
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
2025-10-20 10:01:54 +03:00
let (key, _) = wallet_core
.storage
.user_data
.get_private_account(account_id)
2025-10-20 10:01:54 +03:00
.unwrap();
2025-10-24 11:12:32 +03:00
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}",
2025-10-24 11:12:32 +03:00
);
2026-03-09 12:23:57 -04:00
println!("With npk {}", hex::encode(key.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.viewing_public_key.to_bytes())
2025-10-20 10:01:54 +03:00
);
wallet_core.store_persistent_data().await?;
2025-10-20 10:01:54 +03:00
Ok(SubcommandReturnValue::RegisterAccount { account_id })
2025-10-20 10:01:54 +03:00
}
}
}
}
impl WalletSubcommand for AccountSubcommand {
2026-03-04 18:42:33 +03:00
#[expect(clippy::cognitive_complexity, reason = "TODO: fix later")]
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::Get {
2026-01-07 10:07:44 +11:00
raw,
keys,
account_id,
} => {
let (account_id_str, addr_kind) = parse_addr_with_privacy_prefix(&account_id)?;
2025-10-24 15:26:30 +03:00
let account_id: nssa::AccountId = account_id_str.parse()?;
2025-10-24 15:26:30 +03:00
if let Some(label) = wallet_core.storage.labels.get(&account_id_str) {
println!("Label: {label}");
}
2025-10-28 16:02:30 +02:00
2025-10-24 15:26:30 +03:00
let account = match addr_kind {
AccountPrivacyKind::Public => {
wallet_core.get_account_public(account_id).await?
}
AccountPrivacyKind::Private => wallet_core
.get_account_private(account_id)
2026-03-09 18:27:56 +03:00
.context("Private account not found in storage")?,
2025-10-24 15:26:30 +03:00
};
// Helper closure to display keys for the account
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
2026-01-16 11:03:01 +11:00
match addr_kind {
AccountPrivacyKind::Public => {
let private_key = wallet_core
.storage
.user_data
.get_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()));
}
AccountPrivacyKind::Private => {
let (key, _) = wallet_core
.storage
.user_data
.get_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-03-09 12:23:57 -04:00
println!("npk {}", hex::encode(key.nullifier_public_key.0));
2026-01-21 17:27:23 -05:00
println!("vpk {}", hex::encode(key.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!("{}", serde_json::to_string(&account_hr).unwrap());
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 => {
2025-10-28 16:02:30 +02:00
let curr_last_block = wallet_core
.sequencer_client
.get_last_block()
.await?
.last_block;
2025-11-26 07:32:35 +02:00
if wallet_core
2025-10-28 16:02:30 +02:00
.storage
.user_data
2025-11-10 16:29:33 +02:00
.private_key_tree
.account_id_map
2025-10-28 16:02:30 +02:00
.is_empty()
{
2025-11-26 07:32:35 +02:00
wallet_core.last_synced_block = curr_last_block;
wallet_core.store_persistent_data().await?;
2025-11-26 07:32:35 +02:00
} else {
2025-12-03 00:17:12 +03:00
wallet_core.sync_to_block(curr_last_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 } => {
2025-11-27 04:22:49 +03:00
let user_data = &wallet_core.storage.user_data;
let labels = &wallet_core.storage.labels;
let format_with_label = |prefix: &str, id: nssa::AccountId| {
let id_str = id.to_string();
2026-03-09 18:27:56 +03:00
labels
.get(&id_str)
.map_or_else(|| prefix.to_owned(), |label| format!("{prefix} [{label}]"))
};
if !long {
let accounts =
user_data
.default_pub_account_signing_keys
.keys()
.copied()
.map(|id| format_with_label(&format!("Preconfigured Public/{id}"), id))
.chain(user_data.default_user_private_accounts.keys().copied().map(
|id| format_with_label(&format!("Preconfigured Private/{id}"), id),
))
.chain(user_data.public_key_tree.account_id_map.iter().map(
|(id, chain_index)| {
format_with_label(&format!("{chain_index} Public/{id}"), *id)
},
))
.chain(user_data.private_key_tree.account_id_map.iter().map(
|(id, chain_index)| {
format_with_label(&format!("{chain_index} Private/{id}"), *id)
},
))
.format("\n");
println!("{accounts}");
return Ok(SubcommandReturnValue::Empty);
}
// Detailed listing with --long flag
// Preconfigured public accounts
for id in user_data.default_pub_account_signing_keys.keys().copied() {
println!(
"{}",
format_with_label(&format!("Preconfigured Public/{id}"), id)
);
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}"),
}
}
// Preconfigured private accounts
for id in user_data.default_user_private_accounts.keys().copied() {
println!(
"{}",
format_with_label(&format!("Preconfigured Private/{id}"), id)
);
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"),
}
}
// Public key tree accounts
2026-03-03 23:21:08 +03:00
for (id, chain_index) in &user_data.public_key_tree.account_id_map {
println!(
"{}",
format_with_label(&format!("{chain_index} Public/{id}"), *id)
);
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
2026-03-03 23:21:08 +03:00
for (id, chain_index) in &user_data.private_key_tree.account_id_map {
println!(
"{}",
format_with_label(&format!("{chain_index} Private/{id}"), *id)
);
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"),
}
}
2025-11-27 04:22:49 +03:00
Ok(SubcommandReturnValue::Empty)
}
2026-03-09 18:27:56 +03:00
Self::Label { account_id, label } => {
let (account_id_str, _) = parse_addr_with_privacy_prefix(&account_id)?;
2025-12-03 13:50:10 +02:00
// Check if label is already used by a different account
if let Some(existing_account) = wallet_core
.storage
.labels
.iter()
.find(|(_, l)| l.to_string() == label)
.map(|(a, _)| a.clone())
&& existing_account != account_id_str
{
anyhow::bail!(
"Label '{label}' is already in use by account {existing_account}"
);
}
let old_label = wallet_core
.storage
.labels
.insert(account_id_str.clone(), Label::new(label.clone()));
2025-12-03 13:50:10 +02:00
wallet_core.store_persistent_data().await?;
2025-12-03 13:50:10 +02:00
if let Some(old) = old_label {
eprintln!("Warning: overriding existing label '{old}'");
}
println!("Label '{label}' set for account {account_id_str}");
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(),
)
}
}
}