From: Vihiga Tyonum Date: Fri, 26 Sep 2025 14:16:37 +0000 (+0100) Subject: feat(descriptors): add multipath descs and pretty X-Git-Url: http://internal-gitweb-vhost/?a=commitdiff_plain;h=550e81628779968bdbfedebc54debf8a6e20869e;p=bdk-cli feat(descriptors): add multipath descs and pretty - add generating multipath descriptors - add pretty formatting for descriptors --- diff --git a/src/handlers.rs b/src/handlers.rs index 3787eeb..fc580b7 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 3a4d2cb..6a876da 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}")) +}