lssa/wallet/src/cli/mod.rs

385 lines
13 KiB
Rust
Raw Normal View History

2026-05-14 21:19:25 -04:00
use std::{io::Write as _, path::PathBuf, str::FromStr};
2025-12-09 15:18:48 -03:00
2026-03-04 18:42:33 +03:00
use anyhow::{Context as _, Result};
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
use bip39::Mnemonic;
use clap::{Parser, Subcommand};
use common::{HashType, transaction::NSSATransaction};
2026-05-14 21:19:25 -04:00
use derive_more::Display;
2026-03-13 22:56:14 +03:00
use futures::TryFutureExt as _;
2025-12-05 17:54:51 -03:00
use nssa::{ProgramDeploymentTransaction, program::Program};
use sequencer_service_rpc::RpcClient as _;
2025-10-13 17:25:36 +03:00
2026-05-15 09:07:35 -04:00
pub use crate::helperfunctions::{read_mnemonic, read_pin};
use crate::{
WalletCore,
2026-05-14 21:19:25 -04:00
account::{AccountIdWithPrivacy, Label},
cli::{
account::AccountSubcommand,
chain::ChainSubcommand,
config::ConfigSubcommand,
2026-05-14 21:19:25 -04:00
group::GroupSubcommand,
keycard::KeycardSubcommand,
programs::{
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
token::TokenProgramAgnosticSubcommand,
},
},
2026-05-14 21:19:25 -04:00
storage::Storage,
};
2025-10-13 17:25:36 +03:00
2025-10-20 10:01:54 +03:00
pub mod account;
2025-10-14 15:29:18 +03:00
pub mod chain;
2025-11-03 15:45:50 +02:00
pub mod config;
2026-05-14 21:19:25 -04:00
pub mod group;
pub mod keycard;
pub mod programs;
2025-10-13 17:25:36 +03:00
pub(crate) trait WalletSubcommand {
async fn handle_subcommand(self, wallet_core: &mut WalletCore)
-> Result<SubcommandReturnValue>;
}
2026-03-10 00:17:43 +03:00
/// Represents CLI command for a wallet.
#[derive(Subcommand, Debug, Clone)]
#[clap(about)]
pub enum Command {
2026-03-10 00:17:43 +03:00
/// Authenticated transfer subcommand.
#[command(subcommand)]
AuthTransfer(AuthTransferSubcommand),
2026-03-10 00:17:43 +03:00
/// Generic chain info subcommand.
#[command(subcommand)]
ChainInfo(ChainSubcommand),
2026-03-10 00:17:43 +03:00
/// Account view and sync subcommand.
#[command(subcommand)]
Account(AccountSubcommand),
2026-03-10 00:17:43 +03:00
/// Pinata program interaction subcommand.
#[command(subcommand)]
Pinata(PinataProgramAgnosticSubcommand),
2026-03-10 00:17:43 +03:00
/// Token program interaction subcommand.
#[command(subcommand)]
Token(TokenProgramAgnosticSubcommand),
2026-03-10 00:17:43 +03:00
/// AMM program interaction subcommand.
2025-12-16 14:05:34 +02:00
#[command(subcommand)]
AMM(AmmProgramAgnosticSubcommand),
/// Associated Token Account program interaction subcommand.
#[command(subcommand)]
Ata(AtaSubcommand),
2026-05-14 21:19:25 -04:00
/// Group key management (create, invite, join, derive keys).
#[command(subcommand)]
Group(GroupSubcommand),
/// Check the wallet can connect to the node and builtin local programs
2026-03-10 00:17:43 +03:00
/// match the remote versions.
2026-03-04 18:42:33 +03:00
CheckHealth,
2026-03-10 00:17:43 +03:00
/// Command to setup config, get and set config fields.
#[command(subcommand)]
Config(ConfigSubcommand),
2026-03-10 00:17:43 +03:00
/// Restoring keys from given password at given `depth`.
2025-12-03 13:10:07 +02:00
///
2026-03-10 00:17:43 +03:00
/// !!!WARNING!!! will rewrite current storage.
RestoreKeys {
#[arg(short, long)]
2025-12-03 13:10:07 +02:00
/// Indicates, how deep in tree accounts may be. Affects command complexity.
depth: u32,
},
2026-03-10 00:17:43 +03:00
/// Deploy a program.
DeployProgram { binary_filepath: PathBuf },
2026-05-14 21:19:25 -04:00
/// Keycard hardware wallet management.
#[command(subcommand)]
Keycard(KeycardSubcommand),
}
2026-03-10 00:17:43 +03:00
/// To execute commands, env var `NSSA_WALLET_HOME_DIR` must be set into directory with config.
///
2025-12-08 18:26:35 +03:00
/// All account addresses must be valid 32 byte base58 strings.
///
2026-03-03 23:21:08 +03:00
/// All account `account_ids` must be provided as {`privacy_prefix}/{account_id`},
2026-03-10 00:17:43 +03:00
/// where valid options for `privacy_prefix` is `Public` and `Private`.
#[derive(Parser, Debug)]
#[clap(version, about)]
pub struct Args {
2026-03-10 00:17:43 +03:00
/// Continious run flag.
#[arg(short, long)]
pub continuous_run: bool,
2026-03-10 00:17:43 +03:00
/// Basic authentication in the format `user` or `user:password`.
2025-12-08 18:26:35 +03:00
#[arg(long)]
pub auth: Option<String>,
2026-03-10 00:17:43 +03:00
/// Wallet command.
#[command(subcommand)]
2025-12-03 13:10:07 +02:00
pub command: Option<Command>,
}
#[derive(Debug, Clone)]
pub enum SubcommandReturnValue {
PrivacyPreservingTransfer { tx_hash: HashType },
RegisterAccount { account_id: nssa::AccountId },
Account(nssa::Account),
Empty,
SyncedToBlock(u64),
}
2026-05-14 21:19:25 -04:00
#[derive(Debug, Display, Clone, PartialEq, Eq, Hash)]
pub enum CliAccountMention {
#[display("{_0}")]
Id(AccountIdWithPrivacy),
#[display("{_0}")]
Label(Label),
#[display("{_0}")]
KeyPath(String),
}
impl CliAccountMention {
pub fn resolve(&self, storage: &Storage) -> Result<AccountIdWithPrivacy> {
match self {
Self::Id(account_id) => Ok(*account_id),
Self::Label(label) => storage
.resolve_label(label)
.ok_or_else(|| anyhow::anyhow!("No account found for label `{label}`")),
Self::KeyPath(path) => {
let pin = read_pin()?;
2026-05-15 09:07:35 -04:00
let id_str =
keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path)
.map_err(anyhow::Error::from)?;
2026-05-14 21:19:25 -04:00
AccountIdWithPrivacy::from_str(&id_str)
.map_err(|e| anyhow::anyhow!("Invalid account id from keycard: {e}"))
}
}
}
#[must_use]
pub const fn is_keycard(&self) -> bool {
matches!(self, Self::KeyPath(_))
}
#[must_use]
pub fn key_path(&self) -> Option<&str> {
match self {
Self::KeyPath(path) => Some(path),
Self::Id(_) | Self::Label(_) => None,
}
}
/// Resolve to an [`AccountSigner`] for a sender — must sign, never `Foreign`.
pub fn to_signer(&self, wallet_core: &WalletCore) -> Result<crate::signing::AccountSigner> {
if let Self::KeyPath(path) = self {
return Ok(crate::signing::AccountSigner::Keycard(path.clone()));
}
let account = self.resolve(wallet_core.storage())?;
match account {
AccountIdWithPrivacy::Public(id) => Ok(crate::signing::AccountSigner::Local(id)),
AccountIdWithPrivacy::Private(_) => {
anyhow::bail!("Private accounts not supported as senders here")
}
}
}
/// Resolve to an [`AccountSigner`] for a recipient — returns `Foreign` when the account
/// has no local key and no keycard path, meaning no signature or nonce is required.
2026-05-15 09:07:35 -04:00
pub fn to_recipient_signer(
&self,
wallet_core: &WalletCore,
) -> Result<crate::signing::AccountSigner> {
2026-05-14 21:19:25 -04:00
if let Self::KeyPath(path) = self {
return Ok(crate::signing::AccountSigner::Keycard(path.clone()));
}
let account = self.resolve(wallet_core.storage())?;
match account {
2026-05-15 09:07:35 -04:00
AccountIdWithPrivacy::Public(id) => Ok(
match wallet_core
.storage()
.key_chain()
.pub_account_signing_key(id)
{
2026-05-14 21:19:25 -04:00
Some(_) => crate::signing::AccountSigner::Local(id),
None => crate::signing::AccountSigner::Foreign,
2026-05-15 09:07:35 -04:00
},
),
2026-05-14 21:19:25 -04:00
AccountIdWithPrivacy::Private(_) => {
anyhow::bail!("Private accounts not supported as recipients here")
}
}
}
}
impl FromStr for CliAccountMention {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s.starts_with("m/") {
return Ok(Self::KeyPath(s.to_owned()));
}
AccountIdWithPrivacy::from_str(s).map_or_else(
|_| Ok(Self::Label(Label::new(s.to_owned()))),
|account_id| Ok(Self::Id(account_id)),
)
}
}
impl From<Label> for CliAccountMention {
fn from(label: Label) -> Self {
Self::Label(label)
}
}
impl Default for CliAccountMention {
fn default() -> Self {
Self::Label(Label::new(String::new()))
}
}
pub async fn execute_subcommand(
wallet_core: &mut WalletCore,
2025-12-08 18:26:35 +03:00
command: Command,
) -> Result<SubcommandReturnValue> {
let subcommand_ret = match command {
Command::AuthTransfer(transfer_subcommand) => {
transfer_subcommand.handle_subcommand(wallet_core).await?
}
Command::ChainInfo(chain_subcommand) => {
chain_subcommand.handle_subcommand(wallet_core).await?
}
Command::Account(account_subcommand) => {
account_subcommand.handle_subcommand(wallet_core).await?
}
Command::Pinata(pinata_subcommand) => {
pinata_subcommand.handle_subcommand(wallet_core).await?
}
2026-03-04 18:42:33 +03:00
Command::CheckHealth => {
let remote_program_ids = wallet_core
.sequencer_client
.get_program_ids()
.await
.expect("Error fetching program ids");
let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer")
else {
panic!("Missing authenticated transfer ID from remote");
};
2026-03-03 23:21:08 +03:00
assert!(
authenticated_transfer_id == &Program::authenticated_transfer_program().id(),
"Local ID for authenticated transfer program is different from remote"
);
let Some(token_id) = remote_program_ids.get("token") else {
panic!("Missing token program ID from remote");
};
2026-03-03 23:21:08 +03:00
assert!(
token_id == &Program::token().id(),
"Local ID for token program is different from remote"
);
let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else {
panic!("Missing privacy preserving circuit ID from remote");
};
2026-03-03 23:21:08 +03:00
assert!(
circuit_id == &nssa::PRIVACY_PRESERVING_CIRCUIT_ID,
"Local ID for privacy preserving circuit is different from remote"
);
let Some(amm_id) = remote_program_ids.get("amm") else {
panic!("Missing AMM program ID from remote");
};
2026-03-03 23:21:08 +03:00
assert!(
amm_id == &Program::amm().id(),
"Local ID for AMM program is different from remote"
);
2026-03-04 18:42:33 +03:00
println!("\u{2705}All looks good!");
SubcommandReturnValue::Empty
}
Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?,
Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?,
Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?,
2026-05-14 21:19:25 -04:00
Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?,
Command::Keycard(keycard_subcommand) => {
keycard_subcommand.handle_subcommand(wallet_core).await?
}
Command::Config(config_subcommand) => {
config_subcommand.handle_subcommand(wallet_core).await?
}
2025-12-03 13:10:07 +02:00
Command::RestoreKeys { depth } => {
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
let mnemonic = read_mnemonic_from_stdin()?;
2025-12-03 13:10:07 +02:00
let password = read_password_from_stdin()?;
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
wallet_core.restore_storage(&mnemonic, &password)?;
execute_keys_restoration(wallet_core, depth).await?;
2025-12-03 13:10:07 +02:00
SubcommandReturnValue::Empty
}
2025-12-05 17:54:51 -03:00
Command::DeployProgram { binary_filepath } => {
let bytecode: Vec<u8> = std::fs::read(&binary_filepath).context(format!(
"Failed to read program binary at {}",
binary_filepath.display()
))?;
2025-12-05 17:54:51 -03:00
let message = nssa::program_deployment_transaction::Message::new(bytecode);
let transaction = ProgramDeploymentTransaction::new(message);
let _response = wallet_core
2025-12-05 17:54:51 -03:00
.sequencer_client
.send_transaction(NSSATransaction::ProgramDeployment(transaction))
2025-12-05 17:54:51 -03:00
.await
2026-02-24 19:41:01 +03:00
.context("Transaction submission error")?;
2025-12-05 17:54:51 -03:00
SubcommandReturnValue::Empty
}
};
Ok(subcommand_ret)
}
pub async fn execute_continuous_run(wallet_core: &mut WalletCore) -> Result<()> {
loop {
2026-05-14 21:19:25 -04:00
wallet_core.sync_to_latest_block().await?;
tokio::time::sleep(wallet_core.config().seq_poll_timeout).await;
}
}
2025-12-03 13:10:07 +02:00
pub fn read_password_from_stdin() -> Result<String> {
let mut password = String::new();
print!("Input password: ");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut password)?;
2026-03-04 18:42:33 +03:00
Ok(password.trim().to_owned())
2025-12-03 13:10:07 +02:00
}
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
let mut phrase = String::new();
print!("Input recovery phrase: ");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut phrase)?;
Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase")
}
pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> {
wallet_core
.storage
2026-05-14 21:19:25 -04:00
.key_chain_mut()
.generate_trees_for_depth(depth);
2026-05-14 21:19:25 -04:00
println!(
"Public tree generated\n\
Private tree generated"
);
2026-05-14 21:19:25 -04:00
wallet_core.sync_to_latest_block().await?;
wallet_core
.storage
2026-05-14 21:19:25 -04:00
.key_chain_mut()
.cleanup_trees_remove_uninit_layered(depth, |account_id| {
wallet_core
.sequencer_client
.get_account(account_id)
.map_err(Into::into)
})
.await?;
2026-05-14 21:19:25 -04:00
println!(
"Public tree cleaned up\n\
Private tree cleaned up"
);
2026-05-14 21:19:25 -04:00
wallet_core.store_persistent_data()?;
Ok(())
}