]> Untitled Git - bdk-cli/commitdiff
ref(handlers): fix offline, online and desc mod
authorVihiga Tyonum <withtvpeter@gmail.com>
Thu, 21 May 2026 15:23:38 +0000 (16:23 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Fri, 5 Jun 2026 16:32:14 +0000 (17:32 +0100)
- fix execution logic for subcommands under
offline and online wallet subcommands

13 files changed:
src/commands.rs
src/handlers/key.rs
src/handlers/mod.rs
src/handlers/offline.rs
src/handlers/online.rs
src/handlers/repl.rs
src/main.rs
src/payjoin/mod.rs
src/utils/common.rs
src/utils/descriptors.rs
src/utils/mod.rs
src/utils/output.rs
src/utils/types.rs

index cb1c925103fad65238d00bb44441575f030fbd73..52c24045c8ac3f83261e6849b1dac3a754263b5a 100644 (file)
@@ -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<Vec<OutPoint>>,
-    // },
     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<String>,
-    //     /// 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<u64>,
-    // },
     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<String>,
-    //     /// 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),
 }
 
index 6f3960ee955bf62b3126b2b1967504be11908024..3f175d777380d8aa177b6e5a30eeadf3d370febc 100644 (file)
@@ -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 {
index 7fb0179df98af70feae7e9ff7e93ad481d7a894f..25b53852c3553002c8f7306c477e4fb4927e2c93 100644 (file)
@@ -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<String, Error> {
-    let pretty = cli_opts.pretty;
-    let subcommand = cli_opts.subcommand.clone();
-
-    let result: Result<String, Error> = 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<Self::Output, Error>;
+}
 
-                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<Self::Output, Error>;
 }
