Skip to content

Commit 775ad11

Browse files
committed
feat: add support for descriptor generation from keys and mnemonics
- Created Subcommnds for the descriptor command; generate and info - Created function to get descriptors from mnemonics - Ensured consistent fingerprint when generating descriptors from both mnemonic and key - Removed the previously inserted path variable
1 parent 0759450 commit 775ad11

File tree

3 files changed

+175
-118
lines changed

3 files changed

+175
-118
lines changed

src/commands.rs

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -103,47 +103,15 @@ pub enum CliSubCommand {
103103
#[command(flatten)]
104104
wallet_opts: WalletOpts,
105105
},
106-
/// Generate a Bitcoin descriptor either from a provided (Xprv, Xpub) or by generating a new random mnemonic.
106+
/// Descriptor generation operations.
107107
///
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 = "type", value_parser = clap::value_parser!(u8).range(44..=86), short = 't', default_value = "84")]
136-
pub r#type: u8, // 44, 49, 84, 86
137-
138-
#[arg(long = "multipath", short = 'm', default_value_t = false)]
139-
pub multipath: bool,
140-
141-
#[arg(long)]
142-
pub path: Option<String>,
143-
144-
pub key: Option<String>,
108+
/// Allows users to generate Bitcoin wallet descriptors from either an extended key (e.g., Xprv/Xpub) or by generating a new random mnemonic phrase.
109+
/// This feature is intended for development and testing. Exercise caution when using descriptors created this way in production environments.
110+
Descriptor {
111+
#[clap(subcommand)]
112+
subcommand: DescriptorSubCommand,
113+
},
145114
}
146-
147115
/// Wallet operation subcommands.
148116
#[derive(Debug, Subcommand, Clone, PartialEq)]
149117
pub enum WalletSubCommand {
@@ -509,3 +477,20 @@ pub enum ReplSubCommand {
509477
/// Exit REPL loop.
510478
Exit,
511479
}
480+
/// Subcommands for Key operations.
481+
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
482+
pub enum DescriptorSubCommand {
483+
/// Generate a descriptor
484+
Generate {
485+
#[clap(long = "type", value_parser = clap::value_parser!(u8).range(44..=86), short = 't', default_value = "84")]
486+
r#type: u8, // 44, 49, 84, 86
487+
488+
#[arg(long = "multipath", short = 'm', default_value_t = false)]
489+
multipath: bool,
490+
491+
key: Option<String>,
492+
},
493+
494+
/// Show info about a given descriptor
495+
Info { descriptor: String },
496+
}

src/handlers.rs

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ use bdk_wallet::bitcoin::script::PushBytesBuf;
2323
use bdk_wallet::bitcoin::Network;
2424
use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid};
2525
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
26-
use bdk_wallet::descriptor::Segwitv0;
26+
use bdk_wallet::descriptor::{Descriptor, Segwitv0};
2727
use bdk_wallet::keys::bip39::WordCount;
2828
#[cfg(feature = "sqlite")]
2929
use bdk_wallet::rusqlite::Connection;
3030
#[cfg(feature = "compiler")]
3131
use bdk_wallet::{
32-
descriptor::{Descriptor, Legacy, Miniscript},
32+
descriptor::{Legacy, Miniscript},
3333
miniscript::policy::Concrete,
3434
};
3535
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
3636

