1use bhwi::{
2 bitcoin::{
3 Network,
4 bip32::{self, ChildNumber, DerivationPath, Fingerprint},
5 },
6 miniscript::{
7 self, Descriptor, DescriptorPublicKey,
8 descriptor::{DescriptorType, DescriptorXKey, Wildcard, Wpkh},
9 },
10};
11
12use crate::{HWIDevice, HWIDeviceError};
13
14#[derive(Debug, thiserror::Error)]
16pub enum DescriptorError {
17 #[error("{0}")]
19 Device(
20 #[from]
22 HWIDeviceError,
23 ),
24
25 #[error("{0}")]
27 Bip32(
28 #[from]
30 bip32::Error,
31 ),
32
33 #[error("{0}")]
35 Miniscript(
36 #[from]
38 miniscript::Error,
39 ),
40
41 #[error("Unsupported descriptor type {0:?}")]
43 UnsupportedDescriptorType(
44 DescriptorType,
46 ),
47
48 #[error("Bare PK descriptors aren't supported")]
50 BarePk,
51
52 #[error("keypool start index must be less than or equal to end index")]
54 KeypoolRange,
55
56 #[error("keypool path cannot contain hardened children after an unhardened child")]
58 HardenedAfterUnhardened,
59}
60
61#[derive(Debug, Clone)]
63pub struct GetDescriptorOptions {
64 pub master_fingerprint: Fingerprint,
66 pub target: DescriptorTarget,
68 pub is_change: bool,
70 pub descriptor_type: DescriptorType,
72 pub network: Network,
74}
75
76#[derive(Debug, Clone)]
78pub enum DescriptorTarget {
79 Path(
81 DerivationPath,
83 ),
84 Account(
86 u32,
88 ),
89}
90
91#[derive(Debug, Clone)]
93pub struct GetKeypoolOptions {
94 pub path: DerivationPath,
96 pub start: u32,
98 pub end: u32,
100 pub internal: bool,
102 pub descriptor_type: DescriptorType,
104 pub network: Network,
106}
107
108impl GetDescriptorOptions {
109 pub fn with_path(
111 master_fingerprint: Fingerprint,
112 path: DerivationPath,
113 is_change: bool,
114 descriptor_type: DescriptorType,
115 network: Network,
116 ) -> Self {
117 Self {
118 master_fingerprint,
119 target: DescriptorTarget::Path(path),
120 is_change,
121 descriptor_type,
122 network,
123 }
124 }
125
126 pub fn with_account(
128 master_fingerprint: Fingerprint,
129 account: u32,
130 is_change: bool,
131 descriptor_type: DescriptorType,
132 network: Network,
133 ) -> Self {
134 Self {
135 master_fingerprint,
136 target: DescriptorTarget::Account(account),
137 is_change,
138 descriptor_type,
139 network,
140 }
141 }
142}
143
144impl GetKeypoolOptions {
145 fn descriptor_options(
146 &self,
147 master_fingerprint: Fingerprint,
148 ) -> Result<GetDescriptorOptions, DescriptorError> {
149 validate_keypool_range(self.start, self.end)?;
150 let path = keypool_descriptor_path(&self.path, self.internal)?;
151 Ok(GetDescriptorOptions::with_path(
152 master_fingerprint,
153 path,
154 self.internal,
155 self.descriptor_type,
156 self.network,
157 ))
158 }
159}
160
161pub async fn get_descriptor(
168 device: &mut dyn HWIDevice,
169 options: GetDescriptorOptions,
170) -> Result<Descriptor<DescriptorPublicKey>, DescriptorError> {
171 let GetDescriptorOptions {
172 master_fingerprint,
173 target,
174 is_change,
175 descriptor_type,
176 network,
177 } = options;
178 let path = match target {
179 DescriptorTarget::Path(path) => path,
180 DescriptorTarget::Account(account) => {
181 let purpose = ChildNumber::from_hardened_idx(bip44_purpose(descriptor_type)?)?;
182 let chain = ChildNumber::from_hardened_idx(bip44_chain(network))?;
183 let account = ChildNumber::from_hardened_idx(account)?;
184 let change = ChildNumber::from_normal_idx(is_change.into())?;
185
186 [purpose, chain, account, change].as_ref().into()
187 }
188 };
189
190 let split = path
191 .into_iter()
192 .rposition(ChildNumber::is_hardened)
193 .map(|i| i + 1)
194 .unwrap_or(0);
195 let (origin, suffix) = (&path[..split], &path[split..]);
196
197 let xpub = device.get_extended_pubkey(origin.into(), false).await?;
198 let pk = DescriptorPublicKey::XPub(DescriptorXKey {
199 origin: Some((master_fingerprint, origin.into())),
200 xkey: xpub,
201 derivation_path: suffix.into(),
202 wildcard: Wildcard::Unhardened,
203 });
204 Ok(match descriptor_type {
205 DescriptorType::Pkh => Descriptor::new_pkh(pk)?,
206 DescriptorType::Wpkh => Descriptor::new_wpkh(pk)?,
207 DescriptorType::ShWpkh => {
208 Descriptor::new_sh_with_wpkh(Wpkh::new(pk).map_err(miniscript::Error::from)?)
209 }
210 DescriptorType::Tr => Descriptor::new_tr(pk, None)?,
212 _ => return Err(DescriptorError::UnsupportedDescriptorType(descriptor_type)),
213 })
214}
215
216pub const PUBKEY_DESCRIPTOR_TYPES: [DescriptorType; 4] = [
218 DescriptorType::Pkh,
219 DescriptorType::Wpkh,
220 DescriptorType::ShWpkh,
221 DescriptorType::Tr,
222];
223
224#[derive(Debug, Clone)]
226pub struct PubkeyDescriptors {
227 pub receive: Vec<Descriptor<DescriptorPublicKey>>,
229 pub internal: Vec<Descriptor<DescriptorPublicKey>>,
231}
232
233pub async fn get_pubkey_descriptors(
235 device: &mut dyn HWIDevice,
236 master_fingerprint: Fingerprint,
237 account: u32,
238 network: Network,
239) -> Result<PubkeyDescriptors, DescriptorError> {
240 let mut descriptors = PubkeyDescriptors {
241 receive: Vec::with_capacity(PUBKEY_DESCRIPTOR_TYPES.len()),
242 internal: Vec::with_capacity(PUBKEY_DESCRIPTOR_TYPES.len()),
243 };
244 for descriptor_type in PUBKEY_DESCRIPTOR_TYPES {
245 for (is_change, out) in [
246 (false, &mut descriptors.receive),
247 (true, &mut descriptors.internal),
248 ] {
249 let options = GetDescriptorOptions::with_account(
250 master_fingerprint,
251 account,
252 is_change,
253 descriptor_type,
254 network,
255 );
256 out.push(get_descriptor(device, options).await?);
257 }
258 }
259 Ok(descriptors)
260}
261
262pub async fn get_keypool_descriptor(
267 device: &mut dyn HWIDevice,
268 master_fingerprint: Fingerprint,
269 options: &GetKeypoolOptions,
270) -> Result<Descriptor<DescriptorPublicKey>, DescriptorError> {
271 let descriptor_options = options.descriptor_options(master_fingerprint)?;
272 get_descriptor(device, descriptor_options).await
273}
274
275fn validate_keypool_range(start: u32, end: u32) -> Result<(), DescriptorError> {
276 if start > end {
277 return Err(DescriptorError::KeypoolRange);
278 }
279 Ok(())
280}
281
282fn keypool_descriptor_path(
283 path: &DerivationPath,
284 internal: bool,
285) -> Result<DerivationPath, DescriptorError> {
286 if has_hardened_child_after_unhardened(path) {
287 return Err(DescriptorError::HardenedAfterUnhardened);
288 }
289
290 let branch = ChildNumber::from_normal_idx(internal.into())?;
291 let mut children = path.as_ref().to_vec();
292 children.push(branch);
293 Ok(children.into())
294}
295
296fn has_hardened_child_after_unhardened(path: &DerivationPath) -> bool {
297 let mut seen_unhardened = false;
298 path.as_ref().iter().any(|child| {
299 seen_unhardened |= !child.is_hardened();
300 seen_unhardened && child.is_hardened()
301 })
302}
303
304fn bip44_purpose(desc_type: DescriptorType) -> Result<u32, DescriptorError> {
305 Ok(match desc_type {
306 DescriptorType::Sh | DescriptorType::Pkh | DescriptorType::ShSortedMulti => 44,
307 DescriptorType::Wpkh | DescriptorType::Wsh | DescriptorType::WshSortedMulti => 84,
308 DescriptorType::ShWsh | DescriptorType::ShWpkh | DescriptorType::ShWshSortedMulti => 49,
309 DescriptorType::Tr => 86,
310 DescriptorType::Bare => return Err(DescriptorError::BarePk),
311 })
312}
313
314fn bip44_chain(network: Network) -> u32 {
315 if let Network::Bitcoin = network { 0 } else { 1 }
316}
317
318#[cfg(test)]
319mod tests {
320 use std::str::FromStr;
321
322 use bhwi::bitcoin::bip32::DerivationPath;
323
324 use super::{keypool_descriptor_path, validate_keypool_range};
325
326 #[test]
327 fn sortedmulti_purposes_match_their_script_wrappers() {
328 use bhwi::miniscript::descriptor::DescriptorType;
329
330 for (descriptor_type, expected) in [
331 (DescriptorType::ShSortedMulti, 44),
332 (DescriptorType::WshSortedMulti, 84),
333 (DescriptorType::ShWshSortedMulti, 49),
334 ] {
335 assert_eq!(super::bip44_purpose(descriptor_type).unwrap(), expected);
336 }
337 }
338
339 #[test]
340 fn device_errors_are_not_double_prefixed() {
341 use crate::{HWIDeviceError, descriptors::DescriptorError};
342
343 let inner = HWIDeviceError::new(std::io::Error::other("boom"));
344
345 assert_eq!(DescriptorError::Device(inner).to_string(), "boom");
346 }
347
348 #[test]
349 fn derivation_errors_are_not_prefixed() {
350 use bhwi::bitcoin::bip32::ChildNumber;
351
352 use crate::descriptors::DescriptorError;
353
354 let inner = ChildNumber::from_hardened_idx(4_000_000_000).unwrap_err();
355 let expected = inner.to_string();
356
357 assert_eq!(DescriptorError::Bip32(inner).to_string(), expected);
358 }
359
360 #[test]
361 fn keypool_path_appends_receive_branch() {
362 let path = DerivationPath::from_str("m/84'/0'/0'").unwrap();
363
364 let path = keypool_descriptor_path(&path, false).unwrap();
365
366 assert_eq!(path.to_string(), "84'/0'/0'/0");
367 }
368
369 #[test]
370 fn keypool_path_appends_internal_branch() {
371 let path = DerivationPath::from_str("m/84'/0'/0'").unwrap();
372
373 let path = keypool_descriptor_path(&path, true).unwrap();
374
375 assert_eq!(path.to_string(), "84'/0'/0'/1");
376 }
377
378 #[test]
379 fn keypool_range_rejects_start_after_end() {
380 let err = validate_keypool_range(10, 9).unwrap_err();
381
382 assert!(err.to_string().contains("start index"));
383 }
384
385 #[test]
386 fn keypool_path_rejects_hardened_children_after_unhardened() {
387 let path = DerivationPath::from_str("m/84'/0'/0'/0/1'").unwrap();
388
389 let err = keypool_descriptor_path(&path, false).unwrap_err();
390
391 assert!(err.to_string().contains("hardened children"));
392 }
393}