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 all 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
4 changes: 2 additions & 2 deletions Cargo.lock

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

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.
///
/// 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`
/// - `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),
}
89 changes: 83 additions & 6 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,25 @@ 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};
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 bdk_wallet::serde::ser::Error as SerdeErrorTrait;
use serde_json::json;
use serde_json::Error as SerdeError;
use serde_json::Value;

#[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 @@ -33,18 +47,18 @@ use bdk_wallet::{
miniscript::policy::Concrete,
};
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
use std::fmt;
use std::str::FromStr;

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 All @@ -57,7 +71,7 @@ use bdk_wallet::bitcoin::base64::prelude::*;
))]
use {
crate::commands::OnlineWalletSubCommand::*,
bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction},
bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex},
};
#[cfg(feature = "esplora")]
use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
Expand Down Expand Up @@ -820,10 +834,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(
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 +943,52 @@ mod test {
assert!(is_final(&full_signed_psbt).is_ok());
}
}

pub fn generate_descriptor_from_args(
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)
}
}
Loading
Loading