1use crate::bitstream::BitWriter;
4use crate::error::Error;
5use crate::header::Header;
6use crate::origin_path::{PathDecl, PathDeclPaths};
7use crate::tlv::TlvSection;
8use crate::tree::{Body, Node, write_node};
9use crate::use_site_path::UseSitePath;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Descriptor {
18 pub n: u8,
20 pub path_decl: PathDecl,
22 pub use_site_path: UseSitePath,
24 pub tree: Node,
26 pub tlv: TlvSection,
28}
29
30impl Descriptor {
31 pub fn key_index_width(&self) -> u8 {
38 (32 - (self.n as u32).saturating_sub(1).leading_zeros()) as u8
41 }
42
43 pub fn is_wallet_policy(&self) -> bool {
51 matches!(&self.tlv.pubkeys, Some(v) if !v.is_empty())
52 }
53}
54
55pub fn encode_payload(d: &Descriptor) -> Result<(Vec<u8>, usize), Error> {
66 let mut d_canonical = d.clone();
67 crate::canonicalize::canonicalize_placeholder_indices(&mut d_canonical)?;
68 let d = &d_canonical;
69 crate::validate::validate_placeholder_usage(&d.tree, d.n)?;
70 if let Some(overrides) = &d.tlv.use_site_path_overrides {
71 crate::validate::validate_multipath_consistency(&d.use_site_path, overrides)?;
72 }
73 if matches!(d.tree.tag, crate::tag::Tag::Tr) {
74 if let Body::Tr { tree: Some(t), .. } = &d.tree.body {
75 crate::validate::validate_tap_script_tree(t)?;
76 }
77 }
78
79 let mut w = BitWriter::new();
80 let header = Header {
81 version: Header::WF_REDESIGN_VERSION,
82 divergent_paths: matches!(d.path_decl.paths, PathDeclPaths::Divergent(_)),
83 };
84 header.write(&mut w);
85 d.path_decl.write(&mut w)?;
86 d.use_site_path.write(&mut w)?;
87 let kiw = d.key_index_width();
88 write_node(&mut w, &d.tree, kiw)?;
89 d.tlv.write(&mut w, kiw)?;
90 let total_bits = w.bit_len();
91 Ok((w.into_bytes(), total_bits))
92}
93
94pub fn is_display_separator(c: char) -> bool {
99 c.is_whitespace() || c == '-' || c == ','
100}
101
102pub fn render_grouped(s: &str, group_size: usize, separator: char) -> String {
105 if group_size == 0 {
106 return s.to_string();
107 }
108 let mut out = String::with_capacity(s.len() + s.len() / group_size);
109 for (i, ch) in s.chars().enumerate() {
110 if i > 0 && i % group_size == 0 {
111 out.push(separator);
112 }
113 out.push(ch);
114 }
115 out
116}
117
118pub fn strip_display_separators(s: &str) -> String {
122 s.chars().filter(|&c| !is_display_separator(c)).collect()
123}
124
125pub fn render_codex32_grouped(s: &str, group_size: usize) -> String {
131 render_grouped(s, group_size, '-')
132}
133
134pub fn encode_md1_string(d: &Descriptor) -> Result<String, Error> {
137 let (bytes, bit_len) = encode_payload(d)?;
138 crate::codex32::wrap_payload(&bytes, bit_len)
139}
140
141#[cfg(test)]
142mod render_tests {
143 use super::*;
144
145 #[test]
146 fn render_groups_at_4() {
147 assert_eq!(render_codex32_grouped("md1qpz9r4cy7", 4), "md1q-pz9r-4cy7");
148 }
149
150 #[test]
151 fn render_zero_group_size_no_grouping() {
152 assert_eq!(render_codex32_grouped("md1qpz9r4cy7", 0), "md1qpz9r4cy7");
153 }
154
155 #[test]
156 fn render_grouped_separators_and_unbroken() {
157 assert_eq!(render_grouped("abcdefghij", 5, ' '), "abcde fghij");
158 assert_eq!(render_grouped("abcdefghij", 5, '-'), "abcde-fghij");
159 assert_eq!(render_grouped("abcdefghij", 5, ','), "abcde,fghij");
160 assert_eq!(render_grouped("abcdefghij", 0, ' '), "abcdefghij");
161 assert_eq!(render_grouped("abcde", 5, ' '), "abcde");
162 assert_eq!(render_grouped("abcdefg", 3, '-'), "abc-def-g");
163 assert_eq!(render_grouped("", 5, ' '), "");
164 }
165
166 #[test]
167 fn render_codex32_grouped_still_hyphens() {
168 assert_eq!(render_codex32_grouped("abcdefghij", 5), "abcde-fghij");
170 assert_eq!(render_codex32_grouped("abcde", 0), "abcde");
171 }
172
173 #[test]
174 fn strip_display_separators_whitespace_hyphen_comma() {
175 assert_eq!(strip_display_separators("abcde fghij"), "abcdefghij");
176 assert_eq!(strip_display_separators("ab-cd,ef gh"), "abcdefgh");
177 assert_eq!(strip_display_separators("ab\tcd\r\nef"), "abcdef");
178 assert_eq!(strip_display_separators("ms1qpzry9x8"), "ms1qpzry9x8");
179 let once = strip_display_separators("a b-c,d");
180 assert_eq!(strip_display_separators(&once), once);
181 }
182}
183
184#[cfg(test)]
185mod is_wallet_policy_tests {
186 use super::*;
187 use crate::origin_path::OriginPath;
188 use crate::tag::Tag;
189 use crate::tlv::TlvSection;
190
191 fn wpkh_template_only() -> Descriptor {
192 Descriptor {
193 n: 1,
194 path_decl: PathDecl {
195 n: 1,
196 paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
197 },
198 use_site_path: UseSitePath::standard_multipath(),
199 tree: Node {
200 tag: Tag::Wpkh,
201 body: Body::KeyArg { index: 0 },
202 },
203 tlv: TlvSection::new_empty(),
204 }
205 }
206
207 #[test]
208 fn is_wallet_policy_returns_false_for_template_only() {
209 let d = wpkh_template_only();
211 assert!(!d.is_wallet_policy());
212 }
213
214 #[test]
215 fn is_wallet_policy_returns_false_for_empty_pubkeys() {
216 let mut d = wpkh_template_only();
220 d.tlv.pubkeys = Some(Vec::new());
221 assert!(!d.is_wallet_policy());
222 }
223
224 #[test]
225 fn is_wallet_policy_returns_true_for_populated_pubkeys() {
226 let mut d = wpkh_template_only();
227 d.tlv.pubkeys = Some(vec![(0u8, [0u8; 65])]);
228 assert!(d.is_wallet_policy());
229 }
230}