+++ /dev/null
-#[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(())
-}
-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())
/// 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)?;
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 {
}
}
- // #[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()),
}
}
- // #[cfg(feature = "cbf")]
+ #[cfg(feature = "cbf")]
ClientType::Cbf => {
let scan_type = bdk_kyoto::ScanType::Sync;
let builder = bdk_kyoto::builder::Builder::new(_wallet.network());
}
// Handle Kyoto Client sync
-// #[cfg(feature = "cbf")]
+#[cfg(feature = "cbf")]
pub async fn sync_kyoto_client(
wallet: &mut Wallet,
handle: &KyotoClientHandle,
+++ /dev/null
-// 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,
-}
--- /dev/null
+#[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());
+ }
+}
+++ /dev/null
-#[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());
- }
-}
#[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>),
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))
+ }
+}
+++ /dev/null
-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)
-}
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},
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,
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! {
}
_ = sync_timer.tick() => {
// Time to sync wallet
- sync_wallet(blockchain_client, self.wallet).await?;
+ blockchain_client.sync_wallet(self.wallet).await?;
}
}
}
));
}
- 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(
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();