The BitcoinApp class provides Bitcoin address retrieval,
message signing (BIP-137), and PSBT (Partially Signed Bitcoin
Transaction) signing.
import {
BitcoinApp,
BTC_MAINNET_PATH,
BTC_TESTNET_PATH,
type BtcMessageSignature,
type InputSigningPath,
} from '@openloop/sdk-core'const btc = new BitcoinApp(transport)| Method | Description |
|---|---|
getAddress(testnet?) |
Retrieves a SegWit address (switches between mainnet and testnet automatically) |
getAddressWithPath(path) |
Retrieves an address at an arbitrary BIP32 path |
getAccountXpub(coinType) |
Retrieves the account-level extended public key |
getMasterFingerprint() |
Retrieves the master key fingerprint (4 bytes hex) |
signMessage(message, path) |
BIP-137 message signing |
signPsbt(psbt) |
PSBT signing (binary in and out) |
signPsbtHex(psbt) |
PSBT signing (hex in and out) |
signPsbtWithPaths(psbt, inputPaths) |
PSBT signing with explicit input paths |
Retrieves a SegWit (bech32) address.
async getAddress(testnet?: boolean): Promise<{
publicKey: string // uncompressed public key (hex)
address: string // bech32 address ("bc1..." or "tb1...")
chainCode: string // chain code (hex, 32 bytes)
}>| Name | Type | Default | Description |
|---|---|---|---|
testnet |
boolean |
false |
true: testnet path (84'/1'/0'/0/0) |
| Network | Path | Constant |
|---|---|---|
| Mainnet | 84'/0'/0'/0/0 |
BTC_MAINNET_PATH |
| Testnet | 84'/1'/0'/0/0 |
BTC_TESTNET_PATH |
// Mainnet
const { address } = await btc.getAddress()
// address: "bc1q..."
// Testnet
const { address } = await btc.getAddress(true)
// address: "tb1q..."Retrieves an address at an arbitrary BIP32 path. Use it for multi-account setups and for change addresses.
async getAddressWithPath(path: string): Promise<{
publicKey: string
address: string
chainCode: string
}>// First address of the second account
const { address } = await btc.getAddressWithPath("84'/0'/1'/0/0")
// Change address
const { address } = await btc.getAddressWithPath("84'/0'/0'/1/0")
// The "m/" prefix is also accepted
const { address } = await btc.getAddressWithPath("m/84'/0'/0'/0/5")Retrieves the account-level extended public key
(m/84'/coin'/0'). Derive child keys from it in software to
check balances and generate addresses.
async getAccountXpub(coinType: number): Promise<{
publicKey: string // compressed public key (hex, 33 bytes)
chainCode: string // chain code (hex, 32 bytes)
}>| Name | Type | Description |
|---|---|---|
coinType |
number |
0: mainnet, 1: testnet |
const { publicKey, chainCode } = await btc.getAccountXpub(0) // mainnetRetrieves the wallet’s BIP32 master key fingerprint
— hash160(master public key)[:4]. It is identical to the
value the device computes internally, and it is the value to place in a
PSBT input’s BIP32 derivation (master_fingerprint).
async getMasterFingerprint(): Promise<string> // 8 lowercase hex characters (big-endian), e.g. "631698e0"In a normal wallet integration you fetch this value once, at account
import time, and embed it in the input derivations of every PSBT you
build afterwards. It matters most on the AirGap / watch-only (QR
PSBT) path: the device decides an input is its own by matching
this fingerprint, so a PSBT input without the correct
master_fingerprint is treated as belonging to another
wallet and cannot be signed — there is no input-path injection there
like signPsbtWithPaths over USB.
Internally it fetches the public key at depth 0 (master
m) and returns the first 4 bytes of itshash160. This is the same processing Sparrow / HWI perform.
const fp = await btc.getMasterFingerprint() // e.g. "631698e0"
// Embed it in the PSBT input's BIP32 derivation (type 0x06):
// value = fingerprint(4B) ‖ path(4B LE × N)Signs a message in BIP-137 format.
async signMessage(
message: string,
path: string
): Promise<BtcMessageSignature>| Name | Type | Description |
|---|---|---|
message |
string |
UTF-8 message |
path |
string |
BIP32 path (e.g. "84'/0'/0'/0/0") |
BtcMessageSignature| Field | Type | Description |
|---|---|---|
v |
number |
Recovery ID (35-38: P2WPKH native SegWit) |
r |
string |
R value (32 bytes hex) |
s |
string |
S value (32 bytes hex) |
signature |
string |
Base64 of the full signature (65 bytes: V || R || S) |
const sig = await btc.signMessage('Hello, Bitcoin!', BTC_MAINNET_PATH)
console.log(sig.signature) // Base64 encoded signatureSigns a PSBT (Partially Signed Bitcoin Transaction). The paths are
detected automatically from the BIP32_DERIVATION fields
inside the PSBT.
async signPsbt(psbt: Uint8Array | string): Promise<Uint8Array>| Name | Type | Description |
|---|---|---|
psbt |
Uint8Array \| string |
PSBT binary data or hex string |
The signed PSBT as binary data (Uint8Array)
PSBT signing with hex strings — both input and output are hex strings.
async signPsbtHex(psbt: string): Promise<string>const signedHex = await btc.signPsbtHex('70736274ff...')Signs a PSBT with the BIP32 path of each input given explicitly. Use
it when the PSBT has no BIP32_DERIVATION fields, or when
you want to sign only specific inputs.
async signPsbtWithPaths(
psbt: string,
inputPaths: InputSigningPath[]
): Promise<string>| Name | Type | Description |
|---|---|---|
psbt |
string |
PSBT hex string |
inputPaths |
InputSigningPath[] |
The path for each input |
InputSigningPath| Field | Type | Description |
|---|---|---|
index |
number |
Input index in the PSBT |
path |
string |
BIP32 path (e.g. "84'/0'/0'/0/5") |
const signedHex = await btc.signPsbtWithPaths('70736274ff...', [
{ index: 0, path: "84'/0'/0'/0/0" },
{ index: 1, path: "84'/0'/0'/0/3" },
{ index: 2, path: "84'/0'/0'/1/0" }, // change
])PSBT signing is a three-phase exchange with the device.
| Chunk | P1 | Data format |
|---|---|---|
| First | 0x00 |
[4B total_size][1B num_paths][paths...][psbt_data...] |
| Continuation | 0x80 |
[psbt_data...] |
| Final | 0xFF |
[remaining_data] → the device starts signing |
num_paths=0: detected automatically from
BIP32_DERIVATION inside the PSBTnum_paths>0: the path for each input is given
explicitly
([1B input_index][1B path_len][4B * path_len elements])The user reviews the transaction details on the device screen and presses the approve button.
| Response | Status word | Meaning |
|---|---|---|
[signed_psbt_chunk] |
0x61XX |
More data follows — request the next chunk |
[signed_psbt_chunk] |
0x9000 |
Final chunk — retrieval complete |
Increment the chunk index from 0 and repeat until 0x9000
comes back.
import * as bitcoin from 'bitcoinjs-lib'
import { BitcoinApp, BTC_MAINNET_PATH } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
const transport = await WebHidTransport.connect()
const btc = new BitcoinApp(transport)
// Get the address
const { address, publicKey } = await btc.getAddress()
// Build the PSBT
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin })
psbt.addInput({
hash: 'txid...',
index: 0,
witnessUtxo: {
script: Buffer.from('0014...', 'hex'),
value: 50000,
},
})
psbt.addOutput({
address: 'bc1q...',
value: 40000,
})
// Sign the PSBT on the device
const psbtHex = psbt.toHex()
const signedHex = await btc.signPsbtWithPaths(psbtHex, [
{ index: 0, path: BTC_MAINNET_PATH },
])
// Load the signed PSBT
const signedPsbt = bitcoin.Psbt.fromHex(signedHex)
signedPsbt.finalizeAllInputs()
// Raw transaction ready to broadcast
const rawTx = signedPsbt.extractTransaction().toHex()