Hardware Wallet PSBTs
walletrs does not talk USB or HID. Signing with hardware wallets is the client’s job. The server’s role for HWW flows is:
- Hold the wallet’s descriptor (compiled from registered customer-managed xpubs).
- Build the unsigned PSBT (
fund_transaction). - Combine partial signatures coming back from external signers (
add_verify_transaction_signature). - Finalize and broadcast once the threshold is met.
This guide walks the call sequence for a 2-of-3 multisig wallet with hardware-only signers.
Register customer-managed keys
Section titled “Register customer-managed keys”For each hardware device, fetch its xpub through your client (e.g. via the desktop app talking to the device) and register it as a customer-managed key:
curl -sS -X POST http://127.0.0.1:8080/wallet/create_customer_managed_key \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "user_id": "treasury", "device_id": "ledger-1", "key_name": "ledger-1", "xpub": "xpub6E...", "fingerprint": "6fb270de", "derivation_path": "m/48'\''/0'\''/0'\''/2'\''" }'Repeat for ledger-2, coldcard-1, etc. walletrs only stores the public xpub — no private material is ever sent to the server.
Compose the wallet
Section titled “Compose the wallet”A 2-of-3 multisig:
curl -sS -X POST http://127.0.0.1:8080/wallet/create_generic_wallet \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "user_id": "treasury", "wallet_id": "treasury-2of3", "network": "mainnet", "preferred_script_type": "SCRIPT_TYPE_AUTO", "spending_conditions": [{ "id": "primary", "is_primary": true, "timelock": 0, "threshold": 2, "policy": "MULTI", "managed_key_ids": ["ledger-1", "ledger-2", "coldcard-1"] }] }'This produces a wsh(sortedmulti(2, ...)) descriptor (or tr(...) if you nudge preferred_script_type to taproot).
Reveal an address, fund it, and update_wallet to sync — same as the end-to-end guide up through step 4.
Build the unsigned PSBT
Section titled “Build the unsigned PSBT”curl -sS -X POST http://127.0.0.1:8080/wallet/fund_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "wallet_id":"treasury-2of3", "destination_address":"bc1q...", "destination_value": 100000, "fee_per_kb": 5000 }'The response carries psbt (base64) and pruned_psbt (smaller, no witness_utxo redundancy if your HWW prefers it). Pull the PSBT out and ship it to each device.
Per-device signing (off-server)
Section titled “Per-device signing (off-server)”Each hardware wallet signs a copy of the PSBT independently. The exact mechanism depends on the device — Ledger over USB, Coldcard over SD card, Trezor over USB, etc. Your client (typically the Sigvault desktop app for the integrated flow, or your own tool) coordinates this.
Key point: each device returns a partially-signed PSBT containing its signature plus the same inputs / outputs. walletrs recombines partials below.
Push partials back to walletrs
Section titled “Push partials back to walletrs”For each signed PSBT coming back from a device:
curl -sS -X POST http://127.0.0.1:8080/wallet/add_verify_transaction_signature \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "wallet_id":"treasury-2of3", "txid":"<TXID>", "signedpsbt":"<base64 PSBT from device>", "devicefingerprint":"6fb270de", "devicederivationpath":"m/48'\''/0'\''/0'\''/2'\''" }'walletrs:
- Parses the incoming PSBT.
- Validates that
devicefingerprintmatches a key in the wallet’s descriptor. - Verifies each new partial signature against the corresponding sighash.
- Merges the partials into the stored PSBT.
The response includes a status indicating where in the threshold you are (partial, complete, etc.). Repeat with the next device’s signature.
Finalize and broadcast
Section titled “Finalize and broadcast”Once add_verify_transaction_signature has accumulated the threshold (2-of-3 in this example), finalize and broadcast:
curl -sS -X POST http://127.0.0.1:8080/wallet/finalize_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"treasury-2of3","txid":"<TXID>"}'
curl -sS -X POST http://127.0.0.1:8080/wallet/broadcast_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"treasury-2of3","txid":"<TXID>"}'finalize constructs the final witness from the collected partials; broadcast sends through electrs.
Mixing customer + system keys
Section titled “Mixing customer + system keys”Nothing prevents you from composing a wallet with both customer-managed (HWW) keys and system-managed (in-walletrs) keys in the same condition. For such a wallet:
- Use
sign_wallet_transactionwith the system key’sdevice_idto produce its partial signature in-process. - Use the HWW flow (above) for each customer key’s partial.
walletrs treats both as partials feeding into the same threshold.
Common errors
Section titled “Common errors”| Error | Cause |
|---|---|
INVALID_ARGUMENT: device fingerprint not in wallet descriptor | The devicefingerprint you passed doesn’t match any signer in the wallet’s descriptor. Double-check fingerprint formatting (8 lowercase hex chars). |
INVALID_ARGUMENT: signature did not verify | The partial signature in the PSBT is for a different sighash — typically because the device signed an earlier draft, or the wallet’s UTXO set changed between fund_transaction and the signature coming back. Rebuild and re-sign. |
FAILED_PRECONDITION: not enough signatures to finalize | You called finalize before reaching the threshold. Wait for one more partial. |