Skip to main content

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}