2026-04-10 20:23:25 +03: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};
|
2026-01-20 16:47:34 +01:00
|
|
|
use bip39::Mnemonic;
|
2025-11-27 22:07:53 +03:00
|
|
|
use clap::{Parser, Subcommand};
|
2026-06-01 17:10:46 -03:00
|
|
|
use common::{HashType, transaction::LeeTransaction};
|
2026-04-10 20:23:25 +03:00
|
|
|
use derive_more::Display;
|
2026-03-13 22:56:14 +03:00
|
|
|
use futures::TryFutureExt as _;
|
2026-06-01 17:10:46 -03:00
|
|
|
use lee::{ProgramDeploymentTransaction, program::Program};
|
2026-03-13 22:38:23 +03:00
|
|
|
use sequencer_service_rpc::RpcClient as _;
|
2025-10-13 17:25:36 +03:00
|
|
|
|
2026-05-21 20:46:13 -04:00
|
|
|
pub use crate::helperfunctions::{read_mnemonic, read_pin};
|
2025-11-27 22:07:53 +03:00
|
|
|
use crate::{
|
|
|
|
|
WalletCore,
|
2026-04-10 20:23:25 +03:00
|
|
|
account::{AccountIdWithPrivacy, Label},
|
2025-11-27 22:07:53 +03:00
|
|
|
cli::{
|
|
|
|
|
account::AccountSubcommand,
|
|
|
|
|
chain::ChainSubcommand,
|
|
|
|
|
config::ConfigSubcommand,
|
2026-05-05 18:55:51 +02:00
|
|
|
group::GroupSubcommand,
|
2026-05-21 20:46:13 -04:00
|
|
|
keycard::KeycardSubcommand,
|
2025-11-27 22:07:53 +03:00
|
|
|
programs::{
|
2026-03-07 16:51:50 +01:00
|
|
|
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
|
|
|
|
|
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
2026-06-05 21:34:00 +03:00
|
|
|
token::TokenProgramAgnosticSubcommand, vault::VaultSubcommand,
|
2025-11-27 22:07:53 +03:00
|
|
|
},
|
|
|
|
|
},
|
2026-04-10 20:23:25 +03:00
|
|
|
storage::Storage,
|
2025-11-27 22:07:53 +03:00
|
|
|
};
|
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-05 18:55:51 +02:00
|
|
|
pub mod group;
|
2026-05-21 20:46:13 -04:00
|
|
|
pub mod keycard;
|
2025-11-27 22:07:53 +03:00
|
|
|
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>;
|
|
|
|
|
}
|
2025-11-27 22:07:53 +03:00
|
|
|
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Represents CLI command for a wallet.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
|
|
|
#[clap(about)]
|
|
|
|
|
pub enum Command {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Authenticated transfer subcommand.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[command(subcommand)]
|
|
|
|
|
AuthTransfer(AuthTransferSubcommand),
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Generic chain info subcommand.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[command(subcommand)]
|
|
|
|
|
ChainInfo(ChainSubcommand),
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Account view and sync subcommand.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[command(subcommand)]
|
|
|
|
|
Account(AccountSubcommand),
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Pinata program interaction subcommand.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[command(subcommand)]
|
|
|
|
|
Pinata(PinataProgramAgnosticSubcommand),
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Token program interaction subcommand.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[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),
|
2026-03-07 16:51:50 +01:00
|
|
|
/// Associated Token Account program interaction subcommand.
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
Ata(AtaSubcommand),
|
2026-06-05 21:34:00 +03:00
|
|
|
/// Vault program interaction subcommand.
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
Vault(VaultSubcommand),
|
2026-05-05 18:55:51 +02:00
|
|
|
/// Group key management (create, invite, join, derive keys).
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
Group(GroupSubcommand),
|
2025-11-27 22:07:53 +03:00
|
|
|
/// 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.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[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.
|
2025-12-03 07:13:35 +02:00
|
|
|
RestoreKeys {
|
2025-11-27 22:07:53 +03:00
|
|
|
#[arg(short, long)]
|
2025-12-03 13:10:07 +02:00
|
|
|
/// Indicates, how deep in tree accounts may be. Affects command complexity.
|
2025-12-03 07:13:35 +02:00
|
|
|
depth: u32,
|
2025-11-27 22:07:53 +03:00
|
|
|
},
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Deploy a program.
|
2025-12-10 09:58:45 -03:00
|
|
|
DeployProgram { binary_filepath: PathBuf },
|
2026-05-21 20:46:13 -04:00
|
|
|
/// Keycard hardware wallet management.
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
Keycard(KeycardSubcommand),
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-01 17:10:46 -03:00
|
|
|
/// To execute commands, env var `LEE_WALLET_HOME_DIR` must be set into directory with config.
|
2025-11-27 22:07:53 +03:00
|
|
|
///
|
2025-12-08 18:26:35 +03:00
|
|
|
/// All account addresses must be valid 32 byte base58 strings.
|
2025-11-27 22:07:53 +03:00
|
|
|
///
|
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`.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[clap(version, about)]
|
|
|
|
|
pub struct Args {
|
2026-03-10 00:17:43 +03:00
|
|
|
/// Continious run flag.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[arg(short, long)]
|
2025-11-30 03:16:47 +03:00
|
|
|
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.
|
2025-11-27 22:07:53 +03:00
|
|
|
#[command(subcommand)]
|
2025-12-03 13:10:07 +02:00
|
|
|
pub command: Option<Command>,
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum SubcommandReturnValue {
|
2026-01-29 22:20:42 +03:00
|
|
|
PrivacyPreservingTransfer { tx_hash: HashType },
|
2026-06-01 17:10:46 -03:00
|
|
|
RegisterAccount { account_id: lee::AccountId },
|
|
|
|
|
Account(lee::Account),
|
2025-11-27 22:07:53 +03:00
|
|
|
Empty,
|
|
|
|
|
SyncedToBlock(u64),
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
#[derive(Debug, Display, Clone, PartialEq, Eq, Hash)]
|
|
|
|
|
pub enum CliAccountMention {
|
2026-05-21 20:46:13 -04:00
|
|
|
#[display("{_0}")]
|
2026-04-10 20:23:25 +03:00
|
|
|
Id(AccountIdWithPrivacy),
|
2026-05-21 20:46:13 -04:00
|
|
|
#[display("{_0}")]
|
2026-04-10 20:23:25 +03:00
|
|
|
Label(Label),
|
2026-05-21 20:46:13 -04:00
|
|
|
#[display("{_0}")]
|
|
|
|
|
KeyPath(String),
|
2026-04-10 20:23:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`")),
|
2026-05-21 20:46:13 -04:00
|
|
|
Self::KeyPath(path) => {
|
|
|
|
|
let pin = read_pin()?;
|
|
|
|
|
let id_str =
|
2026-06-05 17:35:10 -04:00
|
|
|
keycard_wallet::KeycardWallet::get_public_account_id_for_path_with_connect(
|
|
|
|
|
&pin, path,
|
|
|
|
|
)
|
|
|
|
|
.map_err(anyhow::Error::from)?;
|
2026-05-21 20:46:13 -04:00
|
|
|
AccountIdWithPrivacy::from_str(&id_str)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Invalid account id from keycard: {e}"))
|
|
|
|
|
}
|
2026-04-10 20:23:25 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-05 17:35:10 -04:00
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn key_path(&self) -> Option<&str> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::KeyPath(path) => Some(path),
|
|
|
|
|
Self::Id(_) | Self::Label(_) => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn into_public_identity(self, account_id: lee::AccountId) -> crate::AccountIdentity {
|
|
|
|
|
match self {
|
|
|
|
|
Self::KeyPath(key_path) => crate::AccountIdentity::PublicKeycard {
|
|
|
|
|
account_id,
|
|
|
|
|
key_path,
|
|
|
|
|
},
|
|
|
|
|
Self::Id(_) | Self::Label(_) => crate::AccountIdentity::Public(account_id),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 20:23:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for CliAccountMention {
|
|
|
|
|
type Err = std::convert::Infallible;
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
2026-05-21 20:46:13 -04:00
|
|
|
if s.starts_with("m/") {
|
|
|
|
|
return Ok(Self::KeyPath(s.to_owned()));
|
|
|
|
|
}
|
2026-04-10 20:23:25 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 20:46:13 -04:00
|
|
|
impl Default for CliAccountMention {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Label(Label::new(String::new()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 04:02:25 +03:00
|
|
|
pub async fn execute_subcommand(
|
|
|
|
|
wallet_core: &mut WalletCore,
|
2025-12-08 18:26:35 +03:00
|
|
|
command: Command,
|
|
|
|
|
) -> Result<SubcommandReturnValue> {
|
2025-11-27 22:07:53 +03:00
|
|
|
let subcommand_ret = match command {
|
|
|
|
|
Command::AuthTransfer(transfer_subcommand) => {
|
2025-12-31 04:02:25 +03:00
|
|
|
transfer_subcommand.handle_subcommand(wallet_core).await?
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
Command::ChainInfo(chain_subcommand) => {
|
2025-12-31 04:02:25 +03:00
|
|
|
chain_subcommand.handle_subcommand(wallet_core).await?
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
Command::Account(account_subcommand) => {
|
2025-12-31 04:02:25 +03:00
|
|
|
account_subcommand.handle_subcommand(wallet_core).await?
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
Command::Pinata(pinata_subcommand) => {
|
2025-12-31 04:02:25 +03:00
|
|
|
pinata_subcommand.handle_subcommand(wallet_core).await?
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
2026-03-04 18:42:33 +03:00
|
|
|
Command::CheckHealth => {
|
2025-11-27 22:07:53 +03:00
|
|
|
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"
|
|
|
|
|
);
|
2025-11-27 22:07:53 +03:00
|
|
|
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"
|
|
|
|
|
);
|
2025-11-27 22:07:53 +03:00
|
|
|
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!(
|
2026-06-01 17:10:46 -03:00
|
|
|
circuit_id == &lee::PRIVACY_PRESERVING_CIRCUIT_ID,
|
2026-03-03 23:21:08 +03:00
|
|
|
"Local ID for privacy preserving circuit is different from remote"
|
|
|
|
|
);
|
2026-02-16 15:32:57 +01:00
|
|
|
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"
|
|
|
|
|
);
|
2025-11-27 22:07:53 +03:00
|
|
|
|
2026-03-04 18:42:33 +03:00
|
|
|
println!("\u{2705}All looks good!");
|
2025-11-27 22:07:53 +03:00
|
|
|
|
|
|
|
|
SubcommandReturnValue::Empty
|
|
|
|
|
}
|
2025-12-31 04:02:25 +03:00
|
|
|
Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?,
|
|
|
|
|
Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?,
|
2026-03-07 16:51:50 +01:00
|
|
|
Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?,
|
2026-06-05 21:34:00 +03:00
|
|
|
Command::Vault(vault_subcommand) => vault_subcommand.handle_subcommand(wallet_core).await?,
|
2026-05-05 18:55:51 +02:00
|
|
|
Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?,
|
2026-05-21 20:46:13 -04:00
|
|
|
Command::Keycard(keycard_subcommand) => {
|
|
|
|
|
keycard_subcommand.handle_subcommand(wallet_core).await?
|
|
|
|
|
}
|
2025-11-27 22:07:53 +03:00
|
|
|
Command::Config(config_subcommand) => {
|
2025-12-31 04:02:25 +03:00
|
|
|
config_subcommand.handle_subcommand(wallet_core).await?
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
2025-12-03 13:10:07 +02:00
|
|
|
Command::RestoreKeys { depth } => {
|
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()?;
|
2026-01-20 16:47:34 +01:00
|
|
|
wallet_core.restore_storage(&mnemonic, &password)?;
|
2025-12-31 04:02:25 +03:00
|
|
|
execute_keys_restoration(wallet_core, depth).await?;
|
2025-12-03 13:10:07 +02:00
|
|
|
|
2025-12-10 09:58:45 -03:00
|
|
|
SubcommandReturnValue::Empty
|
|
|
|
|
}
|
2025-12-05 17:54:51 -03:00
|
|
|
Command::DeployProgram { binary_filepath } => {
|
2025-12-09 15:22:30 -03:00
|
|
|
let bytecode: Vec<u8> = std::fs::read(&binary_filepath).context(format!(
|
|
|
|
|
"Failed to read program binary at {}",
|
|
|
|
|
binary_filepath.display()
|
|
|
|
|
))?;
|
2026-06-01 17:10:46 -03:00
|
|
|
let message = lee::program_deployment_transaction::Message::new(bytecode);
|
2025-12-05 17:54:51 -03:00
|
|
|
let transaction = ProgramDeploymentTransaction::new(message);
|
2025-12-09 15:22:30 -03:00
|
|
|
let _response = wallet_core
|
2025-12-05 17:54:51 -03:00
|
|
|
.sequencer_client
|
2026-06-01 17:10:46 -03:00
|
|
|
.send_transaction(LeeTransaction::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
|
|
|
|
|
}
|
2025-11-27 22:07:53 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(subcommand_ret)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 04:02:25 +03:00
|
|
|
pub async fn execute_continuous_run(wallet_core: &mut WalletCore) -> Result<()> {
|
2025-11-27 22:07:53 +03:00
|
|
|
loop {
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.sync_to_latest_block().await?;
|
2026-02-24 20:52:14 +03:00
|
|
|
tokio::time::sleep(wallet_core.config().seq_poll_timeout).await;
|
2025-11-27 22:07:53 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:40:06 -04:00
|
|
|
/// Parse a keys file exported by `wallet account show-keys`.
|
|
|
|
|
///
|
|
|
|
|
/// The file format is two lines:
|
|
|
|
|
/// - Line 1: npk as hex (64 chars, 32 bytes).
|
|
|
|
|
/// - Line 2: vpk as hex (2368 chars, 1184 bytes).
|
|
|
|
|
///
|
|
|
|
|
/// Returns `(npk_bytes, vpk_bytes)`.
|
|
|
|
|
pub fn read_keys_file(path: &str) -> Result<(Vec<u8>, Vec<u8>)> {
|
|
|
|
|
let content = std::fs::read_to_string(path).with_context(|| {
|
|
|
|
|
format!("wallet::cli::read_keys_file: failed to read keys file: {path}")
|
|
|
|
|
})?;
|
|
|
|
|
let mut lines = content.lines().filter(|l| !l.trim().is_empty());
|
|
|
|
|
let npk_hex = lines.next().ok_or_else(|| {
|
|
|
|
|
anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing npk (line 1)")
|
|
|
|
|
})?;
|
|
|
|
|
let vpk_hex = lines.next().ok_or_else(|| {
|
|
|
|
|
anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing vpk (line 2)")
|
|
|
|
|
})?;
|
|
|
|
|
let npk = hex::decode(npk_hex.trim())
|
|
|
|
|
.context("wallet::cli::read_keys_file: npk in keys file must be valid hex")?;
|
|
|
|
|
let vpk = hex::decode(vpk_hex.trim())
|
|
|
|
|
.context("wallet::cli::read_keys_file: vpk in keys file must be valid hex")?;
|
|
|
|
|
Ok((npk, vpk))
|
|
|
|
|
}
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 04:02:25 +03:00
|
|
|
pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> {
|
2025-12-03 07:13:35 +02:00
|
|
|
wallet_core
|
|
|
|
|
.storage
|
2026-04-10 20:23:25 +03:00
|
|
|
.key_chain_mut()
|
|
|
|
|
.generate_trees_for_depth(depth);
|
2025-12-03 07:13:35 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
println!(
|
|
|
|
|
"Public tree generated\n\
|
|
|
|
|
Private tree generated"
|
|
|
|
|
);
|
2025-12-03 07:13:35 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.sync_to_latest_block().await?;
|
2025-12-03 07:13:35 +02:00
|
|
|
|
|
|
|
|
wallet_core
|
|
|
|
|
.storage
|
2026-04-10 20:23:25 +03:00
|
|
|
.key_chain_mut()
|
|
|
|
|
.cleanup_trees_remove_uninit_layered(depth, |account_id| {
|
2026-03-13 22:38:23 +03:00
|
|
|
wallet_core
|
|
|
|
|
.sequencer_client
|
|
|
|
|
.get_account(account_id)
|
|
|
|
|
.map_err(Into::into)
|
|
|
|
|
})
|
2025-12-03 07:13:35 +02:00
|
|
|
.await?;
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
println!(
|
|
|
|
|
"Public tree cleaned up\n\
|
|
|
|
|
Private tree cleaned up"
|
|
|
|
|
);
|
2025-12-03 07:13:35 +02:00
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
wallet_core.store_persistent_data()?;
|
2025-12-03 07:13:35 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-06-03 14:40:06 -04:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_keys_file_roundtrip() {
|
|
|
|
|
let npk = [0xab_u8; 32];
|
|
|
|
|
let vpk = [0xcd_u8; 1184];
|
|
|
|
|
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let path = dir.path().join("test.keys");
|
|
|
|
|
|
|
|
|
|
// Simulate what `wallet account show-keys` writes.
|
|
|
|
|
std::fs::write(
|
|
|
|
|
&path,
|
|
|
|
|
format!("{}\n{}\n", hex::encode(npk), hex::encode(vpk)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(parsed_npk, npk, "npk must round-trip through the keys file");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parsed_vpk,
|
|
|
|
|
vpk.to_vec(),
|
|
|
|
|
"vpk must round-trip through the keys file"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_keys_file_missing_vpk_returns_error() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let path = dir.path().join("incomplete.keys");
|
|
|
|
|
std::fs::write(&path, format!("{}\n", hex::encode([0xab_u8; 32]))).unwrap();
|
|
|
|
|
|
|
|
|
|
let result = read_keys_file(path.to_str().unwrap());
|
|
|
|
|
assert!(result.is_err(), "missing vpk line must return an error");
|
|
|
|
|
assert!(
|
|
|
|
|
result.unwrap_err().to_string().contains("missing vpk"),
|
|
|
|
|
"error must mention missing vpk"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_keys_file_invalid_hex_returns_error() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let path = dir.path().join("badhex.keys");
|
|
|
|
|
std::fs::write(&path, "not-hex\nalso-not-hex\n").unwrap();
|
|
|
|
|
|
|
|
|
|
let result = read_keys_file(path.to_str().unwrap());
|
|
|
|
|
assert!(result.is_err(), "invalid hex must return an error");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_keys_file_ignores_blank_lines() {
|
|
|
|
|
let npk = [0x11_u8; 32];
|
|
|
|
|
let vpk = [0x22_u8; 1184];
|
|
|
|
|
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let path = dir.path().join("blanks.keys");
|
|
|
|
|
|
|
|
|
|
// Extra blank lines around the data should be tolerated.
|
|
|
|
|
std::fs::write(
|
|
|
|
|
&path,
|
|
|
|
|
format!("\n{}\n\n{}\n\n", hex::encode(npk), hex::encode(vpk)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap();
|
|
|
|
|
assert_eq!(parsed_npk, npk);
|
|
|
|
|
assert_eq!(parsed_vpk, vpk.to_vec());
|
|
|
|
|
}
|
|
|
|
|
}
|