From: Vihiga Tyonum Date: Thu, 15 Oct 2026 00:32:04 +0000 (+0100) Subject: test(hwi): add more tests to helper fns X-Git-Url: http://internal-gitweb-vhost/-script/src/example_cli/database/enum.AnyDatabase.html?a=commitdiff_plain;h=d34e6bf020a9af14edc44af746f4895c9afb01c6;p=bdk-cli test(hwi): add more tests to helper fns --- diff --git a/src/handlers/hwi.rs b/src/handlers/hwi.rs index d04210c..4bf285a 100644 --- a/src/handlers/hwi.rs +++ b/src/handlers/hwi.rs @@ -23,7 +23,7 @@ use crate::utils::descriptors::{SingleSigScriptType, is_multipath_descriptor, st use crate::utils::hwi::{HwiWallet, enumerate_hwi_devices, first_hwi_device}; use bdk_wallet::bitcoin::{ - Network, Psbt, + Network, NetworkKind, Psbt, bip32::{ChildNumber, DerivationPath}, hex::DisplayHex, }; @@ -86,6 +86,20 @@ impl AsyncAppCommand> for HwiCommand { .map_err(|e| Error::Generic(format!("Invalid derivation path: {e}")))?; let device = first_hwi_device(network, &hwi_wallet, target_fp).await?; let xpub = device.get_extended_pubkey(&derivation).await?; + + if xpub.network != network.into() { + let device_network = if xpub.network == NetworkKind::Main { + "mainnet" + } else { + "testnet" + }; + + return Err(Error::Generic(format!( + "Device returned a {device_network} but wallet network is {network}.\ + Set the device to the matching network." + ))); + } + let master_fingerprint = device.get_master_fingerprint().await?.to_string(); let origin = path.trim_start_matches('m').trim_start_matches('/'); let key_expression = format!("[{master_fingerprint}/{origin}]{xpub}"); @@ -210,16 +224,19 @@ impl WalletHwiCommand { let ext = ext_desc.to_string(); let int = int_desc.to_string(); let int = if int == ext { None } else { Some(int) }; - let policy = build_device_policy(&ext, int.as_deref(), network)?; + let policy = matches!(kind, WalletKind::Policy) + .then(|| build_device_policy(&ext, int.as_deref(), network)) + .transpose()?; let hwi_wallet = HwiWallet { name: Some(wallet_name), - policy: matches!(kind, WalletKind::Policy).then_some(policy.as_str()), + policy: policy.as_deref(), hmac: self.hmac.as_deref(), }; match &self.subcommand { WalletHwiSubCommand::Register => { + let policy = build_device_policy(&ext, int.as_deref(), network)?; log::debug!("Registering wallet '{wallet_name}' with policy '{policy}'"); let device = first_hwi_device(network, &hwi_wallet, target_fp).await?; match device.register_wallet(wallet_name, &policy).await { @@ -308,7 +325,7 @@ impl WalletHwiCommand { } /// How a wallet's descriptor maps onto a hardware device's capabilities. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Debug, Eq)] enum WalletKind { /// BIP-86 taproot single-sig — shown on device via the simple P2TR path. TaprootSingleSig, @@ -412,12 +429,52 @@ mod tests { const EXT: &str = "wpkh([f5acc2fd/84'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/0/*)"; const INT: &str = "wpkh([f5acc2fd/84'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/1/*)"; + const TPUB: &str = "tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP"; + + fn desc(s: &str) -> Descriptor { + Descriptor::::from_str(s).unwrap() + } #[test] fn cli_definition_is_valid() { CliOpts::command().debug_assert(); } + #[test] + fn classifies_wallet_kinds() { + assert_eq!(WalletKind::classify(&desc(EXT)), WalletKind::SingleSig); // wpkh + assert_eq!( + WalletKind::classify(&desc(&format!("pkh([f5acc2fd/44'/1'/0']{TPUB}/0/*)"))), + WalletKind::SingleSig + ); + assert_eq!( + WalletKind::classify(&desc(&format!("sh(wpkh([f5acc2fd/49'/1'/0']{TPUB}/0/*))"))), + WalletKind::SingleSig + ); + assert_eq!( + WalletKind::classify(&desc(&format!("tr([f5acc2fd/86'/1'/0']{TPUB}/0/*)"))), + WalletKind::TaprootSingleSig + ); + // wsh(...) is neither single-sig nor taproot single-sig. + assert_eq!( + WalletKind::classify(&desc(&format!( + "wsh(multi(1,[f5acc2fd/84'/1'/0']{TPUB}/0/*))" + ))), + WalletKind::Policy + ); + } + + #[test] + fn taproot_address_path_builds_full_path() { + let tr = desc(&format!("tr([f5acc2fd/86'/1'/0']{TPUB}/0/*)")); + assert_eq!( + taproot_address_path(&tr, 5).unwrap(), + DerivationPath::from_str("m/86'/1'/0'/0/5").unwrap() + ); + // A non-taproot descriptor has no on-device taproot path. + assert!(taproot_address_path(&desc(EXT), 0).is_none()); + } + #[test] fn policy_combines_and_canonicalizes() { let policy = build_device_policy(EXT, Some(INT), Network::Testnet).unwrap(); @@ -425,7 +482,6 @@ mod tests { assert!(policy.contains("<0;1>")); assert!(!policy.contains("/0/*")); assert!(!policy.contains("/1/*")); - // ...and re-serialized with a checksum. assert!(policy.contains('#')); } diff --git a/src/utils/descriptors.rs b/src/utils/descriptors.rs index edba4fc..dcbac46 100644 --- a/src/utils/descriptors.rs +++ b/src/utils/descriptors.rs @@ -352,6 +352,14 @@ mod multipath_tests { assert!(!is_multipath_descriptor(SINGLE, Network::Testnet).unwrap()); } + #[cfg(feature = "hwi")] + #[test] + fn strips_trailing_checksum() { + assert_eq!(strip_checksum(SINGLE), SINGLE.split('#').next().unwrap()); + assert!(!strip_checksum(SINGLE).contains('#')); + assert_eq!(strip_checksum(MULTIPATH), MULTIPATH); + } + #[cfg(feature = "hwi")] #[test] fn infers_script_type_from_purpose() { diff --git a/src/utils/hwi.rs b/src/utils/hwi.rs index 4aafe75..5f144f6 100644 --- a/src/utils/hwi.rs +++ b/src/utils/hwi.rs @@ -64,11 +64,12 @@ pub async fn enumerate_hwi_devices( } match device.get_info().await { Ok(info) => { - if info.jade_state == jade::api::JadeState::Locked - && let Err(e) = device.auth().await - { - warn!("Jade authentication failed: {e:?}"); - continue; + if info.jade_state == jade::api::JadeState::Locked { + eprintln!("Jade is locked; enter its PIN on the device to continue."); + if let Err(e) = device.auth().await { + warn!("Jade authentication failed: {e:?}"); + continue; + } } devices.push(device.into()); } @@ -175,8 +176,10 @@ pub async fn enumerate_hwi_devices( /// Select a single connected device, by `target_fingerprint` when given. /// /// With no target and several devices connected, the first (physical devices -/// rank before simulators) is used and a warning lists the alternatives. The -/// chosen device's fingerprint and model are always logged. +/// rank before simulators) is used and a notice lists the alternatives. The +/// chosen device's fingerprint and model are always printed to stderr, so the +/// user can confirm which device an operation runs against regardless of the +/// log level. pub async fn first_hwi_device( network: Network, wallet: &HwiWallet<'_>, @@ -211,7 +214,7 @@ pub async fn first_hwi_device( None => { if labeled.len() > 1 { let fingerprints: Vec<&str> = labeled.iter().map(|(fp, _)| fp.as_str()).collect(); - warn!( + eprintln!( "{} hardware wallets connected ({}); using the first. Select one with --fingerprint.", labeled.len(), fingerprints.join(", ") @@ -224,7 +227,7 @@ pub async fn first_hwi_device( } }; - warn!( + eprintln!( "Using hardware wallet {fingerprint} ({})", device.device_kind() ); @@ -287,3 +290,33 @@ fn parse_registration_hmac(hmac: Option<&str>) -> Result, Error h.copy_from_slice(&bytes); Ok(Some(h)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hmac_none_is_none() { + assert_eq!(parse_registration_hmac(None).unwrap(), None); + } + + #[test] + fn hmac_parses_32_bytes() { + let hex = "ab".repeat(32); + assert_eq!( + parse_registration_hmac(Some(&hex)).unwrap(), + Some([0xab; 32]) + ); + } + + #[test] + fn hmac_rejects_wrong_length() { + // 31 bytes is one short. + assert!(parse_registration_hmac(Some(&"ab".repeat(31))).is_err()); + } + + #[test] + fn hmac_rejects_non_hex() { + assert!(parse_registration_hmac(Some("zz")).is_err()); + } +}