diff --git a/Cargo.lock b/Cargo.lock index 35d240a..aa0c9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1287,9 +1287,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniscript" -version = "12.3.0" +version = "12.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" +checksum = "0760e92feaf4ee26bd2e616f557de64712bf1e75f3b1b218dfb475c0a84c7943" dependencies = [ "bech32", "bitcoin", diff --git a/src/commands.rs b/src/commands.rs index 67f8453..194de50 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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, @@ -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, // Positional argument (tprv/tpub/xprv/xpub) } /// Wallet operation subcommands. diff --git a/src/error.rs b/src/error.rs index 27606d7..1c9fc65 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } diff --git a/src/handlers.rs b/src/handlers.rs index 1b7bf25..135ffe7 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -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")] @@ -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; @@ -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}; @@ -820,10 +834,24 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } 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 { + 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, @@ -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 { + 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 { + 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) + } +} diff --git a/src/utils.rs b/src/utils.rs index feb08b4..7c6f457 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,12 +10,26 @@ //! //! This module includes all the utility tools used by the App. use crate::error::BDKCliError as Error; +use bdk_wallet::bip39::{Language, Mnemonic}; +use bdk_wallet::bitcoin::bip32::DerivationPath; +use bdk_wallet::bitcoin::bip32::{Xpriv, Xpub}; +use bdk_wallet::bitcoin::secp256k1::Secp256k1; +use bdk_wallet::descriptor::Segwitv0; +use bdk_wallet::descriptor::{Descriptor, DescriptorPublicKey}; +use bdk_wallet::keys::bip39::WordCount; +use bdk_wallet::keys::{GeneratableKey, GeneratedKey}; +use serde_json::json; +use serde_json::Value; + +use bdk_wallet::keys::{DescriptorSecretKey, IntoDescriptorKey}; +use bdk_wallet::miniscript::descriptor::{DescriptorXKey, Wildcard}; + use std::str::FromStr; #[cfg(feature = "sqlite")] use std::path::{Path, PathBuf}; -use crate::commands::WalletOpts; +use crate::commands::*; #[cfg(feature = "cbf")] use bdk_kyoto::{ builder::NodeBuilder, @@ -366,3 +380,204 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: LightClient) -> Resu Ok(()) } + +// Wrapper functions for the specific BIP types +pub fn generate_bip84_descriptor_from_key( + network: &Network, + key: &str, +) -> Result { + 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 { + 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 { + 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 { + generate_bip_descriptor_from_key(network, key, "m/44h/1h/0h", DescriptorType::Bip44) +} + +pub fn generate_new_bip84_descriptor_with_mnemonic( + network: Network, +) -> Result { + let secp = Secp256k1::new(); + + let mnemonic: GeneratedKey = + Mnemonic::generate((WordCount::Words12, Language::English)).map_err(|e| { + Error::MnemonicGenerationError(format!("Mnemonic generation failed: {:?}", e)) + })?; + + let seed = mnemonic.to_seed(""); + let xprv = + Xpriv::new_master(network, &seed).map_err(|e| Error::XprivCreationError(e.to_string()))?; + + let origin = xprv.fingerprint(&secp); + let deriv_base = "/84h/1h/0h"; // You might want to dynamically compute this based on args + let xprv_str = xprv.to_string(); + + let external_desc = format!("wpkh([{}{}]{}{})", origin, deriv_base, xprv_str, "/0/*"); + let internal_desc = format!("wpkh([{}{}]{}{})", origin, deriv_base, xprv_str, "/1/*"); + + let (desc, keymap) = Descriptor::::parse_descriptor(&secp, &external_desc) + .map_err(|e| Error::DescriptorParsingError(e.to_string()))?; + let (int_desc, int_keymap) = + Descriptor::::parse_descriptor(&secp, &internal_desc).map_err( + |e| { + Error::DescriptorParsingError(format!("Failed to parse internal descriptor: {}", e)) + }, + )?; + + Ok(serde_json::json!({ + "mnemonic": mnemonic.to_string(), + "external_descriptor": { + "public": desc.to_string(), + "private": desc.to_string_with_secret(&keymap), + }, + "internal_descriptor": { + "public": int_desc.to_string(), + "private": int_desc.to_string_with_secret(&int_keymap), + } + })) +} + +pub fn generate_multipath_descriptor( + network: &Network, + script_type: u8, + key: &str, +) -> Result { + // Only BIP84 supported in this example + if script_type != 84 { + return Err(Error::Generic( + "Only BIP84 is supported for multipath at the moment.".to_string(), + )); + } + + let xpub: Xpub = key + .parse() + .map_err(|e| Error::InvalidXpub(format!("Invalid xpub: {e}")))?; + + let derivation_path = DerivationPath::from_str("m/84h/1h/0h") + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + let fingerprint = xpub.fingerprint(); + + let make_desc = |change: u32| -> Result<(String, DescriptorPublicKey), Error> { + let branch_path = DerivationPath::from_str(&change.to_string()) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + let desc_xpub = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xpub, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, + }; + + let desc_key = DescriptorPublicKey::XPub(desc_xpub); + let descriptor = Descriptor::new_wpkh(desc_key.clone())?; + Ok((descriptor.to_string(), desc_key)) + }; + + let (external_desc, _) = make_desc(0)?; + let (internal_desc, _) = make_desc(1)?; + + Ok(json!({ + "type": "bip84-multipath", + "external": external_desc, + "internal": internal_desc, + "fingerprint": fingerprint.to_string(), + "network": network.to_string(), + })) +} +pub fn generate_bip_descriptor_from_key( + network: &Network, + key: &str, + derivation_path_str: &str, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + let derivation_path: DerivationPath = derivation_path_str + .parse() + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + let xprv: Xpriv = key + .parse() + .map_err(|e| Error::InvalidXprv(format!("Invalid xprv: {e}")))?; + let fingerprint = xprv.fingerprint(&secp); + + let make_desc_key = |branch: u32| -> Result<(String, String), Error> { + let branch_path: DerivationPath = DerivationPath::from_str(&format!("{branch}")) + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), // only account-level path + xkey: xprv, + derivation_path: branch_path, // just the change (0 for external, 1 for internal) + wildcard: Wildcard::Unhardened, + }; + + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv); + + // Use the BDK extract() to get both descriptor and keymap + let (desc_key, keymap, _) = + IntoDescriptorKey::::into_descriptor_key(desc_secret.clone()) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))?; + + let public_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key.clone())?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key.clone(), None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key.clone())?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key.clone())?, + }; + + let private_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key)?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key, None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key)?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key)?, + }; + + // Convert both to string representations + let public_descriptor_str = public_descriptor.to_string(); + let private_descriptor_str = private_descriptor.to_string_with_secret(&keymap); + + Ok((public_descriptor_str, private_descriptor_str)) + }; + + let (external_pub, external_priv) = make_desc_key(0)?; + let (internal_pub, internal_priv) = make_desc_key(1)?; + + Ok(serde_json::json!({ + "type": descriptor_type.to_string(), + "external": { + "public": external_pub, + "private": external_priv, + }, + "internal": { + "public": internal_pub, + "private": internal_priv, + }, + "fingerprint": fingerprint.to_string(), + "network": network.to_string() + })) +} + +// Enum for descriptor types +pub enum DescriptorType { + Bip44, + Bip49, + Bip84, + Bip86, +}