Skip to content

Commit 6c9c3a1

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 bdb5777 commit 6c9c3a1

File tree

4 files changed

+563
-10
lines changed

4 files changed

+563
-10
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 {
@@ -473,3 +479,27 @@ pub enum ReplSubCommand {
473479
/// Exit REPL loop.
474480
Exit,
475481
}
482+
/// Subcommands for Key operations.
483+
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
484+
pub enum DescriptorSubCommand {
485+
/// Generate a descriptor
486+
Generate {
487+
/// Descriptor type (script type).
488+
#[arg(
489+
long = "type",
490+
short = 't',
491+
value_parser = clap::builder::PossibleValuesParser::new(["44", "49", "84", "86"])
492+
.map(|s| s.parse::<u8>().unwrap()),
493+
default_value = "84"
494+
)]
495+
r#type: u8,
496+
/// Enable multipath descriptors
497+
#[arg(long = "multipath", short = 'm', default_value_t = false)]
498+
multipath: bool,
499+
/// Optional key input
500+
key: Option<String>,
501+
},
502+
503+
/// Show info about a given descriptor
504+
Info { descriptor: String },
505+
}

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: 135 additions & 6 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::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")]
61-
use bdk_kyoto::LightClient;
62-
#[cfg(feature = "compiler")]
63-
use bdk_wallet::bitcoin::XOnlyPublicKey;
86+
use bdk_kyoto::{Info, LightClient};
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};
@@ -1260,6 +1283,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
12601283
}
12611284
Ok("".to_string())
12621285
}
1286+
CliSubCommand::Descriptor {
1287+
subcommand: descriptor_subcommand,
1288+
} => {
1289+
let network = cli_opts.network;
1290+
let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand)
1291+
.map_err(|e| Error::Generic(e.to_string()))?;
1292+
let json = serde_json::to_string_pretty(&descriptor)?;
1293+
Ok(json)
1294+
}
12631295
};
12641296
result
12651297
}
@@ -1333,6 +1365,103 @@ fn readline() -> Result<String, Error> {
13331365
Ok(buffer)
13341366
}
13351367

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

0 commit comments

Comments
 (0)