From bdb6050d05da7f974aef42f932784c55b4996d3a Mon Sep 17 00:00:00 2001 From: benma's agent Date: Wed, 22 Jul 2026 08:49:36 +0000 Subject: [PATCH] Add Bitcoin API support Port the Bitcoin API, PSBT signing flow, and sandbox actions from bitbox-api-rs. Use bitcoinjs-lib for PSBT parsing, signature checks, and finalization in simulator tests. Include the upstream transaction vectors across supported firmware versions. Bump the package version to 0.3.0. --- CHANGELOG.md | 3 + CONTRIBUTING.md | 7 +- README.md | 96 +- package-lock.json | 138 +- package.json | 8 +- sandbox/src/App.tsx | 4 + sandbox/src/Bitcoin.tsx | 495 ++ src/index.ts | 129 +- src/internal/btc/config.ts | 264 + src/internal/btc/methods.ts | 494 ++ src/internal/btc/psbt.ts | 559 +++ src/internal/btc/query.ts | 57 + src/internal/errors.ts | 20 +- src/internal/eth/antiklepto.ts | 27 +- test/btc-methods.test.ts | 147 + test/btc-psbt.test.ts | 111 + test/data/btc-transaction-test-vectors.json | 4858 +++++++++++++++++++ test/error-codes.test.ts | 18 + test/lifecycle.test.ts | 4 +- test/simulator-btc.test.ts | 539 ++ test/simulator-util.ts | 277 +- 21 files changed, 8151 insertions(+), 104 deletions(-) create mode 100644 sandbox/src/Bitcoin.tsx create mode 100644 src/internal/btc/config.ts create mode 100644 src/internal/btc/methods.ts create mode 100644 src/internal/btc/psbt.ts create mode 100644 src/internal/btc/query.ts create mode 100644 test/btc-methods.test.ts create mode 100644 test/btc-psbt.test.ts create mode 100644 test/data/btc-transaction-test-vectors.json create mode 100644 test/simulator-btc.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d7b87..f759185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.3.0 +- Add Bitcoin APIs, sandbox actions, and simulator transaction-vector coverage + ## 0.2.0 - Add Cardano support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b762af..eb9cae5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,7 @@ npm run test:sim On Linux x64, the harness downloads and caches the simulator binaries listed in `test/simulators.json` under `test/simulators/`, verifies their sha256 hashes, -and starts the newest listed simulator by default. +and runs the test matrix against every listed firmware version. Useful environment variables: @@ -92,8 +92,9 @@ The package is intended as a drop-in TypeScript replacement for the current - Public errors must keep the `{ code, message, err? }` shape. New public error behavior should go through `src/internal/errors.ts` and match the compatibility taxonomy unless there is an explicit reason to extend it. -- BTC and BIP85 methods remain compatibility stubs in this iteration. Do not - document or test them as implemented until their protocol support lands. +- Bitcoin xpub, address, script config, PSBT, and message-signing support is + implemented; keep it aligned with the Rust/WASM reference and simulator + vectors. BIP85 methods remain compatibility stubs. - Cardano xpub, address, and transaction-signing support is implemented; keep it aligned with the Rust/WASM reference behavior and simulator vectors. diff --git a/README.md b/README.md index 12bd9dd..0e7afc2 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,14 @@ Pure TypeScript library for integrating BitBox02 hardware wallets in browser applications. -`@bitboxswiss/bitbox-api` is source-compatible with the current `bitbox-api` -Rust/WASM package for the implemented surface, but it does not ship WASM and -does not require a WASM init step. - ## Status - **Implemented:** WebHID and BitBoxBridge transports, Noise XX pairing, - device metadata helpers, Ethereum xpub/address/signing methods, antiklepto, - transaction data streaming, EIP-712 typed messages, `ethIdentifyCase()`, - and Cardano xpub/address/signing methods. -- **Stubbed with `code: 'unsupported'`:** BTC and BIP85 methods. + device metadata helpers, Bitcoin xpub/address/PSBT/message signing methods, + script config registration, Ethereum xpub/address/signing methods, + antiklepto, transaction data streaming, EIP-712 typed messages, + `ethIdentifyCase()`, and Cardano xpub/address/signing methods. +- **Stubbed with `code: 'unsupported'`:** BIP85 methods. - **Stubbed with `code: 'not-implemented'`:** `showMnemonic()` and `changePassword()`. @@ -25,6 +22,8 @@ npm install @bitboxswiss/bitbox-api `@noble/ciphers`, `@noble/curves`, and `@noble/hashes` are peer dependencies so the library does not perturb the crypto dependency graph of wallet/Web3 apps. +`bitcoinjs-lib`, used to parse and update PSBTs, is installed as a regular +dependency. - **npm 7+** installs peer dependencies automatically. - **pnpm/yarn** peer handling depends on your package-manager version and @@ -37,23 +36,6 @@ the library does not perturb the crypto dependency graph of wallet/Web3 apps. The package is ESM-only. -## Migrating from `bitbox-api` - -For currently implemented methods, the intended migration is just the import -name: - -```ts -import * as bitbox from '@bitboxswiss/bitbox-api'; -``` - -There is no `init()` call and no WASM loader. Existing Webpack/Vite WASM plugin -configuration from `bitbox-api` is not needed for this package. - -BTC and BIP85 methods are still present in the public type surface for -compatibility, but currently reject with typed errors as listed in -[Status](#status). Ethereum, Cardano, and the general device helpers listed -above are wired through the TypeScript transport. - ## Browser Requirements - WebHID works in Chromium-based browsers in a secure context, such as HTTPS or @@ -111,6 +93,48 @@ closed. Reconnect before retrying. Call `bb02.close()` when your app is done with the device. `close()` is idempotent and invokes the `onClose` callback supplied to the connect function. +## Bitcoin Usage + +Bitcoin-family methods accept coin, keypath, script config, and xpub type +strings. Keypaths can be strings or number arrays. + +```ts +const account = "m/84'/0'/0'"; +const keypath = `${account}/0/0`; + +const xpub = await bb02.btcXpub('btc', account, 'xpub', false); +const address = await bb02.btcAddress( + 'btc', + keypath, + { simpleType: 'p2wpkh' }, + true, +); +``` + +`btcSignPSBT()` accepts and returns a base64-encoded PSBT. Single-signature +P2WPKH, wrapped P2WPKH, and P2TR script configs are inferred from PSBT key +origins. Pass `force_script_config` for multisig and policy wallets, which must +first be registered on the device. + +```ts +const signedPsbt = await bb02.btcSignPSBT( + 'btc', + psbtBase64, + undefined, + 'default', +); + +const messageSignature = await bb02.btcSignMessage( + 'btc', + { scriptConfig: { simpleType: 'p2wpkh' }, keypath }, + new TextEncoder().encode('hello bitbox'), +); +``` + +The returned PSBT contains the device signatures but is not finalized. Use a +Bitcoin library to validate, combine, or finalize it as appropriate for the +wallet. + ## Ethereum Usage Keypaths can be strings such as `m/44'/60'/0'/0/0` or number arrays. Chain IDs @@ -289,11 +313,12 @@ Common client-facing codes include: - `user-abort` / `bitbox-user-abort`: the user cancelled in the browser or on the device. - `invalid-type`, `keypath-parse`, `chain-id-too-large`: invalid host inputs. +- `psbt-parse` and `psbt-*`: malformed PSBTs or unsupported PSBT contents. +- `btc-sign`: an invalid or incomplete Bitcoin signing exchange. - `communication`, `noise`, `noise-config`, `pairing-rejected`: transport, pairing, or encrypted-channel failures. - `version`: the connected firmware is too old for the requested method. -- `unsupported` / `not-implemented`: public compatibility methods that are not - wired in this TypeScript iteration. +- `unsupported` / `not-implemented`: methods that are not currently available. ## Sandbox and Development @@ -307,13 +332,14 @@ make sandbox-dev Open the printed Vite URL, usually `http://localhost:5173`. -For build, test, simulator, protobuf, and contribution workflow details, see -[CONTRIBUTING.md](CONTRIBUTING.md). +The Bitcoin accordion covers xpubs, addresses, script config registration, +PSBT signing, and message signing. Bitcoin unit tests are part of +`npm run build && npm test`; the firmware transaction-vector suite runs against +the simulator matrix with: -## API Compatibility +```bash +npm run test:sim -- test/simulator-btc.test.ts +``` -`test/api-snapshot.test.ts` compares the built TypeScript declarations against -`../bitbox-api-rs/pkg/bitbox_api.d.ts` when the reference checkout is present. -The guard keeps the public surface drop-in compatible while still allowing -deliberate source-compatible TypeScript widenings, such as optional close -callbacks and safer `bigint` chain ID paths. +For build, test, simulator, protobuf, and contribution workflow details, see +[CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/package-lock.json b/package-lock.json index f16de72..7f5b5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "@bitboxswiss/bitbox-api", - "version": "0.1.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitboxswiss/bitbox-api", - "version": "0.1.1", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { - "@bufbuild/protobuf": "2.11.0" + "@bufbuild/protobuf": "2.11.0", + "bitcoinjs-lib": "7.0.1", + "bs58check": "4.0.0" }, "devDependencies": { "@bufbuild/buf": "1.68.3", @@ -27,6 +29,7 @@ "eslint": "10.2.1", "react": "18.3.1", "react-dom": "18.3.1", + "tiny-secp256k1": "2.2.4", "typescript": "6.0.3", "typescript-eslint": "8.59.0", "vite": "6.4.2", @@ -931,7 +934,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2110,6 +2112,49 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bip174": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz", + "integrity": "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.1.tgz", + "integrity": "sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^1.2.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -2123,6 +2168,25 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3066,6 +3130,29 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3140,7 +3227,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3174,6 +3261,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -3191,6 +3287,38 @@ "punycode": "^2.1.0" } }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", diff --git a/package.json b/package.json index 326d60c..4a710fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitboxswiss/bitbox-api", - "version": "0.2.0", + "version": "0.3.0", "description": "A pure TypeScript library to interact with BitBox hardware wallets.", "license": "Apache-2.0", "type": "module", @@ -52,6 +52,7 @@ "eslint": "10.2.1", "react": "18.3.1", "react-dom": "18.3.1", + "tiny-secp256k1": "2.2.4", "typescript": "6.0.3", "typescript-eslint": "8.59.0", "vite": "6.4.2", @@ -61,6 +62,7 @@ "bitbox", "bitbox02", "hardware-wallet", + "bitcoin", "ethereum", "typescript" ], @@ -70,7 +72,9 @@ }, "homepage": "https://bitbox.swiss/", "dependencies": { - "@bufbuild/protobuf": "2.11.0" + "@bufbuild/protobuf": "2.11.0", + "bitcoinjs-lib": "7.0.1", + "bs58check": "4.0.0" }, "peerDependencies": { "@noble/ciphers": "^1.3.0", diff --git a/sandbox/src/App.tsx b/sandbox/src/App.tsx index 5548e06..1cd050f 100644 --- a/sandbox/src/App.tsx +++ b/sandbox/src/App.tsx @@ -5,6 +5,7 @@ import * as bitbox from '@bitboxswiss/bitbox-api'; import './App.css'; import { Cardano } from './Cardano'; +import { Bitcoin } from './Bitcoin'; import { Ethereum } from './Ethereum'; import { General } from './General'; import { ErrorNotification } from './ErrorNotification'; @@ -89,6 +90,9 @@ function App() { + + + {bb02.ethSupported() && ( diff --git a/sandbox/src/Bitcoin.tsx b/sandbox/src/Bitcoin.tsx new file mode 100644 index 0000000..f36d19d --- /dev/null +++ b/sandbox/src/Bitcoin.tsx @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { type FormEvent, useState } from 'react'; +import * as bitbox from '@bitboxswiss/bitbox-api'; + +import { ErrorNotification } from './ErrorNotification'; +import { ResultBlock, formatResult } from './form-utils'; + +type Props = { bb02: bitbox.PairedBitBox }; + +const BTC_COINS: bitbox.BtcCoin[] = ['btc', 'tbtc', 'ltc', 'tltc', 'rbtc']; +const XPUB_TYPES: bitbox.XPubType[] = [ + 'tpub', 'xpub', 'ypub', 'zpub', 'vpub', 'upub', 'Vpub', 'Zpub', 'Upub', 'Ypub', +]; +const SIMPLE_TYPES: bitbox.BtcSimpleType[] = ['p2wpkhP2sh', 'p2wpkh', 'p2tr']; + +function ErrorView({ + error, + clear, +}: { + error: bitbox.Error | undefined; + clear: () => void; +}) { + return error === undefined + ? null + : ; +} + +function CoinSelect({ + coin, + setCoin, +}: { + coin: bitbox.BtcCoin; + setCoin: (coin: bitbox.BtcCoin) => void; +}) { + return ( + + ); +} + +function BtcXPub({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [keypath, setKeypath] = useState("m/84'/0'/0'"); + const [xpubType, setXpubType] = useState('xpub'); + const [display, setDisplay] = useState(true); + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + setResult(await bb02.btcXpub(coin, keypath, xpubType, display)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( +
+

XPub

+
+ + + + + + + setError(undefined)} /> + +
+ ); +} + +function BtcXPubs({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [keypaths, setKeypaths] = useState( + `["m/49'/0'/0'", "m/84'/0'/0'", "m/86'/0'/0'"]`, + ); + const [xpubType, setXpubType] = useState('xpub'); + const [result, setResult] = useState(); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(undefined); + setError(undefined); + try { + setResult(await bb02.btcXpubs( + coin, + JSON.parse(keypaths) as bitbox.Keypath[], + xpubType, + )); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( +
+

Multiple XPubs

+
+ + +