API リファレンス

全型定義・インターフェース・クラス・関数・定数の一覧です。明記がなければ @openloop/sdk-core からエクスポートされています。

import 元 内容
@openloop/sdk-core App クラス・Transport のインターフェース・エラー・定数・ユーティリティ
@openloop/sdk-core/bitcoin Bitcoin の出口(アドレス導出・UTXO 取得・手数料見積り・PSBT の組み立て・放送)。viem を読み込みません
@openloop/sdk-core/verify 署名検証(参照実装)。viem / @noble を使います

目次


インターフェース

ITransport

デバイスとの通信を抽象化するインターフェース。全 Transport が実装します。

interface ITransport {
  open(): Promise<void>
  close(): Promise<void>
  isConnected(): boolean
  exchange(apdu: Uint8Array): Promise<Uint8Array>
  lock(): Promise<void>
  unlock(): Promise<void>
}
メソッド 説明
open() デバイスへの接続を開く
close() 接続を閉じる
isConnected() 接続中かどうか
exchange(apdu) APDU コマンドを送信してレスポンスを受信。成功時はステータスワードを除いたデータを返し、エラー時は例外を投げる
lock() デバイスの排他アクセスを取得(マルチステップ操作用)。複数のアプリが同時に使うときに APDU の割り込みを防ぐ
unlock() 排他アクセスを解放

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

OpenloopSDK.discover() が返す Transport 情報。

interface TransportInfo {
  type: string        // 登録名(例: 'webhid')
  name: string        // 表示名(例: 'WebHID (USB)')
  available: boolean  // 利用可能か
}

TransportFactory

type TransportFactory = () => Promise<ITransport>

BtcMessageSignature

Bitcoin BIP-137 メッセージ署名結果。

interface BtcMessageSignature {
  v: number       // BIP-137 ヘッダバイト(下表)
  r: string       // R 値 (32 bytes hex)
  s: string       // S 値 (32 bytes hex)
  signature: string // 完全な署名 (V || R || S, 65 bytes) の Base64
}

v は BitcoinApp.signMessage に渡したパスの purpose(1 段目)で決まります。

purpose アドレス型 v
84' P2WPKH(native SegWit) 39〜42
49' P2SH-P2WPKH 35〜38
44' P2PKH(圧縮鍵) 31〜34
上記以外 — 35〜38

InputSigningPath

Bitcoin PSBT の入力ごとの BIP32 パス指定。

interface InputSigningPath {
  index: number   // PSBT の入力インデックス
  path: string    // BIP32 パス(例: "84'/1'/0'/0/5")
}

XrpSignatureResult

XRP 署名結果。

interface XrpSignatureResult {
  signature: string  // 署名 hex (Ed25519: 64B, secp256k1: DER 可変長)
}

XrpCurve

type XrpCurve = 'ed25519' | 'secp256k1'

SignatureResult

Ethereum / TRON の ECDSA 署名結果。

interface SignatureResult {
  v: number    // リカバリー ID
  r: string    // R 値 (32 bytes hex, 0x プレフィックスなし)
  s: string    // S 値 (32 bytes hex, 0x プレフィックスなし)
}

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  // デフォルト: "eip155:1"
}

App クラス

すべての App クラスは constructor(transport: ITransport) を持ちます。デバイスと通信する各操作は、transport.lock() と transport.unlock() で囲んで実行されます。

BitcoinApp・EthereumApp・XrpApp・SolanaApp・TronApp と OpenloopSDK は、同梱されている SDK のバージョンを読むための static readonly sdkBuildId: string を持ちます(@openloop/sdk-core@<バージョン> の形。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>
}
メソッド 説明
getAddress(testnet = false) BTC_MAINNET_PATH(testnet が true なら BTC_TESTNET_PATH)の P2WPKH(bech32)アドレスを取得
getAddressWithPath(path) 任意の BIP32 パス("m/" 付きも可)のアドレスを取得。publicKey は非圧縮(65 バイト)の hex
getAccountXpub(coinType) アカウントレベルの拡張公開鍵 m/84'/coin'/0'(coinType 0 = mainnet、1 = testnet)。このパスに固定です。44’ / 49’ や 2 番目以降のアカウントには getExtendedPubkey を使います
getExtendedPubkey(path, display = false) 任意のパス(10 段まで)の拡張公開鍵を取得し、BtcXpub で返す。デバイスの画面には表示されません(display はファームウェアのログに記録されるだけ)。ファームウェア 0.63.125 以降で BIP-32 として正しい鍵が返ります。44’・49’ も接頭辞は xpub/tpub です(ypub・zpub にはなりません)
getMasterFingerprint() マスター公開鍵の hash160 の先頭 4 バイトを 8 文字の小文字 hex で返す
signMessage(message, path) BIP-137 メッセージ署名。message は UTF-8 文字列
signPsbt(psbt) PSBT 署名(バイナリ入出力)。psbt は Uint8Array か hex 文字列
signPsbtHex(psbt) PSBT 署名(hex 入出力)
signPsbtWithPaths(psbt, inputPaths) 入力ごとの署名パスを指定した PSBT 署名(hex 入出力)

エラー

BitcoinApp が投げる BtcError は、@openloop/sdk-core/bitcoin の BtcError と同じクラスです(0.8.1 以降。instanceof BtcError で判別できます。0.8.0 以前は別のコピーなので name === 'BtcError' と code で判別してください)(エラークラス)。

BitcoinApp と組み合わせる関数(@openloop/sdk-core)

// getExtendedPubkey の APDU データ部を組む(純関数)。path は "m/84'/1'/0'" / "84'/1'/0'" / "m"(根)
function buildExtendedPubkeyRequest(path: string, display?: boolean): Uint8Array

