@scure/btc-signer 1.2.2 → 1.3.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.
Files changed (60) hide show
  1. package/README.md +19 -3
  2. package/lib/_type_test.d.ts +2 -0
  3. package/lib/_type_test.d.ts.map +1 -0
  4. package/lib/_type_test.js +59 -0
  5. package/lib/_type_test.js.map +1 -0
  6. package/lib/esm/_type_test.js +57 -0
  7. package/lib/esm/_type_test.js.map +1 -0
  8. package/lib/esm/index.js +13 -3007
  9. package/lib/esm/index.js.map +1 -1
  10. package/lib/esm/payment.js +681 -0
  11. package/lib/esm/payment.js.map +1 -0
  12. package/lib/esm/psbt.js +441 -0
  13. package/lib/esm/psbt.js.map +1 -0
  14. package/lib/esm/script.js +347 -0
  15. package/lib/esm/script.js.map +1 -0
  16. package/lib/esm/transaction.js +1013 -0
  17. package/lib/esm/transaction.js.map +1 -0
  18. package/lib/esm/utils.js +115 -0
  19. package/lib/esm/utils.js.map +1 -0
  20. package/lib/esm/utxo.js +491 -0
  21. package/lib/esm/utxo.js.map +1 -0
  22. package/lib/index.d.ts +18 -1439
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +53 -3042
  25. package/lib/index.js.map +1 -0
  26. package/lib/payment.d.ts +167 -0
  27. package/lib/payment.d.ts.map +1 -0
  28. package/lib/payment.js +704 -0
  29. package/lib/payment.js.map +1 -0
  30. package/lib/psbt.d.ts +834 -0
  31. package/lib/psbt.d.ts.map +1 -0
  32. package/lib/psbt.js +446 -0
  33. package/lib/psbt.js.map +1 -0
  34. package/lib/script.d.ts +154 -0
  35. package/lib/script.d.ts.map +1 -0
  36. package/lib/script.js +353 -0
  37. package/lib/script.js.map +1 -0
  38. package/lib/transaction.d.ts +223 -0
  39. package/lib/transaction.d.ts.map +1 -0
  40. package/lib/transaction.js +1022 -0
  41. package/lib/transaction.js.map +1 -0
  42. package/lib/utils.d.ts +30 -0
  43. package/lib/utils.d.ts.map +1 -0
  44. package/lib/utils.js +128 -0
  45. package/lib/utils.js.map +1 -0
  46. package/lib/utxo.d.ts +251 -0
  47. package/lib/utxo.d.ts.map +1 -0
  48. package/lib/utxo.js +501 -0
  49. package/lib/utxo.js.map +1 -0
  50. package/package.json +40 -10
  51. package/src/_type_test.ts +69 -0
  52. package/src/index.ts +28 -0
  53. package/src/package.json +3 -0
  54. package/src/payment.ts +749 -0
  55. package/src/psbt.ts +512 -0
  56. package/src/script.ts +236 -0
  57. package/src/transaction.ts +1065 -0
  58. package/src/utils.ts +118 -0
  59. package/src/utxo.ts +517 -0
  60. package/index.ts +0 -3067