3737
use bdk_wallet::keys::DescriptorKey::Secret;
38-
use bdk_wallet::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey};
38+
use bdk_wallet::keys::{
39+
DerivableKey, DescriptorKey, DescriptorPublicKey, ExtendedKey, GeneratableKey, GeneratedKey,
40+
};
3941
use bdk_wallet::miniscript::miniscript;
4042
use serde_json::{json, Value};
4143
use std::collections::BTreeMap;
@@ -901,9 +903,11 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
901903
}
902904
Ok("".to_string())
903905
}
904-
CliSubCommand::Descriptor(args) => {
906+
CliSubCommand::Descriptor {
907+
subcommand: descriptor_subcommand,
908+
} => {
905909
let network = cli_opts.network;
906-
let descriptor = generate_descriptor_from_args(args.clone(), network)
910+
let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand)
907911
.map_err(|e| Error::Generic(e.to_string()))?;
908912
let json = serde_json::to_string_pretty(&descriptor)?;
909913
Ok(json)
@@ -979,49 +983,71 @@ fn readline() -> Result<String, Error> {
979983
Ok(buffer)
980984
}
981985

982-
pub fn generate_descriptor_from_args(
983-
args: GenerateDescriptorArgs,
986+
pub fn handle_descriptor_subcommand(
984987
network: Network,
985-
) -> Result<serde_json::Value, Error> {
986-
let descriptor_type = match args.r#type {
987-
44 => DescriptorType::Bip44,
988-
49 => DescriptorType::Bip49,
989-
84 => DescriptorType::Bip84,
990-
86 => DescriptorType::Bip86,
991-
_ => {
992-
return Err(Error::Generic(format!(
993-
"Unsupported script type {}",
994-
args.r#type
995-
)))
996-
}
997-
};
998-
if args.multipath && args.path.is_some() {
999-
return Err(Error::InvalidArguments(
1000-
"Path can not be called here".to_string(),
1001-
));
1002-
}
1003-
if args.path.is_some() && args.key.is_none() {
1004-
return Err(Error::InvalidArguments(
1005-
"Custom path requires a key".to_string(),
1006-
));
1007-
}
1008-
match (args.multipath, args.key.as_ref()) {
1009-
(true, Some(key)) => generate_multipath_descriptor(&network, args.r#type, key),
1010-
(false, Some(key)) => {
1011-
if let Some(path) = args.path.as_ref() {
1012-
// Use descriptor from custom derivation path
1013-
generate_bip_descriptor_from_key(&network, key, path, descriptor_type)
1014-
} else {
1015-
generate_standard_descriptor(&network, args.r#type, key)
988+
subcommand: DescriptorSubCommand,
989+
) -> Result<Value, Error> {
990+
match subcommand {
991+
DescriptorSubCommand::Generate {
992+
r#type,
993+
multipath,
994+
key,
995+
} => {
996+
let (descriptor_type, derivation_path_str) = match r#type {
997+
44 => (DescriptorType::Bip44, "m/44'/1'/0'"),
998+
49 => (DescriptorType::Bip49, "m/49'/1'/0'"),
999+
84 => (DescriptorType::Bip84, "m/84'/1'/0'"),
1000+
86 => (DescriptorType::Bip86, "m/86'/1'/0'"),
1001+
_ => {
1002+
return Err(Error::Generic(format!(
1003+
"Unsupported script type {}",
1004+
r#type
1005+
)))
1006+
}
1007+
};
1008+
1009+
match (multipath, key.as_ref()) {
1010+
(true, Some(k)) => generate_multipath_descriptor(&network, r#type, k),
1011+
(false, Some(k)) => {
1012+
if is_mnemonic(k) {
1013+
generate_descriptor_from_mnemonic_string(
1014+
k,
1015+
network,
1016+
derivation_path_str,
1017+
descriptor_type,
1018+
)
1019+
} else {
1020+
generate_standard_descriptor(&network, r#type, k)
1021+
}
1022+
}
1023+
(false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type),
1024+
_ => Err(Error::InvalidArguments(
1025+
"Invalid arguments: provide a key or weak string".to_string(),
1026+
)),
10161027
}
10171028
}
1018-
(false, None) => {
1019-
// Generate from fresh mnemonic
1020-
generate_new_descriptor_with_mnemonic(network, descriptor_type)
1029+
DescriptorSubCommand::Info { descriptor } => {
1030+
let parsed: Descriptor<DescriptorPublicKey> = descriptor
1031+
.parse()
1032+
.map_err(|e| Error::Generic(format!("Failed to parse descriptor: {}", e)))?;
1033+
1034+
let checksum = parsed.to_string();
1035+
let script_type = match parsed {
1036+
Descriptor::Wpkh(_) => "wpkh",
1037+
Descriptor::Pkh(_) => "pkh",
1038+
Descriptor::Sh(_) => "sh",
1039+
Descriptor::Tr(_) => "tr",
1040+
_ => "other",
1041+
};
1042+
1043+
let json = json!({
1044+
"descriptor": checksum,
1045+
"type": script_type,
1046+
"is_multipath": descriptor.contains("/*"),
1047+
});
1048+
1049+
Ok(json)
10211050
}
1022-
_ => Err(Error::InvalidArguments(
1023-
"Invalid arguments: please provide a key or a weak string".to_string(),
1024-
)),
10251051
}
10261052
}
10271053

0 commit comments

Comments
 (0)