@sequence0/sdk 1.0.3 → 1.1.2

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.
Files changed (49) hide show
  1. package/README.md +137 -10
  2. package/dist/chains/bitcoin-taproot.d.ts +371 -0
  3. package/dist/chains/bitcoin-taproot.d.ts.map +1 -0
  4. package/dist/chains/bitcoin-taproot.js +1241 -0
  5. package/dist/chains/bitcoin-taproot.js.map +1 -0
  6. package/dist/chains/bitcoin.d.ts +12 -7
  7. package/dist/chains/bitcoin.d.ts.map +1 -1
  8. package/dist/chains/bitcoin.js +14 -9
  9. package/dist/chains/bitcoin.js.map +1 -1
  10. package/dist/core/client.d.ts +4 -5
  11. package/dist/core/client.d.ts.map +1 -1
  12. package/dist/core/client.js +54 -29
  13. package/dist/core/client.js.map +1 -1
  14. package/dist/erc4337/account.d.ts +241 -0
  15. package/dist/erc4337/account.d.ts.map +1 -0
  16. package/dist/erc4337/account.js +810 -0
  17. package/dist/erc4337/account.js.map +1 -0
  18. package/dist/erc4337/index.d.ts +9 -0
  19. package/dist/erc4337/index.d.ts.map +1 -0
  20. package/dist/erc4337/index.js +16 -0
  21. package/dist/erc4337/index.js.map +1 -0
  22. package/dist/erc4337/types.d.ts +173 -0
  23. package/dist/erc4337/types.d.ts.map +1 -0
  24. package/dist/erc4337/types.js +52 -0
  25. package/dist/erc4337/types.js.map +1 -0
  26. package/dist/index.d.ts +29 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +43 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/utils/discovery.d.ts.map +1 -1
  31. package/dist/utils/discovery.js +56 -1
  32. package/dist/utils/discovery.js.map +1 -1
  33. package/dist/utils/eip712.d.ts +36 -0
  34. package/dist/utils/eip712.d.ts.map +1 -0
  35. package/dist/utils/eip712.js +80 -0
  36. package/dist/utils/eip712.js.map +1 -0
  37. package/dist/utils/fee.d.ts +2 -2
  38. package/dist/utils/fee.js +2 -2
  39. package/dist/utils/validation.d.ts +8 -0
  40. package/dist/utils/validation.d.ts.map +1 -1
  41. package/dist/utils/validation.js +18 -0
  42. package/dist/utils/validation.js.map +1 -1
  43. package/dist/utils/websocket.js +1 -1
  44. package/dist/utils/websocket.js.map +1 -1
  45. package/dist/wallet/wallet.d.ts +23 -2
  46. package/dist/wallet/wallet.d.ts.map +1 -1
  47. package/dist/wallet/wallet.js +91 -31
  48. package/dist/wallet/wallet.js.map +1 -1
  49. package/package.json +8 -2