// getExtendedPubkey の応答(base58check の文字列)を解く(純関数)。
// 応答が requestedPath のものか(深さ・最後の段・バージョン)を検めて、食い違えば Error を投げる
function parseExtendedPubkeyResponse(xpub: string, requestedPath: string): BtcXpub

const MAX_EXTENDED_PUBKEY_PATH_LEVELS = 10

Bitcoin の送金を組み立てる一連の流れ(アドレス導出 → UTXO 取得 → PSBT → 署名 → 放送)は Bitcoin の出口を参照してください。

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>
}
メソッド 説明
setKind(kind) / getKind() 接続先の種類。既定は 'openloop'。'ledger' にすると signPersonalMessage と signTypedData が Ledger 互換(CLA 0xE0・chainId なし)で動きます。getAddress と signTransaction は常に CLA 0xE0 です
getAddress(path) アドレスを取得。address は 0x 付き
getAddressWithChainCode(path) 公開鍵・アドレスに加えて 32 バイトの chainCode(hex)を返す。通常は変更ノード("44'/60'/0'/0")を渡し、末尾の index はソフトウェアで導出します(BIP-32 の公開鍵導出)
signPersonalMessage(path, message, chainId = 1) EIP-191 personal_sign。message は 0x で始まれば hex のバイト列、そうでなければ UTF-8 文字列として扱う
signTransaction(path, rawTx) RLP エンコード済みトランザクション(hex。0x あり/なし)に署名
signTypedData(path, domainSeparatorHash, messageHash, chainId = 1) EIP-712。ドメインとメッセージのハッシュ(各 32 バイトの hex)を渡す
signTypedDataStreaming(path, typedData, chainId = 1) EIP-712 の内容そのものをデバイスへ順に送信して署名する(Openloop 専用・'ledger' では Error)。ネストした構造体と配列は拒否します。APDU の往復が約 25 回かかります。EIP-3009 かどうかは isEip3009 で判定します
signAuthorization(path, authorizationRlp) EIP-7702 の認可。authorizationRlp は RLP([chain_id, address, nonce]) の hex(Openloop 専用・'ledger' では Error)

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 の既定は 'ed25519' です。signTransaction の txBlob は署名対象のシリアライズ済みトランザクション(hex。0x あり/なし)で、署名結果は Ed25519 なら 64 バイト、secp256k1 なら DER(可変長)の 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 と signOffchainMessage は 64 バイトの Ed25519 署名を hex 文字列(128 文字)で返します。signOffchainMessage は messageBytes を Solana のオフチェーンメッセージ形式で包んでから署名します(既に包まれていれば二重には包みません。Solana のオフチェーンメッセージ)。

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 は EthereumApp と同じ形(通常は "44'/195'/0'/0" を渡す)。signTransaction の rawDataHex はトランザクションの raw_data(hex。0x あり/なし)、signMessage の message は UTF-8 文字列です。

OpenloopApp

Openloop ファームウェア固有のユーティリティコマンドを扱うクラス。CLA=0xF0 (CLA_OPENLOOP) で公開されている独自コマンドを提供します。

class OpenloopApp {
  constructor(transport: ITransport)

  /**
   * GET_DEVICE_INFO (F0 A0) を送信し、識別 + 状態 + フィーチャー情報を取得します。
   *
   * 戻り値:
   *   - 古い Openloop ファームウェア (v0.91.13 未満) や対応していない
   *     デバイスは null を返します
   *   - APDU の送受信が失敗した場合(デバイスの拒否・通信エラーを含む)も
   *     null を返します
   *   - 応答が 16 バイト未満、または先頭 2 バイトが "OL" でない場合も null を返します
   *   - 排他の取得 (lock) の失敗は throw します
   */
  getDeviceInfo(): Promise<OpenloopDeviceInfo | null>
}

OpenloopDeviceInfo

interface OpenloopDeviceInfo {
  modelId: number              // ハードウェア SKU ID(例: 0x0001 = Openloop V1)
  hwRevision: number           // PCB リビジョン
  mainFwVersion: { major: number; minor: number; patch: number }
  recoveryFwVersion: { major: number; minor: number; patch: number }
  state: number                // 16 bit 状態 bitmask(生値)
  stateBits: OpenloopDeviceState  // state を decode したビュー
  features: number             // 16 bit 機能 bitmask(実装詳細・将来変更あり)
}
フィールド 型 説明
modelId number ハードウェア SKU ID。新しい SKU が出るたびに増える
hwRevision number PCB リビジョン番号
mainFwVersion {major,minor,patch} 動作中のメインファームウェアバージョン
recoveryFwVersion {major,minor,patch} リカバリ(ファクトリー)パーティションのバージョン。取得不可なら全 0
state number 現在の状態を表す 16 bit bitmask(生値)
stateBits OpenloopDeviceState state を decode したオブジェクト
features number ハードウェア機能を表す 16 bit bitmask(生値)。bit 配置はファームウェア実装の詳細で、バージョンによって変わる可能性があります。 ホストアプリでは生値の保存・転送のみに使うことを推奨します

OpenloopDeviceState

state フィールドを decode した、人間に分かる形のオブジェクト。

interface OpenloopDeviceState {
  walletProvisioned: boolean
  language: number            // 0=英語, 1=日本語, 2-7=予約
  usbEnabled: boolean
  usbVidCompat: boolean       // false=Native (0x303A), true=Compatible (0x2C97)
  bleEnabled: boolean
  blePaired: boolean
  fidoEnabled: boolean
  pivEnabled: boolean
}

