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 _; 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 use crate::helperfunctions::{read_mnemonic, read_pin}; 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; } /// 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, /// Wallet command. #[command(subcommand)] pub command: Option, } #[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 { 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 { 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 { 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 { 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