Skip to content

Adding descriptor generator #180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ log = "0.4"
serde_json = "1.0"
thiserror = "2.0.11"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
miniscript = "12.3.2"

# Optional dependencies
bdk_bitcoind_rpc = { version = "0.18.0", optional = true }
Expand Down
37 changes: 36 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
//! All subcommands are defined in the below enums.

#![allow(clippy::large_enum_variant)]

use bdk_wallet::bitcoin::{
bip32::{DerivationPath, Xpriv},
Address, Network, OutPoint, ScriptBuf,
Expand Down Expand Up @@ -104,6 +103,42 @@ pub enum CliSubCommand {
#[command(flatten)]
wallet_opts: WalletOpts,
},
/// Generate a Bitcoin descriptor either from a provided XPRV or by generating a new random mnemonic.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the key could either be Xprv or Xpub

/// Generate a Bitcoin descriptor either from a key (Xprv, Xpub) or by generating a random mnemonic

///
/// This function supports two modes:
///
/// 1. **Using a provided XPRV**:
/// - Generates BIP32-based descriptors from the provided extended private key.
/// - Derives both external (`/0/*`) and internal (`/1/*`) paths.
/// - Automatically detects the script type from the `--type` flag (e.g., BIP44, BIP49, BIP84, BIP86).
///
/// 2. **Generating a new mnemonic**:
/// - Creates a new 12-word BIP39 mnemonic phrase.
/// - Derives a BIP32 root XPRV using the standard derivation path based on the selected script type.
/// - Constructs external and internal descriptors using that XPRV.
///
/// The output is a prettified JSON object containing:
/// - `mnemonic` (if generated): the 12-word seed phrase.
/// - `external`: public and private descriptors for receive addresses (`/0/*`)
/// - `internal`: public and private descriptors for change addresses (`/1/*`)
/// - `fingerprint`: master key fingerprint used in the descriptors
/// - `network`: either `mainnet`, `testnet`, `signet`, or `regtest`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, signet, regtest, or testnet4

/// - `type`: one of `bip44`, `bip49`, `bip84`, or `bip86`
///
/// > ⚠️ **Security Warning**: This feature is intended for testing and development purposes.
/// > Do **not** use generated descriptors or mnemonics to secure real Bitcoin funds on mainnet.
///
Descriptor(GenerateDescriptorArgs),
}
#[derive(Debug, Clone, PartialEq, Args)]
pub struct GenerateDescriptorArgs {
#[clap(long, value_parser = clap::value_parser!(u8).range(44..=86))]
pub r#type: u8, // 44, 49, 84, 86

#[clap(long)]
pub multipath: bool,

pub key: Option<String>, // Positional argument (tprv/tpub/xprv/xpub)
}

/// Wallet operation subcommands.
Expand Down
27 changes: 27 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,31 @@ pub enum BDKCliError {
#[cfg(feature = "cbf")]
#[error("BDK-Kyoto error: {0}")]
BuilderError(#[from] bdk_kyoto::builder::BuilderError),

#[error("Mnemonic generation failed: {0}")]
MnemonicGenerationError(String),

#[error("Xpriv creation failed: {0}")]
XprivCreationError(String),

#[error("Descriptor parsing failed: {0}")]
DescriptorParsingError(String),

#[error("Invalid extended public key (xpub): {0}")]
InvalidXpub(String),

#[error("Invalid extended private key (xprv): {0}")]
InvalidXprv(String),

#[error("Invalid derivation path: {0}")]
InvalidDerivationPath(String),

#[error("Unsupported script type: {0}")]
UnsupportedScriptType(u8),

#[error("Descriptor key conversion failed: {0}")]
DescriptorKeyError(String),

#[error("Invalid arguments: {0}")]
InvalidArguments(String),
}
117 changes: 112 additions & 5 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,28 @@ use bdk_wallet::bip39::{Language, Mnemonic};
use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource};
use bdk_wallet::bitcoin::consensus::encode::serialize_hex;
use bdk_wallet::bitcoin::script::PushBytesBuf;
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you do delete already existing imports?

use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
use bdk_wallet::descriptor::Segwitv0;
use bdk_wallet::keys::bip39::WordCount;
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
use serde::ser::Error as SerdeErrorTrait;
use serde_json::json;
use serde_json::Error as SerdeError;
use serde_json::Value;
use std::fmt;

