From: Vihiga Tyonum Date: Mon, 3 Aug 2026 12:17:37 +0000 (+0100) Subject: feat(hwi): Add hwi subcommand handler X-Git-Url: http://internal-gitweb-vhost/-client-interface/%22https:/encode/struct.CommandStringError.html?a=commitdiff_plain;h=7eab2467994ae5167f2fa19b42baa715b0be1bdc;p=bdk-cli feat(hwi): Add hwi subcommand handler - add devices, register, address and sign tx hwi subcommands --- diff --git a/src/handlers/hwi.rs b/src/handlers/hwi.rs new file mode 100644 index 0000000..5b4d0ca --- /dev/null +++ b/src/handlers/hwi.rs @@ -0,0 +1,142 @@ +//! Hardware wallet (HWI) command handlers. +//! +//! Provides the `hwi` top-level command with subcommands to list connected +//! devices, register a wallet policy, display a receive address on the device +//! and sign a PSBT. Device discovery lives in [`crate::utils::hwi`]. + +use async_hwi::AddressScript; +use bdk_wallet::bitcoin::{Psbt, hex::DisplayHex}; +use clap::{Args, Parser, Subcommand}; +use serde_json::json; +use std::str::FromStr; + +use crate::error::BDKCliError as Error; +use crate::handlers::{AppContext, AsyncAppCommand, Init}; +use crate::utils::hwi::{HwiWallet, enumerate_hwi_devices, first_hwi_device}; + +/// Options shared by all HWI subcommands. +#[derive(Clone, Debug, PartialEq, Eq, Args)] +pub struct HwiOpts { + /// Wallet name registered on the device (required by Ledger and Coldcard). + #[arg(env = "WALLET_NAME", short = 'w', long)] + pub wallet: Option, + + /// External descriptor / wallet policy to register or operate on. + #[arg(env = "EXT_DESCRIPTOR", short = 'e', long)] + pub ext_descriptor: Option, + + /// Registration HMAC returned by `hwi register` (required to reuse a + /// registered policy on a Ledger). + #[arg(env = "HWI_HMAC", long)] + pub hmac: Option, +} + +impl HwiOpts { + fn as_wallet(&self) -> HwiWallet<'_> { + HwiWallet { + name: self.wallet.as_deref(), + policy: self.ext_descriptor.as_deref(), + hmac: self.hmac.as_deref(), + } + } +} + +/// HWI subcommands. +#[derive(Debug, Subcommand, Clone, PartialEq, Eq)] +#[command(rename_all = "snake")] +pub enum HwiSubCommand { + /// List all connected hardware wallet devices. + Devices, + /// Register a wallet policy on the device, returning its HMAC. + Register, + /// Display a receive address on the device for verification. + Address { + /// Address index to derive. + #[arg(long, default_value_t = 0)] + index: u32, + /// Derive from the change (internal) keychain instead of external. + #[arg(long)] + change: bool, + }, + /// Sign a PSBT with the device. + /// + /// The device produces partial signatures; the returned PSBT still needs to + /// be finalized before it can be broadcasted. + Sign { + /// The base64-encoded PSBT to sign. + psbt: String, + }, +} + +/// The `hwi` top-level command. +#[derive(Debug, Parser, Clone, PartialEq, Eq)] +pub struct HwiCommand { + #[command(flatten)] + pub opts: HwiOpts, + #[command(subcommand)] + pub subcommand: HwiSubCommand, +} + +impl AsyncAppCommand> for HwiCommand { + type Output = serde_json::Value; + + async fn execute(&self, ctx: &mut AppContext) -> Result { + let network = ctx.network; + let wallet = self.opts.as_wallet(); + + match &self.subcommand { + HwiSubCommand::Devices => { + let devices = enumerate_hwi_devices(network, &wallet).await?; + let mut listed = Vec::with_capacity(devices.len()); + for device in &devices { + let fingerprint = device.get_master_fingerprint().await?.to_string(); + listed.push(json!({ + "fingerprint": fingerprint, + "model": device.device_kind().to_string(), + })); + } + Ok(json!({ "count": listed.len(), "devices": listed })) + } + + HwiSubCommand::Register => { + let name = self.opts.wallet.as_deref().ok_or_else(|| { + Error::Generic("Wallet name (--wallet) is required to register".to_string()) + })?; + let policy = self.opts.ext_descriptor.as_deref().ok_or_else(|| { + Error::Generic( + "External descriptor (--ext-descriptor) is required to register" + .to_string(), + ) + })?; + let device = first_hwi_device(network, &wallet).await?; + tracing::debug!("Registering wallet '{name}' with policy '{policy}'"); + let hmac = device.register_wallet(name, policy).await?; + let hmac_hex = hmac.map(|h| h.to_lower_hex_string()); + Ok(json!({ "success": true, "hmac": hmac_hex })) + } + + HwiSubCommand::Address { index, change } => { + let device = first_hwi_device(network, &wallet).await?; + let script = AddressScript::Miniscript { + index: *index, + change: *change, + }; + device.display_address(&script).await?; + Ok(json!({ + "success": true, + "index": index, + "change": change, + "message": "Address displayed on device for verification", + })) + } + + HwiSubCommand::Sign { psbt } => { + let mut psbt = Psbt::from_str(psbt) + .map_err(|e| Error::Generic(format!("Failed to parse PSBT: {e}")))?; + let device = first_hwi_device(network, &wallet).await?; + device.sign_tx(&mut psbt).await?; + Ok(json!({ "psbt": psbt.to_string() })) + } + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index a2c4ad5..0dd871a 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -13,6 +13,9 @@ pub mod online; ))] pub mod payjoin; pub mod repl; +#[cfg(feature = "hwi")] +pub mod hwi; + #[cfg(any( feature = "electrum", diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bca0087..17a6dec 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,5 +4,5 @@ pub mod output; pub use common::*; pub mod runtime; pub mod types; -// #[cfg(feature = "hwi")] +#[cfg(feature = "hwi")] pub mod hwi;