Skip to content

Commit f730584

Browse files
authored
Merge pull request #13 from btc-vision/phase/2-7.x
Phase/2 7.x
2 parents d503a42 + dfd631b commit f730584

63 files changed

Lines changed: 5246 additions & 3353 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 536 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
{
22
"name": "@btc-vision/bitcoin",
33
"type": "module",
4-
"version": "7.0.0-alpha.1",
4+
"version": "7.0.0-alpha.2",
55
"sideEffects": false,
6-
"overrides": {
7-
"ecpair": {
8-
"valibot": "^1.2.0"
9-
}
10-
},
116
"description": "Client-side Bitcoin JavaScript library",
127
"engines": {
138
"node": ">=24.0.0"
@@ -33,6 +28,21 @@
3328
"import": "./browser/index.js",
3429
"default": "./browser/index.js"
3530
},
31+
"./workers": {
32+
"browser": {
33+
"types": "./browser/workers/index.d.ts",
34+
"import": "./browser/workers/index.js",
35+
"default": "./browser/workers/index.js"
36+
},
37+
"node": {
38+
"types": "./build/workers/index.node.d.ts",
39+
"import": "./build/workers/index.node.js",
40+
"default": "./build/workers/index.node.js"
41+
},
42+
"types": "./build/workers/index.d.ts",
43+
"import": "./build/workers/index.js",
44+
"default": "./build/workers/index.js"
45+
},
3646
"./address": {
3747
"types": "./build/address.d.ts",
3848
"import": "./build/address.js"
@@ -89,6 +99,8 @@
8999
"browser": {
90100
"./build/index.js": "./browser/index.js",
91101
"./build/index.d.ts": "./browser/index.d.ts",
102+
"./build/workers/index.node.js": "./browser/workers/index.js",
103+
"./build/workers/index.node.d.ts": "./browser/workers/index.d.ts",
92104
"Buffer": "buffer",
93105
"crypto": "./src/crypto/crypto-browser.js",
94106
"stream": "stream-browserify",
@@ -132,6 +144,8 @@
132144
"test": "npm run build && npm run lint && vitest run",
133145
"test:watch": "vitest",
134146
"unit": "vitest run",
147+
"test:browser": "vitest run --config vitest.config.browser.ts",
148+
"test:browser:watch": "vitest --config vitest.config.browser.ts",
135149
"bench": "vitest bench benchmark/",
136150
"bench:run": "npx tsx benchmark/signing.bench.ts",
137151
"check:circular": "madge --circular --extensions ts,tsx src/",
@@ -143,19 +157,21 @@
143157
"@types/bs58check": "^3.0.1",
144158
"@types/node": "^25.0.10",
145159
"@types/randombytes": "^2.0.3",
160+
"@vitest/browser": "^4.0.18",
161+
"@vitest/browser-playwright": "^4.0.18",
146162
"@vitest/coverage-v8": "^4.0.18",
147163
"better-npm-audit": "^3.11.0",
148164
"bip39": "^3.1.0",
149165
"bip65": "^1.0.3",
150166
"bip68": "^1.0.4",
151167
"bs58": "^6.0.0",
152168
"dhttp": "^3.0.3",
153-
"ecpair": "^3.0.0",
154169
"eslint": "^9.39.2",
155170
"https-browserify": "^1.0.0",
156171
"madge": "^8.0.0",
157172
"minimaldata": "^1.0.2",
158173
"os-browserify": "^0.3.0",
174+
"playwright": "^1.58.0",
159175
"prettier": "^3.8.1",
160176
"randombytes": "^2.1.0",
161177
"regtest-client": "0.2.1",
@@ -171,10 +187,13 @@
171187
"vite": "^7.3.1",
172188
"vite-plugin-dts": "^4.5.4",
173189
"vite-plugin-node-polyfills": "^0.25.0",
190+
"vite-plugin-top-level-await": "^1.6.0",
191+
"vite-plugin-wasm": "^3.5.0",
174192
"vitest": "^4.0.18"
175193
},
176194
"dependencies": {
177-
"@btc-vision/bip32": "^6.1.0",
195+
"@btc-vision/bip32": "^7.0.1",
196+
"@btc-vision/ecpair": "^4.0.1",
178197
"@btc-vision/logger": "^1.0.8",
179198
"@noble/hashes": "^2.0.1",
180199
"@noble/secp256k1": "^3.0.0",
@@ -187,4 +206,4 @@
187206
"process": "^0.11.10",
188207
"varuint-bitcoin": "^2.0.0"
189208
}
190-
}
209+
}

