]> Untitled Git - bdk-cli/commitdiff
feat(descriptors): add multipath descs and pretty
authorVihiga Tyonum <withtvpeter@gmail.com>
Fri, 26 Sep 2025 14:16:37 +0000 (15:16 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Mon, 3 Nov 2025 10:43:40 +0000 (11:43 +0100)
- add generating multipath descriptors
- add pretty formatting for descriptors

src/handlers.rs
src/utils.rs

index 3787eebdff75e285fa99493362608c27c684143d..fc580b71c2a99542ec7ee57147970651c70135d8 100644 (file)
@@ -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<String, Error> {
             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<String, Error> {
 pub fn handle_descriptor_subcommand(
     network: Network,
     subcommand: DescriptorSubCommand,
-) -> Result<Value, Error> {
-    match subcommand {
+    pretty: bool,
+) -> Result<String, Error> {
+    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(
index 3a4d2cb7dfffafb6c2673821c10127b2542e9b17..6a876dacdc639037c90e80eb7cc5de3ad3233548 100644 (file)
@@ -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<Value, Error> {
-    type DescriptorConstructor =
-        fn(DescriptorPublicKey) -> Result<Descriptor<DescriptorPublicKey>, 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<dyn Fn(u32) -> Result<(String, Option<String>), 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<String>), 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::<Legacy>::into_descriptor_key(desc_secret)?.extract(&secp)?
-                }
-                DescriptorType::Bip84 | DescriptorType::Bip49 => {
-                    IntoDescriptorKey::<Segwitv0>::into_descriptor_key(desc_secret)?
-                        .extract(&secp)?
-                }
-                DescriptorType::Bip86 => {
-                    IntoDescriptorKey::<Tap>::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<All>,
+) -> Result<Value, Error> {
+    let xprv: Xpriv = key.parse()?;
+    let fingerprint = xprv.fingerprint(secp);
+
+    let build_descriptor = |branch: &str| -> Result<(String, Option<String>), 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<String>), 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<Value, Error> {
+    let xpub: Xpub = key.parse()?;
+    let fingerprint = xpub.fingerprint();
+
+    let build_descriptor = |branch: &str| -> Result<String, Error> {
+        let branch_path = DerivationPath::from_str(branch)?;
+        let desc_xpub = DescriptorXKey {
+            origin: Some((fingerprint, derivation_path.clone())),
+            xkey: xpub,
+            derivation_path: branch_path,
+            wildcard: Wildcard::Unhardened,
+        };
+        let desc_pub = DescriptorPublicKey::XPub(desc_xpub);
+        let descriptor = 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<Value, Error> {
+    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<Self> {
+        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<Descriptor<DescriptorPublicKey>, 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<All>,
+    ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), Error> {
+        Ok(match self {
+            DescriptorType::Bip44 => {
+                let descriptor_key = IntoDescriptorKey::<Legacy>::into_descriptor_key(desc_secret)?;
+                descriptor_key.extract(secp)?
+            }
+            DescriptorType::Bip49 | DescriptorType::Bip84 => {
+                let descriptor_key =
+                    IntoDescriptorKey::<Segwitv0>::into_descriptor_key(desc_secret)?;
+                descriptor_key.extract(secp)?
+            }
+            DescriptorType::Bip86 => {
+                let descriptor_key = IntoDescriptorKey::<Tap>::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<String, Error> {
+    if !pretty {
+        return Ok(serde_json::to_string_pretty(result)?);
+    }
+
+    let mut rows: Vec<Vec<CellStruct>> = vec![];
+
+    if let Some(desc_type) = result.get("type") {
+        rows.push(vec![
+            "Type".cell().bold(true),
+            desc_type.as_str().unwrap_or("N/A").cell(),
+        ]);
+    }
+
+    if let Some(finger_print) = result.get("fingerprint") {
+        rows.push(vec![
+            "Fingerprint".cell().bold(true),
+            finger_print.as_str().unwrap_or("N/A").cell(),
+        ]);
+    }
+
+    if let Some(network) = result.get("network") {
+        rows.push(vec![
+            "Network".cell().bold(true),
+            network.as_str().unwrap_or("N/A").cell(),
+        ]);
+    }
+    if let Some(multipath_desc) = result.get("multipath_descriptor") {
+        rows.push(vec![
+            "Multipart Descriptor".cell().bold(true),
+            multipath_desc.as_str().unwrap_or("N/A").cell(),
+        ]);
+    }
+    if let Some(pub_descs) = result.get("public_descriptors").and_then(|v| v.as_object()) {
+        if let Some(ext) = pub_descs.get("external") {
+            rows.push(vec![
+                "External Public".cell().bold(true),
+                ext.as_str().unwrap_or("N/A").cell(),
+            ]);
+        }
+        if let Some(int) = pub_descs.get("internal") {
+            rows.push(vec![
+                "Internal Public".cell().bold(true),
+                int.as_str().unwrap_or("N/A").cell(),
+            ]);
+        }
+    }
+    if let Some(priv_descs) = result
+        .get("private_descriptors")
+        .and_then(|v| v.as_object())
+    {
+        if let Some(ext) = priv_descs.get("external") {
+            rows.push(vec![
+                "External Private".cell().bold(true),
+                ext.as_str().unwrap_or("N/A").cell(),
+            ]);
+        }
+        if let Some(int) = priv_descs.get("internal") {
+            rows.push(vec![
+                "Internal Private".cell().bold(true),
+                int.as_str().unwrap_or("N/A").cell(),
+            ]);
+        }
+    }
+    if let Some(mnemonic) = result.get("mnemonic") {
+        rows.push(vec![
+            "Mnemonic".cell().bold(true),
+            mnemonic.as_str().unwrap_or("N/A").cell(),
+        ]);
+    }
+
+    let table = rows
+        .table()
+        .display()
+        .map_err(|e| Error::Generic(e.to_string()))?;
+
+    Ok(format!("{table}"))
+}