Skip to content

Commit efdbb5e

Browse files
committed
feat: add support for descriptor generation from keys and mnemonics
This enhancement introduces a new `descriptor` subcommand to `bdk-cli`, allowing users to generate wallet descriptors from either extended private keys (xprv), or fresh BIP39 mnemonics. It supports standard script types (BIP44, BIP49, BIP84, BIP86) and outputs both public and private descriptors for external and internal branches. The design improves developer and user experience by enabling easier wallet creation and script descriptor introspection. Closes #175
1 parent f0b51cb commit efdbb5e

File tree

4 files changed

+341
-7
lines changed

4 files changed

+341
-7
lines changed

src/commands.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
//! All subcommands are defined in the below enums.
1414
1515
#![allow(clippy::large_enum_variant)]
16-
1716
use bdk_wallet::bitcoin::{
1817
bip32::{DerivationPath, Xpriv},
1918
Address, Network, OutPoint, ScriptBuf,
@@ -104,6 +103,42 @@ pub enum CliSubCommand {
104103
#[command(flatten)]
105104
wallet_opts: WalletOpts,
106105
},
106+
/// Generate a Bitcoin descriptor either from a provided (Xprv, Xpub) or by generating a new random mnemonic.
107+
///
108+
/// This function supports two modes:
109+
///
110+
/// 1. **Using a provided XPRV**:
111+
/// - Generates BIP32-based descriptors from the provided extended private key.
112+
/// - Derives both external (`/0/*`) and internal (`/1/*`) paths.
113+
/// - Automatically detects the script type from the `--type` flag (e.g., BIP44, BIP49, BIP84, BIP86).
114+
///
115+
/// 2. **Generating a new mnemonic**:
116+
/// - Creates a new 12-word BIP39 mnemonic phrase.
117+
/// - Derives a BIP32 root XPRV using the standard derivation path based on the selected script type.
118+
/// - Constructs external and internal descriptors using that XPRV.
119+
///
120+
/// The output is a prettified JSON object containing:
121+
/// - `mnemonic` (if generated): the 12-word seed phrase.
122+
/// - `external`: public and private descriptors for receive addresses (`/0/*`)
123+
/// - `internal`: public and private descriptors for change addresses (`/1/*`)
124+
/// - `fingerprint`: master key fingerprint used in the descriptors
125+
/// - `network`: either `mainnet`, `testnet`, `signet`, `regtest`, or `testnet4`
126+
/// - `type`: one of `bip44`, `bip49`, `bip84`, or `bip86`
127+
///
128+
/// > ⚠️ **Security Warning**: This feature is intended for testing and development purposes.
129+
/// > Do **not** use generated descriptors or mnemonics to secure real Bitcoin funds on mainnet.
130+
///
131+
Descriptor(GenerateDescriptorArgs),
132+
}
133+
#[derive(Debug, Clone, PartialEq, Args)]
134+
pub struct GenerateDescriptorArgs {
135+
#[clap(long, value_parser = clap::value_parser!(u8).range(44..=86))]
136+
pub r#type: u8, // 44, 49, 84, 86
137+
138+
#[clap(long)]
139+
pub multipath: bool,
140+
141+
pub key: Option<String>, // Positional argument (tprv/tpub/xprv/xpub)
107142
}
108143

109144
/// Wallet operation subcommands.

src/error.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,33 @@ pub enum BDKCliError {
9494
#[cfg(feature = "cbf")]
9595
#[error("BDK-Kyoto update error: {0}")]
9696
KyotoUpdateError(#[from] bdk_kyoto::UpdateError),
97+
98+
#[error("Mnemonic generation failed: {0}")]
99+
MnemonicGenerationError(String),
100+
101+
#[error("Xpriv creation failed: {0}")]
102+
XprivCreationError(String),
103+
104+
#[error("Descriptor parsing failed: {0}")]
105+
DescriptorParsingError(String),
106+
107+
#[error("Invalid extended public key (xpub): {0}")]
108+
InvalidXpub(String),
109+
110+
#[error("Invalid extended private key (xprv): {0}")]
111+
InvalidXprv(String),
112+
113+
#[error("Invalid derivation path: {0}")]
114+
InvalidDerivationPath(String),
115+
116+
#[error("Unsupported script type: {0}")]
117+
UnsupportedScriptType(u8),
118+
119+
#[error("Descriptor key conversion failed: {0}")]
120+
DescriptorKeyError(String),
121+
122+
#[error("Invalid arguments: {0}")]
123+
InvalidArguments(String),
97124
}
98125

99126
impl From<ExtractTxError> for BDKCliError {

src/handlers.rs

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@ use bdk_wallet::bitcoin::consensus::encode::serialize_hex;
2222
use bdk_wallet::bitcoin::script::PushBytesBuf;
2323
use bdk_wallet::bitcoin::Network;
2424
use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid};
25-
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
2625
use bdk_wallet::descriptor::Segwitv0;
2726
use bdk_wallet::keys::bip39::WordCount;
27+
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
28+
use bdk_wallet::serde::ser::Error as SerdeErrorTrait;
29+
use serde_json::json;
30+
use serde_json::Error as SerdeError;
31+
use serde_json::Value;
32+
33+
#[cfg(any(
34+
feature = "electrum",
35+
feature = "esplora",
36+
feature = "cbf",
37+
feature = "rpc"
38+
))]
39+
use bdk_wallet::bitcoin::Transaction;
40+
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
2841
#[cfg(feature = "sqlite")]
2942
use bdk_wallet::rusqlite::Connection;
3043
#[cfg(feature = "compiler")]
@@ -33,18 +46,18 @@ use bdk_wallet::{
3346
miniscript::policy::Concrete,
3447
};
3548
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
49+
use std::fmt;
50+
use std::str::FromStr;
3651

