全型定義・インターフェース・クラス・関数・定数の一覧です。明記がなければ
@openloop/sdk-core からエクスポートされています。
| import 元 | 内容 |
|---|---|
@openloop/sdk-core |
App クラス・Transport のインターフェース・エラー・定数・ユーティリティ |
@openloop/sdk-core/bitcoin |
Bitcoin の出口(アドレス導出・UTXO 取得・手数料見積り・PSBT
の組み立て・放送)。viem を読み込みません |
@openloop/sdk-core/verify |
署名検証(参照実装)。viem / @noble
を使います |
@openloop/sdk-core/bitcoin@openloop/sdk-core/verifyデバイスとの通信を抽象化するインターフェース。全 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() |
排他アクセスを解放 |
interface TransportStatus {
connected: boolean
deviceName?: string
deviceId?: string
}type TransportEventType = 'connected' | 'disconnected' | 'error'
interface TransportEvent {
type: TransportEventType
error?: Error
}
type TransportEventHandler = (event: TransportEvent) => voidOpenloopSDK.discover() が返す Transport 情報。
interface TransportInfo {
type: string // 登録名(例: 'webhid')
name: string // 表示名(例: 'WebHID (USB)')
available: boolean // 利用可能か
}type TransportFactory = () => Promise<ITransport>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 |
Bitcoin PSBT の入力ごとの BIP32 パス指定。
interface InputSigningPath {
index: number // PSBT の入力インデックス
path: string // BIP32 パス(例: "84'/1'/0'/0/5")
}XRP 署名結果。
interface XrpSignatureResult {
signature: string // 署名 hex (Ed25519: 64B, secp256k1: DER 可変長)
}type XrpCurve = 'ed25519' | 'secp256k1'Ethereum / TRON の ECDSA 署名結果。
interface SignatureResult {
v: number // リカバリー ID
r: string // R 値 (32 bytes hex, 0x プレフィックスなし)
s: string // S 値 (32 bytes hex, 0x プレフィックスなし)
}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 // デフォルト: "eip155:1"
}すべての 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 と同じ値)。
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 入出力) |
エラー
signPsbt / signPsbtHex /
signPsbtWithPaths: 署名済み PSBT が 64,000 バイト(250
バイト × 256 かたまり)を超えるときは
BtcError(code: 'psbt_too_large')を投げます。デバイスが拒否したときは
ApduError です。signPsbtWithPaths: 路の一覧は 1 つの APDU(250
バイト)に収まる必要があります。収まらないとき(5 段の路なら 12
本以上)は、デバイスに何も送らず
BtcError(code: 'bad_argument')を投げます。デバイスは路を最初の
1 フレームだけで受け、路は最大 10 本です(11 本でも 5 段の路なら 1
フレームに入るので、本数では断らずデバイスに送り、デバイスの拒否が
ApduError で返ります)。入力が 11 本以上のときは、PSBT
の各入力に BIP32_DERIVATION を含め(buildPsbt
は含めます)、路を付けずに signPsbtHex で署名します(PSBT
全体の入力はデバイスが最大 32 本まで読みます)。getExtendedPubkey: パスが 10
段を超えるとき、パスの要素が不正なとき、デバイスの応答が訊いたパスと食い違うとき(深さ・最後の段・バージョン)は、BtcError
ではなく通常の Error を投げます。ApduError、通信の失敗を TransportError
で投げます(エラークラス)。
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 = 10Bitcoin の送金を組み立てる一連の流れ(アドレス導出 → UTXO 取得 → PSBT → 署名 → 放送)は Bitcoin の出口を参照してください。
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) |
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 です。
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
のオフチェーンメッセージ)。
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
文字列です。
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>
}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 配置はファームウェア実装の詳細で、バージョンによって変わる可能性があります。 ホストアプリでは生値の保存・転送のみに使うことを推奨します |
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) {
// ウォレット未生成のときの分岐
}
}@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'設計の約束
BtcError の
code(英語のメッセージ付き)で返します。画面に表示する言葉は呼び出し側で
code から用意してください。fetch_failed で止まります(「残高
0」とは読み替えません)。端点への問い合わせ(共通)
BtcNetwork.esplora / feeEndpoints
は先頭(最良)から順に試します。1 つの URL への試行は
retryCount 回(既定 3 回・間隔 retryDelayMs =
1000
ミリ秒)で、すべて失敗したら次の端点へ進みます。各回の呼び出しは、応答の本文を読み切るまでを
timeoutMs の時間切れの中で数えます。fetch_failed
です。端点の配列が空のときは、fetchPrevTx と
getFeeRate は
bad_argument、fetchUtxos と
scanAccount は fetch_failed で投げます。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)と定数は、後の節にあります。
function pathOf(acct: BtcAccount, net: BtcNetworkId, change: 0 | 1, index: number): string
function accountPathOf(acct: BtcAccount, net: BtcNetworkId): stringpathOf は
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。
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 を投げます。
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 |
知らない網 |
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(空として続けません)。
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
の住所」を選ぶための口です。
fetch_failed)。net
に合わない住所は端点に訊く前に断ります(invalid_address/network_mismatch)。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。
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。
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。
function selectUtxos(
utxos: BtcUtxo[],
opts: {
to: BtcRecipient[]
feeRate: number
script: BtcScript
net: BtcNetworkId
excluded?: Iterable<string> // 使用済みの印 "txid:vout"
changeAddress?: string // 省略すると、お釣りは入力と同じ型として数える
}
): BtcSelection大きい順に UTXO を足して、手数料とお釣りを数えます(同額は
txid・vout
の順で、同じ入力から同じ結果になります)。
confirmed === true)の UTXO
だけを候補にします。自分のお釣りも確定するまで使いません。excluded に含まれる "txid:vout"
は候補から外します。DUST_SATS(546)未満になるときは、お釣りの出力を作らずに手数料へ回します(change
は 0、fee は入力合計 − 送金額)。このときは
buildPsbt に change を渡しません。MAX_ABSOLUTE_FEE_SATS(0.1
BTC)を超えるときは fee_too_high。送金額が
FEE_FRACTION_MIN_AMOUNT_SATS(1,000,000
sats)以上のときは、手数料が送金額を超えれば
fee_exceeds_amount。エラー
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 |
上記の手数料の門 |
function buildPsbt(args: {
script: BtcScript
net: BtcNetworkId
inputs: BtcPsbtInput[]
to: Array<{ address: string; value: number }>
change?: BtcChangeOutput // お釣りの出力を作らないときは渡さない
}): BtcPsbtPSBT v0(取引の version 2・locktime 0・全入力の sequence
0xfffffffd = RBF)を hex で返します。paths
はそのまま BitcoinApp.signPsbtWithPaths(psbtHex, paths)
に渡せます。
prevTxHex(前の取引の生の
hex。fetchPrevTx の返り値)が必須です。4,096
バイトを超えるとき、または hash が txid
と合わないときは失敗します。エラー:
入力・宛先がない、金額が正の整数でない、txid
が不正、script が不明、P2PKH の prevTxHex
がない・hash が合わない、公開鍵・パスが不正のときは
bad_argument。prevTxHex が 4,096
バイトを超えるときは
prev_tx_too_large。宛先・お釣りのアドレスが不正なら
invalid_address / network_mismatch。
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。
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)を渡してください。
expectedTxid
を返します(同じ取引を再放送した場合の正しい結果です。expectedTxid
を渡さないと失敗になります)。txid が expectedTxid と違うときは
bad_response を投げます。エラー: rawTxHex が hex でないときは
bad_argument。全端点が失敗したとき(到達不能・HTTP
エラー・txid でない応答・拒否)は
broadcast_rejected(fetch_failed
ではありません。メッセージに最後の端点の理由が入ります)。
function createAddressCache(): BtcAddressCacheアドレス → { path, publicKey }
の覚えを作ります。モジュールに 1
つではなく、呼び出し側が(網・型・アカウントごとなど)必要な数だけ作って持ちます。
| メンバー | 説明 |
|---|---|
get(address) / set(address, entry) |
覚えの参照・追加 |
syncToDevice(deviceId) |
持ち主のデバイスを宣言する。持ち主が変わったら全部捨てます(別のデバイスのアドレスを自分の物として扱わないため)。同じ
deviceId(null
以外)なら何もしません。null
は持ち主不明として、呼ぶたびに捨てます |
size |
覚えている件数 |
clear() |
全部捨てる |
function findAddressPath(
address: string,
xpub: BtcXpub,
acct: BtcAccount,
net: BtcNetworkId,
cache?: BtcAddressCache,
opts?: { maxIndex?: number; step?: number } // 既定 200 / 20
): string | nulladdress のパスを逆引きします。まず 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 }publicKeyToAddress: 圧縮公開鍵でない(66 桁の hex・先頭
02 / 03
でない)、知らない網・script は
bad_argument。estimateVbytes: 知らない script は
bad_argument。addressToScriptPubKey: invalid_address /
network_mismatch。encodeSegwitAddress(version は 0〜16)と
decodeSegwitAddress(チェックサム・大文字小文字の混在・長さ・padding
を検める)は、不正な入力に BtcError ではなく通常の
Error を投げます。@openloop/sdk-core/bitcoin
は、@openloop/sdk-core と同じ BIP-32
の関数・型(deriveChildPublicKey・decodeExtendedPubkey・base58CheckDecode・base58CheckEncode・BIP32_VERSION_XPUB・BIP32_VERSION_TPUB・ExtendedPublicKey)も再エクスポートしています(BIP-32 の公開鍵導出)。
// 手数料率(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 // 同時に走らせる取得の数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
}Promise<VerifyResult>)です。BTC・SOL・TRON・XRP
の関数は同期で VerifyResult を返します。expected
を渡すと、復元した署名者と一致するか(ok)まで判定する。EVM
で expected を渡さないと ok は常に
true(復元したアドレスを recovered
で取得できる)。verifyBtcMessage
は、署名から公開鍵を復元できないとき、投げずに
{ recovered: '(unrecoverable)', ok: false }
を返します。verifyXrpSignedTx は SigningPubKey / TxnSignature
が見つからない、または未対応のフィールドがあるとき、verifyBtcPsbt
は PSBT として読めないときに Error を投げます。verifyBtcPsbt が投げるのは「PSBT
として読めない」ときだけです。署名が合わない・公開鍵が使う出力と結ばない・NON_WITNESS_UTXO
が入力の txid と一致しない、といった検証の失敗はすべて
ok: false で返り、理由は recovered
に入ります。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')で判別します)。
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() |
登録をすべて削除する |
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')を投げます。
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)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 = {
// クラスバイト
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 constconst 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): stringhexToBytes / pathToBuffer /
pathToElements
は入力を検めません。検めた入力が必要なときは、呼び出し側で形式を確認してください。
@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): stringinterface 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 のオフチェーンメッセージ形式で包む。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