API Reference

A complete list of type definitions, interfaces, classes, functions, and constants. Unless stated otherwise, everything here is exported from @openloop/sdk-core.

Import from Contents
@openloop/sdk-core App classes, transport interfaces, errors, constants, utilities
@openloop/sdk-core/bitcoin Bitcoin exports (address derivation, UTXO lookup, fee estimation, PSBT construction, broadcast). Does not load viem
@openloop/sdk-core/verify Signature verification (reference implementation). Uses viem / @noble

Contents


Interfaces

ITransport

The interface that abstracts communication with the device. Every transport implements it.

interface ITransport {
  open(): Promise<void>
  close(): Promise<void>
  isConnected(): boolean
  exchange(apdu: Uint8Array): Promise<Uint8Array>
  lock(): Promise<void>
  unlock(): Promise<void>
}
Method Description
open() Open the connection to the device
close() Close the connection
isConnected() Whether a connection is active
exchange(apdu) Send an APDU command and receive the response. On success it returns the data without the status word; on error it throws
lock() Acquire exclusive access to the device (for multi-step operations). Prevents APDUs from interleaving when several apps use the device at once
unlock() Release exclusive access

TransportStatus

interface TransportStatus {
  connected: boolean
  deviceName?: string
  deviceId?: string
}

TransportEvent

type TransportEventType = 'connected' | 'disconnected' | 'error'

interface TransportEvent {
  type: TransportEventType
  error?: Error
}

type TransportEventHandler = (event: TransportEvent) => void

TransportInfo

Transport information returned by OpenloopSDK.discover().

interface TransportInfo {
  type: string        // Registered name (e.g. 'webhid')
  name: string        // Display name (e.g. 'WebHID (USB)')
  available: boolean  // Whether it can be used
}

TransportFactory

type TransportFactory = () => Promise<ITransport>

BtcMessageSignature

Result of a Bitcoin BIP-137 message signature.

interface BtcMessageSignature {
  v: number       // BIP-137 header byte (see the table below)
  r: string       // R value (32 bytes hex)
  s: string       // S value (32 bytes hex)
  signature: string // Full signature (V || R || S, 65 bytes) in Base64
}

v is determined by the purpose (first level) of the path passed to BitcoinApp.signMessage.

purpose Address type v
84' P2WPKH (native SegWit) 39–42
49' P2SH-P2WPKH 35–38
44' P2PKH (compressed key) 31–34
any other — 35–38

InputSigningPath

Per-input BIP32 path for a Bitcoin PSBT.

interface InputSigningPath {
  index: number   // Input index in the PSBT
  path: string    // BIP32 path (e.g. "84'/1'/0'/0/5")
}

XrpSignatureResult

XRP signature result.

interface XrpSignatureResult {
  signature: string  // Signature hex (Ed25519: 64B, secp256k1: variable-length DER)
}

XrpCurve

type XrpCurve = 'ed25519' | 'secp256k1'

SignatureResult

ECDSA signature result for Ethereum / TRON.

interface SignatureResult {
  v: number    // Recovery ID
  r: string    // R value (32 bytes hex, no 0x prefix)
  s: string    // S value (32 bytes hex, no 0x prefix)
}

DeviceInfo

interface DeviceInfo {
  connected: boolean
  path?: string
  vendorId?: number
  productId?: number
}

WcSession

interface WcSession {
  topic: string
}

IWcSignClient

interface IWcSignClient {
  request<T>(params: {
    topic: string
    chainId: string
    request: { method: string; params: unknown }
  }): Promise<T>
}

WcTransportOptions

interface WcTransportOptions {
  client: IWcSignClient
  session: WcSession
  chainId?: string  // Default: "eip155:1"
}

App Classes

Every App class has a constructor(transport: ITransport). Each operation that talks to the device runs between transport.lock() and transport.unlock().

BitcoinApp, EthereumApp, XrpApp, SolanaApp, TronApp, and OpenloopSDK have a static readonly sdkBuildId: string for reading the bundled SDK version (in the form @openloop/sdk-core@<version>; the same value as SDK_BUILD_ID).

BitcoinApp

class BitcoinApp {
  static readonly sdkBuildId: string

  constructor(transport: ITransport)

  getAddress(testnet?: boolean): Promise<{ publicKey: string; address: string; chainCode: string }>

  getAddressWithPath(path: string): Promise<{ publicKey: string; address: string; chainCode: string }>

  getAccountXpub(coinType: number): Promise<{ publicKey: string; chainCode: string }>

  getExtendedPubkey(path: string, display?: boolean): Promise<BtcXpub>

  getMasterFingerprint(): Promise<string>  // hash160(master pubkey)[:4], 8-char hex (e.g. "631698e0")

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

  signPsbt(psbt: Uint8Array | string): Promise<Uint8Array>

  signPsbtHex(psbt: string): Promise<string>

  signPsbtWithPaths(psbt: string, inputPaths: InputSigningPath[]): Promise<string>
}
Method Description
getAddress(testnet = false) Get the P2WPKH (bech32) address at BTC_MAINNET_PATH (BTC_TESTNET_PATH when testnet is true)
getAddressWithPath(path) Get the address at any BIP32 path (a "m/" prefix is allowed). publicKey is the uncompressed key (65 bytes) as hex
getAccountXpub(coinType) The account-level extended public key m/84'/coin'/0' (coinType 0 = mainnet, 1 = testnet). It is fixed to this path; use getExtendedPubkey for 44’ / 49’ or for the second and later accounts
getExtendedPubkey(path, display = false) Get the extended public key for any path (up to 10 levels) and return it as a BtcXpub. Nothing is shown on the device screen (display is only recorded in the firmware log). Firmware 0.63.125 or later returns a key that is correct as BIP-32. For 44’ and 49’ the prefix is also xpub / tpub (not ypub / zpub)
getMasterFingerprint() The first 4 bytes of the hash160 of the master public key, as an 8-character lowercase hex string
signMessage(message, path) BIP-137 message signature. message is a UTF-8 string
signPsbt(psbt) Sign a PSBT (binary in/out). psbt is a Uint8Array or a hex string
signPsbtHex(psbt) Sign a PSBT (hex in/out)
signPsbtWithPaths(psbt, inputPaths) Sign a PSBT with an explicit signing path per input (hex in/out)

Errors

The BtcError thrown by BitcoinApp is the same class as the BtcError in @openloop/sdk-core/bitcoin (0.8.1 and later; recognise it with instanceof BtcError. On 0.8.0 and earlier it is a separate copy, so use name === 'BtcError' and code) (Error Classes).

Functions to use with BitcoinApp (@openloop/sdk-core)

// Build the APDU data for getExtendedPubkey (pure function). path is "m/84'/1'/0'" / "84'/1'/0'" / "m" (root)
function buildExtendedPubkeyRequest(path: string, display?: boolean): Uint8Array