state bitmask レイアウト (FW v0.91.14+ で確定):

bit 名称 意味
0 WALLET_PROVISIONED ウォレット生成済み
1-3 LANG (3 bit) 言語インデックス(0=EN, 1=JA, …)
4 USB_ENABLED USB 通信 ON
5 USB_VID_COMPAT USB VID = Compatible mode (0x2C97)。0 のとき Native (0x303A)
6 BLE_ENABLED BLE 通信 ON
7 BLE_PAIRED BLE ペアリング済みデバイスあり
8 FIDO_ENABLED FIDO/Passkey 機能 ON
9 PIV_ENABLED PIV / PKCS#11 機能 ON
10-15 reserved 将来拡張用、現在は 0

将来 bit が追加される可能性があります。stateBits に存在しない bit を読みたい場合は生の state 値を直接マスクしてください。

使用例

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) {
    // ウォレット未生成のときの分岐
  }
}

Bitcoin の出口

@openloop/sdk-core/bitcoin サブパス。Bitcoin の送金に必要な処理を、デバイスと通信する BitcoinApp(@openloop/sdk-core)の外側に、ネットワークへの問い合わせを含む関数として提供します。Legacy(P2PKH)・Nested SegWit(P2SH-P2WPKH)・Native SegWit(P2WPKH)の 3 型に対応し、viem を読み込みません。

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

設計の約束

端点への問い合わせ(共通)

Bitcoin の型

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

interface BtcNetwork {
  id: BtcNetworkId
  esplora: string[]       // Esplora 互換の端点。先頭(最良)から順に試す
  feeEndpoints: string[]  // `/v1/fees/recommended` を持つ端点(mempool.space 互換)
  timeoutMs: number       // 1 回の呼び出しの時間切れ(ミリ秒)。SDK は既定値を持たない
}

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

interface BtcAccount {
  script: BtcScript
  account: number         // アカウント番号(強化の境目 0x80000000 未満)
}

interface BtcXpub {       // デバイスから受けた拡張公開鍵(BitcoinApp.getExtendedPubkey の返り値)
  publicKey: string       // 33 バイトの圧縮公開鍵 (hex)
  chainCode: string       // 32 バイト (hex)
  parentFingerprint: number  // 直前の親の指紋(マスターではない)
  depth: number           // m からの段数
  xpub: string            // シリアライズ済みの文字列(xpub… / tpub…)
}

