Skip to content

Commit 193b9cd

Browse files
AmosOO7tvpeter
authored andcommitted
feat: add descriptor generation
- Created Subcommnds for the descriptor command; generate - Created function to get descriptors from mnemonics
1 parent f100d65 commit 193b9cd

File tree

4 files changed

+562
-9
lines changed

4 files changed

+562
-9
lines changed

src/commands.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
//! All subcommands are defined in the below enums.
1414
1515
#![allow(clippy::large_enum_variant)]
16-
1716
use bdk_wallet::bitcoin::{
1817
Address, Network, OutPoint, ScriptBuf,
1918
bip32::{DerivationPath, Xpriv},
2019
};
21-
use clap::{Args, Parser, Subcommand, ValueEnum, value_parser};
20+
use clap::{Args, Parser, Subcommand, ValueEnum, builder::TypedValueParser, value_parser};
2221

2322
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
2423
use crate::utils::parse_proxy_auth;
@@ -107,8 +106,15 @@ pub enum CliSubCommand {
107106
#[command(flatten)]
108107
wallet_opts: WalletOpts,
109108
},
109+
/// Output Descriptors operations.
110+
///
111+
/// Generate output descriptors from either extended key (Xprv/Xpub) or mnemonic phrase.
112+
/// This feature is intended for development and testing purposes only.
113+
Descriptor {
114+
#[clap(subcommand)]
115+
subcommand: DescriptorSubCommand,
116+
},
110117
}
111-
112118
/// Wallet operation subcommands.
113119
#[derive(Debug, Subcommand, Clone, PartialEq)]
114120
pub enum WalletSubCommand {
@@ -477,3 +483,27 @@ pub enum ReplSubCommand {
477483
/// Exit REPL loop.
478484
Exit,
479485
}
486+
/// Subcommands for Key operations.
487+
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
488+
pub enum DescriptorSubCommand {
489+
/// Generate a descriptor
490+
Generate {
491+
/// Descriptor type (script type).
492+
#[arg(
493+
long = "type",
494+
short = 't',
495+
value_parser = clap::builder::PossibleValuesParser::new(["44", "49", "84", "86"])
496+
.map(|s| s.parse::<u8>().unwrap()),
497+
default_value = "84"
498+
)]
499+
r#type: u8,
500+
/// Enable multipath descriptors
501+
#[arg(long = "multipath", short = 'm', default_value_t = false)]
502+
multipath: bool,
503+
/// Optional key input
504+
key: Option<String>,
505+
},
506+
507+
/// Show info about a given descriptor
508+
Info { descriptor: String },
509+
}

src/error.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ pub enum BDKCliError {
103103
#[cfg(feature = "cbf")]
104104
#[error("BDK-Kyoto update error: {0}")]
105105
KyotoUpdateError(#[from] bdk_kyoto::UpdateError),
106+
107+
#[error("Mnemonic generation failed: {0}")]
108+
MnemonicGenerationError(String),
109+
110+
#[error("Xpriv creation failed: {0}")]
111+
XprivCreationError(String),
112+
113+
#[error("Descriptor parsing failed: {0}")]
114+
DescriptorParsingError(String),
115+
116+
#[error("Invalid extended key (xpub): {0}")]
117+
InvalidKey(String),
118+
119+
#[error("Invalid derivation path: {0}")]
120+
InvalidDerivationPath(String),
121+
122+
#[error("Unsupported script type: {0}")]
123+
UnsupportedScriptType(u8),
124+
125+
#[error("Descriptor key conversion failed: {0}")]
126+
DescriptorKeyError(String),
127+
128+
#[error("Invalid arguments: {0}")]
129+
InvalidArguments(String),
106130
}
107131

108132
impl From<ExtractTxError> for BDKCliError {

src/handlers.rs

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,55 @@ use bdk_wallet::keys::{
3535
bip39::WordCount,
3636
};
3737
use bdk_wallet::miniscript::miniscript;
38+
use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource};
39+
use bdk_wallet::bitcoin::consensus::encode::serialize_hex;
40+
use bdk_wallet::bitcoin::script::PushBytesBuf;
41+
use bdk_wallet::bitcoin::Network;
42+
use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid};
43+
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence};
44+
use bdk_wallet::descriptor::{Descriptor, Segwitv0};
45+
use bdk_wallet::keys::bip39::WordCount;
3846
#[cfg(feature = "sqlite")]
3947
use bdk_wallet::rusqlite::Connection;
4048
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
4149
#[cfg(feature = "compiler")]
4250
use bdk_wallet::{
4351
descriptor::{Descriptor, Legacy, Miniscript},
4452
miniscript::{Tap, descriptor::TapTree, policy::Concrete},
53+
descriptor::{Legacy, Miniscript},
54+
miniscript::policy::Concrete,
4555
};
4656
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
4757
use serde_json::json;
58+
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
59+
60+
#[cfg(feature = "electrum")]
61+
use crate::utils::BlockchainClient::Electrum;
62+
#[cfg(feature = "cbf")]
63+
use bdk_kyoto::{Info, LightClient};
64+
#[cfg(feature = "compiler")]
65+
use bdk_wallet::bitcoin::XOnlyPublicKey;
66+
use bdk_wallet::bitcoin::base64::prelude::*;
67+
use bdk_wallet::keys::DescriptorKey::Secret;
68+
use bdk_wallet::keys::{
69+
DerivableKey, DescriptorKey, DescriptorKey::Secret, DescriptorPublicKey, ExtendedKey,
70+
GeneratableKey, GeneratedKey, bip39::WordCount,
71+
};
72+
use bdk_wallet::miniscript::miniscript;
73+
use serde_json::{Value, json};
4874
use std::collections::BTreeMap;
4975
#[cfg(any(feature = "electrum", feature = "esplora"))]
5076
use std::collections::HashSet;
5177
use std::convert::TryFrom;
78+
use std::fmt;
5279
#[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
5380
use std::io::Write;
5481
use std::str::FromStr;
55-
#[cfg(any(feature = "redb", feature = "compiler"))]
56-
use std::sync::Arc;
5782