// Parse the getExtendedPubkey response (a base58check string) (pure function).
// Checks that the answer is for requestedPath (depth, last level, version) and throws an Error if not
function parseExtendedPubkeyResponse(xpub: string, requestedPath: string): BtcXpub

const MAX_EXTENDED_PUBKEY_PATH_LEVELS = 10

For the full Bitcoin send flow (address derivation → UTXO lookup → PSBT → signing → broadcast) see The Bitcoin Module.

EthereumApp

type DeviceKind = 'openloop' | 'ledger'

class EthereumApp {
  static readonly sdkBuildId: string

  constructor(transport: ITransport)

  setKind(kind: DeviceKind): void

  getKind(): DeviceKind

  getAddress(path: string): Promise<{ publicKey: string; address: string }>

  getAddressWithChainCode(path: string): Promise<{ publicKey: string; address: string; chainCode: string }>

  signPersonalMessage(path: string, message: string, chainId?: number): Promise<SignatureResult>

  signTransaction(path: string, rawTx: string): Promise<SignatureResult>

  signTypedData(
    path: string, domainSeparatorHash: string, messageHash: string, chainId?: number
  ): Promise<SignatureResult>

  signTypedDataStreaming(path: string, typedData: TypedDataLike, chainId?: number): Promise<SignatureResult>

  signAuthorization(path: string, authorizationRlp: string): Promise<SignatureResult>
}
Method Description
setKind(kind) / getKind() The kind of device on the other end. Default 'openloop'. With 'ledger', signPersonalMessage and signTypedData use the Ledger-compatible form (CLA 0xE0, no chainId). getAddress and signTransaction always use CLA 0xE0
getAddress(path) Get the address. address has the 0x prefix
getAddressWithChainCode(path) Returns the public key and address plus the 32-byte chainCode (hex). Normally pass the change node ("44'/60'/0'/0") and derive the last index in software (BIP-32 Public Key Derivation)
signPersonalMessage(path, message, chainId = 1) EIP-191 personal_sign. A message starting with 0x is treated as hex bytes, anything else as a UTF-8 string
signTransaction(path, rawTx) Sign an RLP-encoded transaction (hex, with or without 0x)
signTypedData(path, domainSeparatorHash, messageHash, chainId = 1) EIP-712. Pass the domain and message hashes (32 bytes each, hex)
signTypedDataStreaming(path, typedData, chainId = 1) Stream the EIP-712 data itself to the device, field by field, and sign it (Openloop only; throws Error with 'ledger'). Nested structs and arrays are rejected. Takes about 25 APDU round trips. Use isEip3009 to check whether a payload is EIP-3009
signAuthorization(path, authorizationRlp) EIP-7702 authorization. authorizationRlp is the hex of RLP([chain_id, address, nonce]) (Openloop only; throws Error with 'ledger')

XrpApp

class XrpApp {
  static readonly sdkBuildId: string

  constructor(transport: ITransport)

  getAddress(path: string, curve?: XrpCurve): Promise<{ publicKey: string; address: string }>

  signTransaction(path: string, txBlob: string, curve?: XrpCurve): Promise<XrpSignatureResult>
}

curve defaults to 'ed25519'. In signTransaction, txBlob is the serialized transaction to sign (hex, with or without 0x); the signature is 64 bytes for Ed25519 and DER (variable length) for secp256k1, as hex.

SolanaApp

class SolanaApp {
  static readonly sdkBuildId: string

  constructor(transport: ITransport)

  getAddress(path: string): Promise<{ publicKey: string; address: string }>

  signTransaction(path: string, txBytes: Uint8Array): Promise<string>

  signOffchainMessage(path: string, messageBytes: Uint8Array): Promise<string>
}

signTransaction and signOffchainMessage return a 64-byte Ed25519 signature as a hex string (128 characters). signOffchainMessage wraps messageBytes in the Solana off-chain message format before signing (it does not wrap twice if the message is already wrapped; see Solana Off-Chain Messages).

TronApp

class TronApp {
  static readonly sdkBuildId: string

  constructor(transport: ITransport)

  getAddress(path: string): Promise<{ publicKey: string; address: string }>

  getAddressWithChainCode(path: string): Promise<{ publicKey: string; address: string; chainCode: string }>

  signTransaction(path: string, rawDataHex: string): Promise<SignatureResult>

  signMessage(path: string, message: string): Promise<SignatureResult>
}

getAddressWithChainCode has the same form as in EthereumApp (normally pass "44'/195'/0'/0"). In signTransaction, rawDataHex is the transaction’s raw_data (hex, with or without 0x); in signMessage, message is a UTF-8 string.

OpenloopApp

A class for Openloop-firmware-specific utility commands. It provides the proprietary commands exposed under CLA=0xF0 (CLA_OPENLOOP).

class OpenloopApp {
  constructor(transport: ITransport)

  /**
   * Sends GET_DEVICE_INFO (F0 A0) and returns identification + state + feature info.
   *
   * Return value:
   *   - Returns null for old Openloop firmware (before v0.91.13) and for
   *     devices that do not support the command
   *   - Also returns null when sending/receiving the APDU fails (including a
   *     device rejection or a communication error)
   *   - Also returns null when the response is shorter than 16 bytes or does
   *     not start with "OL"
   *   - Throws if acquiring the lock fails
   */
  getDeviceInfo(): Promise<OpenloopDeviceInfo | null>
}

OpenloopDeviceInfo

interface OpenloopDeviceInfo {
  modelId: number              // Hardware SKU ID (e.g. 0x0001 = Openloop V1)
  hwRevision: number           // PCB revision
  mainFwVersion: { major: number; minor: number; patch: number }
  recoveryFwVersion: { major: number; minor: number; patch: number }
  state: number                // 16-bit state bitmask (raw value)
  stateBits: OpenloopDeviceState  // Decoded view of state
  features: number             // 16-bit feature bitmask (implementation detail, subject to change)
}
Field Type Description
modelId number Hardware SKU ID. Increases with every new SKU
hwRevision number PCB revision number
mainFwVersion {major,minor,patch} Version of the main firmware currently running
recoveryFwVersion {major,minor,patch} Version of the recovery (factory) partition. All zeros if unavailable
state number 16-bit bitmask describing the current state (raw value)
stateBits OpenloopDeviceState state decoded into an object
features number 16-bit bitmask of hardware features (raw value). The bit layout is a firmware implementation detail and may change between versions. Host apps should only store or forward the raw value

OpenloopDeviceState

A human-readable object decoded from the state field.

interface OpenloopDeviceState {
  walletProvisioned: boolean
  language: number            // 0=English, 1=Japanese, 2-7=reserved
  usbEnabled: boolean
  usbVidCompat: boolean       // false=Native (0x303A), true=Compatible (0x2C97)
  bleEnabled: boolean
  blePaired: boolean
  fidoEnabled: boolean
  pivEnabled: boolean
}

State bitmask layout (fixed from FW v0.91.14):