interface BtcUtxo {
  txid: string
  vout: number
  value: number           // sats
  address: string
  path: string            // "m/84'/1'/0'/0/3" の形
  publicKey: string       // 33 バイトの圧縮公開鍵 (hex)
  confirmed: boolean      // 1 確認未満は false
  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 は sats

interface BtcSelection {
  inputs: BtcUtxo[]
  fee: number             // 実際に払う手数料(sats)= 入力合計 − 出力合計
  change: number          // お釣り(sats)。0 ならお釣りの出力を作らない
  vbytes: number          // 見積もった大きさ(vB)
}

interface BtcScanResult {
  used: Array<{ address: string; path: string }>   // 使われたことのあるアドレス(受取列・お釣り列の両方)
  utxos: BtcUtxo[]
  nextChange: { index: number; address: string; path: string; publicKey: string }  // お釣りに使う次の未使用アドレス
}

interface BtcPsbtInput extends BtcUtxo {
  prevTxHex?: string      // P2PKH のときだけ必須: 前の取引の生の hex
}

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

interface BtcPsbt {
  psbtHex: string
  paths: Array<{ index: number; path: string }>   // BitcoinApp.signPsbtWithPaths に渡す形
}

interface FetchDeps {     // 試験用に差し替えられる取得の設定
  fetchImpl?: typeof fetch
  retryDelayMs?: number   // 既定 1000
  retryCount?: number     // 既定 3(同じ URL への試行回数)
}

アドレスの型とパスの対応は次のとおりです。

BtcScript purpose アドレス PSBT の入力に入る欄
'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' と 'testnet4' は coin type もアドレスの形式も同じ(1・tb1…)で、違うのは端点だけです。

関数の一覧

関数 役割
pathOf アカウント・チェーン・index からパスを組み立てる
deriveAddresses 拡張公開鍵から連続したアドレスを導出する
validateAddress 宛先のアドレスを検める
scanAccount アカウントを走査して使用済みアドレス・UTXO・お釣りの次のアドレスを得る
addressStats アドレス 1 本が使われたか(取引の数・受け取った額・使った額)を訊く
fetchUtxos 指定したアドレスの UTXO を取得する
fetchPrevTx 前の取引の生の hex を取得する(P2PKH の入力に必要)
getFeeRate 手数料率(sat/vB)を取得する
selectUtxos 使う UTXO を選び、手数料とお釣りを数える
buildPsbt PSBT を組み立てる
extractRawTx 署名済み PSBT から生の取引と txid を取り出す
broadcastTx 生の取引を放送する
createAddressCache アドレス → パス・公開鍵の覚えを作る
findAddressPath アドレスからパスを逆引きする

補助の関数(accountPathOf・publicKeyToAddress・estimateVbytes・addressToScriptPubKey・encodeSegwitAddress・decodeSegwitAddress)と定数は、後の節にあります。

pathOf / accountPathOf

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

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

pathOf は m/{purpose}'/{coin}'/{account}'/{change}/{index}(例: "m/84'/1'/0'/1/3")を返します。purpose は acct.script から(44 / 49 / 84)、coin type は網から(mainnet は 0、testnet3・testnet4 は 1)決まります。accountPathOf はアカウントまでの 3 段("m/84'/1'/0'")で、BitcoinApp.getExtendedPubkey に渡すのはこの形です。

エラー: 知らない script・網、負・小数・NaN・0x80000000 以上の account / index、0 / 1 以外の change は bad_argument。

deriveAddresses

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

アカウントレベル(depth が 3)の拡張公開鍵から、change 列の from 番目から count 本のアドレスを導出します(デバイスとは通信しません)。

エラー: from / count が 0 以上の整数でないとき、xpub.depth が 3 でないとき(別のアカウントのアドレスを黙って作らないため)は bad_argument。知らない網・script も bad_argument です。公開鍵・chain code の中身が不正なときは、通常の Error を投げます。

validateAddress

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

宛先として使えるアドレスを検めて、型(kind)を返します。bech32(P2WPKH・P2WSH)・bech32m(P2TR)・base58check(P2PKH・P2SH)の 5 型を読みます。チェックサムを検証し、bech32 は大文字だけの形も受けます(大文字小文字の混在は不正)。

エラー

code 条件
invalid_address 空・チェックサム不一致・文字や長さが不正・witness version が 2 以上など、アドレスとして読めない
network_mismatch アドレスとしては正しいが、渡した net のものではない(本番のアドレスを試験網で使う、など)。呼び出し側は「網を切り替えますか」と案内できます
bad_argument 知らない網

scanAccount

function scanAccount(
  xpub: BtcXpub,
  acct: BtcAccount,
  net: BtcNetwork,
  opts?: {
    cache?: BtcAddressCache
    gapLimit?: number       // 既定 20(1 以上の整数)
    concurrency?: number    // 既定 4(1 以上の整数)
    knownUsed?: Array<{ chain: 0 | 1; index: number }>  // 呼ぶ側が「使われた」と知っている番号
  } & FetchDeps
): Promise<BtcScanResult>

受取列(change = 0)とお釣り列(change = 1)を、取引のないアドレスが gapLimit 本連続するまで走査します。残高のあるアドレスだけ UTXO を取得します。used は両方の列の使用済みアドレス、utxos は残高のあるアドレスの UTXO(確定待ちを含み、confirmed で区別できる)、nextChange はお釣り列で取引のない最初のアドレスです(送金のたびに同じお釣りのアドレスを再利用しないため)。opts.cache を渡すと、導出したすべてのアドレスのパスと公開鍵を覚えます。opts.knownUsed は、呼ぶ側が「使われた」と知っている番号を渡す口です(端点が未使用と答えても使用済みに数え、nextChange はそこを飛ばします。試験網 Testnet3/4 は同じ鍵を共有するので、もう片方の網で使った番号を渡せます。端点にも訊くので残高は端点の答えのままです。渡さなければ今までと同じ)。

エラー: gapLimit / concurrency が不正なら bad_argument。1 本でも全端点に訊けなかったときは fetch_failed(空として続けません)。

addressStats

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

interface BtcAddressStats {
  txCount: number   // 取引の数(確定+未確定)。0 より大きければ「使われた」
  funded: number    // 受け取った額の合計(sats・確定+未確定)
  spent: number     // 使った額の合計(sats・確定+未確定)。残高は funded - spent
}

アドレス 1 本が使われたかを端点(Esplora の address)に訊きます。WalletConnect 経由のように財布が返した住所だけで拡張公開鍵が無く、scanAccount が使えない呼び手が、お釣りに「取引の数が 0 の住所」を選ぶための口です。

fetchUtxos

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

渡したアドレスの UTXO をすべて取得します(同時に最大 API_PARALLEL_LIMIT = 4 本)。confirmed は端点が確定と答えたものだけが true です。

エラー: addresses が配列でないときは bad_argument、1 本でも全端点が失敗したときまたは答えの形が不正なときは fetch_failed。

fetchPrevTx

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

txid の取引の生の hex(小文字)を取得します。取得した取引が本当にその txid のものかを検めます(P2PKH は前の取引の全体から額を読むため、別の取引を渡されて嘘の額に署名することを防ぎます)。

エラー: txid が 64 桁の hex でないときは bad_argument、全端点が失敗したときは fetch_failed、hash が txid と合わないときは bad_response、4,096 バイトを超えるときは prev_tx_too_large。

getFeeRate

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

net.feeEndpoints の /v1/fees/recommended から halfHourFee(sat/vB)を返します。値は MIN_FEE_RATE_SAT_VB〜MAX_FEE_RATE_SAT_VB の範囲で検め、範囲外は丸めずに失敗させます。

エラー: 全端点が失敗、または halfHourFee が数値でないときは fetch_failed、範囲外は fee_rate_out_of_range。

selectUtxos

function selectUtxos(
  utxos: BtcUtxo[],
  opts: {
    to: BtcRecipient[]
    feeRate: number
    script: BtcScript
    net: BtcNetworkId
    excluded?: Iterable<string>   // 使用済みの印 "txid:vout"
    changeAddress?: string        // 省略すると、お釣りは入力と同じ型として数える
  }
): BtcSelection

大きい順に UTXO を足して、手数料とお釣りを数えます(同額は txid・vout の順で、同じ入力から同じ結果になります)。

エラー

code 条件
bad_argument utxos が配列でない・to が空・金額が正の整数でない
fee_rate_out_of_range feeRate が MIN_FEE_RATE_SAT_VB〜MAX_FEE_RATE_SAT_VB の外
invalid_address / network_mismatch to または changeAddress のアドレスが不正、または別の網のもの
insufficient 確定・未確定を合わせても残高が不足している
insufficient_unconfirmed 確定待ちを数えれば残高が足りている(待てば送金できる)
fee_too_high / fee_exceeds_amount 上記の手数料の門

buildPsbt

function buildPsbt(args: {
  script: BtcScript
  net: BtcNetworkId
  inputs: BtcPsbtInput[]
  to: Array<{ address: string; value: number }>
  change?: BtcChangeOutput    // お釣りの出力を作らないときは渡さない
}): BtcPsbt

PSBT v0(取引の version 2・locktime 0・全入力の sequence 0xfffffffd = RBF)を hex で返します。paths はそのまま BitcoinApp.signPsbtWithPaths(psbtHex, paths) に渡せます。

エラー: 入力・宛先がない、金額が正の整数でない、txid が不正、script が不明、P2PKH の prevTxHex がない・hash が合わない、公開鍵・パスが不正のときは bad_argument。prevTxHex が 4,096 バイトを超えるときは prev_tx_too_large。宛先・お釣りのアドレスが不正なら invalid_address / network_mismatch。

extractRawTx

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

各入力が最終化(FINAL_SCRIPTSIG / FINAL_SCRIPTWITNESS)された署名済み PSBT から、放送できる生の取引と txid(witness を除いた形の double SHA-256 の逆順)を取り出します。1 本でも witness があれば marker と flag を書きます(BIP-144)。Legacy だけの取引には書きません。

エラー: PSBT として読めない・途中で切れている・入力に FINAL_SCRIPTSIG も FINAL_SCRIPTWITNESS もないときは bad_psbt。hex として不正なときは bad_argument。

broadcastTx

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

net.esplora の POST /tx で生の取引を放送し、txid(小文字)を返します。応答の本文が 64 桁の hex(txid)でないものは、その端点の答えとして受け取りません(キャプティブポータルなどが返す HTML を txid として扱わないため)。

expectedTxid(extractRawTx が返した txid)を渡してください。

エラー: rawTxHex が hex でないときは bad_argument。全端点が失敗したとき(到達不能・HTTP エラー・txid でない応答・拒否)は broadcast_rejected(fetch_failed ではありません。メッセージに最後の端点の理由が入ります)。

createAddressCache

function createAddressCache(): BtcAddressCache

アドレス → { path, publicKey } の覚えを作ります。モジュールに 1 つではなく、呼び出し側が(網・型・アカウントごとなど)必要な数だけ作って持ちます。

メンバー 説明
get(address) / set(address, entry) 覚えの参照・追加
syncToDevice(deviceId) 持ち主のデバイスを宣言する。持ち主が変わったら全部捨てます(別のデバイスのアドレスを自分の物として扱わないため)。同じ deviceId(null 以外)なら何もしません。null は持ち主不明として、呼ぶたびに捨てます
size 覚えている件数
clear() 全部捨てる

findAddressPath

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

address のパスを逆引きします。まず cache を見て、なければ受取列 → お釣り列の順に step 本ずつ maxIndex 未満まで導出して探します。導出したアドレスは cache に覚えます。見つからなければ null を返します(投げません。自分の物でないアドレスは誤りではないため)。

エラー: maxIndex / step が 1 以上の整数でない、address が空のときは bad_argument。

補助の関数

// 圧縮公開鍵(hex・0x あり/なし)からその型のアドレスを作る
function publicKeyToAddress(publicKey: string, script: BtcScript, net: BtcNetworkId): string

// 取引の大きさ(vB)を見積もる。outputVbytes は OUTPUT_VBYTES の値の配列
function estimateVbytes(numInputs: number, script: BtcScript, outputVbytes: number[]): number

// 宛先のアドレスを scriptPubKey にする(validateAddress と同じ検査をします)
function addressToScriptPubKey(address: string, net: BtcNetworkId): Uint8Array

// witness アドレス(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 は、@openloop/sdk-core と同じ BIP-32 の関数・型(deriveChildPublicKey・decodeExtendedPubkey・base58CheckDecode・base58CheckEncode・BIP32_VERSION_XPUB・BIP32_VERSION_TPUB・ExtendedPublicKey)も再エクスポートしています(BIP-32 の公開鍵導出)。

Bitcoin の定数

// 手数料率(sat/vB)
const MIN_FEE_RATE_SAT_VB = 1
const MAX_FEE_RATE_SAT_VB = 5000

// 手数料の門
const MAX_ABSOLUTE_FEE_SATS = 10_000_000        // 0.1 BTC
const MAX_FEE_FRACTION = 1.0                    // 手数料 / 送金額の上限(100%)
const FEE_FRACTION_MIN_AMOUNT_SATS = 1_000_000  // この額未満の送金には割合の門を当てない

// 組み立て
const DUST_SATS = 546                           // これ未満のお釣りは出力にしない
const MAX_PREV_TX_BYTES = 4096                  // 前の取引の大きさの上限(デバイスの上限)
const RBF_SEQUENCE = 0xfffffffd                 // 全入力の sequence

// 署名済み PSBT の読み戻し
const PSBT_RESPONSE_CHUNK_SIZE = 250            // 1 かたまりのバイト数
const PSBT_MAX_RESPONSE_CHUNKS = 256            // かたまりの数(番号が APDU の P1 の 1 バイト)
const MAX_SIGNED_PSBT_BYTES = 64000             // 250 × 256

// 大きさの見積り(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

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

// 端点への取得(FetchDeps の既定値)
const API_RETRY_COUNT = 3            // 1 つの URL への試行回数
const API_RETRY_DELAY_MS = 1000      // 試行の間隔(ミリ秒)
const API_PARALLEL_LIMIT = 4         // 同時に走らせる取得の数

BtcError

type BtcErrorCode =
  | 'bad_argument'              // 引数が不正(パス・index・網と型の組み合わせ・長さ)。呼び出し側の誤り
  | 'invalid_address'           // 宛先として読めない(チェックサム・文字・長さ)
  | 'network_mismatch'          // 読めるが、今の網のアドレスではない
  | 'fetch_failed'              // 端点に訊けなかった(全端点が失敗)
  | 'bad_response'              // 端点の答えが不正(hash が合わない・txid が違うなど)
  | 'prev_tx_too_large'         // 前の取引がデバイスの上限(4,096 B)を超える
  | 'fee_rate_out_of_range'     // 手数料率が範囲外
  | 'insufficient'              // 残高が不足している
  | 'insufficient_unconfirmed'  // 確定待ちを数えれば足りる
  | 'fee_too_high'              // 手数料が絶対額の上限を超える
  | 'fee_exceeds_amount'        // 手数料が送金額を超える
  | 'psbt_too_large'            // 署名済み PSBT が読み戻せる上限(64,000 B)を超えた
  | 'bad_psbt'                  // PSBT として読めない
  | 'broadcast_rejected'        // 放送が受理されなかった

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

message は英語のままです。画面に表示する文言は、呼び出し側が code を鍵にして用意してください。

使用例

Testnet3 で P2WPKH のアカウントから送金する流れです。transport はデバイスに接続済みの ITransport です。

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. アカウントの拡張公開鍵("m/84'/1'/0'")を取得し、デバイスの持ち主をキャッシュに伝える
const xpub = await btc.getExtendedPubkey(accountPathOf(acct, net.id))
cache.syncToDevice(await btc.getMasterFingerprint())

// 2. 走査して UTXO とお釣りの次のアドレスを得る
const { utxos, nextChange } = await scanAccount(xpub, acct, net, { cache })

// 3. 手数料率を取得し、使う UTXO を選ぶ
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. PSBT を組み、デバイスで署名し、取り出して放送する
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)

