/// Selects the wallet to use.
#[arg(skip)]
pub wallet: Option<String>,
+ /// 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<String>,
#[cfg(any(
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"))]
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
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)
let ext_descriptor = wallet_opts.ext_descriptor.clone();
let int_descriptor = wallet_opts.int_descriptor.clone();
- match int_descriptor {
- Some(int_descriptor) => {
- let wallet = Wallet::create(ext_descriptor, int_descriptor)
- .network(network)
- .create_wallet_no_persist()?;
- Ok(wallet)
- }
- None => {
- let wallet = Wallet::create_single(ext_descriptor)
- .network(network)
- .create_wallet_no_persist()?;
- Ok(wallet)
- }
+ 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)
}
+use bdk_wallet::bitcoin::Network;
+use bdk_wallet::descriptor::IntoWalletDescriptor;
use bdk_wallet::keys::GeneratableKey;
use std::{str::FromStr, sync::Arc};
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<bool, Error> {
+ 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());
+ }
+}