bit Name Meaning
0 WALLET_PROVISIONED Wallet has been created
1-3 LANG (3 bit) Language index (0=EN, 1=JA, …)
4 USB_ENABLED USB communication ON
5 USB_VID_COMPAT USB VID = Compatible mode (0x2C97). Native (0x303A) when 0
6 BLE_ENABLED BLE communication ON
7 BLE_PAIRED At least one BLE-paired device exists
8 FIDO_ENABLED FIDO/Passkey feature ON
9 PIV_ENABLED PIV / PKCS#11 feature ON
10-15 reserved Reserved for future use, currently 0

Bits may be added in the future. To read a bit that is not in stateBits, mask the raw state value directly.

Example

import { OpenloopApp, formatOpenloopState } from '@openloop/sdk-core'

const app = new OpenloopApp(transport)
const info = await app.getDeviceInfo()

if (info === null) {
  console.log('GET_DEVICE_INFO not supported (old firmware)')
} else {
  console.log(`Model: 0x${info.modelId.toString(16).padStart(4, '0')}`)
  console.log(`FW: ${info.mainFwVersion.major}.${info.mainFwVersion.minor}.${info.mainFwVersion.patch}`)
  console.log(`State: ${formatOpenloopState(info.stateBits)}`)
  if (!info.stateBits.walletProvisioned) {
    // Branch for when no wallet has been created
  }
}

The Bitcoin Module

The @openloop/sdk-core/bitcoin subpath. It provides what a Bitcoin send needs, outside BitcoinApp (which talks to the device, in @openloop/sdk-core), as functions that include the network lookups. It supports three address types: Legacy (P2PKH), Nested SegWit (P2SH-P2WPKH), and Native SegWit (P2WPKH), and does not load viem.

import {
  accountPathOf, deriveAddresses, scanAccount, getFeeRate, selectUtxos,
  fetchPrevTx, buildPsbt, extractRawTx, broadcastTx, BtcError,
  type BtcNetwork, type BtcAccount,
} from '@openloop/sdk-core/bitcoin'

Design principles

Endpoint requests (common)

Bitcoin Types

type BtcNetworkId = 'mainnet' | 'testnet3' | 'testnet4'

interface BtcNetwork {
  id: BtcNetworkId
  esplora: string[]       // Esplora-compatible endpoints, tried in order (best first)
  feeEndpoints: string[]  // Endpoints that serve `/v1/fees/recommended` (mempool.space compatible)
  timeoutMs: number       // Time limit for one call (ms). The SDK has no default
}

type BtcScript = 'p2pkh' | 'p2sh-p2wpkh' | 'p2wpkh'   // purpose 44' / 49' / 84'

interface BtcAccount {
  script: BtcScript
  account: number         // Account number (below the hardened boundary 0x80000000)
}

interface BtcXpub {       // Extended public key received from the device (the return value of BitcoinApp.getExtendedPubkey)
  publicKey: string       // 33-byte compressed public key (hex)
  chainCode: string       // 32 bytes (hex)
  parentFingerprint: number  // Fingerprint of the immediate parent (not the master)
  depth: number           // Number of levels from m
  xpub: string            // The serialized string (xpub… / tpub…)
}

interface BtcUtxo {
  txid: string
  vout: number
  value: number           // sats
  address: string
  path: string            // In the form "m/84'/1'/0'/0/3"
  publicKey: string       // 33-byte compressed public key (hex)
  confirmed: boolean      // false below 1 confirmation
  blockHeight?: number
}

interface BtcAddressCache {
  get(address: string): { path: string; publicKey: string } | undefined
  set(address: string, entry: { path: string; publicKey: string }): void
  syncToDevice(deviceId: string | null): void
  readonly size: number
  clear(): void
}

type BtcAddressKind = 'p2pkh' | 'p2sh' | 'p2wpkh' | 'p2wsh' | 'p2tr'

interface BtcRecipient { address: string; value: number }   // value is in sats

interface BtcSelection {
  inputs: BtcUtxo[]
  fee: number             // Fee actually paid (sats) = total inputs − total outputs
  change: number          // Change (sats). 0 means no change output is created
  vbytes: number          // Estimated size (vB)
}

interface BtcScanResult {
  used: Array<{ address: string; path: string }>   // Addresses that have been used (both the receive and change chains)
  utxos: BtcUtxo[]
  nextChange: { index: number; address: string; path: string; publicKey: string }  // The next unused address to use for change
}

interface BtcPsbtInput extends BtcUtxo {
  prevTxHex?: string      // Required only for P2PKH: the raw hex of the previous transaction
}

interface BtcChangeOutput {
  address: string
  path: string
  publicKey: string
  value: number
}

interface BtcPsbt {
  psbtHex: string
  paths: Array<{ index: number; path: string }>   // The form to pass to BitcoinApp.signPsbtWithPaths
}

interface FetchDeps {     // Lookup settings that tests can replace
  fetchImpl?: typeof fetch
  retryDelayMs?: number   // Default 1000
  retryCount?: number     // Default 3 (attempts per URL)
}

Address types and paths correspond as follows.

BtcScript purpose Address PSBT input fields
'p2pkh' 44' Legacy (1… / m… n…) NON_WITNESS_UTXO + BIP32_DERIVATION
'p2sh-p2wpkh' 49' Nested SegWit (3… / 2…) WITNESS_UTXO + REDEEM_SCRIPT + BIP32_DERIVATION
'p2wpkh' 84' Native SegWit (bc1q… / tb1q…) WITNESS_UTXO + BIP32_DERIVATION

'testnet3' and 'testnet4' share the same coin type and address format (1, tb1…); only the endpoints differ.

Function Overview

Function Purpose
pathOf Build a path from the account, chain, and index
deriveAddresses Derive consecutive addresses from an extended public key
validateAddress Validate a recipient address
scanAccount Scan an account for used addresses, UTXOs, and the next change address
addressStats Ask whether one address has been used (transaction count, total received, total spent)
fetchUtxos Fetch the UTXOs of given addresses
fetchPrevTx Fetch the raw hex of a previous transaction (needed for P2PKH inputs)
getFeeRate Fetch the fee rate (sat/vB)
selectUtxos Choose UTXOs and compute the fee and change
buildPsbt Build a PSBT
extractRawTx Extract the raw transaction and txid from a signed PSBT
broadcastTx Broadcast a raw transaction
createAddressCache Create an address → path/public key cache
findAddressPath Look up the path for an address

The helper functions (accountPathOf, publicKeyToAddress, estimateVbytes, addressToScriptPubKey, encodeSegwitAddress, decodeSegwitAddress) and the constants follow in later sections.

pathOf / accountPathOf

function pathOf(acct: BtcAccount, net: BtcNetworkId, change: 0 | 1, index: number): string

function accountPathOf(acct: BtcAccount, net: BtcNetworkId): string

