feat: add --label option to wallet account new sub command

Following the work done in
https://github.com/logos-blockchain/lssa/pull/292 and the comment on
extending the work
https://github.com/logos-blockchain/lssa/pull/292#pullrequestreview-3672282664,
this commit introduces a new `--label` option to the `wallet account
new` sub command.

**Usage**:

```
wallet account new public --label "Public test account"
wallet account new private --label "Private test account"
```

Labels have to be unique across all accounts in the wallet storage.

The commit also adds tests, which make use of the `WalletSubCommand`
trait functions (hence the change to make it a `pub trait`).
This commit is contained in:
r4bbit 2026-01-19 13:04:43 +01:00
parent 27f31cf3d0
commit 6c2bdb1b20
No known key found for this signature in database
GPG Key ID: E95F1E9447DC91A9
10 changed files with 303 additions and 35 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ storage.json
result
wallet-ffi/wallet_ffi.h
bedrock_signing_key
integration_tests/configs/debug/

View File

@ -93,6 +93,9 @@ Generated new account with account_id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1
```
The relevant part is the account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9`
> [!NOTE]
> You can optionally assign a label to the account for easier identification using the `--label` option: `wallet account new public --label "my-account"`. Labels must be unique across all accounts.
## Check the account state
New accounts are always Uninitialized. Verify:
```bash
@ -269,6 +272,9 @@ Generated new account with account_id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8
```
The relevant part for this tutorial is the account id `7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr`
> [!NOTE]
> As with public accounts, you can use the `--label` option to assign a label: `wallet account new private --label "my-private-account"`.
You can check it's uninitialized with
```bash

View File

@ -3,6 +3,11 @@ use integration_tests::TestContext;
use log::info;
use nssa::program::Program;
use tokio::test;
use wallet::cli::{
Command,
account::{AccountSubcommand, NewSubcommand},
execute_subcommand,
};
#[test]
async fn get_existing_account() -> Result<()> {
@ -26,3 +31,100 @@ async fn get_existing_account() -> Result<()> {
Ok(())
}
#[test]
async fn new_public_account_with_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
let label = "my-test-public-account".to_string();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(label.clone()),
}));
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
// Extract the account_id from the result
let account_id = match result {
wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } => account_id,
_ => panic!("Expected RegisterAccount return value"),
};
// Verify the label was stored
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the new account");
assert_eq!(stored_label.to_string(), label);
info!("Successfully created public account with label");
Ok(())
}
#[test]
async fn new_private_account_with_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
let label = "my-test-private-account".to_string();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: Some(label.clone()),
}));
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
// Extract the account_id from the result
let account_id = match result {
wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } => account_id,
_ => panic!("Expected RegisterAccount return value"),
};
// Verify the label was stored
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the new account");
assert_eq!(stored_label.to_string(), label);
info!("Successfully created private account with label");
Ok(())
}
#[test]
async fn new_public_account_without_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
}));
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
// Extract the account_id from the result
let account_id = match result {
wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } => account_id,
_ => panic!("Expected RegisterAccount return value"),
};
// Verify no label was stored
assert!(
!ctx.wallet()
.storage()
.labels
.contains_key(&account_id.to_string()),
"No label should be stored when not provided"
);
info!("Successfully created public account without label");
Ok(())
}

View File

@ -19,7 +19,10 @@ async fn amm_public() -> Result<()> {
account_id: definition_account_id_1,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -31,7 +34,10 @@ async fn amm_public() -> Result<()> {
account_id: supply_account_id_1,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -43,7 +49,10 @@ async fn amm_public() -> Result<()> {
account_id: recipient_account_id_1,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -55,7 +64,10 @@ async fn amm_public() -> Result<()> {
account_id: definition_account_id_2,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -67,7 +79,10 @@ async fn amm_public() -> Result<()> {
account_id: supply_account_id_2,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -79,7 +94,10 @@ async fn amm_public() -> Result<()> {
account_id: recipient_account_id_2,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
@ -144,7 +162,10 @@ async fn amm_public() -> Result<()> {
account_id: user_holding_lp,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {

View File

@ -149,7 +149,10 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
let from: AccountId = ctx.existing_private_accounts()[0];
// Create a new private account
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None }));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
}));
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {
@ -306,7 +309,10 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
let from: AccountId = ctx.existing_private_accounts()[0];
// Create a new private account
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None }));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
}));
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {
@ -366,7 +372,10 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
async fn initialize_private_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None }));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
anyhow::bail!("Expected RegisterAccount return value");

View File

@ -51,7 +51,10 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
pub async fn successful_transfer_to_new_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
}));
wallet::cli::execute_subcommand(ctx.wallet_mut(), command)
.await
@ -212,7 +215,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
async fn initialize_public_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
anyhow::bail!("Expected RegisterAccount return value");

View File

@ -24,6 +24,7 @@ async fn restore_keys_from_seed() -> Result<()> {
// Create first private account at root
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::root()),
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {
@ -36,6 +37,7 @@ async fn restore_keys_from_seed() -> Result<()> {
// Create second private account at /0
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::from_str("/0")?),
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {
@ -70,6 +72,7 @@ async fn restore_keys_from_seed() -> Result<()> {
// Create first public account at root
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::root()),
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {
@ -82,6 +85,7 @@ async fn restore_keys_from_seed() -> Result<()> {
// Create second public account at /0
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::from_str("/0")?),
label: None,
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount {

View File

@ -112,7 +112,10 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
// Create new private account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {

View File

@ -23,7 +23,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Create new account for the token definition
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -36,7 +39,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Create new account for the token supply holder
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -49,7 +55,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Create new account for receiving a token transaction
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -262,7 +271,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Create new account for the token definition (public)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -275,7 +287,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Create new account for the token supply holder (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -288,7 +303,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Create new account for receiving a token transaction (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -429,6 +447,7 @@ async fn create_token_with_private_definition() -> Result<()> {
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::root()),
label: None,
})),
)
.await?;
@ -444,6 +463,7 @@ async fn create_token_with_private_definition() -> Result<()> {
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::root()),
label: None,
})),
)
.await?;
@ -496,7 +516,10 @@ async fn create_token_with_private_definition() -> Result<()> {
// Create private recipient account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -509,7 +532,10 @@ async fn create_token_with_private_definition() -> Result<()> {
// Create public recipient account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -615,7 +641,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
// Create token definition account (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -628,7 +657,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
// Create supply account (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -685,7 +717,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
// Create recipient account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -762,7 +797,10 @@ async fn shielded_token_transfer() -> Result<()> {
// Create token definition account (public)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -775,7 +813,10 @@ async fn shielded_token_transfer() -> Result<()> {
// Create supply account (public)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -788,7 +829,10 @@ async fn shielded_token_transfer() -> Result<()> {
// Create recipient account (private) for shielded transfer
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -876,7 +920,10 @@ async fn deshielded_token_transfer() -> Result<()> {
// Create token definition account (public)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -889,7 +936,10 @@ async fn deshielded_token_transfer() -> Result<()> {
// Create supply account (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -902,7 +952,10 @@ async fn deshielded_token_transfer() -> Result<()> {
// Create recipient account (public) for deshielded transfer
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -990,7 +1043,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
// Create token definition account (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -1003,7 +1059,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
// Create supply account (private)
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
@ -1031,7 +1090,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
// Create new private account for claiming path
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {

View File

@ -58,12 +58,18 @@ pub enum NewSubcommand {
#[arg(long)]
/// Chain index of a parent node
cci: Option<ChainIndex>,
#[arg(short, long)]
/// Label to assign to the new account
label: Option<String>,
},
/// Register new private account
Private {
#[arg(long)]
/// Chain index of a parent node
cci: Option<ChainIndex>,
#[arg(short, long)]
/// Label to assign to the new account
label: Option<String>,
},
}
@ -73,7 +79,17 @@ impl WalletSubcommand for NewSubcommand {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
NewSubcommand::Public { cci } => {
NewSubcommand::Public { cci, label } => {
if let Some(ref label) = label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
let private_key = wallet_core
@ -84,6 +100,13 @@ impl WalletSubcommand for NewSubcommand {
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));
}
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
@ -93,9 +116,26 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
NewSubcommand::Private { cci } => {
NewSubcommand::Private { cci, label } => {
if let Some(ref label) = label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
let (key, _) = wallet_core
.storage
.user_data
@ -379,6 +419,20 @@ impl WalletSubcommand for AccountSubcommand {
AccountSubcommand::Label { account_id, label } => {
let (account_id_str, _) = parse_addr_with_privacy_prefix(&account_id)?;
// 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