bip388 1.0.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/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/index.cjs +1391 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1020 -0
- package/dist/index.d.ts +1020 -0
- package/dist/index.js +1305 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
// src/miniscript.ts
|
|
2
|
+
function pk(key) {
|
|
3
|
+
return {
|
|
4
|
+
type: "pk",
|
|
5
|
+
key,
|
|
6
|
+
toString() {
|
|
7
|
+
return `pk(${this.key})`;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function pkh(key) {
|
|
12
|
+
return {
|
|
13
|
+
type: "pkh",
|
|
14
|
+
key,
|
|
15
|
+
toString() {
|
|
16
|
+
return `pkh(${this.key})`;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function sha256(hash) {
|
|
21
|
+
validateHash(hash, 64, "sha256");
|
|
22
|
+
return {
|
|
23
|
+
type: "sha256",
|
|
24
|
+
hash,
|
|
25
|
+
toString() {
|
|
26
|
+
return `sha256(${this.hash})`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function hash256(hash) {
|
|
31
|
+
validateHash(hash, 64, "hash256");
|
|
32
|
+
return {
|
|
33
|
+
type: "hash256",
|
|
34
|
+
hash,
|
|
35
|
+
toString() {
|
|
36
|
+
return `hash256(${this.hash})`;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function hash160(hash) {
|
|
41
|
+
validateHash(hash, 40, "hash160");
|
|
42
|
+
return {
|
|
43
|
+
type: "hash160",
|
|
44
|
+
hash,
|
|
45
|
+
toString() {
|
|
46
|
+
return `hash160(${this.hash})`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function ripemd160(hash) {
|
|
51
|
+
validateHash(hash, 40, "ripemd160");
|
|
52
|
+
return {
|
|
53
|
+
type: "ripemd160",
|
|
54
|
+
hash,
|
|
55
|
+
toString() {
|
|
56
|
+
return `ripemd160(${this.hash})`;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function older(blocks) {
|
|
61
|
+
validateTimelock(blocks, "older");
|
|
62
|
+
return {
|
|
63
|
+
type: "older",
|
|
64
|
+
blocks,
|
|
65
|
+
toString() {
|
|
66
|
+
return `older(${this.blocks})`;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function after(time) {
|
|
71
|
+
validateTimelock(time, "after");
|
|
72
|
+
return {
|
|
73
|
+
type: "after",
|
|
74
|
+
time,
|
|
75
|
+
toString() {
|
|
76
|
+
return `after(${this.time})`;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function and_v(left, right) {
|
|
81
|
+
return {
|
|
82
|
+
type: "and_v",
|
|
83
|
+
left,
|
|
84
|
+
right,
|
|
85
|
+
toString() {
|
|
86
|
+
return `and_v(${this.left.toString()},${this.right.toString()})`;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function or_d(left, right) {
|
|
91
|
+
return {
|
|
92
|
+
type: "or_d",
|
|
93
|
+
left,
|
|
94
|
+
right,
|
|
95
|
+
toString() {
|
|
96
|
+
return `or_d(${this.left.toString()},${this.right.toString()})`;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function or_i(left, right) {
|
|
101
|
+
return {
|
|
102
|
+
type: "or_i",
|
|
103
|
+
left,
|
|
104
|
+
right,
|
|
105
|
+
toString() {
|
|
106
|
+
return `or_i(${this.left.toString()},${this.right.toString()})`;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function v(inner) {
|
|
111
|
+
return {
|
|
112
|
+
type: "v",
|
|
113
|
+
inner,
|
|
114
|
+
toString() {
|
|
115
|
+
return `v:${this.inner.toString()}`;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function multi(threshold, keys) {
|
|
120
|
+
validateMultisig(threshold, keys.length, "multi");
|
|
121
|
+
return {
|
|
122
|
+
type: "multi",
|
|
123
|
+
threshold,
|
|
124
|
+
keys,
|
|
125
|
+
toString() {
|
|
126
|
+
return `multi(${this.threshold},${this.keys.join(",")})`;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function sortedmulti(threshold, keys) {
|
|
131
|
+
validateMultisig(threshold, keys.length, "sortedmulti");
|
|
132
|
+
return {
|
|
133
|
+
type: "sortedmulti",
|
|
134
|
+
threshold,
|
|
135
|
+
keys,
|
|
136
|
+
toString() {
|
|
137
|
+
return `sortedmulti(${this.threshold},${this.keys.join(",")})`;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function multi_a(threshold, keys) {
|
|
142
|
+
validateMultisig(threshold, keys.length, "multi_a");
|
|
143
|
+
return {
|
|
144
|
+
type: "multi_a",
|
|
145
|
+
threshold,
|
|
146
|
+
keys,
|
|
147
|
+
toString() {
|
|
148
|
+
return `multi_a(${this.threshold},${this.keys.join(",")})`;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function sortedmulti_a(threshold, keys) {
|
|
153
|
+
validateMultisig(threshold, keys.length, "sortedmulti_a");
|
|
154
|
+
return {
|
|
155
|
+
type: "sortedmulti_a",
|
|
156
|
+
threshold,
|
|
157
|
+
keys,
|
|
158
|
+
toString() {
|
|
159
|
+
return `sortedmulti_a(${this.threshold},${this.keys.join(",")})`;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function validateHash(hash, expectedLength, name) {
|
|
164
|
+
if (!hash || typeof hash !== "string") {
|
|
165
|
+
throw new Error(`${name} must be a hex string`);
|
|
166
|
+
}
|
|
167
|
+
if (hash.length !== expectedLength) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`${name} must be ${expectedLength / 2} bytes (${expectedLength} hex chars), got ${hash.length}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (!/^[0-9a-fA-F]+$/.test(hash)) {
|
|
173
|
+
throw new Error(`${name} must be a valid hex string`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function validateTimelock(value, name) {
|
|
177
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
178
|
+
throw new Error(`${name} must be a positive integer`);
|
|
179
|
+
}
|
|
180
|
+
if (value > 2147483647) {
|
|
181
|
+
throw new Error(`${name} exceeds maximum value (2^31-1)`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function validateMultisig(threshold, numKeys, name) {
|
|
185
|
+
if (!Number.isInteger(threshold) || threshold <= 0) {
|
|
186
|
+
throw new Error(`${name} threshold must be a positive integer`);
|
|
187
|
+
}
|
|
188
|
+
if (numKeys < 1) {
|
|
189
|
+
throw new Error(`${name} requires at least one key`);
|
|
190
|
+
}
|
|
191
|
+
if (threshold > numKeys) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`${name} threshold (${threshold}) cannot exceed number of keys (${numKeys})`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (numKeys > 20 && (name === "multi" || name === "sortedmulti")) {
|
|
197
|
+
throw new Error(`${name} is limited to 20 keys in SegWit`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/bip388.ts
|
|
202
|
+
function keyPlaceholder(index) {
|
|
203
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
204
|
+
throw new Error(`Key index must be a non-negative integer, got: ${index}`);
|
|
205
|
+
}
|
|
206
|
+
return `@${index}/**`;
|
|
207
|
+
}
|
|
208
|
+
function trDescriptor(internalKeyIndex, tree) {
|
|
209
|
+
const internalKey = keyPlaceholder(internalKeyIndex);
|
|
210
|
+
if (!tree) {
|
|
211
|
+
return `tr(${internalKey})`;
|
|
212
|
+
}
|
|
213
|
+
const treeStr = serializeTapTree(tree);
|
|
214
|
+
return `tr(${internalKey},${treeStr})`;
|
|
215
|
+
}
|
|
216
|
+
function wshDescriptor(script) {
|
|
217
|
+
return `wsh(${script.toString()})`;
|
|
218
|
+
}
|
|
219
|
+
function wpkhDescriptor(keyIndex) {
|
|
220
|
+
return `wpkh(${keyPlaceholder(keyIndex)})`;
|
|
221
|
+
}
|
|
222
|
+
function buildTaprootPolicy(internalKeyIndex, tree, options) {
|
|
223
|
+
validatePolicyOptions(options);
|
|
224
|
+
return {
|
|
225
|
+
name: options.name,
|
|
226
|
+
template: trDescriptor(internalKeyIndex, tree),
|
|
227
|
+
keys: options.keys
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function buildSegwitPolicy(script, options) {
|
|
231
|
+
validatePolicyOptions(options);
|
|
232
|
+
return {
|
|
233
|
+
name: options.name,
|
|
234
|
+
template: wshDescriptor(script),
|
|
235
|
+
keys: options.keys
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function formatKeyInfo(info) {
|
|
239
|
+
let result = "";
|
|
240
|
+
if (info.fingerprint || info.originPath) {
|
|
241
|
+
result += "[";
|
|
242
|
+
if (info.fingerprint) {
|
|
243
|
+
result += info.fingerprint;
|
|
244
|
+
}
|
|
245
|
+
if (info.originPath) {
|
|
246
|
+
result += "/" + info.originPath;
|
|
247
|
+
}
|
|
248
|
+
result += "]";
|
|
249
|
+
}
|
|
250
|
+
result += info.xpub;
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
function bareKey(xpub) {
|
|
254
|
+
return { xpub };
|
|
255
|
+
}
|
|
256
|
+
function validatePolicyOptions(options) {
|
|
257
|
+
if (!options.name || typeof options.name !== "string") {
|
|
258
|
+
throw new Error("Policy name is required");
|
|
259
|
+
}
|
|
260
|
+
if (options.name.length > 64) {
|
|
261
|
+
throw new Error("Policy name should not exceed 64 characters");
|
|
262
|
+
}
|
|
263
|
+
if (!Array.isArray(options.keys) || options.keys.length === 0) {
|
|
264
|
+
throw new Error("At least one key is required");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function serializeTapTree(tree) {
|
|
268
|
+
if (Array.isArray(tree)) {
|
|
269
|
+
const [left, right] = tree;
|
|
270
|
+
return `{${serializeTapTree(left)},${serializeTapTree(right)}}`;
|
|
271
|
+
}
|
|
272
|
+
return tree.toString();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/patterns/htlc.ts
|
|
276
|
+
function buildTaprootHtlcPolicy(params) {
|
|
277
|
+
const { name, secretHash, timelock, numsKey, payeeKey, payerKey } = params;
|
|
278
|
+
validateHash2(secretHash, 64, "secretHash");
|
|
279
|
+
validateTimelock2(timelock, "timelock");
|
|
280
|
+
const secretLeaf = and_v(v(sha256(secretHash)), pk(keyPlaceholder(1)));
|
|
281
|
+
const timeoutLeaf = and_v(v(after(timelock)), pk(keyPlaceholder(2)));
|
|
282
|
+
const tree = [secretLeaf, timeoutLeaf];
|
|
283
|
+
return {
|
|
284
|
+
name,
|
|
285
|
+
template: trDescriptor(0, tree),
|
|
286
|
+
keys: [numsKey, payeeKey, payerKey]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function buildSegwitHtlcPolicy(params) {
|
|
290
|
+
const { name, secretHash, timelock, payeeKey, payerKey } = params;
|
|
291
|
+
validateHash2(secretHash, 64, "secretHash");
|
|
292
|
+
validateTimelock2(timelock, "timelock");
|
|
293
|
+
const secretPath = and_v(v(sha256(secretHash)), pk(keyPlaceholder(0)));
|
|
294
|
+
const timeoutPath = and_v(v(pk(keyPlaceholder(1))), after(timelock));
|
|
295
|
+
const script = or_i(secretPath, timeoutPath);
|
|
296
|
+
return {
|
|
297
|
+
name,
|
|
298
|
+
template: wshDescriptor(script),
|
|
299
|
+
keys: [payeeKey, payerKey]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function validateHash2(hash, expectedLength, name) {
|
|
303
|
+
if (!hash || typeof hash !== "string") {
|
|
304
|
+
throw new Error(`${name} must be a hex string`);
|
|
305
|
+
}
|
|
306
|
+
if (hash.length !== expectedLength) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`${name} must be ${expectedLength / 2} bytes (${expectedLength} hex chars), got ${hash.length}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (!/^[0-9a-fA-F]+$/.test(hash)) {
|
|
312
|
+
throw new Error(`${name} must be a valid hex string`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function validateTimelock2(value, name) {
|
|
316
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
317
|
+
throw new Error(`${name} must be a positive integer`);
|
|
318
|
+
}
|
|
319
|
+
if (value > 2147483647) {
|
|
320
|
+
throw new Error(`${name} exceeds maximum value (2^31-1)`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/patterns/multisig.ts
|
|
325
|
+
function buildSegwitMultisigPolicy(params) {
|
|
326
|
+
const { name, threshold, keys, sorted = true } = params;
|
|
327
|
+
if (keys.length === 0) {
|
|
328
|
+
throw new Error("At least one key is required");
|
|
329
|
+
}
|
|
330
|
+
const keyPlaceholders = keys.map((_, i) => keyPlaceholder(i));
|
|
331
|
+
const script = sorted ? sortedmulti(threshold, keyPlaceholders) : multi(threshold, keyPlaceholders);
|
|
332
|
+
return {
|
|
333
|
+
name,
|
|
334
|
+
template: wshDescriptor(script),
|
|
335
|
+
keys
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function buildTaprootMultisigPolicy(params) {
|
|
339
|
+
const { name, threshold, keys, sorted = true } = params;
|
|
340
|
+
if (keys.length < 2) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Taproot multisig requires at least 2 keys (internal key + script key)"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const scriptKeyPlaceholders = keys.slice(1).map((_, i) => keyPlaceholder(i + 1));
|
|
346
|
+
const script = sorted ? sortedmulti_a(threshold, scriptKeyPlaceholders) : multi_a(threshold, scriptKeyPlaceholders);
|
|
347
|
+
return {
|
|
348
|
+
name,
|
|
349
|
+
template: trDescriptor(0, script),
|
|
350
|
+
keys
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/scripts/pk.ts
|
|
355
|
+
import { script as bscript, opcodes } from "bitcoinjs-lib";
|
|
356
|
+
function pkScript(pubkey) {
|
|
357
|
+
if (pubkey.length !== 33 && pubkey.length !== 32) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Invalid pubkey length: ${pubkey.length} (expected 32 or 33 bytes)`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return Buffer.from(bscript.compile([pubkey, opcodes.OP_CHECKSIG]));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/scripts/pkh.ts
|
|
366
|
+
import { script as bscript2, opcodes as opcodes2, crypto } from "bitcoinjs-lib";
|
|
367
|
+
function pkhScript(pubkey) {
|
|
368
|
+
if (pubkey.length !== 33) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Invalid pubkey length: ${pubkey.length} (expected 33 bytes for compressed pubkey)`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const pubkeyHash = crypto.hash160(pubkey);
|
|
374
|
+
return Buffer.from(
|
|
375
|
+
bscript2.compile([
|
|
376
|
+
opcodes2.OP_DUP,
|
|
377
|
+
opcodes2.OP_HASH160,
|
|
378
|
+
pubkeyHash,
|
|
379
|
+
opcodes2.OP_EQUALVERIFY,
|
|
380
|
+
opcodes2.OP_CHECKSIG
|
|
381
|
+
])
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
function pkhScriptFromHash(pubkeyHash) {
|
|
385
|
+
if (pubkeyHash.length !== 20) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Invalid pubkey hash length: ${pubkeyHash.length} (expected 20 bytes)`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return Buffer.from(
|
|
391
|
+
bscript2.compile([
|
|
392
|
+
opcodes2.OP_DUP,
|
|
393
|
+
opcodes2.OP_HASH160,
|
|
394
|
+
pubkeyHash,
|
|
395
|
+
opcodes2.OP_EQUALVERIFY,
|
|
396
|
+
opcodes2.OP_CHECKSIG
|
|
397
|
+
])
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/scripts/multi.ts
|
|
402
|
+
import { script as bscript3, opcodes as opcodes3 } from "bitcoinjs-lib";
|
|
403
|
+
function encodeScriptInt(n) {
|
|
404
|
+
if (n === 0) return opcodes3.OP_0;
|
|
405
|
+
if (n >= 1 && n <= 16) return opcodes3.OP_1 - 1 + n;
|
|
406
|
+
return Buffer.from(bscript3.number.encode(n));
|
|
407
|
+
}
|
|
408
|
+
function multiScript(threshold, pubkeys, sorted = false) {
|
|
409
|
+
if (threshold <= 0 || !Number.isInteger(threshold)) {
|
|
410
|
+
throw new Error("threshold must be a positive integer");
|
|
411
|
+
}
|
|
412
|
+
if (pubkeys.length === 0) {
|
|
413
|
+
throw new Error("At least one pubkey is required");
|
|
414
|
+
}
|
|
415
|
+
if (threshold > pubkeys.length) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`threshold (${threshold}) cannot exceed number of pubkeys (${pubkeys.length})`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (pubkeys.length > 20) {
|
|
421
|
+
throw new Error("SegWit multisig is limited to 20 keys");
|
|
422
|
+
}
|
|
423
|
+
for (const pk2 of pubkeys) {
|
|
424
|
+
if (pk2.length !== 33) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Invalid pubkey length: ${pk2.length} (expected 33 bytes for SegWit)`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const keys = sorted ? [...pubkeys].sort(Buffer.compare) : pubkeys;
|
|
431
|
+
return Buffer.from(
|
|
432
|
+
bscript3.compile([
|
|
433
|
+
encodeScriptInt(threshold),
|
|
434
|
+
...keys,
|
|
435
|
+
encodeScriptInt(keys.length),
|
|
436
|
+
opcodes3.OP_CHECKMULTISIG
|
|
437
|
+
])
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
function sortedMultiScript(threshold, pubkeys) {
|
|
441
|
+
return multiScript(threshold, pubkeys, true);
|
|
442
|
+
}
|
|
443
|
+
function bareMultiScript(threshold, pubkeys, sorted = false) {
|
|
444
|
+
if (threshold <= 0 || !Number.isInteger(threshold)) {
|
|
445
|
+
throw new Error("threshold must be a positive integer");
|
|
446
|
+
}
|
|
447
|
+
if (pubkeys.length === 0) {
|
|
448
|
+
throw new Error("At least one pubkey is required");
|
|
449
|
+
}
|
|
450
|
+
if (threshold > pubkeys.length) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`threshold (${threshold}) cannot exceed number of pubkeys (${pubkeys.length})`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
for (const pk2 of pubkeys) {
|
|
456
|
+
if (pk2.length !== 33 && pk2.length !== 65) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Invalid pubkey length: ${pk2.length} (expected 33 or 65 bytes)`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const keys = sorted ? [...pubkeys].sort(Buffer.compare) : pubkeys;
|
|
463
|
+
return Buffer.from(
|
|
464
|
+
bscript3.compile([
|
|
465
|
+
encodeScriptInt(threshold),
|
|
466
|
+
...keys,
|
|
467
|
+
encodeScriptInt(keys.length),
|
|
468
|
+
opcodes3.OP_CHECKMULTISIG
|
|
469
|
+
])
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
function sortedBareMultiScript(threshold, pubkeys) {
|
|
473
|
+
return bareMultiScript(threshold, pubkeys, true);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/scripts/hashlock.ts
|
|
477
|
+
import { script as bscript4, opcodes as opcodes4 } from "bitcoinjs-lib";
|
|
478
|
+
function getHashOpcode(hashType) {
|
|
479
|
+
switch (hashType) {
|
|
480
|
+
case "sha256":
|
|
481
|
+
return opcodes4.OP_SHA256;
|
|
482
|
+
case "hash256":
|
|
483
|
+
return opcodes4.OP_HASH256;
|
|
484
|
+
case "hash160":
|
|
485
|
+
return opcodes4.OP_HASH160;
|
|
486
|
+
case "ripemd160":
|
|
487
|
+
return opcodes4.OP_RIPEMD160;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function getHashLength(hashType) {
|
|
491
|
+
switch (hashType) {
|
|
492
|
+
case "sha256":
|
|
493
|
+
case "hash256":
|
|
494
|
+
return 32;
|
|
495
|
+
case "hash160":
|
|
496
|
+
case "ripemd160":
|
|
497
|
+
return 20;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function hashlockScript(hash, pubkey, hashType = "sha256") {
|
|
501
|
+
const expectedLen = getHashLength(hashType);
|
|
502
|
+
if (hash.length !== expectedLen) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`Invalid hash length for ${hashType}: ${hash.length} (expected ${expectedLen} bytes)`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (pubkey.length !== 32 && pubkey.length !== 33) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Invalid pubkey length: ${pubkey.length} (expected 32 or 33 bytes)`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return Buffer.from(
|
|
513
|
+
bscript4.compile([
|
|
514
|
+
// v:sha256(H) - verify wrapper with SIZE check for preimage length
|
|
515
|
+
opcodes4.OP_SIZE,
|
|
516
|
+
bscript4.number.encode(expectedLen),
|
|
517
|
+
opcodes4.OP_EQUALVERIFY,
|
|
518
|
+
// Hash check
|
|
519
|
+
getHashOpcode(hashType),
|
|
520
|
+
hash,
|
|
521
|
+
opcodes4.OP_EQUALVERIFY,
|
|
522
|
+
// pk(K)
|
|
523
|
+
pubkey,
|
|
524
|
+
opcodes4.OP_CHECKSIG
|
|
525
|
+
])
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function simpleHashlockScript(hash, hashType = "sha256") {
|
|
529
|
+
const expectedLen = getHashLength(hashType);
|
|
530
|
+
if (hash.length !== expectedLen) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`Invalid hash length for ${hashType}: ${hash.length} (expected ${expectedLen} bytes)`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return Buffer.from(
|
|
536
|
+
bscript4.compile([
|
|
537
|
+
opcodes4.OP_SIZE,
|
|
538
|
+
bscript4.number.encode(expectedLen),
|
|
539
|
+
opcodes4.OP_EQUALVERIFY,
|
|
540
|
+
getHashOpcode(hashType),
|
|
541
|
+
hash,
|
|
542
|
+
opcodes4.OP_EQUAL
|
|
543
|
+
])
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/scripts/timelock.ts
|
|
548
|
+
import { script as bscript5, opcodes as opcodes5 } from "bitcoinjs-lib";
|
|
549
|
+
function encodeScriptNumber(n) {
|
|
550
|
+
if (n === 0) {
|
|
551
|
+
return opcodes5.OP_0;
|
|
552
|
+
}
|
|
553
|
+
if (n >= 1 && n <= 16) {
|
|
554
|
+
return opcodes5.OP_1 - 1 + n;
|
|
555
|
+
}
|
|
556
|
+
return Buffer.from(bscript5.number.encode(n));
|
|
557
|
+
}
|
|
558
|
+
function cltvScript(locktime, pubkey, useDrop = false) {
|
|
559
|
+
if (locktime <= 0) {
|
|
560
|
+
throw new Error("locktime must be a positive number");
|
|
561
|
+
}
|
|
562
|
+
if (locktime > 2147483647) {
|
|
563
|
+
throw new Error("locktime exceeds maximum value (2^31-1)");
|
|
564
|
+
}
|
|
565
|
+
if (pubkey.length !== 32 && pubkey.length !== 33) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`Invalid pubkey length: ${pubkey.length} (expected 32 or 33 bytes)`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return Buffer.from(
|
|
571
|
+
bscript5.compile([
|
|
572
|
+
encodeScriptNumber(locktime),
|
|
573
|
+
opcodes5.OP_CHECKLOCKTIMEVERIFY,
|
|
574
|
+
useDrop ? opcodes5.OP_DROP : opcodes5.OP_VERIFY,
|
|
575
|
+
pubkey,
|
|
576
|
+
opcodes5.OP_CHECKSIG
|
|
577
|
+
])
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
function csvScript(sequence, pubkey, useDrop = false) {
|
|
581
|
+
if (sequence <= 0) {
|
|
582
|
+
throw new Error("sequence must be a positive number");
|
|
583
|
+
}
|
|
584
|
+
if (sequence > 2147483647) {
|
|
585
|
+
throw new Error("sequence exceeds maximum value (2^31-1)");
|
|
586
|
+
}
|
|
587
|
+
if (pubkey.length !== 32 && pubkey.length !== 33) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`Invalid pubkey length: ${pubkey.length} (expected 32 or 33 bytes)`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return Buffer.from(
|
|
593
|
+
bscript5.compile([
|
|
594
|
+
encodeScriptNumber(sequence),
|
|
595
|
+
opcodes5.OP_CHECKSEQUENCEVERIFY,
|
|
596
|
+
useDrop ? opcodes5.OP_DROP : opcodes5.OP_VERIFY,
|
|
597
|
+
pubkey,
|
|
598
|
+
opcodes5.OP_CHECKSIG
|
|
599
|
+
])
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function simpleCltvScript(locktime) {
|
|
603
|
+
if (locktime <= 0) {
|
|
604
|
+
throw new Error("locktime must be a positive number");
|
|
605
|
+
}
|
|
606
|
+
if (locktime > 2147483647) {
|
|
607
|
+
throw new Error("locktime exceeds maximum value (2^31-1)");
|
|
608
|
+
}
|
|
609
|
+
return Buffer.from(
|
|
610
|
+
bscript5.compile([
|
|
611
|
+
encodeScriptNumber(locktime),
|
|
612
|
+
opcodes5.OP_CHECKLOCKTIMEVERIFY,
|
|
613
|
+
opcodes5.OP_DROP,
|
|
614
|
+
opcodes5.OP_TRUE
|
|
615
|
+
])
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/keys.ts
|
|
620
|
+
import * as ecc from "@bitcoinerlab/secp256k1";
|
|
621
|
+
import BIP32Factory from "bip32";
|
|
622
|
+
import { networks } from "bitcoinjs-lib";
|
|
623
|
+
|
|
624
|
+
// src/types.ts
|
|
625
|
+
function isXpubKey(key) {
|
|
626
|
+
return "xpub" in key;
|
|
627
|
+
}
|
|
628
|
+
function isRawKey(key) {
|
|
629
|
+
return "pubkey" in key;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/keys.ts
|
|
633
|
+
var bip32 = BIP32Factory(ecc);
|
|
634
|
+
function getNetwork(name) {
|
|
635
|
+
switch (name) {
|
|
636
|
+
case "mainnet":
|
|
637
|
+
return networks.bitcoin;
|
|
638
|
+
case "testnet":
|
|
639
|
+
return networks.testnet;
|
|
640
|
+
case "regtest":
|
|
641
|
+
return networks.regtest;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function deriveFromXpub(xpub, change, index, network) {
|
|
645
|
+
const node = bip32.fromBase58(xpub, network);
|
|
646
|
+
return node.derive(change).derive(index);
|
|
647
|
+
}
|
|
648
|
+
function resolveCompressedPubkey(key, network) {
|
|
649
|
+
if (isXpubKey(key)) {
|
|
650
|
+
const net = getNetwork(network);
|
|
651
|
+
const child = deriveFromXpub(
|
|
652
|
+
key.xpub,
|
|
653
|
+
key.change ?? 0,
|
|
654
|
+
key.index ?? 0,
|
|
655
|
+
net
|
|
656
|
+
);
|
|
657
|
+
return Buffer.from(child.publicKey);
|
|
658
|
+
} else {
|
|
659
|
+
return parsePublicKey(key.pubkey);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function resolveXOnlyPubkey(key, network) {
|
|
663
|
+
const compressed = resolveCompressedPubkey(key, network);
|
|
664
|
+
return toXOnly(compressed);
|
|
665
|
+
}
|
|
666
|
+
function toXOnly(pubkey) {
|
|
667
|
+
if (pubkey.length === 32) {
|
|
668
|
+
return pubkey;
|
|
669
|
+
}
|
|
670
|
+
if (pubkey.length === 33) {
|
|
671
|
+
return pubkey.subarray(1, 33);
|
|
672
|
+
}
|
|
673
|
+
if (pubkey.length === 65) {
|
|
674
|
+
return pubkey.subarray(1, 33);
|
|
675
|
+
}
|
|
676
|
+
throw new Error(`Invalid public key length: ${pubkey.length}`);
|
|
677
|
+
}
|
|
678
|
+
function parsePublicKey(pubkeyHex) {
|
|
679
|
+
const buf = Buffer.from(pubkeyHex, "hex");
|
|
680
|
+
if (buf.length === 33) {
|
|
681
|
+
if (buf[0] !== 2 && buf[0] !== 3) {
|
|
682
|
+
throw new Error("Compressed public key must start with 02 or 03");
|
|
683
|
+
}
|
|
684
|
+
if (!isValidPoint(buf)) {
|
|
685
|
+
throw new Error("Invalid public key: not a valid curve point");
|
|
686
|
+
}
|
|
687
|
+
return buf;
|
|
688
|
+
}
|
|
689
|
+
if (buf.length === 32) {
|
|
690
|
+
const compressed = Buffer.concat([Buffer.from([2]), buf]);
|
|
691
|
+
if (!isValidPoint(compressed)) {
|
|
692
|
+
throw new Error("Invalid x-only public key: not a valid curve point");
|
|
693
|
+
}
|
|
694
|
+
return compressed;
|
|
695
|
+
}
|
|
696
|
+
if (buf.length === 65) {
|
|
697
|
+
if (buf[0] !== 4) {
|
|
698
|
+
throw new Error("Uncompressed public key must start with 04");
|
|
699
|
+
}
|
|
700
|
+
const prefix = (buf[64] & 1) === 0 ? 2 : 3;
|
|
701
|
+
const compressed = Buffer.concat([
|
|
702
|
+
Buffer.from([prefix]),
|
|
703
|
+
buf.subarray(1, 33)
|
|
704
|
+
]);
|
|
705
|
+
if (!isValidPoint(compressed)) {
|
|
706
|
+
throw new Error("Invalid public key: not a valid curve point");
|
|
707
|
+
}
|
|
708
|
+
return compressed;
|
|
709
|
+
}
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Invalid public key length: ${buf.length} bytes (expected 32, 33, or 65)`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
function isValidPoint(pubkey) {
|
|
715
|
+
try {
|
|
716
|
+
return ecc.isPoint(pubkey);
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function toKeyInfo(key) {
|
|
722
|
+
return {
|
|
723
|
+
fingerprint: key.fingerprint,
|
|
724
|
+
originPath: key.originPath,
|
|
725
|
+
xpub: key.xpub
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function extractKeyInfos(keys) {
|
|
729
|
+
const infos = [];
|
|
730
|
+
for (const key of keys) {
|
|
731
|
+
if (!isXpubKey(key)) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
infos.push(toKeyInfo(key));
|
|
735
|
+
}
|
|
736
|
+
return infos;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/patterns/taproot/htlc.ts
|
|
740
|
+
import * as ecc3 from "@bitcoinerlab/secp256k1";
|
|
741
|
+
import {
|
|
742
|
+
initEccLib,
|
|
743
|
+
payments,
|
|
744
|
+
opcodes as opcodes7,
|
|
745
|
+
script as bscript7
|
|
746
|
+
} from "bitcoinjs-lib";
|
|
747
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
748
|
+
|
|
749
|
+
// src/patterns/taproot/nums.ts
|
|
750
|
+
var NUMS = {
|
|
751
|
+
/** Compressed public key (33 bytes as hex) */
|
|
752
|
+
pubkey: "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
|
|
753
|
+
/** X-only public key (32 bytes as hex) - for taproot internal key */
|
|
754
|
+
xonly: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
|
|
755
|
+
/** X-only as Buffer */
|
|
756
|
+
get xonlyBuffer() {
|
|
757
|
+
return Buffer.from(this.xonly, "hex");
|
|
758
|
+
},
|
|
759
|
+
/** Compressed as Buffer */
|
|
760
|
+
get pubkeyBuffer() {
|
|
761
|
+
return Buffer.from(this.pubkey, "hex");
|
|
762
|
+
},
|
|
763
|
+
/**
|
|
764
|
+
* Get NUMS xpub for a specific network.
|
|
765
|
+
* Constructed with:
|
|
766
|
+
* - Public key: NUMS point
|
|
767
|
+
* - Chain code: 32 zero bytes
|
|
768
|
+
* - Depth/parent/index: all zeros (acts like master key)
|
|
769
|
+
*/
|
|
770
|
+
xpub(network) {
|
|
771
|
+
switch (network) {
|
|
772
|
+
case "mainnet":
|
|
773
|
+
return "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QgnecKFpJFPpdzxKrwoaZoV44qAJewsc4kX9vGaCaBExuvJH57";
|
|
774
|
+
case "testnet":
|
|
775
|
+
case "regtest":
|
|
776
|
+
return "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN";
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// src/patterns/taproot/script.ts
|
|
782
|
+
import * as ecc2 from "@bitcoinerlab/secp256k1";
|
|
783
|
+
import { crypto as crypto2, script as bscript6 } from "bitcoinjs-lib";
|
|
784
|
+
var LEAF_VERSION = 192;
|
|
785
|
+
function tapLeafHash(script) {
|
|
786
|
+
return Buffer.from(
|
|
787
|
+
crypto2.taggedHash(
|
|
788
|
+
"TapLeaf",
|
|
789
|
+
Buffer.concat([Buffer.from([LEAF_VERSION]), serializeScript(script)])
|
|
790
|
+
)
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
function tapBranchHash(left, right) {
|
|
794
|
+
const [first, second] = Buffer.compare(left, right) <= 0 ? [left, right] : [right, left];
|
|
795
|
+
return Buffer.from(
|
|
796
|
+
crypto2.taggedHash("TapBranch", Buffer.concat([first, second]))
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
function tapTweak(internalPubkey, merkleRoot) {
|
|
800
|
+
return Buffer.from(
|
|
801
|
+
crypto2.taggedHash("TapTweak", Buffer.concat([internalPubkey, merkleRoot]))
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
function serializeScript(script) {
|
|
805
|
+
const len = varintEncode(script.length);
|
|
806
|
+
return Buffer.concat([len, script]);
|
|
807
|
+
}
|
|
808
|
+
function buildControlBlock(internalPubkey, leafVersion, merklePath) {
|
|
809
|
+
const parityByte = leafVersion;
|
|
810
|
+
return Buffer.concat([
|
|
811
|
+
Buffer.from([parityByte]),
|
|
812
|
+
internalPubkey,
|
|
813
|
+
...merklePath
|
|
814
|
+
]);
|
|
815
|
+
}
|
|
816
|
+
function varintEncode(n) {
|
|
817
|
+
if (n < 253) {
|
|
818
|
+
const buf = Buffer.allocUnsafe(1);
|
|
819
|
+
buf.writeUInt8(n, 0);
|
|
820
|
+
return buf;
|
|
821
|
+
} else if (n <= 65535) {
|
|
822
|
+
const buf = Buffer.allocUnsafe(3);
|
|
823
|
+
buf.writeUInt8(253, 0);
|
|
824
|
+
buf.writeUInt16LE(n, 1);
|
|
825
|
+
return buf;
|
|
826
|
+
} else if (n <= 4294967295) {
|
|
827
|
+
const buf = Buffer.allocUnsafe(5);
|
|
828
|
+
buf.writeUInt8(254, 0);
|
|
829
|
+
buf.writeUInt32LE(n, 1);
|
|
830
|
+
return buf;
|
|
831
|
+
} else {
|
|
832
|
+
const buf = Buffer.allocUnsafe(9);
|
|
833
|
+
buf.writeUInt8(255, 0);
|
|
834
|
+
buf.writeUInt32LE(n >>> 0, 1);
|
|
835
|
+
buf.writeUInt32LE(n / 4294967296 | 0, 5);
|
|
836
|
+
return buf;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function encodeScriptNumber2(n) {
|
|
840
|
+
return Buffer.from(bscript6.number.encode(n));
|
|
841
|
+
}
|
|
842
|
+
function buildP2TROutput(outputKey) {
|
|
843
|
+
return Buffer.concat([
|
|
844
|
+
Buffer.from([81, 32]),
|
|
845
|
+
// OP_1, push 32 bytes
|
|
846
|
+
outputKey
|
|
847
|
+
]);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/patterns/taproot/htlc.ts
|
|
851
|
+
initEccLib(ecc3);
|
|
852
|
+
function taprootHtlc(params) {
|
|
853
|
+
const { payee, payer, secretHash, timelock, network, name } = params;
|
|
854
|
+
if (!secretHash || secretHash.length !== 64) {
|
|
855
|
+
throw new Error("secretHash must be 32 bytes (64 hex chars)");
|
|
856
|
+
}
|
|
857
|
+
if (timelock <= 0) {
|
|
858
|
+
throw new Error("timelock must be a positive number");
|
|
859
|
+
}
|
|
860
|
+
const payeeXOnly = resolveXOnlyPubkey(payee, network);
|
|
861
|
+
const payerXOnly = resolveXOnlyPubkey(payer, network);
|
|
862
|
+
const hashBuffer = Buffer.from(secretHash, "hex");
|
|
863
|
+
const secretScript = buildHashLockScript(hashBuffer, payeeXOnly);
|
|
864
|
+
const timeoutScript = buildTimelockScript(timelock, payerXOnly);
|
|
865
|
+
const secretLeafHash = tapLeafHash(secretScript);
|
|
866
|
+
const timeoutLeafHash = tapLeafHash(timeoutScript);
|
|
867
|
+
const merkleRoot = tapBranchHash(secretLeafHash, timeoutLeafHash);
|
|
868
|
+
const net = getNetwork(network);
|
|
869
|
+
const numsXpub = NUMS.xpub(network);
|
|
870
|
+
const derivedNums = deriveFromXpub(numsXpub, 0, 0, net);
|
|
871
|
+
const internalPubkey = toXOnly(Buffer.from(derivedNums.publicKey));
|
|
872
|
+
const tweak = tapTweak(internalPubkey, merkleRoot);
|
|
873
|
+
const tweakedKey = ecc3.xOnlyPointAddTweak(internalPubkey, tweak);
|
|
874
|
+
if (!tweakedKey) {
|
|
875
|
+
throw new Error("Failed to tweak internal key");
|
|
876
|
+
}
|
|
877
|
+
const outputKey = Buffer.from(tweakedKey.xOnlyPubkey);
|
|
878
|
+
const scriptPubkey = buildP2TROutput(outputKey);
|
|
879
|
+
const address = payments.p2tr({
|
|
880
|
+
pubkey: outputKey,
|
|
881
|
+
network: net
|
|
882
|
+
}).address;
|
|
883
|
+
const secretControlBlock = buildControlBlock(
|
|
884
|
+
internalPubkey,
|
|
885
|
+
192 | (tweakedKey.parity ? 1 : 0),
|
|
886
|
+
[timeoutLeafHash]
|
|
887
|
+
// path to sibling
|
|
888
|
+
);
|
|
889
|
+
const timeoutControlBlock = buildControlBlock(
|
|
890
|
+
internalPubkey,
|
|
891
|
+
192 | (tweakedKey.parity ? 1 : 0),
|
|
892
|
+
[secretLeafHash]
|
|
893
|
+
// path to sibling
|
|
894
|
+
);
|
|
895
|
+
let policy = null;
|
|
896
|
+
if (isXpubKey(payee) && isXpubKey(payer)) {
|
|
897
|
+
policy = buildTaprootHtlcPolicy({
|
|
898
|
+
name: name || "HTLC",
|
|
899
|
+
secretHash,
|
|
900
|
+
timelock,
|
|
901
|
+
numsKey: bareKey(NUMS.xpub(network)),
|
|
902
|
+
payeeKey: toKeyInfo(payee),
|
|
903
|
+
payerKey: toKeyInfo(payer)
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
address,
|
|
908
|
+
scriptPubkey,
|
|
909
|
+
internalPubkey,
|
|
910
|
+
merkleRoot,
|
|
911
|
+
tweak,
|
|
912
|
+
outputKey,
|
|
913
|
+
timelock,
|
|
914
|
+
secretLeaf: {
|
|
915
|
+
script: secretScript,
|
|
916
|
+
hash: secretLeafHash
|
|
917
|
+
},
|
|
918
|
+
timeoutLeaf: {
|
|
919
|
+
script: timeoutScript,
|
|
920
|
+
hash: timeoutLeafHash
|
|
921
|
+
},
|
|
922
|
+
controlBlock(leaf) {
|
|
923
|
+
return leaf === "secret" ? secretControlBlock : timeoutControlBlock;
|
|
924
|
+
},
|
|
925
|
+
witness: {
|
|
926
|
+
secret(signature, preimage) {
|
|
927
|
+
return [signature, preimage, secretScript, secretControlBlock];
|
|
928
|
+
},
|
|
929
|
+
timeout(signature) {
|
|
930
|
+
return [signature, timeoutScript, timeoutControlBlock];
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
policy
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function extractSecret(witness, secretHash) {
|
|
937
|
+
if (!witness || witness.length < 4) {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const preimage = witness[1];
|
|
941
|
+
if (preimage.length !== 32) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
if (secretHash) {
|
|
945
|
+
const computed = Buffer.from(sha2562(preimage)).toString("hex");
|
|
946
|
+
if (computed !== secretHash) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return preimage.toString("hex");
|
|
951
|
+
}
|
|
952
|
+
function buildHashLockScript(hash, pubkey, hashOp = opcodes7.OP_SHA256) {
|
|
953
|
+
return Buffer.from(
|
|
954
|
+
bscript7.compile([
|
|
955
|
+
// v:sha256(H) - verify wrapper adds SIZE check for 32-byte preimage
|
|
956
|
+
opcodes7.OP_SIZE,
|
|
957
|
+
bscript7.number.encode(32),
|
|
958
|
+
// Push number 32
|
|
959
|
+
opcodes7.OP_EQUALVERIFY,
|
|
960
|
+
// sha256(H)
|
|
961
|
+
hashOp,
|
|
962
|
+
hash,
|
|
963
|
+
opcodes7.OP_EQUALVERIFY,
|
|
964
|
+
// pk(K)
|
|
965
|
+
pubkey,
|
|
966
|
+
opcodes7.OP_CHECKSIG
|
|
967
|
+
])
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
function buildTimelockScript(timelock, pubkey) {
|
|
971
|
+
return Buffer.from(
|
|
972
|
+
bscript7.compile([
|
|
973
|
+
encodeScriptNumber2(timelock),
|
|
974
|
+
opcodes7.OP_CHECKLOCKTIMEVERIFY,
|
|
975
|
+
opcodes7.OP_VERIFY,
|
|
976
|
+
pubkey,
|
|
977
|
+
opcodes7.OP_CHECKSIG
|
|
978
|
+
])
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/patterns/taproot/multisig.ts
|
|
983
|
+
import * as ecc4 from "@bitcoinerlab/secp256k1";
|
|
984
|
+
import {
|
|
985
|
+
initEccLib as initEccLib2,
|
|
986
|
+
payments as payments2,
|
|
987
|
+
opcodes as opcodes8,
|
|
988
|
+
script as bscript8
|
|
989
|
+
} from "bitcoinjs-lib";
|
|
990
|
+
initEccLib2(ecc4);
|
|
991
|
+
function taprootMultisig(params) {
|
|
992
|
+
const { threshold, keys, sorted = true, network, name } = params;
|
|
993
|
+
if (threshold <= 0 || !Number.isInteger(threshold)) {
|
|
994
|
+
throw new Error("threshold must be a positive integer");
|
|
995
|
+
}
|
|
996
|
+
if (keys.length === 0) {
|
|
997
|
+
throw new Error("At least one key is required");
|
|
998
|
+
}
|
|
999
|
+
if (threshold > keys.length) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`threshold (${threshold}) cannot exceed number of keys (${keys.length})`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
const net = getNetwork(network);
|
|
1005
|
+
let pubkeys = keys.map((key) => resolveXOnlyPubkey(key, network));
|
|
1006
|
+
let sortedIndexes = pubkeys.map((_, i) => i);
|
|
1007
|
+
if (sorted) {
|
|
1008
|
+
const indexed = pubkeys.map((pk2, i) => ({ pk: pk2, i }));
|
|
1009
|
+
indexed.sort(
|
|
1010
|
+
(a, b) => Buffer.compare(a.pk, b.pk)
|
|
1011
|
+
);
|
|
1012
|
+
pubkeys = indexed.map((x) => x.pk);
|
|
1013
|
+
sortedIndexes = indexed.map((x) => x.i);
|
|
1014
|
+
}
|
|
1015
|
+
const scriptParts = [];
|
|
1016
|
+
scriptParts.push(pubkeys[0]);
|
|
1017
|
+
scriptParts.push(opcodes8.OP_CHECKSIG);
|
|
1018
|
+
for (let i = 1; i < pubkeys.length; i++) {
|
|
1019
|
+
scriptParts.push(pubkeys[i]);
|
|
1020
|
+
scriptParts.push(opcodes8.OP_CHECKSIGADD);
|
|
1021
|
+
}
|
|
1022
|
+
scriptParts.push(encodeScriptNumber(threshold));
|
|
1023
|
+
scriptParts.push(opcodes8.OP_NUMEQUAL);
|
|
1024
|
+
const multisigScript = Buffer.from(bscript8.compile(scriptParts));
|
|
1025
|
+
const leafHash = tapLeafHash(multisigScript);
|
|
1026
|
+
const internalPubkey = NUMS.xonlyBuffer;
|
|
1027
|
+
const merkleRoot = leafHash;
|
|
1028
|
+
const tweak = tapTweak(internalPubkey, merkleRoot);
|
|
1029
|
+
const tweakedKey = ecc4.xOnlyPointAddTweak(internalPubkey, tweak);
|
|
1030
|
+
if (!tweakedKey) {
|
|
1031
|
+
throw new Error("Failed to tweak internal key");
|
|
1032
|
+
}
|
|
1033
|
+
const outputKey = Buffer.from(tweakedKey.xOnlyPubkey);
|
|
1034
|
+
const scriptPubkey = buildP2TROutput(outputKey);
|
|
1035
|
+
const address = payments2.p2tr({
|
|
1036
|
+
pubkey: outputKey,
|
|
1037
|
+
network: net
|
|
1038
|
+
}).address;
|
|
1039
|
+
const controlBlock = buildControlBlock(
|
|
1040
|
+
internalPubkey,
|
|
1041
|
+
192 | (tweakedKey.parity ? 1 : 0),
|
|
1042
|
+
[]
|
|
1043
|
+
// No sibling for single-leaf tree
|
|
1044
|
+
);
|
|
1045
|
+
let policy = null;
|
|
1046
|
+
const keyInfos = extractKeyInfos(keys);
|
|
1047
|
+
if (keyInfos) {
|
|
1048
|
+
policy = buildTaprootMultisigPolicy({
|
|
1049
|
+
name: name || `${threshold}-of-${keys.length} Taproot Multisig`,
|
|
1050
|
+
threshold,
|
|
1051
|
+
keys: [bareKey(NUMS.xpub(network)), ...keyInfos],
|
|
1052
|
+
sorted
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
address,
|
|
1057
|
+
scriptPubkey,
|
|
1058
|
+
internalPubkey,
|
|
1059
|
+
merkleRoot,
|
|
1060
|
+
tweak,
|
|
1061
|
+
outputKey,
|
|
1062
|
+
scriptLeaf: {
|
|
1063
|
+
script: multisigScript,
|
|
1064
|
+
hash: leafHash
|
|
1065
|
+
},
|
|
1066
|
+
controlBlock,
|
|
1067
|
+
policy,
|
|
1068
|
+
/**
|
|
1069
|
+
* Build witness stack for script path spend.
|
|
1070
|
+
*
|
|
1071
|
+
* For multi_a, signatures must be provided in the same order as keys appear
|
|
1072
|
+
* in the script. Use empty Buffer for keys that don't sign.
|
|
1073
|
+
*
|
|
1074
|
+
* @param signatures Array of signatures (length must equal number of keys)
|
|
1075
|
+
* Use Buffer.alloc(0) for non-signing keys
|
|
1076
|
+
* @returns Witness stack: [sig1, sig2, ..., sigN, script, control_block]
|
|
1077
|
+
*/
|
|
1078
|
+
witness(signatures) {
|
|
1079
|
+
if (signatures.length !== keys.length) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Expected ${keys.length} signature slots (use empty Buffer for non-signers), got ${signatures.length}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const sigCount = signatures.filter((s) => s.length > 0).length;
|
|
1085
|
+
if (sigCount !== threshold) {
|
|
1086
|
+
throw new Error(`Expected ${threshold} signatures, got ${sigCount}`);
|
|
1087
|
+
}
|
|
1088
|
+
const orderedSigs = sorted ? sortedIndexes.map((origIdx) => signatures[origIdx]) : signatures;
|
|
1089
|
+
return [...orderedSigs.reverse(), multisigScript, controlBlock];
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/patterns/segwit/htlc.ts
|
|
1095
|
+
import { script as bscript9, opcodes as opcodes9, payments as payments3 } from "bitcoinjs-lib";
|
|
1096
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
1097
|
+
function segwitHtlc(params) {
|
|
1098
|
+
const { payee, payer, secretHash, timelock, network, name } = params;
|
|
1099
|
+
if (!secretHash || secretHash.length !== 64) {
|
|
1100
|
+
throw new Error("secretHash must be 32 bytes (64 hex chars)");
|
|
1101
|
+
}
|
|
1102
|
+
if (timelock <= 0) {
|
|
1103
|
+
throw new Error("timelock must be a positive number");
|
|
1104
|
+
}
|
|
1105
|
+
const payeePubkey = resolveCompressedPubkey(payee, network);
|
|
1106
|
+
const payerPubkey = resolveCompressedPubkey(payer, network);
|
|
1107
|
+
const secretHashBuffer = Buffer.from(secretHash, "hex");
|
|
1108
|
+
const witnessScript = Buffer.from(
|
|
1109
|
+
bscript9.compile([
|
|
1110
|
+
opcodes9.OP_IF,
|
|
1111
|
+
// and_v(v:sha256(H), pk(@0)) - secret path
|
|
1112
|
+
opcodes9.OP_SIZE,
|
|
1113
|
+
bscript9.number.encode(32),
|
|
1114
|
+
opcodes9.OP_EQUALVERIFY,
|
|
1115
|
+
opcodes9.OP_SHA256,
|
|
1116
|
+
secretHashBuffer,
|
|
1117
|
+
opcodes9.OP_EQUALVERIFY,
|
|
1118
|
+
payeePubkey,
|
|
1119
|
+
opcodes9.OP_CHECKSIG,
|
|
1120
|
+
opcodes9.OP_ELSE,
|
|
1121
|
+
// and_v(v:pk(@1), after(N)) - timeout path
|
|
1122
|
+
payerPubkey,
|
|
1123
|
+
opcodes9.OP_CHECKSIGVERIFY,
|
|
1124
|
+
bscript9.number.encode(timelock),
|
|
1125
|
+
opcodes9.OP_CHECKLOCKTIMEVERIFY,
|
|
1126
|
+
opcodes9.OP_ENDIF
|
|
1127
|
+
])
|
|
1128
|
+
);
|
|
1129
|
+
const net = getNetwork(network);
|
|
1130
|
+
const scriptHash = sha2563(witnessScript);
|
|
1131
|
+
const p2wsh = payments3.p2wsh({
|
|
1132
|
+
redeem: { output: witnessScript, network: net },
|
|
1133
|
+
network: net
|
|
1134
|
+
});
|
|
1135
|
+
if (!p2wsh.address || !p2wsh.output) {
|
|
1136
|
+
throw new Error("Failed to create P2WSH output");
|
|
1137
|
+
}
|
|
1138
|
+
let policy = null;
|
|
1139
|
+
if (isXpubKey(payee) && isXpubKey(payer)) {
|
|
1140
|
+
policy = buildSegwitHtlcPolicy({
|
|
1141
|
+
name: name || "HTLC",
|
|
1142
|
+
secretHash,
|
|
1143
|
+
timelock,
|
|
1144
|
+
payeeKey: toKeyInfo(payee),
|
|
1145
|
+
payerKey: toKeyInfo(payer)
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
address: p2wsh.address,
|
|
1150
|
+
scriptPubkey: Buffer.from(p2wsh.output),
|
|
1151
|
+
witnessScript,
|
|
1152
|
+
scriptHash: Buffer.from(scriptHash),
|
|
1153
|
+
timelock,
|
|
1154
|
+
witness: {
|
|
1155
|
+
secret(signature, preimage) {
|
|
1156
|
+
return [signature, preimage, Buffer.from([1]), witnessScript];
|
|
1157
|
+
},
|
|
1158
|
+
timeout(signature) {
|
|
1159
|
+
return [signature, Buffer.alloc(0), witnessScript];
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
policy
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function extractSecret2(witness, secretHash) {
|
|
1166
|
+
if (!witness || witness.length < 4) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const preimage = witness[1];
|
|
1170
|
+
if (preimage.length !== 32) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
if (secretHash) {
|
|
1174
|
+
const computed = Buffer.from(sha2563(preimage)).toString("hex");
|
|
1175
|
+
if (computed !== secretHash) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return preimage.toString("hex");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/patterns/segwit/multisig.ts
|
|
1183
|
+
import { payments as payments4 } from "bitcoinjs-lib";
|
|
1184
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
1185
|
+
function segwitMultisig(params) {
|
|
1186
|
+
const { threshold, keys, sorted = true, network, name } = params;
|
|
1187
|
+
if (threshold <= 0 || !Number.isInteger(threshold)) {
|
|
1188
|
+
throw new Error("threshold must be a positive integer");
|
|
1189
|
+
}
|
|
1190
|
+
if (keys.length === 0) {
|
|
1191
|
+
throw new Error("At least one key is required");
|
|
1192
|
+
}
|
|
1193
|
+
if (threshold > keys.length) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`threshold (${threshold}) cannot exceed number of keys (${keys.length})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
if (keys.length > 20) {
|
|
1199
|
+
throw new Error("SegWit multisig is limited to 20 keys");
|
|
1200
|
+
}
|
|
1201
|
+
const net = getNetwork(network);
|
|
1202
|
+
const pubkeys = keys.map((key) => resolveCompressedPubkey(key, network));
|
|
1203
|
+
const witnessScript = multiScript(threshold, pubkeys, sorted);
|
|
1204
|
+
const scriptHash = sha2564(witnessScript);
|
|
1205
|
+
const p2wsh = payments4.p2wsh({
|
|
1206
|
+
redeem: { output: witnessScript, network: net },
|
|
1207
|
+
network: net
|
|
1208
|
+
});
|
|
1209
|
+
if (!p2wsh.address || !p2wsh.output) {
|
|
1210
|
+
throw new Error("Failed to create P2WSH output");
|
|
1211
|
+
}
|
|
1212
|
+
let policy = null;
|
|
1213
|
+
const keyInfos = extractKeyInfos(keys);
|
|
1214
|
+
if (keyInfos) {
|
|
1215
|
+
policy = buildSegwitMultisigPolicy({
|
|
1216
|
+
name: name || `${threshold}-of-${keys.length} Multisig`,
|
|
1217
|
+
threshold,
|
|
1218
|
+
keys: keyInfos,
|
|
1219
|
+
sorted
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
address: p2wsh.address,
|
|
1224
|
+
scriptPubkey: Buffer.from(p2wsh.output),
|
|
1225
|
+
witnessScript,
|
|
1226
|
+
scriptHash: Buffer.from(scriptHash),
|
|
1227
|
+
policy,
|
|
1228
|
+
/**
|
|
1229
|
+
* Build witness stack for spending.
|
|
1230
|
+
* @param signatures Array of signatures (must have exactly `threshold` signatures)
|
|
1231
|
+
* @returns Witness stack: [OP_0, sig1, sig2, ..., witnessScript]
|
|
1232
|
+
*/
|
|
1233
|
+
witness(signatures) {
|
|
1234
|
+
if (signatures.length !== threshold) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Expected ${threshold} signatures, got ${signatures.length}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
return [Buffer.alloc(0), ...signatures, witnessScript];
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
export {
|
|
1244
|
+
NUMS,
|
|
1245
|
+
after,
|
|
1246
|
+
and_v,
|
|
1247
|
+
bareKey,
|
|
1248
|
+
bareMultiScript,
|
|
1249
|
+
buildSegwitHtlcPolicy,
|
|
1250
|
+
buildSegwitMultisigPolicy,
|
|
1251
|
+
buildSegwitPolicy,
|
|
1252
|
+
buildTaprootHtlcPolicy,
|
|
1253
|
+
buildTaprootMultisigPolicy,
|
|
1254
|
+
buildTaprootPolicy,
|
|
1255
|
+
cltvScript,
|
|
1256
|
+
csvScript,
|
|
1257
|
+
deriveFromXpub,
|
|
1258
|
+
encodeScriptInt,
|
|
1259
|
+
encodeScriptNumber,
|
|
1260
|
+
extractKeyInfos,
|
|
1261
|
+
extractSecret2 as extractSegwitSecret,
|
|
1262
|
+
extractSecret as extractTaprootSecret,
|
|
1263
|
+
formatKeyInfo,
|
|
1264
|
+
getNetwork,
|
|
1265
|
+
hash160,
|
|
1266
|
+
hash256,
|
|
1267
|
+
hashlockScript,
|
|
1268
|
+
isRawKey,
|
|
1269
|
+
isValidPoint,
|
|
1270
|
+
isXpubKey,
|
|
1271
|
+
keyPlaceholder,
|
|
1272
|
+
multi,
|
|
1273
|
+
multiScript,
|
|
1274
|
+
multi_a,
|
|
1275
|
+
older,
|
|
1276
|
+
or_d,
|
|
1277
|
+
or_i,
|
|
1278
|
+
parsePublicKey,
|
|
1279
|
+
pk,
|
|
1280
|
+
pkScript,
|
|
1281
|
+
pkh,
|
|
1282
|
+
pkhScript,
|
|
1283
|
+
pkhScriptFromHash,
|
|
1284
|
+
resolveCompressedPubkey,
|
|
1285
|
+
resolveXOnlyPubkey,
|
|
1286
|
+
ripemd160,
|
|
1287
|
+
segwitHtlc,
|
|
1288
|
+
segwitMultisig,
|
|
1289
|
+
sha256,
|
|
1290
|
+
simpleCltvScript,
|
|
1291
|
+
simpleHashlockScript,
|
|
1292
|
+
sortedBareMultiScript,
|
|
1293
|
+
sortedMultiScript,
|
|
1294
|
+
sortedmulti,
|
|
1295
|
+
sortedmulti_a,
|
|
1296
|
+
taprootHtlc,
|
|
1297
|
+
taprootMultisig,
|
|
1298
|
+
toKeyInfo,
|
|
1299
|
+
toXOnly,
|
|
1300
|
+
trDescriptor,
|
|
1301
|
+
v,
|
|
1302
|
+
wpkhDescriptor,
|
|
1303
|
+
wshDescriptor
|
|
1304
|
+
};
|
|
1305
|
+
//# sourceMappingURL=index.js.map
|