]> Untitled Git - bdk-cli/commitdiff
update namespace and update types
authorVihiga Tyonum <withtvpeter@gmail.com>
Thu, 14 May 2026 10:13:27 +0000 (11:13 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Fri, 5 Jun 2026 16:31:58 +0000 (17:31 +0100)
src/backend/mod.rs [deleted file]
src/client.rs
src/commands/mod.rs [deleted file]
src/config.rs [new file with mode: 0644]
src/config/mod.rs [deleted file]
src/error.rs
src/handlers/types.rs
src/handlers/wallets.rs [deleted file]
src/payjoin/mod.rs
src/utils/common.rs

diff --git a/src/backend/mod.rs b/src/backend/mod.rs
deleted file mode 100644 (file)
index a4695b7..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "rpc",
-    feature = "cbf"
-))]
-use {
-    crate::commands::{ClientType, WalletOpts},
-    crate::error::BDKCliError as Error,
-    bdk_wallet::Wallet,
-    std::path::PathBuf,
-};
-
-#[cfg(feature = "cbf")]
-use {
-    crate::utils::trace_logger,
-    bdk_kyoto::{BuilderExt, LightClient},
-};
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "rpc",
-    feature = "cbf"
-))]
-pub(crate) enum BlockchainClient {
-    #[cfg(feature = "electrum")]
-    Electrum {
-        client: Box<bdk_electrum::BdkElectrumClient<bdk_electrum::electrum_client::Client>>,
-        batch_size: usize,
-    },
-    #[cfg(feature = "esplora")]
-    Esplora {
-        client: Box<bdk_esplora::esplora_client::AsyncClient>,
-        parallel_requests: usize,
-    },
-    #[cfg(feature = "rpc")]
-    RpcClient {
-        client: Box<bdk_bitcoind_rpc::bitcoincore_rpc::Client>,
-    },
-
-    #[cfg(feature = "cbf")]
-    KyotoClient { client: Box<KyotoClientHandle> },
-}
-
-/// Handle for the Kyoto client after the node has been started.
-/// Contains only the components needed for sync and broadcast operations.
-#[cfg(feature = "cbf")]
-pub struct KyotoClientHandle {
-    pub requester: bdk_kyoto::Requester,
-    pub update_subscriber: tokio::sync::Mutex<bdk_kyoto::UpdateSubscriber>,
-}
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "rpc",
-    feature = "cbf",
-))]
-/// Create a new blockchain from the wallet configuration options.
-pub(crate) fn new_blockchain_client(
-    wallet_opts: &WalletOpts,
-    _wallet: &Wallet,
-    _datadir: PathBuf,
-) -> Result<BlockchainClient, Error> {
-    #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-    let url = &wallet_opts.url;
-    let client = match wallet_opts.client_type {
-        #[cfg(feature = "electrum")]
-        ClientType::Electrum => {
-            let client = bdk_electrum::electrum_client::Client::new(url)
-                .map(bdk_electrum::BdkElectrumClient::new)?;
-            BlockchainClient::Electrum {
-                client: Box::new(client),
-                batch_size: wallet_opts.batch_size,
-            }
-        }
-        #[cfg(feature = "esplora")]
-        ClientType::Esplora => {
-            let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?;
-            BlockchainClient::Esplora {
-                client: Box::new(client),
-                parallel_requests: wallet_opts.parallel_requests,
-            }
-        }
-
-        #[cfg(feature = "rpc")]
-        ClientType::Rpc => {
-            let auth = match &wallet_opts.cookie {
-                Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()),
-                None => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::UserPass(
-                    wallet_opts.basic_auth.0.clone(),
-                    wallet_opts.basic_auth.1.clone(),
-                ),
-            };
-            let client = bdk_bitcoind_rpc::bitcoincore_rpc::Client::new(url, auth)
-                .map_err(|e| Error::Generic(e.to_string()))?;
-            BlockchainClient::RpcClient {
-                client: Box::new(client),
-            }
-        }
-
-        #[cfg(feature = "cbf")]
-        ClientType::Cbf => {
-            let scan_type = bdk_kyoto::ScanType::Sync;
-            let builder = bdk_kyoto::builder::Builder::new(_wallet.network());
-
-            let light_client = builder
-                .required_peers(wallet_opts.compactfilter_opts.conn_count)
-                .data_dir(&_datadir)
-                .build_with_wallet(_wallet, scan_type)?;
-
-            let LightClient {
-                requester,
-                info_subscriber,
-                warning_subscriber,
-                update_subscriber,
-                node,
-            } = light_client;
-
-            let subscriber = tracing_subscriber::FmtSubscriber::new();
-            let _ = tracing::subscriber::set_global_default(subscriber);
-
-            tokio::task::spawn(async move { node.run().await });
-            tokio::task::spawn(
-                async move { trace_logger(info_subscriber, warning_subscriber).await },
-            );
-
-            BlockchainClient::KyotoClient {
-                client: Box::new(KyotoClientHandle {
-                    requester,
-                    update_subscriber: tokio::sync::Mutex::new(update_subscriber),
-                }),
-            }
-        }
-    };
-    Ok(client)
-}
-
-// Handle Kyoto Client sync
-#[cfg(feature = "cbf")]
-pub async fn sync_kyoto_client(
-    wallet: &mut Wallet,
-    handle: &KyotoClientHandle,
-) -> Result<(), Error> {
-    if !handle.requester.is_running() {
-        tracing::error!("Kyoto node is not running");
-        return Err(Error::Generic("Kyoto node failed to start".to_string()));
-    }
-    tracing::info!("Kyoto node is running");
-
-    let update = handle.update_subscriber.lock().await.update().await?;
-    tracing::info!("Received update: applying to wallet");
-    wallet
-        .apply_update(update)
-        .map_err(|e| Error::Generic(format!("Failed to apply update: {e}")))?;
-
-    tracing::info!(
-        "Chain tip: {}, Transactions: {}, Balance: {}",
-        wallet.local_chain().tip().height(),
-        wallet.transactions().count(),
-        wallet.balance().total().to_sat()
-    );
-
-    tracing::info!(
-        "Sync completed: tx_count={}, balance={}",
-        wallet.transactions().count(),
-        wallet.balance().total().to_sat()
-    );
-
-    Ok(())
-}
index 2b97330ee32502204a8ccf86a45a55415c5c5d8c..6c024411fc2702a3678dee7e84e96cfa90fe4a9e 100644 (file)
@@ -1,77 +1,87 @@
-use bdk_bitcoind_rpc::{Emitter, bitcoincore_rpc::RpcApi};
+use crate::error::BDKCliError as Error;
+#[cfg(feature = "esplora")]
 use bdk_esplora::EsploraAsyncExt;
 use bdk_wallet::{
     bitcoin::{Transaction, Txid},
     chain::CanonicalizationParams,
 };
-// #[cfg(any(
-//     feature = "electrum",
-//     feature = "esplora",
-//     feature = "rpc",
-//     feature = "cbf"
-// ))]
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
 use {
     crate::commands::{ClientType, WalletOpts},
-    crate::error::BDKCliError as Error,
+  
     bdk_wallet::Wallet,
     std::path::PathBuf,
 };