@@ -0,0 +1,1241 @@
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
+ * Key features:
12
+ * - **Real secp256k1 EC point arithmetic** for BIP-341 key tweaking
13
+ * (lift_x, point_add, scalar_mul -- no external crypto dependencies)
14
+ * - **Per-input sighash computation** for multi-input transactions
15
+ * (each input gets its own BIP-341 sighash for independent FROST signing)
16
+ * - **Full transaction serialization** with proper segwit witness structure
17
+ * - **Broadcast via Mempool.space API** (mainnet, testnet, signet, regtest)
18
+ *
19
+ * No external dependencies beyond Node.js crypto (SHA-256).
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { BitcoinTaprootAdapter } from '@sequence0/sdk';
24
+ *
25
+ * const btc = new BitcoinTaprootAdapter({ network: 'mainnet' });
26
+ *
27
+ * // Derive a Taproot address from a FROST group public key
28
+ * const addr = btc.deriveAddress('02abcdef...');
29
+ * console.log(addr.address); // bc1p...
30
+ *
31
+ * // Get balance and UTXOs
32
+ * const balance = await btc.getBalance('bc1p...');
33
+ * const utxos = await btc.getUTXOs('bc1p...');
34
+ *
35
+ * // Build, sign, and broadcast
36
+ * const unsignedTx = await btc.buildTransaction(
37
+ * { to: 'bc1p...recipient', amount: 50000, feeRate: 15 },
38
+ * 'bc1p...sender'
39
+ * );
40
+ * // ... sign via FROST ...
41
+ * const txid = await btc.broadcast(signedTxHex);
42
+ * ```
43
+ */
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.BitcoinTaprootAdapter = void 0;
46
+ exports.createBitcoinTaprootAdapter = createBitcoinTaprootAdapter;
47
+ exports.createBitcoinTestnetTaprootAdapter = createBitcoinTestnetTaprootAdapter;
48
+ const errors_1 = require("../utils/errors");
49
+ // ── Constants ──
50
+ const DEFAULT_APIS = {
51
+ mainnet: 'https://mempool.space/api',
52
+ testnet: 'https://mempool.space/testnet/api',
53
+ signet: 'https://mempool.space/signet/api',
54
+ regtest: 'http://localhost:3000/api', // Local mempool instance
55
+ };
56
+ /** Minimum output value in satoshis (dust limit for P2TR) */
57
+ const DUST_THRESHOLD = 546;
58
+ /** Estimated vbytes per Taproot key-path input */
59
+ const VBYTES_PER_INPUT = 58;
60
+ /** Estimated vbytes per P2TR output */
61
+ const VBYTES_PER_OUTPUT = 43;
62
+ /** Fixed overhead vbytes per transaction */
63
+ const TX_OVERHEAD_VBYTES = 11;
64
+ // ── Adapter ──
65
+ class BitcoinTaprootAdapter {
66
+ constructor(options = {}) {
67
+ this.network = options.network || 'mainnet';
68
+ this.apiUrl = options.apiUrl || DEFAULT_APIS[this.network];
69
+ }
70
+ getRpcUrl() {
71
+ return this.apiUrl;
72
+ }
73
+ // ────────────────────────────────────────────────
74
+ // Address Derivation
75
+ // ────────────────────────────────────────────────
76
+ /**
77
+ * Derive a Taproot (P2TR) address from a FROST group public key.
78
+ *
79
+ * The FROST group verifying key is a secp256k1 point. For Taproot:
80
+ * 1. Extract the x-only public key (32 bytes)
81
+ * 2. Compute the Taproot tweak: t = hash_TapTweak(x_only_pubkey)
82
+ * 3. Compute the output key: Q = P + t*G
83
+ * 4. Encode as Bech32m with witness version 1
84
+ *
85
+ * @param groupPubkeyHex - Hex-encoded FROST group verifying key (33 bytes compressed or 32 bytes x-only)
86
+ * @returns TaprootAddressInfo with address, keys, and scriptPubkey
87
+ */
88
+ deriveAddress(groupPubkeyHex) {
89
+ const pubkeyClean = groupPubkeyHex.startsWith('0x')
90
+ ? groupPubkeyHex.slice(2)
91
+ : groupPubkeyHex;
92
+ const pubkeyBytes = hexToBytes(pubkeyClean);
93
+ // Extract x-only public key
94
+ const xOnly = extractXOnlyPubkey(pubkeyBytes);
95
+ // Compute Taproot tweak (key-path only, no script tree)
96
+ const tweak = computeTapTweak(xOnly);
97
+ // Compute tweaked output key
98
+ const outputKey = tweakPublicKey(xOnly, tweak);
99
+ // Encode as Bech32m
100
+ const hrp = networkToHrp(this.network);
101
+ const address = encodeBech32m(hrp, outputKey);
102
+ // Build scriptPubKey: OP_1 <32-byte output key>
103
+ const scriptPubkey = '51' + '20' + bytesToHex(outputKey);
104
+ return {
105
+ address,
106
+ xOnlyPubkey: bytesToHex(xOnly),
107
+ outputKey: bytesToHex(outputKey),
108
+ scriptPubkey,
109
+ tapTweak: bytesToHex(tweak),
110
+ network: this.network,
111
+ };
112
+ }
113
+ /**
114
+ * Compute the Taproot tweak for a FROST group public key.
115
+ *
116
+ * The FROST signing protocol must apply this tweak to the group private key
117
+ * before signing. This ensures the Schnorr signature verifies against the
118
+ * tweaked output key (which is what the scriptPubKey commits to).
119
+ *
120
+ * The tweak scalar t = hash_TapTweak(internal_key) is returned as hex.
121
+ * During FROST signing, the group's secret share is tweaked:
122
+ * tweaked_share = share + t (mod n)
123
+ *
124
+ * @param groupPubkeyHex - Hex-encoded FROST group verifying key (33 or 32 bytes)
125
+ * @returns Hex-encoded 32-byte tweak scalar
126
+ */
127
+ getTapTweak(groupPubkeyHex) {
128
+ const pubkeyClean = groupPubkeyHex.startsWith('0x')
129
+ ? groupPubkeyHex.slice(2)
130
+ : groupPubkeyHex;
131
+ const pubkeyBytes = hexToBytes(pubkeyClean);
132
+ const xOnly = extractXOnlyPubkey(pubkeyBytes);
133
+ const tweak = computeTapTweak(xOnly);
134
+ return bytesToHex(tweak);
135
+ }
136
+ // ────────────────────────────────────────────────
137
+ // UTXO Management
138
+ // ────────────────────────────────────────────────
139
+ /**
140
+ * Fetch unspent transaction outputs (UTXOs) for a Taproot address.
141
+ *
142
+ * @param address - Bech32m Taproot address (bc1p... / tb1p...)
143
+ * @returns Array of UTXOs sorted by value (largest first)
144
+ */
145
+ async getUTXOs(address) {
146
+ try {
147
+ const response = await fetch(`${this.apiUrl}/address/${address}/utxo`);
148
+ if (!response.ok) {
149
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
150
+ }
151
+ const rawUtxos = await response.json();
152
+ // Derive scriptPubkey from the address for all UTXOs
153
+ const scriptPubkey = this.addressToScriptPubkey(address);
154
+ const utxos = rawUtxos.map((u) => ({
155
+ txid: u.txid,
156
+ vout: u.vout,
157
+ value: u.value,
158
+ scriptPubkey,
159
+ status: {
160
+ confirmed: u.status?.confirmed ?? false,
161
+ blockHeight: u.status?.block_height,
162
+ blockHash: u.status?.block_hash,
163
+ blockTime: u.status?.block_time,
164
+ },
165
+ }));
166
+ // Sort by value descending (largest first for optimal UTXO selection)
167
+ utxos.sort((a, b) => b.value - a.value);
168
+ return utxos;
169
+ }
170
+ catch (e) {
171
+ throw new errors_1.ChainError(`Failed to fetch UTXOs for ${address}: ${e.message}`, 'bitcoin');
172
+ }
173
+ }
174
+ // ────────────────────────────────────────────────
175
+ // Balance
176
+ // ────────────────────────────────────────────────
177
+ /**
178
+ * Get the balance of a Bitcoin address in satoshis.
179
+ * Includes both confirmed and unconfirmed (mempool) balances.
180
+ *
181
+ * @param address - Taproot address
182
+ * @returns Balance in satoshis as a string
183
+ */
184
+ async getBalance(address) {
185
+ try {
186
+ const response = await fetch(`${this.apiUrl}/address/${address}`);
187
+ if (!response.ok) {
188
+ return '0';
189
+ }
190
+ const data = await response.json();
191
+ const confirmed = (data.chain_stats?.funded_txo_sum ?? 0) -
192
+ (data.chain_stats?.spent_txo_sum ?? 0);
193
+ const unconfirmed = (data.mempool_stats?.funded_txo_sum ?? 0) -
194
+ (data.mempool_stats?.spent_txo_sum ?? 0);
195
+ return Math.max(0, confirmed + unconfirmed).toString();
196
+ }
197
+ catch {
198
+ return '0';
199
+ }
200
+ }
201
+ /**
202
+ * Get the confirmed balance only (excluding mempool transactions).
203
+ *
204
+ * @param address - Taproot address
205
+ * @returns Confirmed balance in satoshis as a string
206
+ */
207
+ async getConfirmedBalance(address) {
208
+ try {
209
+ const response = await fetch(`${this.apiUrl}/address/${address}`);
210
+ if (!response.ok) {
211
+ return '0';
212
+ }
213
+ const data = await response.json();
214
+ const confirmed = (data.chain_stats?.funded_txo_sum ?? 0) -
215
+ (data.chain_stats?.spent_txo_sum ?? 0);
216
+ return Math.max(0, confirmed).toString();
217
+ }
218
+ catch {
219
+ return '0';
220
+ }
221
+ }
222
+ // ────────────────────────────────────────────────
223
+ // Fee Estimation
224
+ // ────────────────────────────────────────────────
225
+ /**
226
+ * Get recommended fee rates from the mempool.
227
+ *
228
+ * @returns Fee rate estimates in sat/vB
229
+ */
230
+ async getFeeRates() {
231
+ try {
232
+ const response = await fetch(`${this.apiUrl}/v1/fees/recommended`);
233
+ if (!response.ok) {
234
+ throw new Error(`HTTP ${response.status}`);
235
+ }
236
+ const data = await response.json();
237
+ return {
238
+ fastest: data.fastestFee ?? 20,
239
+ halfHour: data.halfHourFee ?? 15,
240
+ hour: data.hourFee ?? 10,
241
+ economy: data.economyFee ?? 5,
242
+ minimum: data.minimumFee ?? 1,
243
+ };
244
+ }
245
+ catch (e) {
246
+ throw new errors_1.ChainError(`Failed to fetch fee rates: ${e.message}`, 'bitcoin');
247
+ }
248
+ }
249
+ /**
250
+ * Estimate the fee for a transaction with the given number of inputs and outputs.
251
+ *
252
+ * @param inputCount - Number of Taproot inputs
253
+ * @param outputCount - Number of outputs (including change)
254
+ * @param feeRate - Fee rate in sat/vB
255
+ * @returns Estimated fee in satoshis
256
+ */
257
+ estimateFee(inputCount, outputCount, feeRate) {
258
+ const vsize = TX_OVERHEAD_VBYTES +
259
+ inputCount * VBYTES_PER_INPUT +
260
+ outputCount * VBYTES_PER_OUTPUT;
261
+ return Math.ceil(vsize * feeRate);
262
+ }
263
+ // ────────────────────────────────────────────────
264
+ // Transaction Building (ChainAdapter interface)
265
+ // ────────────────────────────────────────────────
266
+ /**
267
+ * Build an unsigned Bitcoin Taproot transaction.
268
+ *
269
+ * Fetches UTXOs, selects inputs using a largest-first strategy,
270
+ * constructs outputs (recipient + change), computes the BIP-341
271
+ * sighash, and returns the serialized unsigned transaction.
272
+ *
273
+ * The returned sighash is what gets passed to the FROST signing
274
+ * protocol. The resulting 64-byte Schnorr signature is directly
275
+ * usable as the Taproot witness.
276
+ *
277
+ * @param tx - Transaction parameters (to, amount, feeRate)
278
+ * @param fromAddress - Sender's Taproot address
279
+ * @returns Hex-encoded unsigned transaction data (JSON-encoded internally)
280
+ */
281
+ async buildTransaction(tx, fromAddress) {
282
+ try {
283
+ // Validate addresses
284
+ if (!this.isTaprootAddress(fromAddress)) {
285
+ throw new Error(`Sender address is not a valid Taproot address: ${fromAddress}`);
286
+ }
287
+ if (!this.isTaprootAddress(tx.to)) {
288
+ throw new Error(`Recipient address is not a valid Taproot address: ${tx.to}`);
289
+ }
290
+ // Fetch UTXOs
291
+ const utxos = await this.getUTXOs(fromAddress);
292
+ if (utxos.length === 0) {
293
+ throw new Error('No UTXOs available for this address');
294
+ }
295
+ // Determine fee rate
296
+ const feeRate = tx.feeRate || 10; // Default 10 sat/vB
297
+ // Select UTXOs (largest first)
298
+ const selection = this.selectUTXOs(utxos, tx.amount, feeRate);
299
+ // Build outputs
300
+ const outputs = [
301
+ { address: tx.to, value: tx.amount },
302
+ ];
303
+ // Add change output if above dust threshold
304
+ const change = selection.totalValue - tx.amount - selection.fee;
305
+ if (change > DUST_THRESHOLD) {
306
+ outputs.push({ address: fromAddress, value: change });
307
+ }
308
+ // Compute sighash for FROST signing
309
+ const inputs = selection.selectedUtxos.map(u => ({
310
+ txid: u.txid,
311
+ vout: u.vout,
312
+ value: u.value,
313
+ scriptPubkey: u.scriptPubkey,
314
+ }));
315
+ // Build the unsigned transaction with per-input sighashes
316
+ const sighashes = this.computeAllSighashes(inputs, outputs);
317
+ const unsignedTx = {
318
+ sighash: sighashes[0],
319
+ sighashes,
320
+ rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
321
+ inputs,
322
+ outputs,
323
+ estimatedVsize: selection.estimatedVsize,
324
+ estimatedFee: selection.fee,
325
+ };
326
+ // Return as hex-encoded JSON for the ChainAdapter interface
327
+ return Buffer.from(JSON.stringify(unsignedTx)).toString('hex');
328
+ }
329
+ catch (e) {
330
+ throw new errors_1.ChainError(`Failed to build Taproot transaction: ${e.message}`, 'bitcoin');
331
+ }
332
+ }
333
+ /**
334
+ * Build an unsigned Taproot transaction with explicit control over inputs and outputs.
335
+ *
336
+ * For advanced usage when you want to manually select UTXOs and construct
337
+ * the transaction outputs.
338
+ *
339
+ * @param inputs - UTXOs to spend
340
+ * @param outputs - Transaction outputs
341
+ * @param xOnlyInternalKey - 32-byte x-only internal key (hex-encoded)
342
+ * @param feeRate - Fee rate in sat/vB
343
+ * @returns UnsignedTaprootTx ready for FROST signing
344
+ */
345
+ buildUnsignedTx(inputs, outputs, feeRate) {
346
+ if (inputs.length === 0) {
347
+ throw new errors_1.ChainError('No inputs provided', 'bitcoin');
348
+ }
349
+ if (outputs.length === 0) {
350
+ throw new errors_1.ChainError('No outputs provided', 'bitcoin');
351
+ }
352
+ const totalInput = inputs.reduce((sum, i) => sum + i.value, 0);
353
+ const totalOutput = outputs.reduce((sum, o) => sum + o.value, 0);
354
+ const estimatedVsize = TX_OVERHEAD_VBYTES +
355
+ inputs.length * VBYTES_PER_INPUT +
356
+ outputs.length * VBYTES_PER_OUTPUT;
357
+ const fee = Math.ceil(estimatedVsize * feeRate);
358
+ if (totalInput < totalOutput + fee) {
359
+ throw new errors_1.ChainError(`Insufficient funds: ${totalInput} sat available, ` +
360
+ `${totalOutput + fee} sat needed (${totalOutput} output + ${fee} fee)`, 'bitcoin');
361
+ }
362
+ const sighashes = this.computeAllSighashes(inputs, outputs);
363
+ return {
364
+ sighash: sighashes[0],
365
+ sighashes,
366
+ rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
367
+ inputs,
368
+ outputs,
369
+ estimatedVsize,
370
+ estimatedFee: fee,
371
+ };
372
+ }
373
+ /**
374
+ * Attach FROST Schnorr signature(s) to an unsigned Taproot transaction.
375
+ *
376
+ * The FROST signing protocol produces 64-byte BIP-340 Schnorr signatures
377
+ * (R_x || s) that are directly used as Taproot witness for key-path spends.
378
+ *
379
+ * For single-input transactions: pass a single 128-char hex signature.
380
+ * For multi-input transactions: pass signatures separated by commas, or
381
+ * a single signature that will be applied to all inputs (if all inputs
382
+ * share the same signing key and the caller signs each sighash separately).
383
+ *
384
+ * @param unsignedTxHex - Hex-encoded unsigned transaction (from buildTransaction)
385
+ * @param signatureHex - 64-byte FROST Schnorr signature(s). For multi-input
386
+ * transactions, separate per-input signatures with commas.
387
+ * @returns Hex-encoded signed transaction ready for broadcast
388
+ */
389
+ async attachSignature(unsignedTxHex, signatureHex) {
390
+ try {
391
+ // Parse the unsigned transaction
392
+ const unsignedTx = JSON.parse(Buffer.from(unsignedTxHex, 'hex').toString());
393
+ // Parse signature(s)
394
+ const signatures = this.parseSignatures(signatureHex, unsignedTx.inputs.length);
395
+ // Build the signed transaction with Taproot witness
396
+ const signedTx = this.serializeSignedTx(unsignedTx, signatures);
397
+ return Buffer.from(JSON.stringify(signedTx)).toString('hex');
398
+ }
399
+ catch (e) {
400
+ throw new errors_1.ChainError(`Failed to attach Taproot signature: ${e.message}`, 'bitcoin');
401
+ }
402
+ }
403
+ /**
404
+ * Attach FROST Schnorr signature(s) with full output (returns structured data).
405
+ *
406
+ * @param unsignedTx - The UnsignedTaprootTx from buildUnsignedTx
407
+ * @param signatureHex - 64-byte FROST Schnorr signature(s) (hex). For multi-input
408
+ * transactions, pass an array of per-input signatures or a comma-separated string.
409
+ * @returns SignedTaprootTx with raw_signed, txid, and vsize
410
+ */
411
+ attachSignatureToTx(unsignedTx, signatureHex) {
412
+ const sigs = Array.isArray(signatureHex)
413
+ ? signatureHex.map(s => {
414
+ const clean = s.startsWith('0x') ? s.slice(2) : s;
415
+ if (clean.length !== 128) {
416
+ throw new errors_1.ChainError(`Schnorr signature must be 64 bytes (128 hex chars), got ${clean.length}`, 'bitcoin');
417
+ }
418
+ return clean;
419
+ })
420
+ : this.parseSignatures(signatureHex, unsignedTx.inputs.length);
421
+ return this.serializeSignedTx(unsignedTx, sigs);
422
+ }
423
+ /**
424
+ * Parse signature hex into per-input signatures.
425
+ * Supports: single sig (applied to all inputs), comma-separated, or concatenated.
426
+ */
427
+ parseSignatures(signatureHex, inputCount) {
428
+ const raw = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
429
+ // Check for comma-separated signatures
430
+ if (raw.includes(',')) {
431
+ const parts = raw.split(',').map(s => s.trim());
432
+ if (parts.length !== inputCount) {
433
+ throw new Error(`Expected ${inputCount} signatures for ${inputCount} inputs, got ${parts.length}`);
434
+ }
435
+ for (const s of parts) {
436
+ if (s.length !== 128) {
437
+ throw new Error(`Each Schnorr signature must be 128 hex chars, got ${s.length}`);
438
+ }
439
+ }
440
+ return parts;
441
+ }
442
+ // Single signature (128 hex chars) — replicate for all inputs
443
+ if (raw.length === 128) {
444
+ return new Array(inputCount).fill(raw);
445
+ }
446
+ // Concatenated signatures (128 * inputCount hex chars)
447
+ if (raw.length === 128 * inputCount) {
448
+ const sigs = [];
449
+ for (let i = 0; i < inputCount; i++) {
450
+ sigs.push(raw.slice(i * 128, (i + 1) * 128));
451
+ }
452
+ return sigs;
453
+ }
454
+ throw new Error(`Invalid signature format: expected 128 hex chars (single), ` +
455
+ `${128 * inputCount} chars (concatenated), or comma-separated. Got ${raw.length} chars.`);
456
+ }
457
+ // ────────────────────────────────────────────────
458
+ // Broadcast
459
+ // ────────────────────────────────────────────────
460
+ /**
461
+ * Broadcast a signed Taproot transaction to the Bitcoin network.
462
+ *
463
+ * Accepts either a raw Bitcoin transaction hex or the hex-encoded JSON
464
+ * format from attachSignature().
465
+ *
466
+ * @param signedTx - Hex-encoded signed transaction
467
+ * @returns Transaction ID (txid)
468
+ */
469
+ async broadcast(signedTx) {
470
+ try {
471
+ let rawHex;
472
+ // Check if this is our JSON-wrapped format
473
+ const decoded = Buffer.from(signedTx, 'hex').toString();
474
+ if (decoded.startsWith('{')) {
475
+ const parsed = JSON.parse(decoded);
476
+ rawHex = parsed.rawSigned;
477
+ }
478
+ else {
479
+ rawHex = signedTx;
480
+ }
481
+ const response = await fetch(`${this.apiUrl}/tx`, {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'text/plain' },
484
+ body: rawHex,
485
+ });
486
+ if (!response.ok) {
487
+ const error = await response.text();
488
+ throw new Error(`Broadcast rejected: ${error}`);
489
+ }
490
+ return await response.text(); // Returns the txid
491
+ }
492
+ catch (e) {
493
+ throw new errors_1.ChainError(`Failed to broadcast Taproot TX: ${e.message}`, 'bitcoin');
494
+ }
495
+ }
496
+ // ────────────────────────────────────────────────
497
+ // Transaction Lookup
498
+ // ────────────────────────────────────────────────
499
+ /**
500
+ * Get transaction details by txid.
501
+ *
502
+ * @param txid - Transaction ID
503
+ * @returns Transaction data or null if not found
504
+ */
505
+ async getTransaction(txid) {
506
+ try {
507
+ const response = await fetch(`${this.apiUrl}/tx/${txid}`);
508
+ if (!response.ok)
509
+ return null;
510
+ return await response.json();
511
+ }
512
+ catch {
513
+ return null;
514
+ }
515
+ }
516
+ /**
517
+ * Get the current block height.
518
+ */
519
+ async getBlockHeight() {
520
+ try {
521
+ const response = await fetch(`${this.apiUrl}/blocks/tip/height`);
522
+ if (!response.ok)
523
+ throw new Error(`HTTP ${response.status}`);
524
+ return parseInt(await response.text(), 10);
525
+ }
526
+ catch (e) {
527
+ throw new errors_1.ChainError(`Failed to get block height: ${e.message}`, 'bitcoin');
528
+ }
529
+ }
530
+ // ────────────────────────────────────────────────
531
+ // Utilities
532
+ // ────────────────────────────────────────────────
533
+ /**
534
+ * Check if an address is a valid Taproot (P2TR) address.
535
+ */
536
+ isTaprootAddress(address) {
537
+ if (this.network === 'mainnet')
538
+ return address.startsWith('bc1p');
539
+ if (this.network === 'testnet' || this.network === 'signet')
540
+ return address.startsWith('tb1p');
541
+ if (this.network === 'regtest')
542
+ return address.startsWith('bcrt1p');
543
+ return false;
544
+ }
545
+ /**
546
+ * Convert a Taproot address to its scriptPubKey.
547
+ * P2TR scriptPubKey: OP_1 (0x51) + PUSH32 (0x20) + <32-byte witness program>
548
+ */
549
+ addressToScriptPubkey(address) {
550
+ const witnessProgram = decodeBech32m(address);
551
+ return '51' + '20' + bytesToHex(witnessProgram);
552
+ }
553
+ // ────────────────────────────────────────────────
554
+ // Internal: UTXO Selection
555
+ // ────────────────────────────────────────────────
556
+ selectUTXOs(utxos, targetAmount, feeRate) {
557
+ const selected = [];
558
+ let total = 0;
559
+ // Largest-first selection
560
+ for (const utxo of utxos) {
561
+ selected.push(utxo);
562
+ total += utxo.value;
563
+ // Estimate fee with current selection (2 outputs: recipient + change)
564
+ const outputCount = 2;
565
+ const vsize = TX_OVERHEAD_VBYTES +
566
+ selected.length * VBYTES_PER_INPUT +
567
+ outputCount * VBYTES_PER_OUTPUT;
568
+ const fee = Math.ceil(vsize * feeRate);
569
+ if (total >= targetAmount + fee) {
570
+ return {
571
+ selectedUtxos: selected,
572
+ totalValue: total,
573
+ fee,
574
+ estimatedVsize: vsize,
575
+ };
576
+ }
577
+ }
578
+ // Not enough funds
579
+ const minFee = this.estimateFee(utxos.length, 2, feeRate);
580
+ throw new Error(`Insufficient balance: have ${total} sat, need ${targetAmount + minFee} sat ` +
581
+ `(${targetAmount} amount + ~${minFee} fee)`);
582
+ }
583
+ // ────────────────────────────────────────────────
584
+ // Internal: Transaction Serialization
585
+ // ────────────────────────────────────────────────
586
+ /**
587
+ * Serialize an unsigned Taproot transaction.
588
+ *
589
+ * Bitcoin transaction format (segwit):
590
+ * - Version (4 bytes LE)
591
+ * - Marker + Flag (00 01 for segwit)
592
+ * - Input count (varint)
593
+ * - Inputs (outpoint + empty scriptSig + sequence)
594
+ * - Output count (varint)
595
+ * - Outputs (value + scriptPubkey)
596
+ * - Witness (placeholder for signing)
597
+ * - Locktime (4 bytes LE)
598
+ */
599
+ serializeUnsignedTx(inputs, outputs) {
600
+ const buf = [];
601
+ // Version 2 (for Taproot)
602
+ pushLE32(buf, 2);
603
+ // Segwit marker + flag
604
+ buf.push(0x00, 0x01);
605
+ // Input count
606
+ pushVarint(buf, inputs.length);
607
+ // Inputs
608
+ for (const input of inputs) {
609
+ // Previous txid (reversed byte order)
610
+ const txidBytes = hexToBytes(input.txid);
611
+ txidBytes.reverse();
612
+ buf.push(...txidBytes);
613
+ // Previous vout (4 bytes LE)
614
+ pushLE32(buf, input.vout);
615
+ // ScriptSig length (0 for segwit)
616
+ buf.push(0x00);
617
+ // Sequence (0xFFFFFFFD for RBF compatibility)
618
+ pushLE32(buf, 0xfffffffd);
619
+ }
620
+ // Output count
621
+ pushVarint(buf, outputs.length);
622
+ // Outputs
623
+ for (const output of outputs) {
624
+ // Value (8 bytes LE)
625
+ pushLE64(buf, output.value);
626
+ // ScriptPubKey
627
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
628
+ pushVarint(buf, script.length);
629
+ buf.push(...script);
630
+ }
631
+ // Witness placeholder (1 item per input: 64 zero bytes for Schnorr sig)
632
+ for (let i = 0; i < inputs.length; i++) {
633
+ buf.push(0x01); // 1 witness item
634
+ buf.push(0x40); // 64 bytes
635
+ buf.push(...new Array(64).fill(0)); // placeholder
636
+ }
637
+ // Locktime
638
+ pushLE32(buf, 0);
639
+ return bytesToHex(new Uint8Array(buf));
640
+ }
641
+ /**
642
+ * Serialize a signed Taproot transaction with per-input signatures.
643
+ *
644
+ * @param unsignedTx - The unsigned transaction data
645
+ * @param signatures - Array of per-input signature hex strings (128 chars each)
646
+ */
647
+ serializeSignedTx(unsignedTx, signatures) {
648
+ if (signatures.length !== unsignedTx.inputs.length) {
649
+ throw new Error(`Signature count (${signatures.length}) does not match ` +
650
+ `input count (${unsignedTx.inputs.length})`);
651
+ }
652
+ const buf = [];
653
+ // Version 2
654
+ pushLE32(buf, 2);
655
+ // Segwit marker + flag
656
+ buf.push(0x00, 0x01);
657
+ // Input count
658
+ pushVarint(buf, unsignedTx.inputs.length);
659
+ // Inputs
660
+ for (const input of unsignedTx.inputs) {
661
+ const txidBytes = hexToBytes(input.txid);
662
+ txidBytes.reverse();
663
+ buf.push(...txidBytes);
664
+ pushLE32(buf, input.vout);
665
+ buf.push(0x00); // Empty scriptSig
666
+ pushLE32(buf, 0xfffffffd);
667
+ }
668
+ // Output count
669
+ pushVarint(buf, unsignedTx.outputs.length);
670
+ // Outputs
671
+ for (const output of unsignedTx.outputs) {
672
+ pushLE64(buf, output.value);
673
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
674
+ pushVarint(buf, script.length);
675
+ buf.push(...script);
676
+ }
677
+ // Witness: per-input Schnorr signatures for key-path spend
678
+ for (let i = 0; i < unsignedTx.inputs.length; i++) {
679
+ const sigBytes = hexToBytes(signatures[i]);
680
+ buf.push(0x01); // 1 witness item
681
+ buf.push(0x40); // 64 bytes (Schnorr signature, no sighash type suffix)
682
+ buf.push(...sigBytes);
683
+ }
684
+ // Locktime
685
+ pushLE32(buf, 0);
686
+ const rawSigned = bytesToHex(new Uint8Array(buf));
687
+ // Compute txid (double SHA-256 of non-witness serialization)
688
+ const txid = computeTxid(buf, unsignedTx.inputs.length, unsignedTx.outputs, this);
689
+ // Compute vsize
690
+ const witnessSize = unsignedTx.inputs.length * (1 + 1 + 64); // items + length + sig
691
+ const totalSize = buf.length;
692
+ const baseSize = totalSize - witnessSize - 2; // subtract witness + marker/flag
693
+ const weight = baseSize * 3 + totalSize;
694
+ const vsize = Math.ceil(weight / 4);
695
+ return {
696
+ rawSigned,
697
+ txid,
698
+ vsize,
699
+ };
700
+ }
701
+ /**
702
+ * Compute the BIP-341 Taproot sighash for key-path spend.
703
+ *
704
+ * The sighash message for SIGHASH_DEFAULT (0x00) includes:
705
+ * - Epoch (0x00)
706
+ * - Sighash type (0x00)
707
+ * - Transaction version (4 bytes LE)
708
+ * - Locktime (4 bytes LE)
709
+ * - SHA-256 of prevouts
710
+ * - SHA-256 of amounts
711
+ * - SHA-256 of scriptPubKeys
712
+ * - SHA-256 of sequences
713
+ * - SHA-256 of outputs
714
+ * - Spend type (0x00 for key-path, no annex)
715
+ * - Input index (4 bytes LE)
716
+ */
717
+ /**
718
+ * Compute all per-input BIP-341 sighashes for the transaction.
719
+ *
720
+ * Each input has its own sighash because the input_index field differs.
721
+ * The common transaction-level hashes (prevouts, amounts, scripts, sequences,
722
+ * outputs) are precomputed once and reused across all inputs.
723
+ *
724
+ * @returns Array of hex-encoded sighashes, one per input
725
+ */
726
+ computeAllSighashes(inputs, outputs) {
727
+ // Precompute the transaction-level hashes (shared across all inputs)
728
+ const tagHash = sha256(new TextEncoder().encode('TapSighash'));
729
+ // SHA-256 of prevouts
730
+ const prevoutsBuf = [];
731
+ for (const input of inputs) {
732
+ const txidBytes = hexToBytes(input.txid);
733
+ txidBytes.reverse();
734
+ prevoutsBuf.push(...txidBytes);
735
+ pushLE32(prevoutsBuf, input.vout);
736
+ }
737
+ const hashPrevouts = sha256(new Uint8Array(prevoutsBuf));
738
+ // SHA-256 of amounts
739
+ const amountsBuf = [];
740
+ for (const input of inputs) {
741
+ pushLE64(amountsBuf, input.value);
742
+ }
743
+ const hashAmounts = sha256(new Uint8Array(amountsBuf));
744
+ // SHA-256 of scriptPubKeys (with compact size prefix)
745
+ const scriptsBuf = [];
746
+ for (const input of inputs) {
747
+ const script = hexToBytes(input.scriptPubkey);
748
+ pushVarint(scriptsBuf, script.length);
749
+ scriptsBuf.push(...script);
750
+ }
751
+ const hashScripts = sha256(new Uint8Array(scriptsBuf));
752
+ // SHA-256 of sequences
753
+ const seqsBuf = [];
754
+ for (let i = 0; i < inputs.length; i++) {
755
+ pushLE32(seqsBuf, 0xfffffffd);
756
+ }
757
+ const hashSequences = sha256(new Uint8Array(seqsBuf));
758
+ // SHA-256 of outputs
759
+ const outputsBuf = [];
760
+ for (const output of outputs) {
761
+ pushLE64(outputsBuf, output.value);
762
+ const script = hexToBytes(this.addressToScriptPubkey(output.address));
763
+ pushVarint(outputsBuf, script.length);
764
+ outputsBuf.push(...script);
765
+ }
766
+ const hashOutputs = sha256(new Uint8Array(outputsBuf));
767
+ // Compute per-input sighash
768
+ const sighashes = [];
769
+ for (let inputIdx = 0; inputIdx < inputs.length; inputIdx++) {
770
+ const parts = [];
771
+ // Tag hash prefix (used twice per BIP-340 tagged hash convention)
772
+ parts.push(...tagHash, ...tagHash);
773
+ // Epoch (1 byte)
774
+ parts.push(0x00);
775
+ // Sighash type: SIGHASH_DEFAULT (1 byte)
776
+ parts.push(0x00);
777
+ // Transaction version (4 bytes LE)
778
+ pushLE32(parts, 2);
779
+ // Locktime (4 bytes LE)
780
+ pushLE32(parts, 0);
781
+ // Precomputed hashes
782
+ parts.push(...hashPrevouts);
783
+ parts.push(...hashAmounts);
784
+ parts.push(...hashScripts);
785
+ parts.push(...hashSequences);
786
+ parts.push(...hashOutputs);
787
+ // Spend type: 0x00 (key-path, no annex)
788
+ parts.push(0x00);
789
+ // Input index (4 bytes LE) — THIS is what differs per input
790
+ pushLE32(parts, inputIdx);
791
+ // Final sighash = SHA-256 of the tagged hash message
792
+ const sighash = sha256(new Uint8Array(parts));
793
+ sighashes.push(bytesToHex(sighash));
794
+ }
795
+ return sighashes;
796
+ }
797
+ }
798
+ exports.BitcoinTaprootAdapter = BitcoinTaprootAdapter;
799
+ const Secp256k1 = {
800
+ /** Field prime */
801
+ P: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn,
802
+ /** Curve order */
803
+ N: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n,
804
+ /** Generator x */
805
+ Gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
806
+ /** Generator y */
807
+ Gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
808
+ /** Get generator point */
809
+ G() {
810
+ return { x: this.Gx, y: this.Gy };
811
+ },
812
+ /**
813
+ * Modular exponentiation: base^exp mod m
814
+ * Uses square-and-multiply for efficiency.
815
+ */
816
+ modPow(base, exp, m) {
817
+ let result = 1n;
818
+ base = ((base % m) + m) % m;
819
+ while (exp > 0n) {
820
+ if (exp & 1n) {
821
+ result = (result * base) % m;
822
+ }
823
+ exp >>= 1n;
824
+ base = (base * base) % m;
825
+ }
826
+ return result;
827
+ },
828
+ /** Modular inverse using Fermat's little theorem: a^(p-2) mod p */
829
+ modInverse(a, m) {
830
+ return this.modPow(((a % m) + m) % m, m - 2n, m);
831
+ },
832
+ /** Modular square root: returns r such that r^2 = a (mod p), or throws.
833
+ * Uses the Tonelli-Shanks shortcut for p = 3 mod 4: r = a^((p+1)/4) mod p */
834
+ modSqrt(a) {
835
+ const p = this.P;
836
+ // secp256k1 p % 4 == 3, so we can use the simple formula
837
+ const r = this.modPow(((a % p) + p) % p, (p + 1n) / 4n, p);
838
+ if ((r * r) % p !== ((a % p) + p) % p) {
839
+ throw new Error('No square root exists for this value');
840
+ }
841
+ return r;
842
+ },
843
+ /**
844
+ * Lift an x-only public key to a full curve point with even y-coordinate.
845
+ * Per BIP-340: given x, compute y = sqrt(x^3 + 7), pick the even root.
846
+ */
847
+ liftX(xBytes) {
848
+ const x = bytesToBigInt(xBytes);
849
+ const p = this.P;
850
+ if (x >= p) {
851
+ throw new Error('x-coordinate exceeds field prime');
852
+ }
853
+ // y^2 = x^3 + 7 mod p
854
+ const y2 = (this.modPow(x, 3n, p) + 7n) % p;
855
+ let y = this.modSqrt(y2);
856
+ // BIP-340: pick the even y (y % 2 == 0)
857
+ if (y & 1n) {
858
+ y = p - y;
859
+ }
860
+ return { x, y };
861
+ },
862
+ /**
863
+ * Point addition: P + Q on secp256k1.
864
+ * Handles identity, doubling, and general addition.
865
+ */
866
+ pointAdd(p1, p2) {
867
+ if (p1 === null)
868
+ return p2;
869
+ if (p2 === null)
870
+ return p1;
871
+ const p = this.P;
872
+ if (p1.x === p2.x) {
873
+ if (p1.y === p2.y) {
874
+ // Point doubling
875
+ return this.pointDouble(p1);
876
+ }
877
+ // P + (-P) = O (point at infinity)
878
+ return null;
879
+ }
880
+ // General addition: lambda = (y2 - y1) / (x2 - x1)
881
+ const dy = ((p2.y - p1.y) % p + p) % p;
882
+ const dx = ((p2.x - p1.x) % p + p) % p;
883
+ const lambda = (dy * this.modInverse(dx, p)) % p;
884
+ const x3 = ((lambda * lambda - p1.x - p2.x) % p + p) % p;
885
+ const y3 = ((lambda * (p1.x - x3) - p1.y) % p + p) % p;
886
+ return { x: x3, y: y3 };
887
+ },
888
+ /** Point doubling: 2P on secp256k1 */
889
+ pointDouble(pt) {
890
+ const p = this.P;
891
+ if (pt.y === 0n)
892
+ return null;
893
+ // lambda = (3 * x^2 + a) / (2 * y) where a = 0 for secp256k1
894
+ const num = (3n * pt.x * pt.x) % p;
895
+ const den = (2n * pt.y) % p;
896
+ const lambda = (num * this.modInverse(den, p)) % p;
897
+ const x3 = ((lambda * lambda - 2n * pt.x) % p + p) % p;
898
+ const y3 = ((lambda * (pt.x - x3) - pt.y) % p + p) % p;
899
+ return { x: x3, y: y3 };
900
+ },
901
+ /**
902
+ * Scalar multiplication: n * G using double-and-add.
903
+ * Only used for tweaking, so performance is not critical.
904
+ */
905
+ mulG(n) {
906
+ const order = this.N;
907
+ n = ((n % order) + order) % order;
908
+ if (n === 0n) {
909
+ throw new Error('Zero scalar in mulG');
910
+ }
911
+ let result = null;
912
+ let base = this.G();
913
+ let k = n;
914
+ while (k > 0n) {
915
+ if (k & 1n) {
916
+ result = this.pointAdd(result, base);
917
+ }
918
+ base = this.pointAdd(base, base);
919
+ k >>= 1n;
920
+ }
921
+ if (result === null) {
922
+ throw new Error('mulG produced point at infinity');
923
+ }
924
+ return result;
925
+ },
926
+ };
927
+ /** Convert a Uint8Array to a bigint (big-endian) */
928
+ function bytesToBigInt(bytes) {
929
+ let result = 0n;
930
+ for (const b of bytes) {
931
+ result = (result << 8n) | BigInt(b);
932
+ }
933
+ return result;
934
+ }
935
+ /** Convert a bigint to a 32-byte Uint8Array (big-endian, zero-padded) */
936
+ function bigIntToBytes32(n) {
937
+ const bytes = new Uint8Array(32);
938
+ let val = n;
939
+ for (let i = 31; i >= 0; i--) {
940
+ bytes[i] = Number(val & 0xffn);
941
+ val >>= 8n;
942
+ }
943
+ return bytes;
944
+ }
945
+ // ────────────────────────────────────────────────
946
+ // Pure Helper Functions
947
+ // ────────────────────────────────────────────────
948
+ /** Extract x-only (32-byte) public key from compressed (33-byte) or raw format */
949
+ function extractXOnlyPubkey(bytes) {
950
+ if (bytes.length === 33) {
951
+ // Compressed: drop the 02/03 prefix
952
+ if (bytes[0] !== 0x02 && bytes[0] !== 0x03) {
953
+ throw new Error(`Invalid compressed key prefix: 0x${bytes[0].toString(16)}`);
954
+ }
955
+ return bytes.slice(1, 33);
956
+ }
957
+ if (bytes.length === 32) {
958
+ return bytes;
959
+ }
960
+ if (bytes.length === 65) {
961
+ // Uncompressed: drop the 04 prefix, take x
962
+ if (bytes[0] !== 0x04) {
963
+ throw new Error(`Invalid uncompressed key prefix: 0x${bytes[0].toString(16)}`);
964
+ }
965
+ return bytes.slice(1, 33);
966
+ }
967
+ throw new Error(`Invalid public key length: ${bytes.length} (expected 32, 33, or 65)`);
968
+ }
969
+ /** Compute BIP-341 TapTweak hash for key-path only (no script tree) */
970
+ function computeTapTweak(internalKey) {
971
+ const tag = sha256(new TextEncoder().encode('TapTweak'));
972
+ const msg = new Uint8Array(tag.length * 2 + internalKey.length);
973
+ msg.set(tag, 0);
974
+ msg.set(tag, tag.length);
975
+ msg.set(internalKey, tag.length * 2);
976
+ return sha256(msg);
977
+ }
978
+ /**
979
+ * Tweak a public key per BIP-341: Q = lift_x(P) + int(tweak) * G
980
+ *
981
+ * Uses real secp256k1 EC point arithmetic. The output key Q is the
982
+ * x-only coordinate of the resulting point. If the resulting point
983
+ * has an odd y-coordinate, the tweak is negated (BIP-341 spec).
984
+ *
985
+ * @param internalKey - 32-byte x-only internal public key
986
+ * @param tweak - 32-byte tweak scalar (hash_TapTweak output)
987
+ * @returns 32-byte x-only output key
988
+ */
989
+ function tweakPublicKey(internalKey, tweak) {
990
+ // Lift the x-only key to a full point with even y
991
+ const P = Secp256k1.liftX(internalKey);
992
+ // Convert tweak bytes to a bigint scalar
993
+ const t = bytesToBigInt(tweak);
994
+ if (t >= Secp256k1.N) {
995
+ throw new Error('Tweak scalar exceeds curve order');
996
+ }
997
+ // Compute t * G
998
+ const tG = Secp256k1.mulG(t);
999
+ // Compute Q = P + t * G
1000
+ const Q = Secp256k1.pointAdd(P, tG);
1001
+ if (Q === null) {
1002
+ throw new Error('Tweaked key is point at infinity');
1003
+ }
1004
+ // Return x-coordinate of Q as 32 bytes
1005
+ return bigIntToBytes32(Q.x);
1006
+ }
1007
+ /** Get the Bech32 HRP for a Bitcoin network */
1008
+ function networkToHrp(network) {
1009
+ switch (network) {
1010
+ case 'mainnet': return 'bc';
1011
+ case 'testnet':
1012
+ case 'signet': return 'tb';
1013
+ case 'regtest': return 'bcrt';
1014
+ }
1015
+ }
1016
+ /** Encode data as Bech32m with witness version 1 */
1017
+ function encodeBech32m(hrp, data) {
1018
+ const data5 = [1]; // witness version 1
1019
+ // Convert 8-bit to 5-bit groups
1020
+ const converted = convertBits(Array.from(data), 8, 5, true);
1021
+ data5.push(...converted);
1022
+ // Compute Bech32m checksum
1023
+ const checksum = bech32mChecksum(hrp, data5);
1024
+ data5.push(...checksum);
1025
+ // Encode to Bech32 characters
1026
+ const charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
1027
+ let result = hrp + '1';
1028
+ for (const b of data5) {
1029
+ result += charset[b];
1030
+ }
1031
+ return result;
1032
+ }
1033
+ /** Decode a Bech32m address to get the 32-byte witness program */
1034
+ function decodeBech32m(address) {
1035
+ const charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
1036
+ const sepPos = address.lastIndexOf('1');
1037
+ if (sepPos < 1)
1038
+ throw new Error('No separator found in Bech32m address');
1039
+ const hrp = address.slice(0, sepPos);
1040
+ const dataPart = address.slice(sepPos + 1);
1041
+ if (dataPart.length < 7)
1042
+ throw new Error('Data part too short');
1043
+ // Decode characters to 5-bit values
1044
+ const data5 = [];
1045
+ for (const c of dataPart) {
1046
+ const idx = charset.indexOf(c);
1047
+ if (idx === -1)
1048
+ throw new Error(`Invalid Bech32m character: ${c}`);
1049
+ data5.push(idx);
1050
+ }
1051
+ // Verify checksum
1052
+ const verifyData = [...hrpExpand(hrp), ...data5];
1053
+ if (bech32Polymod(verifyData) !== 0x2bc830a3) {
1054
+ throw new Error('Invalid Bech32m checksum');
1055
+ }
1056
+ // Remove witness version (first) and checksum (last 6)
1057
+ const payload = data5.slice(1, data5.length - 6);
1058
+ // Convert 5-bit to 8-bit
1059
+ const bytes = convertBits(payload, 5, 8, false);
1060
+ if (bytes.length !== 32) {
1061
+ throw new Error(`Invalid witness program length: ${bytes.length} (expected 32)`);
1062
+ }
1063
+ return new Uint8Array(bytes);
1064
+ }
1065
+ /** Convert between bit widths */
1066
+ function convertBits(data, from, to, pad) {
1067
+ let acc = 0;
1068
+ let bits = 0;
1069
+ const result = [];
1070
+ const maxv = (1 << to) - 1;
1071
+ for (const value of data) {
1072
+ if (value >> from !== 0) {
1073
+ throw new Error(`Invalid value for ${from}-bit conversion: ${value}`);
1074
+ }
1075
+ acc = (acc << from) | value;
1076
+ bits += from;
1077
+ while (bits >= to) {
1078
+ bits -= to;
1079
+ result.push((acc >> bits) & maxv);
1080
+ }
1081
+ }
1082
+ if (pad) {
1083
+ if (bits > 0) {
1084
+ result.push((acc << (to - bits)) & maxv);
1085
+ }
1086
+ }
1087
+ else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
1088
+ throw new Error('Invalid padding');
1089
+ }
1090
+ return result;
1091
+ }
1092
+ /** Compute Bech32m checksum */
1093
+ function bech32mChecksum(hrp, data) {
1094
+ const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
1095
+ const polymod = bech32Polymod(values) ^ 0x2bc830a3;
1096
+ const checksum = [];
1097
+ for (let i = 0; i < 6; i++) {
1098
+ checksum.push((polymod >> (5 * (5 - i))) & 31);
1099
+ }
1100
+ return checksum;
1101
+ }
1102
+ /** Expand HRP for Bech32 polymod */
1103
+ function hrpExpand(hrp) {
1104
+ const result = [];
1105
+ for (const c of hrp) {
1106
+ result.push(c.charCodeAt(0) >> 5);
1107
+ }
1108
+ result.push(0);
1109
+ for (const c of hrp) {
1110
+ result.push(c.charCodeAt(0) & 31);
1111
+ }
1112
+ return result;
1113
+ }
1114
+ /** Bech32 polymod function */
1115
+ function bech32Polymod(values) {
1116
+ const generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
1117
+ let chk = 1;
1118
+ for (const v of values) {
1119
+ const top = chk >> 25;
1120
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
1121
+ for (let i = 0; i < 5; i++) {
1122
+ if ((top >> i) & 1) {
1123
+ chk ^= generator[i];
1124
+ }
1125
+ }
1126
+ }
1127
+ return chk;
1128
+ }
1129
+ // ── Byte manipulation helpers ──
1130
+ function hexToBytes(hex) {
1131
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
1132
+ const bytes = new Uint8Array(clean.length / 2);
1133
+ for (let i = 0; i < bytes.length; i++) {
1134
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1135
+ }
1136
+ return bytes;
1137
+ }
1138
+ function bytesToHex(bytes) {
1139
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
1140
+ return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
1141
+ }
1142
+ function pushLE32(buf, value) {
1143
+ buf.push(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff);
1144
+ }
1145
+ function pushLE64(buf, value) {
1146
+ // JavaScript safe integer limit is 2^53, sufficient for satoshis
1147
+ const low = value & 0xffffffff;
1148
+ const high = Math.floor(value / 0x100000000);
1149
+ pushLE32(buf, low);
1150
+ pushLE32(buf, high);
1151
+ }
1152
+ function pushVarint(buf, value) {
1153
+ if (value < 0xfd) {
1154
+ buf.push(value);
1155
+ }
1156
+ else if (value <= 0xffff) {
1157
+ buf.push(0xfd, value & 0xff, (value >> 8) & 0xff);
1158
+ }
1159
+ else if (value <= 0xffffffff) {
1160
+ buf.push(0xfe);
1161
+ pushLE32(buf, value);
1162
+ }
1163
+ else {
1164
+ buf.push(0xff);
1165
+ pushLE64(buf, value);
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Compute SHA-256 hash using Web Crypto API (synchronous fallback via Node.js crypto).
1170
+ *
1171
+ * Note: In a browser environment, this would use crypto.subtle.digest.
1172
+ * For Node.js, we use the built-in crypto module.
1173
+ */
1174
+ function sha256(data) {
1175
+ // Use Node.js crypto module
1176
+ const crypto = require('crypto');
1177
+ const hash = crypto.createHash('sha256');
1178
+ hash.update(data);
1179
+ return new Uint8Array(hash.digest());
1180
+ }
1181
+ /**
1182
+ * Compute txid: double SHA-256 of non-witness serialization, reversed.
1183
+ */
1184
+ function computeTxid(fullTxBytes, inputCount, outputs, adapter) {
1185
+ // Build non-witness serialization (strip marker, flag, and witness sections)
1186
+ const buf = [];
1187
+ // Version (first 4 bytes)
1188
+ buf.push(...fullTxBytes.slice(0, 4));
1189
+ // Skip marker (0x00) and flag (0x01) at bytes 4-5
1190
+ // Input count + inputs start at byte 6
1191
+ let pos = 6;
1192
+ // Input count varint
1193
+ const inputCountByte = fullTxBytes[pos];
1194
+ buf.push(inputCountByte);
1195
+ pos++;
1196
+ // Copy inputs (each: 32 txid + 4 vout + 1 scriptSig len + 0 scriptSig + 4 sequence = 41 bytes)
1197
+ for (let i = 0; i < inputCount; i++) {
1198
+ buf.push(...fullTxBytes.slice(pos, pos + 41));
1199
+ pos += 41;
1200
+ }
1201
+ // Output count
1202
+ const outputCountByte = fullTxBytes[pos];
1203
+ buf.push(outputCountByte);
1204
+ pos++;
1205
+ // Copy outputs
1206
+ for (const output of outputs) {
1207
+ // Value (8 bytes)
1208
+ buf.push(...fullTxBytes.slice(pos, pos + 8));
1209
+ pos += 8;
1210
+ // ScriptPubKey length varint
1211
+ const scriptLen = fullTxBytes[pos];
1212
+ buf.push(scriptLen);
1213
+ pos++;
1214
+ // ScriptPubKey data
1215
+ buf.push(...fullTxBytes.slice(pos, pos + scriptLen));
1216
+ pos += scriptLen;
1217
+ }
1218
+ // Skip witness data
1219
+ // Locktime (last 4 bytes of the full transaction)
1220
+ buf.push(...fullTxBytes.slice(fullTxBytes.length - 4));
1221
+ // Double SHA-256
1222
+ const first = sha256(new Uint8Array(buf));
1223
+ const second = sha256(first);
1224
+ // Reverse for display order
1225
+ const reversed = Array.from(second).reverse();
1226
+ return bytesToHex(new Uint8Array(reversed));
1227
+ }
1228
+ // ── Factory Functions ──
1229
+ /**
1230
+ * Create a Bitcoin Taproot adapter for mainnet.
1231
+ */
1232
+ function createBitcoinTaprootAdapter(options) {
1233
+ return new BitcoinTaprootAdapter({ ...options, network: 'mainnet' });
1234
+ }
1235
+ /**
1236
+ * Create a Bitcoin Taproot adapter for testnet.
1237
+ */
1238
+ function createBitcoinTestnetTaprootAdapter(options) {
1239
+ return new BitcoinTaprootAdapter({ ...options, network: 'testnet' });
1240
+ }
1241
+ //# sourceMappingURL=bitcoin-taproot.js.map