pathOf returns m/{purpose}'/{coin}'/{account}'/{change}/{index} (e.g. "m/84'/1'/0'/1/3"). The purpose comes from acct.script (44 / 49 / 84) and the coin type from the network (0 for mainnet, 1 for testnet3 and testnet4). accountPathOf returns the three levels up to the account ("m/84'/1'/0'"), which is the form to pass to BitcoinApp.getExtendedPubkey.

Errors: bad_argument for an unknown script or network, a negative / fractional / NaN / 0x80000000-or-larger account or index, or a change other than 0 / 1.

deriveAddresses

function deriveAddresses(
  xpub: BtcXpub,
  acct: BtcAccount,
  net: BtcNetworkId,
  change: 0 | 1,
  from: number,
  count: number
): Array<{ address: string; path: string; publicKey: string }>

From an account-level extended public key (depth 3), derives count addresses of the change chain starting at index from (it does not talk to the device).

Errors: bad_argument when from / count is not a non-negative integer, or when xpub.depth is not 3 (so it never silently builds addresses for a different account). An unknown network or script is also bad_argument. If the contents of the public key or chain code are invalid, a plain Error is thrown.

validateAddress

function validateAddress(address: string, net: BtcNetworkId): { kind: BtcAddressKind }

Validates an address usable as a recipient and returns its type (kind). It reads five types: bech32 (P2WPKH, P2WSH), bech32m (P2TR), and base58check (P2PKH, P2SH). It verifies the checksum, and bech32 accepts the all-uppercase form (mixed case is invalid).

Errors

code Condition
invalid_address Cannot be read as an address: empty, checksum mismatch, bad characters or length, witness version 2 or higher, etc.
network_mismatch A valid address, but not for the net you passed (for example a mainnet address on a test network). The caller can offer to “switch networks”
bad_argument Unknown network

scanAccount

function scanAccount(
  xpub: BtcXpub,
  acct: BtcAccount,
  net: BtcNetwork,
  opts?: {
    cache?: BtcAddressCache
    gapLimit?: number       // Default 20 (an integer ≥ 1)
    concurrency?: number    // Default 4 (an integer ≥ 1)
    knownUsed?: Array<{ chain: 0 | 1; index: number }>  // Indexes the caller already knows were used
  } & FetchDeps
): Promise<BtcScanResult>

Scans the receive chain (change = 0) and the change chain (change = 1) until gapLimit consecutive addresses have no transactions. UTXOs are fetched only for addresses that have a balance. used is the used addresses of both chains, utxos are the UTXOs of addresses with a balance (including unconfirmed ones; tell them apart with confirmed), and nextChange is the first address of the change chain with no transactions (so that every send does not reuse the same change address). If you pass opts.cache, the path and public key of every derived address are remembered. opts.knownUsed is where the caller passes indexes it already knows were used (they are counted as used even if the endpoint says unused, and nextChange skips them; the test networks Testnet3 / 4 share keys, so you can pass indexes used on the other network; the endpoint is still queried, so balances remain its answer; omit it and nothing changes).

Errors: bad_argument for an invalid gapLimit / concurrency. If even one address could not be queried at any endpoint, fetch_failed (the scan does not continue as if it were empty).

addressStats

function addressStats(
  address: string,
  net: BtcNetwork,
  deps?: FetchDeps
): Promise<BtcAddressStats>

interface BtcAddressStats {
  txCount: number   // Number of transactions (confirmed + unconfirmed). Greater than 0 means "used"
  funded: number    // Total received (sats, confirmed + unconfirmed)
  spent: number     // Total spent (sats, confirmed + unconfirmed). Balance is funded - spent
}

Asks the endpoint (Esplora address) whether one address has been used. It is for callers that have only the addresses a wallet returned (for example over WalletConnect) and no extended public key, so scanAccount is not available, and that want to pick a change address with no transactions.

fetchUtxos

function fetchUtxos(
  addresses: Array<{ address: string; path: string; publicKey: string }>,
  net: BtcNetwork,
  deps?: FetchDeps
): Promise<BtcUtxo[]>

Fetches all UTXOs of the given addresses (up to API_PARALLEL_LIMIT = 4 at a time). confirmed is true only for outputs the endpoint reports as confirmed.

Errors: bad_argument when addresses is not an array; fetch_failed when even one address failed at every endpoint or an answer has the wrong shape.

fetchPrevTx

function fetchPrevTx(txid: string, net: BtcNetwork, deps?: FetchDeps): Promise<string>

Fetches the raw hex (lowercase) of the transaction txid. It checks that the fetched transaction really is the one with that txid (P2PKH reads the amount from the whole previous transaction, so this prevents being handed a different transaction and signing a false amount).

Errors: bad_argument when txid is not 64 hex digits; fetch_failed when all endpoints fail; bad_response when the hash does not match txid; prev_tx_too_large above 4,096 bytes.

getFeeRate

function getFeeRate(net: BtcNetwork, deps?: FetchDeps): Promise<number>

Returns halfHourFee (sat/vB) from /v1/fees/recommended on net.feeEndpoints. The value is checked against MIN_FEE_RATE_SAT_VB–MAX_FEE_RATE_SAT_VB; out-of-range values are not rounded but rejected.

Errors: fetch_failed when all endpoints fail or halfHourFee is not a number; fee_rate_out_of_range when out of range.

selectUtxos

function selectUtxos(
  utxos: BtcUtxo[],
  opts: {
    to: BtcRecipient[]
    feeRate: number
    script: BtcScript
    net: BtcNetworkId
    excluded?: Iterable<string>   // Spent markers "txid:vout"
    changeAddress?: string        // If omitted, change is counted as the same type as the inputs
  }
): BtcSelection

Adds UTXOs largest first and computes the fee and change (equal amounts are ordered by txid then vout, so the same inputs always give the same result).

Errors

code Condition
bad_argument utxos is not an array, to is empty, or an amount is not a positive integer
fee_rate_out_of_range feeRate is outside MIN_FEE_RATE_SAT_VB–MAX_FEE_RATE_SAT_VB
invalid_address / network_mismatch An address in to or changeAddress is invalid or belongs to another network
insufficient The balance is insufficient even counting unconfirmed UTXOs
insufficient_unconfirmed The balance would be sufficient if unconfirmed UTXOs were counted (waiting makes the send possible)
fee_too_high / fee_exceeds_amount The fee guards above

buildPsbt

function buildPsbt(args: {
  script: BtcScript
  net: BtcNetworkId
  inputs: BtcPsbtInput[]
  to: Array<{ address: string; value: number }>
  change?: BtcChangeOutput    // Omit when no change output is created
}): BtcPsbt

Returns a PSBT v0 as hex (transaction version 2, locktime 0, and sequence 0xfffffffd = RBF on every input). paths can be passed straight to BitcoinApp.signPsbtWithPaths(psbtHex, paths).

Errors: bad_argument for no inputs or recipients, an amount that is not a positive integer, an invalid txid, an unknown script, a missing or mismatching prevTxHex for P2PKH, or an invalid public key or path. prev_tx_too_large when prevTxHex exceeds 4,096 bytes. invalid_address / network_mismatch for an invalid recipient or change address.

