From: Vihiga Tyonum Date: Wed, 29 Jul 2026 22:16:22 +0000 (+0100) Subject: feat(multipath): add multipath desc support X-Git-Url: http://internal-gitweb-vhost/tx_graph/%22https:/enum.FromScriptError.html?a=commitdiff_plain;h=53161348b18c6a0f30dec67021ede2b25a55486a;p=bdk-cli feat(multipath): add multipath desc support - Enable creating wallets and wallet operations using multipath descriptors. --- diff --git a/src/commands.rs b/src/commands.rs index 975f33b..37f8052 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -231,10 +231,12 @@ pub struct WalletOpts { /// Selects the wallet to use. #[arg(skip)] pub wallet: Option, + /// A single external descriptor, or a BIP-389 multipath descriptor. /// Sets the descriptor to use for the external addresses. #[arg(env = "EXT_DESCRIPTOR", short = 'e', long, required = true)] pub ext_descriptor: String, - /// Sets the descriptor to use for internal/change addresses. + /// Optional internal/change descriptor. Omit when `ext_descriptor` is a + /// multipath descriptor. Sets the descriptor to use for internal/change addresses. #[arg(env = "INT_DESCRIPTOR", short = 'i', long)] pub int_descriptor: Option, #[cfg(any( diff --git a/src/error.rs b/src/error.rs index bee8f19..32fe765 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,9 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum BDKCliError { + #[error("Cannot provide both a multipath descriptor and a separate internal descriptor.")] + AmbiguousDescriptors, + #[error("BIP39 error: {0:?}")] BIP39Error(#[from] Option), diff --git a/src/persister.rs b/src/persister.rs index e49b4ae..4ff61f1 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -1,5 +1,6 @@ use crate::commands::WalletOpts; use crate::error::BDKCliError as Error; +use crate::utils::descriptors::is_multipath_descriptor; use bdk_wallet::Wallet; use bdk_wallet::bitcoin::Network; #[cfg(any(feature = "sqlite", feature = "redb"))] @@ -68,14 +69,23 @@ where let ext_descriptor = wallet_opts.ext_descriptor.clone(); let int_descriptor = wallet_opts.int_descriptor.clone(); - let mut wallet_load_params = Wallet::load(); - wallet_load_params = - wallet_load_params.descriptor(KeychainKind::External, Some(ext_descriptor.clone())); - - if int_descriptor.is_some() { - wallet_load_params = - wallet_load_params.descriptor(KeychainKind::Internal, int_descriptor.clone()); + let ext_is_multipath = is_multipath_descriptor(&ext_descriptor, network)?; + if ext_is_multipath && int_descriptor.is_some() { + return Err(Error::AmbiguousDescriptors); } + + let mut wallet_load_params = Wallet::load(); + wallet_load_params = if ext_is_multipath { + // Load a wallet created from a two-path (BIP-389) descriptor. + wallet_load_params.two_path_descriptor(ext_descriptor.clone()) + } else { + let mut params = + wallet_load_params.descriptor(KeychainKind::External, Some(ext_descriptor.clone())); + if int_descriptor.is_some() { + params = params.descriptor(KeychainKind::Internal, int_descriptor.clone()); + } + params + }; wallet_load_params = wallet_load_params.extract_keys(); let wallet_opt = wallet_load_params @@ -85,16 +95,20 @@ where let wallet = match wallet_opt { Some(wallet) => wallet, - None => match int_descriptor { - Some(int_descriptor) => Wallet::create(ext_descriptor, int_descriptor) - .network(network) - .create_wallet(persister) - .map_err(|e| Error::Generic(e.to_string()))?, - None => Wallet::create_single(ext_descriptor) + None => { + let builder = if let Some(int_descriptor) = int_descriptor { + Wallet::create(ext_descriptor, int_descriptor) + } else if ext_is_multipath { + Wallet::create_from_two_path_descriptor(ext_descriptor) + } else { + Wallet::create_single(ext_descriptor) + }; + + builder .network(network) .create_wallet(persister) - .map_err(|e| Error::Generic(e.to_string()))?, - }, + .map_err(|e| Error::Generic(e.to_string()))? + } }; Ok(wallet) @@ -104,18 +118,19 @@ pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result { - let wallet = Wallet::create(ext_descriptor, int_descriptor) - .network(network) - .create_wallet_no_persist()?; - Ok(wallet) - } - None => { - let wallet = Wallet::create_single(ext_descriptor) - .network(network) - .create_wallet_no_persist()?; - Ok(wallet) - } + let ext_is_multipath = is_multipath_descriptor(&ext_descriptor, network)?; + if ext_is_multipath && int_descriptor.is_some() { + return Err(Error::AmbiguousDescriptors); } + + let builder = if let Some(int_descriptor) = int_descriptor { + Wallet::create(ext_descriptor, int_descriptor) + } else if ext_is_multipath { + Wallet::create_from_two_path_descriptor(ext_descriptor) + } else { + Wallet::create_single(ext_descriptor) + }; + + let wallet = builder.network(network).create_wallet_no_persist()?; + Ok(wallet) } diff --git a/src/utils/descriptors.rs b/src/utils/descriptors.rs index 9b0f5b7..d6c5e40 100644 --- a/src/utils/descriptors.rs +++ b/src/utils/descriptors.rs @@ -1,3 +1,5 @@ +use bdk_wallet::bitcoin::Network; +use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::keys::GeneratableKey; use std::{str::FromStr, sync::Arc}; @@ -196,3 +198,36 @@ pub fn generate_descriptor_from_mnemonic( result.mnemonic = Some(mnemonic_str.to_string()); Ok(result) } + +/// Returns `true` if `descriptor` is a BIP-389 multipath descriptor (e.g. `.../<0;1>/*`). +/// +/// Parses via IntoWalletDescriptor and uses miniscript's `is_multipath`. +/// Returns an error if the descriptor is unparseable. +pub fn is_multipath_descriptor(descriptor: &str, network: Network) -> Result { + let secp = Secp256k1::new(); + let (descriptor, _) = descriptor.into_wallet_descriptor(&secp, network.into())?; + Ok(descriptor.is_multipath()) +} + +#[cfg(test)] +mod multipath_tests { + use super::*; + + const MULTIPATH: &str = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)"; + const SINGLE: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg"; + + #[test] + fn detects_multipath() { + assert!(is_multipath_descriptor(MULTIPATH, Network::Testnet).unwrap()); + } + + #[test] + fn detects_single_path() { + assert!(!is_multipath_descriptor(SINGLE, Network::Testnet).unwrap()); + } + + #[test] + fn rejects_unparseable() { + assert!(is_multipath_descriptor("not a descriptor", Network::Testnet).is_err()); + } +}