use std::str::FromStr;

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
use bdk_wallet::bitcoin::Transaction;
use bdk_wallet::bitcoin::Txid;
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
#[cfg(feature = "sqlite")]
use bdk_wallet::rusqlite::Connection;
#[cfg(feature = "compiler")]
Expand All @@ -35,16 +52,14 @@ use bdk_wallet::{
use bdk_wallet::{KeychainKind, SignOptions, Wallet};

use bdk_wallet::keys::DescriptorKey::Secret;
use bdk_wallet::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey};
use bdk_wallet::keys::{DerivableKey, DescriptorKey, ExtendedKey};
use bdk_wallet::miniscript::miniscript;
use serde_json::json;
use std::collections::BTreeMap;
#[cfg(any(feature = "electrum", feature = "esplora"))]
use std::collections::HashSet;
use std::convert::TryFrom;
#[cfg(feature = "repl")]
use std::io::Write;
use std::str::FromStr;

#[cfg(feature = "electrum")]
use crate::utils::BlockchainClient::Electrum;
Expand Down Expand Up @@ -820,10 +835,24 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
}
Ok("".to_string())
}
CliSubCommand::Descriptor(args) => {
let network = cli_opts.network; // Or just use cli_opts directly
let json = handle_generate_descriptor(args.clone(), network)?;
Ok(json)
}
};
result.map_err(|e| e.into())
}

pub fn handle_generate_descriptor(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is need for this fn. You can pretty print in the top-level handler(handle_command fn)

args: GenerateDescriptorArgs,
network: Network,
) -> Result<String, SerdeError> {
let descriptor = generate_descriptor_from_args(args, network)
.map_err(|e| SerdeErrorTrait::custom(e.to_string()))?;
serde_json::to_string_pretty(&descriptor)
}

#[cfg(feature = "repl")]
async fn respond(
network: Network,
Expand Down Expand Up @@ -915,3 +944,81 @@ mod test {
assert!(is_final(&full_signed_psbt).is_ok());
}
}

pub fn generate_descriptor_from_args(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move your code above the test module

args: GenerateDescriptorArgs,
network: Network,
) -> Result<serde_json::Value, Error> {
match (args.multipath, args.key.as_ref()) {
(true, Some(key)) => generate_multipath_descriptor(&network, args.r#type, key),
(false, Some(key)) => generate_standard_descriptor(&network, args.r#type, key),
(false, None) => {
// New default: generate descriptor from fresh mnemonic (for script_type 84 only maybe)
if args.r#type == 84 {
generate_new_bip84_descriptor_with_mnemonic(network)
} else {
Err(Error::Generic(
"Only script type 84 is supported for mnemonic-based generation".to_string(),
))
}
}
_ => Err(Error::InvalidArguments(
"Invalid arguments: please provide a key or a weak string".to_string(),
)),
}
}

pub fn generate_standard_descriptor(
network: &Network,
script_type: u8,
key: &str,
) -> Result<Value, Error> {
match script_type {
84 => generate_bip84_descriptor_from_key(network, key),
86 => generate_bip86_descriptor_from_key(network, key),
49 => generate_bip49_descriptor_from_key(network, key),
44 => generate_bip44_descriptor_from_key(network, key),
_ => Err(Error::UnsupportedScriptType(script_type)),
}
}

impl fmt::Display for DescriptorType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
DescriptorType::Bip44 => "bip44",
DescriptorType::Bip49 => "bip49",
DescriptorType::Bip84 => "bip84",
DescriptorType::Bip86 => "bip86",
};
write!(f, "{}", s)
}
}

// Wrapper functions for the specific BIP types
pub fn generate_bip84_descriptor_from_key(
network: &Network,
key: &str,
) -> Result<serde_json::Value, Error> {
generate_bip_descriptor_from_key(network, key, "m/84h/1h/0h", DescriptorType::Bip84)
}

pub fn generate_bip86_descriptor_from_key(
network: &Network,
key: &str,
) -> Result<serde_json::Value, Error> {
generate_bip_descriptor_from_key(network, key, "m/86h/1h/0h", DescriptorType::Bip86)
}

pub fn generate_bip49_descriptor_from_key(
network: &Network,
key: &str,
) -> Result<serde_json::Value, Error> {
generate_bip_descriptor_from_key(network, key, "m/49h/1h/0h", DescriptorType::Bip49)
}

pub fn generate_bip44_descriptor_from_key(
network: &Network,
key: &str,
) -> Result<serde_json::Value, Error> {
generate_bip_descriptor_from_key(network, key, "m/44h/1h/0h", DescriptorType::Bip44)
}
Loading