P2PKH(Legacy)のアカウントでは、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) }))
)

エラーは code で分岐します。

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

try {
  // …上の流れ…
} catch (e) {
  if (e instanceof BtcError) {
    const code = e.code
    if (code === 'insufficient_unconfirmed') {
      // 「確定を待てば送金できます」と案内する
    }
  }
}

署名検証

@openloop/sdk-core/verify サブパス。デバイスが返した署名を、参照実装(viem / @noble)で 検証するユーティリティ。transport 非依存(USB / BLE / AirGap / WalletConnect どの経路でも可)。 @openloop/sdk-core と @openloop/sdk-core/bitcoin には含めず(viem を読み込ませない)、サブパスで提供する。

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

const result = await verifyPersonalSign(message, sig, address)
// result: { recovered: string; expected?: string; ok: boolean }
関数 チェーン 検証対象
verifyBtcMessage(message, sig, identity) BTC BIP-137 メッセージ(sig は BtcMessageSignature、identity = pubkey hex か BTC アドレス)
verifyBtcPsbt(signedPsbtHex) BTC 署名済み PSBT の署名(Native P2WPKH・Nested P2SH-P2WPKH・Legacy P2PKH の 3 型。pubkey は PSBT 内)
verifyPersonalSign(message, sig, expected?) EVM EIP-191 personal_sign
verifyTypedDataJson(typedDataJson, sig, expected?) EVM EIP-712(フル JSON)
verifyTypedDataHashes(domainHash, msgHash, sig, expected?) EVM EIP-712(事前ハッシュ)
verifyTransaction(unsignedTxHex, sig, expected?) EVM legacy / EIP-2718 typed tx({v,r,s} を受ける)
verifySignedTransaction(signedTxHex, expected?) EVM 署名済みシリアライズ tx から署名者を復元(WalletConnect eth_signTransaction の戻り値)
verifyAuth7702(authRlpHex, sig, expected?) EVM EIP-7702 authorization
verifyXrpTransaction(txBlobHex, curve, result, pubkeyHex) XRP ed25519 / secp256k1(for-signing blob + pubkey 指定)
verifyXrpSignedTx(signedTxHex) XRP 署名済み tx blob から自己完結で検証(SigningPubKey/TxnSignature を抽出。pubkey 不要)
verifySolMessage(message, sigHex, pubkeyHex) SOL ed25519 メッセージ(message の UTF-8 バイト列そのものを検証。オフチェーンメッセージ形式の包みは付けない)
verifySolTransaction(txHex, sigHex, pubkeyHex) SOL ed25519 トランザクション
verifyTronMessage(message, sig, identity) TRON keccak256 TRON プレフィックス(identity = pubkey hex か TRON アドレス)
verifyTronTransaction(rawDataHex, sig, identity) TRON sha256(raw_data)(identity = pubkey hex か TRON アドレス)