extractRawTx

function extractRawTx(signedPsbtHex: string): { rawTxHex: string; txid: string }

From a signed PSBT in which every input is finalized (FINAL_SCRIPTSIG / FINAL_SCRIPTWITNESS), extracts the raw transaction ready to broadcast and its txid (the reversed double SHA-256 of the transaction without witness data). The marker and flag are written when at least one input has a witness (BIP-144), and not for a Legacy-only transaction.

Errors: bad_psbt when it cannot be read as a PSBT, is truncated, or an input has neither FINAL_SCRIPTSIG nor FINAL_SCRIPTWITNESS. bad_argument when it is not valid hex.

broadcastTx

function broadcastTx(
  rawTxHex: string,
  net: BtcNetwork,
  expectedTxid?: string,
  deps?: FetchDeps
): Promise<string>

Broadcasts the raw transaction with POST /tx on net.esplora and returns the txid (lowercase). A response whose body is not 64 hex digits (a txid) is not accepted as that endpoint’s answer (so an HTML page returned by a captive portal is never treated as a txid).

Pass expectedTxid (the txid returned by extractRawTx).

Errors: bad_argument when rawTxHex is not hex. When every endpoint fails (unreachable, HTTP error, a non-txid response, or a rejection) the code is broadcast_rejected (not fetch_failed; the message carries the last endpoint’s reason).

createAddressCache

function createAddressCache(): BtcAddressCache

Creates an address → { path, publicKey } cache. It is not one per module: the caller creates and holds as many as needed (for example per network, type, or account).

Member Description
get(address) / set(address, entry) Look up / add an entry
syncToDevice(deviceId) Declare the owning device. Everything is dropped when the owner changes (so addresses of another device are never treated as your own). The same deviceId (other than null) does nothing. null means the owner is unknown and drops everything on every call
size Number of remembered entries
clear() Drop everything

findAddressPath

function findAddressPath(
  address: string,
  xpub: BtcXpub,
  acct: BtcAccount,
  net: BtcNetworkId,
  cache?: BtcAddressCache,
  opts?: { maxIndex?: number; step?: number }   // Defaults 200 / 20
): string | null

Looks up the path of address. It checks cache first; otherwise it derives step addresses at a time, receive chain then change chain, below maxIndex, and searches them. Derived addresses are remembered in cache. If nothing is found it returns null (it does not throw, since an address that is not yours is not an error).

Errors: bad_argument when maxIndex / step is not an integer ≥ 1 or address is empty.

Helper Functions

// Build the address of the given type from a compressed public key (hex, with or without 0x)
function publicKeyToAddress(publicKey: string, script: BtcScript, net: BtcNetworkId): string

// Estimate the transaction size (vB). outputVbytes is an array of OUTPUT_VBYTES values
function estimateVbytes(numInputs: number, script: BtcScript, outputVbytes: number[]): number

// Convert a recipient address to a scriptPubKey (with the same checks as validateAddress)
function addressToScriptPubKey(address: string, net: BtcNetworkId): Uint8Array

// Build and parse witness addresses (bech32 / bech32m)
function encodeSegwitAddress(hrp: string, version: number, program: Uint8Array): string
function decodeSegwitAddress(address: string): { hrp: string; version: number; program: Uint8Array }

@openloop/sdk-core/bitcoin also re-exports the same BIP-32 functions and types as @openloop/sdk-core (deriveChildPublicKey, decodeExtendedPubkey, base58CheckDecode, base58CheckEncode, BIP32_VERSION_XPUB, BIP32_VERSION_TPUB, ExtendedPublicKey) (BIP-32 Public Key Derivation).

Bitcoin Constants

// Fee rate (sat/vB)
const MIN_FEE_RATE_SAT_VB = 1
const MAX_FEE_RATE_SAT_VB = 5000

// Fee guards
const MAX_ABSOLUTE_FEE_SATS = 10_000_000        // 0.1 BTC
const MAX_FEE_FRACTION = 1.0                    // Upper limit of fee / send amount (100%)
const FEE_FRACTION_MIN_AMOUNT_SATS = 1_000_000  // The ratio guard is not applied to sends below this amount

// Construction
const DUST_SATS = 546                           // Change below this is not made into an output
const MAX_PREV_TX_BYTES = 4096                  // Upper limit of a previous transaction's size (the device limit)
const RBF_SEQUENCE = 0xfffffffd                 // sequence of every input

// Reading back a signed PSBT
const PSBT_RESPONSE_CHUNK_SIZE = 250            // Bytes per chunk
const PSBT_MAX_RESPONSE_CHUNKS = 256            // Number of chunks (the index is one byte, APDU P1)
const MAX_SIGNED_PSBT_BYTES = 64000             // 250 × 256

// Size estimates (vB)
const INPUT_VBYTES = { 'p2pkh': 148, 'p2sh-p2wpkh': 91, 'p2wpkh': 68 }
const OUTPUT_VBYTES = { p2wpkh: 31, p2tr: 43, p2wsh: 43, p2pkh: 34, p2sh: 32 }
const TX_OVERHEAD_VBYTES = 10.5

// Paths
const PURPOSE_OF_SCRIPT = { 'p2pkh': 44, 'p2sh-p2wpkh': 49, 'p2wpkh': 84 }
const COIN_TYPE_OF_NETWORK = { mainnet: 0, testnet3: 1, testnet4: 1 }

// Endpoint requests (the defaults of FetchDeps)
const API_RETRY_COUNT = 3            // Attempts per URL
const API_RETRY_DELAY_MS = 1000      // Interval between attempts (ms)
const API_PARALLEL_LIMIT = 4         // Number of lookups run at the same time

BtcError

type BtcErrorCode =
  | 'bad_argument'              // Invalid argument (path, index, network/type combination, length). A caller mistake
  | 'invalid_address'           // Cannot be read as a recipient address (checksum, characters, length)
  | 'network_mismatch'          // Readable, but not an address of the current network
  | 'fetch_failed'              // Could not query the endpoints (all endpoints failed)
  | 'bad_response'              // An endpoint's answer is invalid (hash mismatch, wrong txid, etc.)
  | 'prev_tx_too_large'         // The previous transaction exceeds the device limit (4,096 B)
  | 'fee_rate_out_of_range'     // Fee rate out of range
  | 'insufficient'              // Insufficient balance
  | 'insufficient_unconfirmed'  // Sufficient if unconfirmed UTXOs are counted
  | 'fee_too_high'              // The fee exceeds the absolute cap
  | 'fee_exceeds_amount'        // The fee exceeds the send amount
  | 'psbt_too_large'            // The signed PSBT exceeds what can be read back (64,000 B)
  | 'bad_psbt'                  // Cannot be read as a PSBT
  | 'broadcast_rejected'        // The broadcast was not accepted

