bhwi_cli/address.rs
1//! Device-backed address retrieval with stdout output.
2
3use anyhow::Result;
4use async_trait::async_trait;
5#[cfg(feature = "ledger")]
6use bhwi::ledger::{LedgerWalletPolicy, Version};
7use bhwi_async::{DeviceContext, DisplayAddress};
8use bitcoin::address::AddressType;
9use miniscript::descriptor::WalletPolicy;
10
11use bhwi_async_transport::DeviceType;
12
13use crate::DeviceManager;
14
15use crate::select_device;
16
17/// An address request from a derivation path or a named wallet policy.
18#[allow(clippy::large_enum_variant)]
19#[derive(Debug, Clone)]
20pub enum AddressTarget {
21 /// An address derived directly from a BIP32 path.
22 Path {
23 /// The BIP32 derivation path to parse.
24 path: String,
25 /// Whether to request display on the device, where supported.
26 display: bool,
27 /// The address encoding, or the backend's default when absent.
28 address_format: Option<AddressType>,
29 },
30 /// An address derived from a wallet policy at a branch and index.
31 ///
32 /// BitBox02 and Specter require `wallet_descriptor`. Ledger accepts an HMAC
33 /// and policy together or neither; missing required context is then reported
34 /// by the backend. Coldcard and Jade resolve the policy by name on-device.
35 /// Trezor and KeepKey do not support this named-policy mode.
36 Descriptor {
37 /// The child address index.
38 index: u32,
39 /// Whether to select the internal change branch.
40 change: bool,
41 /// Whether to request display on the device, where supported.
42 display: bool,
43 /// The registered wallet name, also used when constructing a Ledger policy.
44 descriptor_name: String,
45 /// A Ledger registration HMAC as 64 hexadecimal characters, if supplied.
46 hmac: Option<String>,
47 /// The wallet policy required by BitBox02 and Specter, or paired with a Ledger HMAC.
48 wallet_descriptor: Option<WalletPolicy>,
49 },
50}
51
52/// An asynchronous CLI helper that prints a device-derived address.
53///
54/// Returned futures need not be `Send`.
55#[async_trait(?Send)]
56pub trait AddressOutput {
57 /// Retrieves an address and prints it to stdout.
58 ///
59 /// The [`DeviceManager`] implementation selects a device, may warn on stderr,
60 /// and produces no address output if none is found. Returns selection,
61 /// parsing, context, or device errors rather than an address value.
62 async fn get_address(&self, target: AddressTarget) -> Result<()>;
63}
64
65#[async_trait(?Send)]
66impl AddressOutput for DeviceManager {
67 async fn get_address(&self, target: AddressTarget) -> Result<()> {
68 let Some(mut device) = select_device(self).await? else {
69 return Ok(());
70 };
71 let (display_address, context) = match target {
72 AddressTarget::Path {
73 path,
74 display,
75 address_format,
76 } => (
77 DisplayAddress::ByPath {
78 path: path.parse()?,
79 display,
80 address_format,
81 },
82 None,
83 ),
84 AddressTarget::Descriptor {
85 index,
86 change,
87 display,
88 descriptor_name,
89 hmac,
90 wallet_descriptor,
91 } => {
92 // BitBox re-supplies the policy descriptor each time; Ledger needs the
93 // registered policy plus its hmac; Coldcard/Jade resolve by name on-device.
94 let context = match device.device_type() {
95 #[cfg(feature = "bitbox")]
96 DeviceType::BitBox02 => {
97 let wallet_policy = wallet_descriptor.ok_or_else(|| {
98 anyhow::anyhow!(
99 "--wallet-descriptor is required for BitBox descriptor addresses"
100 )
101 })?;
102 Some(DeviceContext::BitBox {
103 policy: wallet_policy,
104 })
105 }
106 #[cfg(feature = "ledger")]
107 DeviceType::Ledger => match (hmac, wallet_descriptor) {
108 (Some(hmac_hex), Some(wallet_policy)) => {
109 let hmac = hex::decode(&hmac_hex)
110 .map_err(|e| anyhow::anyhow!("invalid hmac hex: {e}"))?;
111 let hmac: [u8; 32] = hmac.try_into().map_err(|_| {
112 anyhow::anyhow!("hmac must be 32 bytes (64 hex chars)")
113 })?;
114 let ledger_policy = LedgerWalletPolicy::new(
115 descriptor_name.clone(),
116 Version::V2,
117 wallet_policy,
118 );
119 Some(DeviceContext::Ledger {
120 wallet_policy: ledger_policy,
121 wallet_hmac: Some(hmac),
122 })
123 }
124 (None, None) => None,
125 _ => anyhow::bail!(
126 "both --hmac and --wallet-descriptor must be provided for Ledger descriptor addresses"
127 ),
128 },
129 #[cfg(feature = "specter")]
130 DeviceType::Specter => {
131 let policy = wallet_descriptor.ok_or_else(|| {
132 anyhow::anyhow!(
133 "--wallet-descriptor is required for Specter descriptor addresses"
134 )
135 })?;
136 Some(DeviceContext::Specter { policy })
137 }
138 _ => None,
139 };
140 (
141 DisplayAddress::ByDescriptor {
142 index,
143 change,
144 display,
145 descriptor_name,
146 },
147 context,
148 )
149 }
150 };
151 let address = device
152 .device()
153 .display_address(display_address, context)
154 .await?;
155 println!("{address}");
156 Ok(())
157 }
158}