sig は、EVM と TRON が SignatureResult、BTC のメッセージが BtcMessageSignature です(XrpSignatureResult は verifyXrpTransaction の result)。curve は XrpCurve です。

// 戻り値(いずれの関数も)
interface VerifyResult {
  recovered: string   // 復元した署名者(EVM: アドレス)。ed25519 の検証では結果を表す文字列
  expected?: string
  ok: boolean
}

identity(pubkey か アドレス)について: BTC / TRON の検証は、署名から復元した公開鍵を そのまま比較するか、復元鍵からチェーンアドレスを導出して比較するかを identity の形で自動判定します。 WalletConnect のように公開鍵が得られない経路ではアドレスで照合できます。 verifySignedTransaction(EVM)と verifyXrpSignedTx(XRP)は、署名済みデータ自体から 署名者/公開鍵を復元するため、外部から pubkey を渡す必要がありません。


エラークラス

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

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

type TransportErrorCode =
  | 'not-supported'   // 実行環境がデバイスと話せない
  | 'not-found'       // 一致するデバイスが無い / 選択がキャンセルされた
  | 'not-connected'   // 接続が開いていない
  | 'timeout'         // 応答が時間内に来なかった
  | 'disconnected'    // 使用中に切断された
  | 'io'              // それ以外

class TransportError extends OpenloopError {
  name: string  // 'TransportError'
  readonly code: TransportErrorCode        // v0.6.0 で追加
  stale?: boolean
  busy?: boolean
  noResponse?: boolean
  sent?: number
  awaited?: boolean
  constructor(message: string, code?: TransportErrorCode)  // 既定は 'io'
}

