use crate::error::CliError;
use std::str::FromStr;
#[derive(Debug)]
pub enum CompileError {
Parse(String),
Compile(String),
BadContext(String),
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompileError::Parse(m) => write!(f, "parse: {m}"),
CompileError::Compile(m) => write!(f, "compile: {m}"),
CompileError::BadContext(m) => write!(f, "bad-context: {m}"),
}
}
}
impl From<CompileError> for CliError {
fn from(e: CompileError) -> Self {
CliError::Compile(e.to_string())
}
}
#[derive(Debug, Clone, Copy)]
pub enum ScriptContext {
Tap,
SegwitV0,
}
impl FromStr for ScriptContext {
type Err = CompileError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"tap" => Ok(Self::Tap),
"segwitv0" => Ok(Self::SegwitV0),
other => Err(CompileError::BadContext(other.into())),
}
}
}
pub fn compile_policy_to_template(
expr: &str,
ctx: ScriptContext,
unspendable_key: Option<&str>,
) -> Result<String, CompileError> {
use miniscript::policy::concrete::Policy;
let policy: Policy<String> = expr
.parse()
.map_err(|e| CompileError::Parse(format!("{e}")))?;
match ctx {
ScriptContext::SegwitV0 => {
debug_assert!(
unspendable_key.is_none(),
"unspendable_key must be None for SegwitV0; CLI should reject upstream"
);
let ms = policy
.compile::<miniscript::Segwitv0>()
.map_err(|e| CompileError::Compile(format!("{e}")))?;
Ok(format!("wsh({ms})"))
}
ScriptContext::Tap => {
let unspendable = unspendable_key
.map(String::from)
.or_else(|| Some(crate::parse::template::NUMS_H_POINT_X_ONLY_HEX.to_string()));
let desc = policy
.compile_tr(unspendable)
.map_err(|e| CompileError::Compile(format!("{e}")))?;
let rendered = desc.to_string();
let template = rendered
.split_once('#')
.map(|(t, _)| t.to_string())
.unwrap_or(rendered);
Ok(template)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::template::NUMS_H_POINT_X_ONLY_HEX;
#[test]
fn compile_segwitv0_pk() {
let s = compile_policy_to_template("pk(@0)", ScriptContext::SegwitV0, None).unwrap();
assert!(s.starts_with("wsh("));
assert!(s.contains("@0"));
}
#[test]
fn bad_context() {
assert!("xpub".parse::<ScriptContext>().is_err());
}
#[test]
fn compile_pk_tap_keypath_only() {
let s = compile_policy_to_template("pk(@0)", ScriptContext::Tap, None).unwrap();
assert_eq!(
s, "tr(@0)",
"single-key tap should extract @0 as internal key"
);
}
#[test]
fn compile_or_two_keys_tap() {
let s = compile_policy_to_template("or(pk(@0),pk(@1))", ScriptContext::Tap, None).unwrap();
assert_eq!(s, "tr(@1,pk(@0))");
}
#[test]
fn compile_or_pk_and_pk_older_tap() {
let s = compile_policy_to_template(
"or(pk(@0),and(pk(@1),older(144)))",
ScriptContext::Tap,
None,
)
.unwrap();
assert_eq!(s, "tr(@0,and_v(v:pk(@1),older(144)))");
}
#[test]
fn compile_thresh_2_of_3_tap_auto_nums() {
let s =
compile_policy_to_template("thresh(2,pk(@0),pk(@1),pk(@2))", ScriptContext::Tap, None)
.unwrap();
assert_eq!(
s,
format!("tr({NUMS_H_POINT_X_ONLY_HEX},multi_a(2,@0,@1,@2))")
);
}
#[test]
fn compile_and_pk_pk_tap_auto_nums() {
let s = compile_policy_to_template("and(pk(@0),pk(@1))", ScriptContext::Tap, None).unwrap();
assert_eq!(
s,
format!("tr({NUMS_H_POINT_X_ONLY_HEX},and_v(v:pk(@0),pk(@1)))")
);
}
#[test]
fn compile_pk_tap_explicit_nums_extract_still_wins() {
let s =
compile_policy_to_template("pk(@0)", ScriptContext::Tap, Some(NUMS_H_POINT_X_ONLY_HEX))
.unwrap();
assert_eq!(s, "tr(@0)");
}
#[test]
fn compile_strips_descriptor_checksum() {
let s = compile_policy_to_template("pk(@0)", ScriptContext::Tap, None).unwrap();
assert!(
!s.contains('#'),
"compile_policy_to_template output must not include #<checksum>; got {s:?}"
);
}
}