+
+// context for online and online
+// => cli.rs
+// handlers/{mod for commands}
+// wallet subdir /
+// wallet-offline and wallet-online (client mod)
index b75e6ed7d81615d8ec2d1de6ed1908fc7fe8f114..62d89d5793b8f37e1916180cbc068f239eb74f4d 100644 (file)
-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<String, Error> {
-    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<UnspentDetails> = 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<TransactionDetails> = transactions
-                .map(|tx| {
-                    let total_value = tx
-                        .tx_node
-                        .output
-                        .iter()
-                        .map(|output| output.value.to_sat())
-                        .sum::<u64>();
-
-                    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<Self::Output, Error> {
+        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<Self::Output, Error> {
+        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<Self::Output, Error> {
+        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::<BTreeMap<String, Vec<usize>>>(&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<Self::Output, Error> {
+        let wallet = ctx
+            .wallet
+            .as_deref_mut()
+            .ok_or(Error::Generic("wallet required".to_string()))?;
+
+        let transactions = wallet.transactions();
+
+        let txns: Vec<TransactionDetails> = transactions
+            .map(|tx| {
+                let total_value = tx
+                    .tx_node
+                    .output
+                    .iter()
+                    .map(|output| output.value.to_sat())
+                    .sum::<u64>();
+
+                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<Self::Output, Error> {
+        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<Vec<OutPoint>>,
+
+    /// Marks a utxo as unspendable.
+    #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
+    pub unspendable: Option<Vec<OutPoint>>,
+
+    /// Fee rate to use in sat/vbyte.
+    #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
+    pub fee_rate: Option<f32>,
+
+    /// Selects which policy should be used to satisfy the external descriptor.
+    #[arg(env = "EXT_POLICY", long = "external_policy")]
+    pub external_policy: Option<String>,
+
+    /// Selects which policy should be used to satisfy the internal descriptor.
+    #[arg(env = "INT_POLICY", long = "internal_policy")]
+    pub internal_policy: Option<String>,
+
+    /// 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<String>,
+
+    /// 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<String>,
+}
 
-            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<Self::Output, Error> {
+        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::<serde_json::Value> {
-                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::<String> {
-                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::<BTreeMap<String, Vec<usize>>>(&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<Address>,
+
+    /// 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<Vec<OutPoint>>,
+
+    /// 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<Vec<OutPoint>>,
+
+    /// 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<Self::Output, Error> {
+        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::<Result<Vec<_>, 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<Psbt, Error>>(
-                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<serde_json::Value>;
+
+    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        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<String>;
+
+    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        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<u32>,
+
+    /// 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<bool>,
+}
+
+impl AppCommand for SignCommand {
+    type Output = PsbtResult;
+
+    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        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<Self::Output, Error> {
+        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<u32>,
+
+    /// 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<bool>,
+}
+
+impl AppCommand for FinalizePsbtCommand {
+    type Output = PsbtResult;
+
+    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        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<String>,
+}
+
+impl AppCommand for CombinePsbtCommand {
+    type Output = PsbtResult;
+
+    fn execute(&self, _ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        let mut psbts = self
+            .psbt
+            .iter()
+            .map(|s| {
+                let psbt = BASE64_STANDARD.decode(s)?;
+                Ok(Psbt::deserialize(&psbt)?)
             })
-            .to_string())
+            .collect::<Result<Vec<_>, 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<Psbt, Error>>(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<Vec<OutPoint>>,
+}
+
+#[cfg(feature = "bip322")]
+impl AppCommand for SignMessageCommand {
+    type Output = MessageResult;
+
+    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+        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<Self::Output, Error> {
+        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()
+        })
     }
 }
index ec3ff2dec399e1cd78a7836f9065e2cd2f0d8b16..eb125815152d3fc578a21655865cbdbfa4c77d6d 100644 (file)
@@ -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<String, Error> {
-    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::<KeychainKind>::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<Self::Output, Error> {
+        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::<KeychainKind>::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::<u8>::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<Self::Output, Error> {
+        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<String>,
+    /// Sets the raw transaction to broadcast.
+    #[arg(
+        env = "RAWTX",
+        long = "tx",
+        required_unless_present = "psbt",
+        conflicts_with = "psbt"
+    )]
+    tx: Option<String>,
+}
+
 #[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<Txid, Error> {
-    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<Self::Output, Error> {
+        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::<u8>::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<String>,
+
+    // /// 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<String>,
+    /// 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<u64>,
+}
+
+#[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<Self::Output, Error> {
+        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<String>,
+
+    // /// 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<String>,
+    /// 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<Self::Output, Error> {
+        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 })
     }
 }
index 432af0133cb1c87a70eeb55f48d54faa588b3357..6f9bd521780e886e87d82ea0365213db5c23e3df 100644 (file)
@@ -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<bool, String> {
-    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 {
index df5cfc4c49eaed3ed1a2665db7c47fd976bd90ec..f8877a66a43a82a81a96fb575601d00a3bc1d80a 100644 (file)
@@ -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) => {
index 894508b575d64707357894c0494f914aa7086cc0..0eaa0598dfb1008b4d4ad9e36bf8e73760b34854 100644 (file)
@@ -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,
index 2cfb985693013a1f36839f7a21a9e69232c1f1af..03e62db35512efffb48c262cb2731e6c49ae3d6a 100644 (file)
@@ -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();
index d228999367f56115f4007a619d03cebdccf02a16..04391e503214acaa184f55eb110323b5745f79b7 100644 (file)
@@ -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,
index cea89351fea13a7dde06597713ca7cda6b669517..afd02f9ab712ab488e37bdf088a795ad7bc05181 100644 (file)
@@ -2,3 +2,4 @@ pub mod common;
 pub mod descriptors;
 pub mod output;
 pub use common::*;
+pub mod types;
index 4ad6e8e25ea7fbcab612eea60db58ecf74cdc5eb..3f9a5e9fb329332bdc1ee755a3c476cce710159b 100644 (file)
@@ -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<String, Error>;
-
-    /// Formats the output based on the user's `--pretty` flag.
-    fn format(&self, pretty: bool) -> Result<String, Error> {
-        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<String, Error> {
+        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<R, C>(rows: R, title: Option<Vec<CellStruct>>) -> Result<String, Error>
-where
-    R: IntoIterator<Item = C>,
-    C: IntoIterator<Item = CellStruct>,
-{
-    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())
 }
index f3c5ddc8ad8382ff33c45933ec1a68160d03e43a..2fa16b974448662b905c10a8934db214d9d5066f 100644 (file)
@@ -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<String, WalletConfigInner>);
 
 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 {