+#[cfg(feature = "rpc")]
+use bdk_bitcoind_rpc::{Emitter, bitcoincore_rpc::RpcApi};
 
-// #[cfg(feature = "cbf")]
+
+#[cfg(feature = "cbf")]
 use {
     crate::utils::trace_logger,
     bdk_kyoto::{BuilderExt, LightClient},
 };
 
-// #[cfg(any(
-//     feature = "electrum",
-//     feature = "esplora",
-//     feature = "rpc",
-//     feature = "cbf"
-// ))]
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
 pub(crate) enum BlockchainClient {
-    // #[cfg(feature = "electrum")]
+    #[cfg(feature = "electrum")]
     Electrum {
         client: Box<bdk_electrum::BdkElectrumClient<bdk_electrum::electrum_client::Client>>,
         batch_size: usize,
     },
-    // #[cfg(feature = "esplora")]
+    #[cfg(feature = "esplora")]
     Esplora {
         client: Box<bdk_esplora::esplora_client::AsyncClient>,
         parallel_requests: usize,
     },
-    // #[cfg(feature = "rpc")]
+    #[cfg(feature = "rpc")]
     RpcClient {
         client: Box<bdk_bitcoind_rpc::bitcoincore_rpc::Client>,
     },
 
-    // #[cfg(feature = "cbf")]
+    #[cfg(feature = "cbf")]
     KyotoClient {
         client: Box<KyotoClientHandle>,
     },
 }
 
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
 impl BlockchainClient {
     pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
         match self {
-            // #[cfg(feature = "electrum")]
+            #[cfg(feature = "electrum")]
             Self::Electrum { client, .. } => client
                 .transaction_broadcast(&tx)
                 .map_err(|e| Error::Generic(e.to_string())),
 
-            // #[cfg(feature = "esplora")]
+            #[cfg(feature = "esplora")]
             Self::Esplora { client, .. } => client
                 .broadcast(&tx)
                 .await
                 .map(|()| tx.compute_txid())
                 .map_err(|e| Error::Generic(e.to_string())),
 
-            // #[cfg(feature = "rpc")]
+            #[cfg(feature = "rpc")]
             Self::RpcClient { client } => client
                 .send_raw_transaction(&tx)
                 .map_err(|e| Error::Generic(e.to_string())),
 
-            // #[cfg(feature = "cbf")]
+            #[cfg(feature = "cbf")]
             Self::KyotoClient { client } => {
                 // ... (Kyoto broadcast logic from your online.rs) ...
                 Ok(tx.compute_txid())
@@ -166,28 +176,28 @@ impl BlockchainClient {
 
 /// Handle for the Kyoto client after the node has been started.
 /// Contains only the components needed for sync and broadcast operations.
-// #[cfg(feature = "cbf")]
+#[cfg(feature = "cbf")]
 pub struct KyotoClientHandle {
     pub requester: bdk_kyoto::Requester,
     pub update_subscriber: tokio::sync::Mutex<bdk_kyoto::UpdateSubscriber>,
 }
 
-// #[cfg(any(
-//     feature = "electrum",
-//     feature = "esplora",
-//     feature = "rpc",
-//     feature = "cbf",
-// ))]
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf",
+))]
 /// Create a new blockchain from the wallet configuration options.
 pub(crate) fn new_blockchain_client(
     wallet_opts: &WalletOpts,
     _wallet: &Wallet,
     _datadir: PathBuf,
 ) -> Result<BlockchainClient, Error> {
-    // #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+    #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
     let url = &wallet_opts.url;
     let client = match wallet_opts.client_type {
-        // #[cfg(feature = "electrum")]
+        #[cfg(feature = "electrum")]
         ClientType::Electrum => {
             let client = bdk_electrum::electrum_client::Client::new(url)
                 .map(bdk_electrum::BdkElectrumClient::new)?;
@@ -196,7 +206,7 @@ pub(crate) fn new_blockchain_client(
                 batch_size: wallet_opts.batch_size,
             }
         }
-        // #[cfg(feature = "esplora")]
+        #[cfg(feature = "esplora")]
         ClientType::Esplora => {
             let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?;
             BlockchainClient::Esplora {
@@ -205,7 +215,7 @@ pub(crate) fn new_blockchain_client(
             }
         }
 
-        // #[cfg(feature = "rpc")]
+        #[cfg(feature = "rpc")]
         ClientType::Rpc => {
             let auth = match &wallet_opts.cookie {
                 Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()),
@@ -221,7 +231,7 @@ pub(crate) fn new_blockchain_client(
             }
         }
 
-        // #[cfg(feature = "cbf")]
+        #[cfg(feature = "cbf")]
         ClientType::Cbf => {
             let scan_type = bdk_kyoto::ScanType::Sync;
             let builder = bdk_kyoto::builder::Builder::new(_wallet.network());
@@ -259,7 +269,7 @@ pub(crate) fn new_blockchain_client(
 }
 
 // Handle Kyoto Client sync
-// #[cfg(feature = "cbf")]
+#[cfg(feature = "cbf")]
 pub async fn sync_kyoto_client(
     wallet: &mut Wallet,
     handle: &KyotoClientHandle,
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
deleted file mode 100644 (file)
index c8fb1a0..0000000
+++ /dev/null
@@ -1,709 +0,0 @@
-// Copyright (c) 2020-2025 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! bdk-cli Command structure
-//!
-//! This module defines all the bdk-cli commands structure.
-//! All optional args are defined in the structs below.
-//! All subcommands are defined in the below enums.
-
-#![allow(clippy::large_enum_variant)]
-#[cfg(feature = "silent-payments")]
-use {crate::utils::parse_sp_code_value_pairs, bdk_sp::encoding::SilentPaymentCode};
-
-use bdk_wallet::bitcoin::{
-    Address, Network, OutPoint, ScriptBuf,
-    bip32::{DerivationPath, Xpriv},
-};
-use clap::{Args, Parser, Subcommand, ValueEnum, value_parser};
-use clap_complete::Shell;
-
-use crate::utils::{parse_address, parse_outpoint, parse_recipient};
-
-#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-use crate::utils::parse_proxy_auth;
-
-/// The BDK Command Line Wallet App
-///
-/// bdk-cli is a lightweight command line bitcoin wallet, powered by BDK.
-/// This app can be used as a playground as well as testing environment to simulate
-/// various wallet testing situations. If you are planning to use BDK in your wallet, bdk-cli
-/// is also a great intro tool to get familiar with the BDK API.
-///
-/// But this is not just any toy.
-/// bdk-cli is also a fully functioning Bitcoin wallet with taproot support!
-///
-/// For more information checkout <https://bitcoindevkit.org/>
-#[derive(PartialEq, Clone, Debug, Parser)]
-#[command(version, about, long_about = None)]
-pub struct CliOpts {
-    /// Sets the network.
-    #[arg(
-        env = "NETWORK",
-        short = 'n',
-        long = "network",
-        default_value = "testnet",
-        value_parser = value_parser!(Network)
-    )]
-    pub network: Network,
-    /// Sets the wallet data directory.
-    /// Default value : ~/.bdk-bitcoin
-    #[arg(env = "DATADIR", short = 'd', long = "datadir")]
-    pub datadir: Option<std::path::PathBuf>,
-    /// Output results in pretty format (instead of JSON).
-    #[arg(long = "pretty", global = true)]
-    pub pretty: bool,
-    /// Top level cli sub-commands.
-    #[command(subcommand)]
-    pub subcommand: CliSubCommand,
-}
-
-/// Top level cli sub-commands.
-#[derive(Debug, Subcommand, Clone, PartialEq)]
-#[command(rename_all = "snake")]
-pub enum CliSubCommand {
-    /// Wallet operations.
-    ///
-    /// bdk-cli wallet operations includes all the basic wallet level tasks.
-    /// Most commands can be used without connecting to any backend. To use commands that
-    /// needs backend like `sync` and `broadcast`, compile the binary with specific backend feature
-    /// and use the configuration options below to configure for that backend.
-    Wallet {
-        /// Selects the wallet to use.
-        #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
-        wallet: String,
-
-        #[command(subcommand)]
-        subcommand: WalletSubCommand,
-    },
-    /// Key management operations.
-    ///
-    /// Provides basic key operations that are not related to a specific wallet such as generating a
-    /// new random master extended key or restoring a master extended key from mnemonic words.
-    ///
-    /// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this
-    /// feature to create keys that secure actual funds on the Bitcoin mainnet.
-    Key {
-        #[clap(subcommand)]
-        subcommand: KeySubCommand,
-    },
-    /// Compile a miniscript policy to an output descriptor.
-    #[cfg(feature = "compiler")]
-    #[clap(long_about = "Miniscript policy compiler")]
-    Compile {
-        /// Sets the spending policy to compile.
-        #[arg(env = "POLICY", required = true, index = 1)]
-        policy: String,
-        /// Sets the script type used to embed the compiled policy.
-        #[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh", "tr"]
-        )]
-        script_type: String,
-    },
-    #[cfg(feature = "repl")]
-    /// REPL command loop mode.
-    ///
-    /// REPL command loop can be used to make recurring callbacks to an already loaded wallet.
-    /// This mode is useful for hands on live testing of wallet operations.
-    Repl {
-        /// Wallet name for this REPL session
-        #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
-        wallet: String,
-    },
-    /// Output Descriptors operations.
-    ///
-    /// Generate output descriptors from either extended key (Xprv/Xpub) or mnemonic phrase.
-    /// This feature is intended for development and testing purposes only.
-    Descriptor {
-        /// Descriptor type (script type)
-        #[arg(
-            long = "type",
-            short = 't',
-            value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"],
-            default_value = "wsh"
-        )]
-        desc_type: String,
-        /// Optional key: xprv, xpub, or mnemonic phrase
-        key: Option<String>,
-    },
-    /// List all saved wallet configurations.
-    Wallets,
-    /// Generate tab-completion scripts for your shell.
-    ///
-    /// The completion script is output on stdout, allowing you to redirect
-    /// it to a file of your choosing. Where you place the file will depend
-    /// on your shell and operating system.
-    ///
-    /// Here are common setups for supported shells:
-    ///
-    /// Bash:
-    ///
-    ///     Completion files are commonly stored in
-    ///     `~/.local/share/bash-completion/completions` for user-specific commands.
-    ///     Run the commands:
-    ///
-    ///         $ mkdir -p ~/.local/share/bash-completion/completions
-    ///         $ bdk-cli completions bash > ~/.local/share/bash-completion/completions/bdk-cli
-    ///
-    /// Zsh:
-    ///
-    ///     Completion files are commonly stored in a directory listed in your `fpath`.
-    ///     Run the commands:
-    ///
-    ///         $ mkdir -p ~/.zfunc
-    ///         $ bdk-cli completions zsh > ~/.zfunc/_bdk-cli
-    ///
-    ///     Make sure `~/.zfunc` is in your fpath by adding to your `.zshrc`:
-    ///
-    ///         fpath=(~/.zfunc $fpath)
-    ///         autoload -Uz compinit && compinit
-    ///
-    /// Fish:
-    ///
-    ///     Completion files are commonly stored in
-    ///     `~/.config/fish/completions`. Run the commands:
-    ///
-    ///         $ mkdir -p ~/.config/fish/completions
-    ///         $ bdk-cli completions fish > ~/.config/fish/completions/bdk-cli.fish
-    ///
-    /// PowerShell:
-    ///
-    ///         $ bdk-cli completions powershell >> $PROFILE
-    ///
-    /// Elvish:
-    ///
-    ///         $ bdk-cli completions elvish >> ~/.elvish/rc.elv
-    ///
-    /// After installing the completion script, restart your shell or source
-    /// the configuration file for the changes to take effect.
-    #[command(verbatim_doc_comment)]
-    Completions {
-        /// Target shell syntax
-        #[arg(value_enum)]
-        shell: Shell,
-    },
-    /// Silent payment code generation tool.
-    ///
-    /// Allows the encoding of two public keys into a silent payment code.
-    /// Useful to create silent payment transactions using fake silent payment codes.
-    #[cfg(feature = "silent-payments")]
-    SilentPaymentCode {
-        /// The scan public key to use on the silent payment code.
-        #[arg(long = "scan_key")]
-        scan: bdk_sp::bitcoin::secp256k1::PublicKey,
-        /// The spend public key to use on the silent payment code.
-        #[arg(long = "spend_key")]
-        spend: bdk_sp::bitcoin::secp256k1::PublicKey,
-    },
-}
-
-/// Wallet operation subcommands.
-#[derive(Debug, Subcommand, Clone, PartialEq)]
-pub enum WalletSubCommand {
-    /// Save wallet configuration to `config.toml`.
-    Config {
-        /// Overwrite existing wallet configuration if it exists.
-        #[arg(short = 'f', long = "force", default_value_t = false)]
-        force: bool,
-
-        #[command(flatten)]
-        wallet_opts: WalletOpts,
-    },
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "cbf",
-        feature = "rpc"
-    ))]
-    #[command(flatten)]
-    OnlineWalletSubCommand(OnlineWalletSubCommand),
-    #[command(flatten)]
-    OfflineWalletSubCommand(OfflineWalletSubCommand),
-}
-
-#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]
-pub enum DatabaseType {
-    /// Sqlite database
-    #[cfg(feature = "sqlite")]
-    Sqlite,
-    /// Redb database
-    #[cfg(feature = "redb")]
-    Redb,
-}
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "rpc",
-    feature = "cbf"
-))]
-#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]
-pub enum ClientType {
-    #[cfg(feature = "electrum")]
-    Electrum,
-    #[cfg(feature = "esplora")]
-    Esplora,
-    #[cfg(feature = "rpc")]
-    Rpc,
-    #[cfg(feature = "cbf")]
-    Cbf,
-}
-
-/// Config options wallet operations can take.
-#[derive(Debug, Args, Clone, PartialEq, Eq)]
-pub struct WalletOpts {
-    /// Selects the wallet to use.
-    #[arg(skip)]
-    pub wallet: Option<String>,
-    /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects.
-    #[arg(env = "VERBOSE", short = 'v', long = "verbose")]
-    pub verbose: bool,
-    /// Sets the descriptor to use for the external addresses.
-    #[arg(env = "EXT_DESCRIPTOR", short = 'e', long, required = true)]
-    pub ext_descriptor: String,
-    /// Sets the descriptor to use for internal/change addresses.
-    #[arg(env = "INT_DESCRIPTOR", short = 'i', long)]
-    pub int_descriptor: Option<String>,
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "rpc",
-        feature = "cbf"
-    ))]
-    #[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)]
-    pub client_type: ClientType,
-    #[cfg(any(feature = "sqlite", feature = "redb"))]
-    #[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)]
-    pub database_type: DatabaseType,
-    /// Sets the server url.
-    #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-    #[arg(env = "SERVER_URL", short = 'u', long, required = true)]
-    pub url: String,
-    /// Electrum batch size.
-    #[cfg(feature = "electrum")]
-    #[arg(env = "ELECTRUM_BATCH_SIZE", short = 'b', long, default_value = "10")]
-    pub batch_size: usize,
-    /// Esplora parallel requests.
-    #[cfg(feature = "esplora")]
-    #[arg(
-        env = "ESPLORA_PARALLEL_REQUESTS",
-        short = 'p',
-        long,
-        default_value = "5"
-    )]
-    pub parallel_requests: usize,
-    #[cfg(feature = "rpc")]
-    /// Sets the rpc basic authentication.
-    #[arg(
-        env = "USER:PASSWD",
-        short = 'a',
-        long,
-        value_parser = parse_proxy_auth,
-        default_value = "user:password",
-    )]
-    pub basic_auth: (String, String),
-    #[cfg(feature = "rpc")]
-    /// Sets an optional cookie authentication.
-    #[arg(env = "COOKIE")]
-    pub cookie: Option<String>,
-    #[cfg(feature = "cbf")]
-    #[clap(flatten)]
-    pub compactfilter_opts: CompactFilterOpts,
-}
-
-/// Options to configure a SOCKS5 proxy for a blockchain client connection.
-#[cfg(any(feature = "electrum", feature = "esplora"))]
-#[derive(Debug, Args, Clone, PartialEq, Eq)]
-pub struct ProxyOpts {
-    /// Sets the SOCKS5 proxy for a blockchain client.
-    #[arg(env = "PROXY_ADDRS:PORT", long = "proxy", short = 'p')]
-    pub proxy: Option<String>,
-
-    /// Sets the SOCKS5 proxy credential.
-    #[arg(env = "PROXY_USER:PASSWD", long="proxy_auth", short='a', value_parser = parse_proxy_auth)]
-    pub proxy_auth: Option<(String, String)>,
-
-    /// Sets the SOCKS5 proxy retries for the blockchain client.
-    #[arg(
-        env = "PROXY_RETRIES",
-        short = 'r',
-        long = "retries",
-        default_value = "5"
-    )]
-    pub retries: u8,
-
-    /// Sets the SOCKS5 proxy timeout for the blockchain client.
-    #[arg(env = "PROXY_TIMEOUT", short = 't', long = "timeout")]
-    pub timeout: Option<u8>,
-}
-
-/// Options to configure a BIP157 Compact Filter backend.
-#[cfg(feature = "cbf")]
-#[derive(Debug, Args, Clone, PartialEq, Eq)]
-pub struct CompactFilterOpts {
-    /// Sets the number of parallel node connections.
-    #[clap(name = "CONNECTIONS", long = "cbf-conn-count", default_value = "2", value_parser = value_parser!(u8).range(1..=15))]
-    pub conn_count: u8,
-}
-
-/// Wallet subcommands that can be issued without a blockchain backend.
-#[derive(Debug, Subcommand, Clone, PartialEq)]
-#[command(rename_all = "snake")]
-pub enum OfflineWalletSubCommand {
-    /// Get a new external address.
-    NewAddress,
-    /// Get the first unused external address.
-    UnusedAddress,
-    /// Lists the available spendable UTXOs.
-    Unspent,
-    /// Lists all the incoming and outgoing transactions of the wallet.
-    Transactions,
-    /// Returns the current wallet balance.
-    Balance,
-    /// Creates a new unsigned transaction.
-    CreateTx {
-        /// Adds a recipient to the transaction.
-        // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
-        // Address and amount parsing is done at run time in handler function.
-        #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
-        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')]
-        send_all: bool,
-        /// Enables Replace-By-Fee (BIP125).
-        #[arg(long = "enable_rbf", short = 'r', default_value_t = true)]
-        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")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be spent.
-        #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks a utxo as unspendable.
-        #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
-        unspendable: Option<Vec<OutPoint>>,
-        /// Fee rate to use in sat/vbyte.
-        #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
-        fee_rate: Option<f32>,
-        /// Selects which policy should be used to satisfy the external descriptor.
-        #[arg(env = "EXT_POLICY", long = "external_policy")]
-        external_policy: Option<String>,
-        /// Selects which policy should be used to satisfy the internal descriptor.
-        #[arg(env = "INT_POLICY", long = "internal_policy")]
-        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"
-        )]
-        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"
-        )]
-        add_data: Option<String>, //base 64 econding
-    },
-    /// Creates a silent payment transaction
-    ///
-    /// This sub-command is **EXPERIMENTAL** and should only be used for testing. Do not use this
-    /// feature to create transactions that spend actual funds on the Bitcoin mainnet.
-
-    // This command DOES NOT return a PSBT. Instead, it directly returns a signed transaction
-    // ready for broadcast, as it is not yet possible to perform a shared derivation of a silent
-    // payment script pubkey in a secure and trustless manner.
-    #[cfg(feature = "silent-payments")]
-    CreateSpTx {
-        /// Adds a recipient to the transaction.
-        // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
-        // Address and amount parsing is done at run time in handler function.
-        #[arg(env = "ADDRESS:SAT", long = "to", required = false, value_parser = parse_recipient)]
-        recipients: Option<Vec<(ScriptBuf, u64)>>,
-        /// Parse silent payment recipients
-        #[arg(long = "to-sp", required = true, value_parser = parse_sp_code_value_pairs)]
-        silent_payment_recipients: Vec<(SilentPaymentCode, u64)>,
-        /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
-        #[arg(long = "send_all", short = 'a')]
-        send_all: 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")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be spent.
-        #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks a utxo as unspendable.
-        #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
-        unspendable: Option<Vec<OutPoint>>,
-        /// Fee rate to use in sat/vbyte.
-        #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
-        fee_rate: Option<f32>,
-        /// Selects which policy should be used to satisfy the external descriptor.
-        #[arg(env = "EXT_POLICY", long = "external_policy")]
-        external_policy: Option<String>,
-        /// Selects which policy should be used to satisfy the internal descriptor.
-        #[arg(env = "INT_POLICY", long = "internal_policy")]
-        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"
-        )]
-        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"
-        )]
-        add_data: Option<String>, //base 64 econding
-    },
-    /// Bumps the fees of an RBF transaction.
-    BumpFee {
-        /// TXID of the transaction to update.
-        #[arg(env = "TXID", long = "txid")]
-        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)]
-        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")]
-        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)]
-        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)]
-        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"
-        )]
-        fee_rate: f32,
-    },
-    /// Returns the available spending policies for the descriptor.
-    Policies,
-    /// Returns the public version of the wallet's descriptor(s).
-    PublicDescriptor,
-    /// Signs and tries to finalize a PSBT.
-    Sign {
-        /// Sets the PSBT to sign.
-        #[arg(env = "BASE64_PSBT")]
-        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")]
-        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")]
-        trust_witness_utxo: Option<bool>,
-    },
-    /// Extracts a raw transaction from a PSBT.
-    ExtractPsbt {
-        /// Sets the PSBT to extract
-        #[arg(env = "BASE64_PSBT")]
-        psbt: String,
-    },
-    /// Finalizes a PSBT.
-    FinalizePsbt {
-        /// Sets the PSBT to finalize.
-        #[arg(env = "BASE64_PSBT")]
-        psbt: String,
-        /// Assume the blockchain has reached a specific height.
-        #[arg(env = "HEIGHT", long = "assume_height")]
-        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")]
-        trust_witness_utxo: Option<bool>,
-    },
-    /// Combines multiple PSBTs into one.
-    CombinePsbt {
-        /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT.
-        #[arg(env = "BASE64_PSBT", required = true)]
-        psbt: Vec<String>,
-    },
-    /// 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>>,
-    },
-    /// 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,
-    },
-}
-
-/// 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"
-))]
-pub enum OnlineWalletSubCommand {
-    /// Full Scan with the chosen blockchain server.
-    FullScan {
-        /// 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,
-    },
-    /// Syncs with the chosen blockchain server.
-    Sync,
-    /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract.
-    Broadcast {
-        /// 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>,
-    },
-    /// 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>,
-    },
-    /// 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,
-    },
-}
-
-/// Subcommands for Key operations.
-#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
-pub enum KeySubCommand {
-    /// Generates new random seed mnemonic phrase and corresponding master extended key.
-    Generate {
-        /// Entropy level based on number of random seed mnemonic words.
-        #[arg(
-            env = "WORD_COUNT",
-            short = 'e',
-            long = "entropy",
-            default_value = "12"
-        )]
-        word_count: usize,
-        /// Seed password.
-        #[arg(env = "PASSWORD", short = 'p', long = "password")]
-        password: Option<String>,
-    },
-    /// Restore a master extended key from seed backup mnemonic words.
-    Restore {
-        /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...").
-        #[arg(env = "MNEMONIC", short = 'm', long = "mnemonic")]
-        mnemonic: String,
-        /// Seed password.
-        #[arg(env = "PASSWORD", short = 'p', long = "password")]
-        password: Option<String>,
-    },
-    /// Derive a child key pair from a master extended key and a derivation path string (eg. "m/84'/1'/0'/0" or "m/84h/1h/0h/0").
-    Derive {
-        /// Extended private key to derive from.
-        #[arg(env = "XPRV", short = 'x', long = "xprv")]
-        xprv: Xpriv,
-        /// Path to use to derive extended public key from extended private key.
-        #[arg(env = "PATH", short = 'p', long = "path")]
-        path: DerivationPath,
-    },
-}
-
-/// Subcommands available in REPL mode.
-#[cfg(any(feature = "repl", target_arch = "wasm32"))]
-#[derive(Debug, Parser)]
-#[command(rename_all = "lower", multicall = true)]
-pub enum ReplSubCommand {
-    /// Execute wallet commands.
-    Wallet {
-        #[command(subcommand)]
-        subcommand: WalletSubCommand,
-    },
-    /// Execute key commands.
-    Key {
-        #[command(subcommand)]
-        subcommand: KeySubCommand,
-    },
-    /// Generate descriptors
-    Descriptor {
-        /// Descriptor type (script type).
-        #[arg(
-            long = "type",
-            short = 't',
-            value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"],
-            default_value = "wsh"
-        )]
-        desc_type: String,
-        /// Optional key: xprv, xpub, or mnemonic phrase
-        key: Option<String>,
-    },
-    /// Exit REPL loop.
-    Exit,
-}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644 (file)
index 0000000..197dc07
--- /dev/null
@@ -0,0 +1,302 @@
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
+use crate::commands::ClientType;
+#[cfg(feature = "sqlite")]
+use crate::commands::DatabaseType;
+use crate::commands::WalletOpts;
+use crate::error::BDKCliError as Error;
+use bdk_wallet::bitcoin::Network;
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use clap::ValueEnum;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+use std::str::FromStr;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct WalletConfig {
+    pub wallets: HashMap<String, WalletConfigInner>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct WalletConfigInner {
+    pub wallet: String,
+    pub network: String,
+    pub ext_descriptor: String,
+    pub int_descriptor: Option<String>,
+    #[cfg(any(feature = "sqlite", feature = "redb"))]
+    pub database_type: String,
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "rpc",
+        feature = "cbf"
+    ))]
+    pub client_type: Option<String>,
+    #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+    pub server_url: Option<String>,
+    #[cfg(feature = "rpc")]
+    pub rpc_user: Option<String>,
+    #[cfg(feature = "rpc")]
+    pub rpc_password: Option<String>,
+    #[cfg(feature = "electrum")]
+    pub batch_size: Option<usize>,
+    #[cfg(feature = "esplora")]
+    pub parallel_requests: Option<usize>,
+    #[cfg(feature = "rpc")]
+    pub cookie: Option<String>,
+}
+
+impl WalletConfig {
+    /// Load configuration from a TOML file in the wallet's data directory
+    pub fn load(datadir: &Path) -> Result<Option<WalletConfig>, Error> {
+        let config_path = datadir.join("config.toml");
+        if !config_path.exists() {
+            return Ok(None);
+        }
+        let config_content = fs::read_to_string(&config_path)
+            .map_err(|e| Error::Generic(format!("Failed to read config file: {e}")))?;
+        let config: WalletConfig = toml::from_str(&config_content)
+            .map_err(|e| Error::Generic(format!("Failed to parse config file: {e}")))?;
+        Ok(Some(config))
+    }
+
+    /// Save configuration to a TOML file
+    pub fn save(&self, datadir: &Path) -> Result<(), Error> {
+        let config_path = datadir.join("config.toml");
+        let config_content = toml::to_string_pretty(self)
+            .map_err(|e| Error::Generic(format!("Failed to serialize config: {e}")))?;
+        fs::create_dir_all(datadir)
+            .map_err(|e| Error::Generic(format!("Failed to create directory {datadir:?}: {e}")))?;
+        fs::write(&config_path, config_content).map_err(|e| {
+            Error::Generic(format!("Failed to write config file {config_path:?}: {e}"))
+        })?;
+        log::debug!("Saved config to {config_path:?}");
+        Ok(())
+    }
+
+    /// Get config for a wallet
+    pub fn get_wallet_opts(&self, wallet_name: &str) -> Result<WalletOpts, Error> {
+        self.wallets
+            .get(wallet_name)
+            .ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))?
+            .try_into()
+    }
+}
+
+impl TryFrom<&WalletConfigInner> for WalletOpts {
+    type Error = Error;
+
+    fn try_from(config: &WalletConfigInner) -> Result<Self, Self::Error> {
+        let _network = Network::from_str(&config.network)
+            .map_err(|_| Error::Generic("Invalid network".to_string()))?;
+
+        #[cfg(any(feature = "sqlite", feature = "redb"))]
+        let database_type = DatabaseType::from_str(&config.database_type, true)
+            .map_err(|_| Error::Generic("Invalid database type".to_string()))?;
+
+        #[cfg(any(
+            feature = "electrum",
+            feature = "esplora",
+            feature = "rpc",
+            feature = "cbf"
+        ))]
+        let client_type = config
+            .client_type
+            .as_deref()
+            .ok_or_else(|| Error::Generic("Client type missing".into()))
+            .and_then(|s| {
+                ClientType::from_str(s, true)
+                    .map_err(|_| Error::Generic("Invalid client type".into()))
+            })?;
+
+        Ok(WalletOpts {
+            wallet: Some(config.wallet.clone()),
+            verbose: false,
+            ext_descriptor: config.ext_descriptor.clone(),
+            int_descriptor: config.int_descriptor.clone(),
+
+            #[cfg(any(
+                feature = "electrum",
+                feature = "esplora",
+                feature = "rpc",
+                feature = "cbf"
+            ))]
+            client_type,
+
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            database_type,
+
+            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+            url: config
+                .server_url
+                .clone()
+                .ok_or_else(|| Error::Generic("Server url not found".into()))?,
+
+            #[cfg(feature = "electrum")]
+            batch_size: config.batch_size.unwrap_or(10),
+
+            #[cfg(feature = "esplora")]
+            parallel_requests: config.parallel_requests.unwrap_or(5),
+
+            #[cfg(feature = "rpc")]
+            basic_auth: (
+                config.rpc_user.clone().unwrap_or_else(|| "user".into()),
+                config
+                    .rpc_password
+                    .clone()
+                    .unwrap_or_else(|| "password".into()),
+            ),
+
+            #[cfg(feature = "rpc")]
+            cookie: config.cookie.clone(),
+
+            #[cfg(feature = "cbf")]
+            compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 },
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::convert::TryInto;
+    const EXT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg";
+    const INT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts";
+
+    #[test]
+    fn test_wallet_config_inner_to_opts_conversion() {
+        #[cfg(any(
+            feature = "electrum",
+            feature = "esplora",
+            feature = "rpc",
+            feature = "cbf"
+        ))]
+        let client_type = {
+            if cfg!(feature = "esplora") {
+                Some("esplora".to_string())
+            } else if cfg!(feature = "rpc") {
+                Some("rpc".to_string())
+            } else if cfg!(feature = "electrum") {
+                Some("electrum".to_string())
+            } else if cfg!(feature = "cbf") {
+                Some("cbf".to_string())
+            } else {
+                None
+            }
+        };
+
+        let wallet_config = WalletConfigInner {
+            wallet: "test_wallet".to_string(),
+            network: "testnet4".to_string(),
+            ext_descriptor: EXT_DESCRIPTOR.to_string(),
+            int_descriptor: Some(INT_DESCRIPTOR.to_string()),
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            database_type: "sqlite".to_string(),
+
+            #[cfg(any(
+                feature = "electrum",
+                feature = "esplora",
+                feature = "rpc",
+                feature = "cbf"
+            ))]
+            client_type,
+
+            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+            server_url: Some("https://example.com/testnet/api".to_string()),
+            #[cfg(feature = "electrum")]
+            batch_size: None,
+            #[cfg(feature = "esplora")]
+            parallel_requests: None,
+            #[cfg(feature = "rpc")]
+            rpc_user: None,
+            #[cfg(feature = "rpc")]
+            rpc_password: None,
+            #[cfg(feature = "rpc")]
+            cookie: None,
+        };
+
+        let opts: WalletOpts = (&wallet_config)
+            .try_into()
+            .expect("Conversion should succeed");
+
+        assert_eq!(opts.wallet, Some("test_wallet".to_string()));
+
+        #[cfg(all(
+            feature = "esplora",
+            not(any(feature = "electrum", feature = "rpc", feature = "cbf"))
+        ))]
+        assert_eq!(opts.client_type, ClientType::Esplora);
+
+        #[cfg(all(
+            feature = "rpc",
+            not(any(feature = "esplora", feature = "electrum", feature = "cbf"))
+        ))]
+        assert_eq!(opts.client_type, ClientType::Rpc);
+
+        #[cfg(all(feature = "electrum", not(any(feature = "esplora", feature = "rpc"))))]
+        assert_eq!(opts.client_type, ClientType::Electrum);
+
+        #[cfg(all(
+            feature = "cbf",
+            not(any(feature = "esplora", feature = "rpc", feature = "electrum"))
+        ))]
+        assert_eq!(opts.client_type, ClientType::Cbf);
+
+        #[cfg(feature = "sqlite")]
+        assert_eq!(opts.database_type, DatabaseType::Sqlite);
+
+        assert_eq!(opts.ext_descriptor, EXT_DESCRIPTOR);
+
+        #[cfg(feature = "electrum")]
+        assert_eq!(opts.batch_size, 10);
+
+        #[cfg(feature = "esplora")]
+        assert_eq!(opts.parallel_requests, 5);
+    }
+
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "rpc",
+        feature = "cbf"
+    ))]
+    #[test]
+    fn test_invalid_client_type_fails() {
+        let inner = WalletConfigInner {
+            wallet: "test".to_string(),
+            network: "regtest".to_string(),
+            ext_descriptor: "desc".to_string(),
+            int_descriptor: None,
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            database_type: "sqlite".to_string(),
+            #[cfg(any(
+                feature = "electrum",
+                feature = "esplora",
+                feature = "rpc",
+                feature = "cbf"
+            ))]
+            client_type: Some("invalid_backend".to_string()),
+            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+            server_url: Some("url".to_string()),
+            #[cfg(feature = "electrum")]
+            batch_size: None,
+            #[cfg(feature = "esplora")]
+            parallel_requests: None,
+            #[cfg(feature = "rpc")]
+            rpc_user: None,
+            #[cfg(feature = "rpc")]
+            rpc_password: None,
+            #[cfg(feature = "rpc")]
+            cookie: None,
+        };
+
+        let result: Result<WalletOpts, Error> = (&inner).try_into();
+        assert!(result.is_err());
+    }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
deleted file mode 100644 (file)
index 197dc07..0000000
+++ /dev/null
@@ -1,302 +0,0 @@
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "rpc",
-    feature = "cbf"
-))]
-use crate::commands::ClientType;
-#[cfg(feature = "sqlite")]
-use crate::commands::DatabaseType;
-use crate::commands::WalletOpts;
-use crate::error::BDKCliError as Error;
-use bdk_wallet::bitcoin::Network;
-#[cfg(any(feature = "sqlite", feature = "redb"))]
-use clap::ValueEnum;
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-use std::fs;
-use std::path::Path;
-use std::str::FromStr;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct WalletConfig {
-    pub wallets: HashMap<String, WalletConfigInner>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct WalletConfigInner {
-    pub wallet: String,
-    pub network: String,
-    pub ext_descriptor: String,
-    pub int_descriptor: Option<String>,
-    #[cfg(any(feature = "sqlite", feature = "redb"))]
-    pub database_type: String,
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "rpc",
-        feature = "cbf"
-    ))]
-    pub client_type: Option<String>,
-    #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-    pub server_url: Option<String>,
-    #[cfg(feature = "rpc")]
-    pub rpc_user: Option<String>,
-    #[cfg(feature = "rpc")]
-    pub rpc_password: Option<String>,
-    #[cfg(feature = "electrum")]
-    pub batch_size: Option<usize>,
-    #[cfg(feature = "esplora")]
-    pub parallel_requests: Option<usize>,
-    #[cfg(feature = "rpc")]
-    pub cookie: Option<String>,
-}
-
-impl WalletConfig {
-    /// Load configuration from a TOML file in the wallet's data directory
-    pub fn load(datadir: &Path) -> Result<Option<WalletConfig>, Error> {
-        let config_path = datadir.join("config.toml");
-        if !config_path.exists() {
-            return Ok(None);
-        }
-        let config_content = fs::read_to_string(&config_path)
-            .map_err(|e| Error::Generic(format!("Failed to read config file: {e}")))?;
-        let config: WalletConfig = toml::from_str(&config_content)
-            .map_err(|e| Error::Generic(format!("Failed to parse config file: {e}")))?;
-        Ok(Some(config))
-    }
-
-    /// Save configuration to a TOML file
-    pub fn save(&self, datadir: &Path) -> Result<(), Error> {
-        let config_path = datadir.join("config.toml");
-        let config_content = toml::to_string_pretty(self)
-            .map_err(|e| Error::Generic(format!("Failed to serialize config: {e}")))?;
-        fs::create_dir_all(datadir)
-            .map_err(|e| Error::Generic(format!("Failed to create directory {datadir:?}: {e}")))?;
-        fs::write(&config_path, config_content).map_err(|e| {
-            Error::Generic(format!("Failed to write config file {config_path:?}: {e}"))
-        })?;
-        log::debug!("Saved config to {config_path:?}");
-        Ok(())
-    }
-
-    /// Get config for a wallet
-    pub fn get_wallet_opts(&self, wallet_name: &str) -> Result<WalletOpts, Error> {
-        self.wallets
-            .get(wallet_name)
-            .ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))?
-            .try_into()
-    }
-}
-
-impl TryFrom<&WalletConfigInner> for WalletOpts {
-    type Error = Error;
-
-    fn try_from(config: &WalletConfigInner) -> Result<Self, Self::Error> {
-        let _network = Network::from_str(&config.network)
-            .map_err(|_| Error::Generic("Invalid network".to_string()))?;
-
-        #[cfg(any(feature = "sqlite", feature = "redb"))]
-        let database_type = DatabaseType::from_str(&config.database_type, true)
-            .map_err(|_| Error::Generic("Invalid database type".to_string()))?;
-
-        #[cfg(any(
-            feature = "electrum",
-            feature = "esplora",
-            feature = "rpc",
-            feature = "cbf"
-        ))]
-        let client_type = config
-            .client_type
-            .as_deref()
-            .ok_or_else(|| Error::Generic("Client type missing".into()))
-            .and_then(|s| {
-                ClientType::from_str(s, true)
-                    .map_err(|_| Error::Generic("Invalid client type".into()))
-            })?;
-
-        Ok(WalletOpts {
-            wallet: Some(config.wallet.clone()),
-            verbose: false,
-            ext_descriptor: config.ext_descriptor.clone(),
-            int_descriptor: config.int_descriptor.clone(),
-
-            #[cfg(any(
-                feature = "electrum",
-                feature = "esplora",
-                feature = "rpc",
-                feature = "cbf"
-            ))]
-            client_type,
-
-            #[cfg(any(feature = "sqlite", feature = "redb"))]
-            database_type,
-
-            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-            url: config
-                .server_url
-                .clone()
-                .ok_or_else(|| Error::Generic("Server url not found".into()))?,
-
-            #[cfg(feature = "electrum")]
-            batch_size: config.batch_size.unwrap_or(10),
-
-            #[cfg(feature = "esplora")]
-            parallel_requests: config.parallel_requests.unwrap_or(5),
-
-            #[cfg(feature = "rpc")]
-            basic_auth: (
-                config.rpc_user.clone().unwrap_or_else(|| "user".into()),
-                config
-                    .rpc_password
-                    .clone()
-                    .unwrap_or_else(|| "password".into()),
-            ),
-
-            #[cfg(feature = "rpc")]
-            cookie: config.cookie.clone(),
-
-            #[cfg(feature = "cbf")]
-            compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 },
-        })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::convert::TryInto;
-    const EXT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg";
-    const INT_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts";
-
-    #[test]
-    fn test_wallet_config_inner_to_opts_conversion() {
-        #[cfg(any(
-            feature = "electrum",
-            feature = "esplora",
-            feature = "rpc",
-            feature = "cbf"
-        ))]
-        let client_type = {
-            if cfg!(feature = "esplora") {
-                Some("esplora".to_string())
-            } else if cfg!(feature = "rpc") {
-                Some("rpc".to_string())
-            } else if cfg!(feature = "electrum") {
-                Some("electrum".to_string())
-            } else if cfg!(feature = "cbf") {
-                Some("cbf".to_string())
-            } else {
-                None
-            }
-        };
-
-        let wallet_config = WalletConfigInner {
-            wallet: "test_wallet".to_string(),
-            network: "testnet4".to_string(),
-            ext_descriptor: EXT_DESCRIPTOR.to_string(),
-            int_descriptor: Some(INT_DESCRIPTOR.to_string()),
-            #[cfg(any(feature = "sqlite", feature = "redb"))]
-            database_type: "sqlite".to_string(),
-
-            #[cfg(any(
-                feature = "electrum",
-                feature = "esplora",
-                feature = "rpc",
-                feature = "cbf"
-            ))]
-            client_type,
-
-            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-            server_url: Some("https://example.com/testnet/api".to_string()),
-            #[cfg(feature = "electrum")]
-            batch_size: None,
-            #[cfg(feature = "esplora")]
-            parallel_requests: None,
-            #[cfg(feature = "rpc")]
-            rpc_user: None,
-            #[cfg(feature = "rpc")]
-            rpc_password: None,
-            #[cfg(feature = "rpc")]
-            cookie: None,
-        };
-
-        let opts: WalletOpts = (&wallet_config)
-            .try_into()
-            .expect("Conversion should succeed");
-
-        assert_eq!(opts.wallet, Some("test_wallet".to_string()));
-
-        #[cfg(all(
-            feature = "esplora",
-            not(any(feature = "electrum", feature = "rpc", feature = "cbf"))
-        ))]
-        assert_eq!(opts.client_type, ClientType::Esplora);
-
-        #[cfg(all(
-            feature = "rpc",
-            not(any(feature = "esplora", feature = "electrum", feature = "cbf"))
-        ))]
-        assert_eq!(opts.client_type, ClientType::Rpc);
-
-        #[cfg(all(feature = "electrum", not(any(feature = "esplora", feature = "rpc"))))]
-        assert_eq!(opts.client_type, ClientType::Electrum);
-
-        #[cfg(all(
-            feature = "cbf",
-            not(any(feature = "esplora", feature = "rpc", feature = "electrum"))
-        ))]
-        assert_eq!(opts.client_type, ClientType::Cbf);
-
-        #[cfg(feature = "sqlite")]
-        assert_eq!(opts.database_type, DatabaseType::Sqlite);
-
-        assert_eq!(opts.ext_descriptor, EXT_DESCRIPTOR);
-
-        #[cfg(feature = "electrum")]
-        assert_eq!(opts.batch_size, 10);
-
-        #[cfg(feature = "esplora")]
-        assert_eq!(opts.parallel_requests, 5);
-    }
-
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "rpc",
-        feature = "cbf"
-    ))]
-    #[test]
-    fn test_invalid_client_type_fails() {
-        let inner = WalletConfigInner {
-            wallet: "test".to_string(),
-            network: "regtest".to_string(),
-            ext_descriptor: "desc".to_string(),
-            int_descriptor: None,
-            #[cfg(any(feature = "sqlite", feature = "redb"))]
-            database_type: "sqlite".to_string(),
-            #[cfg(any(
-                feature = "electrum",
-                feature = "esplora",
-                feature = "rpc",
-                feature = "cbf"
-            ))]
-            client_type: Some("invalid_backend".to_string()),
-            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
-            server_url: Some("url".to_string()),
-            #[cfg(feature = "electrum")]
-            batch_size: None,
-            #[cfg(feature = "esplora")]
-            parallel_requests: None,
-            #[cfg(feature = "rpc")]
-            rpc_user: None,
-            #[cfg(feature = "rpc")]
-            rpc_password: None,
-            #[cfg(feature = "rpc")]
-            cookie: None,
-        };
-
-        let result: Result<WalletOpts, Error> = (&inner).try_into();
-        assert!(result.is_err());
-    }
-}
index 1d5fd82f19c40fa839d06a06e3e945e0dbe65dab..3019e5caae3ad3064294458ed32dc785dc44df0c 100644 (file)
@@ -61,7 +61,7 @@ pub enum BDKCliError {
     #[error("PsbtError: {0}")]
     PsbtError(#[from] bdk_wallet::bitcoin::psbt::Error),
 
-    #[cfg(feature = "sqlite")]
+    // #[cfg(feature = "sqlite")]
     #[error("Rusqlite error: {0}")]
     RusqliteError(Box<bdk_wallet::rusqlite::Error>),
 
index dacd5783b303fce16e6ec76d43dc8a17889dfc10..f6f03d45d444e30ffe79ecb4765faaed5cec703b 100644 (file)
@@ -490,3 +490,84 @@ impl FormatOutput for DescriptorResult {
         simple_table(rows, None)
     }
 }
+
+#[derive(Serialize, Debug, Default)]
+pub struct MessageResult {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub proof: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub valid: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub proven_amount: Option<u64>,
+}
+
+impl FormatOutput for MessageResult {
+    fn to_table(&self) -> Result<String, Error> {
+        let mut rows = vec![];
+
+        if let Some(proof) = &self.proof {
+            rows.push(vec!["Proof".cell().bold(true), proof.cell()]);
+        }
+        if let Some(valid) = self.valid {
+            rows.push(vec!["Is Valid".cell().bold(true), valid.to_string().cell()]);
+        }
+        if let Some(amount) = self.proven_amount {
+            rows.push(vec![
+                "Proven Amount (sats)".cell().bold(true),
+                amount.cell(),
+            ]);
+        }
+
+        let title = vec!["Property".cell().bold(true), "Value".cell().bold(true)];
+
+        simple_table(rows, Some(title))
+    }
+}
+
+#[derive(Serialize, Debug)]
+pub struct StatusResult {
+    pub message: String,
+}
+
+impl StatusResult {
+    pub fn new(msg: &str) -> Self {
+        Self {
+            message: msg.to_string(),
+        }
+    }
+}
+
+impl FormatOutput for StatusResult {
+    fn to_table(&self) -> Result<String, Error> {
+        Ok(self.message.clone())
+    }
+}
+
+#[derive(Serialize, Debug)]
+pub struct TransactionResult {
+    pub txid: String,
+}
+
+impl FormatOutput for TransactionResult {
+    fn to_table(&self) -> Result<String, Error> {
+        simple_table(
+            vec![vec!["TXID".cell().bold(true), self.txid.clone().cell()]],
+            None,
+        )
+    }
+}
+
+#[derive(Serialize, Debug)]
+pub struct ConfigResult {
+    pub wallet_name: String,
+    pub path: String,
+    pub message: String,
+}
+
+impl FormatOutput for ConfigResult {
+    fn to_table(&self) -> Result<String, Error> {
+        Ok(format!("{} saved to {}", self.message, self.path))
+    }
+}
diff --git a/src/handlers/wallets.rs b/src/handlers/wallets.rs
deleted file mode 100644 (file)
index fdfdff2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-use crate::error::BDKCliError as Error;
-use crate::utils::output::FormatOutput;
-use crate::{config::WalletConfig, handlers::types::WalletsListResult};
-use std::path::Path;
-
-/// Handle the top-level `wallets` command (lists all saved wallets)
-pub fn handle_wallets_subcommand(home_dir: &Path, pretty: bool) -> Result<String, Error> {
-    let config = match WalletConfig::load(home_dir)? {
-        Some(cfg) => cfg,
-        None => return Ok("No wallets configured yet.".to_string()),
-    };
-
-    let result = WalletsListResult(config.wallets);
-    result.format(pretty)
-}
index 30a6774102cb9ca968bd97ed5552585ccbd58dee..894508b575d64707357894c0494f914aa7086cc0 100644 (file)
@@ -5,10 +5,7 @@
     feature = "cbf"
 ))]
 
