1use std::str::FromStr;
2
3use bhwi::{
4 bitcoin::{
5 address::AddressType,
6 bip32::{self, DerivationPath, Fingerprint, Xpub},
7 hex::{self, DisplayHex},
8 secp256k1::{PublicKey as SecpPublicKey, XOnlyPublicKey},
9 },
10 common::{DisplayAddress, MultisigAddressType, MultisigDisplayAddress},
11 miniscript::{DescriptorPublicKey, descriptor::DescriptorKeyParseError},
12};
13
14use bhwi::common::DeviceContext;
15use bhwi::miniscript::descriptor::WalletPolicy;
16
17use crate::{
18 HWIDeviceError,
19 device::{Device, DeviceType, supports_multisig_display_address},
20};
21
22#[derive(Debug, thiserror::Error)]
24pub enum DisplayAddressContextError {
25 #[error("{0} needs the wallet policy descriptor of the registered wallet")]
27 MissingPolicy(
28 DeviceType,
30 ),
31
32 #[error("{0} needs both the wallet policy descriptor and its registration hmac")]
34 IncompletePolicy(
35 DeviceType,
37 ),
38}
39
40pub fn display_address_context(
46 device_type: DeviceType,
47 name: &str,
48 policy: Option<WalletPolicy>,
49 hmac: Option<[u8; 32]>,
50) -> Result<Option<DeviceContext>, DisplayAddressContextError> {
51 let _ = (name, &policy, hmac);
52 match device_type {
53 #[cfg(feature = "bitbox")]
54 DeviceType::BitBox02 => Ok(Some(DeviceContext::BitBox {
55 policy: policy.ok_or(DisplayAddressContextError::MissingPolicy(device_type))?,
56 })),
57 #[cfg(feature = "ledger")]
58 DeviceType::Ledger => match (policy, hmac) {
59 (None, None) => Ok(None),
60 (Some(policy), Some(hmac)) => Ok(Some(crate::signing::ledger::registered_context(
61 name.to_owned(),
62 policy,
63 Some(hmac),
64 ))),
65 _ => Err(DisplayAddressContextError::IncompletePolicy(device_type)),
66 },
67 #[cfg(feature = "specter")]
68 DeviceType::Specter => Ok(Some(DeviceContext::Specter {
69 policy: policy.ok_or(DisplayAddressContextError::MissingPolicy(device_type))?,
70 })),
71 _ => Ok(None),
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
77pub enum DisplayAddressError {
78 #[error("Unsupported displayaddress descriptor: {0}")]
80 UnsupportedDescriptor(
81 String,
83 ),
84
85 #[error("Descriptor missing origin info: {0}")]
87 MissingOrigin(
88 String,
90 ),
91
92 #[error("Descriptor fingerprint does not match device: {0}")]
94 FingerprintMismatch(
95 String,
97 ),
98
99 #[error("Key in descriptor does not match device: {0}")]
101 KeyMismatch(
102 String,
104 ),
105
106 #[error("Character '{0}' is not a valid base58 character")]
108 InvalidBase58Character(
109 char,
111 ),
112
113 #[error(
115 "Either the redeem script provided is invalid or the keypaths provided are insufficient"
116 )]
117 InvalidMultisig,
118
119 #[error(transparent)]
121 Bip32(
122 #[from]
124 bip32::Error,
125 ),
126
127 #[error(transparent)]
129 Fingerprint(
130 #[from]
132 hex::HexToArrayError,
133 ),
134
135 #[error(transparent)]
137 Key(
138 #[from]
140 DescriptorKeyParseError,
141 ),
142
143 #[error(transparent)]
145 Threshold(
146 #[from]
148 std::num::ParseIntError,
149 ),
150
151 #[error(transparent)]
153 Device(
154 #[from]
156 HWIDeviceError,
157 ),
158}
159
160pub async fn display_address_from_descriptor(
166 device: &mut Device,
167 descriptor: &str,
168) -> Result<DisplayAddress, DisplayAddressError> {
169 let device_type = device.device_type();
170 let singlesig_error = match singlesig_display_address_from_descriptor(device, descriptor).await
171 {
172 Ok(address) => return Ok(address),
173 Err(error) => error,
174 };
175 if !supports_multisig_display_address(device_type) {
176 return Err(singlesig_error);
177 }
178 match multisig_display_address_from_descriptor(descriptor) {
179 Ok(address) => Ok(DisplayAddress::ByMultisig(address)),
180 Err(_) => Err(singlesig_error),
181 }
182}
183
184pub async fn singlesig_display_address_from_descriptor(
190 device: &mut Device,
191 descriptor: &str,
192) -> Result<DisplayAddress, DisplayAddressError> {
193 let descriptor = strip_descriptor_checksum(descriptor);
194 let parsed = parse_singlesig_display_descriptor(descriptor)?;
195 let fingerprint = device.fingerprint().await?;
196 if parsed.fingerprint != fingerprint {
197 return Err(DisplayAddressError::FingerprintMismatch(
198 descriptor.to_owned(),
199 ));
200 }
201
202 let xpub = device
203 .device()
204 .get_extended_pubkey(parsed.origin_path.clone(), false)
205 .await?;
206
207 if !descriptor_key_matches_xpub(&parsed.key, xpub) {
208 return Err(DisplayAddressError::KeyMismatch(descriptor.to_owned()));
209 }
210
211 Ok(DisplayAddress::ByPath {
212 path: parsed.full_path,
213 display: true,
214 address_format: Some(parsed.addr_type),
215 })
216}
217
218#[derive(Debug)]
219struct ParsedSingleSigDisplayDescriptor {
220 addr_type: AddressType,
221 fingerprint: Fingerprint,
222 origin_path: DerivationPath,
223 full_path: DerivationPath,
224 key: String,
225}
226
227fn parse_singlesig_display_descriptor(
228 descriptor: &str,
229) -> Result<ParsedSingleSigDisplayDescriptor, DisplayAddressError> {
230 let (addr_type, key_expr) = if let Some(inner) = descriptor
231 .strip_prefix("sh(wpkh(")
232 .and_then(|value| value.strip_suffix("))"))
233 {
234 (AddressType::P2sh, inner)
235 } else if let Some(inner) = descriptor
236 .strip_prefix("wpkh(")
237 .and_then(|value| value.strip_suffix(')'))
238 {
239 (AddressType::P2wpkh, inner)
240 } else if let Some(inner) = descriptor
241 .strip_prefix("pkh(")
242 .and_then(|value| value.strip_suffix(')'))
243 {
244 (AddressType::P2pkh, inner)
245 } else if let Some(inner) = descriptor
246 .strip_prefix("tr(")
247 .and_then(|value| value.strip_suffix(')'))
248 {
249 (AddressType::P2tr, inner)
250 } else {
251 return Err(DisplayAddressError::UnsupportedDescriptor(
252 descriptor.to_owned(),
253 ));
254 };
255
256 let Some(rest) = key_expr.strip_prefix('[') else {
257 return Err(DisplayAddressError::MissingOrigin(descriptor.to_owned()));
258 };
259 let Some((origin, key_and_suffix)) = rest.split_once(']') else {
260 return Err(DisplayAddressError::MissingOrigin(descriptor.to_owned()));
261 };
262 let (fingerprint, origin_path) = parse_key_origin(origin)?;
263 let (key, suffix_path) = split_key_suffix(key_and_suffix)?;
264 validate_singlesig_display_key(key)?;
265 let full_path = join_derivation_path(&origin_path, &suffix_path);
266
267 Ok(ParsedSingleSigDisplayDescriptor {
268 addr_type,
269 fingerprint,
270 origin_path,
271 full_path,
272 key: key.to_owned(),
273 })
274}
275
276fn validate_singlesig_display_key(key: &str) -> Result<(), DisplayAddressError> {
277 if SecpPublicKey::from_str(key).is_ok() || XOnlyPublicKey::from_str(key).is_ok() {
278 return Ok(());
279 }
280
281 Xpub::from_str(key).map(|_| ()).map_err(|err| {
282 invalid_base58_character(key)
283 .map_or_else(|| err.into(), DisplayAddressError::InvalidBase58Character)
284 })
285}
286
287fn invalid_base58_character(value: &str) -> Option<char> {
288 const BASE58_ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
289
290 value.chars().find(|ch| !BASE58_ALPHABET.contains(*ch))
291}
292
293pub fn multisig_display_address_from_descriptor(
299 descriptor: &str,
300) -> Result<MultisigDisplayAddress, DisplayAddressError> {
301 let descriptor = strip_descriptor_checksum(descriptor);
302 let (address_type, sorted, inner) = parse_multisig_descriptor_envelope(descriptor)
303 .ok_or_else(|| DisplayAddressError::UnsupportedDescriptor(descriptor.to_owned()))?;
304
305 let mut parts = inner.split(',');
306 let threshold = parts.next().unwrap_or_default().parse::<u8>()?;
308 let keys = parts
309 .map(|key| DescriptorPublicKey::from_str(key).map_err(DisplayAddressError::from))
310 .collect::<Result<Vec<_>, _>>()?;
311 if keys.is_empty() || threshold == 0 || usize::from(threshold) > keys.len() {
312 return Err(DisplayAddressError::InvalidMultisig);
313 }
314 Ok(MultisigDisplayAddress {
315 threshold,
316 address_type,
317 sorted,
318 keys,
319 })
320}
321
322fn parse_multisig_descriptor_envelope(
323 descriptor: &str,
324) -> Option<(MultisigAddressType, bool, &str)> {
325 let wrappers = [
326 ("sh(wsh(", "))", MultisigAddressType::ShWit),
327 ("wsh(", ")", MultisigAddressType::Wit),
328 ("sh(", ")", MultisigAddressType::Legacy),
329 ];
330 for (prefix, suffix, address_type) in wrappers {
331 let Some(inner) = descriptor
332 .strip_prefix(prefix)
333 .and_then(|value| value.strip_suffix(suffix))
334 else {
335 continue;
336 };
337 if let Some(inner) = inner
338 .strip_prefix("sortedmulti(")
339 .and_then(|value| value.strip_suffix(')'))
340 {
341 return Some((address_type, true, inner));
342 }
343 if let Some(inner) = inner
344 .strip_prefix("multi(")
345 .and_then(|value| value.strip_suffix(')'))
346 {
347 return Some((address_type, false, inner));
348 }
349 }
350 None
351}
352
353fn strip_descriptor_checksum(descriptor: &str) -> &str {
354 descriptor
355 .split_once('#')
356 .map_or(descriptor, |(desc, _)| desc)
357}
358
359fn parse_key_origin(origin: &str) -> Result<(Fingerprint, DerivationPath), DisplayAddressError> {
360 let (fingerprint, path) = origin
361 .split_once('/')
362 .map_or((origin, ""), |(fp, path)| (fp, path));
363 let fingerprint = Fingerprint::from_str(fingerprint)?;
364 let path = if path.is_empty() {
365 DerivationPath::master()
366 } else {
367 DerivationPath::from_str(&format!("m/{path}"))?
368 };
369 Ok((fingerprint, path))
370}
371
372fn split_key_suffix(key_and_suffix: &str) -> Result<(&str, DerivationPath), DisplayAddressError> {
373 let Some((key, suffix)) = key_and_suffix.split_once('/') else {
374 return Ok((key_and_suffix, DerivationPath::master()));
375 };
376 let suffix = DerivationPath::from_str(&format!("m/{suffix}"))?;
377 Ok((key, suffix))
378}
379
380fn join_derivation_path(base: &DerivationPath, suffix: &DerivationPath) -> DerivationPath {
381 let mut children = base.as_ref().to_vec();
382 children.extend_from_slice(suffix.as_ref());
383 DerivationPath::from(children)
384}
385
386fn descriptor_key_matches_xpub(key: &str, xpub: Xpub) -> bool {
387 key == xpub.to_string()
388 || key.eq_ignore_ascii_case(&xpub.public_key.serialize().to_lower_hex_string())
389 || key.eq_ignore_ascii_case(
390 &xpub
391 .public_key
392 .x_only_public_key()
393 .0
394 .serialize()
395 .to_lower_hex_string(),
396 )
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[cfg(feature = "specter")]
404 #[test]
405 fn specter_display_carries_the_policy_it_is_given() {
406 let policy = WalletPolicy::from_str(
407 "wpkh([f5acc2fd/84'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/<0;1>/*)",
408 )
409 .expect("valid wallet policy");
410 let context = display_address_context(DeviceType::Specter, "wallet", Some(policy), None)
411 .expect("a Specter context");
412 assert!(matches!(context, Some(DeviceContext::Specter { .. })));
413 assert!(matches!(
414 display_address_context(DeviceType::Specter, "wallet", None, None),
415 Err(DisplayAddressContextError::MissingPolicy(
416 DeviceType::Specter
417 ))
418 ));
419 }
420
421 #[test]
422 fn parses_singlesig_display_descriptor() {
423 let descriptor = "sh(wpkh([f5acc2fd/49h/1h/0h]tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/0/7))#checksum";
424
425 let parsed = parse_singlesig_display_descriptor(strip_descriptor_checksum(descriptor))
426 .expect("display descriptor");
427
428 assert_eq!(parsed.addr_type, AddressType::P2sh);
429 assert_eq!(
430 parsed.fingerprint,
431 Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd])
432 );
433 assert_eq!(
434 parsed.origin_path,
435 DerivationPath::from_str("m/49h/1h/0h").unwrap()
436 );
437 assert_eq!(
438 parsed.full_path,
439 DerivationPath::from_str("m/49h/1h/0h/0/7").unwrap()
440 );
441 assert_eq!(
442 parsed.key,
443 "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"
444 );
445 }
446
447 #[test]
448 fn display_descriptor_requires_origin() {
449 let err = parse_singlesig_display_descriptor("wpkh(tpubDD8d3xExampleKeyMaterial/0/7)")
450 .expect_err("missing origin");
451
452 assert!(err.to_string().contains("Descriptor missing origin info"));
453 }
454
455 #[test]
456 fn display_descriptor_rejects_invalid_base58_key_like_python_hwi() {
457 let err = parse_singlesig_display_descriptor("wpkh([0f056943/84h/1h/0h]not_an_xpub/0/0)")
458 .expect_err("invalid key");
459
460 assert_eq!(
461 err.to_string(),
462 "Character '_' is not a valid base58 character"
463 );
464 }
465
466 #[test]
467 fn parses_coldcard_sortedmulti_display_descriptor() {
468 let descriptor = "sh(wsh(sortedmulti(2,[f5acc2fd/48h/1h/0h/0h/0]0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,[aaaaaaaa/48h/1h/1h/0h/0]03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a)))";
469
470 let parsed = multisig_display_address_from_descriptor(descriptor)
471 .expect("multisig display descriptor");
472
473 assert_eq!(parsed.threshold, 2);
474 assert!(matches!(parsed.address_type, MultisigAddressType::ShWit));
475 assert!(parsed.sorted);
476 assert_eq!(parsed.keys.len(), 2);
477 let origins = parsed
478 .keys
479 .iter()
480 .map(|key| match key {
481 DescriptorPublicKey::Single(key) => key.origin.clone().unwrap().1,
482 _ => panic!("expected concrete public key"),
483 })
484 .collect::<Vec<_>>();
485 assert_eq!(
486 origins,
487 vec![
488 DerivationPath::from_str("m/48h/1h/0h/0h/0").unwrap(),
489 DerivationPath::from_str("m/48h/1h/1h/0h/0").unwrap(),
490 ]
491 );
492 }
493}