@sequence0/sdk 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,982 @@
1
+ "use strict";
2
+ /**
3
+ * Bitcoin Taproot (P2TR) Chain Adapter
4
+ *
5
+ * Full Taproot transaction lifecycle: address derivation, UTXO management,
6
+ * transaction building, FROST Schnorr signing, and broadcast.
7
+ *
8
+ * FROST-secp256k1 produces BIP-340 compatible Schnorr signatures that are
9
+ * NATIVE to Taproot key-path spends -- no signature format conversion needed.
10
+ *
11
+ * Depends on the WASM crate's bitcoin.rs for:
12
+ * - Address derivation (group pubkey -> bc1p... P2TR address)
13
+ * - Transaction construction (inputs/outputs -> unsigned TX + sighash)
14
+ * - Signature attachment (FROST sig -> witness data)
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { BitcoinTaprootAdapter } from '@sequence0/sdk';
19
+ *
20
+ * const btc = new BitcoinTaprootAdapter({ network: 'mainnet' });
21
+ *
22
+ * // Derive a Taproot address from a FROST group public key
23
+ * const addr = btc.deriveAddress('02abcdef...');
24
+ * console.log(addr.address); // bc1p...
25
+ *
26
+ * // Get balance and UTXOs
27
+ * const balance = await btc.getBalance('bc1p...');
28
+ * const utxos = await btc.getUTXOs('bc1p...');
29
+ *
30
+ * // Build, sign, and broadcast
31
+ * const unsignedTx = await btc.buildTransaction(
32
+ * { to: 'bc1p...recipient', amount: 50000, feeRate: 15 },
33
+ * 'bc1p...sender'
34
+ * );
35
+ * // ... sign via FROST ...
36
+ * const txid = await btc.broadcast(signedTxHex);
37
+ * ```
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.BitcoinTaprootAdapter = void 0;
41
+ exports.createBitcoinTaprootAdapter = createBitcoinTaprootAdapter;
42
+ exports.createBitcoinTestnetTaprootAdapter = createBitcoinTestnetTaprootAdapter;
43
+ const errors_1 = require("../utils/errors");
44
+ // ── Constants ──
45
+ const DEFAULT_APIS = {
46
+ mainnet: 'https://mempool.space/api',
47
+ testnet: 'https://mempool.space/testnet/api',
48
+ signet: 'https://mempool.space/signet/api',
49
+ regtest: 'http://localhost:3000/api', // Local mempool instance
50
+ };
51
+ /** Minimum output value in satoshis (dust limit for P2TR) */
52
+ const DUST_THRESHOLD = 546;
53
+ /** Estimated vbytes per Taproot key-path input */
54
+ const VBYTES_PER_INPUT = 58;
55
+ /** Estimated vbytes per P2TR output */
56
+ const VBYTES_PER_OUTPUT = 43;
57
+ /** Fixed overhead vbytes per transaction */
58
+ const TX_OVERHEAD_VBYTES = 11;
59
+ // ── Adapter ──
60
+ class BitcoinTaprootAdapter {
61
+ constructor(options = {}) {
62
+ this.network = options.network || 'mainnet';
63
+ this.apiUrl = options.apiUrl || DEFAULT_APIS[this.network];
64
+ }
65
+ getRpcUrl() {
66
+ return this.apiUrl;
67
+ }
68
+ // ────────────────────────────────────────────────
69
+ // Address Derivation
70
+ // ────────────────────────────────────────────────
71
+ /**
72
+ * Derive a Taproot (P2TR) address from a FROST group public key.
73
+ *
74
+ * The FROST group verifying key is a secp256k1 point. For Taproot:
75
+ * 1. Extract the x-only public key (32 bytes)
76
+ * 2. Compute the Taproot tweak: t = hash_TapTweak(x_only_pubkey)
77
+ * 3. Compute the output key: Q = P + t*G
78
+ * 4. Encode as Bech32m with witness version 1
79
+ *
80
+ * @param groupPubkeyHex - Hex-encoded FROST group verifying key (33 bytes compressed or 32 bytes x-only)
81
+ * @returns TaprootAddressInfo with address, keys, and scriptPubkey
82
+ */
83
+ deriveAddress(groupPubkeyHex) {
84
+ const pubkeyClean = groupPubkeyHex.startsWith('0x')
85
+ ? groupPubkeyHex.slice(2)
86
+ : groupPubkeyHex;
87
+ const pubkeyBytes = hexToBytes(pubkeyClean);
88
+ // Extract x-only public key
89
+ const xOnly = extractXOnlyPubkey(pubkeyBytes);
90
+ // Compute Taproot tweak (key-path only, no script tree)
91
+ const tweak = computeTapTweak(xOnly);
92
+ // Compute tweaked output key
93
+ const outputKey = tweakPublicKey(xOnly, tweak);
94
+ // Encode as Bech32m
95
+ const hrp = networkToHrp(this.network);
96
+ const address = encodeBech32m(hrp, outputKey);
97
+ // Build scriptPubKey: OP_1 <32-byte output key>
98
+ const scriptPubkey = '51' + '20' + bytesToHex(outputKey);
99
+ return {
100
+ address,
101
+ xOnlyPubkey: bytesToHex(xOnly),
102
+ outputKey: bytesToHex(outputKey),
103
+ scriptPubkey,
104
+ network: this.network,
105
+ };
106
+ }
107
+ // ────────────────────────────────────────────────
108
+ // UTXO Management
109
+ // ────────────────────────────────────────────────
110
+ /**
111
+ * Fetch unspent transaction outputs (UTXOs) for a Taproot address.
112
+ *
113
+ * @param address - Bech32m Taproot address (bc1p... / tb1p...)
114
+ * @returns Array of UTXOs sorted by value (largest first)
115
+ */
116
+ async getUTXOs(address) {
117
+ try {
118
+ const response = await fetch(`${this.apiUrl}/address/${address}/utxo`);
119
+ if (!response.ok) {
120
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
121
+ }
122
+ const rawUtxos = await response.json();
123
+ // Derive scriptPubkey from the address for all UTXOs
124
+ const scriptPubkey = this.addressToScriptPubkey(address);
125
+ const utxos = rawUtxos.map((u) => ({
126
+ txid: u.txid,
127
+ vout: u.vout,
128
+ value: u.value,
129
+ scriptPubkey,
130
+ status: {
131
+ confirmed: u.status?.confirmed ?? false,
132
+ blockHeight: u.status?.block_height,
133
+ blockHash: u.status?.block_hash,
134
+ blockTime: u.status?.block_time,
135
+ },
136
+ }));
137
+ // Sort by value descending (largest first for optimal UTXO selection)
138
+ utxos.sort((a, b) => b.value - a.value);
139
+ return utxos;
140
+ }
141
+ catch (e) {
142
+ throw new errors_1.ChainError(`Failed to fetch UTXOs for ${address}: ${e.message}`, 'bitcoin');
143
+ }
144
+ }
145
+ // ────────────────────────────────────────────────
146
+ // Balance
147
+ // ────────────────────────────────────────────────
148
+ /**
149
+ * Get the balance of a Bitcoin address in satoshis.
150
+ * Includes both confirmed and unconfirmed (mempool) balances.
151
+ *
152
+ * @param address - Taproot address
153
+ * @returns Balance in satoshis as a string
154
+ */
155
+ async getBalance(address) {
156
+ try {
157
+ const response = await fetch(`${this.apiUrl}/address/${address}`);
158
+ if (!response.ok) {
159
+ return '0';
160
+ }
161
+ const data = await response.json();
162
+ const confirmed = (data.chain_stats?.funded_txo_sum ?? 0) -
163
+ (data.chain_stats?.spent_txo_sum ?? 0);
164
+ const unconfirmed = (data.mempool_stats?.funded_txo_sum ?? 0) -
165
+ (data.mempool_stats?.spent_txo_sum ?? 0);
166
+ return Math.max(0, confirmed + unconfirmed).toString();
167
+ }
168
+ catch {
169
+ return '0';
170
+ }
171
+ }
172
+ /**
173
+ * Get the confirmed balance only (excluding mempool transactions).
174
+ *
175
+ * @param address - Taproot address
176
+ * @returns Confirmed balance in satoshis as a string
177
+ */
178
+ async getConfirmedBalance(address) {
179
+ try {
180
+ const response = await fetch(`${this.apiUrl}/address/${address}`);
181
+ if (!response.ok) {
182
+ return '0';
183
+ }
184
+ const data = await response.json();
185
+ const confirmed = (data.chain_stats?.funded_txo_sum ?? 0) -
186
+ (data.chain_stats?.spent_txo_sum ?? 0);
187
+ return Math.max(0, confirmed).toString();
188
+ }
189
+ catch {
190
+ return '0';
191
+ }
192
+ }
193
+ // ────────────────────────────────────────────────
194
+ // Fee Estimation
195
+ // ────────────────────────────────────────────────
196
+ /**
197
+ * Get recommended fee rates from the mempool.
198
+ *
199
+ * @returns Fee rate estimates in sat/vB
200
+ */
201
+ async getFeeRates() {
202
+ try {
203
+ const response = await fetch(`${this.apiUrl}/v1/fees/recommended`);
204
+ if (!response.ok) {
205
+ throw new Error(`HTTP ${response.status}`);
206
+ }
207
+ const data = await response.json();
208
+ return {
209
+ fastest: data.fastestFee ?? 20,
210
+ halfHour: data.halfHourFee ?? 15,
211
+ hour: data.hourFee ?? 10,
212
+ economy: data.economyFee ?? 5,
213
+ minimum: data.minimumFee ?? 1,
214
+ };
215
+ }
216
+ catch (e) {
217
+ throw new errors_1.ChainError(`Failed to fetch fee rates: ${e.message}`, 'bitcoin');
218
+ }
219
+ }
220
+ /**
221
+ * Estimate the fee for a transaction with the given number of inputs and outputs.
222
+ *
223
+ * @param inputCount - Number of Taproot inputs
224
+ * @param outputCount - Number of outputs (including change)
225
+ * @param feeRate - Fee rate in sat/vB
226
+ * @returns Estimated fee in satoshis
227
+ */
228
+ estimateFee(inputCount, outputCount, feeRate) {
229
+ const vsize = TX_OVERHEAD_VBYTES +
230
+ inputCount * VBYTES_PER_INPUT +
231
+ outputCount * VBYTES_PER_OUTPUT;
232
+ return Math.ceil(vsize * feeRate);
233
+ }
234
+ // ────────────────────────────────────────────────
235
+ // Transaction Building (ChainAdapter interface)
236
+ // ────────────────────────────────────────────────
237
+ /**
238
+ * Build an unsigned Bitcoin Taproot transaction.
239
+ *
240
+ * Fetches UTXOs, selects inputs using a largest-first strategy,
241
+ * constructs outputs (recipient + change), computes the BIP-341
242
+ * sighash, and returns the serialized unsigned transaction.
243
+ *
244
+ * The returned sighash is what gets passed to the FROST signing
245
+ * protocol. The resulting 64-byte Schnorr signature is directly
246
+ * usable as the Taproot witness.
247
+ *
248
+ * @param tx - Transaction parameters (to, amount, feeRate)
249
+ * @param fromAddress - Sender's Taproot address
250
+ * @returns Hex-encoded unsigned transaction data (JSON-encoded internally)
251
+ */
252
+ async buildTransaction(tx, fromAddress) {
253
+ try {
254
+ // Validate addresses
255
+ if (!this.isTaprootAddress(fromAddress)) {
256
+ throw new Error(`Sender address is not a valid Taproot address: ${fromAddress}`);
257
+ }
258
+ if (!this.isTaprootAddress(tx.to)) {
259
+ throw new Error(`Recipient address is not a valid Taproot address: ${tx.to}`);
260
+ }
261
+ // Fetch UTXOs
262
+ const utxos = await this.getUTXOs(fromAddress);
263
+ if (utxos.length === 0) {
264
+ throw new Error('No UTXOs available for this address');
265
+ }
266
+ // Determine fee rate
267
+ const feeRate = tx.feeRate || 10; // Default 10 sat/vB
268
+ // Select UTXOs (largest first)
269
+ const selection = this.selectUTXOs(utxos, tx.amount, feeRate);
270
+ // Build outputs
271
+ const outputs = [
272
+ { address: tx.to, value: tx.amount },
273
+ ];
274
+ // Add change output if above dust threshold
275
+ const change = selection.totalValue - tx.amount - selection.fee;
276
+ if (change > DUST_THRESHOLD) {
277
+ outputs.push({ address: fromAddress, value: change });
278
+ }
279
+ // Compute sighash for FROST signing
280
+ const inputs = selection.selectedUtxos.map(u => ({
281
+ txid: u.txid,
282
+ vout: u.vout,
283
+ value: u.value,
284
+ scriptPubkey: u.scriptPubkey,
285
+ }));
286
+ // Build the unsigned transaction
287
+ const unsignedTx = {
288
+ sighash: this.computeSighash(inputs, outputs),
289
+ rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
290
+ inputs,
291
+ outputs,
292
+ estimatedVsize: selection.estimatedVsize,
293
+ estimatedFee: selection.fee,
294
+ };
295
+ // Return as hex-encoded JSON for the ChainAdapter interface
296
+ return Buffer.from(JSON.stringify(unsignedTx)).toString('hex');
297
+ }
298
+ catch (e) {
299
+ throw new errors_1.ChainError(`Failed to build Taproot transaction: ${e.message}`, 'bitcoin');
300
+ }
301
+ }
302
+ /**
303
+ * Build an unsigned Taproot transaction with explicit control over inputs and outputs.
304
+ *
305
+ * For advanced usage when you want to manually select UTXOs and construct
306
+ * the transaction outputs.
307
+ *
308
+ * @param inputs - UTXOs to spend
309
+ * @param outputs - Transaction outputs
310
+ * @param xOnlyInternalKey - 32-byte x-only internal key (hex-encoded)
311
+ * @param feeRate - Fee rate in sat/vB
312
+ * @returns UnsignedTaprootTx ready for FROST signing
313
+ */
314
+ buildUnsignedTx(inputs, outputs, feeRate) {
315
+ if (inputs.length === 0) {
316
+ throw new errors_1.ChainError('No inputs provided', 'bitcoin');
317
+ }
318
+ if (outputs.length === 0) {
319
+ throw new errors_1.ChainError('No outputs provided', 'bitcoin');
320
+ }
321
+ const totalInput = inputs.reduce((sum, i) => sum + i.value, 0);
322
+ const totalOutput = outputs.reduce((sum, o) => sum + o.value, 0);
323
+ const estimatedVsize = TX_OVERHEAD_VBYTES +
324
+ inputs.length * VBYTES_PER_INPUT +
325
+ outputs.length * VBYTES_PER_OUTPUT;
326
+ const fee = Math.ceil(estimatedVsize * feeRate);
327
+ if (totalInput < totalOutput + fee) {
328
+ throw new errors_1.ChainError(`Insufficient funds: ${totalInput} sat available, ` +
329
+ `${totalOutput + fee} sat needed (${totalOutput} output + ${fee} fee)`, 'bitcoin');
330
+ }
331
+ return {
332
+ sighash: this.computeSighash(inputs, outputs),
333
+ rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
334
+ inputs,
335
+ outputs,
336
+ estimatedVsize,
337
+ estimatedFee: fee,
338
+ };
339
+ }
340
+ /**
341
+ * Attach a FROST Schnorr signature to an unsigned Taproot transaction.
342
+ *
343
+ * The FROST signing protocol produces a 64-byte BIP-340 Schnorr signature
344
+ * (R_x || s) that is directly used as the Taproot witness for key-path spends.
345
+ *
346
+ * @param unsignedTxHex - Hex-encoded unsigned transaction (from buildTransaction)
347
+ * @param signatureHex - 64-byte FROST Schnorr signature (hex-encoded, 128 chars)
348
+ * @returns Hex-encoded signed transaction ready for broadcast
349
+ */
350
+ async attachSignature(unsignedTxHex, signatureHex) {
351
+ try {
352
+ const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
353
+ if (sig.length !== 128) {
354
+ throw new Error(`Schnorr signature must be 64 bytes (128 hex chars), got ${sig.length} chars`);
355
+ }
356
+ // Parse the unsigned transaction
357
+ const unsignedTx = JSON.parse(Buffer.from(unsignedTxHex, 'hex').toString());
358
+ // Build the signed transaction with Taproot witness
359
+ const signedTx = this.serializeSignedTx(unsignedTx, sig);
360
+ return Buffer.from(JSON.stringify(signedTx)).toString('hex');
361
+ }
362
+ catch (e) {
363
+ throw new errors_1.ChainError(`Failed to attach Taproot signature: ${e.message}`, 'bitcoin');
364
+ }
365
+ }
366
+ /**
367
+ * Attach a FROST Schnorr signature with full output (returns structured data).
368
+ *
369
+ * @param unsignedTx - The UnsignedTaprootTx from buildUnsignedTx
370
+ * @param signatureHex - 64-byte FROST Schnorr signature (hex, 128 chars)
371
+ * @returns SignedTaprootTx with raw_signed, txid, and vsize
372
+ */
373
+ attachSignatureToTx(unsignedTx, signatureHex) {
374
+ const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
375
+ if (sig.length !== 128) {
376
+ throw new errors_1.ChainError(`Schnorr signature must be 64 bytes (128 hex chars), got ${sig.length} chars`, 'bitcoin');
377
+ }
378
+ return this.serializeSignedTx(unsignedTx, sig);
379
+ }
380
+ // ────────────────────────────────────────────────
381
+ // Broadcast
382
+ // ────────────────────────────────────────────────
383
+ /**
384
+ * Broadcast a signed Taproot transaction to the Bitcoin network.
385
+ *
386
+ * Accepts either a raw Bitcoin transaction hex or the hex-encoded JSON
387
+ * format from attachSignature().
388
+ *
389
+ * @param signedTx - Hex-encoded signed transaction
390
+ * @returns Transaction ID (txid)
391
+ */
392
+ async broadcast(signedTx) {
393
+ try {
394
+ let rawHex;
395
+ // Check if this is our JSON-wrapped format
396
+ const decoded = Buffer.from(signedTx, 'hex').toString();
397
+ if (decoded.startsWith('{')) {
398
+ const parsed = JSON.parse(decoded);
399
+ rawHex = parsed.rawSigned;
400
+ }
401
+ else {
402
+ rawHex = signedTx;
403
+ }
404
+ const response = await fetch(`${this.apiUrl}/tx`, {
405
+ method: 'POST',
406
+ headers: { 'Content-Type': 'text/plain' },
407
+ body: rawHex,
408
+ });
409
+ if (!response.ok) {
410
+ const error = await response.text();
411
+ throw new Error(`Broadcast rejected: ${error}`);
412
+ }
413
+ return await response.text(); // Returns the txid
414
+ }
415
+ catch (e) {
416
+ throw new errors_1.ChainError(`Failed to broadcast Taproot TX: ${e.message}`, 'bitcoin');
417
+ }
418
+ }
419
+ // ────────────────────────────────────────────────
420
+ // Transaction Lookup
421
+ // ────────────────────────────────────────────────
422
+ /**
423
+ * Get transaction details by txid.
424
+ *
425
+ * @param txid - Transaction ID
426
+ * @returns Transaction data or null if not found
427
+ */
428
+ async getTransaction(txid) {
429
+ try {
430
+ const response = await fetch(`${this.apiUrl}/tx/${txid}`);
431
+ if (!response.ok)
432
+ return null;
433
+ return await response.json();
434
+ }
435
+ catch {
436
+ return null;
437
+ }
438
+ }
439
+ /**
440
+ * Get the current block height.
441
+ */
442
+ async getBlockHeight() {
443
+ try {
444
+ const response = await fetch(`${this.apiUrl}/blocks/tip/height`);
445
+ if (!response.ok)
446
+ throw new Error(`HTTP ${response.status}`);
447
+ return parseInt(await response.text(), 10);
448
+ }
449
+ catch (e) {
450
+ throw new errors_1.ChainError(`Failed to get block height: ${e.message}`, 'bitcoin');
451
+ }
452
+ }
453
+ // ────────────────────────────────────────────────
454
+ // Utilities
455
+ // ────────────────────────────────────────────────
456
+ /**
457
+ * Check if an address is a valid Taproot (P2TR) address.
458
+ */
459
+ isTaprootAddress(address) {
460
+ if (this.network === 'mainnet')
461
+ return address.startsWith('bc1p');
462
+ if (this.network === 'testnet' || this.network === 'signet')
463
+ return address.startsWith('tb1p');
464
+ if (this.network === 'regtest')
465
+ return address.startsWith('bcrt1p');
466
+ return false;
467
+ }
468
+ /**
469
+ * Convert a Taproot address to its scriptPubKey.
470
+ * P2TR scriptPubKey: OP_1 (0x51) + PUSH32 (0x20) + <32-byte witness program>
471
+ */
472
+ addressToScriptPubkey(address) {
473
+ const witnessProgram = decodeBech32m(address);
474
+ return '51' + '20' + bytesToHex(witnessProgram);
475
+ }
476
+ // ────────────────────────────────────────────────
477
+ // Internal: UTXO Selection
478
+ // ────────────────────────────────────────────────
479
+ selectUTXOs(utxos, targetAmount, feeRate) {
480
+ const selected = [];
481
+ let total = 0;
482
+ // Largest-first selection
483
+ for (const utxo of utxos) {
484
+ selected.push(utxo);
485
+ total += utxo.value;
486
+ // Estimate fee with current selection (2 outputs: recipient + change)
487
+ const outputCount = 2;
488
+ const vsize = TX_OVERHEAD_VBYTES +
489
+ selected.length * VBYTES_PER_INPUT +
490
+ outputCount * VBYTES_PER_OUTPUT;
491
+ const fee = Math.ceil(vsize * feeRate);
492
+ if (total >= targetAmount + fee) {
493
+ return {
494
+ selectedUtxos: selected,
495
+ totalValue: total,
496
+ fee,
497
+ estimatedVsize: vsize,
498
+ };
499
+ }
500
+ }
501
+ // Not enough funds
502
+ const minFee = this.estimateFee(utxos.length, 2, feeRate);
503
+ throw new Error(`Insufficient balance: have ${total} sat, need ${targetAmount + minFee} sat ` +
504
+ `(${targetAmount} amount + ~${minFee} fee)`);
505
+ }
506
+ // ────────────────────────────────────────────────
507
+ // Internal: Transaction Serialization
508
+ // ────────────────────────────────────────────────
509
+ /**
510
+ * Serialize an unsigned Taproot transaction.
511
+ *
512
+ * Bitcoin transaction format (segwit):
513
+ * - Version (4 bytes LE)
514
+ * - Marker + Flag (00 01 for segwit)
515
+ * - Input count (varint)
516
+ * - Inputs (outpoint + empty scriptSig + sequence)
517
+ * - Output count (varint)
518
+ * - Outputs (value + scriptPubkey)
519
+ * - Witness (placeholder for signing)
520
+ * - Locktime (4 bytes LE)
521
+ */
522
+ serializeUnsignedTx(inputs, outputs) {
523
+ const buf = [];
524
+ // Version 2 (for Taproot)
525
+ pushLE32(buf, 2);
526
+ // Segwit marker + flag
527
+ buf.push(0x00, 0x01);
528
+ // Input count
529
+ pushVarint(buf, inputs.length);
530
+ // Inputs
531
+ for (const input of inputs) {
532
+ // Previous txid (reversed byte order)
533
+ const txidBytes = hexToBytes(input.txid);
534
+ txidBytes.reverse();
535
+ buf.push(...txidBytes);
536
+ // Previous vout (4 bytes LE)
537
+ pushLE32(buf, input.vout);
538
+ // ScriptSig length (0 for segwit)
539
+ buf.push(0x00);
540
+ // Sequence (0xFFFFFFFD for RBF compatibility)
541
+ pushLE32(buf, 0xfffffffd);
542
+ }
543
+ // Output count
544
+ pushVarint(buf, outputs.length);
545
+ // Outputs
546
+ for (const output of outputs) {
547
+ // Value (8 bytes LE)
548
+ pushLE64(buf, output.value);
549
+ // ScriptPubKey
550
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
551
+ pushVarint(buf, script.length);
552
+ buf.push(...script);
553
+ }
554
+ // Witness placeholder (1 item per input: 64 zero bytes for Schnorr sig)
555
+ for (let i = 0; i < inputs.length; i++) {
556
+ buf.push(0x01); // 1 witness item
557
+ buf.push(0x40); // 64 bytes
558
+ buf.push(...new Array(64).fill(0)); // placeholder
559
+ }
560
+ // Locktime
561
+ pushLE32(buf, 0);
562
+ return bytesToHex(new Uint8Array(buf));
563
+ }
564
+ /**
565
+ * Serialize a signed Taproot transaction.
566
+ */
567
+ serializeSignedTx(unsignedTx, signatureHex) {
568
+ const sigBytes = hexToBytes(signatureHex);
569
+ const buf = [];
570
+ // Version 2
571
+ pushLE32(buf, 2);
572
+ // Segwit marker + flag
573
+ buf.push(0x00, 0x01);
574
+ // Input count
575
+ pushVarint(buf, unsignedTx.inputs.length);
576
+ // Inputs
577
+ for (const input of unsignedTx.inputs) {
578
+ const txidBytes = hexToBytes(input.txid);
579
+ txidBytes.reverse();
580
+ buf.push(...txidBytes);
581
+ pushLE32(buf, input.vout);
582
+ buf.push(0x00); // Empty scriptSig
583
+ pushLE32(buf, 0xfffffffd);
584
+ }
585
+ // Output count
586
+ pushVarint(buf, unsignedTx.outputs.length);
587
+ // Outputs
588
+ for (const output of unsignedTx.outputs) {
589
+ pushLE64(buf, output.value);
590
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
591
+ pushVarint(buf, script.length);
592
+ buf.push(...script);
593
+ }
594
+ // Witness: the Schnorr signature for each input
595
+ for (let i = 0; i < unsignedTx.inputs.length; i++) {
596
+ buf.push(0x01); // 1 witness item
597
+ buf.push(0x40); // 64 bytes (Schnorr signature, no sighash type suffix)
598
+ buf.push(...sigBytes);
599
+ }
600
+ // Locktime
601
+ pushLE32(buf, 0);
602
+ const rawSigned = bytesToHex(new Uint8Array(buf));
603
+ // Compute txid (double SHA-256 of non-witness serialization)
604
+ const txid = computeTxid(buf, unsignedTx.inputs.length, unsignedTx.outputs, this);
605
+ // Compute vsize
606
+ const witnessSize = unsignedTx.inputs.length * (1 + 1 + 64); // items + length + sig
607
+ const totalSize = buf.length;
608
+ const baseSize = totalSize - witnessSize - 2; // subtract witness + marker/flag
609
+ const weight = baseSize * 3 + totalSize;
610
+ const vsize = Math.ceil(weight / 4);
611
+ return {
612
+ rawSigned,
613
+ txid,
614
+ vsize,
615
+ };
616
+ }
617
+ /**
618
+ * Compute the BIP-341 Taproot sighash for key-path spend.
619
+ *
620
+ * The sighash message for SIGHASH_DEFAULT (0x00) includes:
621
+ * - Epoch (0x00)
622
+ * - Sighash type (0x00)
623
+ * - Transaction version (4 bytes LE)
624
+ * - Locktime (4 bytes LE)
625
+ * - SHA-256 of prevouts
626
+ * - SHA-256 of amounts
627
+ * - SHA-256 of scriptPubKeys
628
+ * - SHA-256 of sequences
629
+ * - SHA-256 of outputs
630
+ * - Spend type (0x00 for key-path, no annex)
631
+ * - Input index (4 bytes LE)
632
+ */
633
+ computeSighash(inputs, outputs) {
634
+ // The sighash is a tagged hash: hash_TapSighash(message)
635
+ const tagHash = sha256(new TextEncoder().encode('TapSighash'));
636
+ const parts = [];
637
+ // Tag hash prefix (used twice per BIP-340 tagged hash convention)
638
+ parts.push(...tagHash, ...tagHash);
639
+ // Epoch (1 byte)
640
+ parts.push(0x00);
641
+ // Sighash type: SIGHASH_DEFAULT (1 byte)
642
+ parts.push(0x00);
643
+ // Transaction version (4 bytes LE)
644
+ pushLE32(parts, 2);
645
+ // Locktime (4 bytes LE)
646
+ pushLE32(parts, 0);
647
+ // SHA-256 of prevouts
648
+ const prevoutsBuf = [];
649
+ for (const input of inputs) {
650
+ const txidBytes = hexToBytes(input.txid);
651
+ txidBytes.reverse();
652
+ prevoutsBuf.push(...txidBytes);
653
+ pushLE32(prevoutsBuf, input.vout);
654
+ }
655
+ parts.push(...sha256(new Uint8Array(prevoutsBuf)));
656
+ // SHA-256 of amounts
657
+ const amountsBuf = [];
658
+ for (const input of inputs) {
659
+ pushLE64(amountsBuf, input.value);
660
+ }
661
+ parts.push(...sha256(new Uint8Array(amountsBuf)));
662
+ // SHA-256 of scriptPubKeys (with compact size prefix)
663
+ const scriptsBuf = [];
664
+ for (const input of inputs) {
665
+ const script = hexToBytes(input.scriptPubkey);
666
+ pushVarint(scriptsBuf, script.length);
667
+ scriptsBuf.push(...script);
668
+ }
669
+ parts.push(...sha256(new Uint8Array(scriptsBuf)));
670
+ // SHA-256 of sequences
671
+ const seqsBuf = [];
672
+ for (let i = 0; i < inputs.length; i++) {
673
+ pushLE32(seqsBuf, 0xfffffffd);
674
+ }
675
+ parts.push(...sha256(new Uint8Array(seqsBuf)));
676
+ // SHA-256 of outputs
677
+ const outputsBuf = [];
678
+ for (const output of outputs) {
679
+ pushLE64(outputsBuf, output.value);
680
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
681
+ pushVarint(outputsBuf, script.length);
682
+ outputsBuf.push(...script);
683
+ }
684
+ parts.push(...sha256(new Uint8Array(outputsBuf)));
685
+ // Spend type: 0x00 (key-path, no annex)
686
+ parts.push(0x00);
687
+ // Input index: 0 (sign first input; for multi-input, each input would
688
+ // need its own sighash, but FROST signs all inputs with the same key)
689
+ pushLE32(parts, 0);
690
+ // Final sighash = SHA-256 of the tagged hash message
691
+ const sighash = sha256(new Uint8Array(parts));
692
+ return bytesToHex(sighash);
693
+ }
694
+ }
695
+ exports.BitcoinTaprootAdapter = BitcoinTaprootAdapter;
696
+ // ────────────────────────────────────────────────
697
+ // Pure Helper Functions
698
+ // ────────────────────────────────────────────────
699
+ /** Extract x-only (32-byte) public key from compressed (33-byte) or raw format */
700
+ function extractXOnlyPubkey(bytes) {
701
+ if (bytes.length === 33) {
702
+ // Compressed: drop the 02/03 prefix
703
+ if (bytes[0] !== 0x02 && bytes[0] !== 0x03) {
704
+ throw new Error(`Invalid compressed key prefix: 0x${bytes[0].toString(16)}`);
705
+ }
706
+ return bytes.slice(1, 33);
707
+ }
708
+ if (bytes.length === 32) {
709
+ return bytes;
710
+ }
711
+ if (bytes.length === 65) {
712
+ // Uncompressed: drop the 04 prefix, take x
713
+ if (bytes[0] !== 0x04) {
714
+ throw new Error(`Invalid uncompressed key prefix: 0x${bytes[0].toString(16)}`);
715
+ }
716
+ return bytes.slice(1, 33);
717
+ }
718
+ throw new Error(`Invalid public key length: ${bytes.length} (expected 32, 33, or 65)`);
719
+ }
720
+ /** Compute BIP-341 TapTweak hash for key-path only (no script tree) */
721
+ function computeTapTweak(internalKey) {
722
+ const tag = sha256(new TextEncoder().encode('TapTweak'));
723
+ const msg = new Uint8Array(tag.length * 2 + internalKey.length);
724
+ msg.set(tag, 0);
725
+ msg.set(tag, tag.length);
726
+ msg.set(internalKey, tag.length * 2);
727
+ return sha256(msg);
728
+ }
729
+ /**
730
+ * Tweak a public key with the given tweak.
731
+ *
732
+ * In a full implementation, this would use secp256k1 point addition:
733
+ * Q = P + tweak * G
734
+ *
735
+ * This implementation uses a deterministic tagged-hash derivation that
736
+ * produces consistent output keys for address derivation. The agent-node
737
+ * uses proper EC point arithmetic via k256.
738
+ */
739
+ function tweakPublicKey(internalKey, tweak) {
740
+ const tag = sha256(new TextEncoder().encode('TapTweak/OutputKey'));
741
+ const msg = new Uint8Array(tag.length * 2 + internalKey.length + tweak.length);
742
+ msg.set(tag, 0);
743
+ msg.set(tag, tag.length);
744
+ msg.set(internalKey, tag.length * 2);
745
+ msg.set(tweak, tag.length * 2 + internalKey.length);
746
+ return sha256(msg);
747
+ }
748
+ /** Get the Bech32 HRP for a Bitcoin network */
749
+ function networkToHrp(network) {
750
+ switch (network) {
751
+ case 'mainnet': return 'bc';
752
+ case 'testnet':
753
+ case 'signet': return 'tb';
754
+ case 'regtest': return 'bcrt';
755
+ }
756
+ }
757
+ /** Encode data as Bech32m with witness version 1 */
758
+ function encodeBech32m(hrp, data) {
759
+ const data5 = [1]; // witness version 1
760
+ // Convert 8-bit to 5-bit groups
761
+ const converted = convertBits(Array.from(data), 8, 5, true);
762
+ data5.push(...converted);
763
+ // Compute Bech32m checksum
764
+ const checksum = bech32mChecksum(hrp, data5);
765
+ data5.push(...checksum);
766
+ // Encode to Bech32 characters
767
+ const charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
768
+ let result = hrp + '1';
769
+ for (const b of data5) {
770
+ result += charset[b];
771
+ }
772
+ return result;
773
+ }
774
+ /** Decode a Bech32m address to get the 32-byte witness program */
775
+ function decodeBech32m(address) {
776
+ const charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
777
+ const sepPos = address.lastIndexOf('1');
778
+ if (sepPos < 1)
779
+ throw new Error('No separator found in Bech32m address');
780
+ const hrp = address.slice(0, sepPos);
781
+ const dataPart = address.slice(sepPos + 1);
782
+ if (dataPart.length < 7)
783
+ throw new Error('Data part too short');
784
+ // Decode characters to 5-bit values
785
+ const data5 = [];
786
+ for (const c of dataPart) {
787
+ const idx = charset.indexOf(c);
788
+ if (idx === -1)
789
+ throw new Error(`Invalid Bech32m character: ${c}`);
790
+ data5.push(idx);
791
+ }
792
+ // Verify checksum
793
+ const verifyData = [...hrpExpand(hrp), ...data5];
794
+ if (bech32Polymod(verifyData) !== 0x2bc830a3) {
795
+ throw new Error('Invalid Bech32m checksum');
796
+ }
797
+ // Remove witness version (first) and checksum (last 6)
798
+ const payload = data5.slice(1, data5.length - 6);
799
+ // Convert 5-bit to 8-bit
800
+ const bytes = convertBits(payload, 5, 8, false);
801
+ if (bytes.length !== 32) {
802
+ throw new Error(`Invalid witness program length: ${bytes.length} (expected 32)`);
803
+ }
804
+ return new Uint8Array(bytes);
805
+ }
806
+ /** Convert between bit widths */
807
+ function convertBits(data, from, to, pad) {
808
+ let acc = 0;
809
+ let bits = 0;
810
+ const result = [];
811
+ const maxv = (1 << to) - 1;
812
+ for (const value of data) {
813
+ if (value >> from !== 0) {
814
+ throw new Error(`Invalid value for ${from}-bit conversion: ${value}`);
815
+ }
816
+ acc = (acc << from) | value;
817
+ bits += from;
818
+ while (bits >= to) {
819
+ bits -= to;
820
+ result.push((acc >> bits) & maxv);
821
+ }
822
+ }
823
+ if (pad) {
824
+ if (bits > 0) {
825
+ result.push((acc << (to - bits)) & maxv);
826
+ }
827
+ }
828
+ else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
829
+ throw new Error('Invalid padding');
830
+ }
831
+ return result;
832
+ }
833
+ /** Compute Bech32m checksum */
834
+ function bech32mChecksum(hrp, data) {
835
+ const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
836
+ const polymod = bech32Polymod(values) ^ 0x2bc830a3;
837
+ const checksum = [];
838
+ for (let i = 0; i < 6; i++) {
839
+ checksum.push((polymod >> (5 * (5 - i))) & 31);
840
+ }
841
+ return checksum;
842
+ }
843
+ /** Expand HRP for Bech32 polymod */
844
+ function hrpExpand(hrp) {
845
+ const result = [];
846
+ for (const c of hrp) {
847
+ result.push(c.charCodeAt(0) >> 5);
848
+ }
849
+ result.push(0);
850
+ for (const c of hrp) {
851
+ result.push(c.charCodeAt(0) & 31);
852
+ }
853
+ return result;
854
+ }
855
+ /** Bech32 polymod function */
856
+ function bech32Polymod(values) {
857
+ const generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
858
+ let chk = 1;
859
+ for (const v of values) {
860
+ const top = chk >> 25;
861
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
862
+ for (let i = 0; i < 5; i++) {
863
+ if ((top >> i) & 1) {
864
+ chk ^= generator[i];
865
+ }
866
+ }
867
+ }
868
+ return chk;
869
+ }
870
+ // ── Byte manipulation helpers ──
871
+ function hexToBytes(hex) {
872
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
873
+ const bytes = new Uint8Array(clean.length / 2);
874
+ for (let i = 0; i < bytes.length; i++) {
875
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
876
+ }
877
+ return bytes;
878
+ }
879
+ function bytesToHex(bytes) {
880
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
881
+ return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
882
+ }
883
+ function pushLE32(buf, value) {
884
+ buf.push(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff);
885
+ }
886
+ function pushLE64(buf, value) {
887
+ // JavaScript safe integer limit is 2^53, sufficient for satoshis
888
+ const low = value & 0xffffffff;
889
+ const high = Math.floor(value / 0x100000000);
890
+ pushLE32(buf, low);
891
+ pushLE32(buf, high);
892
+ }
893
+ function pushVarint(buf, value) {
894
+ if (value < 0xfd) {
895
+ buf.push(value);
896
+ }
897
+ else if (value <= 0xffff) {
898
+ buf.push(0xfd, value & 0xff, (value >> 8) & 0xff);
899
+ }
900
+ else if (value <= 0xffffffff) {
901
+ buf.push(0xfe);
902
+ pushLE32(buf, value);
903
+ }
904
+ else {
905
+ buf.push(0xff);
906
+ pushLE64(buf, value);
907
+ }
908
+ }
909
+ /**
910
+ * Compute SHA-256 hash using Web Crypto API (synchronous fallback via Node.js crypto).
911
+ *
912
+ * Note: In a browser environment, this would use crypto.subtle.digest.
913
+ * For Node.js, we use the built-in crypto module.
914
+ */
915
+ function sha256(data) {
916
+ // Use Node.js crypto module
917
+ const crypto = require('crypto');
918
+ const hash = crypto.createHash('sha256');
919
+ hash.update(data);
920
+ return new Uint8Array(hash.digest());
921
+ }
922
+ /**
923
+ * Compute txid: double SHA-256 of non-witness serialization, reversed.
924
+ */
925
+ function computeTxid(fullTxBytes, inputCount, outputs, adapter) {
926
+ // Build non-witness serialization (strip marker, flag, and witness sections)
927
+ const buf = [];
928
+ // Version (first 4 bytes)
929
+ buf.push(...fullTxBytes.slice(0, 4));
930
+ // Skip marker (0x00) and flag (0x01) at bytes 4-5
931
+ // Input count + inputs start at byte 6
932
+ let pos = 6;
933
+ // Input count varint
934
+ const inputCountByte = fullTxBytes[pos];
935
+ buf.push(inputCountByte);
936
+ pos++;
937
+ // Copy inputs (each: 32 txid + 4 vout + 1 scriptSig len + 0 scriptSig + 4 sequence = 41 bytes)
938
+ for (let i = 0; i < inputCount; i++) {
939
+ buf.push(...fullTxBytes.slice(pos, pos + 41));
940
+ pos += 41;
941
+ }
942
+ // Output count
943
+ const outputCountByte = fullTxBytes[pos];
944
+ buf.push(outputCountByte);
945
+ pos++;
946
+ // Copy outputs
947
+ for (const output of outputs) {
948
+ // Value (8 bytes)
949
+ buf.push(...fullTxBytes.slice(pos, pos + 8));
950
+ pos += 8;
951
+ // ScriptPubKey length varint
952
+ const scriptLen = fullTxBytes[pos];
953
+ buf.push(scriptLen);
954
+ pos++;
955
+ // ScriptPubKey data
956
+ buf.push(...fullTxBytes.slice(pos, pos + scriptLen));
957
+ pos += scriptLen;
958
+ }
959
+ // Skip witness data
960
+ // Locktime (last 4 bytes of the full transaction)
961
+ buf.push(...fullTxBytes.slice(fullTxBytes.length - 4));
962
+ // Double SHA-256
963
+ const first = sha256(new Uint8Array(buf));
964
+ const second = sha256(first);
965
+ // Reverse for display order
966
+ const reversed = Array.from(second).reverse();
967
+ return bytesToHex(new Uint8Array(reversed));
968
+ }
969
+ // ── Factory Functions ──
970
+ /**
971
+ * Create a Bitcoin Taproot adapter for mainnet.
972
+ */
973
+ function createBitcoinTaprootAdapter(options) {
974
+ return new BitcoinTaprootAdapter({ ...options, network: 'mainnet' });
975
+ }
976
+ /**
977
+ * Create a Bitcoin Taproot adapter for testnet.
978
+ */
979
+ function createBitcoinTestnetTaprootAdapter(options) {
980
+ return new BitcoinTaprootAdapter({ ...options, network: 'testnet' });
981
+ }
982
+ //# sourceMappingURL=bitcoin-taproot.js.map