5883
#[cfg(feature = "electrum")]
5984
use crate::utils::BlockchainClient::Electrum;
6085
#[cfg(feature = "cbf")]
6186
use bdk_kyoto::{Info, LightClient};
62-
#[cfg(feature = "compiler")]
63-
use bdk_wallet::bitcoin::XOnlyPublicKey;
6487
use bdk_wallet::bitcoin::base64::prelude::*;
6588
#[cfg(feature = "cbf")]
6689
use tokio::select;
@@ -72,7 +95,7 @@ use tokio::select;
7295
))]
7396
use {
7497
crate::commands::OnlineWalletSubCommand::*,
75-
bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex},
98+
bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction},
7699
};
77100
#[cfg(feature = "esplora")]
78101
use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
@@ -1280,6 +1303,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
12801303
}
12811304
Ok("".to_string())
12821305
}
1306+
CliSubCommand::Descriptor {
1307+
subcommand: descriptor_subcommand,
1308+
} => {
1309+
let network = cli_opts.network;
1310+
let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand)
1311+
.map_err(|e| Error::Generic(e.to_string()))?;
1312+
let json = serde_json::to_string_pretty(&descriptor)?;
1313+
Ok(json)
1314+
}
12831315
};
12841316
result
12851317
}
@@ -1353,6 +1385,103 @@ fn readline() -> Result<String, Error> {
13531385
Ok(buffer)
13541386
}
13551387

1388+
pub fn handle_descriptor_subcommand(
1389+
network: Network,
1390+
subcommand: DescriptorSubCommand,
1391+
) -> Result<Value, Error> {
1392+
match subcommand {
1393+
DescriptorSubCommand::Generate {
1394+
r#type,
1395+
multipath,
1396+
key,
1397+
} => {
1398+
let (descriptor_type, derivation_path_str) = match r#type {
1399+
44 => (DescriptorType::Bip44, "m/44h/1h/0h"),
1400+
49 => (DescriptorType::Bip49, "m/49h/1h/0h"),
1401+
84 => (DescriptorType::Bip84, "m/84h/1h/0h"),
1402+
86 => (DescriptorType::Bip86, "m/86h/1h/0h"),
1403+
_ => return Err(Error::UnsupportedScriptType(r#type)),
1404+
};
1405+
1406+
match (multipath, key.as_ref()) {
1407+
(true, Some(k)) => generate_multipath_descriptor(&network, r#type, k),
1408+
(false, Some(k)) => {
1409+
if is_mnemonic(k) {
1410+
generate_descriptor_from_mnemonic_string(
1411+
k,
1412+
network,
1413+
derivation_path_str,
1414+
descriptor_type,
1415+
)
1416+
} else {
1417+
generate_standard_descriptor(&network, r#type, k)
1418+
}
1419+
}
1420+
(false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type),
1421+
_ => Err(Error::InvalidArguments(
1422+
"Provide a key or weak string".to_string(),
1423+
)),
1424+
}
1425+
}
1426+
DescriptorSubCommand::Info { descriptor } => {
1427+
let parsed: Descriptor<DescriptorPublicKey> = descriptor
1428+
.parse()
1429+
.map_err(|e| Error::Generic(format!("Failed to parse descriptor: {}", e)))?;
1430+
1431+
let checksum = parsed.to_string();
1432+
let script_type = match parsed {
1433+
Descriptor::Wpkh(_) => "wpkh",
1434+
Descriptor::Pkh(_) => "pkh",
1435+
Descriptor::Sh(_) => "sh",
1436+
Descriptor::Tr(_) => "tr",
1437+
_ => "other",
1438+
};
1439+
1440+
let json = json!({
1441+
"descriptor": checksum,
1442+
"type": script_type,
1443+
"is_multipath": descriptor.contains("/*"),
1444+
});
1445+
1446+
Ok(json)
1447+
}
1448+
}
1449+
}
1450+
1451+
pub fn generate_standard_descriptor(
1452+
network: &Network,
1453+
script_type: u8,
1454+
key: &str,
1455+
) -> Result<Value, Error> {
1456+
let descriptor_type = match script_type {
1457+
44 => DescriptorType::Bip44,
1458+
49 => DescriptorType::Bip49,
1459+
84 => DescriptorType::Bip84,
1460+
86 => DescriptorType::Bip86,
1461+
_ => return Err(Error::UnsupportedScriptType(script_type)),
1462+
};
1463+
1464+
generate_descriptor_from_key_by_type(network, key, descriptor_type)
1465+
}
1466+
1467+
impl fmt::Display for DescriptorType {
1468+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1469+
let s = match self {
1470+
DescriptorType::Bip44 => "bip44",
1471+
DescriptorType::Bip49 => "bip49",
1472+
DescriptorType::Bip84 => "bip84",
1473+
DescriptorType::Bip86 => "bip86",
1474+
};
1475+
write!(f, "{}", s)
1476+
}
1477+
}
1478+
1479+
#[cfg(any(
1480+
feature = "electrum",
1481+
feature = "esplora",
1482+
feature = "cbf",
1483+
feature = "rpc"
1484+
))]
13561485
#[cfg(test)]
13571486
mod test {
13581487
#[cfg(any(

0 commit comments

Comments
 (0)