class BtcError extends OpenloopError {
  static readonly sdkBuildId: string
  name: string  // 'BtcError'
  readonly code: BtcErrorCode
  constructor(code: BtcErrorCode, message: string)
}

message is always English. The caller prepares the text shown on screen, keyed on code.

Example

The flow of sending from a P2WPKH account on Testnet3. transport is an ITransport already connected to the device.

import { BitcoinApp } from '@openloop/sdk-core'
import {
  accountPathOf, scanAccount, getFeeRate, selectUtxos, buildPsbt,
  extractRawTx, broadcastTx, createAddressCache,
  type BtcNetwork, type BtcAccount,
} from '@openloop/sdk-core/bitcoin'

const net: BtcNetwork = {
  id: 'testnet3',
  esplora: ['https://blockstream.info/testnet/api', 'https://mempool.space/testnet/api'],
  feeEndpoints: ['https://mempool.space/testnet/api'],
  timeoutMs: 15_000,
}
const acct: BtcAccount = { script: 'p2wpkh', account: 0 }
const cache = createAddressCache()

const btc = new BitcoinApp(transport)

// 1. Get the account's extended public key ("m/84'/1'/0'") and tell the cache which device owns it
const xpub = await btc.getExtendedPubkey(accountPathOf(acct, net.id))
cache.syncToDevice(await btc.getMasterFingerprint())

// 2. Scan to get the UTXOs and the next change address
const { utxos, nextChange } = await scanAccount(xpub, acct, net, { cache })

