mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-02 15:20:01 +00:00
385 lines
13 KiB
Rust
385 lines
13 KiB
Rust
use std::{io::Write as _, path::PathBuf, str::FromStr};
|
|
|
|
use anyhow::{Context as _, Result};
|
|
use bip39::Mnemonic;
|
|
use clap::{Parser, Subcommand};
|
|
use common::{HashType, transaction::NSSATransaction};
|
|
use derive_more::Display;
|
|
use futures::TryFutureExt as _;
|
|
use nssa::{ProgramDeploymentTransaction, program::Program};
|
|
use sequencer_service_rpc::RpcClient as _;
|
|
|
|
pub use crate::helperfunctions::{read_mnemonic, read_pin};
|
|
use crate::{
|
|
WalletCore,
|
|
account::{AccountIdWithPrivacy, Label},
|
|
cli::{
|
|
account::AccountSubcommand,
|
|
chain::ChainSubcommand,
|
|
config::ConfigSubcommand,
|
|
group::GroupSubcommand,
|
|
keycard::KeycardSubcommand,
|
|
programs::{
|
|
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
|
|
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
|
token::TokenProgramAgnosticSubcommand,
|
|
},
|
|
},
|
|
storage::Storage,
|
|
};
|
|
|
|
pub mod account;
|
|
pub mod chain;
|
|
pub mod config;
|
|
pub mod group;
|
|
pub mod keycard;
|
|
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),
|
|
/// Associated Token Account program interaction subcommand.
|
|
#[command(subcommand)]
|
|
Ata(AtaSubcommand),
|
|
/// Group key management (create, invite, join, derive keys).
|
|
#[command(subcommand)]
|
|
Group(GroupSubcommand),
|
|
/// 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 },
|
|
/// Keycard hardware wallet management.
|
|
#[command(subcommand)]
|
|
Keycard(KeycardSubcommand),
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
|
|
#[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()?;
|
|
let id_str =
|
|
keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path)
|
|
.map_err(anyhow::Error::from)?;
|
|
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.
|
|
pub fn to_recipient_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(
|
|
match wallet_core
|
|
.storage()
|
|
.key_chain()
|
|
.pub_account_signing_key(id)
|
|
{
|
|
Some(_) => crate::signing::AccountSigner::Local(id),
|
|
None => crate::signing::AccountSigner::Foreign,
|
|
},
|
|
),
|
|
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,
|
|
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");
|
|
};
|
|
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");
|
|
};
|
|
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");
|
|
};
|
|
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");
|
|
};
|
|
assert!(
|
|
amm_id == &Program::amm().id(),
|
|
"Local ID for AMM program is different from remote"
|
|
);
|
|
|
|
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?,
|
|
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?
|
|
}
|
|
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_transaction(NSSATransaction::ProgramDeployment(transaction))
|
|
.await
|
|
.context("Transaction submission error")?;
|
|
|
|
SubcommandReturnValue::Empty
|
|
}
|
|
};
|
|
|
|
Ok(subcommand_ret)
|
|
}
|
|
|
|
pub async fn execute_continuous_run(wallet_core: &mut WalletCore) -> Result<()> {
|
|
loop {
|
|
wallet_core.sync_to_latest_block().await?;
|
|
tokio::time::sleep(wallet_core.config().seq_poll_timeout).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_owned())
|
|
}
|
|
|
|
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
|
|
.key_chain_mut()
|
|
.generate_trees_for_depth(depth);
|
|
|
|
println!(
|
|
"Public tree generated\n\
|
|
Private tree generated"
|
|
);
|
|
|
|
wallet_core.sync_to_latest_block().await?;
|
|
|
|
wallet_core
|
|
.storage
|
|
.key_chain_mut()
|
|
.cleanup_trees_remove_uninit_layered(depth, |account_id| {
|
|
wallet_core
|
|
.sequencer_client
|
|
.get_account(account_id)
|
|
.map_err(Into::into)
|
|
})
|
|
.await?;
|
|
|
|
println!(
|
|
"Public tree cleaned up\n\
|
|
Private tree cleaned up"
|
|
);
|
|
|
|
wallet_core.store_persistent_data()?;
|
|
|
|
Ok(())
|
|
}
|