#![allow(missing_docs)]
use assert_cmd::Command;
use predicates::prelude::*;
const TPUB_FIXTURE: &str = "tpubDC8msFGeGuwnKG9Upg7DM2b4DaRqg3CUZa5g8v2SRQ6K4NSkxUgd7HsL2XVWbVm39yBA4LAxysQAm397zwQSQoQgewGiYZqrA9DsP4zbQ1M";
#[test]
fn encode_template_only_emits_a_phrase() {
let mut cmd = Command::cargo_bin("md").unwrap();
cmd.args(["encode", "wpkh(@0/<0;1>/*)"])
.assert()
.success()
.stdout(predicate::str::starts_with("md1"));
}
#[test]
fn encode_with_policy_id_fingerprint_prints_two_lines() {
let mut cmd = Command::cargo_bin("md").unwrap();
cmd.args(["encode", "wpkh(@0/<0;1>/*)", "--policy-id-fingerprint"])
.assert()
.success()
.stdout(predicate::str::contains("policy-id-fingerprint: 0x"));
}
#[cfg(feature = "json")]
#[test]
fn encode_json_has_schema_and_phrase() {
Command::cargo_bin("md")
.unwrap()
.args(["encode", "wpkh(@0/<0;1>/*)", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("\"schema\": \"md-cli/1\""))
.stdout(predicate::str::contains("\"phrase\":"));
}
#[test]
fn encode_with_explicit_path_raw_template_differs_from_baseline() {
let baseline = Command::cargo_bin("md")
.unwrap()
.args(["encode", "wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))"])
.output()
.unwrap();
assert!(baseline.status.success(), "baseline encode failed");
let baseline_phrase = String::from_utf8(baseline.stdout)
.unwrap()
.lines()
.next()
.unwrap()
.to_string();
let with_path = Command::cargo_bin("md")
.unwrap()
.args([
"encode",
"wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))",
"--path",
"84'/0'/0'",
])
.output()
.unwrap();
assert!(with_path.status.success(), "--path encode failed");
let with_path_phrase = String::from_utf8(with_path.stdout)
.unwrap()
.lines()
.next()
.unwrap()
.to_string();
assert!(baseline_phrase.starts_with("md1"));
assert!(with_path_phrase.starts_with("md1"));
assert_ne!(
baseline_phrase, with_path_phrase,
"expected --path override to change the encoded phrase"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_from_policy_segwitv0() {
Command::cargo_bin("md")
.unwrap()
.args(["encode", "--from-policy", "pk(@0)", "--context", "segwitv0"])
.assert()
.success()
.stdout(predicate::str::starts_with("md1"));
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_from_policy_thresh_2_of_3_tap() {
Command::cargo_bin("md")
.unwrap()
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
])
.assert()
.success()
.stdout(predicate::str::starts_with("md1"));
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_from_policy_inheritance_tap() {
Command::cargo_bin("md")
.unwrap()
.args([
"encode",
"--from-policy",
"or(pk(@0),and(pk(@1),older(144)))",
"--context",
"tap",
])
.assert()
.success()
.stdout(predicate::str::starts_with("md1"));
}
#[cfg(feature = "json")]
#[test]
fn encode_json_network_field_default_mainnet() {
Command::cargo_bin("md")
.unwrap()
.args(["encode", "wpkh(@0/<0;1>/*)", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("\"network\": \"mainnet\""));
}
#[cfg(feature = "json")]
#[test]
fn encode_json_network_field_testnet() {
Command::cargo_bin("md")
.unwrap()
.args([
"encode",
"wpkh(@0/<0;1>/*)",
"--network",
"testnet",
"--key",
&format!("@0={TPUB_FIXTURE}"),
"--json",
])
.assert()
.success()
.stdout(predicate::str::contains("\"network\": \"testnet\""));
}
#[test]
fn encode_rejects_tpub_under_default_mainnet() {
Command::cargo_bin("md")
.unwrap()
.args([
"encode",
"wpkh(@0/<0;1>/*)",
"--key",
&format!("@0={TPUB_FIXTURE}"),
])
.assert()
.code(1)
.stderr(predicate::str::contains("expected mainnet"));
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_with_explicit_path_populates_path_decl() {
use std::process::Command as StdCommand;
let baseline = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
])
.output()
.expect("baseline encode");
let baseline_phrase = String::from_utf8(baseline.stdout)
.unwrap()
.trim()
.to_string();
let with_path = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"48'/0'/0'/2'",
])
.output()
.expect("with-path encode");
let with_path_phrase = String::from_utf8(with_path.stdout)
.unwrap()
.trim()
.to_string();
assert!(baseline_phrase.starts_with("md1"));
assert!(with_path_phrase.starts_with("md1"));
assert_ne!(
baseline_phrase, with_path_phrase,
"explicit --path must change the encoded phrase (was silently dropped pre-v0.18)"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_with_named_path_bip48() {
use std::process::Command as StdCommand;
let named = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"bip48",
])
.output()
.expect("named-path encode");
let named_phrase = String::from_utf8(named.stdout).unwrap().trim().to_string();
let literal = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"48'/0'/0'/2'",
])
.output()
.expect("literal-path encode");
let literal_phrase = String::from_utf8(literal.stdout)
.unwrap()
.trim()
.to_string();
assert!(named_phrase.starts_with("md1"));
assert_eq!(
named_phrase, literal_phrase,
"`--path bip48` must resolve to `48'/0'/0'/2'` (parse_path::parse_path_name)"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_path_overrides_canonical_default() {
use std::process::Command as StdCommand;
let path_a = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"48'/0'/0'/2'",
])
.output()
.expect("path-A encode");
let phrase_a = String::from_utf8(path_a.stdout).unwrap().trim().to_string();
let path_b = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"86'/0'/0'",
])
.output()
.expect("path-B encode");
let phrase_b = String::from_utf8(path_b.stdout).unwrap().trim().to_string();
assert!(phrase_a.starts_with("md1"));
assert!(phrase_b.starts_with("md1"));
assert_ne!(
phrase_a, phrase_b,
"different explicit --path values must produce different encoded phrases"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_decode_roundtrip_thresh_2_of_3_tap_with_explicit_path() {
use std::process::Command as StdCommand;
let encode_out = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"thresh(2,pk(@0),pk(@1),pk(@2))",
"--context",
"tap",
"--path",
"48'/0'/0'/2'",
])
.output()
.expect("encode");
let phrase = String::from_utf8(encode_out.stdout)
.unwrap()
.trim()
.to_string();
assert!(
phrase.starts_with("md1"),
"encode must produce an md1 phrase, got: {phrase}"
);
let decode_out = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args(["decode", &phrase])
.output()
.expect("decode");
let template = String::from_utf8(decode_out.stdout)
.unwrap()
.trim()
.to_string();
assert!(
template.contains("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"),
"decoded template must include NUMS hex (Tag::Tr+sentinel rendered \
as tr(<NUMS>, ...)). Got: {template}"
);
assert!(
template.contains("multi_a(2,@0"),
"decoded template must include multi_a(2,@0... body. Got: {template}"
);
}
#[cfg(feature = "cli-compiler")]
#[test]
fn encode_decode_roundtrip_inheritance_pattern_with_explicit_path() {
use std::process::Command as StdCommand;
let encode_out = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args([
"encode",
"--from-policy",
"or(pk(@0),and(pk(@1),older(144)))",
"--context",
"tap",
"--path",
"86'/0'/0'",
])
.output()
.expect("encode");
let phrase = String::from_utf8(encode_out.stdout)
.unwrap()
.trim()
.to_string();
assert!(phrase.starts_with("md1"));
let decode_out = StdCommand::new(env!("CARGO_BIN_EXE_md"))
.args(["decode", &phrase])
.output()
.expect("decode");
let template = String::from_utf8(decode_out.stdout)
.unwrap()
.trim()
.to_string();
assert!(
template.starts_with("tr(@0"),
"decoded must start with tr(@0 (extracted @0 internal key). Got: {template}"
);
assert!(
template.contains("and_v(v:pk(@1"),
"decoded must include and_v(v:pk(@1 body. Got: {template}"
);
assert!(
template.contains("older(144)"),
"decoded must include older(144). Got: {template}"
);
}