// 3. Fetch the fee rate and choose the UTXOs to spend
const feeRate = await getFeeRate(net)
const to = [{ address: 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx', value: 50_000 }]
const sel = selectUtxos(utxos, { to, feeRate, script: acct.script, net: net.id, changeAddress: nextChange.address })

// 4. Build the PSBT, sign it on the device, extract the transaction, and broadcast it
const { psbtHex, paths } = buildPsbt({
  script: acct.script,
  net: net.id,
  inputs: sel.inputs,
  to,
  change: sel.change > 0 ? { ...nextChange, value: sel.change } : undefined,
})
const signedPsbtHex = await btc.signPsbtWithPaths(psbtHex, paths)
const { rawTxHex, txid } = extractRawTx(signedPsbtHex)
await broadcastTx(rawTxHex, net, txid)

For a P2PKH (Legacy) account, attach the previous transaction to each input before buildPsbt.

import { fetchPrevTx, type BtcPsbtInput } from '@openloop/sdk-core/bitcoin'

const inputs: BtcPsbtInput[] = await Promise.all(
  sel.inputs.map(async (u) => ({ ...u, prevTxHex: await fetchPrevTx(u.txid, net) }))
)

Branch on errors by code.

import { BtcError } from '@openloop/sdk-core/bitcoin'

try {
  // …the flow above…
} catch (e) {
  if (e instanceof BtcError) {
    const code = e.code
    if (code === 'insufficient_unconfirmed') {
      // Tell the user: "You can send once the incoming funds confirm"
    }
  }
}

Signature Verification

The @openloop/sdk-core/verify subpath. Utilities that verify signatures returned by the device with a reference implementation (viem / @noble). Transport-independent (works with USB / BLE / AirGap / WalletConnect alike). It is not included in @openloop/sdk-core or @openloop/sdk-core/bitcoin (so they never load viem) and is provided as a subpath.

import { verifyPersonalSign } from '@openloop/sdk-core/verify'

const result = await verifyPersonalSign(message, sig, address)
// result: { recovered: string; expected?: string; ok: boolean }
Function Chain What it verifies
verifyBtcMessage(message, sig, identity) BTC BIP-137 message (sig is a BtcMessageSignature; identity = pubkey hex or a BTC address)
verifyBtcPsbt(signedPsbtHex) BTC The signature of a signed PSBT (three types: Native P2WPKH, Nested P2SH-P2WPKH, Legacy P2PKH; the pubkey is inside the PSBT)
verifyPersonalSign(message, sig, expected?) EVM EIP-191 personal_sign
verifyTypedDataJson(typedDataJson, sig, expected?) EVM EIP-712 (full JSON)
verifyTypedDataHashes(domainHash, msgHash, sig, expected?) EVM EIP-712 (pre-hashed)
verifyTransaction(unsignedTxHex, sig, expected?) EVM Legacy / EIP-2718 typed tx (takes {v,r,s})
verifySignedTransaction(signedTxHex, expected?) EVM Recover the signer from a signed serialized tx (the return value of WalletConnect eth_signTransaction)
verifyAuth7702(authRlpHex, sig, expected?) EVM EIP-7702 authorization
verifyXrpTransaction(txBlobHex, curve, result, pubkeyHex) XRP ed25519 / secp256k1 (for-signing blob + pubkey given)
verifyXrpSignedTx(signedTxHex) XRP Self-contained verification from a signed tx blob (extracts SigningPubKey/TxnSignature; no pubkey needed)
verifySolMessage(message, sigHex, pubkeyHex) SOL ed25519 message (verifies the UTF-8 bytes of message as they are, without the off-chain message wrapper)
verifySolTransaction(txHex, sigHex, pubkeyHex) SOL ed25519 transaction
verifyTronMessage(message, sig, identity) TRON keccak256 TRON prefix (identity = pubkey hex or a TRON address)
verifyTronTransaction(rawDataHex, sig, identity) TRON sha256(raw_data) (identity = pubkey hex or a TRON address)

sig is a SignatureResult for EVM and TRON and a BtcMessageSignature for BTC messages (XrpSignatureResult is the result of verifyXrpTransaction). curve is an XrpCurve.

// Return value (of every function)
interface VerifyResult {
  recovered: string   // The recovered signer (EVM: the address). For ed25519 verification, a string describing the result
  expected?: string
  ok: boolean
}

About identity (pubkey or address): BTC / TRON verification decides from the form of identity whether to compare the recovered public key directly, or to derive the chain address from the recovered key and compare that. On paths such as WalletConnect where the public key is not available, you can match by address. verifySignedTransaction (EVM) and verifyXrpSignedTx (XRP) recover the signer/public key from the signed data itself, so no external pubkey is needed.


Error Classes

class OpenloopError extends Error {
  name: string  // 'OpenloopError'
}

class ApduError extends OpenloopError {
  name: string  // 'ApduError'
  readonly statusWord: number
  constructor(statusWord: number)
}

type TransportErrorCode =
  | 'not-supported'   // The runtime cannot talk to the device
  | 'not-found'       // No matching device / the selection was cancelled
  | 'not-connected'   // No connection is open
  | 'timeout'         // The response did not arrive in time
  | 'disconnected'    // Disconnected while in use
  | 'io'              // Anything else

class TransportError extends OpenloopError {
  name: string  // 'TransportError'
  readonly code: TransportErrorCode        // Added in v0.6.0
  stale?: boolean
  busy?: boolean
  noResponse?: boolean
  sent?: number
  awaited?: boolean
  constructor(message: string, code?: TransportErrorCode)  // Defaults to 'io'
}

The ApduError message has the form APDU error: 0x6985 (User rejected on device); branch on statusWord.

statusWord Description (in the message)
0x6700 Wrong length (Lc/Le)
0x6982 Security status not satisfied
0x6983 Authentication blocked (PIN locked)
0x6984 Referenced data invalid
0x6985 User rejected on device
0x6986 Command not allowed
0x6a80 Invalid data
0x6a82 App not found
0x6a84 Not enough memory
0x6a86 Incorrect P1/P2
0x6a88 Referenced data not found
0x6b00 Wrong P1/P2
0x6c00 Wrong Le
0x6d00 Instruction not supported
0x6e00 CLA not supported
0x6f00 Internal error
anything else Unknown error

stale / busy / noResponse / sent / awaited on TransportError are optional fields set by connections through the Safari extension (@openloop/transport-safari). A missing field does not mean “safe”.

Field Meaning
stale The extension was replaced and the page’s bridge is no longer valid. Reconnecting does not fix it; Safari must be opened again
busy The queue for touching the device was busy and the request was refused without touching the device. Safe to retry
noResponse The bridge could not return an answer. It is not known whether the device acted, so do not resend automatically
sent Number of frames sent in full. 0 means the device received not a single byte
awaited Failed while waiting for the response after all frames were sent. If true, the request may have been signed

About instanceof: @openloop/sdk-core, @openloop/sdk-core/bitcoin, and @openloop/sdk-core/verify share the definitions of the error classes (0.8.1 and later). The BtcError of /bitcoin, and the BtcError thrown by BitcoinApp, both match instanceof for the BtcError of /bitcoin and for the OpenloopError of @openloop/sdk-core. Branch on code / statusWord (on 0.8.0 and earlier they are separate copies and do not match, so tell them apart with name: 'BtcError', 'TransportError', 'ApduError').


OpenloopSDK Class

A class that manages transport registration, discovery, and connection. Every method is static.

class OpenloopSDK {
  static readonly sdkBuildId: string

  static registerTransport(type: string, config: {
    factory: TransportFactory
    name: string
    isAvailable: () => boolean | Promise<boolean>
  }): void

  static discover(): Promise<TransportInfo[]>

  static connect(opts?: { transport?: string }): Promise<ITransport>

  static getRegisteredTransports(): string[]

  static clearTransports(): void
}
Method Description
registerTransport(type, config) Register a transport under type (e.g. 'webhid'). The same type is overwritten
discover() Check every registered transport in registration order and return the result of isAvailable() (available: false if isAvailable() throws)
connect(opts?) With opts.transport, runs that transport’s factory() → open() and returns it. If it is not registered, throws TransportError (code: 'not-found'); a failure in factory() / open() is thrown as is. Without it, tries the available transports in registration order and moves on to the next when one fails. If all fail, throws TransportError (code: 'not-found')
getRegisteredTransports() The array of registered types (in registration order)
clearTransports() Remove all registrations

WcTransport Class

An ITransport implementation that relays device operations to a remote Openloop Connect over the WalletConnect v2 relay. It calls the custom methods openloop_exchange (takes the APDU as hex and receives { data: hex }), openloop_lock, and openloop_unlock through client.request.

class WcTransport implements ITransport {
  constructor(options: WcTransportOptions)
  open(): Promise<void>
  close(): Promise<void>
  isConnected(): boolean
  lock(): Promise<void>
  unlock(): Promise<void>
  exchange(apdu: Uint8Array): Promise<Uint8Array>
}

open() only sets the connected flag and does not communicate. Calling lock() / unlock() / exchange() before open() (or after close()) throws a TransportError with code: 'not-connected'.


Constants

USB Identifiers

const OPENLOOP_VENDOR_ID  = 0x303a   // Espressif VID
const OPENLOOP_PRODUCT_ID = 0x8341   // Openloop PID
const LEDGER_VENDOR_ID    = 0x2c97   // Ledger SAS VID
const LEDGER_PRODUCT_ID   = 0x1011   // Nano S compatible
const LEDGER_USAGE_PAGE   = 0xffa0   // Ledger HID usage page (APDU transport)

BIP44 Default Paths

const BTC_MAINNET_PATH  = "84'/0'/0'/0/0"
const BTC_TESTNET_PATH  = "84'/1'/0'/0/0"
const DEFAULT_ETH_PATH  = "44'/60'/0'/0/0"
const DEFAULT_XRP_PATH  = "44'/144'/0'/0'/0'"
const DEFAULT_SOL_PATH  = "44'/501'/0'/0'"
const DEFAULT_TRON_PATH = "44'/195'/0'/0/0"

BLE Constants

const BLE_SERVICE_UUID      = '13d63400-2c97-0004-0000-4c6564676572'
const BLE_NOTIFY_UUID       = '13d63400-2c97-0004-0001-4c6564676572'
const BLE_WRITE_UUID        = '13d63400-2c97-0004-0002-4c6564676572'
const BLE_DEVICE_NAME_PREFIX = 'Openloop'
const BLE_DEFAULT_MTU       = 155

WalletConnect Constants

const WALLETCONNECT_PROJECT_ID = 'b3d610140c2d0d39f50150a030c7c985'

WalletConnect Supported Chains (CAIP-2)

const SUPPORTED_BIP122_CHAINS = [
  'bip122:000000000019d6689c085ae165831e93', // Bitcoin Mainnet
  'bip122:000000000933ea01ad0ee984209779ba', // Bitcoin Testnet3
] as const

const SUPPORTED_EVM_CHAINS = [
  'eip155:1',         // Ethereum Mainnet
  'eip155:11155111',  // Sepolia Testnet
] as const

const SUPPORTED_SOLANA_CHAINS = [
  'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Mainnet Beta
  'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',  // Devnet
] as const

const SUPPORTED_TRON_CHAINS = [
  'tron:0x2b6653dc',  // TRON Mainnet
  'tron:0x94a9059e',  // TRON Shasta Testnet
] as const

WalletConnect Supported Methods

const SUPPORTED_BIP122_METHODS = [
  'signPsbt', 'getAccountAddresses', 'signMessage', 'sendTransfer',
] as const

const SUPPORTED_EVM_METHODS = [
  'eth_sendTransaction', 'eth_signTransaction', 'eth_sign',
  'personal_sign', 'eth_signTypedData', 'eth_signTypedData_v3',
  'eth_signTypedData_v4', 'wallet_getCapabilities',
] as const

const SUPPORTED_SOLANA_METHODS = [
  'solana_signTransaction', 'solana_signAllTransactions',
  'solana_signAndSendTransaction', 'solana_signMessage',
] as const

const SUPPORTED_TRON_METHODS = [
  'tron_signTransaction', 'tron_signMessage',
] as const

const SUPPORTED_EVENTS = ['chainChanged', 'accountsChanged'] as const

APDU Instruction Set

const APDU = {
  // Class bytes
  CLA_ETHEREUM: 0xe0,
  CLA_OPENLOOP: 0xf0,         // Openloop proprietary (chain_id-aware signing, PSBT)
  CLA_V2: 0xe1,               // Ledger Bitcoin v2 protocol (extended public key retrieval)

  // Shared INS (Ethereum / TRON / XRP; routed automatically by the BIP44 coin_type)
  INS_GET_PUBLIC_KEY: 0x02,    // Get address / public key
  INS_SIGN_TX: 0x04,           // Sign a transaction
  INS_SIGN_MESSAGE: 0x08,      // personal_sign (EIP-191) / TRON message
  INS_SIGN_EIP712: 0x0c,       // EIP-712 Typed Data
  INS_EIP712_STRUCT_DEF: 0x1a, // EIP-712 streaming: type definitions (P2=0x00 struct name / 0xFF field)
  INS_EIP712_STRUCT_IMPL: 0x1c, // EIP-712 streaming: values (P2=0x00 root / 0x0F array / 0xFF field)
  INS_SIGN_AUTHORIZATION: 0x34, // EIP-7702

  // Bitcoin extended public key (CLA_V2)
  INS_V2_GET_EXTENDED_PUBKEY: 0x00, // Get the extended public key for any path

  // Bitcoin PSBT (Openloop proprietary)
  INS_BTC_SIGN_PSBT: 0x70,          // Send a PSBT
  INS_BTC_GET_SIGNED_PSBT: 0x71,    // Get the signed PSBT
  INS_BTC_GET_WALLET_PUBLIC_KEY: 0x40, // Get the BTC public key / address
  INS_BTC_GET_ACCOUNT_XPUB: 0x72,   // Get the account xpub
  INS_BTC_SIGN_MESSAGE: 0x73,       // Sign a BTC message

  // Solana (Ledger Solana compatible)
  INS_SOL_GET_PUBKEY: 0x05,         // Get the Ed25519 public key
  INS_SOL_SIGN_MESSAGE: 0x06,       // Sign a transaction / message
  INS_SOL_SIGN_OFFCHAIN_MSG: 0x07,  // Sign an off-chain message
} as const

SDK_BUILD_ID

const SDK_BUILD_ID: string   // e.g. "@openloop/sdk-core@0.7.0"

A string identifying the bundled @openloop/sdk-core version (in the form @openloop/<package name>@<version>). Use it to read which SDK version a shipped bundle contains. When the source is imported directly (not built), it is "dev".


Utility Functions

// Build an APDU command (ISO 7816-4). The Short form is chosen automatically for data of 255 bytes or less:
// [CLA][INS][P1][P2][Lc][Data...], and the Extended form for 256 bytes or more:
// [CLA][INS][P1][P2][0x00][Lc_HI][Lc_LO][Data...]
function buildApdu(cla: number, ins: number, p1: number, p2: number, data?: Uint8Array): Uint8Array

// Convert a BIP32 path to a buffer: [element count (1 byte)][each element 4 bytes big-endian]. "m/" is optional, ' marks hardened. Elements are not validated
function pathToBuffer(path: string): Uint8Array

// Convert a BIP32 path to an array of elements (hardened adds 0x80000000). Example: "84'/0'/0'/0/0" -> [0x80000054, 0x80000000, 0x80000000, 0, 0]. Elements are not validated
function pathToElements(path: string): number[]

// Concatenate Uint8Arrays
function concat(...arrays: Uint8Array[]): Uint8Array

// Convert a hex string to a Uint8Array (with or without the 0x prefix). Length and characters are not validated
function hexToBytes(hex: string): Uint8Array

// Convert a Uint8Array to a hex string (no 0x prefix)
function bytesToHex(bytes: Uint8Array): string

// Base58 encode/decode (no checksum). decode throws an Error on an invalid character
function base58Encode(bytes: Uint8Array): string
function base58Decode(str: string): Uint8Array

// Format an OpenloopDeviceState as a one-line string for logs
// Example: "wallet=N lang=0 USB(native) ble=off FIDO PIV"
function formatOpenloopState(s: OpenloopDeviceState): string

hexToBytes / pathToBuffer / pathToElements do not validate their input. When you need validated input, check the format in the caller.

BIP-32 Public Key Derivation

Exported from both @openloop/sdk-core and @openloop/sdk-core/bitcoin. Derives non-hardened children from a public key alone and reads extended public keys (xpub / tpub) (private keys are never handled). It can also be used to derive Ethereum and TRON addresses in software from the result of getAddressWithChainCode. Invalid input throws a plain Error, not a BtcError.

const BIP32_VERSION_XPUB = 0x0488b21e   // Mainnet (xpub…)
const BIP32_VERSION_TPUB = 0x043587cf   // Test networks (tpub…)

interface ExtendedPublicKey {
  version: number          // BIP32_VERSION_XPUB or BIP32_VERSION_TPUB
  depth: number
  parentFingerprint: number
  childNumber: number      // This key's own child number (includes 0x80000000 if hardened)
  chainCode: string        // 32 bytes (hex)
  publicKey: string        // 33-byte compressed public key (hex)
}

// Derive a non-hardened child (index is an integer below 0x80000000) from the parent's compressed public key (hex) and chain code (hex)
function deriveChildPublicKey(
  parentPubkey: string, parentChainCode: string, index: number
): { publicKey: string; chainCode: string }

// Parse a serialized extended public key. Throws an Error for anything other than xpub / tpub, a point not on the curve, or a private key
function decodeExtendedPubkey(serialized: string): ExtendedPublicKey

// base58check (with a 4-byte checksum). decode always verifies the checksum
function base58CheckDecode(str: string): Uint8Array
function base58CheckEncode(payload: Uint8Array): string

EIP-712 Streaming

interface TypedDataLike {
  domain: Record<string, unknown>
  types: Record<string, Array<{ name: string; type: string }>>
  primaryType: string
  message: Record<string, unknown>
}

// Stream the EIP-712 data itself to the device, field by field, and sign it (the implementation behind EthereumApp.signTypedDataStreaming)
function signTypedDataStreaming(
  transport: ITransport, path: string, td: TypedDataLike, chainId: number
): Promise<SignatureResult>

// Whether the payload has the EIP-3009 form (TransferWithAuthorization / ReceiveWithAuthorization)
function isEip3009(td: TypedDataLike): boolean

Solana Off-Chain Messages

// Wrap a message in the Solana off-chain message format. If format is omitted, it is 0
// when every byte is printable ASCII (0x20–0x7E) and 1 otherwise
function wrapSolanaOffchainMessage(message: Uint8Array, formatOverride?: 0 | 1 | 2): Uint8Array

// Whether data starts with the Solana off-chain message signing domain
function isSolanaOffchainMessageWrapped(data: Uint8Array): boolean

// The signing domain (16 bytes: 0xFF + "solana offchain")
const SOLANA_OFFCHAIN_SIGNING_DOMAIN: Uint8Array