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