mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-02-19 04:43:36 +00:00
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>
267 lines
8.4 KiB
Rust
267 lines
8.4 KiB
Rust
use std::{io::Write, path::PathBuf, str::FromStr};
|
|
|
|
use anyhow::{Context, Result};
|
|
use bip39::Mnemonic;
|
|
use clap::{Parser, Subcommand};
|
|
use common::HashType;
|
|
use nssa::{ProgramDeploymentTransaction, program::Program};
|
|
|
|
use crate::{
|
|
WalletCore,
|
|
cli::{
|
|
account::AccountSubcommand,
|
|
chain::ChainSubcommand,
|
|
config::ConfigSubcommand,
|
|
programs::{
|
|
amm::AmmProgramAgnosticSubcommand, native_token_transfer::AuthTransferSubcommand,
|
|
pinata::PinataProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand,
|
|
},
|
|
},
|
|
};
|
|
|
|
pub mod account;
|
|
pub mod chain;
|
|
pub mod config;
|
|
pub mod programs;
|
|
|
|
pub(crate) trait WalletSubcommand {
|
|
async fn handle_subcommand(self, wallet_core: &mut WalletCore)
|
|
-> Result<SubcommandReturnValue>;
|
|
}
|
|
|
|
/// Represents CLI command for a wallet
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
#[clap(about)]
|
|
pub enum Command {
|
|
/// Authenticated transfer subcommand
|
|
#[command(subcommand)]
|
|
AuthTransfer(AuthTransferSubcommand),
|
|
/// Generic chain info subcommand
|
|
#[command(subcommand)]
|
|
ChainInfo(ChainSubcommand),
|
|
/// Account view and sync subcommand
|
|
#[command(subcommand)]
|
|
Account(AccountSubcommand),
|
|
/// Pinata program interaction subcommand
|
|
#[command(subcommand)]
|
|
Pinata(PinataProgramAgnosticSubcommand),
|
|
/// Token program interaction subcommand
|
|
#[command(subcommand)]
|
|
Token(TokenProgramAgnosticSubcommand),
|
|
/// AMM program interaction subcommand
|
|
#[command(subcommand)]
|
|
AMM(AmmProgramAgnosticSubcommand),
|
|
/// Check the wallet can connect to the node and builtin local programs
|
|
/// match the remote versions
|
|
CheckHealth {},
|
|
/// Command to setup config, get and set config fields
|
|
#[command(subcommand)]
|
|
Config(ConfigSubcommand),
|
|
/// Restoring keys from given password at given `depth`
|
|
///
|
|
/// !!!WARNING!!! will rewrite current storage
|
|
RestoreKeys {
|
|
#[arg(short, long)]
|
|
/// Indicates, how deep in tree accounts may be. Affects command complexity.
|
|
depth: u32,
|
|
},
|
|
/// Deploy a program
|
|
DeployProgram { binary_filepath: PathBuf },
|
|
}
|
|
|
|
/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config
|
|
///
|
|
/// All account addresses must be valid 32 byte base58 strings.
|
|
///
|
|
/// All account account_ids must be provided as {privacy_prefix}/{account_id},
|
|
/// where valid options for `privacy_prefix` is `Public` and `Private`
|
|
#[derive(Parser, Debug)]
|
|
#[clap(version, about)]
|
|
pub struct Args {
|
|
/// Continious run flag
|
|
#[arg(short, long)]
|
|
pub continuous_run: bool,
|
|
/// Basic authentication in the format `user` or `user:password`
|
|
#[arg(long)]
|
|
pub auth: Option<String>,
|
|
/// Wallet command
|
|
#[command(subcommand)]
|
|
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),
|
|
}
|
|
|
|
pub async fn execute_subcommand(
|
|
wallet_core: &mut WalletCore,
|
|
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?
|
|
}
|
|
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");
|
|
};
|
|
if authenticated_transfer_id != &Program::authenticated_transfer_program().id() {
|
|
panic!("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");
|
|
};
|
|
if token_id != &Program::token().id() {
|
|
panic!("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");
|
|
};
|
|
if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID {
|
|
panic!("Local ID for privacy preserving circuit is different from remote");
|
|
}
|
|
|
|
println!("✅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::Config(config_subcommand) => {
|
|
config_subcommand.handle_subcommand(wallet_core).await?
|
|
}
|
|
Command::RestoreKeys { depth } => {
|
|
let mnemonic = read_mnemonic_from_stdin()?;
|
|
let password = read_password_from_stdin()?;
|
|
wallet_core.restore_storage(&mnemonic, &password)?;
|
|
execute_keys_restoration(wallet_core, depth).await?;
|
|
|
|
SubcommandReturnValue::Empty
|
|
}
|
|
Command::DeployProgram { binary_filepath } => {
|
|
let bytecode: Vec<u8> = std::fs::read(&binary_filepath).context(format!(
|
|
"Failed to read program binary at {}",
|
|
binary_filepath.display()
|
|
))?;
|
|
let message = nssa::program_deployment_transaction::Message::new(bytecode);
|
|
let transaction = ProgramDeploymentTransaction::new(message);
|
|
let _response = wallet_core
|
|
.sequencer_client
|
|
.send_tx_program(transaction)
|
|
.await
|
|
.context("Transaction submission error");
|
|
|
|
SubcommandReturnValue::Empty
|
|
}
|
|
};
|
|
|
|
Ok(subcommand_ret)
|
|
}
|
|
|
|
pub async fn execute_continuous_run(wallet_core: &mut WalletCore) -> Result<()> {
|
|
loop {
|
|
let latest_block_num = wallet_core
|
|
.sequencer_client
|
|
.get_last_block()
|
|
.await?
|
|
.last_block;
|
|
wallet_core.sync_to_block(latest_block_num).await?;
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(
|
|
wallet_core.config().seq_poll_timeout_millis,
|
|
))
|
|
.await;
|
|
}
|
|
}
|
|
|
|
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)?;
|
|
|
|
Ok(password.trim().to_string())
|
|
}
|
|
|
|
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
|
|
.user_data
|
|
.public_key_tree
|
|
.generate_tree_for_depth(depth);
|
|
|
|
println!("Public tree generated");
|
|
|
|
wallet_core
|
|
.storage
|
|
.user_data
|
|
.private_key_tree
|
|
.generate_tree_for_depth(depth);
|
|
|
|
println!("Private tree generated");
|
|
|
|
wallet_core
|
|
.storage
|
|
.user_data
|
|
.public_key_tree
|
|
.cleanup_tree_remove_uninit_layered(depth, wallet_core.sequencer_client.clone())
|
|
.await?;
|
|
|
|
println!("Public tree cleaned up");
|
|
|
|
let last_block = wallet_core
|
|
.sequencer_client
|
|
.get_last_block()
|
|
.await?
|
|
.last_block;
|
|
|
|
println!("Last block is {last_block}");
|
|
|
|
wallet_core.sync_to_block(last_block).await?;
|
|
|
|
println!("Private tree clean up start");
|
|
|
|
wallet_core
|
|
.storage
|
|
.user_data
|
|
.private_key_tree
|
|
.cleanup_tree_remove_uninit_layered(depth);
|
|
|
|
println!("Private tree cleaned up");
|
|
|
|
wallet_core.store_persistent_data().await?;
|
|
|
|
Ok(())
|
|
}
|