From 6c9c3a1bb13eada4af00e41be1ccded2928a6ba2 Mon Sep 17 00:00:00 2001 From: AmosOO7 Date: Wed, 30 Apr 2025 21:31:23 +0100 Subject: [PATCH 1/6] feat: add descriptor generation - Created Subcommnds for the descriptor command; generate - Created function to get descriptors from mnemonics --- src/commands.rs | 36 ++++- src/error.rs | 24 ++++ src/handlers.rs | 141 +++++++++++++++++- src/utils.rs | 372 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 563 insertions(+), 10 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 54cddf04..09cbc5dd 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,12 +13,11 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] - use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; -use clap::{Args, Parser, Subcommand, ValueEnum, value_parser}; +use clap::{Args, Parser, Subcommand, ValueEnum, builder::TypedValueParser, value_parser}; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -107,8 +106,15 @@ pub enum CliSubCommand { #[command(flatten)] wallet_opts: WalletOpts, }, + /// 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 { + #[clap(subcommand)] + subcommand: DescriptorSubCommand, + }, } - /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] pub enum WalletSubCommand { @@ -473,3 +479,27 @@ pub enum ReplSubCommand { /// Exit REPL loop. Exit, } +/// Subcommands for Key operations. +#[derive(Debug, Subcommand, Clone, PartialEq, Eq)] +pub enum DescriptorSubCommand { + /// Generate a descriptor + Generate { + /// Descriptor type (script type). + #[arg( + long = "type", + short = 't', + value_parser = clap::builder::PossibleValuesParser::new(["44", "49", "84", "86"]) + .map(|s| s.parse::().unwrap()), + default_value = "84" + )] + r#type: u8, + /// Enable multipath descriptors + #[arg(long = "multipath", short = 'm', default_value_t = false)] + multipath: bool, + /// Optional key input + key: Option, + }, + + /// Show info about a given descriptor + Info { descriptor: String }, +} diff --git a/src/error.rs b/src/error.rs index 5f548d91..3916f261 100644 --- a/src/error.rs +++ b/src/error.rs @@ -103,6 +103,30 @@ pub enum BDKCliError { #[cfg(feature = "cbf")] #[error("BDK-Kyoto update error: {0}")] KyotoUpdateError(#[from] bdk_kyoto::UpdateError), + + #[error("Mnemonic generation failed: {0}")] + MnemonicGenerationError(String), + + #[error("Xpriv creation failed: {0}")] + XprivCreationError(String), + + #[error("Descriptor parsing failed: {0}")] + DescriptorParsingError(String), + + #[error("Invalid extended key (xpub): {0}")] + InvalidKey(String), + + #[error("Invalid derivation path: {0}")] + InvalidDerivationPath(String), + + #[error("Unsupported script type: {0}")] + UnsupportedScriptType(u8), + + #[error("Descriptor key conversion failed: {0}")] + DescriptorKeyError(String), + + #[error("Invalid arguments: {0}")] + InvalidArguments(String), } impl From for BDKCliError { diff --git a/src/handlers.rs b/src/handlers.rs index d9d2cbef..9264e477 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -35,6 +35,14 @@ use bdk_wallet::keys::{ bip39::WordCount, }; use bdk_wallet::miniscript::miniscript; +use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource}; +use bdk_wallet::bitcoin::consensus::encode::serialize_hex; +use bdk_wallet::bitcoin::script::PushBytesBuf; +use bdk_wallet::bitcoin::Network; +use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid}; +use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence}; +use bdk_wallet::descriptor::{Descriptor, Segwitv0}; +use bdk_wallet::keys::bip39::WordCount; #[cfg(feature = "sqlite")] use bdk_wallet::rusqlite::Connection; use bdk_wallet::{KeychainKind, SignOptions, Wallet}; @@ -42,25 +50,40 @@ use bdk_wallet::{KeychainKind, SignOptions, Wallet}; use bdk_wallet::{ descriptor::{Descriptor, Legacy, Miniscript}, miniscript::{Tap, descriptor::TapTree, policy::Concrete}, + descriptor::{Legacy, Miniscript}, + miniscript::policy::Concrete, }; use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; use serde_json::json; +use bdk_wallet::{KeychainKind, SignOptions, Wallet}; + +#[cfg(feature = "electrum")] +use crate::utils::BlockchainClient::Electrum; +#[cfg(feature = "cbf")] +use bdk_kyoto::LightClient; +#[cfg(feature = "compiler")] +use bdk_wallet::bitcoin::XOnlyPublicKey; +use bdk_wallet::bitcoin::base64::prelude::*; +use bdk_wallet::keys::DescriptorKey::Secret; +use bdk_wallet::keys::{ + DerivableKey, DescriptorKey, DescriptorKey::Secret, DescriptorPublicKey, ExtendedKey, + GeneratableKey, GeneratedKey, bip39::WordCount, +}; +use bdk_wallet::miniscript::miniscript; +use serde_json::{Value, json}; use std::collections::BTreeMap; #[cfg(any(feature = "electrum", feature = "esplora"))] use std::collections::HashSet; use std::convert::TryFrom; +use std::fmt; #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))] use std::io::Write; use std::str::FromStr; -#[cfg(any(feature = "redb", feature = "compiler"))] -use std::sync::Arc; #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; #[cfg(feature = "cbf")] -use bdk_kyoto::LightClient; -#[cfg(feature = "compiler")] -use bdk_wallet::bitcoin::XOnlyPublicKey; +use bdk_kyoto::{Info, LightClient}; use bdk_wallet::bitcoin::base64::prelude::*; #[cfg(feature = "cbf")] use tokio::select; @@ -72,7 +95,7 @@ use tokio::select; ))] use { crate::commands::OnlineWalletSubCommand::*, - bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex}, + bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction}, }; #[cfg(feature = "esplora")] use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; @@ -1260,6 +1283,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } Ok("".to_string()) } + CliSubCommand::Descriptor { + subcommand: descriptor_subcommand, + } => { + let network = cli_opts.network; + let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand) + .map_err(|e| Error::Generic(e.to_string()))?; + let json = serde_json::to_string_pretty(&descriptor)?; + Ok(json) + } }; result } @@ -1333,6 +1365,103 @@ fn readline() -> Result { Ok(buffer) } +pub fn handle_descriptor_subcommand( + network: Network, + subcommand: DescriptorSubCommand, +) -> Result { + match subcommand { + DescriptorSubCommand::Generate { + r#type, + multipath, + key, + } => { + let (descriptor_type, derivation_path_str) = match r#type { + 44 => (DescriptorType::Bip44, "m/44h/1h/0h"), + 49 => (DescriptorType::Bip49, "m/49h/1h/0h"), + 84 => (DescriptorType::Bip84, "m/84h/1h/0h"), + 86 => (DescriptorType::Bip86, "m/86h/1h/0h"), + _ => return Err(Error::UnsupportedScriptType(r#type)), + }; + + match (multipath, key.as_ref()) { + (true, Some(k)) => generate_multipath_descriptor(&network, r#type, k), + (false, Some(k)) => { + if is_mnemonic(k) { + generate_descriptor_from_mnemonic_string( + k, + network, + derivation_path_str, + descriptor_type, + ) + } else { + generate_standard_descriptor(&network, r#type, k) + } + } + (false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type), + _ => Err(Error::InvalidArguments( + "Provide a key or weak string".to_string(), + )), + } + } + DescriptorSubCommand::Info { descriptor } => { + let parsed: Descriptor = descriptor + .parse() + .map_err(|e| Error::Generic(format!("Failed to parse descriptor: {}", e)))?; + + let checksum = parsed.to_string(); + let script_type = match parsed { + Descriptor::Wpkh(_) => "wpkh", + Descriptor::Pkh(_) => "pkh", + Descriptor::Sh(_) => "sh", + Descriptor::Tr(_) => "tr", + _ => "other", + }; + + let json = json!({ + "descriptor": checksum, + "type": script_type, + "is_multipath": descriptor.contains("/*"), + }); + + Ok(json) + } + } +} + +pub fn generate_standard_descriptor( + network: &Network, + script_type: u8, + key: &str, +) -> Result { + let descriptor_type = match script_type { + 44 => DescriptorType::Bip44, + 49 => DescriptorType::Bip49, + 84 => DescriptorType::Bip84, + 86 => DescriptorType::Bip86, + _ => return Err(Error::UnsupportedScriptType(script_type)), + }; + + generate_descriptor_from_key_by_type(network, key, descriptor_type) +} + +impl fmt::Display for DescriptorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + DescriptorType::Bip44 => "bip44", + DescriptorType::Bip49 => "bip49", + DescriptorType::Bip84 => "bip84", + DescriptorType::Bip86 => "bip86", + }; + write!(f, "{}", s) + } +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] #[cfg(test)] mod test { #[cfg(any( diff --git a/src/utils.rs b/src/utils.rs index cb810743..93392ba5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,7 +10,7 @@ //! //! This module includes all the utility tools used by the App. use crate::error::BDKCliError as Error; -use std::fmt::Display; +use std::{fmt::Display, path::{Path, PathBuf}, str::FromStr}; use std::str::FromStr; use std::path::{Path, PathBuf}; @@ -35,6 +35,26 @@ use bdk_wallet::Wallet; #[cfg(any(feature = "sqlite", feature = "redb"))] use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; +use bdk_wallet::bip39::{Language, Mnemonic}; +use bdk_wallet::bitcoin::bip32::ChildNumber; +use bdk_wallet::bitcoin::{ + bip32::{DerivationPath, Xpriv, Xpub}, + secp256k1::Secp256k1, +}; +use bdk_wallet::descriptor::{ + Segwitv0, {Descriptor, DescriptorPublicKey}, +}; +use bdk_wallet::keys::{ + DerivableKey, ExtendedKey, + bip39::WordCount, + {DescriptorSecretKey, GeneratableKey, GeneratedKey, IntoDescriptorKey}, +}; +use bdk_wallet::miniscript::{ + Tap, + descriptor::{DescriptorXKey, Wildcard}, +}; +use serde_json::{Value, json}; + /// 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(); @@ -363,4 +383,354 @@ pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { 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 generate_descriptor_from_key_by_type( + network: &Network, + key: &str, + descriptor_type: DescriptorType, +) -> Result { + let derivation_path = match descriptor_type { + DescriptorType::Bip44 => "m/44h/1h/0h", + DescriptorType::Bip49 => "m/49h/1h/0h", + DescriptorType::Bip84 => "m/84h/1h/0h", + DescriptorType::Bip86 => "m/86h/1h/0h", + }; + + generate_bip_descriptor_from_key(network, key, derivation_path, descriptor_type) +} + +pub fn generate_new_descriptor_with_mnemonic( + network: Network, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + // Generate a new BIP39 mnemonic + let mnemonic: GeneratedKey = + Mnemonic::generate((WordCount::Words12, Language::English)).map_err(|e| { + Error::MnemonicGenerationError(format!("Mnemonic generation failed: {:?}", e)) + })?; + + let seed = mnemonic.to_seed(""); + let xprv = + Xpriv::new_master(network, &seed).map_err(|e| Error::XprivCreationError(e.to_string()))?; + + let origin = xprv.fingerprint(&secp); + + let (derivation_base, external_fmt, internal_fmt) = match descriptor_type { + DescriptorType::Bip44 => ("/44h/1h/0h", "pkh", "pkh"), + DescriptorType::Bip49 => ("/49h/1h/0h", "sh(wpkh", "sh(wpkh"), + DescriptorType::Bip84 => ("/84h/1h/0h", "wpkh", "wpkh"), + DescriptorType::Bip86 => ("/86h/1h/0h", "tr", "tr"), + }; + let path = DerivationPath::from_str(&format!("m{}", derivation_base)) + .map_err(|e| Error::Generic(e.to_string()))?; + + let derived_xprv = xprv + .derive_priv(&secp, &path) + .map_err(|e| Error::Generic(e.to_string()))?; + + let xprv_str = derived_xprv.to_string(); + + // Construct descriptors + let external_desc = match descriptor_type { + DescriptorType::Bip49 => format!( + "{}([{}{}]{}{}))", + external_fmt, origin, derivation_base, xprv_str, "/0" + ), + _ => format!( + "{}([{}{}]{}{})", + external_fmt, origin, derivation_base, xprv_str, "/0" + ), + }; + + let internal_desc = match descriptor_type { + DescriptorType::Bip49 => format!( + "{}([{}{}]{}{}))", + internal_fmt, origin, derivation_base, xprv_str, "/1" + ), + _ => format!( + "{}([{}{}]{}{})", + internal_fmt, origin, derivation_base, xprv_str, "/1" + ), + }; + + // Parse descriptors + let (ext_desc, ext_keymap) = + Descriptor::::parse_descriptor(&secp, &external_desc) + .map_err(|e| Error::DescriptorParsingError(e.to_string()))?; + + let (int_desc, int_keymap) = + Descriptor::::parse_descriptor(&secp, &internal_desc).map_err( + |e| { + Error::DescriptorParsingError(format!("Failed to parse internal descriptor: {}", e)) + }, + )?; + + Ok(serde_json::json!({ + "type": descriptor_type.to_string(), + "mnemonic": mnemonic.to_string(), + "private_descriptors": { + "external": ext_desc.to_string_with_secret(&ext_keymap), + "internal": int_desc.to_string_with_secret(&int_keymap), + }, + "public_descriptors": { + "external": ext_desc.to_string(), + "internal": int_desc.to_string(), + } + })) +} + +pub fn generate_multipath_descriptor( + network: &Network, + script_type: u8, + key: &str, +) -> Result { + use DescriptorType::*; + + let descriptor_type = match script_type { + 44 => Bip44, + 49 => Bip49, + 84 => Bip84, + 86 => Bip86, + _ => return Err(Error::UnsupportedScriptType(script_type)), + }; + + type DescriptorConstructor = + fn(DescriptorPublicKey) -> Result, Error>; + + let (derivation_base, descriptor_constructor): (&str, DescriptorConstructor) = + match descriptor_type { + Bip44 => ("/44h/1h/0h", |key| { + Descriptor::new_pkh(key).map_err(Error::from) + }), + Bip49 => ("/49h/1h/0h", |key| { + Descriptor::new_sh_wpkh(key).map_err(Error::from) + }), + Bip84 => ("/84h/1h/0h", |key| { + Descriptor::new_wpkh(key).map_err(Error::from) + }), + Bip86 => ("/86h/1h/0h", |key| { + Descriptor::new_tr(key, None).map_err(Error::from) + }), + }; + + let secp = Secp256k1::new(); + let derivation_path = DerivationPath::from_str(&format!("m{}", derivation_base)) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + // Determine if it's an xprv or xpub + let is_private = key.starts_with("xprv") || key.starts_with("tprv"); + + // Use xprv or xpub accordingly + type DescriptorBuilderFn = Box Result<(String, Option), Error>>; + + let (fingerprint, make_desc): (_, DescriptorBuilderFn) = if is_private { + let xprv: Xpriv = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; + let fingerprint = xprv.fingerprint(&secp); + + let closure = move |change: u32| -> Result<(String, Option), Error> { + let branch_path = DerivationPath::from_str(&change.to_string()) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xprv, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, + }; + + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); + let (desc_key, keymap, _) = match descriptor_type { + DescriptorType::Bip84 | DescriptorType::Bip49 | DescriptorType::Bip44 => { + IntoDescriptorKey::::into_descriptor_key(desc_secret) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + } + DescriptorType::Bip86 => IntoDescriptorKey::::into_descriptor_key(desc_secret) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))?, + }; + + let public_descriptor = descriptor_constructor(desc_key.clone())?; + let private_descriptor = descriptor_constructor(desc_key)?; + + Ok(( + public_descriptor.to_string(), + Some(private_descriptor.to_string_with_secret(&keymap)), + )) + }; + + (fingerprint, Box::new(closure)) + } else { + let xpub: Xpub = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xpub: {e}")))?; + let fingerprint = xpub.fingerprint(); + + let closure = move |change: u32| -> Result<(String, Option), Error> { + let branch_path = DerivationPath::from_str(&change.to_string()) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + let desc_xpub = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xpub, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, + }; + + let desc_key = DescriptorPublicKey::XPub(desc_xpub); + let descriptor = descriptor_constructor(desc_key)?; + Ok((descriptor.to_string(), None)) + }; + + (fingerprint, Box::new(closure)) + }; + + // Build descriptors + let (external_pub, external_priv) = make_desc(0)?; + let (internal_pub, internal_priv) = make_desc(1)?; + + let mut result = json!({ + "type": format!("{}-multipath", descriptor_type), + "public_descriptors": { + "external": external_pub, + "internal": internal_pub + }, + "fingerprint": fingerprint.to_string(), + "network": network.to_string(), + }); + + if let (Some(priv_ext), Some(priv_int)) = (external_priv, internal_priv) { + result["private_descriptors"] = json!({ + "external": priv_ext, + "internal": priv_int + }); + } + + Ok(result) +} + +pub fn generate_bip_descriptor_from_key( + network: &Network, + key: &str, + derivation_path_str: &str, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + let derivation_path: DerivationPath = derivation_path_str + .parse() + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + + let xprv: Xpriv = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; + + let fingerprint = xprv.fingerprint(&secp); + + let make_desc_key = |branch: u32| -> Result<(String, String), Error> { + let branch_path = DerivationPath::from(vec![ChildNumber::Normal { index: branch }]); + + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xprv, + derivation_path: branch_path.clone(), + wildcard: Wildcard::Unhardened, + }; + + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); + + let (desc_key, keymap, _) = + IntoDescriptorKey::::into_descriptor_key(desc_secret.clone()) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))?; + + let public_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key.clone())?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key.clone(), None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key.clone())?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key.clone())?, + }; + + let private_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key)?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key, None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key)?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key)?, + }; + + Ok(( + public_descriptor.to_string(), + private_descriptor.to_string_with_secret(&keymap), + )) + }; + + let (external_pub, external_priv) = make_desc_key(0)?; + let (internal_pub, internal_priv) = make_desc_key(1)?; + + Ok(json!({ + "type": descriptor_type.to_string(), + "fingerprint": fingerprint.to_string(), + "network": network.to_string(), + "private_descriptors": { + "external": external_priv, + "internal": internal_priv + }, + "public_descriptors": { + "external": external_pub, + "internal": internal_pub + } + })) +} + +pub fn generate_descriptor_from_mnemonic_string( + mnemonic_str: &str, + network: Network, + derivation_path_str: &str, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str) + .map_err(|e| Error::Generic(e.to_string()))?; + let ext_key: ExtendedKey = mnemonic + .into_extended_key() + .map_err(|e| Error::Generic(e.to_string()))?; + let xprv = ext_key + .into_xprv(network) + .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; + + let _fingerprint = xprv.fingerprint(&secp); + let derivation_path: DerivationPath = derivation_path_str + .parse() + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + + let xprv = xprv + .derive_priv(&secp, &derivation_path) + .map_err(|e| Error::InvalidKey(format!("Failed to derive xprv: {e}")))?; + + generate_bip_descriptor_from_key( + &network, + &xprv.to_string(), + derivation_path_str, + descriptor_type, + ) +} + +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()) +} +// Enum for descriptor types +#[derive(Debug, Clone, Copy)] +pub enum DescriptorType { + Bip44, + Bip49, + Bip84, + Bip86, } From 1f77842ee7b415af581ca482de07cddf8cd5968c Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Wed, 24 Sep 2025 14:57:33 +0100 Subject: [PATCH 2/6] fix descriptors generation --- src/commands.rs | 11 +- src/error.rs | 28 +--- src/handlers.rs | 129 +++++------------- src/utils.rs | 344 ++++++++++++------------------------------------ 4 files changed, 125 insertions(+), 387 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 09cbc5dd..6983d6de 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -17,7 +17,11 @@ use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; -use clap::{Args, Parser, Subcommand, ValueEnum, builder::TypedValueParser, value_parser}; +use clap::{ + Args, Parser, Subcommand, ValueEnum, + builder::{PossibleValuesParser, TypedValueParser}, + value_parser, +}; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -488,7 +492,7 @@ pub enum DescriptorSubCommand { #[arg( long = "type", short = 't', - value_parser = clap::builder::PossibleValuesParser::new(["44", "49", "84", "86"]) + value_parser = PossibleValuesParser::new(["44", "49", "84", "86"]) .map(|s| s.parse::().unwrap()), default_value = "84" )] @@ -499,7 +503,4 @@ pub enum DescriptorSubCommand { /// Optional key input key: Option, }, - - /// Show info about a given descriptor - Info { descriptor: String }, } diff --git a/src/error.rs b/src/error.rs index 3916f261..1b8b5b45 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,8 +5,8 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum BDKCliError { - #[error("BIP39 error: {0}")] - BIP39Error(#[from] bdk_wallet::bip39::Error), + #[error("BIP39 error: {0:?}")] + BIP39Error(#[from] Option), #[error("BIP32 error: {0}")] BIP32Error(#[from] bdk_wallet::bitcoin::bip32::Error), @@ -103,30 +103,6 @@ pub enum BDKCliError { #[cfg(feature = "cbf")] #[error("BDK-Kyoto update error: {0}")] KyotoUpdateError(#[from] bdk_kyoto::UpdateError), - - #[error("Mnemonic generation failed: {0}")] - MnemonicGenerationError(String), - - #[error("Xpriv creation failed: {0}")] - XprivCreationError(String), - - #[error("Descriptor parsing failed: {0}")] - DescriptorParsingError(String), - - #[error("Invalid extended key (xpub): {0}")] - InvalidKey(String), - - #[error("Invalid derivation path: {0}")] - InvalidDerivationPath(String), - - #[error("Unsupported script type: {0}")] - UnsupportedScriptType(u8), - - #[error("Descriptor key conversion failed: {0}")] - DescriptorKeyError(String), - - #[error("Invalid arguments: {0}")] - InvalidArguments(String), } impl From for BDKCliError { diff --git a/src/handlers.rs b/src/handlers.rs index 9264e477..3787eebd 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -15,8 +15,6 @@ use crate::commands::*; use crate::error::BDKCliError as Error; #[cfg(any(feature = "sqlite", feature = "redb"))] use crate::persister::Persister; -#[cfg(feature = "cbf")] -use crate::utils::BlockchainClient::KyotoClient; use crate::utils::*; #[cfg(feature = "redb")] use bdk_redb::Store as RedbStore; @@ -35,27 +33,25 @@ use bdk_wallet::keys::{ bip39::WordCount, }; use bdk_wallet::miniscript::miniscript; -use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource}; -use bdk_wallet::bitcoin::consensus::encode::serialize_hex; -use bdk_wallet::bitcoin::script::PushBytesBuf; -use bdk_wallet::bitcoin::Network; -use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid}; -use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence}; -use bdk_wallet::descriptor::{Descriptor, Segwitv0}; -use bdk_wallet::keys::bip39::WordCount; #[cfg(feature = "sqlite")] use bdk_wallet::rusqlite::Connection; use bdk_wallet::{KeychainKind, SignOptions, Wallet}; #[cfg(feature = "compiler")] use bdk_wallet::{ + bitcoin::XOnlyPublicKey, descriptor::{Descriptor, Legacy, Miniscript}, - miniscript::{Tap, descriptor::TapTree, policy::Concrete}, descriptor::{Legacy, Miniscript}, miniscript::policy::Concrete, + miniscript::{Tap, descriptor::TapTree, policy::Concrete}, }; use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; use serde_json::json; -use bdk_wallet::{KeychainKind, SignOptions, Wallet}; +#[cfg(feature = "cbf")] +use { + crate::utils::BlockchainClient::KyotoClient, + bdk_kyoto::{Info, LightClient}, + tokio::select, +}; #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; @@ -64,29 +60,16 @@ use bdk_kyoto::LightClient; #[cfg(feature = "compiler")] use bdk_wallet::bitcoin::XOnlyPublicKey; use bdk_wallet::bitcoin::base64::prelude::*; -use bdk_wallet::keys::DescriptorKey::Secret; -use bdk_wallet::keys::{ - DerivableKey, DescriptorKey, DescriptorKey::Secret, DescriptorPublicKey, ExtendedKey, - GeneratableKey, GeneratedKey, bip39::WordCount, -}; -use bdk_wallet::miniscript::miniscript; -use serde_json::{Value, json}; +use serde_json::Value; use std::collections::BTreeMap; #[cfg(any(feature = "electrum", feature = "esplora"))] use std::collections::HashSet; use std::convert::TryFrom; -use std::fmt; #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))] use std::io::Write; use std::str::FromStr; - -#[cfg(feature = "electrum")] -use crate::utils::BlockchainClient::Electrum; -#[cfg(feature = "cbf")] -use bdk_kyoto::{Info, LightClient}; -use bdk_wallet::bitcoin::base64::prelude::*; -#[cfg(feature = "cbf")] -use tokio::select; +#[cfg(any(feature = "redb", feature = "compiler"))] +use std::sync::Arc; #[cfg(any( feature = "electrum", feature = "esplora", @@ -95,7 +78,7 @@ use tokio::select; ))] use { crate::commands::OnlineWalletSubCommand::*, - bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction}, + bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex}, }; #[cfg(feature = "esplora")] use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; @@ -1287,8 +1270,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { subcommand: descriptor_subcommand, } => { let network = cli_opts.network; - let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand) - .map_err(|e| Error::Generic(e.to_string()))?; + let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand)?; let json = serde_json::to_string_pretty(&descriptor)?; Ok(json) } @@ -1375,84 +1357,35 @@ pub fn handle_descriptor_subcommand( multipath, key, } => { - let (descriptor_type, derivation_path_str) = match r#type { - 44 => (DescriptorType::Bip44, "m/44h/1h/0h"), - 49 => (DescriptorType::Bip49, "m/49h/1h/0h"), - 84 => (DescriptorType::Bip84, "m/84h/1h/0h"), - 86 => (DescriptorType::Bip86, "m/86h/1h/0h"), - _ => return Err(Error::UnsupportedScriptType(r#type)), + let descriptor_type = match r#type { + 44 => DescriptorType::Bip44, + 49 => DescriptorType::Bip49, + 84 => DescriptorType::Bip84, + 86 => DescriptorType::Bip86, + _ => { + return Err(Error::Generic( + "Unsupported script type: {r#type}".to_string(), + )); + } }; match (multipath, key.as_ref()) { - (true, Some(k)) => generate_multipath_descriptor(&network, r#type, k), + // generate multipath descriptors + (true, Some(k)) => generate_descriptors(&network, descriptor_type, k, true), (false, Some(k)) => { if is_mnemonic(k) { - generate_descriptor_from_mnemonic_string( - k, - network, - derivation_path_str, - descriptor_type, - ) + // generate descriptors from given mnemonic string + generate_descriptor_from_mnemonic_string(k, network, descriptor_type) } else { - generate_standard_descriptor(&network, r#type, k) + // generate descriptors from key + generate_descriptors(&network, descriptor_type, k, false) } } + // generate mnemonic and descriptors (false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type), - _ => Err(Error::InvalidArguments( - "Provide a key or weak string".to_string(), - )), + _ => Err(Error::Generic("Provide a key or string".to_string())), } } - DescriptorSubCommand::Info { descriptor } => { - let parsed: Descriptor = descriptor - .parse() - .map_err(|e| Error::Generic(format!("Failed to parse descriptor: {}", e)))?; - - let checksum = parsed.to_string(); - let script_type = match parsed { - Descriptor::Wpkh(_) => "wpkh", - Descriptor::Pkh(_) => "pkh", - Descriptor::Sh(_) => "sh", - Descriptor::Tr(_) => "tr", - _ => "other", - }; - - let json = json!({ - "descriptor": checksum, - "type": script_type, - "is_multipath": descriptor.contains("/*"), - }); - - Ok(json) - } - } -} - -pub fn generate_standard_descriptor( - network: &Network, - script_type: u8, - key: &str, -) -> Result { - let descriptor_type = match script_type { - 44 => DescriptorType::Bip44, - 49 => DescriptorType::Bip49, - 84 => DescriptorType::Bip84, - 86 => DescriptorType::Bip86, - _ => return Err(Error::UnsupportedScriptType(script_type)), - }; - - generate_descriptor_from_key_by_type(network, key, descriptor_type) -} - -impl fmt::Display for DescriptorType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - DescriptorType::Bip44 => "bip44", - DescriptorType::Bip49 => "bip49", - DescriptorType::Bip84 => "bip84", - DescriptorType::Bip86 => "bip86", - }; - write!(f, "{}", s) } } diff --git a/src/utils.rs b/src/utils.rs index 93392ba5..3a4d2cb7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,10 +10,11 @@ //! //! This module includes all the utility tools used by the App. use crate::error::BDKCliError as Error; -use std::{fmt::Display, path::{Path, PathBuf}, str::FromStr}; -use std::str::FromStr; - -use std::path::{Path, PathBuf}; +use std::{ + fmt::{Display, Formatter}, + path::{Path, PathBuf}, + str::FromStr, +}; use crate::commands::WalletOpts; #[cfg(feature = "cbf")] @@ -22,6 +23,7 @@ use bdk_kyoto::{ builder::Builder, }; use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; +use bdk_wallet::miniscript::Legacy; #[cfg(any( feature = "electrum", @@ -36,7 +38,7 @@ use bdk_wallet::Wallet; use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; use bdk_wallet::bip39::{Language, Mnemonic}; -use bdk_wallet::bitcoin::bip32::ChildNumber; +use bdk_wallet::bitcoin::bip32::Fingerprint; use bdk_wallet::bitcoin::{ bip32::{DerivationPath, Xpriv, Xpub}, secp256k1::Secp256k1, @@ -378,161 +380,49 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box) -> Ok(()) } -pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { - let displayable = displayable.to_string(); - 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 generate_descriptor_from_key_by_type( - network: &Network, - key: &str, - descriptor_type: DescriptorType, -) -> Result { - let derivation_path = match descriptor_type { - DescriptorType::Bip44 => "m/44h/1h/0h", - DescriptorType::Bip49 => "m/49h/1h/0h", - DescriptorType::Bip84 => "m/84h/1h/0h", - DescriptorType::Bip86 => "m/86h/1h/0h", - }; - - generate_bip_descriptor_from_key(network, key, derivation_path, descriptor_type) -} - -pub fn generate_new_descriptor_with_mnemonic( - network: Network, - descriptor_type: DescriptorType, -) -> Result { - let secp = Secp256k1::new(); - - // Generate a new BIP39 mnemonic - let mnemonic: GeneratedKey = - Mnemonic::generate((WordCount::Words12, Language::English)).map_err(|e| { - Error::MnemonicGenerationError(format!("Mnemonic generation failed: {:?}", e)) - })?; - - let seed = mnemonic.to_seed(""); - let xprv = - Xpriv::new_master(network, &seed).map_err(|e| Error::XprivCreationError(e.to_string()))?; - - let origin = xprv.fingerprint(&secp); - - let (derivation_base, external_fmt, internal_fmt) = match descriptor_type { - DescriptorType::Bip44 => ("/44h/1h/0h", "pkh", "pkh"), - DescriptorType::Bip49 => ("/49h/1h/0h", "sh(wpkh", "sh(wpkh"), - DescriptorType::Bip84 => ("/84h/1h/0h", "wpkh", "wpkh"), - DescriptorType::Bip86 => ("/86h/1h/0h", "tr", "tr"), - }; - let path = DerivationPath::from_str(&format!("m{}", derivation_base)) - .map_err(|e| Error::Generic(e.to_string()))?; - - let derived_xprv = xprv - .derive_priv(&secp, &path) - .map_err(|e| Error::Generic(e.to_string()))?; - - let xprv_str = derived_xprv.to_string(); - - // Construct descriptors - let external_desc = match descriptor_type { - DescriptorType::Bip49 => format!( - "{}([{}{}]{}{}))", - external_fmt, origin, derivation_base, xprv_str, "/0" - ), - _ => format!( - "{}([{}{}]{}{})", - external_fmt, origin, derivation_base, xprv_str, "/0" - ), - }; - - let internal_desc = match descriptor_type { - DescriptorType::Bip49 => format!( - "{}([{}{}]{}{}))", - internal_fmt, origin, derivation_base, xprv_str, "/1" - ), - _ => format!( - "{}([{}{}]{}{})", - internal_fmt, origin, derivation_base, xprv_str, "/1" - ), - }; - - // Parse descriptors - let (ext_desc, ext_keymap) = - Descriptor::::parse_descriptor(&secp, &external_desc) - .map_err(|e| Error::DescriptorParsingError(e.to_string()))?; - - let (int_desc, int_keymap) = - Descriptor::::parse_descriptor(&secp, &internal_desc).map_err( - |e| { - Error::DescriptorParsingError(format!("Failed to parse internal descriptor: {}", e)) - }, - )?; - - Ok(serde_json::json!({ - "type": descriptor_type.to_string(), - "mnemonic": mnemonic.to_string(), - "private_descriptors": { - "external": ext_desc.to_string_with_secret(&ext_keymap), - "internal": int_desc.to_string_with_secret(&int_keymap), - }, - "public_descriptors": { - "external": ext_desc.to_string(), - "internal": int_desc.to_string(), - } - })) +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_multipath_descriptor( +pub fn generate_descriptors( network: &Network, - script_type: u8, + descriptor_type: DescriptorType, key: &str, + multipath_label: bool, ) -> Result { - use DescriptorType::*; - - let descriptor_type = match script_type { - 44 => Bip44, - 49 => Bip49, - 84 => Bip84, - 86 => Bip86, - _ => return Err(Error::UnsupportedScriptType(script_type)), - }; - type DescriptorConstructor = fn(DescriptorPublicKey) -> Result, Error>; - let (derivation_base, descriptor_constructor): (&str, DescriptorConstructor) = - match descriptor_type { - Bip44 => ("/44h/1h/0h", |key| { - Descriptor::new_pkh(key).map_err(Error::from) - }), - Bip49 => ("/49h/1h/0h", |key| { - Descriptor::new_sh_wpkh(key).map_err(Error::from) - }), - Bip84 => ("/84h/1h/0h", |key| { - Descriptor::new_wpkh(key).map_err(Error::from) - }), - Bip86 => ("/86h/1h/0h", |key| { - Descriptor::new_tr(key, None).map_err(Error::from) - }), - }; + let purpose = match descriptor_type { + DescriptorType::Bip44 => 44, + DescriptorType::Bip49 => 49, + DescriptorType::Bip84 => 84, + DescriptorType::Bip86 => 86, + }; + + let derivation_base = format!("/{purpose}h/1h/0h"); + + let descriptor_constructor: DescriptorConstructor = match descriptor_type { + DescriptorType::Bip44 => |key| Descriptor::new_pkh(key).map_err(Error::from), + DescriptorType::Bip49 => |key| Descriptor::new_sh_wpkh(key).map_err(Error::from), + DescriptorType::Bip84 => |key| Descriptor::new_wpkh(key).map_err(Error::from), + DescriptorType::Bip86 => |key| Descriptor::new_tr(key, None).map_err(Error::from), + }; let secp = Secp256k1::new(); - let derivation_path = DerivationPath::from_str(&format!("m{}", derivation_base)) - .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; - // Determine if it's an xprv or xpub let is_private = key.starts_with("xprv") || key.starts_with("tprv"); - // Use xprv or xpub accordingly type DescriptorBuilderFn = Box Result<(String, Option), Error>>; - let (fingerprint, make_desc): (_, DescriptorBuilderFn) = if is_private { - let xprv: Xpriv = key - .parse() - .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; + let (fingerprint, make_desc): (Fingerprint, DescriptorBuilderFn) = if is_private { + let xprv: Xpriv = key.parse()?; let fingerprint = xprv.fingerprint(&secp); let closure = move |change: u32| -> Result<(String, Option), Error> { - let branch_path = DerivationPath::from_str(&change.to_string()) - .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + let branch_path = DerivationPath::from_str(&change.to_string())?; let desc_xprv = DescriptorXKey { origin: Some((fingerprint, derivation_path.clone())), @@ -543,37 +433,32 @@ pub fn generate_multipath_descriptor( let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); let (desc_key, keymap, _) = match descriptor_type { - DescriptorType::Bip84 | DescriptorType::Bip49 | DescriptorType::Bip44 => { - IntoDescriptorKey::::into_descriptor_key(desc_secret) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))? - .extract(&secp) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + DescriptorType::Bip44 => { + IntoDescriptorKey::::into_descriptor_key(desc_secret)?.extract(&secp)? + } + DescriptorType::Bip84 | DescriptorType::Bip49 => { + IntoDescriptorKey::::into_descriptor_key(desc_secret)? + .extract(&secp)? + } + DescriptorType::Bip86 => { + IntoDescriptorKey::::into_descriptor_key(desc_secret)?.extract(&secp)? } - DescriptorType::Bip86 => IntoDescriptorKey::::into_descriptor_key(desc_secret) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))? - .extract(&secp) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))?, }; - let public_descriptor = descriptor_constructor(desc_key.clone())?; - let private_descriptor = descriptor_constructor(desc_key)?; - + let descriptor = descriptor_constructor(desc_key)?; Ok(( - public_descriptor.to_string(), - Some(private_descriptor.to_string_with_secret(&keymap)), + descriptor.to_string(), + Some(descriptor.to_string_with_secret(&keymap)), )) }; (fingerprint, Box::new(closure)) } else { - let xpub: Xpub = key - .parse() - .map_err(|e| Error::InvalidKey(format!("Invalid xpub: {e}")))?; + let xpub: Xpub = key.parse()?; let fingerprint = xpub.fingerprint(); let closure = move |change: u32| -> Result<(String, Option), Error> { - let branch_path = DerivationPath::from_str(&change.to_string()) - .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + let branch_path = DerivationPath::from_str(&change.to_string())?; let desc_xpub = DescriptorXKey { origin: Some((fingerprint, derivation_path.clone())), @@ -590,12 +475,17 @@ pub fn generate_multipath_descriptor( (fingerprint, Box::new(closure)) }; - // Build descriptors let (external_pub, external_priv) = make_desc(0)?; let (internal_pub, internal_priv) = make_desc(1)?; + let type_label = if multipath_label { + format!("{descriptor_type}-multipath") + } else { + descriptor_type.to_string() + }; + let mut result = json!({ - "type": format!("{}-multipath", descriptor_type), + "type": type_label, "public_descriptors": { "external": external_pub, "internal": internal_pub @@ -614,118 +504,37 @@ pub fn generate_multipath_descriptor( Ok(result) } -pub fn generate_bip_descriptor_from_key( - network: &Network, - key: &str, - derivation_path_str: &str, +pub fn generate_new_descriptor_with_mnemonic( + network: Network, descriptor_type: DescriptorType, ) -> Result { - let secp = Secp256k1::new(); - - let derivation_path: DerivationPath = derivation_path_str - .parse() - .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; - - let xprv: Xpriv = key - .parse() - .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; - - let fingerprint = xprv.fingerprint(&secp); - - let make_desc_key = |branch: u32| -> Result<(String, String), Error> { - let branch_path = DerivationPath::from(vec![ChildNumber::Normal { index: branch }]); - - let desc_xprv = DescriptorXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: xprv, - derivation_path: branch_path.clone(), - wildcard: Wildcard::Unhardened, - }; - - let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); - - let (desc_key, keymap, _) = - IntoDescriptorKey::::into_descriptor_key(desc_secret.clone()) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))? - .extract(&secp) - .map_err(|e| Error::DescriptorKeyError(e.to_string()))?; - - let public_descriptor = match descriptor_type { - DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key.clone())?, - DescriptorType::Bip86 => Descriptor::new_tr(desc_key.clone(), None)?, - DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key.clone())?, - DescriptorType::Bip44 => Descriptor::new_pkh(desc_key.clone())?, - }; - - let private_descriptor = match descriptor_type { - DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key)?, - DescriptorType::Bip86 => Descriptor::new_tr(desc_key, None)?, - DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key)?, - DescriptorType::Bip44 => Descriptor::new_pkh(desc_key)?, - }; - - Ok(( - public_descriptor.to_string(), - private_descriptor.to_string_with_secret(&keymap), - )) - }; + let mnemonic: GeneratedKey = + Mnemonic::generate((WordCount::Words12, Language::English)).map_err(Error::BIP39Error)?; - let (external_pub, external_priv) = make_desc_key(0)?; - let (internal_pub, internal_priv) = make_desc_key(1)?; + let seed = mnemonic.to_seed(""); + let xprv = Xpriv::new_master(network, &seed)?; - Ok(json!({ - "type": descriptor_type.to_string(), - "fingerprint": fingerprint.to_string(), - "network": network.to_string(), - "private_descriptors": { - "external": external_priv, - "internal": internal_priv - }, - "public_descriptors": { - "external": external_pub, - "internal": internal_pub - } - })) + let mut result = generate_descriptors(&network, descriptor_type, &xprv.to_string(), false)?; + result["mnemonic"] = json!(mnemonic.to_string()); + Ok(result) } pub fn generate_descriptor_from_mnemonic_string( mnemonic_str: &str, network: Network, - derivation_path_str: &str, descriptor_type: DescriptorType, ) -> Result { - let secp = Secp256k1::new(); - - let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str) - .map_err(|e| Error::Generic(e.to_string()))?; - let ext_key: ExtendedKey = mnemonic - .into_extended_key() - .map_err(|e| Error::Generic(e.to_string()))?; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?; + let ext_key: ExtendedKey = mnemonic.into_extended_key()?; let xprv = ext_key .into_xprv(network) .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; - let _fingerprint = xprv.fingerprint(&secp); - let derivation_path: DerivationPath = derivation_path_str - .parse() - .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; - - let xprv = xprv - .derive_priv(&secp, &derivation_path) - .map_err(|e| Error::InvalidKey(format!("Failed to derive xprv: {e}")))?; - - generate_bip_descriptor_from_key( - &network, - &xprv.to_string(), - derivation_path_str, - descriptor_type, - ) + let mut result = generate_descriptors(&network, descriptor_type, &xprv.to_string(), false)?; + result["mnemonic"] = json!(mnemonic_str); + Ok(result) } -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()) -} // Enum for descriptor types #[derive(Debug, Clone, Copy)] pub enum DescriptorType { @@ -734,3 +543,22 @@ pub enum DescriptorType { Bip84, Bip86, } + +impl Display for DescriptorType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + DescriptorType::Bip44 => "bip44", + DescriptorType::Bip49 => "bip49", + DescriptorType::Bip84 => "bip84", + DescriptorType::Bip86 => "bip86", + }; + write!(f, "{s}") + } +} + +pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { + let displayable = displayable.to_string(); + let start_str: &str = &displayable[0..start as usize]; + let end_str: &str = &displayable[displayable.len() - end as usize..]; + format!("{start_str}...{end_str}") +} From 550e81628779968bdbfedebc54debf8a6e20869e Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 26 Sep 2025 15:16:37 +0100 Subject: [PATCH 3/6] feat(descriptors): add multipath descs and pretty - add generating multipath descriptors - add pretty formatting for descriptors --- src/handlers.rs | 61 +++++---- src/utils.rs | 355 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 286 insertions(+), 130 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 3787eebd..fc580b71 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -19,6 +19,8 @@ use crate::utils::*; #[cfg(feature = "redb")] use bdk_redb::Store as RedbStore; use bdk_wallet::bip39::{Language, Mnemonic}; +use bdk_wallet::bitcoin::base64::Engine; +use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD; use bdk_wallet::bitcoin::{ Address, Amount, FeeRate, Network, Psbt, Sequence, Txid, bip32::{DerivationPath, KeySource}, @@ -40,8 +42,6 @@ use bdk_wallet::{KeychainKind, SignOptions, Wallet}; use bdk_wallet::{ bitcoin::XOnlyPublicKey, descriptor::{Descriptor, Legacy, Miniscript}, - descriptor::{Legacy, Miniscript}, - miniscript::policy::Concrete, miniscript::{Tap, descriptor::TapTree, policy::Concrete}, }; use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; @@ -1270,9 +1270,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { subcommand: descriptor_subcommand, } => { let network = cli_opts.network; - let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand)?; - let json = serde_json::to_string_pretty(&descriptor)?; - Ok(json) + let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand, pretty)?; + Ok(descriptor) } }; result @@ -1350,43 +1349,45 @@ fn readline() -> Result { pub fn handle_descriptor_subcommand( network: Network, subcommand: DescriptorSubCommand, -) -> Result { - match subcommand { + pretty: bool, +) -> Result { + let result = match subcommand { DescriptorSubCommand::Generate { r#type, multipath, key, } => { - let descriptor_type = match r#type { - 44 => DescriptorType::Bip44, - 49 => DescriptorType::Bip49, - 84 => DescriptorType::Bip84, - 86 => DescriptorType::Bip86, - _ => { - return Err(Error::Generic( - "Unsupported script type: {r#type}".to_string(), - )); + let descriptor_type = DescriptorType::from_bip32_num(r#type) + .ok_or_else(|| Error::Generic(format!("Unsupported script type: {type}")))?; + + match (multipath, key) { + // generate multipath descriptors with a key + (true, Some(key)) => { + if is_mnemonic(&key) { + return Err(Error::Generic( + "Mnemonic not supported for multipath descriptors".to_string(), + )); + } + generate_descriptors(descriptor_type, &key, true) } - }; - - match (multipath, key.as_ref()) { - // generate multipath descriptors - (true, Some(k)) => generate_descriptors(&network, descriptor_type, k, true), - (false, Some(k)) => { - if is_mnemonic(k) { - // generate descriptors from given mnemonic string - generate_descriptor_from_mnemonic_string(k, network, descriptor_type) + // generate descriptors with a key or mnemonic + (false, Some(key)) => { + if is_mnemonic(&key) { + generate_descriptor_from_mnemonic_string(&key, network, descriptor_type) } else { - // generate descriptors from key - generate_descriptors(&network, descriptor_type, k, false) + generate_descriptors(descriptor_type, &key, false) } } - // generate mnemonic and descriptors + // Generate new mnemonic and descriptors (false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type), - _ => Err(Error::Generic("Provide a key or string".to_string())), + // Invalid case + (true, None) => Err(Error::Generic( + "A key is required for multipath descriptors".to_string(), + )), } } - } + }?; + format_descriptor_output(&result, pretty) } #[cfg(any( diff --git a/src/utils.rs b/src/utils.rs index 3a4d2cb7..6a876dac 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -22,8 +22,16 @@ use bdk_kyoto::{ BuilderExt, Info, LightClient, Receiver, ScanType::Sync, UnboundedReceiver, Warning, builder::Builder, }; -use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; -use bdk_wallet::miniscript::Legacy; +use bdk_wallet::{ + bitcoin::secp256k1::All, + keys::{KeyMap, ValidNetworks}, + miniscript::Legacy, +}; +use bdk_wallet::{ + bitcoin::{Address, Network, OutPoint, ScriptBuf}, + miniscript::descriptor::{DerivPaths, DescriptorMultiXKey}, +}; +use cli_table::{Cell, CellStruct, Style, Table}; #[cfg(any( feature = "electrum", @@ -38,7 +46,6 @@ use bdk_wallet::Wallet; use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; use bdk_wallet::bip39::{Language, Mnemonic}; -use bdk_wallet::bitcoin::bip32::Fingerprint; use bdk_wallet::bitcoin::{ bip32::{DerivationPath, Xpriv, Xpub}, secp256k1::Secp256k1, @@ -384,124 +391,139 @@ 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( - network: &Network, +fn generate_multipath_descriptors( descriptor_type: DescriptorType, key: &str, - multipath_label: bool, + derivation_path: &DerivationPath, ) -> Result { - type DescriptorConstructor = - fn(DescriptorPublicKey) -> Result, Error>; - - let purpose = match descriptor_type { - DescriptorType::Bip44 => 44, - DescriptorType::Bip49 => 49, - DescriptorType::Bip84 => 84, - DescriptorType::Bip86 => 86, - }; - - let derivation_base = format!("/{purpose}h/1h/0h"); - - let descriptor_constructor: DescriptorConstructor = match descriptor_type { - DescriptorType::Bip44 => |key| Descriptor::new_pkh(key).map_err(Error::from), - DescriptorType::Bip49 => |key| Descriptor::new_sh_wpkh(key).map_err(Error::from), - DescriptorType::Bip84 => |key| Descriptor::new_wpkh(key).map_err(Error::from), - DescriptorType::Bip86 => |key| Descriptor::new_tr(key, None).map_err(Error::from), + let xpub: Xpub = key.parse()?; + let fingerprint = xpub.fingerprint(); + + let paths = vec![ + DerivationPath::from_str("m/0")?, + DerivationPath::from_str("m/1")?, + ]; + let deriv_paths = DerivPaths::new(paths) + .ok_or_else(|| Error::Generic("Empty derivation paths".to_string()))?; + + let desc_multi_xpub = DescriptorMultiXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xpub, + derivation_paths: deriv_paths, + wildcard: Wildcard::Unhardened, }; - let secp = Secp256k1::new(); - let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; - - let is_private = key.starts_with("xprv") || key.starts_with("tprv"); - - type DescriptorBuilderFn = Box Result<(String, Option), Error>>; + let desc_pub = DescriptorPublicKey::MultiXPub(desc_multi_xpub); + let descriptor = descriptor_type.constructor()(desc_pub)?; - let (fingerprint, make_desc): (Fingerprint, DescriptorBuilderFn) = if is_private { - let xprv: Xpriv = key.parse()?; - let fingerprint = xprv.fingerprint(&secp); - - let closure = move |change: u32| -> Result<(String, Option), Error> { - let branch_path = DerivationPath::from_str(&change.to_string())?; - - let desc_xprv = DescriptorXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: xprv, - derivation_path: branch_path, - wildcard: Wildcard::Unhardened, - }; - - let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); - let (desc_key, keymap, _) = match descriptor_type { - DescriptorType::Bip44 => { - IntoDescriptorKey::::into_descriptor_key(desc_secret)?.extract(&secp)? - } - DescriptorType::Bip84 | DescriptorType::Bip49 => { - IntoDescriptorKey::::into_descriptor_key(desc_secret)? - .extract(&secp)? - } - DescriptorType::Bip86 => { - IntoDescriptorKey::::into_descriptor_key(desc_secret)?.extract(&secp)? - } - }; + Ok(json!({ + "multipath_descriptor": descriptor.to_string(), + "fingerprint": fingerprint.to_string() + })) +} - let descriptor = descriptor_constructor(desc_key)?; - Ok(( - descriptor.to_string(), - Some(descriptor.to_string_with_secret(&keymap)), - )) +fn generate_private_descriptors( + descriptor_type: DescriptorType, + key: &str, + derivation_path: &DerivationPath, + secp: &Secp256k1, +) -> Result { + let xprv: Xpriv = key.parse()?; + let fingerprint = xprv.fingerprint(secp); + + let build_descriptor = |branch: &str| -> Result<(String, Option), Error> { + let branch_path = DerivationPath::from_str(branch)?; + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xprv, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, }; + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv); - (fingerprint, Box::new(closure)) - } else { - let xpub: Xpub = key.parse()?; - let fingerprint = xpub.fingerprint(); + let (desc_key, keymap, _) = descriptor_type.extract_descriptor_key(desc_secret, secp)?; + let descriptor = descriptor_type.constructor()(desc_key)?; - let closure = move |change: u32| -> Result<(String, Option), Error> { - let branch_path = DerivationPath::from_str(&change.to_string())?; + Ok(( + descriptor.to_string(), + Some(descriptor.to_string_with_secret(&keymap)), + )) + }; - let desc_xpub = DescriptorXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: xpub, - derivation_path: branch_path, - wildcard: Wildcard::Unhardened, - }; + let (external_pub, external_priv) = build_descriptor("0")?; + let (internal_pub, internal_priv) = build_descriptor("1")?; - let desc_key = DescriptorPublicKey::XPub(desc_xpub); - let descriptor = descriptor_constructor(desc_key)?; - Ok((descriptor.to_string(), None)) - }; + Ok(json!({ + "public_descriptors": { + "external": external_pub, + "internal": internal_pub + }, + "private_descriptors": { + "external": external_priv.unwrap(), + "internal": internal_priv.unwrap() + }, + "fingerprint": fingerprint.to_string() + })) +} - (fingerprint, Box::new(closure)) +fn generate_public_descriptors( + descriptor_type: DescriptorType, + key: &str, + derivation_path: &DerivationPath, +) -> Result { + let xpub: Xpub = key.parse()?; + let fingerprint = xpub.fingerprint(); + + let build_descriptor = |branch: &str| -> Result { + 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 = descriptor_type.constructor()(desc_pub)?; + Ok(descriptor.to_string()) }; - let (external_pub, external_priv) = make_desc(0)?; - let (internal_pub, internal_priv) = make_desc(1)?; - - let type_label = if multipath_label { - format!("{descriptor_type}-multipath") - } else { - descriptor_type.to_string() - }; + let external_pub = build_descriptor("0")?; + let internal_pub = build_descriptor("1")?; - let mut result = json!({ - "type": type_label, + Ok(json!({ "public_descriptors": { "external": external_pub, "internal": internal_pub }, - "fingerprint": fingerprint.to_string(), - "network": network.to_string(), - }); + "fingerprint": fingerprint.to_string() + })) +} + +pub fn generate_descriptors( + descriptor_type: DescriptorType, + key: &str, + multipath: bool, +) -> Result { + let secp = Secp256k1::new(); + let derivation_base = format!("/{0}h/1h/0h", descriptor_type.purpose()); + let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; + + let is_private = key.starts_with("xprv") || key.starts_with("tprv"); - if let (Some(priv_ext), Some(priv_int)) = (external_priv, internal_priv) { - result["private_descriptors"] = json!({ - "external": priv_ext, - "internal": priv_int - }); + if multipath { + if is_private { + return Err(Error::Generic( + "Multipath descriptors are only supported for public keys".to_string(), + )); + } + return generate_multipath_descriptors(descriptor_type, key, &derivation_path); } - Ok(result) + if is_private { + generate_private_descriptors(descriptor_type, key, &derivation_path, &secp) + } else { + generate_public_descriptors(descriptor_type, key, &derivation_path) + } } pub fn generate_new_descriptor_with_mnemonic( @@ -514,7 +536,7 @@ pub fn generate_new_descriptor_with_mnemonic( let seed = mnemonic.to_seed(""); let xprv = Xpriv::new_master(network, &seed)?; - let mut result = generate_descriptors(&network, descriptor_type, &xprv.to_string(), false)?; + let mut result = generate_descriptors(descriptor_type, &xprv.to_string(), false)?; result["mnemonic"] = json!(mnemonic.to_string()); Ok(result) } @@ -530,7 +552,7 @@ pub fn generate_descriptor_from_mnemonic_string( .into_xprv(network) .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; - let mut result = generate_descriptors(&network, descriptor_type, &xprv.to_string(), false)?; + let mut result = generate_descriptors(descriptor_type, &xprv.to_string(), false)?; result["mnemonic"] = json!(mnemonic_str); Ok(result) } @@ -544,6 +566,60 @@ pub enum DescriptorType { Bip86, } +impl DescriptorType { + fn purpose(&self) -> u32 { + match self { + DescriptorType::Bip44 => 44, + DescriptorType::Bip49 => 49, + DescriptorType::Bip84 => 84, + DescriptorType::Bip86 => 86, + } + } + + pub fn from_bip32_num(bip32_purpose: u8) -> Option { + match bip32_purpose { + 44 => Some(DescriptorType::Bip44), + 49 => Some(DescriptorType::Bip49), + 84 => Some(DescriptorType::Bip84), + 86 => Some(DescriptorType::Bip86), + _ => None, + } + } + + fn constructor( + &self, + ) -> fn(DescriptorPublicKey) -> Result, Error> { + match self { + DescriptorType::Bip44 => |key| Descriptor::new_pkh(key).map_err(Error::from), + DescriptorType::Bip49 => |key| Descriptor::new_sh_wpkh(key).map_err(Error::from), + DescriptorType::Bip84 => |key| Descriptor::new_wpkh(key).map_err(Error::from), + DescriptorType::Bip86 => |key| Descriptor::new_tr(key, None).map_err(Error::from), + } + } + + fn extract_descriptor_key( + &self, + desc_secret: DescriptorSecretKey, + secp: &Secp256k1, + ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), Error> { + Ok(match self { + DescriptorType::Bip44 => { + let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + DescriptorType::Bip49 | DescriptorType::Bip84 => { + let descriptor_key = + IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + DescriptorType::Bip86 => { + let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + }) + } +} + impl Display for DescriptorType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let s = match self { @@ -562,3 +638,82 @@ pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { let end_str: &str = &displayable[displayable.len() - end as usize..]; format!("{start_str}...{end_str}") } + +pub fn format_descriptor_output(result: &Value, pretty: bool) -> Result { + if !pretty { + return Ok(serde_json::to_string_pretty(result)?); + } + + let mut rows: Vec> = 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}")) +} From c8f23ba5138a5c9c2e2f8df88d5913ab458a4b8f Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Mon, 3 Nov 2025 22:02:56 +0100 Subject: [PATCH 4/6] feat(descriptors): fix descriptor generation - remove multipath descriptor generation - fix descriptor type --- src/commands.rs | 16 +-- src/handlers.rs | 45 ++------ src/utils.rs | 295 +++++++++++++++++++----------------------------- 3 files changed, 128 insertions(+), 228 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 6983d6de..2b82c4b2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -17,11 +17,7 @@ use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; -use clap::{ - Args, Parser, Subcommand, ValueEnum, - builder::{PossibleValuesParser, TypedValueParser}, - value_parser, -}; +use clap::{Args, Parser, Subcommand, ValueEnum, value_parser}; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -492,14 +488,10 @@ pub enum DescriptorSubCommand { #[arg( long = "type", short = 't', - value_parser = PossibleValuesParser::new(["44", "49", "84", "86"]) - .map(|s| s.parse::().unwrap()), - default_value = "84" + value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"], + default_value = "wsh" )] - r#type: u8, - /// Enable multipath descriptors - #[arg(long = "multipath", short = 'm', default_value_t = false)] - multipath: bool, + desc_type: String, /// Optional key input key: Option, }, diff --git a/src/handlers.rs b/src/handlers.rs index fc580b71..b89ee7d3 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -47,20 +47,10 @@ use bdk_wallet::{ use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; use serde_json::json; #[cfg(feature = "cbf")] -use { - crate::utils::BlockchainClient::KyotoClient, - bdk_kyoto::{Info, LightClient}, - tokio::select, -}; +use {crate::utils::BlockchainClient::KyotoClient, bdk_kyoto::LightClient, tokio::select}; #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; -#[cfg(feature = "cbf")] -use bdk_kyoto::LightClient; -#[cfg(feature = "compiler")] -use bdk_wallet::bitcoin::XOnlyPublicKey; -use bdk_wallet::bitcoin::base64::prelude::*; -use serde_json::Value; use std::collections::BTreeMap; #[cfg(any(feature = "electrum", feature = "esplora"))] use std::collections::HashSet; @@ -1352,44 +1342,23 @@ pub fn handle_descriptor_subcommand( pretty: bool, ) -> Result { let result = match subcommand { - DescriptorSubCommand::Generate { - r#type, - multipath, - key, - } => { - let descriptor_type = DescriptorType::from_bip32_num(r#type) - .ok_or_else(|| Error::Generic(format!("Unsupported script type: {type}")))?; - - match (multipath, key) { - // generate multipath descriptors with a key - (true, Some(key)) => { - if is_mnemonic(&key) { - return Err(Error::Generic( - "Mnemonic not supported for multipath descriptors".to_string(), - )); - } - generate_descriptors(descriptor_type, &key, true) - } + DescriptorSubCommand::Generate { desc_type, key } => { + match key { // generate descriptors with a key or mnemonic - (false, Some(key)) => { + Some(key) => { if is_mnemonic(&key) { - generate_descriptor_from_mnemonic_string(&key, network, descriptor_type) + generate_descriptor_from_mnemonic(&key, network, &desc_type) } else { - generate_descriptors(descriptor_type, &key, false) + generate_descriptors(&desc_type, &key, network) } } // Generate new mnemonic and descriptors - (false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type), - // Invalid case - (true, None) => Err(Error::Generic( - "A key is required for multipath descriptors".to_string(), - )), + None => generate_descriptor_with_mnemonic(network, &desc_type), } } }?; format_descriptor_output(&result, pretty) } - #[cfg(any( feature = "electrum", feature = "esplora", diff --git a/src/utils.rs b/src/utils.rs index 6a876dac..6614b78c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,9 +11,10 @@ //! This module includes all the utility tools used by the App. use crate::error::BDKCliError as Error; use std::{ - fmt::{Display, Formatter}, + fmt::Display, path::{Path, PathBuf}, str::FromStr, + sync::Arc, }; use crate::commands::WalletOpts; @@ -24,12 +25,8 @@ use bdk_kyoto::{ }; use bdk_wallet::{ bitcoin::secp256k1::All, - keys::{KeyMap, ValidNetworks}, - miniscript::Legacy, -}; -use bdk_wallet::{ - bitcoin::{Address, Network, OutPoint, ScriptBuf}, - miniscript::descriptor::{DerivPaths, DescriptorMultiXKey}, + keys::{IntoDescriptorKey, KeyMap}, + miniscript::{Legacy, Miniscript, Terminal}, }; use cli_table::{Cell, CellStruct, Style, Table}; @@ -47,6 +44,7 @@ use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; use bdk_wallet::bip39::{Language, Mnemonic}; use bdk_wallet::bitcoin::{ + Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv, Xpub}, secp256k1::Secp256k1, }; @@ -54,9 +52,7 @@ use bdk_wallet::descriptor::{ Segwitv0, {Descriptor, DescriptorPublicKey}, }; use bdk_wallet::keys::{ - DerivableKey, ExtendedKey, - bip39::WordCount, - {DescriptorSecretKey, GeneratableKey, GeneratedKey, IntoDescriptorKey}, + DerivableKey, DescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey, bip39::WordCount, }; use bdk_wallet::miniscript::{ Tap, @@ -387,67 +383,119 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box) -> Ok(()) } +pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { + let displayable = displayable.to_string(); + 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()) } -fn generate_multipath_descriptors( - descriptor_type: DescriptorType, - key: &str, - derivation_path: &DerivationPath, -) -> Result { - let xpub: Xpub = key.parse()?; - let fingerprint = xpub.fingerprint(); - let paths = vec![ - DerivationPath::from_str("m/0")?, - DerivationPath::from_str("m/1")?, - ]; - let deriv_paths = DerivPaths::new(paths) - .ok_or_else(|| Error::Generic("Empty derivation paths".to_string()))?; - - let desc_multi_xpub = DescriptorMultiXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: xpub, - derivation_paths: deriv_paths, - wildcard: Wildcard::Unhardened, +pub fn extract_keymap( + desc_type: &str, + desc_secret: DescriptorSecretKey, + secp: &Secp256k1, +) -> Result<(DescriptorPublicKey, KeyMap), Error> { + let (desc_pub, keymap, _) = match desc_type.to_lowercase().as_str() { + "pkh" => { + let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + "wpkh" | "sh" | "wsh" => { + let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + "tr" => { + let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; + descriptor_key.extract(secp)? + } + _ => { + return Err(Error::Generic(format!( + "Unsupported descriptor type: {desc_type}" + ))); + } }; + Ok((desc_pub, keymap)) +} - let desc_pub = DescriptorPublicKey::MultiXPub(desc_multi_xpub); - let descriptor = descriptor_type.constructor()(desc_pub)?; +pub fn build_public_descriptor( + desc_type: &str, + key: DescriptorPublicKey, +) -> Result, 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 = + 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}" + ))), + } +} - Ok(json!({ - "multipath_descriptor": descriptor.to_string(), - "fingerprint": fingerprint.to_string() - })) +pub fn generate_descriptors(desc_type: &str, key: &str, network: Network) -> Result { + let secp = Secp256k1::new(); + 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_base = format!("/{purpose}h/{coin_type}h/0h"); + let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; + + let is_private = key.starts_with("xprv") || key.starts_with("tprv"); + + if is_private { + generate_private_descriptors(desc_type, key, &derivation_path, &secp) + } else { + generate_public_descriptors(desc_type, key, &derivation_path) + } } fn generate_private_descriptors( - descriptor_type: DescriptorType, + desc_type: &str, key: &str, - derivation_path: &DerivationPath, + account_path: &DerivationPath, secp: &Secp256k1, ) -> Result { let xprv: Xpriv = key.parse()?; let fingerprint = xprv.fingerprint(secp); - let build_descriptor = |branch: &str| -> Result<(String, Option), Error> { + let account_xprv = xprv.derive_priv(secp, account_path)?; + + let build_descriptor = |branch: &str| -> Result<(String, String), Error> { let branch_path = DerivationPath::from_str(branch)?; + let desc_xprv = DescriptorXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: xprv, + origin: Some((fingerprint, account_path.clone())), + xkey: account_xprv, derivation_path: branch_path, wildcard: Wildcard::Unhardened, }; let desc_secret = DescriptorSecretKey::XPrv(desc_xprv); - let (desc_key, keymap, _) = descriptor_type.extract_descriptor_key(desc_secret, secp)?; - let descriptor = descriptor_type.constructor()(desc_key)?; + let (desc_pub, keymap) = extract_keymap(desc_type, desc_secret, secp)?; + let descriptor = build_public_descriptor(desc_type, desc_pub)?; + let public_str = descriptor.to_string(); + let private_str = descriptor.to_string_with_secret(&keymap); - Ok(( - descriptor.to_string(), - Some(descriptor.to_string_with_secret(&keymap)), - )) + Ok((public_str, private_str)) }; let (external_pub, external_priv) = build_descriptor("0")?; @@ -459,15 +507,30 @@ fn generate_private_descriptors( "internal": internal_pub }, "private_descriptors": { - "external": external_priv.unwrap(), - "internal": internal_priv.unwrap() + "external": external_priv, + "internal": internal_priv }, "fingerprint": fingerprint.to_string() })) } -fn generate_public_descriptors( - descriptor_type: DescriptorType, +pub fn generate_descriptor_with_mnemonic( + network: Network, + desc_type: &str, +) -> Result { + let mnemonic: GeneratedKey = + 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) +} + +pub fn generate_public_descriptors( + desc_type: &str, key: &str, derivation_path: &DerivationPath, ) -> Result { @@ -483,7 +546,7 @@ fn generate_public_descriptors( wildcard: Wildcard::Unhardened, }; let desc_pub = DescriptorPublicKey::XPub(desc_xpub); - let descriptor = descriptor_type.constructor()(desc_pub)?; + let descriptor = build_public_descriptor(desc_type, desc_pub)?; Ok(descriptor.to_string()) }; @@ -499,52 +562,10 @@ fn generate_public_descriptors( })) } -pub fn generate_descriptors( - descriptor_type: DescriptorType, - key: &str, - multipath: bool, -) -> Result { - let secp = Secp256k1::new(); - let derivation_base = format!("/{0}h/1h/0h", descriptor_type.purpose()); - let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; - - let is_private = key.starts_with("xprv") || key.starts_with("tprv"); - - if multipath { - if is_private { - return Err(Error::Generic( - "Multipath descriptors are only supported for public keys".to_string(), - )); - } - return generate_multipath_descriptors(descriptor_type, key, &derivation_path); - } - - if is_private { - generate_private_descriptors(descriptor_type, key, &derivation_path, &secp) - } else { - generate_public_descriptors(descriptor_type, key, &derivation_path) - } -} - -pub fn generate_new_descriptor_with_mnemonic( - network: Network, - descriptor_type: DescriptorType, -) -> Result { - let mnemonic: GeneratedKey = - 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(descriptor_type, &xprv.to_string(), false)?; - result["mnemonic"] = json!(mnemonic.to_string()); - Ok(result) -} - -pub fn generate_descriptor_from_mnemonic_string( +pub fn generate_descriptor_from_mnemonic( mnemonic_str: &str, network: Network, - descriptor_type: DescriptorType, + desc_type: &str, ) -> Result { let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?; let ext_key: ExtendedKey = mnemonic.into_extended_key()?; @@ -552,93 +573,11 @@ pub fn generate_descriptor_from_mnemonic_string( .into_xprv(network) .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; - let mut result = generate_descriptors(descriptor_type, &xprv.to_string(), false)?; + let mut result = generate_descriptors(desc_type, &xprv.to_string(), network)?; result["mnemonic"] = json!(mnemonic_str); Ok(result) } -// Enum for descriptor types -#[derive(Debug, Clone, Copy)] -pub enum DescriptorType { - Bip44, - Bip49, - Bip84, - Bip86, -} - -impl DescriptorType { - fn purpose(&self) -> u32 { - match self { - DescriptorType::Bip44 => 44, - DescriptorType::Bip49 => 49, - DescriptorType::Bip84 => 84, - DescriptorType::Bip86 => 86, - } - } - - pub fn from_bip32_num(bip32_purpose: u8) -> Option { - match bip32_purpose { - 44 => Some(DescriptorType::Bip44), - 49 => Some(DescriptorType::Bip49), - 84 => Some(DescriptorType::Bip84), - 86 => Some(DescriptorType::Bip86), - _ => None, - } - } - - fn constructor( - &self, - ) -> fn(DescriptorPublicKey) -> Result, Error> { - match self { - DescriptorType::Bip44 => |key| Descriptor::new_pkh(key).map_err(Error::from), - DescriptorType::Bip49 => |key| Descriptor::new_sh_wpkh(key).map_err(Error::from), - DescriptorType::Bip84 => |key| Descriptor::new_wpkh(key).map_err(Error::from), - DescriptorType::Bip86 => |key| Descriptor::new_tr(key, None).map_err(Error::from), - } - } - - fn extract_descriptor_key( - &self, - desc_secret: DescriptorSecretKey, - secp: &Secp256k1, - ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), Error> { - Ok(match self { - DescriptorType::Bip44 => { - let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - DescriptorType::Bip49 | DescriptorType::Bip84 => { - let descriptor_key = - IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - DescriptorType::Bip86 => { - let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - }) - } -} - -impl Display for DescriptorType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = match self { - DescriptorType::Bip44 => "bip44", - DescriptorType::Bip49 => "bip49", - DescriptorType::Bip84 => "bip84", - DescriptorType::Bip86 => "bip86", - }; - write!(f, "{s}") - } -} - -pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { - let displayable = displayable.to_string(); - 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 format_descriptor_output(result: &Value, pretty: bool) -> Result { if !pretty { return Ok(serde_json::to_string_pretty(result)?); From 32a2c8847f1574fa7659a568a29ecf180b175d52 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 4 Nov 2025 18:58:03 +0100 Subject: [PATCH 5/6] feat(desc):update descriptors gen to use templates --- src/handlers.rs | 3 +- src/utils.rs | 221 +++++++++++++++++++++--------------------------- 2 files changed, 100 insertions(+), 124 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index b89ee7d3..b39712ee 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1344,11 +1344,12 @@ pub fn handle_descriptor_subcommand( let result = match subcommand { DescriptorSubCommand::Generate { desc_type, key } => { match key { - // generate descriptors with a key or mnemonic Some(key) => { if is_mnemonic(&key) { + // User provided mnemonic generate_descriptor_from_mnemonic(&key, network, &desc_type) } else { + // User provided xprv/xpub generate_descriptors(&desc_type, &key, network) } } diff --git a/src/utils.rs b/src/utils.rs index 6614b78c..8a3ee040 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,9 +24,14 @@ use bdk_kyoto::{ builder::Builder, }; use bdk_wallet::{ - bitcoin::secp256k1::All, - keys::{IntoDescriptorKey, KeyMap}, - miniscript::{Legacy, Miniscript, Terminal}, + KeychainKind, + bitcoin::bip32::{DerivationPath, Xpub}, + keys::DescriptorPublicKey, + miniscript::{ + Descriptor, Miniscript, Terminal, + descriptor::{DescriptorXKey, Wildcard}, + }, + template::DescriptorTemplate, }; use cli_table::{Cell, CellStruct, Style, Table}; @@ -40,24 +45,14 @@ use crate::commands::ClientType; use bdk_wallet::Wallet; #[cfg(any(feature = "sqlite", feature = "redb"))] -use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; +use bdk_wallet::{PersistedWallet, WalletPersister}; use bdk_wallet::bip39::{Language, Mnemonic}; use bdk_wallet::bitcoin::{ - Address, Network, OutPoint, ScriptBuf, - bip32::{DerivationPath, Xpriv, Xpub}, - secp256k1::Secp256k1, -}; -use bdk_wallet::descriptor::{ - Segwitv0, {Descriptor, DescriptorPublicKey}, -}; -use bdk_wallet::keys::{ - DerivableKey, DescriptorSecretKey, ExtendedKey, GeneratableKey, GeneratedKey, bip39::WordCount, -}; -use bdk_wallet::miniscript::{ - Tap, - descriptor::{DescriptorXKey, Wildcard}, + 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}; /// Parse the recipient (Address,Amount) argument from cli input. @@ -395,111 +390,68 @@ pub fn is_mnemonic(s: &str) -> bool { (12..=24).contains(&word_count) && s.chars().all(|c| c.is_alphanumeric() || c.is_whitespace()) } -pub fn extract_keymap( - desc_type: &str, - desc_secret: DescriptorSecretKey, - secp: &Secp256k1, -) -> Result<(DescriptorPublicKey, KeyMap), Error> { - let (desc_pub, keymap, _) = match desc_type.to_lowercase().as_str() { - "pkh" => { - let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - "wpkh" | "sh" | "wsh" => { - let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - "tr" => { - let descriptor_key = IntoDescriptorKey::::into_descriptor_key(desc_secret)?; - descriptor_key.extract(secp)? - } - _ => { - return Err(Error::Generic(format!( - "Unsupported descriptor type: {desc_type}" - ))); - } - }; - Ok((desc_pub, keymap)) -} - -pub fn build_public_descriptor( - desc_type: &str, - key: DescriptorPublicKey, -) -> Result, 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 = - 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}" - ))), - } -} - pub fn generate_descriptors(desc_type: &str, key: &str, network: Network) -> Result { - let secp = Secp256k1::new(); - 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_base = format!("/{purpose}h/{coin_type}h/0h"); - let derivation_path = DerivationPath::from_str(&format!("m{derivation_base}"))?; - let is_private = key.starts_with("xprv") || key.starts_with("tprv"); if is_private { - generate_private_descriptors(desc_type, key, &derivation_path, &secp) + 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, - account_path: &DerivationPath, - secp: &Secp256k1, + network: Network, ) -> Result { - let xprv: Xpriv = key.parse()?; - let fingerprint = xprv.fingerprint(secp); + use bdk_wallet::template::{Bip44, Bip49, Bip84, Bip86}; - let account_xprv = xprv.derive_priv(secp, account_path)?; - - let build_descriptor = |branch: &str| -> Result<(String, String), Error> { - let branch_path = DerivationPath::from_str(branch)?; - - let desc_xprv = DescriptorXKey { - origin: Some((fingerprint, account_path.clone())), - xkey: account_xprv, - derivation_path: branch_path, - wildcard: Wildcard::Unhardened, - }; - let desc_secret = DescriptorSecretKey::XPrv(desc_xprv); + let secp = Secp256k1::new(); + let xprv: Xpriv = key.parse()?; + let fingerprint = xprv.fingerprint(&secp); - let (desc_pub, keymap) = extract_keymap(desc_type, desc_secret, secp)?; - let descriptor = build_public_descriptor(desc_type, desc_pub)?; - let public_str = descriptor.to_string(); - let private_str = descriptor.to_string_with_secret(&keymap); + 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}" + ))); + } + }; - Ok((public_str, private_str)) + 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_pub, external_priv) = build_descriptor("0")?; - let (internal_pub, internal_priv) = build_descriptor("1")?; + 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": { @@ -514,21 +466,7 @@ fn generate_private_descriptors( })) } -pub fn generate_descriptor_with_mnemonic( - network: Network, - desc_type: &str, -) -> Result { - let mnemonic: GeneratedKey = - 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 public key (xpub/tpub) pub fn generate_public_descriptors( desc_type: &str, key: &str, @@ -562,16 +500,53 @@ pub fn generate_public_descriptors( })) } +/// Build a descriptor from a public key +pub fn build_public_descriptor( + desc_type: &str, + key: DescriptorPublicKey, +) -> Result, 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 = + 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 { + let mnemonic: GeneratedKey = + 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 { let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?; - let ext_key: ExtendedKey = mnemonic.into_extended_key()?; - let xprv = ext_key - .into_xprv(network) - .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; + 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); From 7d3720e382410af5ebe18aa48c5c819bf21a1ec3 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Wed, 5 Nov 2025 18:41:04 +0100 Subject: [PATCH 6/6] feat(desc): remove generate subcommand - remove generate subcommand - add descriptor to the repl --- src/commands.rs | 26 +++++++++++++++----------- src/handlers.rs | 43 ++++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 2b82c4b2..d3f2d983 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -111,8 +111,16 @@ pub enum CliSubCommand { /// Generate output descriptors from either extended key (Xprv/Xpub) or mnemonic phrase. /// This feature is intended for development and testing purposes only. Descriptor { - #[clap(subcommand)] - subcommand: DescriptorSubCommand, + /// 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, }, } /// Wallet operation subcommands. @@ -476,14 +484,8 @@ pub enum ReplSubCommand { #[command(subcommand)] subcommand: KeySubCommand, }, - /// Exit REPL loop. - Exit, -} -/// Subcommands for Key operations. -#[derive(Debug, Subcommand, Clone, PartialEq, Eq)] -pub enum DescriptorSubCommand { - /// Generate a descriptor - Generate { + /// Generate descriptors + Descriptor { /// Descriptor type (script type). #[arg( long = "type", @@ -492,7 +494,9 @@ pub enum DescriptorSubCommand { default_value = "wsh" )] desc_type: String, - /// Optional key input + /// Optional key: xprv, xpub, or mnemonic phrase key: Option, }, + /// Exit REPL loop. + Exit, } diff --git a/src/handlers.rs b/src/handlers.rs index b39712ee..6c58a838 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1256,11 +1256,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } Ok("".to_string()) } - CliSubCommand::Descriptor { - subcommand: descriptor_subcommand, - } => { - let network = cli_opts.network; - let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand, pretty)?; + CliSubCommand::Descriptor { desc_type, key } => { + let descriptor = handle_descriptor_command(cli_opts.network, desc_type, key, pretty)?; Ok(descriptor) } }; @@ -1310,6 +1307,11 @@ async fn respond( .map_err(|e| e.to_string())?; Some(value) } + ReplSubCommand::Descriptor { desc_type, key } => { + let value = handle_descriptor_command(network, desc_type, key, cli_opts.pretty) + .map_err(|e| e.to_string())?; + Some(value) + } ReplSubCommand::Exit => None, }; if let Some(value) = response { @@ -1336,30 +1338,29 @@ fn readline() -> Result { Ok(buffer) } -pub fn handle_descriptor_subcommand( +/// Handle the descriptor command +pub fn handle_descriptor_command( network: Network, - subcommand: DescriptorSubCommand, + desc_type: String, + key: Option, pretty: bool, ) -> Result { - let result = match subcommand { - DescriptorSubCommand::Generate { desc_type, key } => { - match key { - Some(key) => { - if is_mnemonic(&key) { - // User provided mnemonic - generate_descriptor_from_mnemonic(&key, network, &desc_type) - } else { - // User provided xprv/xpub - generate_descriptors(&desc_type, &key, network) - } - } - // Generate new mnemonic and descriptors - None => generate_descriptor_with_mnemonic(network, &desc_type), + let result = match key { + Some(key) => { + if is_mnemonic(&key) { + // User provided mnemonic + generate_descriptor_from_mnemonic(&key, network, &desc_type) + } else { + // User provided xprv/xpub + generate_descriptors(&desc_type, &key, network) } } + // Generate new mnemonic and descriptors + None => generate_descriptor_with_mnemonic(network, &desc_type), }?; format_descriptor_output(&result, pretty) } + #[cfg(any( feature = "electrum", feature = "esplora",