2026-03-04 18:42:33 +03:00
|
|
|
use std::{collections::HashMap, path::PathBuf, str::FromStr as _};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2026-03-18 18:32:05 +02:00
|
|
|
use anyhow::{Context as _, Result};
|
|
|
|
|
use base58::ToBase58 as _;
|
2026-03-19 18:01:15 +02:00
|
|
|
use key_protocol::key_protocol_core::NSSAUserData;
|
2025-12-03 00:17:12 +03:00
|
|
|
use nssa::Account;
|
2025-11-26 00:27:20 +03:00
|
|
|
use nssa_core::account::Nonce;
|
2026-03-04 18:42:33 +03:00
|
|
|
use rand::{RngCore as _, rngs::OsRng};
|
2025-09-05 22:50:10 -03:00
|
|
|
use serde::Serialize;
|
2026-03-19 18:01:15 +02:00
|
|
|
use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2025-08-19 14:14:09 +03:00
|
|
|
use crate::{
|
2025-12-03 00:17:12 +03:00
|
|
|
HOME_DIR_ENV_VAR,
|
2025-09-11 18:32:46 +03:00
|
|
|
config::{
|
2026-03-13 18:23:39 +02:00
|
|
|
InitialAccountData, Label, PersistentAccountDataPrivate, PersistentAccountDataPublic,
|
|
|
|
|
PersistentStorage,
|
2025-09-11 18:32:46 +03:00
|
|
|
},
|
2025-08-19 14:14:09 +03:00
|
|
|
};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2026-03-04 18:42:33 +03:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum AccountPrivacyKind {
|
|
|
|
|
Public,
|
|
|
|
|
Private,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Human-readable representation of an account.
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub(crate) struct HumanReadableAccount {
|
|
|
|
|
balance: u128,
|
|
|
|
|
program_owner: String,
|
|
|
|
|
data: String,
|
|
|
|
|
nonce: u128,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Account> for HumanReadableAccount {
|
|
|
|
|
fn from(account: Account) -> Self {
|
|
|
|
|
let program_owner = account
|
|
|
|
|
.program_owner
|
|
|
|
|
.iter()
|
|
|
|
|
.flat_map(|n| n.to_le_bytes())
|
|
|
|
|
.collect::<Vec<u8>>()
|
|
|
|
|
.to_base58();
|
|
|
|
|
let data = hex::encode(account.data);
|
|
|
|
|
Self {
|
|
|
|
|
balance: account.balance,
|
|
|
|
|
program_owner,
|
|
|
|
|
data,
|
2026-03-18 10:28:52 -04:00
|
|
|
nonce: account.nonce.0,
|
2026-03-04 18:42:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/// Resolve an account id-or-label pair to a `Privacy/id` string.
|
|
|
|
|
///
|
|
|
|
|
/// Exactly one of `id` or `label` must be `Some`. If `id` is provided it is
|
|
|
|
|
/// returned as-is; if `label` is provided it is resolved via
|
|
|
|
|
/// [`resolve_account_label`]. Any other combination returns an error.
|
|
|
|
|
pub fn resolve_id_or_label(
|
|
|
|
|
id: Option<String>,
|
|
|
|
|
label: Option<String>,
|
|
|
|
|
labels: &HashMap<String, Label>,
|
|
|
|
|
user_data: &NSSAUserData,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
match (id, label) {
|
|
|
|
|
(Some(id), None) => Ok(id),
|
|
|
|
|
(None, Some(label)) => resolve_account_label(&label, labels, user_data),
|
|
|
|
|
_ => anyhow::bail!("provide exactly one of account id or account label"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolve an account label to its full `Privacy/id` string representation.
|
|
|
|
|
///
|
|
|
|
|
/// Looks up the label in the labels map and determines whether the account is
|
|
|
|
|
/// public or private by checking the user data key trees.
|
|
|
|
|
pub fn resolve_account_label(
|
|
|
|
|
label: &str,
|
|
|
|
|
labels: &HashMap<String, Label>,
|
|
|
|
|
user_data: &NSSAUserData,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
let account_id_str = labels
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, l)| l.to_string() == label)
|
|
|
|
|
.map(|(k, _)| k.clone())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("No account found with label '{label}'"))?;
|
|
|
|
|
|
|
|
|
|
let account_id: nssa::AccountId = account_id_str.parse()?;
|
|
|
|
|
|
|
|
|
|
let privacy = if user_data
|
|
|
|
|
.public_key_tree
|
|
|
|
|
.account_id_map
|
|
|
|
|
.contains_key(&account_id)
|
|
|
|
|
|| user_data
|
|
|
|
|
.default_pub_account_signing_keys
|
|
|
|
|
.contains_key(&account_id)
|
|
|
|
|
{
|
|
|
|
|
"Public"
|
|
|
|
|
} else if user_data
|
|
|
|
|
.private_key_tree
|
|
|
|
|
.account_id_map
|
|
|
|
|
.contains_key(&account_id)
|
|
|
|
|
|| user_data
|
|
|
|
|
.default_user_private_accounts
|
|
|
|
|
.contains_key(&account_id)
|
|
|
|
|
{
|
|
|
|
|
"Private"
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!("Account with label '{label}' not found in wallet");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(format!("{privacy}/{account_id_str}"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 22:50:10 -03:00
|
|
|
/// Get home dir for wallet. Env var `NSSA_WALLET_HOME_DIR` must be set before execution to succeed.
|
2025-12-31 04:02:25 +03:00
|
|
|
fn get_home_nssa_var() -> Result<PathBuf> {
|
2025-08-07 14:07:34 +03:00
|
|
|
Ok(PathBuf::from_str(&std::env::var(HOME_DIR_ENV_VAR)?)?)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:33:58 +02:00
|
|
|
/// Get home dir for wallet. Env var `HOME` must be set before execution to succeed.
|
2025-12-31 04:02:25 +03:00
|
|
|
fn get_home_default_path() -> Result<PathBuf> {
|
2025-10-30 15:33:58 +02:00
|
|
|
std::env::home_dir()
|
|
|
|
|
.map(|path| path.join(".nssa").join("wallet"))
|
2026-03-09 18:27:56 +03:00
|
|
|
.context("Failed to get HOME")
|
2025-10-29 13:23:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:33:58 +02:00
|
|
|
/// Get home dir for wallet.
|
|
|
|
|
pub fn get_home() -> Result<PathBuf> {
|
2026-03-09 18:27:56 +03:00
|
|
|
get_home_nssa_var().or_else(|_| get_home_default_path())
|
2025-10-30 15:33:58 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Fetch config path from default home.
|
2025-12-31 04:02:25 +03:00
|
|
|
pub fn fetch_config_path() -> Result<PathBuf> {
|
|
|
|
|
let home = get_home()?;
|
|
|
|
|
let config_path = home.join("wallet_config.json");
|
|
|
|
|
Ok(config_path)
|
2025-12-08 18:26:35 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Fetch path to data storage from default home.
|
2025-08-08 15:22:04 +03:00
|
|
|
///
|
2025-11-26 07:32:35 +02:00
|
|
|
/// File must be created through setup beforehand.
|
2025-12-31 04:02:25 +03:00
|
|
|
pub fn fetch_persistent_storage_path() -> Result<PathBuf> {
|
2025-08-08 15:22:04 +03:00
|
|
|
let home = get_home()?;
|
2025-10-28 16:02:30 +02:00
|
|
|
let accs_path = home.join("storage.json");
|
2025-12-31 04:02:25 +03:00
|
|
|
Ok(accs_path)
|
2025-08-08 15:22:04 +03:00
|
|
|
}
|
2025-08-20 17:16:51 +03:00
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Produces data for storage.
|
2026-03-03 23:21:08 +03:00
|
|
|
#[must_use]
|
2025-10-28 16:02:30 +02:00
|
|
|
pub fn produce_data_for_storage(
|
|
|
|
|
user_data: &NSSAUserData,
|
|
|
|
|
last_synced_block: u64,
|
2026-01-07 13:13:14 +11:00
|
|
|
labels: HashMap<String, Label>,
|
2025-10-28 16:02:30 +02:00
|
|
|
) -> PersistentStorage {
|
2025-08-20 17:16:51 +03:00
|
|
|
let mut vec_for_storage = vec![];
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, key) in &user_data.public_key_tree.account_id_map {
|
2025-11-10 16:29:33 +02:00
|
|
|
if let Some(data) = user_data.public_key_tree.key_map.get(key) {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
PersistentAccountDataPublic {
|
2025-11-27 13:46:35 +02:00
|
|
|
account_id: *account_id,
|
2025-11-11 12:15:20 +02:00
|
|
|
chain_index: key.clone(),
|
|
|
|
|
data: data.clone(),
|
|
|
|
|
}
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
2025-11-10 16:29:33 +02:00
|
|
|
}
|
2025-09-11 18:32:46 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:09:32 -03:00
|
|
|
for (chain_index, node) in &user_data.private_key_tree.key_map {
|
2026-05-04 21:40:30 -03:00
|
|
|
let kinds = node.value.1.iter().map(|(kind, _)| kind.clone()).collect();
|
2026-04-27 21:09:33 -03:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
PersistentAccountDataPrivate {
|
2026-05-04 21:40:30 -03:00
|
|
|
kinds,
|
2026-04-27 21:09:33 -03:00
|
|
|
chain_index: chain_index.clone(),
|
|
|
|
|
data: node.clone(),
|
|
|
|
|
}
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
2026-04-17 00:09:32 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, key) in &user_data.default_pub_account_signing_keys {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
2026-03-13 18:23:39 +02:00
|
|
|
InitialAccountData::Public(PublicAccountPrivateInitialData {
|
2026-01-29 22:20:42 +03:00
|
|
|
account_id: *account_id,
|
2025-11-11 12:15:20 +02:00
|
|
|
pub_sign_key: key.clone(),
|
|
|
|
|
})
|
|
|
|
|
.into(),
|
2026-03-03 23:21:08 +03:00
|
|
|
);
|
2025-11-11 12:15:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 00:18:57 -03:00
|
|
|
for entry in user_data.default_user_private_accounts.values() {
|
2026-05-04 21:40:30 -03:00
|
|
|
for (kind, account) in &entry.accounts {
|
2026-04-16 03:03:13 -03:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
|
|
|
|
|
account: account.clone(),
|
2026-04-24 00:04:22 -03:00
|
|
|
key_chain: entry.key_chain.clone(),
|
2026-05-04 21:40:30 -03:00
|
|
|
identifier: kind.identifier(),
|
2026-04-16 03:03:13 -03:00
|
|
|
}))
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-11-11 12:15:20 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-28 16:02:30 +02:00
|
|
|
PersistentStorage {
|
|
|
|
|
accounts: vec_for_storage,
|
|
|
|
|
last_synced_block,
|
2026-01-07 13:13:14 +11:00
|
|
|
labels,
|
2026-05-07 17:35:51 +02:00
|
|
|
group_key_holders: user_data.group_key_holders.clone(),
|
|
|
|
|
shared_private_accounts: user_data.shared_private_accounts.clone(),
|
2026-05-08 08:19:55 +02:00
|
|
|
sealing_secret_key: user_data.sealing_secret_key,
|
2025-10-28 16:02:30 +02:00
|
|
|
}
|
2025-08-20 17:16:51 +03:00
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
|
2026-03-18 13:10:36 -04:00
|
|
|
#[expect(dead_code, reason = "Maybe used later")]
|
2025-10-03 01:30:40 -03:00
|
|
|
pub(crate) fn produce_random_nonces(size: usize) -> Vec<Nonce> {
|
|
|
|
|
let mut result = vec![[0; 16]; size];
|
2026-03-03 23:21:08 +03:00
|
|
|
for bytes in &mut result {
|
|
|
|
|
OsRng.fill_bytes(bytes);
|
|
|
|
|
}
|
2026-03-18 10:28:52 -04:00
|
|
|
result
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|x| Nonce(u128::from_le_bytes(x)))
|
|
|
|
|
.collect()
|
2025-10-03 01:30:40 -03:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:26:30 +03:00
|
|
|
pub(crate) fn parse_addr_with_privacy_prefix(
|
2025-11-24 17:09:30 +03:00
|
|
|
account_base58: &str,
|
|
|
|
|
) -> Result<(String, AccountPrivacyKind)> {
|
|
|
|
|
if account_base58.starts_with("Public/") {
|
2025-10-24 15:26:30 +03:00
|
|
|
Ok((
|
2026-03-04 18:42:33 +03:00
|
|
|
account_base58.strip_prefix("Public/").unwrap().to_owned(),
|
2025-11-24 17:09:30 +03:00
|
|
|
AccountPrivacyKind::Public,
|
2025-10-24 15:26:30 +03:00
|
|
|
))
|
2025-11-24 17:09:30 +03:00
|
|
|
} else if account_base58.starts_with("Private/") {
|
2025-10-24 15:26:30 +03:00
|
|
|
Ok((
|
2026-03-04 18:42:33 +03:00
|
|
|
account_base58.strip_prefix("Private/").unwrap().to_owned(),
|
2025-11-24 17:09:30 +03:00
|
|
|
AccountPrivacyKind::Private,
|
2025-10-24 15:26:30 +03:00
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!("Unsupported privacy kind, available variants is Public/ and Private/");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 09:16:46 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-03-04 18:42:33 +03:00
|
|
|
fn addr_parse_with_privacy() {
|
2025-10-24 15:26:30 +03:00
|
|
|
let addr_base58 = "Public/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap();
|
|
|
|
|
|
2025-11-24 17:09:30 +03:00
|
|
|
assert_eq!(addr_kind, AccountPrivacyKind::Public);
|
2025-10-24 15:26:30 +03:00
|
|
|
|
|
|
|
|
let addr_base58 = "Private/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap();
|
|
|
|
|
|
2025-11-24 17:09:30 +03:00
|
|
|
assert_eq!(addr_kind, AccountPrivacyKind::Private);
|
2025-10-24 15:26:30 +03:00
|
|
|
|
|
|
|
|
let addr_base58 = "asdsada/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
assert!(parse_addr_with_privacy_prefix(addr_base58).is_err());
|
|
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
}
|