@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.
- package/README.md +137 -10
- package/dist/chains/bitcoin-taproot.d.ts +371 -0
- package/dist/chains/bitcoin-taproot.d.ts.map +1 -0
- package/dist/chains/bitcoin-taproot.js +1241 -0
- package/dist/chains/bitcoin-taproot.js.map +1 -0
- package/dist/chains/bitcoin.d.ts +12 -7
- package/dist/chains/bitcoin.d.ts.map +1 -1
- package/dist/chains/bitcoin.js +14 -9
- package/dist/chains/bitcoin.js.map +1 -1
- package/dist/core/client.d.ts +4 -5
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +54 -29
- package/dist/core/client.js.map +1 -1
- 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 +29 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/discovery.d.ts.map +1 -1
- package/dist/utils/discovery.js +56 -1
- package/dist/utils/discovery.js.map +1 -1
- package/dist/utils/eip712.d.ts +36 -0
- package/dist/utils/eip712.d.ts.map +1 -0
- package/dist/utils/eip712.js +80 -0
- package/dist/utils/eip712.js.map +1 -0
- package/dist/utils/fee.d.ts +2 -2
- package/dist/utils/fee.js +2 -2
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +18 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/websocket.js +1 -1
- package/dist/utils/websocket.js.map +1 -1
- package/dist/wallet/wallet.d.ts +23 -2
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +91 -31
- package/dist/wallet/wallet.js.map +1 -1
- 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
|