use crate::bitstream::BitWriter;
use crate::error::Error;
use crate::header::Header;
use crate::origin_path::{PathDecl, PathDeclPaths};
use crate::tlv::TlvSection;
use crate::tree::{Body, Node, write_node};
use crate::use_site_path::UseSitePath;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Descriptor {
pub n: u8,
pub path_decl: PathDecl,
pub use_site_path: UseSitePath,
pub tree: Node,
pub tlv: TlvSection,
}
impl Descriptor {
pub fn key_index_width(&self) -> u8 {
(32 - (self.n as u32).saturating_sub(1).leading_zeros()) as u8
}
pub fn is_wallet_policy(&self) -> bool {
matches!(&self.tlv.pubkeys, Some(v) if !v.is_empty())
}
}
pub fn encode_payload(d: &Descriptor) -> Result<(Vec<u8>, usize), Error> {
let mut d_canonical = d.clone();
crate::canonicalize::canonicalize_placeholder_indices(&mut d_canonical)?;
let d = &d_canonical;
crate::validate::validate_placeholder_usage(&d.tree, d.n)?;
if let Some(overrides) = &d.tlv.use_site_path_overrides {
crate::validate::validate_multipath_consistency(&d.use_site_path, overrides)?;
}
if matches!(d.tree.tag, crate::tag::Tag::Tr) {
if let Body::Tr { tree: Some(t), .. } = &d.tree.body {
crate::validate::validate_tap_script_tree(t)?;
}
}
let mut w = BitWriter::new();
let header = Header {
version: Header::WF_REDESIGN_VERSION,
divergent_paths: matches!(d.path_decl.paths, PathDeclPaths::Divergent(_)),
};
header.write(&mut w);
d.path_decl.write(&mut w)?;
d.use_site_path.write(&mut w)?;
let kiw = d.key_index_width();
write_node(&mut w, &d.tree, kiw)?;
d.tlv.write(&mut w, kiw)?;
let total_bits = w.bit_len();
Ok((w.into_bytes(), total_bits))
}
pub fn is_display_separator(c: char) -> bool {
c.is_whitespace() || c == '-' || c == ','
}
pub fn render_grouped(s: &str, group_size: usize, separator: char) -> String {
if group_size == 0 {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + s.len() / group_size);
for (i, ch) in s.chars().enumerate() {
if i > 0 && i % group_size == 0 {
out.push(separator);
}
out.push(ch);
}
out
}
pub fn strip_display_separators(s: &str) -> String {
s.chars().filter(|&c| !is_display_separator(c)).collect()
}
pub fn render_codex32_grouped(s: &str, group_size: usize) -> String {
render_grouped(s, group_size, '-')
}
pub fn encode_md1_string(d: &Descriptor) -> Result<String, Error> {
let (bytes, bit_len) = encode_payload(d)?;
crate::codex32::wrap_payload(&bytes, bit_len)
}
#[cfg(test)]
mod render_tests {
use super::*;
#[test]
fn render_groups_at_4() {
assert_eq!(render_codex32_grouped("md1qpz9r4cy7", 4), "md1q-pz9r-4cy7");
}
#[test]
fn render_zero_group_size_no_grouping() {
assert_eq!(render_codex32_grouped("md1qpz9r4cy7", 0), "md1qpz9r4cy7");
}
#[test]
fn render_grouped_separators_and_unbroken() {
assert_eq!(render_grouped("abcdefghij", 5, ' '), "abcde fghij");
assert_eq!(render_grouped("abcdefghij", 5, '-'), "abcde-fghij");
assert_eq!(render_grouped("abcdefghij", 5, ','), "abcde,fghij");
assert_eq!(render_grouped("abcdefghij", 0, ' '), "abcdefghij");
assert_eq!(render_grouped("abcde", 5, ' '), "abcde");
assert_eq!(render_grouped("abcdefg", 3, '-'), "abc-def-g");
assert_eq!(render_grouped("", 5, ' '), "");
}
#[test]
fn render_codex32_grouped_still_hyphens() {
assert_eq!(render_codex32_grouped("abcdefghij", 5), "abcde-fghij");
assert_eq!(render_codex32_grouped("abcde", 0), "abcde");
}
#[test]
fn strip_display_separators_whitespace_hyphen_comma() {
assert_eq!(strip_display_separators("abcde fghij"), "abcdefghij");
assert_eq!(strip_display_separators("ab-cd,ef gh"), "abcdefgh");
assert_eq!(strip_display_separators("ab\tcd\r\nef"), "abcdef");
assert_eq!(strip_display_separators("ms1qpzry9x8"), "ms1qpzry9x8");
let once = strip_display_separators("a b-c,d");
assert_eq!(strip_display_separators(&once), once);
}
}
#[cfg(test)]
mod is_wallet_policy_tests {
use super::*;
use crate::origin_path::OriginPath;
use crate::tag::Tag;
use crate::tlv::TlvSection;
fn wpkh_template_only() -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
}
}
#[test]
fn is_wallet_policy_returns_false_for_template_only() {
let d = wpkh_template_only();
assert!(!d.is_wallet_policy());
}
#[test]
fn is_wallet_policy_returns_false_for_empty_pubkeys() {
let mut d = wpkh_template_only();
d.tlv.pubkeys = Some(Vec::new());
assert!(!d.is_wallet_policy());
}
#[test]
fn is_wallet_policy_returns_true_for_populated_pubkeys() {
let mut d = wpkh_template_only();
d.tlv.pubkeys = Some(vec![(0u8, [0u8; 65])]);
assert!(d.is_wallet_policy());
}
}