From: Vihiga Tyonum Date: Tue, 4 Aug 2026 10:08:07 +0000 (+0100) Subject: feat(hwi): Reuse saved wallet config for hwi X-Git-Url: http://internal-gitweb-vhost/-script/src/example_cli/database/struct.AddrsOutput.html?a=commitdiff_plain;h=a12186601c5fdfc13887d9bf6df096284757602c;p=bdk-cli feat(hwi): Reuse saved wallet config for hwi --- diff --git a/src/handlers/hwi.rs b/src/handlers/hwi.rs index 5b4d0ca..ea6b256 100644 --- a/src/handlers/hwi.rs +++ b/src/handlers/hwi.rs @@ -4,16 +4,20 @@ //! devices, register a wallet policy, display a receive address on the device //! and sign a PSBT. Device discovery lives in [`crate::utils::hwi`]. +use crate::commands::WalletOpts; +use crate::error::BDKCliError as Error; +use crate::handlers::{AppContext, AsyncAppCommand, Init}; +use crate::persister::new_wallet; +use crate::utils::hwi::{HwiWallet, enumerate_hwi_devices, first_hwi_device}; +use crate::utils::load_wallet_config; use async_hwi::AddressScript; -use bdk_wallet::bitcoin::{Psbt, hex::DisplayHex}; +use bdk_wallet::bitcoin::{Psbt, bip32::DerivationPath, hex::DisplayHex}; +use bdk_wallet::{KeychainKind, Wallet}; use clap::{Args, Parser, Subcommand}; use serde_json::json; +use std::path::Path; 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 { @@ -31,20 +35,19 @@ pub struct HwiOpts { 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 { + /// Read an extended public key from the device at a derivation path. + /// + /// Use this first to obtain the key material needed to build a wallet + /// descriptor for the device. + Xpub { + /// Derivation path, e.g. `m/84'/1'/0'`. + #[arg(default_value = "m/84'/1'/0'")] + path: String, + }, /// List all connected hardware wallet devices. Devices, /// Register a wallet policy on the device, returning its HMAC. @@ -77,15 +80,88 @@ pub struct HwiCommand { pub subcommand: HwiSubCommand, } +impl HwiCommand { + /// Load the saved wallet config when a descriptor is needed. Errors if `--wallet` + /// is set but its config cannot be read. + fn load_wallet_opts(&self, datadir: &Path) -> Result, Error> { + if self.opts.ext_descriptor.is_some() { + return Ok(None); + } + match &self.opts.wallet { + Some(name) => load_wallet_config(datadir, name).map(|(opts, _network)| Some(opts)), + None => Ok(None), + } + } + + /// Build the device wallet context, preferring an explicit `--ext-descriptor` + /// and falling back to the loaded config's external descriptor. + fn hwi_wallet<'a>(&'a self, opts: Option<&'a WalletOpts>) -> HwiWallet<'a> { + let policy = self + .opts + .ext_descriptor + .as_deref() + .or_else(|| opts.map(|o| o.ext_descriptor.as_str())); + HwiWallet { + name: self.opts.wallet.as_deref(), + policy, + hmac: self.opts.hmac.as_deref(), + } + } + + /// Derive the address at `index` for the given keychain, from the saved + /// config when available or from an explicit external descriptor otherwise. + fn derive_address( + &self, + network: bdk_wallet::bitcoin::Network, + opts: Option<&WalletOpts>, + index: u32, + change: bool, + ) -> Result, Error> { + let keychain = if change { + KeychainKind::Internal + } else { + KeychainKind::External + }; + + let wallet = if let Some(opts) = opts { + Some(new_wallet(network, opts)?) + } else if let Some(desc) = self.opts.ext_descriptor.as_deref() { + Some( + Wallet::create_single(desc.to_string()) + .network(network) + .create_wallet_no_persist()?, + ) + } else { + None + }; + + Ok(wallet.map(|w| w.peek_address(keychain, index).address.to_string())) + } +} + 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::Xpub { path } => { + let derivation = DerivationPath::from_str(path) + .map_err(|e| Error::Generic(format!("Invalid derivation path: {e}")))?; + let wallet = self.hwi_wallet(None); + let device = first_hwi_device(network, &wallet).await?; + let xpub = device.get_extended_pubkey(&derivation).await?; + let fingerprint = device.get_master_fingerprint().await?.to_string(); + Ok(json!({ + "path": path, + "xpub": xpub.to_string(), + "master_fingerprint": fingerprint, + })) + } + HwiSubCommand::Devices => { + let wallet = self.hwi_wallet(None); let devices = enumerate_hwi_devices(network, &wallet).await?; let mut listed = Vec::with_capacity(devices.len()); for device in &devices { @@ -99,40 +175,49 @@ impl AsyncAppCommand> for HwiCommand { } HwiSubCommand::Register => { - let name = self.opts.wallet.as_deref().ok_or_else(|| { - Error::Generic("Wallet name (--wallet) is required to register".to_string()) + let opts = self.load_wallet_opts(&ctx.datadir)?; + let wallet = self.hwi_wallet(opts.as_ref()); + let name = wallet.name.ok_or_else(|| { + Error::Generic("Wallet name is required to register".to_string()) })?; - let policy = self.opts.ext_descriptor.as_deref().ok_or_else(|| { + let policy = wallet.policy.ok_or_else(|| { Error::Generic( - "External descriptor (--ext-descriptor) is required to register" + "No descriptor found: pass --ext-descriptor or save a wallet config for --wallet" .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 })) + // let device = first_hwi_device(network, &wallet).await?; + let hmac = device_register(network, &wallet, name, policy).await?; + + // let hmac_hex = hmac.map(|h| h.to_lower_hex_string()); + Ok(json!({ "success": true, "hmac": hmac })) } HwiSubCommand::Address { index, change } => { + let opts = self.load_wallet_opts(&ctx.datadir)?; + let wallet = self.hwi_wallet(opts.as_ref()); let device = first_hwi_device(network, &wallet).await?; let script = AddressScript::Miniscript { index: *index, change: *change, }; device.display_address(&script).await?; + let address = self.derive_address(network, opts.as_ref(), *index, *change)?; Ok(json!({ "success": true, "index": index, "change": change, - "message": "Address displayed on device for verification", + "address": address, + "message": "Verify this address matches the one shown on your device", })) } HwiSubCommand::Sign { psbt } => { let mut psbt = Psbt::from_str(psbt) .map_err(|e| Error::Generic(format!("Failed to parse PSBT: {e}")))?; + let opts = self.load_wallet_opts(&ctx.datadir)?; + let wallet = self.hwi_wallet(opts.as_ref()); let device = first_hwi_device(network, &wallet).await?; device.sign_tx(&mut psbt).await?; Ok(json!({ "psbt": psbt.to_string() })) @@ -140,3 +225,15 @@ impl AsyncAppCommand> for HwiCommand { } } } + +/// Register a policy on the first connected device and return its HMAC (hex). +async fn device_register( + network: bdk_wallet::bitcoin::Network, + wallet: &HwiWallet<'_>, + name: &str, + policy: &str, +) -> Result, Error> { + let device = first_hwi_device(network, wallet).await?; + let hmac = device.register_wallet(name, policy).await?; + Ok(hmac.map(|h| h.to_lower_hex_string())) +} diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 971d68e..6aebddd 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -212,7 +212,7 @@ pub struct CreateTxCommand { pub enable_rbf: bool, /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `PSBT_GLOBAL_XPUB` and more details to let the signer identify the change output. - #[arg(long = "offline_signer")] + #[arg(long = "offline_signer", default_value_t = DEFAULT_OFFLINE_SIGNER)] pub offline_signer: bool, /// Selects which utxos *must* be spent. @@ -550,7 +550,7 @@ pub struct BumpFeeCommand { pub shrink_address: Option
, /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. - #[arg(long = "offline_signer", default_value_t = DEFAULT_OFFLINE_SIGNER)] + #[arg(long = "offline_signer")] pub offline_signer: bool, /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used.