Skip to main content

bhwi_cli/
get_descriptors.rs

1//! Device-backed descriptor derivation and stdout formatting.
2//!
3//! Descriptor options and derivation helpers are reexported from `bhwi-async`.
4
5use crate::DeviceManager;
6use anyhow::Result;
7use async_trait::async_trait;
8
9pub use bhwi_async::descriptors::*;
10use miniscript::{Descriptor, DescriptorPublicKey};
11use serde::{Serialize, Serializer};
12
13use crate::{OutputFormat, select_device};
14
15/// A ranged descriptor record suitable for Bitcoin Core keypool import.
16#[derive(Debug, Clone, Serialize)]
17pub struct KeypoolDescriptor {
18    /// The wildcard descriptor, serialized as a string without a checksum.
19    #[serde(serialize_with = "serialize_descriptor")]
20    pub descriptor: Descriptor<DescriptorPublicKey>,
21    /// The inclusive first and last child indices; the descriptor remains ranged.
22    pub range: [u32; 2],
23    /// Whether this describes the internal change branch.
24    pub internal: bool,
25    /// Whether the descriptor should be added to the keypool.
26    pub keypool: bool,
27}
28
29fn serialize_descriptor<S>(
30    descriptor: &Descriptor<DescriptorPublicKey>,
31    ser: S,
32) -> Result<S::Ok, S::Error>
33where
34    S: Serializer,
35{
36    ser.serialize_str(&format!("{descriptor:#}"))
37}
38
39/// Asynchronous CLI helpers that print descriptors to stdout.
40///
41/// Returned futures need not be `Send`.
42#[async_trait(?Send)]
43pub trait DescriptorOutput {
44    /// Derives and prints one ranged keypool descriptor.
45    ///
46    /// `format` selects a table or JSON record; `None` prints a descriptor with
47    /// range and import flags. The [`DeviceManager`] implementation performs
48    /// device I/O and prints nothing if no device is found.
49    async fn get_keypool(
50        &self,
51        options: GetKeypoolOptions,
52        format: Option<OutputFormat>,
53    ) -> Result<()>;
54    /// Derives and prints receive and change descriptors for supported address types.
55    ///
56    /// `account` defaults to zero. `format` selects a table or a JSON object with
57    /// `receive` and `internal` arrays; `None` prints one descriptor per line.
58    /// The [`DeviceManager`] implementation performs device I/O and prints
59    /// nothing if no device is found.
60    async fn get_pubkey_descriptors(
61        &self,
62        account: Option<u32>,
63        format: Option<OutputFormat>,
64    ) -> Result<()>;
65}
66
67#[async_trait(?Send)]
68impl DescriptorOutput for DeviceManager {
69    async fn get_keypool(
70        &self,
71        options: GetKeypoolOptions,
72        format: Option<OutputFormat>,
73    ) -> Result<()> {
74        let Some(mut device) = select_device(self).await? else {
75            return Ok(());
76        };
77        let master_fingerprint = device.fingerprint().await?;
78        let descriptor =
79            get_keypool_descriptor(device.device().as_mut(), master_fingerprint, &options).await?;
80        let keypool = KeypoolDescriptor {
81            descriptor,
82            range: [options.start, options.end],
83            internal: options.internal,
84            keypool: true,
85        };
86        match format {
87            Some(OutputFormat::Json) => println!("{}", serde_json::to_string(&keypool)?),
88            Some(OutputFormat::Pretty) => {
89                println!("{:<9} | {:<8} | {:<10}", "Range", "Internal", "Descriptor");
90                println!("{}", "-".repeat(120));
91                println!(
92                    "{}-{} | {:<8} | {}",
93                    keypool.range[0], keypool.range[1], keypool.internal, keypool.descriptor
94                );
95            }
96            None => {
97                println!(
98                    "{:#} range={}-{} internal={} keypool=true",
99                    keypool.descriptor, keypool.range[0], keypool.range[1], keypool.internal
100                );
101            }
102        }
103        Ok(())
104    }
105
106    // TODO: Python HWI outputs using h instead of ' for hardened paths but
107    // rust-miniscript doesn't allow this when displaying an entire Descriptor.
108    async fn get_pubkey_descriptors(
109        &self,
110        account: Option<u32>,
111        format: Option<OutputFormat>,
112    ) -> Result<()> {
113        let Some(mut device) = select_device(self).await? else {
114            return Ok(());
115        };
116        let network = self.selector.network;
117        let fingerprint = device.fingerprint().await?;
118        let PubkeyDescriptors { receive, internal } = get_pubkey_descriptors(
119            device.device().as_mut(),
120            fingerprint,
121            account.unwrap_or(0),
122            network,
123        )
124        .await?;
125        match format {
126            Some(OutputFormat::Pretty) => {
127                let header = format!("{:<10} | {:<120}", "Purpose", "Descriptor");
128                println!("{}", header);
129                println!("{}", "-".repeat(header.len()));
130                for (purpose, items) in [("internal", internal), ("receive", receive)] {
131                    for item in items {
132                        println!("{:<10} | {:<120}", purpose, item);
133                    }
134                    println!("{}", "-".repeat(header.len()));
135                }
136            }
137            Some(OutputFormat::Json) => {
138                println!(
139                    "{}",
140                    serde_json::json!(
141                    {
142                        "receive": receive,
143                        "internal": internal
144                    })
145                );
146            }
147            None => {
148                receive
149                    .iter()
150                    .chain(internal.iter())
151                    .for_each(|d| println!("{d:#}"));
152            }
153        }
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use std::str::FromStr;
161
162    use miniscript::{Descriptor, DescriptorPublicKey};
163
164    use super::KeypoolDescriptor;
165
166    #[test]
167    fn keypool_descriptor_serializes_json_shape() {
168        let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
169            "wpkh([e3ebcc79/84'/1'/0']tpubDCKD5cdxMEFd2i4cNa3PJUbUHMsGDxsnfqjxVpMoG1ymWYUQUaZzTcHQo3JwYgaKe2FyKGA2FzGPSVczBoAiHGyERuA1mZ2UkGKufEnUxKk/0/*)",
170        )
171        .unwrap();
172        let keypool = KeypoolDescriptor {
173            descriptor,
174            range: [7, 11],
175            internal: false,
176            keypool: true,
177        };
178
179        let json = serde_json::to_value(keypool).unwrap();
180
181        assert_eq!(json["range"], serde_json::json!([7, 11]));
182        assert_eq!(json["internal"], false);
183        assert_eq!(json["keypool"], true);
184        assert!(json["descriptor"].as_str().unwrap().starts_with("wpkh("));
185    }
186}