Skip to content

feat(hwi): Add Hardware Wallet Support - #311

Merged
tvpeter merged 11 commits into
bitcoindevkit:masterfrom
tvpeter:feat/add-hwi
Oct 21, 2026
Merged

tvpeter merged 11 commits into
bitcoindevkit:masterfrom
tvpeter:feat/add-hwi

Conversation

@tvpeter

@tvpeter tvpeter commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Adds hardware wallet support to the project using async-hwi library. Supported hardware devices include Specter, Jade, Ledger, Coldcard, and BitBox02 together with their simulators.

This supersedes #207, which was approved but went stale after the project restructure. The feature has been reimplemented from scratch on the current modular structure, and the design is reworked around the saved wallet config.

Commands:

The feature introduced two top-level commands (hwi devices and hwi xpub) and wallet subcommands (register, address, and sign).

  • hwi xpub [<path>] [--type <wpkh|tr|sh-wpkh|pkh>]: reads a device extended public key. Outputs a ready-to-use key expression and external/internal descriptors.
  • hwi devices: list every connected device (fingerprint + model).
  • register: Wallet scoped operation (wallet -w <walletname> hwi register). It registers the wallet policy on the device (returns the HMAC where the device supports it). The policy comes from the saved wallet config.
  • address: Also a wallet-scoped operation. It displays the address on-device where supported, and derive it locally.
  • sign <psbt>: sign a PSBT with the device.

fixes #194

