#![allow(missing_docs)]
mod cmd;
#[cfg(feature = "cli-compiler")]
mod compile;
mod error;
mod format;
mod parse;
use clap::{Parser, Subcommand};
use std::process::ExitCode;
use error::CliError;
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
enum CliNetwork {
Mainnet,
Testnet,
Signet,
Regtest,
}
impl From<CliNetwork> for bitcoin::Network {
fn from(n: CliNetwork) -> Self {
match n {
CliNetwork::Mainnet => bitcoin::Network::Bitcoin,
CliNetwork::Testnet => bitcoin::Network::Testnet,
CliNetwork::Signet => bitcoin::Network::Signet,
CliNetwork::Regtest => bitcoin::Network::Regtest,
}
}
}
impl CliNetwork {
fn as_str(self) -> &'static str {
match self {
CliNetwork::Mainnet => "mainnet",
CliNetwork::Testnet => "testnet",
CliNetwork::Signet => "signet",
CliNetwork::Regtest => "regtest",
}
}
}
#[derive(Debug, Parser)]
#[command(name = "md", version, about = "Mnemonic Descriptor (MD) — engravable BIP 388 wallet policy backups", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(
after_long_help = "EXAMPLES:\n $ md encode wpkh(@0/<0;1>/*)\n md1yqpqqxqq8xtwhw4xwn4qh"
)]
Encode {
template: Option<String>,
#[arg(long = "from-policy", value_name = "EXPR", conflicts_with = "template")]
from_policy: Option<String>,
#[arg(long, value_name = "CTX", value_parser = ["tap", "segwitv0"])]
context: Option<String>,
#[arg(long, value_name = "KEY")]
unspendable_key: Option<String>,
#[arg(long, value_name = "PATH")]
path: Option<String>,
#[arg(long = "key", value_name = "@i=XPUB")]
keys: Vec<String>,
#[arg(long = "fingerprint", value_name = "@i=HEX")]
fingerprints: Vec<String>,
#[arg(long, value_enum, default_value_t = CliNetwork::Mainnet)]
network: CliNetwork,
#[arg(long)]
force_chunked: bool,
#[arg(long)]
force_long_code: bool,
#[arg(long)]
policy_id_fingerprint: bool,
#[arg(long)]
json: bool,
},
#[command(
after_long_help = "EXAMPLES:\n $ md decode md1yqpqqxqq8xtwhw4xwn4qh\n wpkh(@0/<0;1>/*)"
)]
Decode {
#[arg(required = true, num_args = 1..)]
strings: Vec<String>,
#[arg(long)]
json: bool,
},
Verify {
#[arg(required = true, num_args = 1..)]
strings: Vec<String>,
#[arg(long, required = true)]
template: String,
#[arg(long = "key", value_name = "@i=XPUB")]
keys: Vec<String>,
#[arg(long = "fingerprint", value_name = "@i=HEX")]
fingerprints: Vec<String>,
#[arg(long, value_enum, default_value_t = CliNetwork::Mainnet)]
network: CliNetwork,
},
Inspect {
#[arg(required = true, num_args = 1..)]
strings: Vec<String>,
#[arg(long)]
json: bool,
},
Bytecode {
#[arg(required = true, num_args = 1..)]
strings: Vec<String>,
#[arg(long)]
json: bool,
},
Vectors {
#[arg(long, value_name = "DIR")]
out: Option<String>,
},
Compile {
expr: String,
#[arg(long, value_name = "CTX", value_parser = ["tap", "segwitv0"], required = true)]
context: String,
#[arg(long, value_name = "KEY")]
unspendable_key: Option<String>,
#[arg(long)]
json: bool,
},
#[command(after_long_help = "EXAMPLES:\n $ md address md1qq...\n bc1q...",
group = clap::ArgGroup::new("address_input").required(true).args(["phrases", "template"]))]
Address {
#[arg(num_args = 0..)]
phrases: Vec<String>,
#[arg(long, value_name = "TEMPLATE", conflicts_with = "phrases")]
template: Option<String>,
#[arg(long = "key", value_name = "@i=XPUB", requires = "template")]
keys: Vec<String>,
#[arg(long = "fingerprint", value_name = "@i=HEX", requires = "template")]
fingerprints: Vec<String>,
#[arg(long, value_enum, default_value_t = CliNetwork::Mainnet)]
network: CliNetwork,
#[arg(long, default_value_t = 0)]
chain: u32,
#[arg(long, conflicts_with = "chain")]
change: bool,
#[arg(long, default_value_t = 0)]
index: u32,
#[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u32).range(1..=1000))]
count: u32,
#[arg(long)]
json: bool,
},
#[cfg(feature = "json")]
#[command(name = "gui-schema")]
GuiSchema,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match dispatch(cli.command) {
Ok(()) => ExitCode::from(0),
Err(CliError::BadArg(m)) => {
eprintln!("md: {m}");
ExitCode::from(2)
}
Err(e) => {
eprintln!("md: {e}");
ExitCode::from(1)
}
}
}
#[cfg(feature = "cli-compiler")]
fn validate_unspendable_key_nums_only(uk: Option<&str>) -> Result<(), CliError> {
if let Some(v) = uk {
if v != parse::template::NUMS_H_POINT_X_ONLY_HEX {
return Err(CliError::BadArg(
"--unspendable-key currently only accepts the BIP-341 NUMS H-point literal hex \
(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) or omitted \
(auto-NUMS default). Other forms (xpub-style descriptor keys, arbitrary x-only \
hex) are deferred to a future version."
.into(),
));
}
}
Ok(())
}
fn dispatch(c: Command) -> Result<(), CliError> {
match c {
Command::Encode {
template,
from_policy,
context,
unspendable_key,
path,
keys,
fingerprints,
network,
force_chunked,
force_long_code,
policy_id_fingerprint,
json,
} => {
let template_str: String = if let Some(expr) = from_policy {
#[cfg(feature = "cli-compiler")]
{
if unspendable_key.as_deref() == Some("") {
return Err(CliError::BadArg(
"--unspendable-key must not be empty (omit the flag for auto-NUMS default)".into()));
}
let ctx: compile::ScriptContext = context
.ok_or_else(|| {
CliError::BadArg("--from-policy requires --context tap|segwitv0".into())
})?
.parse()
.map_err(|e: compile::CompileError| CliError::Compile(e.to_string()))?;
if matches!(ctx, compile::ScriptContext::SegwitV0) && unspendable_key.is_some()
{
return Err(CliError::BadArg(
"--unspendable-key is only valid for --context tap (segwitv0 has no internal key)".into()));
}
validate_unspendable_key_nums_only(unspendable_key.as_deref())?;
compile::compile_policy_to_template(&expr, ctx, unspendable_key.as_deref())
.map_err(CliError::from)?
}
#[cfg(not(feature = "cli-compiler"))]
{
let _ = (expr, context, unspendable_key);
return Err(CliError::BadArg(
"--from-policy requires the cli-compiler feature".into(),
));
}
} else {
if unspendable_key.is_some() {
return Err(CliError::BadArg(
"--unspendable-key is only meaningful with --from-policy".into(),
));
}
template.ok_or_else(|| {
CliError::BadArg(
"encode: TEMPLATE required (or use --from-policy with cli-compiler)".into(),
)
})?
};
cmd::encode::run(cmd::encode::EncodeArgs {
template: &template_str,
keys: &keys,
fingerprints: &fingerprints,
path: path.as_deref(),
network: network.into(),
network_str: network.as_str(),
force_chunked,
force_long_code,
policy_id_fingerprint,
json,
})
}
Command::Decode { strings, json } => cmd::decode::run(&strings, json),
Command::Verify {
strings,
template,
keys,
fingerprints,
network,
} => cmd::verify::run(cmd::verify::VerifyArgs {
strings: &strings,
template: &template,
keys: &keys,
fingerprints: &fingerprints,
network: network.into(),
}),
Command::Inspect { strings, json } => cmd::inspect::run(&strings, json),
Command::Bytecode { strings, json } => cmd::bytecode::run(&strings, json),
Command::Vectors { out } => cmd::vectors::run(out),
Command::Compile {
expr,
context,
unspendable_key,
json,
} => {
#[cfg(feature = "cli-compiler")]
{
if unspendable_key.as_deref() == Some("") {
return Err(CliError::BadArg(
"--unspendable-key must not be empty (omit the flag for auto-NUMS default)"
.into(),
));
}
if context == "segwitv0" && unspendable_key.is_some() {
return Err(CliError::BadArg(
"--unspendable-key is only valid for --context tap (segwitv0 has no internal key)".into()));
}
validate_unspendable_key_nums_only(unspendable_key.as_deref())?;
cmd::compile::run(&expr, &context, unspendable_key.as_deref(), json)
}
#[cfg(not(feature = "cli-compiler"))]
{
let _ = (expr, context, unspendable_key, json);
Err(CliError::BadArg(
"compile requires the cli-compiler feature; rebuild with --features cli-compiler".into()))
}
}
Command::Address {
phrases,
template,
keys,
fingerprints,
network,
chain,
change,
index,
count,
json,
} => {
let chain = if change { 1 } else { chain };
cmd::address::run(cmd::address::AddressArgs {
phrases: &phrases,
template: template.as_deref(),
keys: &keys,
fingerprints: &fingerprints,
network: network.into(),
network_str: network.as_str(),
chain,
index,
count,
json,
})
}
#[cfg(feature = "json")]
Command::GuiSchema => cmd::gui_schema::run(),
}
}