ApduError のメッセージは APDU error: 0x6985 (User rejected on device) の形で、分岐には statusWord を使います。

statusWord 説明(メッセージ内)
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
上記以外 Unknown error

TransportError の stale / busy / noResponse / sent / awaited は、Safari 拡張経由の接続(@openloop/transport-safari)が付ける省略可能な欄です。欄がないことは「安全」を意味しません。

欄 意味
stale 拡張が入れ替わって橋が失効した。再接続では解消せず、Safari を再度開く必要がある
busy デバイスを触る列が混んでいて、デバイスに触れずに断った。安全に再試行できる
noResponse 橋が答えを返せなかった。デバイスが動いたかどうかは分からないので、自動で再送しない
sent 送信し終えたフレーム数。0 ならデバイスは 1 バイトも受け取っていない
awaited 全フレームを送信し終えた後、応答待ちで失敗した。true なら署名済みの可能性がある

instanceof について: @openloop/sdk-core・@openloop/sdk-core/bitcoin・@openloop/sdk-core/verify はエラークラスの定義を共有します(0.8.1 以降)。/bitcoin の BtcError も、BitcoinApp が投げる BtcError も、/bitcoin の BtcError の instanceof と @openloop/sdk-core の OpenloopError の instanceof に一致します。処理は code / statusWord で分けてください(0.8.0 以前は別のコピーで一致しないので、name('BtcError'・'TransportError'・'ApduError')で判別します)。


OpenloopSDK クラス

Transport の登録・検出・接続を管理するクラス。全メソッドは 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
}
メソッド 説明
registerTransport(type, config) Transport を type(例: 'webhid')で登録する。同じ type は上書き
discover() 登録済みの全 Transport を登録順に調べ、isAvailable() の結果を返す(isAvailable() が例外を投げたときは available: false)
connect(opts?) opts.transport を指定すると、その Transport の factory() → open() を実行して返す。未登録なら TransportError(code: 'not-found')、factory() / open() の失敗はそのまま投げる。省略すると、登録順に利用可能な Transport を試し、失敗したものは次へ進む。すべて失敗なら TransportError(code: 'not-found')
getRegisteredTransports() 登録済みの type の配列(登録順)
clearTransports() 登録をすべて削除する

WcTransport クラス

WalletConnect v2 のリレー経由で、リモートの Openloop Connect にデバイス操作を中継する ITransport 実装です。カスタムメソッド openloop_exchange(hex の APDU を渡し、{ data: hex } を受け取る)・openloop_lock・openloop_unlock を 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() は接続済みの印を立てるだけで、通信はしません。open() の前(または close() の後)に lock() / unlock() / exchange() を呼ぶと TransportError(code: 'not-connected')を投げます。


定数

USB 識別子

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 互換
const LEDGER_USAGE_PAGE   = 0xffa0   // Ledger HID usage page (APDU transport)

BIP44 デフォルトパス

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 定数

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 定数

const WALLETCONNECT_PROJECT_ID = 'b3d610140c2d0d39f50150a030c7c985'