src/address.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
type Bytes20,
2626
isBytes20,
2727
isUInt8,
28-
type Script,
2928
toBytes20,
3029
toBytes32,
3130
type XOnlyPublicKey,
@@ -188,27 +187,51 @@ export function toBech32(
188187
}
189188

190189
/**
191-
* decode address from output script with network, return address if matched
190+
* decode address from output script with network, return address if matched.
191+
*
192+
* Uses fast byte-pattern matching for common script types (P2PKH, P2SH,
193+
* P2WPKH, P2WSH, P2TR) to avoid constructing payment objects and catching
194+
* exceptions. Falls back to payment constructors for exotic types.
192195
*/
193196
export function fromOutputScript(output: Uint8Array, network?: Network): string {
194-
// TODO: Network
195197
network = network || networks.bitcoin;
198+
const len = output.length;
199+
200+
// P2PKH: OP_DUP(0x76) OP_HASH160(0xa9) 0x14 <20-byte hash> OP_EQUALVERIFY(0x88) OP_CHECKSIG(0xac)
201+
if (
202+
len === 25 &&
203+
output[0] === 0x76 &&
204+
output[1] === 0xa9 &&
205+
output[2] === 0x14 &&
206+
output[23] === 0x88 &&
207+
output[24] === 0xac
208+
) {
209+
return toBase58Check(output.subarray(3, 23) as Bytes20, network.pubKeyHash);
210+
}
196211

197-
try {
198-
return p2pkh({ output: output as Script, network }).address as string;
199-
} catch (e) {}
200-
try {
201-
return p2sh({ output: output as Script, network }).address as string;
202-
} catch (e) {}
203-
try {
204-
return p2wpkh({ output: output as Script, network }).address as string;
205-
} catch (e) {}
206-
try {
207-
return p2wsh({ output: output as Script, network }).address as string;
208-
} catch (e) {}
209-
try {
210-
return p2tr({ output: output as Script, network }).address as string;
211-
} catch (e) {}
212+
// P2SH: OP_HASH160(0xa9) 0x14 <20-byte hash> OP_EQUAL(0x87)
213+
if (len === 23 && output[0] === 0xa9 && output[1] === 0x14 && output[22] === 0x87) {
214+
return toBase58Check(output.subarray(2, 22) as Bytes20, network.scriptHash);
215+
}
216+
217+
// P2WPKH: OP_0(0x00) 0x14 <20-byte hash>
218+
if (len === 22 && output[0] === 0x00 && output[1] === 0x14) {
219+
return toBech32(output.subarray(2, 22), 0, network.bech32);
220+
}
221+
222+
// P2WSH: OP_0(0x00) 0x20 <32-byte hash>
223+
if (len === 34 && output[0] === 0x00 && output[1] === 0x20) {
224+
return toBech32(output.subarray(2, 34), 0, network.bech32);
225+
}
226+
227+
// P2TR: OP_1(0x51) 0x20 <32-byte x-only pubkey>
228+
if (len === 34 && output[0] === 0x51 && output[1] === 0x20) {
229+
const words = bech32m.toWords(output.subarray(2, 34));
230+
words.unshift(1);
231+
return bech32m.encode(network.bech32, words);
232+
}
233+
234+
// Fallback for exotic types
212235
try {
213236
return toFutureOPNetAddress(output, network);
214237
} catch (e) {}

src/branded.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
/**
22
* Branded type definitions for type-safe primitives.
33
*
4+
* Re-exported from @btc-vision/ecpair to ensure cross-package type compatibility.
5+
*
46
* @packageDocumentation
57
*/
6-
7-
declare const __brand: unique symbol;
8-
type Brand<T, B extends string> = T & { readonly [__brand]: B };
9-
10-
export type Bytes32 = Brand<Uint8Array, 'Bytes32'>;
11-
export type Bytes20 = Brand<Uint8Array, 'Bytes20'>;
12-
export type PublicKey = Brand<Uint8Array, 'PublicKey'>;
13-
export type XOnlyPublicKey = Brand<Uint8Array, 'XOnlyPublicKey'>;
14-
export type Satoshi = Brand<bigint, 'Satoshi'>;
15-
export type PrivateKey = Brand<Uint8Array, 'PrivateKey'>;
16-
export type Signature = Brand<Uint8Array, 'Signature'>;
17-
export type SchnorrSignature = Brand<Uint8Array, 'SchnorrSignature'>;
18-
export type Script = Brand<Uint8Array, 'Script'>;
8+
export type {
9+
Brand,
10+
Bytes32,
11+
Bytes20,
12+
PublicKey,
13+
XOnlyPublicKey,
14+
Satoshi,
15+
PrivateKey,
16+
Signature,
17+
SchnorrSignature,
18+
MessageHash,
19+
Script,
20+
} from '@btc-vision/ecpair';

src/ecc/context.ts

Lines changed: 75 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export class EccContext {
5959
* ```
6060
*/
6161
static init(lib: EccLib): EccContext {
62+
// Skip re-verification if same lib is already initialized
63+
if (EccContext.#instance && EccContext.#instance.#lib === lib) {
64+
return EccContext.#instance;
65+
}
6266
verifyEcc(lib);
6367
EccContext.#instance = new EccContext(lib);
6468
return EccContext.#instance;
@@ -177,32 +181,64 @@ export function getEccLib(): EccLib {
177181
// ============================================================================
178182

179183
interface TweakAddVector {
180-
pubkey: string;
181-
tweak: string;
184+
pubkey: Uint8Array;
185+
tweak: Uint8Array;
182186
parity: 0 | 1 | -1;
183-
result: string | null;
187+
result: Uint8Array | null;
188+
}
189+
190+
// Lazily decoded test vectors (decoded once on first verification)
191+
let _tweakVectors: TweakAddVector[] | undefined;
192+
let _validPoints: Uint8Array[] | undefined;
193+
let _invalidPoints: Uint8Array[] | undefined;
194+
195+
function getTweakVectors(): TweakAddVector[] {
196+
if (!_tweakVectors) {
197+
_tweakVectors = [
198+
{
199+
pubkey: fromHex('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
200+
tweak: fromHex('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'),
201+
parity: -1,
202+
result: null,
203+
},
204+
{
205+
pubkey: fromHex('1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b'),
206+
tweak: fromHex('a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac'),
207+
parity: 1,
208+
result: fromHex('e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf'),
209+
},
210+
{
211+
pubkey: fromHex('2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),
212+
tweak: fromHex('823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47'),
213+
parity: 0,
214+
result: fromHex('9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c'),
215+
},
216+
];
217+
}
218+
return _tweakVectors;
184219
}
185220

186-
const TWEAK_ADD_VECTORS: TweakAddVector[] = [
187-
{
188-
pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
189-
tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
190-
parity: -1,
191-
result: null,
192-
},
193-
{
194-
pubkey: '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b',
195-
tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac',
196-
parity: 1,
197-
result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf',
198-
},
199-
{
200-
pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991',
201-
tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47',
202-
parity: 0,
203-
result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c',
204-
},
205-
];
221+
function getValidPoints(): Uint8Array[] {
222+
if (!_validPoints) {
223+
_validPoints = [
224+
fromHex('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
225+
fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'),
226+
fromHex('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'),
227+
fromHex('0000000000000000000000000000000000000000000000000000000000000001'),
228+
];
229+
}
230+
return _validPoints;
231+
}
232+
233+
function getInvalidPoints(): Uint8Array[] {
234+
if (!_invalidPoints) {
235+
_invalidPoints = [
236+
fromHex('0000000000000000000000000000000000000000000000000000000000000000'),
237+
fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'),
238+
];
239+
}
240+
return _invalidPoints;
241+
}
206242

207243
/**
208244
* Verifies that the ECC library implementation is correct.
@@ -216,29 +252,17 @@ function verifyEcc(ecc: EccLib): void {
216252
throw new Error('ECC library missing isXOnlyPoint function');
217253
}
218254

219-
// Test isXOnlyPoint with valid points
220-
const validPoints = [
221-
'79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
222-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e',
223-
'f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
224-
'0000000000000000000000000000000000000000000000000000000000000001',
225-
];
226-
227-
for (const hex of validPoints) {
228-
if (!ecc.isXOnlyPoint(fromHex(hex))) {
229-
throw new Error(`ECC library isXOnlyPoint failed for valid point: ${hex}`);
255+
// Test isXOnlyPoint with valid points (pre-decoded)
256+
for (const point of getValidPoints()) {
257+
if (!ecc.isXOnlyPoint(point)) {
258+
throw new Error('ECC library isXOnlyPoint failed for a valid point');
230259
}
231260
}
232261

233-
// Test isXOnlyPoint with invalid points
234-
const invalidPoints = [
235-
'0000000000000000000000000000000000000000000000000000000000000000',
236-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
237-
];
238-
239-
for (const hex of invalidPoints) {
240-
if (ecc.isXOnlyPoint(fromHex(hex))) {
241-
throw new Error(`ECC library isXOnlyPoint should reject invalid point: ${hex}`);
262+
// Test isXOnlyPoint with invalid points (pre-decoded)
263+
for (const point of getInvalidPoints()) {
264+
if (ecc.isXOnlyPoint(point)) {
265+
throw new Error('ECC library isXOnlyPoint should reject invalid point');
242266
}
243267
}
244268

@@ -247,33 +271,27 @@ function verifyEcc(ecc: EccLib): void {
247271
throw new Error('ECC library missing xOnlyPointAddTweak function');
248272
}
249273

250-
for (const vector of TWEAK_ADD_VECTORS) {
274+
for (const vector of getTweakVectors()) {
251275
const result = ecc.xOnlyPointAddTweak(
252-
fromHex(vector.pubkey) as XOnlyPublicKey,
253-
fromHex(vector.tweak) as Bytes32,
276+
vector.pubkey as XOnlyPublicKey,
277+
vector.tweak as Bytes32,
254278
);
255279

256280
if (vector.result === null) {
257281
if (result !== null) {
258282
throw new Error(
259-
`ECC library xOnlyPointAddTweak should return null for: ${vector.pubkey}`,
283+
'ECC library xOnlyPointAddTweak should return null for test vector',
260284
);
261285
}
262286
} else {
263287
if (result === null) {
264-
throw new Error(
265-
`ECC library xOnlyPointAddTweak returned null unexpectedly for: ${vector.pubkey}`,
266-
);
288+
throw new Error('ECC library xOnlyPointAddTweak returned null unexpectedly');
267289
}
268290
if (result.parity !== vector.parity) {
269-
throw new Error(
270-
`ECC library xOnlyPointAddTweak parity mismatch for: ${vector.pubkey}`,
271-
);
291+
throw new Error('ECC library xOnlyPointAddTweak parity mismatch');
272292
}
273-
if (!equals(result.xOnlyPubkey, fromHex(vector.result))) {
274-
throw new Error(
275-
`ECC library xOnlyPointAddTweak result mismatch for: ${vector.pubkey}`,
276-
);
293+
if (!equals(result.xOnlyPubkey, vector.result)) {
294+
throw new Error('ECC library xOnlyPointAddTweak result mismatch');
277295
}
278296
}
279297
}

0 commit comments

Comments
 (0)