From: Vihiga Tyonum Date: Thu, 21 May 2026 15:23:38 +0000 (+0100) Subject: ref(handlers): fix offline, online and desc mod X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.ScriptHash.html?a=commitdiff_plain;h=1b05c0309670574bec54f88aa1354332aed3e452;p=bdk-cli ref(handlers): fix offline, online and desc mod - fix execution logic for subcommands under offline and online wallet subcommands --- diff --git a/src/commands.rs b/src/commands.rs index cb1c925..52c2404 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -24,9 +24,6 @@ use crate::handlers::{ FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand, SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand, }, - online::{ - BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand, SyncCommand, - }, }; #[cfg(any( @@ -35,7 +32,12 @@ use crate::handlers::{ feature = "rpc", feature = "cbf" ))] -use crate::client::ClientType; +use crate::{ + client::ClientType, + online::{ + BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand, SyncCommand, + }, +}; #[cfg(feature = "compiler")] use crate::handlers::descriptor::CompileCommand; @@ -46,7 +48,7 @@ use bdk_wallet::bitcoin::Network; use clap::{Args, Parser, Subcommand, value_parser}; use clap_complete::Shell; -// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] +#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; /// The BDK Command Line Wallet App @@ -197,8 +199,8 @@ pub enum WalletSubCommand { #[cfg(any( feature = "electrum", feature = "esplora", - feature = "rpc", - feature = "cbf" + feature = "cbf", + feature = "rpc" ))] #[command(flatten)] OnlineWalletSubCommand(OnlineWalletSubCommand), @@ -269,7 +271,7 @@ pub struct WalletOpts { } /// Options to configure a SOCKS5 proxy for a blockchain client connection. -// #[cfg(any(feature = "electrum", feature = "esplora"))] +#[cfg(any(feature = "electrum", feature = "esplora"))] #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct ProxyOpts { /// Sets the SOCKS5 proxy for a blockchain client. @@ -295,7 +297,7 @@ pub struct ProxyOpts { } /// Options to configure a BIP157 Compact Filter backend. -// #[cfg(feature = "cbf")] +#[cfg(feature = "cbf")] #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct CompactFilterOpts { /// Sets the number of parallel node connections. @@ -336,46 +338,21 @@ pub enum OfflineWalletSubCommand { CombinePsbt(CombinePsbtCommand), /// Sign a message using BIP322 #[cfg(feature = "bip322")] - // SignMessage { - // /// The message to sign - // #[arg(long)] - // message: String, - // /// The signature format (e.g., Legacy, Simple, Full) - // #[arg(long, default_value = "simple")] - // signature_type: String, - // /// Address to sign - // #[arg(long)] - // address: String, - // /// Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`) - // #[arg(long)] - // utxos: Option>, - // }, SignMessage(SignMessageCommand), /// Verify a BIP322 signature #[cfg(feature = "bip322")] - // VerifyMessage { - // /// The signature proof to verify - // #[arg(long)] - // proof: String, - // /// The message that was signed - // #[arg(long)] - // message: String, - // /// The address associated with the signature - // #[arg(long)] - // address: String, - // }, VerifyMessage(VerifyMessageCommand), } /// Wallet subcommands that needs a blockchain backend. #[derive(Debug, Subcommand, Clone, PartialEq, Eq)] #[command(rename_all = "snake")] -// #[cfg(any( -// feature = "electrum", -// feature = "esplora", -// feature = "cbf", -// feature = "rpc" -// ))] +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] pub enum OnlineWalletSubCommand { /// Full Scan with the chosen blockchain server. FullScan(FullScanCommand), @@ -384,41 +361,8 @@ pub enum OnlineWalletSubCommand { /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract. Broadcast(BroadcastCommand), /// Generates a Payjoin receive URI and processes the sender's Payjoin proposal. - // ReceivePayjoin { - // /// Amount to be received in sats. - // #[arg(env = "PAYJOIN_AMOUNT", long = "amount", required = true)] - // amount: u64, - // /// Payjoin directory which will be used to store the PSBTs which are pending action - // /// from one of the parties. - // #[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)] - // directory: String, - // /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the - // /// operation with multiple relays for redundancy. - // #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] - // ohttp_relay: Vec, - // /// Maximum effective fee rate the receiver is willing to pay for their own input/output contributions. - // #[arg(env = "PAYJOIN_RECEIVER_MAX_FEE_RATE", long = "max_fee_rate")] - // max_fee_rate: Option, - // }, ReceivePayjoin(ReceivePayjoinCommand), /// Sends an original PSBT to a BIP 21 URI and broadcasts the returned Payjoin PSBT. - // SendPayjoin { - // /// BIP 21 URI for the Payjoin. - // #[arg(env = "PAYJOIN_URI", long = "uri", required = true)] - // uri: String, - // /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the - // /// operation with multiple relays for redundancy. - // #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] - // ohttp_relay: Vec, - // /// Fee rate to use in sat/vbyte. - // #[arg( - // env = "PAYJOIN_SENDER_FEE_RATE", - // short = 'f', - // long = "fee_rate", - // required = true - // )] - // fee_rate: u64, - // }, SendPayjoin(SendPayjoinCommand), } diff --git a/src/handlers/key.rs b/src/handlers/key.rs index 6f3960e..3f175d7 100644 --- a/src/handlers/key.rs +++ b/src/handlers/key.rs @@ -14,8 +14,6 @@ use bdk_wallet::keys::{DescriptorKey, ExtendedKey, GeneratedKey}; use bdk_wallet::miniscript::{self, Segwitv0}; use clap::Parser; - - impl KeySubCommand { pub fn execute(&self, ctx: &mut AppContext) -> Result<(), Error> { match self { diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 7fb0179..25b5385 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,295 +4,83 @@ pub mod key; pub mod offline; pub mod online; pub mod repl; -pub mod types; -pub mod wallets; -#[cfg(feature = "repl")] -use crate::handlers::repl::respond; -use crate::{ - commands::{CliOpts, CliSubCommand, WalletSubCommand}, - error::BDKCliError as Error, - handlers::{ - config::handle_config_subcommand, descriptor::handle_descriptor_command, - key::handle_key_subcommand, wallets::handle_wallets_subcommand, - }, - utils::{load_wallet_config, prepare_home_dir}, -}; - -#[cfg(any(feature = "sqlite", feature = "redb"))] -use crate::utils::prepare_wallet_db_dir; -#[cfg(not(any(feature = "sqlite", feature = "redb")))] -use crate::wallet::new_wallet; - -#[cfg(feature = "compiler")] -use { - crate::handlers::descriptor::handle_compile_subcommand, bdk_redb::Store as RedbStore, - std::sync::Arc, -}; - -#[cfg(feature = "repl")] -use crate::handlers::repl::readline; - -#[cfg(any(feature = "sqlite", feature = "redb"))] -use crate::commands::DatabaseType; -use crate::handlers::offline::handle_offline_wallet_subcommand; -use clap::CommandFactory; #[cfg(any( feature = "electrum", feature = "esplora", feature = "rpc", - feature = "cbf", + feature = "cbf" ))] -use { - crate::backend::new_blockchain_client, crate::handlers::online::handle_online_wallet_subcommand, -}; -#[cfg(any(feature = "sqlite", feature = "redb"))] -use { - crate::wallet::{new_persisted_wallet, persister::Persister}, - bdk_wallet::rusqlite::Connection, - std::io::Write, -}; - -/// The global top level handler. -pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { - let pretty = cli_opts.pretty; - let subcommand = cli_opts.subcommand.clone(); - - let result: Result = match subcommand { - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "cbf", - feature = "rpc" - ))] - CliSubCommand::Wallet { - wallet, - subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), - } => { - let home_dir = prepare_home_dir(cli_opts.datadir)?; - - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?; - - let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?; - - #[cfg(any(feature = "sqlite", feature = "redb"))] - let result = { - #[cfg(feature = "sqlite")] - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - DatabaseType::Sqlite => { - let db_file = database_path.join("wallet.sqlite"); - let connection = Connection::open(db_file)?; - log::debug!("Sqlite database opened successfully"); - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - DatabaseType::Redb => { - let wallet_name = &wallet_opts.wallet; - let db = Arc::new(bdk_redb::redb::Database::create( - home_dir.join("wallet.redb"), - )?); - let store = RedbStore::new( - db, - wallet_name.as_deref().unwrap_or("wallet").to_string(), - )?; - log::debug!("Redb database opened successfully"); - Persister::RedbStore(store) - } - }; - - let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; - let blockchain_client = - new_blockchain_client(&wallet_opts, &wallet, database_path)?; - - let result = handle_online_wallet_subcommand( - &mut wallet, - &blockchain_client, - online_subcommand, - ) - .await?; - wallet.persist(&mut persister)?; - result - }; - #[cfg(not(any(feature = "sqlite", feature = "redb")))] - let result = { - let mut wallet = new_wallet(network, &wallet_opts)?; - let blockchain_client = - new_blockchain_client(&wallet_opts, &wallet, database_path)?; - handle_online_wallet_subcommand(&mut wallet, &blockchain_client, online_subcommand) - .await? - }; - Ok(result) - } - CliSubCommand::Wallet { - wallet: wallet_name, - subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), - } => { - let datadir = cli_opts.datadir.clone(); - let home_dir = prepare_home_dir(datadir)?; - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; - - #[cfg(any(feature = "sqlite", feature = "redb"))] - let result = { - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - DatabaseType::Sqlite => { - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - let db_file = database_path.join("wallet.sqlite"); - let connection = Connection::open(db_file)?; - log::debug!("Sqlite database opened successfully"); - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - DatabaseType::Redb => { - let db = Arc::new(bdk_redb::redb::Database::create( - home_dir.join("wallet.redb"), - )?); - let store = RedbStore::new(db, wallet_name)?; - log::debug!("Redb database opened successfully"); - Persister::RedbStore(store) - } - }; - - let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; +use crate::client::BlockchainClient; +use std::path::PathBuf; + +use crate::{error::BDKCliError as Error, utils::output::FormatOutput}; +use bdk_wallet::{Wallet, bitcoin::Network}; + +/// The shared environment for all commands +pub struct AppContext<'a> { + pub network: Network, + pub datadir: PathBuf, + pub wallet: Option<&'a mut Wallet>, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + pub client: Option<&'a BlockchainClient>, +} - let result = handle_offline_wallet_subcommand( - &mut wallet, - &wallet_opts, - &cli_opts, - offline_subcommand.clone(), - )?; - wallet.persist(&mut persister)?; - result - }; - #[cfg(not(any(feature = "sqlite", feature = "redb")))] - let result = { - let mut wallet = new_wallet(network, &wallet_opts)?; - handle_offline_wallet_subcommand( - &mut wallet, - &wallet_opts, - &cli_opts, - offline_subcommand.clone(), - )? - }; - Ok(result) - } - CliSubCommand::Wallet { - wallet, - subcommand: WalletSubCommand::Config { force, wallet_opts }, - } => { - let network = cli_opts.network; - let home_dir = prepare_home_dir(cli_opts.datadir)?; - let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?; - Ok(result) - } - CliSubCommand::Wallets => { - let home_dir = prepare_home_dir(cli_opts.datadir)?; - let result = handle_wallets_subcommand(&home_dir, pretty)?; - Ok(result) +impl<'a> AppContext<'a> { + pub fn new(network: Network, datadir: PathBuf) -> Self { + Self { + network, + datadir, + wallet: None, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + client: None, } - CliSubCommand::Key { - subcommand: key_subcommand, - } => { - let network = cli_opts.network; - let result = handle_key_subcommand(network, key_subcommand, pretty)?; - Ok(result) - } - #[cfg(feature = "compiler")] - CliSubCommand::Compile { - policy, - script_type, - } => { - let network = cli_opts.network; - let result = handle_compile_subcommand(network, policy, script_type, pretty)?; - Ok(result) - } - #[cfg(feature = "repl")] - CliSubCommand::Repl { - wallet: wallet_name, - } => { - let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; - - #[cfg(any(feature = "sqlite", feature = "redb"))] - let (mut wallet, mut persister) = { - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - DatabaseType::Sqlite => { - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - let db_file = database_path.join("wallet.sqlite"); - let connection = Connection::open(db_file)?; - log::debug!("Sqlite database opened successfully"); - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - DatabaseType::Redb => { - let db = Arc::new(bdk_redb::redb::Database::create( - home_dir.join("wallet.redb"), - )?); - let store = RedbStore::new(db, wallet_name.clone())?; - log::debug!("Redb database opened successfully"); - Persister::RedbStore(store) - } - }; - let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; - (wallet, persister) - }; - #[cfg(not(any(feature = "sqlite", feature = "redb")))] - let mut wallet = new_wallet(network, &loaded_wallet_opts)?; - let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - loop { - let line = readline()?; - let line = line.trim(); - if line.is_empty() { - continue; - } + } + + /// Attach a mutable wallet reference to the context. + pub fn with_wallet(mut self, wallet: &'a mut Wallet) -> Self { + self.wallet = Some(wallet); + self + } + + /// Attach a client reference to the context. + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + pub fn with_client(mut self, client: &'a BlockchainClient) -> Self { + self.client = Some(client); + self + } +} - let result = respond( - network, - &mut wallet, - &wallet_name, - &mut wallet_opts.clone(), - line, - database_path.clone(), - &cli_opts, - ) - .await; - #[cfg(any(feature = "sqlite", feature = "redb"))] - wallet.persist(&mut persister)?; +pub trait AsyncCommand { + type Output: FormatOutput; + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result; +} - match result { - Ok(quit) => { - if quit { - break; - } - } - Err(err) => { - writeln!(std::io::stdout(), "{err}") - .map_err(|e| Error::Generic(e.to_string()))?; - std::io::stdout() - .flush() - .map_err(|e| Error::Generic(e.to_string()))?; - } - } - } - Ok("".to_string()) - } - CliSubCommand::Descriptor { desc_type, key } => { - let descriptor = handle_descriptor_command(cli_opts.network, desc_type, key, pretty)?; - Ok(descriptor) - } - CliSubCommand::Completions { shell } => { - clap_complete::generate( - shell, - &mut CliOpts::command(), - "bdk-cli", - &mut std::io::stdout(), - ); +/// The command trait +pub trait AppCommand { + type Output: FormatOutput; - Ok("".to_string()) - } - }; - result + /// The execution logic + fn execute(&self, ctx: &mut AppContext) -> Result; } + +// context for online and online +// => cli.rs +// handlers/{mod for commands} +// wallet subdir / +// wallet-offline and wallet-online (client mod) diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index b75e6ed..62d89d5 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -1,320 +1,639 @@ -use crate::commands::OfflineWalletSubCommand::*; -use crate::commands::{CliOpts, OfflineWalletSubCommand, WalletOpts}; +use crate::commands::OfflineWalletSubCommand; use crate::error::BDKCliError as Error; -use crate::handlers::types::{ +use crate::handlers::{AppCommand, AppContext}; +use crate::utils::output::FormatOutput; +use crate::utils::parse_address; +use crate::utils::types::{ AddressResult, BalanceResult, KeychainPair, PsbtResult, RawPsbt, TransactionDetails, TransactionListResult, UnspentDetails, UnspentListResult, }; -use crate::utils::output::FormatOutput; +use crate::utils::{parse_outpoint, parse_recipient}; use bdk_wallet::bitcoin::base64::Engine; use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD; use bdk_wallet::bitcoin::script::PushBytesBuf; -use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence, Txid}; -use bdk_wallet::{KeychainKind, SignOptions, Wallet}; +use bdk_wallet::bitcoin::{Address, Amount, FeeRate, OutPoint, Psbt, ScriptBuf, Sequence, Txid}; +use bdk_wallet::{KeychainKind, SignOptions}; +use clap::Parser; use serde_json::json; use std::collections::BTreeMap; use std::str::FromStr; #[cfg(feature = "bip322")] use { - crate::utils::{parse_address, parse_signature_format}, - bdk_bip322::{BIP322, MessageProof, MessageVerificationResult}, - bdk_wallet::bitcoin::Address, + crate::utils::parse_signature_format, + crate::utils::types::MessageResult, + bdk_bip322::{BIP322, MessageProof}, }; -/// Execute an offline wallet sub-command -/// -/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. -pub fn handle_offline_wallet_subcommand( - wallet: &mut Wallet, - wallet_opts: &WalletOpts, - cli_opts: &CliOpts, - offline_subcommand: OfflineWalletSubCommand, -) -> Result { - let pretty = cli_opts.pretty; - match offline_subcommand { - NewAddress => { - let addr = wallet.reveal_next_address(KeychainKind::External); - let result: AddressResult = addr.into(); - result.format(pretty) - } - UnusedAddress => { - let addr = wallet.next_unused_address(KeychainKind::External); - let result: AddressResult = addr.into(); - result.format(pretty) +impl OfflineWalletSubCommand { + pub fn execute(&self, ctx: &mut AppContext<'_>) -> Result<(), Error> { + match self { + Self::NewAddress(new_address) => new_address.execute(ctx)?.print(), + Self::Balance(balance) => balance.execute(ctx)?.print(), + Self::UnusedAddress(unused_address_command) => { + unused_address_command.execute(ctx)?.print() + } + Self::Unspent(unspent_command) => unspent_command.execute(ctx)?.print(), + Self::Transactions(transactions_command) => transactions_command.execute(ctx)?.print(), + Self::CreateTx(createtx_command) => createtx_command.execute(ctx)?.print(), + Self::BumpFee(bumpfee_command) => bumpfee_command.execute(ctx)?.print(), + Self::Policies(policies_command) => policies_command.execute(ctx)?.print(), + Self::PublicDescriptor(public_descriptor_command) => { + public_descriptor_command.execute(ctx)?.print() + } + Self::Sign(sign_command) => sign_command.execute(ctx)?.print(), + Self::ExtractPsbt(extract_psbt_command) => extract_psbt_command.execute(ctx)?.print(), + Self::FinalizePsbt(finalize_psbt_command) => { + finalize_psbt_command.execute(ctx)?.print() + } + Self::CombinePsbt(combine_psbt_command) => combine_psbt_command.execute(ctx)?.print(), + #[cfg(feature = "bip322")] + Self::SignMessage(sign_message_command) => sign_message_command.execute(ctx)?.print(), + #[cfg(feature = "bip322")] + Self::VerifyMessage(verify_message_command) => { + verify_message_command.execute(ctx)?.print() + } } - Unspent => { - let utxos: Vec = wallet - .list_unspent() - .map(|utxo| UnspentDetails::from_local_output(&utxo, cli_opts.network)) - .collect(); + } +} - let result = UnspentListResult(utxos); - result.format(pretty) - } - Transactions => { - let transactions = wallet.transactions(); - - let txns: Vec = transactions - .map(|tx| { - let total_value = tx - .tx_node - .output - .iter() - .map(|output| output.value.to_sat()) - .sum::(); - - TransactionDetails { - txid: tx.tx_node.txid.to_string(), - is_coinbase: tx.tx_node.is_coinbase(), - wtxid: tx.tx_node.compute_wtxid().to_string(), - version: serde_json::to_value(tx.tx_node.version).unwrap_or(json!(1)), - version_display: tx.tx_node.version.to_string(), - is_rbf: tx.tx_node.is_explicitly_rbf(), - inputs: serde_json::to_value(&tx.tx_node.input).unwrap_or_default(), - outputs: serde_json::to_value(&tx.tx_node.output).unwrap_or_default(), - input_count: tx.tx_node.input.len(), - output_count: tx.tx_node.output.len(), - total_value, - } - }) - .collect(); +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct NewAddressCommand {} - let result = TransactionListResult(txns); - result.format(pretty) - } - Balance => { - let balance = wallet.balance(); - let result: BalanceResult = balance.into(); - result.format(pretty) - } +impl AppCommand for NewAddressCommand { + type Output = AddressResult; - CreateTx { - recipients, - send_all, - enable_rbf, - offline_signer, - utxos, - unspendable, - fee_rate, - external_policy, - internal_policy, - add_data, - add_string, - } => { - let mut tx_builder = wallet.build_tx(); - - if send_all { - tx_builder.drain_wallet().drain_to(recipients[0].0.clone()); - } else { - let recipients = recipients - .into_iter() - .map(|(script, amount)| (script, Amount::from_sat(amount))) - .collect(); - tx_builder.set_recipients(recipients); - } + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".to_string()))?; + let address_info = wallet.reveal_next_address(KeychainKind::External); + Ok(AddressResult::from(address_info)) + } +} - if !enable_rbf { - tx_builder.set_exact_sequence(Sequence::MAX); - } +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct UnusedAddressCommand {} - if offline_signer { - tx_builder.include_output_redeem_witness_script(); - } +impl AppCommand for UnusedAddressCommand { + type Output = AddressResult; - if let Some(fee_rate) = fee_rate - && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) - { - tx_builder.fee_rate(fee_rate); - } + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("wallet is required".to_string()))?; + let address_info = wallet.next_unused_address(KeychainKind::External); + Ok(AddressResult::from(address_info)) + } +} - if let Some(utxos) = utxos { - tx_builder.add_utxos(&utxos[..]).unwrap(); - } +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct UnspentCommand {} - if let Some(unspendable) = unspendable { - tx_builder.unspendable(unspendable); - } +impl AppCommand for UnspentCommand { + type Output = UnspentListResult; - if let Some(base64_data) = add_data { - let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); - tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); - } else if let Some(string_data) = add_string { - let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); - tx_builder.add_data(&data); - } + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet is required".to_string()))?; + let utxos = wallet + .list_unspent() + .map(|utxo| UnspentDetails::from_local_output(&utxo, ctx.network)) + .collect(); - let policies = vec![ - external_policy.map(|p| (p, KeychainKind::External)), - internal_policy.map(|p| (p, KeychainKind::Internal)), - ]; + Ok(UnspentListResult(utxos)) + } +} - for (policy, keychain) in policies.into_iter().flatten() { - let policy = serde_json::from_str::>>(&policy)?; - tx_builder.policy_path(policy, keychain); - } +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct TransactionsCommand {} + +impl AppCommand for TransactionsCommand { + type Output = TransactionListResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("wallet required".to_string()))?; + + let transactions = wallet.transactions(); + + let txns: Vec = transactions + .map(|tx| { + let total_value = tx + .tx_node + .output + .iter() + .map(|output| output.value.to_sat()) + .sum::(); + + TransactionDetails { + txid: tx.tx_node.txid.to_string(), + is_coinbase: tx.tx_node.is_coinbase(), + wtxid: tx.tx_node.compute_wtxid().to_string(), + version: serde_json::to_value(tx.tx_node.version).unwrap_or(json!(1)), + version_display: tx.tx_node.version.to_string(), + is_rbf: tx.tx_node.is_explicitly_rbf(), + inputs: serde_json::to_value(&tx.tx_node.input).unwrap_or_default(), + outputs: serde_json::to_value(&tx.tx_node.output).unwrap_or_default(), + input_count: tx.tx_node.input.len(), + output_count: tx.tx_node.output.len(), + total_value, + } + }) + .collect(); - let psbt = tx_builder.finish()?; + Ok(TransactionListResult(txns)) + } +} - let result = PsbtResult::new(&psbt, wallet_opts.verbose, None); - result.format(pretty) - } - BumpFee { - txid, - shrink_address, - offline_signer, - utxos, - unspendable, - fee_rate, - } => { - let txid = Txid::from_str(txid.as_str())?; - - let mut tx_builder = wallet.build_fee_bump(txid)?; - let fee_rate = - FeeRate::from_sat_per_vb(fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN); - tx_builder.fee_rate(fee_rate); +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct BalanceCommand {} - if let Some(address) = shrink_address { - let script_pubkey = address.script_pubkey(); - tx_builder.drain_to(script_pubkey); - } +impl AppCommand for BalanceCommand { + type Output = BalanceResult; - if offline_signer { - tx_builder.include_output_redeem_witness_script(); - } + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or_else(|| Error::Generic("Wallet required".into()))?; + let balance = wallet.balance(); + Ok(BalanceResult::from(balance)) + } +} +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct CreateTxCommand { + /// Adds a recipient to the transaction. + #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)] + pub recipients: Vec<(ScriptBuf, u64)>, + + /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0. + #[arg(long = "send_all", short = 'a')] + pub send_all: bool, + + /// Enables Replace-By-Fee (BIP125). + #[arg(long = "enable_rbf", short = 'r', default_value_t = true)] + pub enable_rbf: bool, + + /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. + #[arg(long = "offline_signer")] + pub offline_signer: bool, + + /// Selects which utxos *must* be spent. + #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + pub utxos: Option>, + + /// Marks a utxo as unspendable. + #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + pub unspendable: Option>, + + /// Fee rate to use in sat/vbyte. + #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] + pub fee_rate: Option, + + /// Selects which policy should be used to satisfy the external descriptor. + #[arg(env = "EXT_POLICY", long = "external_policy")] + pub external_policy: Option, + + /// Selects which policy should be used to satisfy the internal descriptor. + #[arg(env = "INT_POLICY", long = "internal_policy")] + pub internal_policy: Option, + + /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes) + #[arg( + env = "ADD_STRING", + long = "add_string", + short = 's', + conflicts_with = "add_data" + )] + pub add_string: Option, + + /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes) + #[arg( + env = "ADD_DATA", + long = "add_data", + short = 'o', + conflicts_with = "add_string" + )] + pub add_data: Option, +} - if let Some(utxos) = utxos { - tx_builder.add_utxos(&utxos[..]).unwrap(); - } +impl AppCommand for CreateTxCommand { + type Output = PsbtResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or_else(|| Error::Generic("Wallet required".into()))?; + + let mut tx_builder = wallet.build_tx(); + + if self.send_all { + tx_builder + .drain_wallet() + .drain_to(self.recipients[0].0.clone()); + } else { + let recipients = self + .recipients + .clone() + .into_iter() + .map(|(script, amount)| (script, Amount::from_sat(amount))) + .collect(); + tx_builder.set_recipients(recipients); + } - if let Some(unspendable) = unspendable { - tx_builder.unspendable(unspendable); - } + if !self.enable_rbf { + tx_builder.set_exact_sequence(Sequence::MAX); + } - let psbt = tx_builder.finish()?; + if self.offline_signer { + tx_builder.include_output_redeem_witness_script(); + } - let result = PsbtResult::new(&psbt, wallet_opts.verbose, None); - result.format(pretty) + if let Some(fee_rate) = self.fee_rate + && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) + { + tx_builder.fee_rate(fee_rate); } - Policies => { - let external_policy = wallet.policies(KeychainKind::External)?; - let internal_policy = wallet.policies(KeychainKind::Internal)?; - let result = KeychainPair:: { - external: serde_json::to_value(&external_policy).unwrap_or(json!(null)), - internal: serde_json::to_value(&internal_policy).unwrap_or(json!(null)), - }; - result.format(cli_opts.pretty) + + if let Some(utxos) = &self.utxos { + tx_builder.add_utxos(&utxos[..]).unwrap(); } - PublicDescriptor => { - let result = KeychainPair:: { - external: wallet.public_descriptor(KeychainKind::External).to_string(), - internal: wallet.public_descriptor(KeychainKind::Internal).to_string(), - }; - result.format(cli_opts.pretty) + + if let Some(unspendable) = &self.unspendable { + tx_builder.unspendable(unspendable.to_vec()); } - Sign { - psbt, - assume_height, - trust_witness_utxo, - } => { - let psbt_bytes = BASE64_STANDARD.decode(psbt)?; - let mut psbt = Psbt::deserialize(&psbt_bytes)?; - let signopt = SignOptions { - assume_height, - trust_witness_utxo: trust_witness_utxo.unwrap_or(false), - ..Default::default() - }; - let finalized = wallet.sign(&mut psbt, signopt)?; - - let result = PsbtResult::new(&psbt, wallet_opts.verbose, Some(finalized)); - result.format(pretty) + + if let Some(base64_data) = &self.add_data { + let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); + tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); + } else if let Some(string_data) = &self.add_string { + let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); + tx_builder.add_data(&data); } - ExtractPsbt { psbt } => { - let psbt_serialized = BASE64_STANDARD.decode(psbt)?; - let psbt = Psbt::deserialize(&psbt_serialized)?; - let raw_tx = psbt.extract_tx()?; - let result = RawPsbt::new(&raw_tx); - result.format(pretty) + + let policies = vec![ + self.external_policy + .clone() + .map(|p| (p, KeychainKind::External)), + self.internal_policy + .clone() + .map(|p| (p, KeychainKind::Internal)), + ]; + + for (policy, keychain) in policies.into_iter().flatten() { + let policy = serde_json::from_str::>>(&policy)?; + tx_builder.policy_path(policy, keychain); } - FinalizePsbt { - psbt, - assume_height, - trust_witness_utxo, - } => { - let psbt_bytes = BASE64_STANDARD.decode(psbt)?; - let mut psbt: Psbt = Psbt::deserialize(&psbt_bytes)?; - - let signopt = SignOptions { - assume_height, - trust_witness_utxo: trust_witness_utxo.unwrap_or(false), - ..Default::default() - }; - let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; - - let result = PsbtResult::new(&psbt, wallet_opts.verbose, Some(finalized)); - result.format(pretty) + + let psbt = tx_builder.finish()?; + + // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); + + Ok(PsbtResult::new(&psbt, false, Some(false))) + } +} + +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct BumpFeeCommand { + /// TXID of the transaction to update. + #[arg(env = "TXID", long = "txid")] + pub txid: String, + + /// Allows the wallet to reduce the amount to the specified address in order to increase fees. + #[arg(env = "SHRINK_ADDRESS", long = "shrink", value_parser = parse_address)] + pub shrink_address: Option
, + + /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. + #[arg(long = "offline_signer")] + pub offline_signer: bool, + + /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used. + #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + pub utxos: Option>, + + /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees. + #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + pub unspendable: Option>, + + /// The new targeted fee rate in sat/vbyte. + #[arg( + env = "SATS_VBYTE", + short = 'f', + long = "fee_rate", + default_value = "1.0" + )] + pub fee_rate: f32, +} + +impl AppCommand for BumpFeeCommand { + type Output = PsbtResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or_else(|| Error::Generic("Wallet required".into()))?; + + let txid = Txid::from_str(self.txid.as_str())?; + + let mut tx_builder = wallet.build_fee_bump(txid)?; + let fee_rate = + FeeRate::from_sat_per_vb(self.fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN); + tx_builder.fee_rate(fee_rate); + + if let Some(address) = &self.shrink_address { + let script_pubkey = address.script_pubkey(); + tx_builder.drain_to(script_pubkey); } - CombinePsbt { psbt } => { - let mut psbts = psbt - .iter() - .map(|s| { - let psbt = BASE64_STANDARD.decode(s)?; - Ok(Psbt::deserialize(&psbt)?) - }) - .collect::, Error>>()?; - - let init_psbt = psbts - .pop() - .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?; - let final_psbt = psbts.into_iter().try_fold::<_, _, Result>( - init_psbt, - |mut acc, x| { - let _ = acc.combine(x); - Ok(acc) - }, - )?; - let result = PsbtResult::new(&final_psbt, wallet_opts.verbose, None); - result.format(pretty) + + if self.offline_signer { + tx_builder.include_output_redeem_witness_script(); } - #[cfg(feature = "bip322")] - SignMessage { - message, - signature_type, - address, - utxos, - } => { - let address: Address = parse_address(&address)?; - let signature_format = parse_signature_format(&signature_type)?; - - if !wallet.is_mine(address.script_pubkey()) { - return Err(Error::Generic(format!( - "Address {} does not belong to this wallet.", - address - ))); - } - let proof: MessageProof = - wallet.sign_message(message.as_str(), signature_format, &address, utxos)?; + if let Some(utxos) = &self.utxos { + tx_builder.add_utxos(&utxos[..]).unwrap(); + } - Ok(json!({"proof": proof.to_base64()}).to_string()) + if let Some(unspendable) = &self.unspendable { + tx_builder.unspendable(unspendable.to_vec()); } - #[cfg(feature = "bip322")] - VerifyMessage { - proof, - message, - address, - } => { - let address: Address = parse_address(&address)?; - let parsed_proof: MessageProof = MessageProof::from_base64(&proof) - .map_err(|e| Error::Generic(format!("Invalid proof: {e}")))?; - - let is_valid: MessageVerificationResult = - wallet.verify_message(&parsed_proof, &message, &address)?; - - Ok(json!({ - "valid": is_valid.valid, - "proven_amount": is_valid.proven_amount.map(|a| a.to_sat()) // optional field + + let psbt = tx_builder.finish()?; + + // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); + + Ok(PsbtResult::new(&psbt, false, Some(false))) + } +} + +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct PoliciesCommand {} + +impl AppCommand for PoliciesCommand { + type Output = KeychainPair; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let external_policy = wallet.policies(KeychainKind::External)?; + let internal_policy = wallet.policies(KeychainKind::Internal)?; + + Ok(KeychainPair { + external: serde_json::to_value(&external_policy).unwrap_or(serde_json::json!(null)), + internal: serde_json::to_value(&internal_policy).unwrap_or(serde_json::json!(null)), + }) + } +} + +#[derive(Parser, Debug, PartialEq, Clone)] +pub struct PublicDescriptorCommand {} + +impl AppCommand for PublicDescriptorCommand { + type Output = KeychainPair; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + Ok(KeychainPair { + external: wallet.public_descriptor(KeychainKind::External).to_string(), + internal: wallet.public_descriptor(KeychainKind::Internal).to_string(), + }) + } +} + +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct SignCommand { + /// Sets the PSBT to sign. + #[arg(env = "BASE64_PSBT")] + pub psbt: String, + + /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor. + #[arg(env = "HEIGHT", long = "assume_height")] + pub assume_height: Option, + + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided. + #[arg(env = "WITNESS", long = "trust_witness_utxo")] + pub trust_witness_utxo: Option, +} + +impl AppCommand for SignCommand { + type Output = PsbtResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let psbt_bytes = BASE64_STANDARD + .decode(&self.psbt) + .map_err(|e| Error::Generic(e.to_string()))?; + let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| Error::Generic(e.to_string()))?; + + let signopt = SignOptions { + assume_height: self.assume_height, + trust_witness_utxo: self.trust_witness_utxo.unwrap_or(false), + ..Default::default() + }; + let finalized = wallet.sign(&mut psbt, signopt)?; + Ok(PsbtResult::new(&psbt, false, Some(finalized))) + } +} + +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct ExtractPsbtCommand { + /// Sets the PSBT to extract + #[arg(env = "BASE64_PSBT")] + pub psbt: String, +} + +impl AppCommand for ExtractPsbtCommand { + type Output = RawPsbt; + + fn execute(&self, _ctx: &mut AppContext) -> Result { + let psbt_serialized = BASE64_STANDARD.decode(self.psbt.clone())?; + let psbt = Psbt::deserialize(&psbt_serialized)?; + let raw_tx = psbt.extract_tx()?; + + Ok(RawPsbt::new(&raw_tx)) + } +} + +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct FinalizePsbtCommand { + /// Sets the PSBT to finalize. + #[arg(env = "BASE64_PSBT")] + pub psbt: String, + + /// Assume the blockchain has reached a specific height. + #[arg(env = "HEIGHT", long = "assume_height")] + pub assume_height: Option, + + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided. + #[arg(env = "WITNESS", long = "trust_witness_utxo")] + pub trust_witness_utxo: Option, +} + +impl AppCommand for FinalizePsbtCommand { + type Output = PsbtResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let psbt_bytes = BASE64_STANDARD + .decode(&self.psbt) + .map_err(|e| Error::Generic(e.to_string()))?; + let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| Error::Generic(e.to_string()))?; + + let signopt = SignOptions { + assume_height: self.assume_height, + trust_witness_utxo: self.trust_witness_utxo.unwrap_or(false), + ..Default::default() + }; + + let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; + + Ok(PsbtResult::new(&psbt, false, Some(finalized))) + } +} + +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct CombinePsbtCommand { + /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT. + #[arg(env = "BASE64_PSBT", required = true)] + pub psbt: Vec, +} + +impl AppCommand for CombinePsbtCommand { + type Output = PsbtResult; + + fn execute(&self, _ctx: &mut AppContext) -> Result { + let mut psbts = self + .psbt + .iter() + .map(|s| { + let psbt = BASE64_STANDARD.decode(s)?; + Ok(Psbt::deserialize(&psbt)?) }) - .to_string()) + .collect::, Error>>()?; + + let init_psbt = psbts + .pop() + .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?; + let final_psbt = + psbts + .into_iter() + .try_fold::<_, _, Result>(init_psbt, |mut acc, x| { + let _ = acc.combine(x); + Ok(acc) + })?; + + Ok(PsbtResult::new(&final_psbt, false, None)) + } +} + +#[cfg(feature = "bip322")] +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct SignMessageCommand { + /// The message to sign + #[arg(long)] + pub message: String, + + /// The signature format (e.g., Legacy, Simple, Full) + #[arg(long, default_value = "simple")] + pub signature_type: String, + + /// Address to sign + #[arg(long)] + pub address: String, + + /// Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`) + #[arg(long)] + pub utxos: Option>, +} + +#[cfg(feature = "bip322")] +impl AppCommand for SignMessageCommand { + type Output = MessageResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let address: Address = parse_address(&self.address)?; + let signature_format = parse_signature_format(&self.signature_type)?; + + if !wallet.is_mine(address.script_pubkey()) { + return Err(Error::Generic(format!( + "Address {} does not belong to this wallet.", + address + ))); } + + let proof = wallet.sign_message( + self.message.as_str(), + signature_format, + &address, + self.utxos.clone(), + )?; + + Ok(MessageResult { + proof: Some(proof.to_base64()), + ..Default::default() + }) + } +} + +// #[cfg(feature = "bip322")] +#[derive(Debug, Parser, Clone, PartialEq)] +pub struct VerifyMessageCommand { + /// The signature proof to verify + #[arg(long)] + pub proof: String, + + /// The message that was signed + #[arg(long)] + pub message: String, + + /// The address associated with the signature + #[arg(long)] + pub address: String, +} + +#[cfg(feature = "bip322")] +impl AppCommand for VerifyMessageCommand { + type Output = MessageResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + + let address: Address = parse_address(&self.address)?; + + let parsed_proof = MessageProof::from_base64(&self.proof) + .map_err(|e| Error::Generic(format!("Invalid proof format: {e}")))?; + + let is_valid = wallet.verify_message(&parsed_proof, &self.message, &address)?; + + Ok(MessageResult { + valid: Some(is_valid.valid), + proven_amount: is_valid.proven_amount.map(|a| a.to_sat()), + ..Default::default() + }) } } diff --git a/src/handlers/online.rs b/src/handlers/online.rs index ec3ff2d..eb12581 100644 --- a/src/handlers/online.rs +++ b/src/handlers/online.rs @@ -1,11 +1,13 @@ +use clap::Parser; + #[cfg(feature = "electrum")] -use crate::backend::BlockchainClient::Electrum; -#[cfg(feature = "cbf")] -use crate::backend::BlockchainClient::KyotoClient; +use crate::client::BlockchainClient::Electrum; #[cfg(feature = "rpc")] -use crate::backend::BlockchainClient::RpcClient; +use crate::client::BlockchainClient::RpcClient; +#[cfg(feature = "cbf")] +use crate::client::{BlockchainClient::KyotoClient, sync_kyoto_client}; #[cfg(feature = "esplora")] -use {crate::backend::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; +use {crate::client::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; #[cfg(feature = "rpc")] use { bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXS, bitcoincore_rpc::RpcApi}, @@ -21,307 +23,445 @@ use {bdk_wallet::KeychainKind, std::collections::HashSet, std::io::Write}; feature = "rpc" ))] use { - crate::backend::{BlockchainClient, sync_kyoto_client}, - crate::commands::OnlineWalletSubCommand::*, + crate::commands::OnlineWalletSubCommand, crate::error::BDKCliError as Error, - crate::payjoin::PayjoinManager, - crate::payjoin::ohttp::RelayManager, + crate::handlers::{AppContext, AsyncCommand}, + crate::payjoin::{PayjoinManager, ohttp::RelayManager}, crate::utils::is_final, - bdk_wallet::Wallet, + crate::utils::output::FormatOutput, + crate::utils::types::{StatusResult, TransactionResult}, bdk_wallet::bitcoin::{ - Psbt, Transaction, Txid, base64::Engine, base64::prelude::BASE64_STANDARD, - consensus::Decodable, hex::FromHex, + Psbt, Transaction, base64::Engine, base64::prelude::BASE64_STANDARD, consensus::Decodable, + hex::FromHex, }, - serde_json::json, std::sync::{Arc, Mutex}, }; +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +impl OnlineWalletSubCommand { + pub async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<(), Error> { + match self { + OnlineWalletSubCommand::FullScan(full_scan_command) => { + full_scan_command.execute(ctx).await?.print() + } + OnlineWalletSubCommand::Sync(sync_command) => sync_command.execute(ctx).await?.print(), + OnlineWalletSubCommand::Broadcast(broadcast_command) => { + broadcast_command.execute(ctx).await?.print() + } + OnlineWalletSubCommand::ReceivePayjoin(receive_payjoin_command) => { + receive_payjoin_command.execute(ctx).await?.print() + } + OnlineWalletSubCommand::SendPayjoin(send_payjoin_command) => { + send_payjoin_command.execute(ctx).await?.print() + } + } + } +} + +#[derive(Parser, Debug, PartialEq, Clone, Eq)] +pub struct FullScanCommand { + /// Stop searching addresses for transactions after finding an unused gap of this length. + #[arg(env = "STOP_GAP", long = "scan-stop-gap", default_value = "20")] + stop_gap: usize, + // #[clap(long, default_value = "5")] + // pub parallel_request: usize, +} -/// Execute an online wallet sub-command -/// -/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`]. #[cfg(any( feature = "electrum", feature = "esplora", feature = "cbf", feature = "rpc" ))] -pub(crate) async fn handle_online_wallet_subcommand( - wallet: &mut Wallet, - client: &BlockchainClient, - online_subcommand: crate::commands::OnlineWalletSubCommand, -) -> Result { - match online_subcommand { - FullScan { - stop_gap: _stop_gap, - } => { - #[cfg(any(feature = "electrum", feature = "esplora"))] - let request = wallet.start_full_scan().inspect({ - let mut stdout = std::io::stdout(); - let mut once = HashSet::::new(); - move |k, spk_i, _| { - if once.insert(k) { - print!("\nScanning keychain [{k:?}]"); - } - print!(" {spk_i:<3}"); - stdout.flush().expect("must flush"); - } - }); - match client { - #[cfg(feature = "electrum")] - Electrum { client, batch_size } => { - // Populate the electrum client's transaction cache so it doesn't re-download transaction we - // already have. - client - .populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); - - let update = client.full_scan(request, _stop_gap, *batch_size, false)?; - wallet.apply_update(update)?; - } - #[cfg(feature = "esplora")] - Esplora { - client, - parallel_requests, - } => { - let update = client - .full_scan(request, _stop_gap, *parallel_requests) - .await - .map_err(|e| *e)?; - wallet.apply_update(update)?; +impl AsyncCommand for FullScanCommand { + type Output = StatusResult; + + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let client = ctx + .client + .ok_or(Error::Generic("Online client required".into()))?; + + #[cfg(any(feature = "electrum", feature = "esplora"))] + let request = wallet.start_full_scan().inspect({ + let mut stdout = std::io::stdout(); + let mut once = HashSet::::new(); + move |k, spk_i, _| { + if once.insert(k) { + print!("\nScanning keychain [{k:?}]"); } + print!(" {spk_i:<3}"); + stdout.flush().expect("must flush"); + } + }); - #[cfg(feature = "rpc")] - RpcClient { client } => { - let blockchain_info = client.get_blockchain_info()?; - - let genesis_block = - bdk_wallet::bitcoin::constants::genesis_block(wallet.network()); - let genesis_cp = CheckPoint::new(BlockId { - height: 0, - hash: genesis_block.block_hash(), - }); - let mut emitter = Emitter::new( - client.as_ref(), - genesis_cp.clone(), - genesis_cp.height(), - NO_EXPECTED_MEMPOOL_TXS, - ); - - while let Some(block_event) = emitter.next_block()? { - if block_event.block_height() % 10_000 == 0 { - let percent_done = f64::from(block_event.block_height()) - / f64::from(blockchain_info.headers as u32) - * 100f64; - println!( - "Applying block at height: {}, {:.2}% done.", - block_event.block_height(), - percent_done - ); - } - - wallet.apply_block_connected_to( - &block_event.block, + match client { + #[cfg(feature = "electrum")] + Electrum { client, batch_size } => { + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + let update = client.full_scan(request, self.stop_gap, *batch_size, false)?; + wallet.apply_update(update)?; + } + + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests, + } => { + let update = client + .full_scan(request, self.stop_gap, *parallel_requests) + .await + .map_err(|e| *e)?; + wallet.apply_update(update)?; + } + #[cfg(feature = "rpc")] + RpcClient { client } => { + let blockchain_info = client.get_blockchain_info()?; + + let genesis_block = bdk_wallet::bitcoin::constants::genesis_block(wallet.network()); + let genesis_cp = CheckPoint::new(BlockId { + height: 0, + hash: genesis_block.block_hash(), + }); + let mut emitter = Emitter::new( + client.as_ref(), + genesis_cp.clone(), + genesis_cp.height(), + NO_EXPECTED_MEMPOOL_TXS, + ); + + while let Some(block_event) = emitter.next_block()? { + if block_event.block_height() % 10_000 == 0 { + let percent_done = f64::from(block_event.block_height()) + / f64::from(blockchain_info.headers as u32) + * 100f64; + println!( + "Applying block at height: {}, {:.2}% done.", block_event.block_height(), - block_event.connected_to(), - )?; + percent_done + ); } - let mempool_txs = emitter.mempool()?; - wallet.apply_unconfirmed_txs(mempool_txs.update); - } - #[cfg(feature = "cbf")] - KyotoClient { client } => { - sync_kyoto_client(wallet, client).await?; + wallet.apply_block_connected_to( + &block_event.block, + block_event.block_height(), + block_event.connected_to(), + )?; } + + let mempool_txs = emitter.mempool()?; + wallet.apply_unconfirmed_txs(mempool_txs.update); + } + + #[cfg(feature = "cbf")] + KyotoClient { client } => { + sync_kyoto_client(wallet, client).await?; } - Ok(serde_json::to_string_pretty(&json!({}))?) - } - Sync => { - sync_wallet(client, wallet).await?; - Ok(serde_json::to_string_pretty(&json!({}))?) - } - Broadcast { psbt, tx } => { - let tx = match (psbt, tx) { - (Some(psbt), None) => { - let psbt = BASE64_STANDARD - .decode(psbt) - .map_err(|e| Error::Generic(e.to_string()))?; - let psbt: Psbt = Psbt::deserialize(&psbt)?; - is_final(&psbt)?; - psbt.extract_tx()? - } - (None, Some(tx)) => { - let tx_bytes = Vec::::from_hex(&tx)?; - Transaction::consensus_decode(&mut tx_bytes.as_slice())? - } - (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"), - (None, None) => panic!("Missing `psbt` and `tx` option"), - }; - let txid = broadcast_transaction(client, tx).await?; - Ok(serde_json::to_string_pretty(&json!({ "txid": txid }))?) - } - ReceivePayjoin { - amount, - directory, - ohttp_relay, - max_fee_rate, - } => { - let relay_manager = Arc::new(Mutex::new(RelayManager::new())); - let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); - return payjoin_manager - .receive_payjoin(amount, directory, max_fee_rate, ohttp_relay, client) - .await; - } - SendPayjoin { - uri, - ohttp_relay, - fee_rate, - } => { - let relay_manager = Arc::new(Mutex::new(RelayManager::new())); - let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); - return payjoin_manager - .send_payjoin(uri, fee_rate, ohttp_relay, client) - .await; } + Ok(StatusResult::new("Full scan completed successfully.")) } } +#[derive(Parser, Debug, PartialEq, Eq, Clone)] +pub struct SyncCommand {} + #[cfg(any( feature = "electrum", feature = "esplora", feature = "cbf", feature = "rpc" ))] -/// Syncs a given wallet using the blockchain client. -pub async fn sync_wallet(client: &BlockchainClient, wallet: &mut Wallet) -> Result<(), Error> { - // #[cfg(any(feature = "electrum", feature = "esplora"))] - let request = wallet - .start_sync_with_revealed_spks() - .inspect(|item, progress| { - let pc = (100 * progress.consumed()) as f32 / progress.total() as f32; - eprintln!("[ SCANNING {pc:03.0}% ] {item}"); - }); - match client { - #[cfg(feature = "electrum")] - Electrum { client, batch_size } => { - // Populate the electrum client's transaction cache so it doesn't re-download transaction we - // already have. - client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); - - let update = client.sync(request, *batch_size, false)?; - wallet - .apply_update(update) - .map_err(|e| Error::Generic(e.to_string())) - } - #[cfg(feature = "esplora")] - Esplora { - client, - parallel_requests, - } => { - let update = client - .sync(request, *parallel_requests) - .await - .map_err(|e| *e)?; - wallet - .apply_update(update) - .map_err(|e| Error::Generic(e.to_string())) - } - #[cfg(feature = "rpc")] - RpcClient { client } => { - let blockchain_info = client.get_blockchain_info()?; - let wallet_cp = wallet.latest_checkpoint(); - - // reload the last 200 blocks in case of a reorg - let emitter_height = wallet_cp.height().saturating_sub(200); - let mut emitter = Emitter::new( - client.as_ref(), - wallet_cp, - emitter_height, +impl AsyncCommand for SyncCommand { + type Output = StatusResult; + + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".to_string()))?; + let client = ctx + .client + .ok_or(Error::Generic("Client is required".to_string()))?; + #[cfg(any(feature = "electrum", feature = "esplora"))] + let request = wallet + .start_sync_with_revealed_spks() + .inspect(|item, progress| { + let pc = (100 * progress.consumed()) as f32 / progress.total() as f32; + eprintln!("[ SCANNING {pc:03.0}% ] {item}"); + }); + + match client { + #[cfg(feature = "electrum")] + Electrum { client, batch_size } => { + // Populate the electrum client's transaction cache so it doesn't re-download transaction we + // already have. + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + + let update = client.sync(request, *batch_size, false)?; + wallet + .apply_update(update) + .map_err(|e| Error::Generic(e.to_string()))?; + } + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests, + } => { + let update = client + .sync(request, *parallel_requests) + .await + .map_err(|e| *e)?; wallet - .tx_graph() - .list_canonical_txs( - wallet.local_chain(), - wallet.local_chain().tip().block_id(), - CanonicalizationParams::default(), - ) - .filter(|tx| tx.chain_position.is_unconfirmed()), - ); - - while let Some(block_event) = emitter.next_block()? { - if block_event.block_height() % 10_000 == 0 { - let percent_done = f64::from(block_event.block_height()) - / f64::from(blockchain_info.headers as u32) - * 100f64; - println!( - "Applying block at height: {}, {:.2}% done.", + .apply_update(update) + .map_err(|e| Error::Generic(e.to_string()))?; + } + #[cfg(feature = "rpc")] + RpcClient { client } => { + let blockchain_info = client.get_blockchain_info()?; + let wallet_cp = wallet.latest_checkpoint(); + + let emitter_height = wallet_cp.height().saturating_sub(200); + let mut emitter = Emitter::new( + client.as_ref(), + wallet_cp, + emitter_height, + wallet + .tx_graph() + .list_canonical_txs( + wallet.local_chain(), + wallet.local_chain().tip().block_id(), + CanonicalizationParams::default(), + ) + .filter(|tx| tx.chain_position.is_unconfirmed()), + ); + + while let Some(block_event) = emitter.next_block()? { + if block_event.block_height() % 10_000 == 0 { + let percent_done = f64::from(block_event.block_height()) + / f64::from(blockchain_info.headers as u32) + * 100f64; + println!( + "Applying block at height: {}, {:.2}% done.", + block_event.block_height(), + percent_done + ); + } + + wallet.apply_block_connected_to( + &block_event.block, block_event.block_height(), - percent_done - ); + block_event.connected_to(), + )?; } - wallet.apply_block_connected_to( - &block_event.block, - block_event.block_height(), - block_event.connected_to(), - )?; + let mempool_txs = emitter.mempool()?; + wallet.apply_unconfirmed_txs(mempool_txs.update); } - - let mempool_txs = emitter.mempool()?; - wallet.apply_unconfirmed_txs(mempool_txs.update); - Ok(()) + #[cfg(feature = "cbf")] + KyotoClient { client } => sync_kyoto_client(wallet, client) + .await + .map_err(|e| Error::Generic(e.to_string()))?, } - #[cfg(feature = "cbf")] - KyotoClient { client } => sync_kyoto_client(wallet, client) - .await - .map_err(|e| Error::Generic(e.to_string())), + Ok(StatusResult::new("Wallet synced successfully.")) } } +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct BroadcastCommand { + /// Sets the PSBT to sign. + #[arg( + env = "BASE64_PSBT", + long = "psbt", + required_unless_present = "tx", + conflicts_with = "tx" + )] + psbt: Option, + /// Sets the raw transaction to broadcast. + #[arg( + env = "RAWTX", + long = "tx", + required_unless_present = "psbt", + conflicts_with = "psbt" + )] + tx: Option, +} + #[cfg(any( feature = "electrum", feature = "esplora", feature = "cbf", feature = "rpc" ))] -/// Broadcasts a given transaction using the blockchain client. -pub async fn broadcast_transaction( - client: &BlockchainClient, - tx: Transaction, -) -> Result { - match client { - #[cfg(feature = "electrum")] - Electrum { - client, - batch_size: _, - } => client - .transaction_broadcast(&tx) - .map_err(|e| Error::Generic(e.to_string())), - #[cfg(feature = "esplora")] - Esplora { - client, - parallel_requests: _, - } => client - .broadcast(&tx) - .await - .map(|()| tx.compute_txid()) - .map_err(|e| Error::Generic(e.to_string())), - #[cfg(feature = "rpc")] - RpcClient { client } => client - .send_raw_transaction(&tx) - .map_err(|e| Error::Generic(e.to_string())), - - #[cfg(feature = "cbf")] - KyotoClient { client } => { - let txid = tx.compute_txid(); - let wtxid = client - .requester - .broadcast_random(tx.clone()) - .await - .map_err(|_| { - tracing::warn!("Broadcast was unsuccessful"); - Error::Generic("Transaction broadcast timed out after 30 seconds".into()) - })?; - tracing::info!("Successfully broadcast WTXID: {wtxid}"); - Ok(txid) - } + +impl AsyncCommand for BroadcastCommand { + type Output = TransactionResult; + + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + let client = ctx + .client + .ok_or(Error::Generic("Online client required".into()))?; + + let tx = match (&self.psbt, &self.tx) { + (Some(psbt), None) => { + let psbt = BASE64_STANDARD + .decode(psbt) + .map_err(|e| Error::Generic(e.to_string()))?; + let psbt: Psbt = Psbt::deserialize(&psbt)?; + is_final(&psbt)?; + psbt.extract_tx()? + } + (None, Some(tx)) => { + let tx_bytes = Vec::::from_hex(&tx)?; + Transaction::consensus_decode(&mut tx_bytes.as_slice())? + } + (Some(_), Some(_)) => { + return Err(Error::Generic( + "Both `psbt` and `tx` options are not allowed".into(), + )); + } + (None, None) => { + return Err(Error::Generic( + "Must provide either a `psbt` or `tx` to broadcast".into(), + )); + } + }; + + let txid = client.broadcast(tx).await?; + + Ok(TransactionResult { + txid: txid.to_string(), + }) + } +} + +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct ReceivePayjoinCommand { + // /// The amount to receive in satoshis. + // pub amount: u64, + + // /// The payjoin directory URL. + // #[clap(long)] + // pub directory: String, + + // /// Optional OHTTP relay URLs for privacy. + // #[clap(long)] + // pub ohttp_relays: Vec, + + // /// Maximum fee rate in sat/vB to pay for the payjoin transaction. + // #[clap(long)] + // pub max_fee_rate: u64, + /// Amount to be received in sats. + #[arg(env = "PAYJOIN_AMOUNT", long = "amount", required = true)] + amount: u64, + /// Payjoin directory which will be used to store the PSBTs which are pending action + /// from one of the parties. + #[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)] + directory: String, + /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the + /// operation with multiple relays for redundancy. + #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] + ohttp_relay: Vec, + /// Maximum effective fee rate the receiver is willing to pay for their own input/output contributions. + #[arg(env = "PAYJOIN_RECEIVER_MAX_FEE_RATE", long = "max_fee_rate")] + max_fee_rate: Option, +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +impl AsyncCommand for ReceivePayjoinCommand { + type Output = StatusResult; + + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let client = ctx + .client + .ok_or(Error::Generic("Online client required".into()))?; + + let relay_manager = Arc::new(Mutex::new(RelayManager::new())); + let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); + let result = payjoin_manager + .receive_payjoin( + self.amount, + self.directory.clone(), + self.max_fee_rate, + self.ohttp_relay.clone(), + client, + ) + .await?; + + Ok(StatusResult { message: result }) + } +} + +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct SendPayjoinCommand { + // /// The Payjoin URI string to send to. + // pub uri: String, + + // /// Optional OHTTP relay URLs for privacy. + // #[clap(long)] + // pub ohttp_relays: Vec, + + // /// Fee rate in sat/vB for the transaction. + // #[clap(long, short)] + // pub fee_rate: u64, + /// BIP 21 URI for the Payjoin. + #[arg(env = "PAYJOIN_URI", long = "uri", required = true)] + uri: String, + /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the + /// operation with multiple relays for redundancy. + #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] + ohttp_relay: Vec, + /// Fee rate to use in sat/vbyte. + #[arg( + env = "PAYJOIN_SENDER_FEE_RATE", + short = 'f', + long = "fee_rate", + required = true + )] + fee_rate: u64, +} +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +impl AsyncCommand for SendPayjoinCommand { + type Output = StatusResult; + + async fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + let wallet = ctx + .wallet + .as_deref_mut() + .ok_or(Error::Generic("Wallet required".into()))?; + let client = ctx.client.ok_or(Error::Generic("client required".into()))?; + + let relay_manager = Arc::new(Mutex::new(RelayManager::new())); + let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); + let result = payjoin_manager + .send_payjoin( + self.uri.clone(), + self.fee_rate, + self.ohttp_relay.clone(), + client, + ) + .await?; + + Ok(StatusResult { message: result }) } } diff --git a/src/handlers/repl.rs b/src/handlers/repl.rs index 432af01..6f9bd52 100644 --- a/src/handlers/repl.rs +++ b/src/handlers/repl.rs @@ -1,93 +1,81 @@ -#[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "cbf", - feature = "rpc" -))] -use crate::{backend::new_blockchain_client, handlers::online::handle_online_wallet_subcommand}; +use bdk_wallet::{Wallet, bitcoin::Network}; + +// #[cfg(feature = "repl")] +use crate::handlers::{AppCommand, AppContext}; +use crate::utils::output::FormatOutput; #[cfg(feature = "sqlite")] use crate::commands::ReplSubCommand; +use clap::Parser; #[cfg(feature = "repl")] +use std::io::Write; use { + crate::commands::{CliOpts, WalletSubCommand}, crate::error::BDKCliError as Error, - crate::{ - commands::{CliOpts, WalletOpts, WalletSubCommand}, - handlers::{ - config::handle_config_subcommand, descriptor::handle_descriptor_command, - key::handle_key_subcommand, offline::handle_offline_wallet_subcommand, - }, - }, - bdk_wallet::{Wallet, bitcoin::Network}, - std::io::Write, }; #[cfg(feature = "repl")] pub(crate) async fn respond( network: Network, wallet: &mut Wallet, - wallet_name: &String, - wallet_opts: &mut WalletOpts, line: &str, - _datadir: std::path::PathBuf, + datadir: std::path::PathBuf, cli_opts: &CliOpts, ) -> Result { - use clap::Parser; - let args = shlex::split(line).ok_or("error: Invalid quoting".to_string())?; - let repl_subcommand = ReplSubCommand::try_parse_from(args).map_err(|e| e.to_string())?; - let response = match repl_subcommand { - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "cbf", - feature = "rpc" - ))] - ReplSubCommand::Wallet { - subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), - } => { - let blockchain = - new_blockchain_client(wallet_opts, wallet, _datadir).map_err(|e| e.to_string())?; - let value = handle_online_wallet_subcommand(wallet, &blockchain, online_subcommand) - .await - .map_err(|e| e.to_string())?; - Some(value) - } - ReplSubCommand::Wallet { - subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), - } => { - let value = - handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand) - .map_err(|e| e.to_string())?; - Some(value) - } - ReplSubCommand::Wallet { - subcommand: WalletSubCommand::Config { force, wallet_opts }, - } => { - let value = handle_config_subcommand( - &_datadir, - network, - wallet_name.to_string(), - &wallet_opts, - force, - ) - .map_err(|e| e.to_string())?; - Some(value) - } - ReplSubCommand::Key { subcommand } => { - let value = handle_key_subcommand(network, subcommand, cli_opts.pretty) - .map_err(|e| e.to_string())?; - Some(value) + + let mut repl_args = vec!["repl".to_string()]; + repl_args.extend(args); + + let repl_subcommand = match ReplSubCommand::try_parse_from(&repl_args) { + Ok(cmd) => cmd, + Err(e) => { + writeln!(std::io::stdout(), "{}", e).map_err(|e| e.to_string())?; + return Ok(false); } - ReplSubCommand::Descriptor { desc_type, key } => { - let value = handle_descriptor_command(network, desc_type, key, cli_opts.pretty) - .map_err(|e| e.to_string())?; - Some(value) + }; + + let mut ctx = AppContext::new(network, datadir.clone()).with_wallet(&mut *wallet); + + let response = match repl_subcommand { + ReplSubCommand::Wallet { subcommand } => match subcommand { + WalletSubCommand::OfflineWalletSubCommand(cmd) => { + cmd.execute(&mut ctx).map_err(|e| e.to_string())?; + Some(()) + } + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" + ))] + WalletSubCommand::OnlineWalletSubCommand(cmd) => { + let value = cmd.execute(&mut ctx).await.map_err(|e| e.to_string())?; + Some(()) + // Some(value) + } + WalletSubCommand::Config(config_cmd) => { + let mut ctx = AppContext::new(network, datadir); + let res = config_cmd + .execute(&mut ctx) + .map_err(|e| e.to_string())? + .print(); + Some(()) + } + }, + + // Assuming your REPL Descriptor command is an inline struct based on commands.rs + ReplSubCommand::Descriptor(cmd) => { + let value = cmd.execute(&mut ctx).map_err(|e| e.to_string())?.print(); + Some(()) } + ReplSubCommand::Exit => None, + _ => todo!(), }; + if let Some(value) = response { - writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?; + // writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?; std::io::stdout().flush().map_err(|e| e.to_string())?; Ok(false) } else { diff --git a/src/main.rs b/src/main.rs index df5cfc4..f8877a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -169,7 +169,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { } CliSubCommand::Repl { wallet: _ } => todo!(), CliSubCommand::Completions { shell } => { - shell; + shell; } #[cfg(feature = "compiler")] CliSubCommand::Compile(cmd) => { diff --git a/src/payjoin/mod.rs b/src/payjoin/mod.rs index 894508b..0eaa059 100644 --- a/src/payjoin/mod.rs +++ b/src/payjoin/mod.rs @@ -117,12 +117,12 @@ impl<'a> PayjoinManager<'a> { Ok(to_string_pretty(&json!({}))?) } - // #[cfg(any( - // feature = "electrum", - // feature = "esplora", - // feature = "rpc", - // feature = "cbf" - // ))] + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] pub async fn send_payjoin( &mut self, uri: String, diff --git a/src/utils/common.rs b/src/utils/common.rs index 2cfb985..03e62db 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -76,7 +76,7 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { Ok((addr.script_pubkey(), val)) } -// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] +#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] /// Parse the proxy (Socket:Port) argument from the cli input. pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> { let parts: Vec<_> = s.split(':').collect(); diff --git a/src/utils/descriptors.rs b/src/utils/descriptors.rs index d228999..04391e5 100644 --- a/src/utils/descriptors.rs +++ b/src/utils/descriptors.rs @@ -19,7 +19,8 @@ use bdk_wallet::{ }; use crate::error::BDKCliError as Error; -use crate::handlers::types::{DescriptorResult, KeychainPair}; +use crate::utils::types::DescriptorResult; +use crate::utils::types::KeychainPair; pub fn generate_descriptors( desc_type: &str, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index cea8935..afd02f9 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,3 +2,4 @@ pub mod common; pub mod descriptors; pub mod output; pub use common::*; +pub mod types; diff --git a/src/utils/output.rs b/src/utils/output.rs index 4ad6e8e..3f9a5e9 100644 --- a/src/utils/output.rs +++ b/src/utils/output.rs @@ -1,35 +1,16 @@ use crate::error::BDKCliError as Error; -use cli_table::{CellStruct, Table}; use serde::Serialize; /// A trait for types that can be presented to the user. pub trait FormatOutput: Serialize { - /// Return a pretty table representation. - fn to_table(&self) -> Result; - - /// Formats the output based on the user's `--pretty` flag. - fn format(&self, pretty: bool) -> Result { - if pretty { - self.to_table() - } else { - serde_json::to_string_pretty(self) - .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}"))) - } + fn format(&self) -> Result { + serde_json::to_string_pretty(self) + .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}"))) } -} -/// Helper for building simple tables -pub fn simple_table(rows: R, title: Option>) -> Result -where - R: IntoIterator, - C: IntoIterator, -{ - let mut table = rows.table(); - if let Some(title) = title { - table = table.title(title); + fn print(&self) -> Result<(), Error> { + let output = self.format()?; + println!("{}", output); + Ok(()) } - table - .display() - .map_err(|e| Error::Generic(e.to_string())) - .map(|t| t.to_string()) } diff --git a/src/utils/types.rs b/src/utils/types.rs index f3c5ddc..2fa16b9 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -65,7 +65,6 @@ pub struct UnspentDetails { pub is_spent: bool, pub derivation_index: u32, pub chain_position: serde_json::Value, - } impl UnspentDetails { @@ -196,8 +195,6 @@ pub struct TransactionResult { impl FormatOutput for TransactionResult {} - - /// Return type definition #[derive(Serialize)] #[serde(transparent)] @@ -205,7 +202,6 @@ pub struct WalletsListResult(pub HashMap); impl FormatOutput for WalletsListResult {} - /// return type #[derive(Serialize)] pub struct DescriptorResult { @@ -246,7 +242,6 @@ pub struct KeyResult { impl FormatOutput for KeyResult {} - /// Balance representation #[derive(Serialize)] pub struct BalanceResult {