-use crate::{
-    backend::BlockchainClient,
-    handlers::online::{broadcast_transaction, sync_wallet},
-};
+use crate::client::BlockchainClient;
 use bdk_wallet::{
     SignOptions, Wallet,
     bitcoin::{FeeRate, Psbt, Txid, consensus::encode::serialize_hex},
@@ -120,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,
@@ -612,7 +609,7 @@ impl<'a> PayjoinManager<'a> {
             let mut sync_timer = tokio::time::interval(sync_interval);
             poll_timer.tick().await;
             sync_timer.tick().await;
-            sync_wallet(blockchain_client, self.wallet).await?;
+            blockchain_client.sync_wallet(self.wallet).await?;
 
             loop {
                 tokio::select! {
@@ -653,7 +650,7 @@ impl<'a> PayjoinManager<'a> {
                     }
                     _ = sync_timer.tick() => {
                         // Time to sync wallet
-                        sync_wallet(blockchain_client, self.wallet).await?;
+                        blockchain_client.sync_wallet(self.wallet).await?;
                     }
                 }
             }
@@ -809,7 +806,9 @@ impl<'a> PayjoinManager<'a> {
             ));
         }
 
-        broadcast_transaction(blockchain_client, psbt.extract_tx_fee_rate_limit()?).await
+        blockchain_client
+            .broadcast(psbt.extract_tx_fee_rate_limit()?)
+            .await
     }
 
     async fn send_payjoin_post_request(
index 03e62db35512efffb48c262cb2731e6c49ae3d6a..2cfb985693013a1f36839f7a21a9e69232c1f1af 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();