WalletConnect 対応チェーン (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 対応メソッド

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 命令セット

const APDU = {
  // クラスバイト
  CLA_ETHEREUM: 0xe0,
  CLA_OPENLOOP: 0xf0,         // Openloop 独自(chain_id 付き署名・PSBT)
  CLA_V2: 0xe1,               // Ledger Bitcoin v2 プロトコル(拡張公開鍵の取得)

  // 共通 INS(Ethereum / TRON / XRP。BIP44 の coin_type で自動ルーティング)
  INS_GET_PUBLIC_KEY: 0x02,    // アドレス/公開鍵取得
  INS_SIGN_TX: 0x04,           // トランザクション署名
  INS_SIGN_MESSAGE: 0x08,      // personal_sign (EIP-191) / TRON メッセージ
  INS_SIGN_EIP712: 0x0c,       // EIP-712 Typed Data
  INS_EIP712_STRUCT_DEF: 0x1a, // EIP-712 ストリーミング: 型定義(P2=0x00 構造体名 / 0xFF フィールド)
  INS_EIP712_STRUCT_IMPL: 0x1c, // EIP-712 ストリーミング: 値(P2=0x00 ルート / 0x0F 配列 / 0xFF フィールド)
  INS_SIGN_AUTHORIZATION: 0x34, // EIP-7702

  // Bitcoin 拡張公開鍵(CLA_V2)
  INS_V2_GET_EXTENDED_PUBKEY: 0x00, // 任意のパスの拡張公開鍵取得

  // Bitcoin PSBT(Openloop 独自)
  INS_BTC_SIGN_PSBT: 0x70,          // PSBT 送信
  INS_BTC_GET_SIGNED_PSBT: 0x71,    // 署名済み PSBT 取得
  INS_BTC_GET_WALLET_PUBLIC_KEY: 0x40, // BTC 公開鍵/アドレス取得
  INS_BTC_GET_ACCOUNT_XPUB: 0x72,   // アカウント xpub 取得
  INS_BTC_SIGN_MESSAGE: 0x73,       // BTC メッセージ署名

  // Solana(Ledger Solana 互換)
  INS_SOL_GET_PUBKEY: 0x05,         // Ed25519 公開鍵取得
  INS_SOL_SIGN_MESSAGE: 0x06,       // トランザクション/メッセージ署名
  INS_SOL_SIGN_OFFCHAIN_MSG: 0x07,  // オフチェーンメッセージ署名
} as const

SDK_BUILD_ID

const SDK_BUILD_ID: string   // 例: "@openloop/sdk-core@0.7.0"

同梱されている @openloop/sdk-core のバージョンを示す文字列です(@openloop/<パッケージ名>@<バージョン> の形)。配布物を開いてどのバージョンの SDK が入っているかを読むために使います。ソースを直接 import した場合(ビルドしていない場合)は "dev" になります。


ユーティリティ関数

// APDU コマンド構築(ISO 7816-4)。データが 255 バイト以下なら Short 形式
// [CLA][INS][P1][P2][Lc][Data...]、256 バイト以上なら Extended 形式
// [CLA][INS][P1][P2][0x00][Lc_HI][Lc_LO][Data...] を自動で選ぶ
function buildApdu(cla: number, ins: number, p1: number, p2: number, data?: Uint8Array): Uint8Array

// BIP32 パスをバッファに変換: [要素数 (1 byte)][各要素 4 byte ビッグエンディアン]。"m/" は省略可、強化は '。要素の検査はしない
function pathToBuffer(path: string): Uint8Array

// BIP32 パスを要素配列に変換(強化は 0x80000000 を加算)。例: "84'/0'/0'/0/0" -> [0x80000054, 0x80000000, 0x80000000, 0, 0]。要素の検査はしない
function pathToElements(path: string): number[]

// Uint8Array の結合
function concat(...arrays: Uint8Array[]): Uint8Array

// hex 文字列を Uint8Array に変換(0x プレフィックスあり/なし対応)。桁数・文字の検査はしない
function hexToBytes(hex: string): Uint8Array

// Uint8Array を hex 文字列に変換(0x プレフィックスなし)
function bytesToHex(bytes: Uint8Array): string

// Base58 エンコード/デコード(検査符号なし)。decode は不正な文字で Error
function base58Encode(bytes: Uint8Array): string
function base58Decode(str: string): Uint8Array

// OpenloopDeviceState を 1 行のログ用文字列に整形
// 例: "wallet=N lang=0 USB(native) ble=off FIDO PIV"
function formatOpenloopState(s: OpenloopDeviceState): string

hexToBytes / pathToBuffer / pathToElements は入力を検めません。検めた入力が必要なときは、呼び出し側で形式を確認してください。

BIP-32 の公開鍵導出

@openloop/sdk-core と @openloop/sdk-core/bitcoin の両方からエクスポートされます。公開鍵だけで非強化の子を導出し、拡張公開鍵(xpub / tpub)を読みます(秘密鍵は扱いません)。Ethereum・TRON のアドレスを getAddressWithChainCode の結果からソフトウェアで導出するときにも使えます。不正な入力には BtcError ではなく通常の Error を投げます。

const BIP32_VERSION_XPUB = 0x0488b21e   // 本番(xpub…)
const BIP32_VERSION_TPUB = 0x043587cf   // 試験網(tpub…)

interface ExtendedPublicKey {
  version: number          // BIP32_VERSION_XPUB か BIP32_VERSION_TPUB
  depth: number
  parentFingerprint: number
  childNumber: number      // この鍵自身の子の番号(強化なら 0x80000000 を含む)
  chainCode: string        // 32 バイト (hex)
  publicKey: string        // 33 バイトの圧縮公開鍵 (hex)
}

// 親の圧縮公開鍵(hex)と chain code(hex)から、非強化の子(index は 0x80000000 未満の整数)を導出する
function deriveChildPublicKey(
  parentPubkey: string, parentChainCode: string, index: number
): { publicKey: string; chainCode: string }

// 直列化された拡張公開鍵を解く。xpub / tpub 以外・曲線上にない点・秘密鍵入りは Error
function decodeExtendedPubkey(serialized: string): ExtendedPublicKey

// base58check(4 バイトの検査符号付き)。decode は検査符号を必ず照合する
function base58CheckDecode(str: string): Uint8Array
function base58CheckEncode(payload: Uint8Array): string

EIP-712 ストリーミング

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

// EIP-712 の内容そのものをデバイスへ順に送信して署名する(EthereumApp.signTypedDataStreaming の実体)
function signTypedDataStreaming(
  transport: ITransport, path: string, td: TypedDataLike, chainId: number
): Promise<SignatureResult>

// ペイロードが EIP-3009(TransferWithAuthorization / ReceiveWithAuthorization)の形か
function isEip3009(td: TypedDataLike): boolean

Solana のオフチェーンメッセージ

// メッセージを Solana のオフチェーンメッセージ形式で包む。format を省略すると、
// すべて印字可能な ASCII(0x20〜0x7E)なら 0、そうでなければ 1
function wrapSolanaOffchainMessage(message: Uint8Array, formatOverride?: 0 | 1 | 2): Uint8Array

// data が Solana オフチェーンメッセージの署名ドメインで始まるか
function isSolanaOffchainMessageWrapped(data: Uint8Array): boolean

// 署名ドメイン(16 バイト: 0xFF + "solana offchain")
const SOLANA_OFFCHAIN_SIGNING_DOMAIN: Uint8Array