@sequence0/sdk 1.0.2 → 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.
- package/README.md +137 -10
- package/dist/chains/bitcoin-taproot.d.ts +308 -0
- package/dist/chains/bitcoin-taproot.d.ts.map +1 -0
- package/dist/chains/bitcoin-taproot.js +982 -0
- package/dist/chains/bitcoin-taproot.js.map +1 -0
- package/dist/erc4337/account.d.ts +241 -0
- package/dist/erc4337/account.d.ts.map +1 -0
- package/dist/erc4337/account.js +810 -0
- package/dist/erc4337/account.js.map +1 -0
- package/dist/erc4337/index.d.ts +9 -0
- package/dist/erc4337/index.d.ts.map +1 -0
- package/dist/erc4337/index.js +16 -0
- package/dist/erc4337/index.js.map +1 -0
- package/dist/erc4337/types.d.ts +173 -0
- package/dist/erc4337/types.d.ts.map +1 -0
- package/dist/erc4337/types.js +52 -0
- package/dist/erc4337/types.js.map +1 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +1 -9
- package/dist/utils/http.js.map +1 -1
- package/dist/wallet/wallet.d.ts +7 -0
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +18 -3
- package/dist/wallet/wallet.js.map +1 -1
- package/package.json +8 -2
|
@@ -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
|