3752
use bdk_wallet::keys::DescriptorKey::Secret;
38-
use bdk_wallet::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey};
53+
use bdk_wallet::keys::{DerivableKey, DescriptorKey, ExtendedKey};
3954
use bdk_wallet::miniscript::miniscript;
40-
use serde_json::json;
4155
use std::collections::BTreeMap;
4256
#[cfg(any(feature = "electrum", feature = "esplora"))]
4357
use std::collections::HashSet;
4458
use std::convert::TryFrom;
4559
#[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
4660
use std::io::Write;
47-
use std::str::FromStr;
4861

4962
#[cfg(feature = "electrum")]
5063
use crate::utils::BlockchainClient::Electrum;
@@ -61,7 +74,7 @@ use tokio::select;
6174
))]
6275
use {
6376
crate::commands::OnlineWalletSubCommand::*,
64-
bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction},
77+
bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex},
6578
};
6679
#[cfg(feature = "esplora")]
6780
use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
@@ -909,6 +922,13 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
909922
}
910923
Ok("".to_string())
911924
}
925+
CliSubCommand::Descriptor(args) => {
926+
let network = cli_opts.network;
927+
let descriptor = generate_descriptor_from_args(args.clone(), network)
928+
.map_err(|e| SerdeError::custom(e.to_string()))?;
929+
let json = serde_json::to_string_pretty(&descriptor)?;
930+
Ok(json)
931+
}
912932
};
913933
result.map_err(|e| e.into())
914934
}
@@ -980,6 +1000,57 @@ fn readline() -> Result<String, Error> {
9801000
Ok(buffer)
9811001
}
9821002

1003+
pub fn generate_descriptor_from_args(
1004+
args: GenerateDescriptorArgs,
1005+
network: Network,
1006+
) -> Result<serde_json::Value, Error> {
1007+
match (args.multipath, args.key.as_ref()) {
1008+
(true, Some(key)) => generate_multipath_descriptor(&network, args.r#type, key),
1009+
(false, Some(key)) => generate_standard_descriptor(&network, args.r#type, key),
1010+
(false, None) => {
1011+
// New default: generate descriptor from fresh mnemonic (for script_type 84 only)
1012+
if args.r#type == 84 {
1013+
generate_new_bip84_descriptor_with_mnemonic(network)
1014+
} else {
1015+
Err(Error::Generic(
1016+
"Only script type 84 is supported for mnemonic-based generation".to_string(),
1017+
))
1018+
}
1019+
}
1020+
_ => Err(Error::InvalidArguments(
1021+
"Invalid arguments: please provide a key or a weak string".to_string(),
1022+
)),
1023+
}
1024+
}
1025+
1026+
pub fn generate_standard_descriptor(
1027+
network: &Network,
1028+
script_type: u8,
1029+
key: &str,
1030+
) -> Result<Value, Error> {
1031+
let descriptor_type = match script_type {
1032+
44 => DescriptorType::Bip44,
1033+
49 => DescriptorType::Bip49,
1034+
84 => DescriptorType::Bip84,
1035+
86 => DescriptorType::Bip86,
1036+
_ => return Err(Error::UnsupportedScriptType(script_type)),
1037+
};
1038+
1039+
generate_descriptor_from_key_by_type(network, key, descriptor_type)
1040+
}
1041+
1042+
impl fmt::Display for DescriptorType {
1043+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1044+
let s = match self {
1045+
DescriptorType::Bip44 => "bip44",
1046+
DescriptorType::Bip49 => "bip49",
1047+
DescriptorType::Bip84 => "bip84",
1048+
DescriptorType::Bip86 => "bip86",
1049+
};
1050+
write!(f, "{}", s)
1051+
}
1052+
}
1053+
9831054
#[cfg(any(
9841055
feature = "electrum",
9851056
feature = "esplora",

0 commit comments

Comments
 (0)