]> Untitled Git - bdk-cli/commitdiff
test(hwi): add more tests to helper fns
authorVihiga Tyonum <withtvpeter@gmail.com>
Thu, 15 Oct 2026 00:32:04 +0000 (01:32 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Wed, 21 Oct 2026 11:43:10 +0000 (12:43 +0100)
src/handlers/hwi.rs
src/utils/descriptors.rs
src/utils/hwi.rs

index d04210cf2c91c4e58b7b1785ae32fbff174af9c3..4bf285a42c75c83b1e546e0163f75a261a4f88f7 100644 (file)
@@ -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<AppContext<Init>> 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<DescriptorPublicKey> {
+        Descriptor::<DescriptorPublicKey>::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('#'));
     }
 
index edba4fcbf10ba0de8434ffe03341dc9310584292..dcbac46b79c69c27a97d512341685b0d4eae2c68 100644 (file)
@@ -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() {
index 4aafe7560ec1e85e7c2d8e6908d194cc4dd79414..5f144f62b251c7445e4f185864f77ce931be0b34 100644 (file)
@@ -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<Option<[u8; 32]>, 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());
+    }
+}