Bitcoin API

The BitcoinApp class provides Bitcoin address retrieval, message signing (BIP-137), and PSBT (Partially Signed Bitcoin Transaction) signing.

Import

import {
  BitcoinApp,
  BTC_MAINNET_PATH,
  BTC_TESTNET_PATH,
  type BtcMessageSignature,
  type InputSigningPath,
} from '@openloop/sdk-core'

Initialization

const btc = new BitcoinApp(transport)

Methods

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

getAddress

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)
}>

Parameters

Name Type Default Description
testnet boolean false true: testnet path (84'/1'/0'/0/0)

Default Paths

Network Path Constant
Mainnet 84'/0'/0'/0/0 BTC_MAINNET_PATH
Testnet 84'/1'/0'/0/0 BTC_TESTNET_PATH

Example

// Mainnet
const { address } = await btc.getAddress()
// address: "bc1q..."

// Testnet
const { address } = await btc.getAddress(true)
// address: "tb1q..."

getAddressWithPath

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
}>

Example

// 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")

getAccountXpub

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)
}>

Parameters

Name Type Description
coinType number 0: mainnet, 1: testnet

Example

const { publicKey, chainCode } = await btc.getAccountXpub(0)  // mainnet

getMasterFingerprint

Retrieves the wallet’s BIP32 master key fingerprinthash160(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"

When to Use It

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 its hash160. This is the same processing Sparrow / HWI perform.

Example

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)

signMessage

Signs a message in BIP-137 format.

async signMessage(
  message: string,
  path: string
): Promise<BtcMessageSignature>

Parameters

Name Type Description
message string UTF-8 message
path string BIP32 path (e.g. "84'/0'/0'/0/0")

Returns: 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)

Example

const sig = await btc.signMessage('Hello, Bitcoin!', BTC_MAINNET_PATH)
console.log(sig.signature) // Base64 encoded signature

signPsbt

Signs 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>

Parameters

Name Type Description
psbt Uint8Array \| string PSBT binary data or hex string

Returns

The signed PSBT as binary data (Uint8Array)


signPsbtHex

PSBT signing with hex strings — both input and output are hex strings.

async signPsbtHex(psbt: string): Promise<string>

Example

const signedHex = await btc.signPsbtHex('70736274ff...')

signPsbtWithPaths

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>

Parameters

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")

Example

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 Protocol Details

PSBT signing is a three-phase exchange with the device.

Diagram 0

Phase 1: Sending the PSBT

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

Phase 2: Signing on the Device

The user reviews the transaction details on the device screen and presses the approve button.

Phase 3: Retrieving the Signed PSBT

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.


bitcoinjs-lib Integration

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()

Next Steps