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 |
@openloop/sdk-core/bitcoin@openloop/sdk-core/verifyThe 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 |
interface TransportStatus {
connected: boolean
deviceName?: string
deviceId?: string
}type TransportEventType = 'connected' | 'disconnected' | 'error'
interface TransportEvent {
type: TransportEventType
error?: Error
}
type TransportEventHandler = (event: TransportEvent) => voidTransport 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
}type TransportFactory = () => Promise<ITransport>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 |
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")
}XRP signature result.
interface XrpSignatureResult {
signature: string // Signature hex (Ed25519: 64B, secp256k1: variable-length DER)
}type XrpCurve = 'ed25519' | 'secp256k1'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)
}interface DeviceInfo {
connected: boolean
path?: string
vendorId?: number
productId?: number
}interface WcSession {
topic: string
}interface IWcSignClient {
request<T>(params: {
topic: string
chainId: string
request: { method: string; params: unknown }
}): Promise<T>
}interface WcTransportOptions {
client: IWcSignClient
session: WcSession
chainId?: string // Default: "eip155:1"
}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).
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
signPsbt / signPsbtHex /
signPsbtWithPaths: if the signed PSBT exceeds 64,000 bytes
(250 bytes × 256 chunks) they throw a BtcError with
code: 'psbt_too_large'. If the device rejects the request,
an ApduError is thrown.signPsbtWithPaths: the path list must fit in one APDU
(250 bytes). If it does not (12 or more 5-level paths), nothing is sent
to the device and a BtcError with
code: 'bad_argument' is thrown. The device reads paths from
the first frame only and accepts at most 10 (11 five-level paths still
fit in one frame, so they are sent as is and the device’s rejection
comes back as an ApduError). With 11 or more inputs, put
BIP32_DERIVATION in each PSBT input (buildPsbt does) and
sign with signPsbtHex without paths (the device reads up to
32 inputs in the whole PSBT).getExtendedPubkey: when the path has more than 10
levels, when a path element is invalid, or when the device’s answer does
not match the requested path (depth, last level, version), it throws a
plain Error, not a BtcError.ApduError when the device
rejects and TransportError when communication fails (Error Classes).The
BtcErrorthrown byBitcoinAppis the same class as theBtcErrorin@openloop/sdk-core/bitcoin(0.8.1 and later; recognise it withinstanceof BtcError. On 0.8.0 and earlier it is a separate copy, so usename === 'BtcError'andcode) (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 = 10For the full Bitcoin send flow (address derivation → UTXO lookup → PSBT → signing → broadcast) see The Bitcoin Module.
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') |
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.
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).
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.
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>
}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 |
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.
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 @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
BtcError code (with an English message). The
caller prepares the text shown on screen from code.fetch_failed
(it is never read as “balance 0”).Endpoint requests (common)
BtcNetwork.esplora / feeEndpoints are
tried in order, best first. Each URL is tried up to
retryCount times (default 3, retryDelayMs =
1000 ms apart); when all attempts fail, the next endpoint is tried. The
time limit timeoutMs covers each call including reading the
whole response body.fetch_failed.
With an empty endpoint array, fetchPrevTx and
getFeeRate throw bad_argument, while
fetchUtxos and scanAccount throw
fetch_failed.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 | 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.
function pathOf(acct: BtcAccount, net: BtcNetworkId, change: 0 | 1, index: number): string
function accountPathOf(acct: BtcAccount, net: BtcNetworkId): stringpathOf 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.
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.
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 |
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).
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.
fetch_failed when every endpoint fails).net is refused before any
endpoint is queried (invalid_address /
network_mismatch).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.
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.
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.
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
}
): BtcSelectionAdds 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).
confirmed === true)
UTXOs are candidates. Your own change is not used until it
confirms."txid:vout" in excluded is dropped
from the candidates.DUST_SATS (546), no
change output is created and the amount goes to the fee
(change is 0, fee is total inputs
− send amount). In that case do not pass change to
buildPsbt.fee_too_high when the fee exceeds
MAX_ABSOLUTE_FEE_SATS (0.1 BTC); when the send amount is at
least FEE_FRACTION_MIN_AMOUNT_SATS (1,000,000 sats),
fee_exceeds_amount if the fee exceeds the send amount.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 |
function buildPsbt(args: {
script: BtcScript
net: BtcNetworkId
inputs: BtcPsbtInput[]
to: Array<{ address: string; value: number }>
change?: BtcChangeOutput // Omit when no change output is created
}): BtcPsbtReturns 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).
prevTxHex (the raw hex of the
previous transaction, the return value of fetchPrevTx). It
fails when it exceeds 4,096 bytes or when its hash does not match
txid.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.
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.
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).
expectedTxid (the correct result for rebroadcasting the
same transaction; without expectedTxid it fails).txid differs from
expectedTxid, bad_response is thrown.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).
function createAddressCache(): BtcAddressCacheCreates 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 |
function findAddressPath(
address: string,
xpub: BtcXpub,
acct: BtcAccount,
net: BtcNetworkId,
cache?: BtcAddressCache,
opts?: { maxIndex?: number; step?: number } // Defaults 200 / 20
): string | nullLooks 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.
// 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 }publicKeyToAddress: bad_argument for a key
that is not compressed (not 66 hex digits starting with 02
/ 03), or an unknown network or script.estimateVbytes: bad_argument for an
unknown script.addressToScriptPubKey: invalid_address /
network_mismatch.encodeSegwitAddress (version 0–16) and
decodeSegwitAddress (checks the checksum, mixed case,
length, and padding) throw a plain Error, not a
BtcError, for invalid input.@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).
// 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 timetype 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.
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"
}
}
}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
}Promise<VerifyResult>). The BTC, SOL, TRON, and XRP
functions are synchronous and return a VerifyResult.expected, the result also tells you
(ok) whether it matches the recovered signer. For EVM
without expected, ok is always
true (you can read the recovered address from
recovered).verifyBtcMessage does not throw but returns
{ recovered: '(unrecoverable)', ok: false }.verifyXrpSignedTx throws an Error when
SigningPubKey / TxnSignature are not found or there is an unsupported
field, and verifyBtcPsbt throws when the PSBT cannot be
read.verifyBtcPsbt throws only when the PSBT cannot
be read. Every verification failure — a signature that does not
match, a public key that is not bound to the output being spent, a
NON_WITNESS_UTXO that does not hash to the input’s txid —
comes back as ok: false, with the reason in
recovered.About identity (pubkey or address): BTC / TRON verification decides from the form of
identitywhether 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) andverifyXrpSignedTx(XRP) recover the signer/public key from the signed data itself, so no external pubkey is needed.
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/verifyshare the definitions of the error classes (0.8.1 and later). TheBtcErrorof/bitcoin, and theBtcErrorthrown byBitcoinApp, both matchinstanceoffor theBtcErrorof/bitcoinand for theOpenloopErrorof@openloop/sdk-core. Branch oncode/statusWord(on 0.8.0 and earlier they are separate copies and do not match, so tell them apart withname:'BtcError','TransportError','ApduError').
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 |
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'.
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)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"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 = 155const WALLETCONNECT_PROJECT_ID = 'b3d610140c2d0d39f50150a030c7c985'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 constconst 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 constconst 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 constconst 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".
// 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): stringhexToBytes / pathToBuffer /
pathToElements do not validate their input. When you need
validated input, check the format in the caller.
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): stringinterface 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// 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