From: Vihiga Tyonum Date: Thu, 14 May 2026 10:13:27 +0000 (+0100) Subject: update namespace and update types X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/consensus/bitcoin/struct.ScriptHash.html?a=commitdiff_plain;h=7a0e68bd3379cfe880cc7d25f57e7aaccc1e0ed4;p=bdk-cli update namespace and update types --- diff --git a/src/backend/mod.rs b/src/backend/mod.rs deleted file mode 100644 index a4695b7..0000000 --- a/src/backend/mod.rs +++ /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>, - batch_size: usize, - }, - #[cfg(feature = "esplora")] - Esplora { - client: Box, - parallel_requests: usize, - }, - #[cfg(feature = "rpc")] - RpcClient { - client: Box, - }, - - #[cfg(feature = "cbf")] - KyotoClient { client: Box }, -} - -/// 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, -} - -#[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 { - #[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(()) -} diff --git a/src/client.rs b/src/client.rs index 2b97330..6c02441 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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>, batch_size: usize, }, - // #[cfg(feature = "esplora")] + #[cfg(feature = "esplora")] Esplora { client: Box, parallel_requests: usize, }, - // #[cfg(feature = "rpc")] + #[cfg(feature = "rpc")] RpcClient { client: Box, }, - // #[cfg(feature = "cbf")] + #[cfg(feature = "cbf")] KyotoClient { client: Box, }, } +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" +))] impl BlockchainClient { pub async fn broadcast(&self, tx: Transaction) -> Result { 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, } -// #[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 { - // #[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 index c8fb1a0..0000000 --- a/src/commands/mod.rs +++ /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 or the MIT license -// , 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 -#[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, - /// 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, - }, - /// 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, - /// 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, - #[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, - #[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, - - /// 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, -} - -/// 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>, - /// Marks a utxo as unspendable. - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] - unspendable: Option>, - /// Fee rate to use in sat/vbyte. - #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] - fee_rate: Option, - /// Selects which policy should be used to satisfy the external descriptor. - #[arg(env = "EXT_POLICY", long = "external_policy")] - external_policy: Option, - /// Selects which policy should be used to satisfy the internal descriptor. - #[arg(env = "INT_POLICY", long = "internal_policy")] - internal_policy: Option, - /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes) - #[arg( - env = "ADD_STRING", - long = "add_string", - short = 's', - conflicts_with = "add_data" - )] - add_string: Option, - /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes) - #[arg( - env = "ADD_DATA", - long = "add_data", - short = 'o', - conflicts_with = "add_string" - )] - add_data: Option, //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>, - /// 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>, - /// Marks a utxo as unspendable. - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] - unspendable: Option>, - /// Fee rate to use in sat/vbyte. - #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] - fee_rate: Option, - /// Selects which policy should be used to satisfy the external descriptor. - #[arg(env = "EXT_POLICY", long = "external_policy")] - external_policy: Option, - /// Selects which policy should be used to satisfy the internal descriptor. - #[arg(env = "INT_POLICY", long = "internal_policy")] - internal_policy: Option, - /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes) - #[arg( - env = "ADD_STRING", - long = "add_string", - short = 's', - conflicts_with = "add_data" - )] - add_string: Option, - /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes) - #[arg( - env = "ADD_DATA", - long = "add_data", - short = 'o', - conflicts_with = "add_string" - )] - add_data: Option, //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
, - /// 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>, - /// 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>, - /// 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, - /// 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, - }, - /// 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, - /// 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, - }, - /// 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, - }, - /// 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>, - }, - /// 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, - /// Sets the raw transaction to broadcast. - #[arg( - env = "RAWTX", - long = "tx", - required_unless_present = "psbt", - conflicts_with = "psbt" - )] - tx: Option, - }, - /// Generates a Payjoin receive URI and processes the sender's Payjoin proposal. - ReceivePayjoin { - /// Amount to be received in sats. - #[arg(env = "PAYJOIN_AMOUNT", long = "amount", required = true)] - amount: u64, - /// Payjoin directory which will be used to store the PSBTs which are pending action - /// from one of the parties. - #[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)] - directory: String, - /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the - /// operation with multiple relays for redundancy. - #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] - ohttp_relay: Vec, - /// Maximum effective fee rate the receiver is willing to pay for their own input/output contributions. - #[arg(env = "PAYJOIN_RECEIVER_MAX_FEE_RATE", long = "max_fee_rate")] - max_fee_rate: Option, - }, - /// Sends an original PSBT to a BIP 21 URI and broadcasts the returned Payjoin PSBT. - SendPayjoin { - /// BIP 21 URI for the Payjoin. - #[arg(env = "PAYJOIN_URI", long = "uri", required = true)] - uri: String, - /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the - /// operation with multiple relays for redundancy. - #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)] - ohttp_relay: Vec, - /// Fee rate to use in sat/vbyte. - #[arg( - env = "PAYJOIN_SENDER_FEE_RATE", - short = 'f', - long = "fee_rate", - required = true - )] - fee_rate: u64, - }, -} - -/// 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, - }, - /// 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, - }, - /// 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, - }, - /// Exit REPL loop. - Exit, -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..197dc07 --- /dev/null +++ b/src/config.rs @@ -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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WalletConfigInner { + pub wallet: String, + pub network: String, + pub ext_descriptor: String, + pub int_descriptor: Option, + #[cfg(any(feature = "sqlite", feature = "redb"))] + pub database_type: String, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + pub client_type: Option, + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + pub server_url: Option, + #[cfg(feature = "rpc")] + pub rpc_user: Option, + #[cfg(feature = "rpc")] + pub rpc_password: Option, + #[cfg(feature = "electrum")] + pub batch_size: Option, + #[cfg(feature = "esplora")] + pub parallel_requests: Option, + #[cfg(feature = "rpc")] + pub cookie: Option, +} + +impl WalletConfig { + /// Load configuration from a TOML file in the wallet's data directory + pub fn load(datadir: &Path) -> Result, 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 { + 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 { + 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 = (&inner).try_into(); + assert!(result.is_err()); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index 197dc07..0000000 --- a/src/config/mod.rs +++ /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, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WalletConfigInner { - pub wallet: String, - pub network: String, - pub ext_descriptor: String, - pub int_descriptor: Option, - #[cfg(any(feature = "sqlite", feature = "redb"))] - pub database_type: String, - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - pub client_type: Option, - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] - pub server_url: Option, - #[cfg(feature = "rpc")] - pub rpc_user: Option, - #[cfg(feature = "rpc")] - pub rpc_password: Option, - #[cfg(feature = "electrum")] - pub batch_size: Option, - #[cfg(feature = "esplora")] - pub parallel_requests: Option, - #[cfg(feature = "rpc")] - pub cookie: Option, -} - -impl WalletConfig { - /// Load configuration from a TOML file in the wallet's data directory - pub fn load(datadir: &Path) -> Result, 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 { - 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 { - 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 = (&inner).try_into(); - assert!(result.is_err()); - } -} diff --git a/src/error.rs b/src/error.rs index 1d5fd82..3019e5c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), diff --git a/src/handlers/types.rs b/src/handlers/types.rs index dacd578..f6f03d4 100644 --- a/src/handlers/types.rs +++ b/src/handlers/types.rs @@ -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, + + #[serde(skip_serializing_if = "Option::is_none")] + pub valid: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub proven_amount: Option, +} + +impl FormatOutput for MessageResult { + fn to_table(&self) -> Result { + 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 { + Ok(self.message.clone()) + } +} + +#[derive(Serialize, Debug)] +pub struct TransactionResult { + pub txid: String, +} + +impl FormatOutput for TransactionResult { + fn to_table(&self) -> Result { + 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 { + Ok(format!("{} saved to {}", self.message, self.path)) + } +} diff --git a/src/handlers/wallets.rs b/src/handlers/wallets.rs deleted file mode 100644 index fdfdff2..0000000 --- a/src/handlers/wallets.rs +++ /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 { - 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) -} diff --git a/src/payjoin/mod.rs b/src/payjoin/mod.rs index 30a6774..894508b 100644 --- a/src/payjoin/mod.rs +++ b/src/payjoin/mod.rs @@ -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( diff --git a/src/utils/common.rs b/src/utils/common.rs index 03e62db..2cfb985 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -76,7 +76,7 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { Ok((addr.script_pubkey(), val)) } -#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] +// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] /// Parse the proxy (Socket:Port) argument from the cli input. pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> { let parts: Vec<_> = s.split(':').collect();