Notes to the reviewers

  • xpub and devices are top-level because you need xpub to build a wallet.
  • register, address, sign live under wallet and read descriptors from the saved config.
  • Policy handling. async-hwi expects a single multipath (…/<0;1>/*) public wallet policy. BDK stores the external/internal chains as two single-path descriptors, so the code combines them (from the wallet's public_descriptor, guaranteeing xpub-only) into the multipath form.
  • create_tx defaults --offline_signer to true when built with hwi, so PSBTs carry the xpub data the hardware wallets need.

Known limitations:

  • Only the first connected device is used
  • sign produces partial signatures only; finalize separately with finalize_psbt
  • On-device address display for single-sig is limited to taproot (async-hwi has no simple wpkh display path); other single-sig returns the locally derived address.

HWI subcommand implementation

  • hwi xpub
  • hwi devices
  • wallet … hwi register
  • wallet … hwi address
  • wallet … hwi sign

Devices tested:

  • specter simulator: devices, xpub, register, sign
  • bitbox simulator: devices, xpub, register, sign
  • coldcard device: devices, xpub, address, sign

Changelog notice

  • Added hwi feature with hardware wallet support (Specter, Jade, Ledger, Coldcard, BitBox02) to read device xpubs, list devices, register a wallet policy, display/verify addresses, and sign PSBTs.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@codecov

codecov Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 30.13972% with 350 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.97%. Comparing base (3f6b023) to head (d34e6bf).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/utils/hwi.rs 14.59% 158 Missing ⚠️
src/handlers/hwi.rs 36.40% 152 Missing ⚠️
src/utils/descriptors.rs 60.65% 24 Missing ⚠️
src/main.rs 0.00% 13 Missing ⚠️
src/handlers/repl.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #311      +/-   ##
==========================================
- Coverage   61.46%   57.97%   -3.50%     
==========================================
  Files          23       25       +2     
  Lines        3989     4490     +501     
==========================================
+ Hits         2452     2603     +151     
- Misses       1537     1887     +350     
Flag Coverage Δ
rust 57.97% <30.13%> (-3.50%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tvpeter tvpeter added this to the CLI 4.1.0 milestone Sep 28, 2026
@tvpeter

tvpeter commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Below are the steps one can follow to test this hardware integration using specter simulator and bitcoind in regtest mode:

  1. Build the specter hardware wallet simulator by cloning their repo and following the build instruction.
  2. From the UI of the simulator, set a PIN and create a seed
  3. Run the hwi devices command:
cargo run --features hwi -- -n regtest hwi devices 
`
Finished dev profile [unoptimized + debuginfo] target(s) in 0.17s
     Running target/debug/bdk-cli -n regtest hwi devices
{
  "count": 1,
  "devices": [
    {
      "fingerprint": "2b1f6f60",
      "model": "specter-simulator"
    }
  ]
}
`
  1. Generate the xpub and descriptors:
cargo run --features hwi -- -n regtest hwi xpub "m/84'/1'/1'"
`
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.17s
     Running target/debug/bdk-cli -n regtest hwi xpub 'm/84'\''/1'\''/1'\'''
{
  "external_descriptor": "wpkh([2b1f6f60/84'/1'/1']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/0/*)#3d9lanp5",
  "internal_descriptor": "wpkh([2b1f6f60/84'/1'/1']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/1/*)#qeq7qx3v",
  "key_expression": "[2b1f6f60/84'/1'/1']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA",
  "master_fingerprint": "2b1f6f60",
  "path": "m/84'/1'/1'",
  "script_type": "wpkh",
  "xpub": "tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA"
}
`
  1. Create a wallet config using the descriptors obtained in step 4:
cargo run --features hwi,rpc -- --network regtest wallet -w spec_regtest_wallett config -f -e 
"wpkh([2b1f6f60/84'/1'/1']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/0/*)#3d9lanp5" -i "wpkh([2b1f6f60/84'/1'/1']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/1/*)#qeq7qx3v" -d sqlite -c rpc -u "127.0.0.1:18443" -a username:password
`
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.28s
     Running target/debug/bdk-cli --network regtest wallet -w spec_regtest_wallett config -f -e 'wpkh([2b1f6f60/84'\''/1'\''/1'\'']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/0/*)#3d9lanp5' -i 'wpkh([2b1f6f60/84'\''/1'\''/1'\'']tpubDD1quQtjKgfEz2bqvkcz3VqSeV3vvv72bfhC88kRh1Dh7tFhyh3yUomH3qLruGevPv9Zyek4JqHJob7aDwtAKbKrqLASyv137CQyRhDSncA/1/*)#qeq7qx3v' -d sqlite -c rpc -u '127.0.0.1:18443' -a 'username:password'
{
  "message": "Wallet 'spec_regtest_wallett' initialized successfully in \"/Users/x/x/config.toml\""
}
`
  1. Register the saved wallet config on the device:
cargo run --features hwi,rpc -- --network regtest wallet -w spec_regtest_wallett hwi register
`
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.18s
     Running target/debug/bdk-cli --network regtest wallet -w spec_regtest_wallett hwi register
{
  "hmac": null,
  "next_step": null,
  "success": true
}
`
  1. Generate an address:
cargo run --features hwi,rpc -- --network regtest wallet -w spec_regtest_wallett hwi address --index 0
`
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.20s
     Running target/debug/bdk-cli --network regtest wallet -w spec_regtest_wallett hwi address --index 0
{
  "address": "bcrt1qwp5fqrqfuzph8uvhwsrews5pdtfjq6adzqqety",
  "change": false,
  "displayed_on_device": false,
  "index": 0,
  "message": "Device did not display the address (Unimplemented method); showing the locally derived address"
}
`
  1. The rest of the steps are to fund the address, sync, create a transaction and sign (using the simulator), finalize the psbt, extract and broadcast. Sync and confirm the balance. I have attached a screenshot below as this writeup is becoming too long
end_to_end_specter_1

@vadim-anfv vadim-anfv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

Hardware wallet support in bdk-cli is something I have been waiting for, so thanks for taking this on. Left a few comments inline. I tested this with a Jade.

Comment thread src/handlers/hwi.rs Outdated
Comment thread src/handlers/offline.rs Outdated
Comment thread src/handlers/hwi.rs
Comment thread src/handlers/hwi.rs Outdated
Comment thread src/utils/hwi.rs Outdated
Comment thread src/handlers/hwi.rs
Comment thread src/utils/hwi.rs
Comment thread src/utils/hwi.rs Outdated
@Musab1258

Copy link
Copy Markdown
Contributor

Concept ACK, I reviewed at 1ec353b. I tested on the Specter simulator (regtest): hwi devices → hwi xpub → wallet config → hwi register → hwi address → create_tx → hwi sign (confirmed on the simulator) → finalize_psbt → extract_psbt → broadcast. The tx reached the mempool, and the balance change matched.

Comment thread src/utils/hwi.rs Outdated
Comment on lines +216 to +219
warn!(
"Using hardware wallet {fingerprint} ({})",
device.device_kind()
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment on this function says, "The chosen device's fingerprint and model are always logged," but as per my reply to @vadim-anfv on the Jade thread, this warn! doesn't reach the user by default (I only see it with RUST_LOG=warn, and only after switching the import to log). In the sign handler (handlers/hwi.rs lines 300–301 as at commit 1ec353b) the selection happens right before sign_tx, so this line is the user's only indication of which device they are about to confirm on. The "N hardware wallets connected … using the first" warning above it has the same problem.

Since the point is to tell the user before the operation starts, would it make sense to print these two notices unconditionally instead of through the logger? There seems to be a precedent: the "WARNING: … was accessible to other users" message from config (src/utils/common.rs:403) is printed with eprintln!, so it shows without RUST_LOG.

I only tested with a single simulator. With one device connected, --fingerprint behaves as expected: a matching fingerprint works, and a non-matching one fails with "No connected device matches fingerprint …". I could not test the multi-device behavior.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for the review. I have replaced tracing with log and used eprintln! to display the selected device.

@tvpeter

tvpeter commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Concept ACK

Hardware wallet support in bdk-cli is something I have been waiting for, so thanks for taking this on. Left a few comments inline. I tested this with a Jade.

Thank you so much for testing this PR. I know it is a tedious task testing this feature. I have updated the PR to cover all your feedback.

@tvpeter
tvpeter force-pushed the feat/add-hwi branch 2 times, most recently from 22dd104 to 4e8d272 Compare October 15, 2026 16:13
- add devices, register, address and sign tx
hwi subcommands
- add hidapi dependencies to workflows to fix
failing compilation
- move the register, address and sign commands
to subwallet commands
- add --fingerprint to `hwi xpub` and `wallet <name> hwi`, select the
  matching device, and enumerate physical devices before simulators.
- Move the single-sig script-type logic and multipath detection
  out of the HWI handler and into utils/descriptors.

- Add SingleSigScriptType (ValueEnum) with purpose/as_str/descriptor
  and from_derivation_path.
- Add is_multipath_descriptor, a wrapper over the existing
  descriptor_path_count primitive.
- Drop the contains_private_key helper; the final DescriptorPublicKey
  parse already rejects private keys.
- Add BitBox simulator endpoint as a const.

@vadim-anfv vadim-anfv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK cc730d8

Thanks for working through all eight of my comments. Some of the fixes are better than what I suggested, the simulator endpoint as a named constant for one.

Re-tested on a Jade with a throwaway seed:

  • hwi xpub --type tr now reads m/86'/0'/0'
  • the warn! lines reach the user with no RUST_LOG set
  • --fingerprint is accepted in both positions and shows up in --help
  • hwi devices answers immediately instead of waiting on the BitBox simulator

Two I checked by reading rather than running: the per-device error needs an uninitialized device, and the Jade with_wallet fix is identical to the suggestion I tested earlier.

One thing left inline.

Comment thread src/utils/hwi.rs Outdated

@vadim-anfv vadim-anfv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 7cbce6e

Comment thread src/handlers/hwi.rs Outdated
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)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above says single-sig wallets skip the policy, but build_device_policy(...)? runs for every wallet kind, and the result is only used when kind is Policy. When I tested at 7cbce6e on the Specter simulator (regtest):

A wpkh wallet with external /0/* and internal /5/* fails hwi address with “Cannot derive a multipath policy: …”, although no policy is needed for single-sig.

A wpkh wallet with only an external descriptor prints, by default, “Wallet has no separate internal (change) descriptor; the device policy will only cover receive addresses.”, although no policy is attached.

Would it make sense to build the policy only for the Policy kind, like this:

matches!(kind, WalletKind::Policy)
    .then(|| build_device_policy(...))
    .transpose()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the policy building to only applicable WalletKind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified at d34e6bf (Specter simulator, regtest, single device): the hwi address on the wpkh wallet with internal /5/* now returns the address instead of the “Cannot derive a multipath policy” error, and the external-only wallet no longer prints the receive-only warning.

Comment thread src/handlers/hwi.rs
let derivation = DerivationPath::from_str(&path)
.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?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At 7cbce6e, On Specter simulator: bdk-cli -n bitcoin hwi xpub "m/84'/0'/0'" returns a tpub… key and no warning. The key follows my Specter device’s network setting, not -n. Should xpub compare the network of the returned key with -n and fail early, rather than emitting descriptors that the wallet config will later reject?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check for the device network against the supplied network.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though the network comparison is coarse because hardware devices are aware of only two kinds of network, "mainnet" and "testnet" and thankfully, "NetworkKind" provides that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified at d34e6bf: with the Specter simulator on testnet, -n bitcoin hwi xpub "m/84'/0'/0'" now fails, and -n regtest still returns a tpub.

@Musab1258 Musab1258 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK d34e6bf

cargo test --features hwi,rpc passes (35 unit tests, 18 integration tests). I tested on the Specter simulator (regtest) with one device: hwi devices, hwi xpub, wallet config, register, address, create_tx, sign (the “Using hardware wallet” notice appears before the confirmation screen), finalize_psbt, extract_psbt, broadcast, --fingerprint before and after the subcommand, and the three follow-up fixes (the “Using hardware wallet” notice now shows without RUST_LOG, single-sig policy building, and the xpub network check). I reviewed the PR head, not commit by commit.

@tvpeter

tvpeter commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

tACK d34e6bf

cargo test --features hwi,rpc passes (35 unit tests, 18 integration tests). I tested on the Specter simulator (regtest) with one device: hwi devices, hwi xpub, wallet config, register, address, create_tx, sign (the “Using hardware wallet” notice appears before the confirmation screen), finalize_psbt, extract_psbt, broadcast, --fingerprint before and after the subcommand, and the three follow-up fixes (the “Using hardware wallet” notice now shows without RUST_LOG, single-sig policy building, and the xpub network check). I reviewed the PR head, not commit by commit.

Thank you for the comprehensive review. This feature is a heavy one to test.

@tvpeter
tvpeter merged commit 525250e into bitcoindevkit:master Oct 21, 2026
7 of 9 checks passed
@github-project-automation github-project-automation Bot moved this to Done in BDK-CLI Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Add back HWI signing support

3 participants