package/src/payment.ts ADDED
@@ -0,0 +1,749 @@
1
+ import { Coder, hex, bech32, bech32m, createBase58check } from '@scure/base';
2
+ import * as P from 'micro-packed';
3
+ import { TaprootControlBlock, TransactionInput } from './psbt.js';
4
+ import { OpToNum, ScriptType, Script, VarBytes } from './script.js';
5
+ import { Bytes, NETWORK } from './utils.js';
6
+ import * as u from './utils.js';
7
+
8
+ // We need following items:
9
+ // - encode/decode output script
10
+ // - generate input script
11
+ // - generate address/output/redeem from user input
12
+ // P2ret represents generic interface for all p2* methods
13
+ export type P2Ret = {
14
+ type: string;
15
+ script: Bytes;
16
+ address?: string;
17
+ redeemScript?: Bytes;
18
+ witnessScript?: Bytes;
19
+ };
20
+
21
+ // Public Key (P2PK)
22
+ type OutPKType = { type: 'pk'; pubkey: Bytes };
23
+ export type OptScript = ScriptType | undefined;
24
+
25
+ function isValidPubkey(pub: Bytes, type: u.PubT): boolean {
26
+ try {
27
+ u.validatePubkey(pub, type);
28
+ return true;
29
+ } catch (e) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ const OutPK: Coder<OptScript, OutPKType | undefined> = {
35
+ encode(from: ScriptType): OutPKType | undefined {
36
+ if (
37
+ from.length !== 2 ||
38
+ !u.isBytes(from[0]) ||
39
+ !isValidPubkey(from[0], u.PubT.ecdsa) ||
40
+ from[1] !== 'CHECKSIG'
41
+ )
42
+ return;
43
+ return { type: 'pk', pubkey: from[0] };
44
+ },
45
+ decode: (to: OutPKType): OptScript => (to.type === 'pk' ? [to.pubkey, 'CHECKSIG'] : undefined),
46
+ };
47
+
48
+ // Public Key Hash (P2PKH)
49
+ type OutPKHType = { type: 'pkh'; hash: Bytes };
50
+ const OutPKH: Coder<OptScript, OutPKHType | undefined> = {
51
+ encode(from: ScriptType): OutPKHType | undefined {
52
+ if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
53
+ return;
54
+ if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG') return;
55
+ return { type: 'pkh', hash: from[2] };
56
+ },
57
+ decode: (to: OutPKHType): OptScript =>
58
+ to.type === 'pkh' ? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG'] : undefined,
59
+ };
60
+ // Script Hash (P2SH)
61
+ type OutSHType = { type: 'sh'; hash: Bytes };
62
+ const OutSH: Coder<OptScript, OutSHType | undefined> = {
63
+ encode(from: ScriptType): OutSHType | undefined {
64
+ if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
65
+ return;
66
+ return { type: 'sh', hash: from[1] };
67
+ },
68
+ decode: (to: OutSHType): OptScript =>
69
+ to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined,
70
+ };
71
+
72
+ // Witness Script Hash (P2WSH)
73
+ type OutWSHType = { type: 'wsh'; hash: Bytes };
74
+ const OutWSH: Coder<OptScript, OutWSHType | undefined> = {
75
+ encode(from: ScriptType): OutWSHType | undefined {
76
+ if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
77
+ if (from[1].length !== 32) return;
78
+ return { type: 'wsh', hash: from[1] };
79
+ },
80
+ decode: (to: OutWSHType): OptScript => (to.type === 'wsh' ? [0, to.hash] : undefined),
81
+ };
82
+
83
+ // Witness Public Key Hash (P2WPKH)
84
+ type OutWPKHType = { type: 'wpkh'; hash: Bytes };
85
+ const OutWPKH: Coder<OptScript, OutWPKHType | undefined> = {
86
+ encode(from: ScriptType): OutWPKHType | undefined {
87
+ if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
88
+ if (from[1].length !== 20) return;
89
+ return { type: 'wpkh', hash: from[1] };
90
+ },
91
+ decode: (to: OutWPKHType): OptScript => (to.type === 'wpkh' ? [0, to.hash] : undefined),
92
+ };
93
+
94
+ // Multisig (P2MS)
95
+ type OutMSType = { type: 'ms'; pubkeys: Bytes[]; m: number };
96
+ const OutMS: Coder<OptScript, OutMSType | undefined> = {
97
+ encode(from: ScriptType): OutMSType | undefined {
98
+ const last = from.length - 1;
99
+ if (from[last] !== 'CHECKMULTISIG') return;
100
+ const m = from[0];
101
+ const n = from[last - 1];
102
+ if (typeof m !== 'number' || typeof n !== 'number') return;
103
+ const pubkeys = from.slice(1, -2);
104
+ if (n !== pubkeys.length) return;
105
+ for (const pub of pubkeys) if (!u.isBytes(pub)) return;
106
+ return { type: 'ms', m, pubkeys: pubkeys as Bytes[] }; // we don't need n, since it is the same as pubkeys
107
+ },
108
+ // checkmultisig(n, ..pubkeys, m)
109
+ decode: (to: OutMSType): OptScript =>
110
+ to.type === 'ms' ? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG'] : undefined,
111
+ };
112
+ // Taproot (P2TR)
113
+ type OutTRType = { type: 'tr'; pubkey: Bytes };
114
+ const OutTR: Coder<OptScript, OutTRType | undefined> = {
115
+ encode(from: ScriptType): OutTRType | undefined {
116
+ if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1])) return;
117
+ return { type: 'tr', pubkey: from[1] };
118
+ },
119
+ decode: (to: OutTRType): OptScript => (to.type === 'tr' ? [1, to.pubkey] : undefined),
120
+ };
121
+
122
+ // Taproot N-of-N multisig (P2TR_NS)
123
+ type OutTRNSType = { type: 'tr_ns'; pubkeys: Bytes[] };
124
+ const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
125
+ encode(from: ScriptType): OutTRNSType | undefined {
126
+ const last = from.length - 1;
127
+ if (from[last] !== 'CHECKSIG') return;
128
+ const pubkeys = [];
129
+ // On error return, since it can be different script
130
+ for (let i = 0; i < last; i++) {
131
+ const elm = from[i];
132
+ if (i & 1) {
133
+ if (elm !== 'CHECKSIGVERIFY' || i === last - 1) return;
134
+ continue;
135
+ }
136
+ if (!u.isBytes(elm)) return;
137
+ pubkeys.push(elm);
138
+ }
139
+ return { type: 'tr_ns', pubkeys };
140
+ },
141
+ decode: (to: OutTRNSType): OptScript => {
142
+ if (to.type !== 'tr_ns') return;
143
+ const out: ScriptType = [];
144
+ for (let i = 0; i < to.pubkeys.length - 1; i++) out.push(to.pubkeys[i], 'CHECKSIGVERIFY');
145
+ out.push(to.pubkeys[to.pubkeys.length - 1], 'CHECKSIG');
146
+ return out;
147
+ },
148
+ };
149
+
150
+ // Taproot M-of-N Multisig (P2TR_MS)
151
+ type OutTRMSType = { type: 'tr_ms'; pubkeys: Bytes[]; m: number };
152
+ const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
153
+ encode(from: ScriptType): OutTRMSType | undefined {
154
+ const last = from.length - 1;
155
+ if (from[last] !== 'NUMEQUAL' || from[1] !== 'CHECKSIG') return;
156
+ const pubkeys = [];
157
+ const m = OpToNum(from[last - 1]);
158
+ if (typeof m !== 'number') return;
159
+ for (let i = 0; i < last - 1; i++) {
160
+ const elm = from[i];
161
+ if (i & 1) {
162
+ if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD'))
163
+ throw new Error('OutScript.encode/tr_ms: wrong element');
164
+ continue;
165
+ }
166
+ if (!u.isBytes(elm)) throw new Error('OutScript.encode/tr_ms: wrong key element');
167
+ pubkeys.push(elm);
168
+ }
169
+ return { type: 'tr_ms', pubkeys, m };
170
+ },
171
+ decode: (to: OutTRMSType): OptScript => {
172
+ if (to.type !== 'tr_ms') return;
173
+ const out: ScriptType = [to.pubkeys[0], 'CHECKSIG'];
174
+ for (let i = 1; i < to.pubkeys.length; i++) out.push(to.pubkeys[i], 'CHECKSIGADD');
175
+ out.push(to.m, 'NUMEQUAL');
176
+ return out;
177
+ },
178
+ };
179
+
180
+ // Unknown output type
181
+ type OutUnknownType = { type: 'unknown'; script: Bytes };
182
+ const OutUnknown: Coder<OptScript, OutUnknownType | undefined> = {
183
+ encode(from: ScriptType): OutUnknownType | undefined {
184
+ return { type: 'unknown', script: Script.encode(from) };
185
+ },
186
+ decode: (to: OutUnknownType): OptScript =>
187
+ to.type === 'unknown' ? Script.decode(to.script) : undefined,
188
+ };
189
+ // /Payments
190
+
191
+ const OutScripts = [
192
+ OutPK,
193
+ OutPKH,
194
+ OutSH,
195
+ OutWSH,
196
+ OutWPKH,
197
+ OutMS,
198
+ OutTR,
199
+ OutTRNS,
200
+ OutTRMS,
201
+ OutUnknown,
202
+ ];
203
+ // TODO: we can support user supplied output scripts now
204
+ // - addOutScript
205
+ // - removeOutScript
206
+ // - We can do that as log we modify array in-place
207
+ // - Actually is very hard, since there is sign/finalize logic
208
+ const _OutScript = P.apply(Script, P.coders.match(OutScripts));
209
+
210
+ /*
211
+ * UNSAFE: Custom scripts: mostly ordinals, be very careful when crafting new scripts
212
+ * Only taproot supported for now.
213
+ * NOTE: we can use same to move finalization logic from Transaction, but it will significantly change audited code.
214
+ */
215
+
216
+ type FinalizeSignature = [{ pubKey: Bytes; leafHash: Bytes }, Bytes];
217
+ type CustomScriptOut = { type: string } & Record<string, any>;
218
+ export type CustomScript = Coder<OptScript, CustomScriptOut | undefined> & {
219
+ finalizeTaproot?: (
220
+ script: Bytes,
221
+ parsed: CustomScriptOut,
222
+ signatures: FinalizeSignature[]
223
+ ) => Bytes[] | undefined;
224
+ };
225
+
226
+ // We can validate this once, because of packed & coders
227
+ export const OutScript = P.validate(_OutScript, (i) => {
228
+ if (i.type === 'pk' && !isValidPubkey(i.pubkey, u.PubT.ecdsa))
229
+ throw new Error('OutScript/pk: wrong key');
230
+ if (
231
+ (i.type === 'pkh' || i.type === 'sh' || i.type === 'wpkh') &&
232
+ (!u.isBytes(i.hash) || i.hash.length !== 20)
233
+ )
234
+ throw new Error(`OutScript/${i.type}: wrong hash`);
235
+ if (i.type === 'wsh' && (!u.isBytes(i.hash) || i.hash.length !== 32))
236
+ throw new Error(`OutScript/wsh: wrong hash`);
237
+ if (i.type === 'tr' && (!u.isBytes(i.pubkey) || !isValidPubkey(i.pubkey, u.PubT.schnorr)))
238
+ throw new Error('OutScript/tr: wrong taproot public key');
239
+ if (i.type === 'ms' || i.type === 'tr_ns' || i.type === 'tr_ms')
240
+ if (!Array.isArray(i.pubkeys)) throw new Error('OutScript/multisig: wrong pubkeys array');
241
+ if (i.type === 'ms') {
242
+ const n = i.pubkeys.length;
243
+ for (const p of i.pubkeys)
244
+ if (!isValidPubkey(p, u.PubT.ecdsa)) throw new Error('OutScript/multisig: wrong pubkey');
245
+ if (i.m <= 0 || n > 16 || i.m > n) throw new Error('OutScript/multisig: invalid params');
246
+ }
247
+ if (i.type === 'tr_ns' || i.type === 'tr_ms') {
248
+ for (const p of i.pubkeys)
249
+ if (!isValidPubkey(p, u.PubT.schnorr)) throw new Error(`OutScript/${i.type}: wrong pubkey`);
250
+ }
251
+ if (i.type === 'tr_ms') {
252
+ const n = i.pubkeys.length;
253
+ if (i.m <= 0 || n > 999 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
254
+ }
255
+ return i;
256
+ });
257
+ export type OutScriptType = typeof OutScript;
258
+
259
+ // Basic sanity check for scripts
260
+ function checkWSH(s: OutWSHType, witnessScript: Bytes) {
261
+ if (!P.equalBytes(s.hash, u.sha256(witnessScript)))
262
+ throw new Error('checkScript: wsh wrong witnessScript hash');
263
+ const w = OutScript.decode(witnessScript);
264
+ if (w.type === 'tr' || w.type === 'tr_ns' || w.type === 'tr_ms')
265
+ throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2SH`);
266
+ if (w.type === 'wpkh' || w.type === 'sh')
267
+ throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2WSH`);
268
+ }
269
+
270
+ export function checkScript(script?: Bytes, redeemScript?: Bytes, witnessScript?: Bytes) {
271
+ if (script) {
272
+ const s = OutScript.decode(script);
273
+ // ms||pk maybe work, but there will be no address, hard to spend
274
+ if (s.type === 'tr_ns' || s.type === 'tr_ms' || s.type === 'ms' || s.type == 'pk')
275
+ throw new Error(`checkScript: non-wrapped ${s.type}`);
276
+ if (s.type === 'sh' && redeemScript) {
277
+ if (!P.equalBytes(s.hash, u.hash160(redeemScript)))
278
+ throw new Error('checkScript: sh wrong redeemScript hash');
279
+ const r = OutScript.decode(redeemScript);
280
+ if (r.type === 'tr' || r.type === 'tr_ns' || r.type === 'tr_ms')
281
+ throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
282
+ // Not sure if this unspendable, but we cannot represent this via PSBT
283
+ if (r.type === 'sh') throw new Error('checkScript: P2SH cannot be wrapped in P2SH');
284
+ }
285
+ if (s.type === 'wsh' && witnessScript) checkWSH(s, witnessScript);
286
+ }
287
+ if (redeemScript) {
288
+ const r = OutScript.decode(redeemScript);
289
+ if (r.type === 'wsh' && witnessScript) checkWSH(r, witnessScript);
290
+ }
291
+ }
292
+
293
+ function uniqPubkey(pubkeys: Bytes[]) {
294
+ const map: Record<string, boolean> = {};
295
+ for (const pub of pubkeys) {
296
+ const key = hex.encode(pub);
297
+ if (map[key]) throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
298
+ map[key] = true;
299
+ }
300
+ }
301
+
302
+ // @ts-ignore
303
+ export const p2pk = (pubkey: Bytes, network = NETWORK): P2Ret => {
304
+ // network is unused
305
+ if (!isValidPubkey(pubkey, u.PubT.ecdsa)) throw new Error('P2PK: invalid publicKey');
306
+ return {
307
+ type: 'pk',
308
+ script: OutScript.encode({ type: 'pk', pubkey }),
309
+ };
310
+ };
311
+ export const p2pkh = (publicKey: Bytes, network = NETWORK): P2Ret => {
312
+ if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2PKH: invalid publicKey');
313
+ const hash = u.hash160(publicKey);
314
+ return {
315
+ type: 'pkh',
316
+ script: OutScript.encode({ type: 'pkh', hash }),
317
+ address: Address(network).encode({ type: 'pkh', hash }),
318
+ };
319
+ };
320
+ export const p2sh = (child: P2Ret, network = NETWORK): P2Ret => {
321
+ // It is already tested inside noble-hashes and checkScript
322
+ const cs = child.script;
323
+ if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof child.script}, expected Uint8Array`);
324
+ const hash = u.hash160(cs);
325
+ const script = OutScript.encode({ type: 'sh', hash });
326
+ checkScript(script, cs, child.witnessScript);
327
+ const res: P2Ret = {
328
+ type: 'sh',
329
+ redeemScript: cs,
330
+ script: OutScript.encode({ type: 'sh', hash }),
331
+ address: Address(network).encode({ type: 'sh', hash }),
332
+ };
333
+ if (child.witnessScript) res.witnessScript = child.witnessScript;
334
+ return res;
335
+ };
336
+ export const p2wsh = (child: P2Ret, network = NETWORK): P2Ret => {
337
+ const cs = child.script;
338
+ if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
339
+ const hash = u.sha256(cs);
340
+ const script = OutScript.encode({ type: 'wsh', hash });
341
+ checkScript(script, undefined, cs);
342
+ return {
343
+ type: 'wsh',
344
+ witnessScript: cs,
345
+ script: OutScript.encode({ type: 'wsh', hash }),
346
+ address: Address(network).encode({ type: 'wsh', hash }),
347
+ };
348
+ };
349
+ export const p2wpkh = (publicKey: Bytes, network = NETWORK): P2Ret => {
350
+ if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2WPKH: invalid publicKey');
351
+ if (publicKey.length === 65) throw new Error('P2WPKH: uncompressed public key');
352
+ const hash = u.hash160(publicKey);
353
+ return {
354
+ type: 'wpkh',
355
+ script: OutScript.encode({ type: 'wpkh', hash }),
356
+ address: Address(network).encode({ type: 'wpkh', hash }),
357
+ };
358
+ };
359
+ export const p2ms = (m: number, pubkeys: Bytes[], allowSamePubkeys = false): P2Ret => {
360
+ if (!allowSamePubkeys) uniqPubkey(pubkeys);
361
+ return { type: 'ms', script: OutScript.encode({ type: 'ms', pubkeys, m }) };
362
+ };
363
+
364
+ type HashedTree =
365
+ | { type: 'leaf'; version?: number; script: Bytes; hash: Bytes }
366
+ | { type: 'branch'; left: HashedTree; right: HashedTree; hash: Bytes };
367
+ function checkTaprootScript(
368
+ script: Bytes,
369
+ internalPubKey: Bytes,
370
+ allowUnknownOutputs = false,
371
+ customScripts?: CustomScript[]
372
+ ) {
373
+ const out = OutScript.decode(script);
374
+ if (out.type === 'unknown') {
375
+ // NOTE: this check should be before allowUnknownOutputs, otherwise it will
376
+ // disable custom. All custom scripts for taproot should have prefix 'tr_'
377
+ if (customScripts) {
378
+ const cs = P.apply(Script, P.coders.match(customScripts));
379
+ const c = cs.decode(script);
380
+ if (c !== undefined) {
381
+ if (typeof c.type !== 'string' || !c.type.startsWith('tr_'))
382
+ throw new Error(`P2TR: invalid custom type=${c.type}`);
383
+ return;
384
+ }
385
+ }
386
+ if (allowUnknownOutputs) return;
387
+ }
388
+ if (!['tr_ns', 'tr_ms'].includes(out.type))
389
+ throw new Error(`P2TR: invalid leaf script=${out.type}`);
390
+ const outms = out as OutTRNSType | OutTRMSType;
391
+ if (!allowUnknownOutputs && outms.pubkeys) {
392
+ for (const p of outms.pubkeys) {
393
+ if (P.equalBytes(p, u.TAPROOT_UNSPENDABLE_KEY))
394
+ throw new Error('Unspendable taproot key in leaf script');
395
+ // It's likely a mistake at this point:
396
+ // 1. p2tr(A, p2tr_ns(2, [A, B])) == p2tr(A, p2tr_pk(B)) (A or B key)
397
+ // but will take more space and fees.
398
+ // 2. For multi-sig p2tr(A, p2tr_ns(2, [A, B, C])) it's probably a security issue:
399
+ // User creates 2 of 3 multisig of keys [A, B, C],
400
+ // but key A always can spend whole output without signatures from other keys.
401
+ // p2tr(A, p2tr_ns(2, [B, C, D])) is ok: A or (B and C) or (B and D) or (C and D)
402
+ if (P.equalBytes(p, internalPubKey)) {
403
+ throw new Error(
404
+ 'Using P2TR with leaf script with same key as internal key is not supported'
405
+ );
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ export type P2TROut = P2Ret & {
412
+ tweakedPubkey: Uint8Array;
413
+ tapInternalKey: Uint8Array;
414
+ tapMerkleRoot?: Uint8Array;
415
+ tapLeafScript?: TransactionInput['tapLeafScript'];
416
+ leaves?: TaprootLeaf[];
417
+ };
418
+
419
+ export type TaprootNode = {
420
+ script: Bytes | string;
421
+ leafVersion?: number;
422
+ weight?: number;
423
+ } & Partial<P2TROut>;
424
+ export type TaprootScriptTree = TaprootNode | TaprootScriptTree[];
425
+ export type TaprootScriptList = TaprootNode[];
426
+ type _TaprootTreeInternal = {
427
+ weight?: number;
428
+ childs?: [_TaprootTreeInternal[], _TaprootTreeInternal[]];
429
+ };
430
+
431
+ // Helper for generating binary tree from list, with weights
432
+ export function taprootListToTree(taprootList: TaprootScriptList): TaprootScriptTree {
433
+ // Clone input in order to not corrupt it
434
+ const lst = Array.from(taprootList) as _TaprootTreeInternal[];
435
+ // We have at least 2 elements => can create branch
436
+ while (lst.length >= 2) {
437
+ // Sort: elements with smallest weight are in the end of queue
438
+ lst.sort((a, b) => (b.weight || 1) - (a.weight || 1));
439
+ const b = lst.pop()!;
440
+ const a = lst.pop()!;
441
+ const weight = (a?.weight || 1) + (b?.weight || 1);
442
+ lst.push({
443
+ weight,
444
+ // Unwrap children array
445
+ // TODO: Very hard to remove any here
446
+ childs: [a?.childs || (a as any[]), b?.childs || (b as any)],
447
+ });
448
+ }
449
+ // At this point there is always 1 element in lst
450
+ const last = lst[0];
451
+ return (last?.childs || last) as TaprootScriptTree;
452
+ }
453
+
454
+ type TaprootLeaf = {
455
+ type: 'leaf';
456
+ version?: number;
457
+ script: Bytes;
458
+ hash: Bytes;
459
+ path: Bytes[];
460
+ };
461
+
462
+ type HashedTreeWithPath =
463
+ | TaprootLeaf
464
+ | {
465
+ type: 'branch';
466
+ left: HashedTreeWithPath;
467
+ right: HashedTreeWithPath;
468
+ hash: Bytes;
469
+ path: Bytes[];
470
+ };
471
+
472
+ function taprootAddPath(tree: HashedTree, path: Bytes[] = []): HashedTreeWithPath {
473
+ if (!tree) throw new Error(`taprootAddPath: empty tree`);
474
+ if (tree.type === 'leaf') return { ...tree, path };
475
+ if (tree.type !== 'branch') throw new Error(`taprootAddPath: wrong type=${tree}`);
476
+ return {
477
+ ...tree,
478
+ path,
479
+ // Left element has right hash in path and otherwise
480
+ left: taprootAddPath(tree.left, [tree.right.hash, ...path]),
481
+ right: taprootAddPath(tree.right, [tree.left.hash, ...path]),
482
+ };
483
+ }
484
+ function taprootWalkTree(tree: HashedTreeWithPath): TaprootLeaf[] {
485
+ if (!tree) throw new Error(`taprootAddPath: empty tree`);
486
+ if (tree.type === 'leaf') return [tree];
487
+ if (tree.type !== 'branch') throw new Error(`taprootWalkTree: wrong type=${tree}`);
488
+ return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)];
489
+ }
490
+
491
+ function taprootHashTree(
492
+ tree: TaprootScriptTree,
493
+ internalPubKey: Bytes,
494
+ allowUnknownOutputs = false,
495
+ customScripts?: CustomScript[]
496
+ ): HashedTree {
497
+ if (!tree) throw new Error('taprootHashTree: empty tree');
498
+ if (Array.isArray(tree) && tree.length === 1) tree = tree[0];
499
+ // Terminal node (leaf)
500
+ if (!Array.isArray(tree)) {
501
+ const { leafVersion: version, script: leafScript } = tree;
502
+ // Earliest tree walk where we can validate tapScripts
503
+ if (tree.tapLeafScript || (tree.tapMerkleRoot && !P.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
504
+ throw new Error('P2TR: tapRoot leafScript cannot have tree');
505
+ const script = typeof leafScript === 'string' ? hex.decode(leafScript) : leafScript;
506
+ if (!u.isBytes(script)) throw new Error(`checkScript: wrong script type=${script}`);
507
+ checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
508
+ return {
509
+ type: 'leaf',
510
+ version,
511
+ script,
512
+ hash: tapLeafHash(script, version),
513
+ };
514
+ }
515
+ // If tree / branch is not binary tree, convert it
516
+ if (tree.length !== 2) tree = taprootListToTree(tree as TaprootNode[]) as TaprootNode[];
517
+ if (tree.length !== 2) throw new Error('hashTree: non binary tree!');
518
+ // branch
519
+ // Both nodes should exist
520
+ const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
521
+ const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
522
+ // We cannot swap left/right here, since it will change structure of tree
523
+ let [lH, rH] = [left.hash, right.hash];
524
+ if (u.compareBytes(rH, lH) === -1) [lH, rH] = [rH, lH];
525
+ return { type: 'branch', left, right, hash: u.tagSchnorr('TapBranch', lH, rH) };
526
+ }
527
+
528
+ export const TAP_LEAF_VERSION = 0xc0;
529
+ export const tapLeafHash = (script: Bytes, version = TAP_LEAF_VERSION) =>
530
+ u.tagSchnorr('TapLeaf', new Uint8Array([version]), VarBytes.encode(script));
531
+
532
+ // Works as key OR tree.
533
+ // If we only have tree, need to add unspendable key, otherwise
534
+ // complex multisig wallet can be spent by owner of key only. See TAPROOT_UNSPENDABLE_KEY
535
+ export function p2tr(
536
+ internalPubKey?: Bytes | string,
537
+ tree?: TaprootScriptTree,
538
+ network = NETWORK,
539
+ allowUnknownOutputs = false,
540
+ customScripts?: CustomScript[]
541
+ ): P2TROut {
542
+ // Unspendable
543
+ if (!internalPubKey && !tree) throw new Error('p2tr: should have pubKey or scriptTree (or both)');
544
+ const pubKey =
545
+ typeof internalPubKey === 'string'
546
+ ? hex.decode(internalPubKey)
547
+ : internalPubKey || u.TAPROOT_UNSPENDABLE_KEY;
548
+ if (!isValidPubkey(pubKey, u.PubT.schnorr)) throw new Error('p2tr: non-schnorr pubkey');
549
+ let hashedTree = tree
550
+ ? taprootAddPath(taprootHashTree(tree, pubKey, allowUnknownOutputs, customScripts))
551
+ : undefined;
552
+ const tapMerkleRoot = hashedTree ? hashedTree.hash : undefined;
553
+ const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot || P.EMPTY);
554
+ let leaves;
555
+ if (hashedTree) {
556
+ leaves = taprootWalkTree(hashedTree).map((l) => ({
557
+ ...l,
558
+ controlBlock: TaprootControlBlock.encode({
559
+ version: (l.version || TAP_LEAF_VERSION) + parity,
560
+ internalKey: pubKey,
561
+ merklePath: l.path,
562
+ }),
563
+ }));
564
+ }
565
+ let tapLeafScript: TransactionInput['tapLeafScript'];
566
+ if (leaves) {
567
+ tapLeafScript = leaves.map((l) => [
568
+ TaprootControlBlock.decode(l.controlBlock),
569
+ u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
570
+ ]);
571
+ }
572
+ const res: P2TROut = {
573
+ type: 'tr',
574
+ script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
575
+ address: Address(network).encode({ type: 'tr', pubkey: tweakedPubkey }),
576
+ // For tests
577
+ tweakedPubkey,
578
+ // PSBT stuff
579
+ tapInternalKey: pubKey,
580
+ };
581
+ // Just in case someone would want to select a specific script
582
+ if (leaves) res.leaves = leaves;
583
+ if (tapLeafScript) res.tapLeafScript = tapLeafScript;
584
+ if (tapMerkleRoot) res.tapMerkleRoot = tapMerkleRoot;
585
+ return res;
586
+ }
587
+
588
+ // Returns all combinations of size M from lst
589
+ export function combinations<T>(m: number, list: T[]): T[][] {
590
+ const res: T[][] = [];
591
+ if (!Array.isArray(list)) throw new Error('combinations: lst arg should be array');
592
+ const n = list.length;
593
+ if (m > n) throw new Error('combinations: m > lst.length, no combinations possible');
594
+ /*
595
+ Basically works as M nested loops like:
596
+ for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
597
+ but since we cannot create nested loops dynamically, we unroll it to a single loop
598
+ */
599
+ const idx = Array.from({ length: m }, (_, i) => i);
600
+ const last = idx.length - 1;
601
+ main: for (;;) {
602
+ res.push(idx.map((i) => list[i]));
603
+ idx[last] += 1;
604
+ let i = last;
605
+ // Propagate increment
606
+ // idx[i] cannot be bigger than n-m+i, otherwise last elements in right part will overflow
607
+ for (; i >= 0 && idx[i] > n - m + i; i--) {
608
+ idx[i] = 0;
609
+ // Overflow in idx[0], break
610
+ if (i === 0) break main;
611
+ idx[i - 1] += 1;
612
+ }
613
+ // Propagate: idx[i+1] = idx[idx]+1
614
+ for (i += 1; i < idx.length; i++) idx[i] = idx[i - 1] + 1;
615
+ }
616
+ return res;
617
+ }
618
+
619
+ /**
620
+ * M-of-N multi-leaf wallet via p2tr_ns. If m == n, single script is emitted.
621
+ * Takes O(n^2) if m != n. 99-of-100 is ok, 5-of-100 is not.
622
+ * `2-of-[A,B,C] => [A,B] | [A,C] | [B,C]`
623
+ */
624
+ export const p2tr_ns = (m: number, pubkeys: Bytes[], allowSamePubkeys = false): P2Ret[] => {
625
+ if (!allowSamePubkeys) uniqPubkey(pubkeys);
626
+ return combinations(m, pubkeys).map((i) => ({
627
+ type: 'tr_ns',
628
+ script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
629
+ }));
630
+ };
631
+ // Taproot public key (case of p2tr_ns)
632
+ export const p2tr_pk = (pubkey: Bytes): P2Ret => p2tr_ns(1, [pubkey], undefined)[0];
633
+
634
+ export function p2tr_ms(m: number, pubkeys: Bytes[], allowSamePubkeys = false) {
635
+ if (!allowSamePubkeys) uniqPubkey(pubkeys);
636
+ return {
637
+ type: 'tr_ms',
638
+ script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
639
+ };
640
+ }
641
+
642
+ // Simple pubkey address, without complex scripts
643
+ export function getAddress(type: 'pkh' | 'wpkh' | 'tr', privKey: Bytes, network = NETWORK) {
644
+ if (type === 'tr') {
645
+ return p2tr(u.pubSchnorr(privKey), undefined, network).address;
646
+ }
647
+ const pubKey = u.pubECDSA(privKey);
648
+ if (type === 'pkh') return p2pkh(pubKey, network).address;
649
+ if (type === 'wpkh') return p2wpkh(pubKey, network).address;
650
+ throw new Error(`getAddress: unknown type=${type}`);
651
+ }
652
+
653
+ export const _sortPubkeys = (pubkeys: Bytes[]) => Array.from(pubkeys).sort(u.compareBytes);
654
+
655
+ export function multisig(m: number, pubkeys: Bytes[], sorted = false, witness = false) {
656
+ const ms = p2ms(m, sorted ? _sortPubkeys(pubkeys) : pubkeys);
657
+ return witness ? p2wsh(ms) : p2sh(ms);
658
+ }
659
+
660
+ export function sortedMultisig(m: number, pubkeys: Bytes[], witness = false) {
661
+ return multisig(m, pubkeys, true, witness);
662
+ }
663
+
664
+ const base58check = createBase58check(u.sha256);
665
+
666
+ function validateWitness(version: number, data: Bytes) {
667
+ if (data.length < 2 || data.length > 40) throw new Error('Witness: invalid length');
668
+ if (version > 16) throw new Error('Witness: invalid version');
669
+ if (version === 0 && !(data.length === 20 || data.length === 32))
670
+ throw new Error('Witness: invalid length for version');
671
+ }
672
+
673
+ function programToWitness(version: number, data: Bytes, network = NETWORK) {
674
+ validateWitness(version, data);
675
+ const coder = version === 0 ? bech32 : bech32m;
676
+ return coder.encode(network.bech32, [version].concat(coder.toWords(data)));
677
+ }
678
+
679
+ function formatKey(hashed: Bytes, prefix: number[]): string {
680
+ return base58check.encode(u.concatBytes(Uint8Array.from(prefix), hashed));
681
+ }
682
+
683
+ export function WIF(network = NETWORK): Coder<Bytes, string> {
684
+ return {
685
+ encode(privKey: Bytes) {
686
+ const compressed = u.concatBytes(privKey, new Uint8Array([0x01]));
687
+ return formatKey(compressed.subarray(0, 33), [network.wif]);
688
+ },
689
+ decode(wif: string) {
690
+ let parsed = base58check.decode(wif);
691
+ if (parsed[0] !== network.wif) throw new Error('Wrong WIF prefix');
692
+ parsed = parsed.subarray(1);
693
+ // Check what it is. Compressed flag?
694
+ if (parsed.length !== 33) throw new Error('Wrong WIF length');
695
+ if (parsed[32] !== 0x01) throw new Error('Wrong WIF postfix');
696
+ return parsed.subarray(0, -1);
697
+ },
698
+ };
699
+ }
700
+
701
+ // Returns OutType, which can be used to create outscript
702
+ export function Address(network = NETWORK) {
703
+ return {
704
+ encode(from: P.UnwrapCoder<OutScriptType>): string {
705
+ const { type } = from;
706
+ if (type === 'wpkh') return programToWitness(0, from.hash, network);
707
+ else if (type === 'wsh') return programToWitness(0, from.hash, network);
708
+ else if (type === 'tr') return programToWitness(1, from.pubkey, network);
709
+ else if (type === 'pkh') return formatKey(from.hash, [network.pubKeyHash]);
710
+ else if (type === 'sh') return formatKey(from.hash, [network.scriptHash]);
711
+ throw new Error(`Unknown address type=${type}`);
712
+ },
713
+ decode(address: string): P.UnwrapCoder<OutScriptType> {
714
+ if (address.length < 14 || address.length > 74) throw new Error('Invalid address length');
715
+ // Bech32
716
+ if (network.bech32 && address.toLowerCase().startsWith(network.bech32)) {
717
+ let res;
718
+ try {
719
+ res = bech32.decode(address);
720
+ if (res.words[0] !== 0) throw new Error(`bech32: wrong version=${res.words[0]}`);
721
+ } catch (_) {
722
+ // Starting from version 1 it is decoded as bech32m
723
+ res = bech32m.decode(address);
724
+ if (res.words[0] === 0) throw new Error(`bech32m: wrong version=${res.words[0]}`);
725
+ }
726
+ if (res.prefix !== network.bech32) throw new Error(`wrong bech32 prefix=${res.prefix}`);
727
+ const [version, ...program] = res.words;
728
+ const data = bech32.fromWords(program);
729
+ validateWitness(version, data);
730
+ if (version === 0 && data.length === 32) return { type: 'wsh', hash: data };
731
+ else if (version === 0 && data.length === 20) return { type: 'wpkh', hash: data };
732
+ else if (version === 1 && data.length === 32) return { type: 'tr', pubkey: data };
733
+ else throw new Error('Unknown witness program');
734
+ }
735
+ const data = base58check.decode(address);
736
+ if (data.length !== 21) throw new Error('Invalid base58 address');
737
+ // Pay To Public Key Hash
738
+ if (data[0] === network.pubKeyHash) {
739
+ return { type: 'pkh', hash: data.slice(1) };
740
+ } else if (data[0] === network.scriptHash) {
741
+ return {
742
+ type: 'sh',
743
+ hash: data.slice(1),
744
+ };
745
+ }
746
+ throw new Error(`Invalid address prefix=${data[0]}`);
747
+ },
748
+ };
749
+ }