+++ /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.
-
-//! Utility Tools
-//!
-//! This module includes all the utility tools used by the App.
-use crate::config::WalletConfig;
-use crate::error::BDKCliError as Error;
-use std::{
- fmt::Display,
- path::{Path, PathBuf},
- str::FromStr,
- sync::Arc,
-};
-
-use crate::commands::WalletOpts;
-#[cfg(feature = "cbf")]
-use bdk_kyoto::{
- BuilderExt, Info, LightClient, Receiver, ScanType::Sync, UnboundedReceiver, Warning,
- builder::Builder,
-};
-#[cfg(feature = "silent-payments")]
-use bdk_sp::encoding::SilentPaymentCode;
-use bdk_wallet::{
- KeychainKind,
- bitcoin::bip32::{DerivationPath, Xpub},
- keys::DescriptorPublicKey,
- miniscript::{
- Descriptor, Miniscript, Terminal,
- descriptor::{DescriptorXKey, Wildcard},
- },
- template::DescriptorTemplate,
-};
-use cli_table::{Cell, CellStruct, Style, Table};
-
-#[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
-))]
-use crate::commands::ClientType;
-
-use bdk_wallet::Wallet;
-#[cfg(any(feature = "sqlite", feature = "redb"))]
-use bdk_wallet::{PersistedWallet, WalletPersister};
-
-use bdk_wallet::bip39::{Language, Mnemonic};
-use bdk_wallet::bitcoin::{
- Address, Network, OutPoint, ScriptBuf, bip32::Xpriv, secp256k1::Secp256k1,
-};
-use bdk_wallet::descriptor::Segwitv0;
-use bdk_wallet::keys::{GeneratableKey, GeneratedKey, bip39::WordCount};
-use serde_json::{Value, json};
-
-#[cfg(feature = "bip322")]
-use bdk_bip322::SignatureFormat;
-
-/// Parse the recipient (Address,Amount) argument from cli input.
-pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> {
- let parts: Vec<_> = s.split(':').collect();
- if parts.len() != 2 {
- return Err("Invalid format".to_string());
- }
- let addr = Address::from_str(parts[0])
- .map_err(|e| e.to_string())?
- .assume_checked();
- let val = u64::from_str(parts[1]).map_err(|e| e.to_string())?;
-
- Ok((addr.script_pubkey(), val))
-}
-
-#[cfg(feature = "silent-payments")]
-pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), Error> {
- let parts: Vec<&str> = s.split(':').collect();
- if parts.len() != 2 {
- return Err(Error::Generic(format!(
- "Invalid format '{}'. Expected 'key:value'",
- s
- )));
- }
-
- let value_0 = parts[0].trim();
- let key = SilentPaymentCode::try_from(value_0)?;
-
- let value = parts[1]
- .trim()
- .parse::<u64>()
- .map_err(|_| Error::Generic(format!("Invalid number '{}' for key '{}'", parts[1], key)))?;
-
- Ok((key, value))
-}
-
-#[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();
- if parts.len() != 2 {
- return Err(Error::Generic("Invalid format".to_string()));
- }
-
- let user = parts[0].to_string();
- let passwd = parts[1].to_string();
-
- Ok((user, passwd))
-}
-
-/// Parse a outpoint (Txid:Vout) argument from cli input.
-pub(crate) fn parse_outpoint(s: &str) -> Result<OutPoint, Error> {
- Ok(OutPoint::from_str(s)?)
-}
-
-/// Parse an address string into `Address<NetworkChecked>`.
-pub(crate) fn parse_address(address_str: &str) -> Result<Address, Error> {
- let unchecked_address = Address::from_str(address_str)?;
- Ok(unchecked_address.assume_checked())
-}
-
-/// Function to parse the signature format from a string
-#[cfg(feature = "bip322")]
-pub(crate) fn parse_signature_format(format_str: &str) -> Result<SignatureFormat, Error> {
- match format_str.to_lowercase().as_str() {
- "legacy" => Ok(SignatureFormat::Legacy),
- "simple" => Ok(SignatureFormat::Simple),
- "full" => Ok(SignatureFormat::Full),
- "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds),
- _ => Err(Error::Generic(
- "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'"
- .to_string(),
- )),
- }
-}
-
-/// Prepare bdk-cli home directory
-///
-/// This function is called to check if [`crate::CliOpts`] datadir is set.
-/// If not the default home directory is created at `~/.bdk-bitcoin`.
-#[allow(dead_code)]
-pub(crate) fn prepare_home_dir(home_path: Option<PathBuf>) -> Result<PathBuf, Error> {
- let dir = home_path.unwrap_or_else(|| {
- let mut dir = PathBuf::new();
- dir.push(
- dirs::home_dir()
- .ok_or_else(|| Error::Generic("home dir not found".to_string()))
- .unwrap(),
- );
- dir.push(".bdk-bitcoin");
- dir
- });
-
- if !dir.exists() {
- std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
- }
-
- Ok(dir)
-}
-
-/// Prepare wallet database directory.
-#[allow(dead_code)]
-pub(crate) fn prepare_wallet_db_dir(
- home_path: &Path,
- wallet_name: &str,
-) -> Result<std::path::PathBuf, Error> {
- let mut dir = home_path.to_owned();
- dir.push(wallet_name);
-
- if !dir.exists() {
- std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
- }
-
- Ok(dir)
-}
-
-#[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 = Sync;
- let 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)
-}
-
-#[cfg(any(feature = "sqlite", feature = "redb"))]
-/// Create a new persisted wallet from given wallet configuration options.
-pub(crate) fn new_persisted_wallet<P: WalletPersister>(
- network: Network,
- persister: &mut P,
- wallet_opts: &WalletOpts,
-) -> Result<PersistedWallet<P>, Error>
-where
- P::Error: std::fmt::Display,
-{
- let ext_descriptor = wallet_opts.ext_descriptor.clone();
- let int_descriptor = wallet_opts.int_descriptor.clone();
-
- let mut wallet_load_params = Wallet::load();
- wallet_load_params =
- wallet_load_params.descriptor(KeychainKind::External, Some(ext_descriptor.clone()));
-
- if int_descriptor.is_some() {
- wallet_load_params =
- wallet_load_params.descriptor(KeychainKind::Internal, int_descriptor.clone());
- }
- wallet_load_params = wallet_load_params.extract_keys();
-
- let wallet_opt = wallet_load_params
- .check_network(network)
- .load_wallet(persister)
- .map_err(|e| Error::Generic(e.to_string()))?;
-
- let wallet = match wallet_opt {
- Some(wallet) => wallet,
- None => match int_descriptor {
- Some(int_descriptor) => Wallet::create(ext_descriptor, int_descriptor)
- .network(network)
- .create_wallet(persister)
- .map_err(|e| Error::Generic(e.to_string()))?,
- None => Wallet::create_single(ext_descriptor)
- .network(network)
- .create_wallet(persister)
- .map_err(|e| Error::Generic(e.to_string()))?,
- },
- };
-
- Ok(wallet)
-}
-
-#[cfg(not(any(feature = "sqlite", feature = "redb")))]
-/// Create a new non-persisted wallet from given wallet configuration options.
-pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result<Wallet, Error> {
- let ext_descriptor = wallet_opts.ext_descriptor.clone();
- let int_descriptor = wallet_opts.int_descriptor.clone();
-
- match int_descriptor {
- Some(int_descriptor) => {
- let wallet = Wallet::create(ext_descriptor, int_descriptor)
- .network(network)
- .create_wallet_no_persist()?;
- Ok(wallet)
- }
- None => {
- let wallet = Wallet::create_single(ext_descriptor)
- .network(network)
- .create_wallet_no_persist()?;
- Ok(wallet)
- }
- }
-}
-
-#[cfg(feature = "cbf")]
-pub async fn trace_logger(
- mut info_subcriber: Receiver<Info>,
- mut warning_subscriber: UnboundedReceiver<Warning>,
-) {
- loop {
- tokio::select! {
- info = info_subcriber.recv() => {
- if let Some(info) = info {
- tracing::info!("{info}")
- }
- }
- warn = warning_subscriber.recv() => {
- if let Some(warn) = warn {
- tracing::warn!("{warn}")
- }
- }
- }
- }
-}
-
-// 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(())
-}
-
-pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String {
- let displayable = displayable.to_string();
-
- if displayable.len() <= (start + end) as usize {
- return displayable;
- }
-
- let start_str: &str = &displayable[0..start as usize];
- let end_str: &str = &displayable[displayable.len() - end as usize..];
- format!("{start_str}...{end_str}")
-}
-
-pub fn is_mnemonic(s: &str) -> bool {
- let word_count = s.split_whitespace().count();
- (12..=24).contains(&word_count) && s.chars().all(|c| c.is_alphanumeric() || c.is_whitespace())
-}
-
-pub fn generate_descriptors(desc_type: &str, key: &str, network: Network) -> Result<Value, Error> {
- let is_private = key.starts_with("xprv") || key.starts_with("tprv");
-
- if is_private {
- generate_private_descriptors(desc_type, key, network)
- } else {
- let purpose = match desc_type.to_lowercase().as_str() {
- "pkh" => 44u32,
- "sh" => 49u32,
- "wpkh" | "wsh" => 84u32,
- "tr" => 86u32,
- _ => 84u32,
- };
- let coin_type = match network {
- Network::Bitcoin => 0u32,
- _ => 1u32,
- };
- let derivation_path = DerivationPath::from_str(&format!("m/{purpose}h/{coin_type}h/0h"))?;
- generate_public_descriptors(desc_type, key, &derivation_path)
- }
-}
-
-/// Generate descriptors from private key using BIP templates
-fn generate_private_descriptors(
- desc_type: &str,
- key: &str,
- network: Network,
-) -> Result<Value, Error> {
- use bdk_wallet::template::{Bip44, Bip49, Bip84, Bip86};
-
- let secp = Secp256k1::new();
- let xprv: Xpriv = key.parse()?;
- let fingerprint = xprv.fingerprint(&secp);
-
- let (external_desc, external_keymap, _) = match desc_type.to_lowercase().as_str() {
- "pkh" => Bip44(xprv, KeychainKind::External).build(network)?,
- "sh" => Bip49(xprv, KeychainKind::External).build(network)?,
- "wpkh" | "wsh" => Bip84(xprv, KeychainKind::External).build(network)?,
- "tr" => Bip86(xprv, KeychainKind::External).build(network)?,
- _ => {
- return Err(Error::Generic(format!(
- "Unsupported descriptor type: {desc_type}"
- )));
- }
- };
-
- let (internal_desc, internal_keymap, _) = match desc_type.to_lowercase().as_str() {
- "pkh" => Bip44(xprv, KeychainKind::Internal).build(network)?,
- "sh" => Bip49(xprv, KeychainKind::Internal).build(network)?,
- "wpkh" | "wsh" => Bip84(xprv, KeychainKind::Internal).build(network)?,
- "tr" => Bip86(xprv, KeychainKind::Internal).build(network)?,
- _ => {
- return Err(Error::Generic(format!(
- "Unsupported descriptor type: {desc_type}"
- )));
- }
- };
-
- let external_priv = external_desc.to_string_with_secret(&external_keymap);
- let external_pub = external_desc.to_string();
- let internal_priv = internal_desc.to_string_with_secret(&internal_keymap);
- let internal_pub = internal_desc.to_string();
-
- Ok(json!({
- "public_descriptors": {
- "external": external_pub,
- "internal": internal_pub
- },
- "private_descriptors": {
- "external": external_priv,
- "internal": internal_priv
- },
- "fingerprint": fingerprint.to_string()
- }))
-}
-
-/// Generate descriptors from public key (xpub/tpub)
-pub fn generate_public_descriptors(
- desc_type: &str,
- key: &str,
- derivation_path: &DerivationPath,
-) -> Result<Value, Error> {
- let xpub: Xpub = key.parse()?;
- let fingerprint = xpub.fingerprint();
-
- let build_descriptor = |branch: &str| -> Result<String, Error> {
- let branch_path = DerivationPath::from_str(branch)?;
- let desc_xpub = DescriptorXKey {
- origin: Some((fingerprint, derivation_path.clone())),
- xkey: xpub,
- derivation_path: branch_path,
- wildcard: Wildcard::Unhardened,
- };
- let desc_pub = DescriptorPublicKey::XPub(desc_xpub);
- let descriptor = build_public_descriptor(desc_type, desc_pub)?;
- Ok(descriptor.to_string())
- };
-
- let external_pub = build_descriptor("0")?;
- let internal_pub = build_descriptor("1")?;
-
- Ok(json!({
- "public_descriptors": {
- "external": external_pub,
- "internal": internal_pub
- },
- "fingerprint": fingerprint.to_string()
- }))
-}
-
-/// Build a descriptor from a public key
-pub fn build_public_descriptor(
- desc_type: &str,
- key: DescriptorPublicKey,
-) -> Result<Descriptor<DescriptorPublicKey>, Error> {
- match desc_type.to_lowercase().as_str() {
- "pkh" => Descriptor::new_pkh(key).map_err(Error::from),
- "wpkh" => Descriptor::new_wpkh(key).map_err(Error::from),
- "sh" => Descriptor::new_sh_wpkh(key).map_err(Error::from),
- "wsh" => {
- let pk_k = Miniscript::from_ast(Terminal::PkK(key)).map_err(Error::from)?;
- let pk_ms: Miniscript<DescriptorPublicKey, Segwitv0> =
- Miniscript::from_ast(Terminal::Check(Arc::new(pk_k))).map_err(Error::from)?;
- Descriptor::new_wsh(pk_ms).map_err(Error::from)
- }
- "tr" => Descriptor::new_tr(key, None).map_err(Error::from),
- _ => Err(Error::Generic(format!(
- "Unsupported descriptor type: {desc_type}"
- ))),
- }
-}
-
-/// Generate new mnemonic and descriptors
-pub fn generate_descriptor_with_mnemonic(
- network: Network,
- desc_type: &str,
-) -> Result<serde_json::Value, Error> {
- let mnemonic: GeneratedKey<Mnemonic, Segwitv0> =
- Mnemonic::generate((WordCount::Words12, Language::English)).map_err(Error::BIP39Error)?;
-
- let seed = mnemonic.to_seed("");
- let xprv = Xpriv::new_master(network, &seed)?;
-
- let mut result = generate_descriptors(desc_type, &xprv.to_string(), network)?;
- result["mnemonic"] = json!(mnemonic.to_string());
- Ok(result)
-}
-
-/// Generate descriptors from existing mnemonic
-pub fn generate_descriptor_from_mnemonic(
- mnemonic_str: &str,
- network: Network,
- desc_type: &str,
-) -> Result<serde_json::Value, Error> {
- let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?;
- let seed = mnemonic.to_seed("");
- let xprv = Xpriv::new_master(network, &seed)?;
-
- let mut result = generate_descriptors(desc_type, &xprv.to_string(), network)?;
- result["mnemonic"] = json!(mnemonic_str);
- Ok(result)
-}
-
-pub fn format_descriptor_output(result: &Value, pretty: bool) -> Result<String, Error> {
- if !pretty {
- return Ok(serde_json::to_string_pretty(result)?);
- }
-
- let mut rows: Vec<Vec<CellStruct>> = vec![];
-
- if let Some(desc_type) = result.get("type") {
- rows.push(vec![
- "Type".cell().bold(true),
- desc_type.as_str().unwrap_or("N/A").cell(),
- ]);
- }
-
- if let Some(finger_print) = result.get("fingerprint") {
- rows.push(vec![
- "Fingerprint".cell().bold(true),
- finger_print.as_str().unwrap_or("N/A").cell(),
- ]);
- }
-
- if let Some(network) = result.get("network") {
- rows.push(vec![
- "Network".cell().bold(true),
- network.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- if let Some(multipath_desc) = result.get("multipath_descriptor") {
- rows.push(vec![
- "Multipart Descriptor".cell().bold(true),
- multipath_desc.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- if let Some(pub_descs) = result.get("public_descriptors").and_then(|v| v.as_object()) {
- if let Some(ext) = pub_descs.get("external") {
- rows.push(vec![
- "External Public".cell().bold(true),
- ext.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- if let Some(int) = pub_descs.get("internal") {
- rows.push(vec![
- "Internal Public".cell().bold(true),
- int.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- }
- if let Some(priv_descs) = result
- .get("private_descriptors")
- .and_then(|v| v.as_object())
- {
- if let Some(ext) = priv_descs.get("external") {
- rows.push(vec![
- "External Private".cell().bold(true),
- ext.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- if let Some(int) = priv_descs.get("internal") {
- rows.push(vec![
- "Internal Private".cell().bold(true),
- int.as_str().unwrap_or("N/A").cell(),
- ]);
- }
- }
- if let Some(mnemonic) = result.get("mnemonic") {
- rows.push(vec![
- "Mnemonic".cell().bold(true),
- mnemonic.as_str().unwrap_or("N/A").cell(),
- ]);
- }
-
- let table = rows
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
-
- Ok(format!("{table}"))
-}
-
-pub fn load_wallet_config(
- home_dir: &Path,
- wallet_name: &str,
-) -> Result<(WalletOpts, Network), Error> {
- let config = WalletConfig::load(home_dir)?.ok_or(Error::Generic(format!(
- "No config found for wallet {wallet_name}",
- )))?;
-
- let wallet_opts = config.get_wallet_opts(wallet_name)?;
- let wallet_config = config
- .wallets
- .get(wallet_name)
- .ok_or(Error::Generic(format!(
- "Wallet '{wallet_name}' not found in config"
- )))?;
-
- let network = match wallet_config.network.as_str() {
- "bitcoin" => Ok(Network::Bitcoin),
- "testnet" => Ok(Network::Testnet),
- "regtest" => Ok(Network::Regtest),
- "signet" => Ok(Network::Signet),
- "testnet4" => Ok(Network::Testnet4),
- _ => Err(Error::Generic("Invalid network in config".to_string())),
- }?;
-
- Ok((wallet_opts, network))
-}
--- /dev/null
+use crate::{commands::WalletOpts, config::WalletConfig, error::BDKCliError as Error};
+#[cfg(feature = "cbf")]
+use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning};
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
+use bdk_wallet::bitcoin::Psbt;
+use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
+#[cfg(feature = "silent-payments")]
+use bdk_sp::encoding::SilentPaymentCode;
+
+use std::{
+ fmt::Display,
+ path::{Path, PathBuf},
+ str::FromStr,
+};
+
+/// Determine if PSBT has final script sigs or witnesses for all unsigned tx inputs.
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
+pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
+ let unsigned_tx_inputs = psbt.unsigned_tx.input.len();
+ let psbt_inputs = psbt.inputs.len();
+ if unsigned_tx_inputs != psbt_inputs {
+ return Err(Error::Generic(format!(
+ "Malformed PSBT, {unsigned_tx_inputs} unsigned tx inputs and {psbt_inputs} psbt inputs."
+ )));
+ }
+ let sig_count = psbt.inputs.iter().fold(0, |count, input| {
+ if input.final_script_sig.is_some() || input.final_script_witness.is_some() {
+ count + 1
+ } else {
+ count
+ }
+ });
+ if unsigned_tx_inputs > sig_count {
+ return Err(Error::Generic(
+ "The PSBT is not finalized, inputs are are not fully signed.".to_string(),
+ ));
+ }
+ Ok(())
+}
+
+pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String {
+ let displayable = displayable.to_string();
+
+ if displayable.len() <= (start + end) as usize {
+ return displayable;
+ }
+
+ let start_str: &str = &displayable[0..start as usize];
+ let end_str: &str = &displayable[displayable.len() - end as usize..];
+ format!("{start_str}...{end_str}")
+}
+
+/// Parse the recipient (Address,Amount) argument from cli input.
+pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> {
+ let parts: Vec<_> = s.split(':').collect();
+ if parts.len() != 2 {
+ return Err("Invalid format".to_string());
+ }
+ let addr = Address::from_str(parts[0])
+ .map_err(|e| e.to_string())?
+ .assume_checked();
+ let val = u64::from_str(parts[1]).map_err(|e| e.to_string())?;
+
+ Ok((addr.script_pubkey(), val))
+}
+
+#[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();
+ if parts.len() != 2 {
+ return Err(Error::Generic("Invalid format".to_string()));
+ }
+
+ let user = parts[0].to_string();
+ let passwd = parts[1].to_string();
+
+ Ok((user, passwd))
+}
+
+/// Parse a outpoint (Txid:Vout) argument from cli input.
+pub(crate) fn parse_outpoint(s: &str) -> Result<OutPoint, Error> {
+ Ok(OutPoint::from_str(s)?)
+}
+
+/// Parse an address string into `Address<NetworkChecked>`.
+pub(crate) fn parse_address(address_str: &str) -> Result<Address, Error> {
+ let unchecked_address = Address::from_str(address_str)?;
+ Ok(unchecked_address.assume_checked())
+}
+
+/// Prepare bdk-cli home directory
+///
+/// This function is called to check if [`crate::CliOpts`] datadir is set.
+/// If not the default home directory is created at `~/.bdk-bitcoin`.
+#[allow(dead_code)]
+pub(crate) fn prepare_home_dir(home_path: Option<PathBuf>) -> Result<PathBuf, Error> {
+ let dir = home_path.unwrap_or_else(|| {
+ let mut dir = PathBuf::new();
+ dir.push(
+ dirs::home_dir()
+ .ok_or_else(|| Error::Generic("home dir not found".to_string()))
+ .unwrap(),
+ );
+ dir.push(".bdk-bitcoin");
+ dir
+ });
+
+ if !dir.exists() {
+ std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
+ }
+
+ Ok(dir)
+}
+
+/// Prepare wallet database directory.
+#[allow(dead_code)]
+pub(crate) fn prepare_wallet_db_dir(
+ home_path: &Path,
+ wallet_name: &str,
+) -> Result<std::path::PathBuf, Error> {
+ let mut dir = home_path.to_owned();
+ dir.push(wallet_name);
+
+ if !dir.exists() {
+ std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
+ }
+
+ Ok(dir)
+}
+
+pub fn is_mnemonic(s: &str) -> bool {
+ let word_count = s.split_whitespace().count();
+ (12..=24).contains(&word_count) && s.chars().all(|c| c.is_alphanumeric() || c.is_whitespace())
+}
+
+#[cfg(feature = "cbf")]
+pub async fn trace_logger(
+ mut info_subcriber: Receiver<Info>,
+ mut warning_subscriber: UnboundedReceiver<Warning>,
+) {
+ loop {
+ tokio::select! {
+ info = info_subcriber.recv() => {
+ if let Some(info) = info {
+ tracing::info!("{info}")
+ }
+ }
+ warn = warning_subscriber.recv() => {
+ if let Some(warn) = warn {
+ tracing::warn!("{warn}")
+ }
+ }
+ }
+ }
+}
+
+pub fn load_wallet_config(
+ home_dir: &Path,
+ wallet_name: &str,
+) -> Result<(WalletOpts, Network), Error> {
+ let config = WalletConfig::load(home_dir)?.ok_or(Error::Generic(format!(
+ "No config found for wallet {wallet_name}",
+ )))?;
+
+ let wallet_opts = config.get_wallet_opts(wallet_name)?;
+ let wallet_config = config
+ .wallets
+ .get(wallet_name)
+ .ok_or(Error::Generic(format!(
+ "Wallet '{wallet_name}' not found in config"
+ )))?;
+
+ let network = match wallet_config.network.as_str() {
+ "bitcoin" => Ok(Network::Bitcoin),
+ "testnet" => Ok(Network::Testnet),
+ "regtest" => Ok(Network::Regtest),
+ "signet" => Ok(Network::Signet),
+ "testnet4" => Ok(Network::Testnet4),
+ _ => Err(Error::Generic("Invalid network in config".to_string())),
+ }?;
+
+ Ok((wallet_opts, network))
+}
+
+
+// #[cfg(feature = "silent-payments")]
+pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), Error> {
+ let parts: Vec<&str> = s.split(':').collect();
+ if parts.len() != 2 {
+ return Err(Error::Generic(format!(
+ "Invalid format '{}'. Expected 'key:value'",
+ s
+ )));
+ }
+
+ let value_0 = parts[0].trim();
+ let key = SilentPaymentCode::try_from(value_0)?;
+
+ let value = parts[1]
+ .trim()
+ .parse::<u64>()
+ .map_err(|_| Error::Generic(format!("Invalid number '{}' for key '{}'", parts[1], key)))?;
+
+ Ok((key, value))
+}
--- /dev/null
+use bdk_wallet::keys::GeneratableKey;
+use std::{str::FromStr, sync::Arc};
+
+use bdk_wallet::keys::DescriptorPublicKey;
+use bdk_wallet::{
+ KeychainKind,
+ bip39::{Language, Mnemonic},
+ bitcoin::{
+ Network,
+ bip32::{DerivationPath, Xpriv, Xpub},
+ secp256k1::Secp256k1,
+ },
+ keys::{GeneratedKey, bip39::WordCount},
+ miniscript::{
+ Descriptor, Miniscript, Segwitv0, Terminal,
+ descriptor::{DescriptorXKey, Wildcard},
+ },
+ template::DescriptorTemplate,
+};
+use cli_table::{Cell, CellStruct, Style, Table};
+use serde_json::{Value, json};
+
+use crate::error::BDKCliError as Error;
+
+pub fn generate_descriptors(desc_type: &str, key: &str, network: Network) -> Result<Value, Error> {
+ let is_private = key.starts_with("xprv") || key.starts_with("tprv");
+
+ if is_private {
+ generate_private_descriptors(desc_type, key, network)
+ } else {
+ let purpose = match desc_type.to_lowercase().as_str() {
+ "pkh" => 44u32,
+ "sh" => 49u32,
+ "wpkh" | "wsh" => 84u32,
+ "tr" => 86u32,
+ _ => 84u32,
+ };
+ let coin_type = match network {
+ Network::Bitcoin => 0u32,
+ _ => 1u32,
+ };
+ let derivation_path = DerivationPath::from_str(&format!("m/{purpose}h/{coin_type}h/0h"))?;
+ generate_public_descriptors(desc_type, key, &derivation_path)
+ }
+}
+
+/// Generate descriptors from private key using BIP templates
+fn generate_private_descriptors(
+ desc_type: &str,
+ key: &str,
+ network: Network,
+) -> Result<Value, Error> {
+ use bdk_wallet::template::{Bip44, Bip49, Bip84, Bip86};
+
+ let secp = Secp256k1::new();
+ let xprv: Xpriv = key.parse()?;
+ let fingerprint = xprv.fingerprint(&secp);
+
+ let (external_desc, external_keymap, _) = match desc_type.to_lowercase().as_str() {
+ "pkh" => Bip44(xprv, KeychainKind::External).build(network)?,
+ "sh" => Bip49(xprv, KeychainKind::External).build(network)?,
+ "wpkh" | "wsh" => Bip84(xprv, KeychainKind::External).build(network)?,
+ "tr" => Bip86(xprv, KeychainKind::External).build(network)?,
+ _ => {
+ return Err(Error::Generic(format!(
+ "Unsupported descriptor type: {desc_type}"
+ )));
+ }
+ };
+
+ let (internal_desc, internal_keymap, _) = match desc_type.to_lowercase().as_str() {
+ "pkh" => Bip44(xprv, KeychainKind::Internal).build(network)?,
+ "sh" => Bip49(xprv, KeychainKind::Internal).build(network)?,
+ "wpkh" | "wsh" => Bip84(xprv, KeychainKind::Internal).build(network)?,
+ "tr" => Bip86(xprv, KeychainKind::Internal).build(network)?,
+ _ => {
+ return Err(Error::Generic(format!(
+ "Unsupported descriptor type: {desc_type}"
+ )));
+ }
+ };
+
+ let external_priv = external_desc.to_string_with_secret(&external_keymap);
+ let external_pub = external_desc.to_string();
+ let internal_priv = internal_desc.to_string_with_secret(&internal_keymap);
+ let internal_pub = internal_desc.to_string();
+
+ Ok(json!({
+ "public_descriptors": {
+ "external": external_pub,
+ "internal": internal_pub
+ },
+ "private_descriptors": {
+ "external": external_priv,
+ "internal": internal_priv
+ },
+ "fingerprint": fingerprint.to_string()
+ }))
+}
+
+/// Generate descriptors from public key (xpub/tpub)
+pub fn generate_public_descriptors(
+ desc_type: &str,
+ key: &str,
+ derivation_path: &DerivationPath,
+) -> Result<Value, Error> {
+ let xpub: Xpub = key.parse()?;
+ let fingerprint = xpub.fingerprint();
+
+ let build_descriptor = |branch: &str| -> Result<String, Error> {
+ let branch_path = DerivationPath::from_str(branch)?;
+ let desc_xpub = DescriptorXKey {
+ origin: Some((fingerprint, derivation_path.clone())),
+ xkey: xpub,
+ derivation_path: branch_path,
+ wildcard: Wildcard::Unhardened,
+ };
+ let desc_pub = DescriptorPublicKey::XPub(desc_xpub);
+ let descriptor = build_public_descriptor(desc_type, desc_pub)?;
+ Ok(descriptor.to_string())
+ };
+
+ let external_pub = build_descriptor("0")?;
+ let internal_pub = build_descriptor("1")?;
+
+ Ok(json!({
+ "public_descriptors": {
+ "external": external_pub,
+ "internal": internal_pub
+ },
+ "fingerprint": fingerprint.to_string()
+ }))
+}
+
+/// Build a descriptor from a public key
+pub fn build_public_descriptor(
+ desc_type: &str,
+ key: DescriptorPublicKey,
+) -> Result<Descriptor<DescriptorPublicKey>, Error> {
+ match desc_type.to_lowercase().as_str() {
+ "pkh" => Descriptor::new_pkh(key).map_err(Error::from),
+ "wpkh" => Descriptor::new_wpkh(key).map_err(Error::from),
+ "sh" => Descriptor::new_sh_wpkh(key).map_err(Error::from),
+ "wsh" => {
+ let pk_k = Miniscript::from_ast(Terminal::PkK(key)).map_err(Error::from)?;
+ let pk_ms: Miniscript<DescriptorPublicKey, Segwitv0> =
+ Miniscript::from_ast(Terminal::Check(Arc::new(pk_k))).map_err(Error::from)?;
+ Descriptor::new_wsh(pk_ms).map_err(Error::from)
+ }
+ "tr" => Descriptor::new_tr(key, None).map_err(Error::from),
+ _ => Err(Error::Generic(format!(
+ "Unsupported descriptor type: {desc_type}"
+ ))),
+ }
+}
+
+/// Generate new mnemonic and descriptors
+pub fn generate_descriptor_with_mnemonic(
+ network: Network,
+ desc_type: &str,
+) -> Result<serde_json::Value, Error> {
+ let mnemonic: GeneratedKey<Mnemonic, Segwitv0> =
+ Mnemonic::generate((WordCount::Words12, Language::English)).map_err(Error::BIP39Error)?;
+
+ let seed = mnemonic.to_seed("");
+ let xprv = Xpriv::new_master(network, &seed)?;
+
+ let mut result = generate_descriptors(desc_type, &xprv.to_string(), network)?;
+ result["mnemonic"] = json!(mnemonic.to_string());
+ Ok(result)
+}
+
+/// Generate descriptors from existing mnemonic
+pub fn generate_descriptor_from_mnemonic(
+ mnemonic_str: &str,
+ network: Network,
+ desc_type: &str,
+) -> Result<serde_json::Value, Error> {
+ let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?;
+ let seed = mnemonic.to_seed("");
+ let xprv = Xpriv::new_master(network, &seed)?;
+
+ let mut result = generate_descriptors(desc_type, &xprv.to_string(), network)?;
+ result["mnemonic"] = json!(mnemonic_str);
+ Ok(result)
+}
+
+pub fn format_descriptor_output(result: &Value, pretty: bool) -> Result<String, Error> {
+ if !pretty {
+ return Ok(serde_json::to_string_pretty(result)?);
+ }
+
+ let mut rows: Vec<Vec<CellStruct>> = vec![];
+
+ if let Some(desc_type) = result.get("type") {
+ rows.push(vec![
+ "Type".cell().bold(true),
+ desc_type.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+
+ if let Some(finger_print) = result.get("fingerprint") {
+ rows.push(vec![
+ "Fingerprint".cell().bold(true),
+ finger_print.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+
+ if let Some(network) = result.get("network") {
+ rows.push(vec![
+ "Network".cell().bold(true),
+ network.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ if let Some(multipath_desc) = result.get("multipath_descriptor") {
+ rows.push(vec![
+ "Multipart Descriptor".cell().bold(true),
+ multipath_desc.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ if let Some(pub_descs) = result.get("public_descriptors").and_then(|v| v.as_object()) {
+ if let Some(ext) = pub_descs.get("external") {
+ rows.push(vec![
+ "External Public".cell().bold(true),
+ ext.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ if let Some(int) = pub_descs.get("internal") {
+ rows.push(vec![
+ "Internal Public".cell().bold(true),
+ int.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ }
+ if let Some(priv_descs) = result
+ .get("private_descriptors")
+ .and_then(|v| v.as_object())
+ {
+ if let Some(ext) = priv_descs.get("external") {
+ rows.push(vec![
+ "External Private".cell().bold(true),
+ ext.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ if let Some(int) = priv_descs.get("internal") {
+ rows.push(vec![
+ "Internal Private".cell().bold(true),
+ int.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+ }
+ if let Some(mnemonic) = result.get("mnemonic") {
+ rows.push(vec![
+ "Mnemonic".cell().bold(true),
+ mnemonic.as_str().unwrap_or("N/A").cell(),
+ ]);
+ }
+
+ let table = rows
+ .table()
+ .display()
+ .map_err(|e| Error::Generic(e.to_string()))?;
+
+ Ok(format!("{table}"))
+}