@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
@@ -0,0 +1,1065 @@
1
+ import * as P from 'micro-packed';
2
+ import { hex } from '@scure/base';
3
+
4
+ import { Address, CustomScript, OutScript, checkScript, tapLeafHash } from './payment.js';
5
+ import * as psbt from './psbt.js'; // circular
6
+ import { CompactSizeLen, RawOutput, RawTx, RawWitness, Script, VarBytes } from './script.js';
7
+ import { NETWORK, Bytes, concatBytes, isBytes } from './utils.js';
8
+ import * as u from './utils.js';
9
+ import { getInputType, toVsize, normalizeInput, getPrevOut } from './utxo.js'; // circular
10
+
11
+ const EMPTY32 = new Uint8Array(32);
12
+ const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
13
+ amount: 0xffffffffffffffffn,
14
+ script: P.EMPTY,
15
+ };
16
+
17
+ // @scure/bip32 interface
18
+ interface HDKey {
19
+ publicKey: Bytes;
20
+ privateKey: Bytes;
21
+ fingerprint: number;
22
+ derive(path: string): HDKey;
23
+ deriveChild(index: number): HDKey;
24
+ sign(hash: Bytes): Bytes;
25
+ }
26
+
27
+ export type Signer = Bytes | HDKey;
28
+
29
+ export const PRECISION = 8;
30
+ export const DEFAULT_VERSION = 2;
31
+ export const DEFAULT_LOCKTIME = 0;
32
+ export const DEFAULT_SEQUENCE = 4294967295;
33
+ export const Decimal = P.coders.decimal(PRECISION);
34
+
35
+ // Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
36
+ export const def = <T>(value: T | undefined, def: T) => (value === undefined ? def : value);
37
+
38
+ export function cloneDeep<T>(obj: T): T {
39
+ if (Array.isArray(obj)) return obj.map((i) => cloneDeep(i)) as unknown as T;
40
+ // slice of nodejs Buffer doesn't copy
41
+ else if (obj instanceof Uint8Array) return Uint8Array.from(obj) as unknown as T;
42
+ // immutable
43
+ else if (['number', 'bigint', 'boolean', 'string', 'undefined'].includes(typeof obj)) return obj;
44
+ // null is object
45
+ else if (obj === null) return obj;
46
+ // should be last, so it won't catch other types
47
+ else if (typeof obj === 'object') {
48
+ return Object.fromEntries(
49
+ Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)])
50
+ ) as unknown as T;
51
+ }
52
+ throw new Error(`cloneDeep: unknown type=${obj} (${typeof obj})`);
53
+ }
54
+
55
+ // Mostly security features, hardened defaults;
56
+ // but you still can parse other people tx with unspendable outputs and stuff if you want
57
+ export type TxOpts = {
58
+ version?: number;
59
+ lockTime?: number;
60
+ PSBTVersion?: number;
61
+ // Flags
62
+ // Allow output scripts to be unknown scripts (probably unspendable)
63
+ /** @deprecated Use `allowUnknownOutputs` */
64
+ allowUnknowOutput?: boolean;
65
+ allowUnknownOutputs?: boolean;
66
+ // Try to sign/finalize unknown input. All bets are off, but there is chance that it will work
67
+ /** @deprecated Use `allowUnknownInputs` */
68
+ allowUnknowInput?: boolean;
69
+ allowUnknownInputs?: boolean;
70
+ // Check input/output scripts for sanity
71
+ disableScriptCheck?: boolean;
72
+ // There is strange behaviour where tx without outputs encoded with empty output in the end,
73
+ // tx without outputs in BIP174 doesn't have itb
74
+ bip174jsCompat?: boolean;
75
+ // If transaction data comes from untrusted source, then it can be modified in such way that will
76
+ // result paying higher mining fee
77
+ allowLegacyWitnessUtxo?: boolean;
78
+ lowR?: boolean; // Use lowR signatures
79
+ customScripts?: CustomScript[]; // UNSAFE: Custom payment scripts
80
+ };
81
+
82
+ /**
83
+ * Internal, exported only for backwards-compat. Use `SigHash` instead.
84
+ * @deprecated
85
+ */
86
+ export enum SignatureHash {
87
+ DEFAULT,
88
+ ALL,
89
+ NONE,
90
+ SINGLE,
91
+ ANYONECANPAY = 0x80,
92
+ }
93
+
94
+ export enum SigHash {
95
+ DEFAULT = SignatureHash.DEFAULT,
96
+ ALL = SignatureHash.ALL,
97
+ NONE = SignatureHash.NONE,
98
+ SINGLE = SignatureHash.SINGLE,
99
+ DEFAULT_ANYONECANPAY = SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
100
+ ALL_ANYONECANPAY = SignatureHash.ALL | SignatureHash.ANYONECANPAY,
101
+ NONE_ANYONECANPAY = SignatureHash.NONE | SignatureHash.ANYONECANPAY,
102
+ SINGLE_ANYONECANPAY = SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
103
+ }
104
+
105
+ function getTaprootKeys(
106
+ privKey: Bytes,
107
+ pubKey: Bytes,
108
+ internalKey: Bytes,
109
+ merkleRoot: Bytes = P.EMPTY
110
+ ) {
111
+ if (P.equalBytes(internalKey, pubKey)) {
112
+ privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
113
+ pubKey = u.pubSchnorr(privKey);
114
+ }
115
+ return { privKey, pubKey };
116
+ }
117
+
118
+ // User facing API with decoders
119
+ export type TransactionInputRequired = {
120
+ txid: Bytes;
121
+ index: number;
122
+ sequence: number;
123
+ finalScriptSig: Bytes;
124
+ };
125
+
126
+ // Force check amount/script
127
+ function outputBeforeSign(i: psbt.TransactionOutput): psbt.TransactionOutputRequired {
128
+ if (i.script === undefined || i.amount === undefined)
129
+ throw new Error('Transaction/output: script and amount required');
130
+ return { script: i.script, amount: i.amount };
131
+ }
132
+
133
+ // Force check index/txid/sequence
134
+ export function inputBeforeSign(i: psbt.TransactionInput): TransactionInputRequired {
135
+ if (i.txid === undefined || i.index === undefined)
136
+ throw new Error('Transaction/input: txid and index required');
137
+ return {
138
+ txid: i.txid,
139
+ index: i.index,
140
+ sequence: def(i.sequence, DEFAULT_SEQUENCE),
141
+ finalScriptSig: def(i.finalScriptSig, P.EMPTY),
142
+ };
143
+ }
144
+ function cleanFinalInput(i: psbt.TransactionInput) {
145
+ for (const _k in i) {
146
+ const k = _k as keyof psbt.TransactionInput;
147
+ if (!psbt.PSBTInputFinalKeys.includes(k)) delete i[k];
148
+ }
149
+ }
150
+
151
+ // (TxHash, Idx)
152
+ const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
153
+
154
+ function validateSigHash(s: SigHash) {
155
+ if (typeof s !== 'number' || typeof SigHash[s] !== 'string')
156
+ throw new Error(`Invalid SigHash=${s}`);
157
+ return s;
158
+ }
159
+
160
+ function unpackSighash(hashType: number) {
161
+ const masked = hashType & 0b0011111;
162
+ return {
163
+ isAny: !!(hashType & SignatureHash.ANYONECANPAY),
164
+ isNone: masked === SignatureHash.NONE,
165
+ isSingle: masked === SignatureHash.SINGLE,
166
+ };
167
+ }
168
+
169
+ function validateOpts(opts: TxOpts) {
170
+ if (opts !== undefined && {}.toString.call(opts) !== '[object Object]')
171
+ throw new Error(`Wrong object type for transaction options: ${opts}`);
172
+
173
+ const _opts = {
174
+ ...opts,
175
+ // Defaults
176
+ version: def(opts.version, DEFAULT_VERSION),
177
+ lockTime: def(opts.lockTime, 0),
178
+ PSBTVersion: def(opts.PSBTVersion, 0),
179
+ };
180
+ if (typeof _opts.allowUnknowInput !== 'undefined')
181
+ opts.allowUnknownInputs = _opts.allowUnknowInput;
182
+ if (typeof _opts.allowUnknowOutput !== 'undefined')
183
+ opts.allowUnknownOutputs = _opts.allowUnknowOutput;
184
+ // 0 and -1 happens in tests
185
+ if (![-1, 0, 1, 2].includes(_opts.version)) throw new Error(`Unknown version: ${_opts.version}`);
186
+ if (typeof _opts.lockTime !== 'number') throw new Error('Transaction lock time should be number');
187
+ P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
188
+ // There is no PSBT v1, and any new version will probably have fields which we don't know how to parse, which
189
+ // can lead to constructing broken transactions
190
+ if (_opts.PSBTVersion !== 0 && _opts.PSBTVersion !== 2)
191
+ throw new Error(`Unknown PSBT version ${_opts.PSBTVersion}`);
192
+ // Flags
193
+ for (const k of [
194
+ 'allowUnknownOutputs',
195
+ 'allowUnknownInputs',
196
+ 'disableScriptCheck',
197
+ 'bip174jsCompat',
198
+ 'allowLegacyWitnessUtxo',
199
+ 'lowR',
200
+ ] as const) {
201
+ const v = _opts[k];
202
+ if (v === undefined) continue; // optional
203
+ if (typeof v !== 'boolean')
204
+ throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
205
+ }
206
+ if (_opts.customScripts !== undefined) {
207
+ const cs = _opts.customScripts;
208
+ if (!Array.isArray(cs)) {
209
+ throw new Error(
210
+ `wrong custom scripts type (expected array): customScripts=${cs} (${typeof cs})`
211
+ );
212
+ }
213
+ for (const s of cs) {
214
+ if (typeof s.encode !== 'function' || typeof s.decode !== 'function')
215
+ throw new Error(`wrong script=${s} (${typeof s})`);
216
+ if (s.finalizeTaproot !== undefined && typeof s.finalizeTaproot !== 'function')
217
+ throw new Error(`wrong script=${s} (${typeof s})`);
218
+ }
219
+ }
220
+ return Object.freeze(_opts);
221
+ }
222
+
223
+ export class Transaction {
224
+ private global: psbt.PSBTKeyMapKeys<typeof psbt.PSBTGlobal> = {};
225
+ private inputs: psbt.TransactionInput[] = []; // use getInput()
226
+ private outputs: psbt.TransactionOutput[] = []; // use getOutput()
227
+ readonly opts: ReturnType<typeof validateOpts>;
228
+ constructor(opts: TxOpts = {}) {
229
+ const _opts = (this.opts = validateOpts(opts));
230
+ // Merge with global structure of PSBTv2
231
+ if (_opts.lockTime !== DEFAULT_LOCKTIME) this.global.fallbackLocktime = _opts.lockTime;
232
+ this.global.txVersion = _opts.version;
233
+ }
234
+
235
+ // Import
236
+ static fromRaw(raw: Bytes, opts: TxOpts = {}) {
237
+ const parsed = RawTx.decode(raw);
238
+ const tx = new Transaction({ ...opts, version: parsed.version, lockTime: parsed.lockTime });
239
+ for (const o of parsed.outputs) tx.addOutput(o);
240
+ tx.outputs = parsed.outputs;
241
+ tx.inputs = parsed.inputs;
242
+ if (parsed.witnesses) {
243
+ for (let i = 0; i < parsed.witnesses.length; i++)
244
+ tx.inputs[i].finalScriptWitness = parsed.witnesses[i];
245
+ }
246
+ return tx;
247
+ }
248
+ // PSBT
249
+ static fromPSBT(psbt_: Bytes, opts: TxOpts = {}) {
250
+ let parsed: P.UnwrapCoder<typeof psbt.RawPSBTV0>;
251
+ try {
252
+ parsed = psbt.RawPSBTV0.decode(psbt_);
253
+ } catch (e0) {
254
+ try {
255
+ parsed = psbt.RawPSBTV2.decode(psbt_);
256
+ } catch (e2) {
257
+ // Throw error for v0 parsing, since it popular, otherwise it would be shadowed by v2 error
258
+ throw e0;
259
+ }
260
+ }
261
+ const PSBTVersion = parsed.global.version || 0;
262
+ if (PSBTVersion !== 0 && PSBTVersion !== 2)
263
+ throw new Error(`Wrong PSBT version=${PSBTVersion}`);
264
+ const unsigned = parsed.global.unsignedTx;
265
+ const version = PSBTVersion === 0 ? unsigned?.version : parsed.global.txVersion;
266
+ const lockTime = PSBTVersion === 0 ? unsigned?.lockTime : parsed.global.fallbackLocktime;
267
+ const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
268
+ // We need slice here, because otherwise
269
+ const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
270
+ tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => ({
271
+ finalScriptSig: P.EMPTY,
272
+ ...parsed.global.unsignedTx?.inputs[j],
273
+ ...i,
274
+ }));
275
+ const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
276
+ tx.outputs = parsed.outputs.slice(0, outputCount).map((i, j) => ({
277
+ ...i,
278
+ ...parsed.global.unsignedTx?.outputs[j],
279
+ }));
280
+ tx.global = { ...parsed.global, txVersion: version }; // just in case proprietary/unknown fields
281
+ if (lockTime !== DEFAULT_LOCKTIME) tx.global.fallbackLocktime = lockTime;
282
+ return tx;
283
+ }
284
+ toPSBT(PSBTVersion = this.opts.PSBTVersion) {
285
+ if (PSBTVersion !== 0 && PSBTVersion !== 2)
286
+ throw new Error(`Wrong PSBT version=${PSBTVersion}`);
287
+ const inputs = this.inputs.map((i) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, i));
288
+ for (const inp of inputs) {
289
+ // Don't serialize empty fields
290
+ if (inp.partialSig && !inp.partialSig.length) delete inp.partialSig;
291
+ if (inp.finalScriptSig && !inp.finalScriptSig.length) delete inp.finalScriptSig;
292
+ if (inp.finalScriptWitness && !inp.finalScriptWitness.length) delete inp.finalScriptWitness;
293
+ }
294
+ const outputs = this.outputs.map((i) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i));
295
+ const global = { ...this.global };
296
+ if (PSBTVersion === 0) {
297
+ global.unsignedTx = RawTx.decode(this.unsignedTx);
298
+ delete global.fallbackLocktime;
299
+ delete global.txVersion;
300
+ } else {
301
+ global.version = PSBTVersion;
302
+ global.txVersion = this.version;
303
+ global.inputCount = this.inputs.length;
304
+ global.outputCount = this.outputs.length;
305
+ if (global.fallbackLocktime && global.fallbackLocktime === DEFAULT_LOCKTIME)
306
+ delete global.fallbackLocktime;
307
+ }
308
+ if (this.opts.bip174jsCompat) {
309
+ if (!inputs.length) inputs.push({});
310
+ if (!outputs.length) outputs.push({});
311
+ }
312
+ return (PSBTVersion === 0 ? psbt.RawPSBTV0 : psbt.RawPSBTV2).encode({
313
+ global,
314
+ inputs,
315
+ outputs,
316
+ });
317
+ }
318
+
319
+ // BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
320
+ get lockTime() {
321
+ let height = DEFAULT_LOCKTIME;
322
+ let heightCnt = 0;
323
+ let time = DEFAULT_LOCKTIME;
324
+ let timeCnt = 0;
325
+ for (const i of this.inputs) {
326
+ if (i.requiredHeightLocktime) {
327
+ height = Math.max(height, i.requiredHeightLocktime);
328
+ heightCnt++;
329
+ }
330
+ if (i.requiredTimeLocktime) {
331
+ time = Math.max(time, i.requiredTimeLocktime);
332
+ timeCnt++;
333
+ }
334
+ }
335
+ if (heightCnt && heightCnt >= timeCnt) return height;
336
+ if (time !== DEFAULT_LOCKTIME) return time;
337
+ return this.global.fallbackLocktime || DEFAULT_LOCKTIME;
338
+ }
339
+
340
+ get version() {
341
+ // Should be not possible
342
+ if (this.global.txVersion === undefined) throw new Error('No global.txVersion');
343
+ return this.global.txVersion;
344
+ }
345
+
346
+ private inputStatus(idx: number) {
347
+ this.checkInputIdx(idx);
348
+ const input = this.inputs[idx];
349
+ // Finalized
350
+ if (input.finalScriptSig && input.finalScriptSig.length) return 'finalized';
351
+ if (input.finalScriptWitness && input.finalScriptWitness.length) return 'finalized';
352
+ // Signed taproot
353
+ if (input.tapKeySig) return 'signed';
354
+ if (input.tapScriptSig && input.tapScriptSig.length) return 'signed';
355
+ // Signed
356
+ if (input.partialSig && input.partialSig.length) return 'signed';
357
+ return 'unsigned';
358
+ }
359
+ // Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range
360
+ // We will lose some vectors -> smaller test coverage of preimages (very important!)
361
+ private inputSighash(idx: number) {
362
+ this.checkInputIdx(idx);
363
+ const sighash = getInputType(this.inputs[idx], this.opts.allowLegacyWitnessUtxo).sighash;
364
+ // ALL or DEFAULT -- everything signed
365
+ // NONE -- all inputs + no outputs
366
+ // SINGLE -- all inputs + output with same index
367
+ // ALL + ANYONE -- specific input + all outputs
368
+ // NONE + ANYONE -- specific input + no outputs
369
+ // SINGLE -- specific inputs + output with same index
370
+ const sigOutputs = sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11;
371
+ const sigInputs = sighash & SignatureHash.ANYONECANPAY;
372
+ return { sigInputs, sigOutputs };
373
+ }
374
+ // Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic.
375
+ // Some cache will be nice, but there chance to have bugs with cache invalidation
376
+ private signStatus() {
377
+ // if addInput or addOutput is not possible, then all inputs or outputs are signed
378
+ let addInput = true,
379
+ addOutput = true;
380
+ let inputs = [],
381
+ outputs = [];
382
+ for (let idx = 0; idx < this.inputs.length; idx++) {
383
+ const status = this.inputStatus(idx);
384
+ // Unsigned input doesn't affect anything
385
+ if (status === 'unsigned') continue;
386
+ const { sigInputs, sigOutputs } = this.inputSighash(idx);
387
+ // Input type
388
+ if (sigInputs === SignatureHash.ANYONECANPAY) inputs.push(idx);
389
+ else addInput = false;
390
+ // Output type
391
+ if (sigOutputs === SignatureHash.ALL) addOutput = false;
392
+ else if (sigOutputs === SignatureHash.SINGLE) outputs.push(idx);
393
+ else if (sigOutputs === SignatureHash.NONE) {
394
+ // Doesn't affect any outputs at all
395
+ } else throw new Error(`Wrong signature hash output type: ${sigOutputs}`);
396
+ }
397
+ return { addInput, addOutput, inputs, outputs };
398
+ }
399
+
400
+ get isFinal() {
401
+ for (let idx = 0; idx < this.inputs.length; idx++)
402
+ if (this.inputStatus(idx) !== 'finalized') return false;
403
+ return true;
404
+ }
405
+
406
+ // Info utils
407
+ get hasWitnesses(): boolean {
408
+ let out = false;
409
+ for (const i of this.inputs)
410
+ if (i.finalScriptWitness && i.finalScriptWitness.length) out = true;
411
+ return out;
412
+ }
413
+ // https://en.bitcoin.it/wiki/Weight_units
414
+ get weight(): number {
415
+ if (!this.isFinal) throw new Error('Transaction is not finalized');
416
+ let out = 32;
417
+ // Outputs
418
+ const outputs = this.outputs.map(outputBeforeSign);
419
+ out += 4 * CompactSizeLen.encode(this.outputs.length).length;
420
+ for (const o of outputs) out += 32 + 4 * VarBytes.encode(o.script).length;
421
+ // Inputs
422
+ if (this.hasWitnesses) out += 2;
423
+ out += 4 * CompactSizeLen.encode(this.inputs.length).length;
424
+ for (const i of this.inputs) {
425
+ out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
426
+ if (this.hasWitnesses && i.finalScriptWitness)
427
+ out += RawWitness.encode(i.finalScriptWitness).length;
428
+ }
429
+ return out;
430
+ }
431
+ get vsize(): number {
432
+ return toVsize(this.weight);
433
+ }
434
+ toBytes(withScriptSig = false, withWitness = false) {
435
+ return RawTx.encode({
436
+ version: this.version,
437
+ lockTime: this.lockTime,
438
+ inputs: this.inputs.map(inputBeforeSign).map((i) => ({
439
+ ...i,
440
+ finalScriptSig: (withScriptSig && i.finalScriptSig) || P.EMPTY,
441
+ })),
442
+ outputs: this.outputs.map(outputBeforeSign),
443
+ witnesses: this.inputs.map((i) => i.finalScriptWitness || []),
444
+ segwitFlag: withWitness && this.hasWitnesses,
445
+ });
446
+ }
447
+ get unsignedTx(): Bytes {
448
+ return this.toBytes(false, false);
449
+ }
450
+ get hex() {
451
+ return hex.encode(this.toBytes(true, this.hasWitnesses));
452
+ }
453
+
454
+ get hash() {
455
+ if (!this.isFinal) throw new Error('Transaction is not finalized');
456
+ return hex.encode(u.sha256x2(this.toBytes(true)));
457
+ }
458
+ get id() {
459
+ if (!this.isFinal) throw new Error('Transaction is not finalized');
460
+ return hex.encode(u.sha256x2(this.toBytes(true)).reverse());
461
+ }
462
+ // Input stuff
463
+ private checkInputIdx(idx: number) {
464
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
465
+ throw new Error(`Wrong input index=${idx}`);
466
+ }
467
+ getInput(idx: number) {
468
+ this.checkInputIdx(idx);
469
+ return cloneDeep(this.inputs[idx]);
470
+ }
471
+ get inputsLength() {
472
+ return this.inputs.length;
473
+ }
474
+ // Modification
475
+ addInput(input: psbt.TransactionInputUpdate, _ignoreSignStatus = false): number {
476
+ if (!_ignoreSignStatus && !this.signStatus().addInput)
477
+ throw new Error('Tx has signed inputs, cannot add new one');
478
+ this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
479
+ return this.inputs.length - 1;
480
+ }
481
+ updateInput(idx: number, input: psbt.TransactionInputUpdate, _ignoreSignStatus = false) {
482
+ this.checkInputIdx(idx);
483
+ let allowedFields = undefined;
484
+ if (!_ignoreSignStatus) {
485
+ const status = this.signStatus();
486
+ if (!status.addInput || status.inputs.includes(idx))
487
+ allowedFields = psbt.PSBTInputUnsignedKeys;
488
+ }
489
+ this.inputs[idx] = normalizeInput(
490
+ input,
491
+ this.inputs[idx],
492
+ allowedFields,
493
+ this.opts.disableScriptCheck
494
+ );
495
+ }
496
+ // Output stuff
497
+ private checkOutputIdx(idx: number) {
498
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.outputs.length)
499
+ throw new Error(`Wrong output index=${idx}`);
500
+ }
501
+ getOutput(idx: number) {
502
+ this.checkOutputIdx(idx);
503
+ return cloneDeep(this.outputs[idx]);
504
+ }
505
+ get outputsLength() {
506
+ return this.outputs.length;
507
+ }
508
+ private normalizeOutput(
509
+ o: psbt.TransactionOutputUpdate,
510
+ cur?: psbt.TransactionOutput,
511
+ allowedFields?: (keyof typeof psbt.PSBTOutput)[]
512
+ ): psbt.TransactionOutput {
513
+ let { amount, script } = o;
514
+ if (amount === undefined) amount = cur?.amount;
515
+ if (typeof amount !== 'bigint') throw new Error('amount must be bigint sats');
516
+ if (typeof script === 'string') script = hex.decode(script);
517
+ if (script === undefined) script = cur?.script;
518
+ let res: psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput> = { ...cur, ...o, amount, script };
519
+ if (res.amount === undefined) delete res.amount;
520
+ res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields);
521
+ psbt.PSBTOutputCoder.encode(res);
522
+ if (
523
+ res.script &&
524
+ !this.opts.allowUnknownOutputs &&
525
+ OutScript.decode(res.script).type === 'unknown'
526
+ ) {
527
+ throw new Error(
528
+ 'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
529
+ );
530
+ }
531
+ if (!this.opts.disableScriptCheck) checkScript(res.script, res.redeemScript, res.witnessScript);
532
+ return res;
533
+ }
534
+ addOutput(o: psbt.TransactionOutputUpdate, _ignoreSignStatus = false): number {
535
+ if (!_ignoreSignStatus && !this.signStatus().addOutput)
536
+ throw new Error('Tx has signed outputs, cannot add new one');
537
+ this.outputs.push(this.normalizeOutput(o));
538
+ return this.outputs.length - 1;
539
+ }
540
+ updateOutput(idx: number, output: psbt.TransactionOutputUpdate, _ignoreSignStatus = false) {
541
+ this.checkOutputIdx(idx);
542
+ let allowedFields = undefined;
543
+ if (!_ignoreSignStatus) {
544
+ const status = this.signStatus();
545
+ if (!status.addOutput || status.outputs.includes(idx))
546
+ allowedFields = psbt.PSBTOutputUnsignedKeys;
547
+ }
548
+ this.outputs[idx] = this.normalizeOutput(output, this.outputs[idx], allowedFields);
549
+ }
550
+ addOutputAddress(address: string, amount: bigint, network = NETWORK): number {
551
+ return this.addOutput({ script: OutScript.encode(Address(network).decode(address)), amount });
552
+ }
553
+ // Utils
554
+ get fee(): bigint {
555
+ let res = 0n;
556
+ for (const i of this.inputs) {
557
+ const prevOut = getPrevOut(i);
558
+ if (!prevOut) throw new Error('Empty input amount');
559
+ res += prevOut.amount;
560
+ }
561
+ const outputs = this.outputs.map(outputBeforeSign);
562
+ for (const o of outputs) res -= o.amount;
563
+ return res;
564
+ }
565
+
566
+ // Signing
567
+ // Based on https://github.com/bitcoin/bitcoin/blob/5871b5b5ab57a0caf9b7514eb162c491c83281d5/test/functional/test_framework/script.py#L624
568
+ // There is optimization opportunity to re-use hashes for multiple inputs for witness v0/v1,
569
+ // but we are trying to be less complicated for audit purpose for now.
570
+ private preimageLegacy(idx: number, prevOutScript: Bytes, hashType: number) {
571
+ const { isAny, isNone, isSingle } = unpackSighash(hashType);
572
+ if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
573
+ if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
574
+ return P.U256BE.encode(1n);
575
+ prevOutScript = Script.encode(
576
+ Script.decode(prevOutScript).filter((i) => i !== 'CODESEPARATOR')
577
+ );
578
+ let inputs: TransactionInputRequired[] = this.inputs
579
+ .map(inputBeforeSign)
580
+ .map((input, inputIdx) => ({
581
+ ...input,
582
+ finalScriptSig: inputIdx === idx ? prevOutScript : P.EMPTY,
583
+ }));
584
+ if (isAny) inputs = [inputs[idx]];
585
+ else if (isNone || isSingle) {
586
+ inputs = inputs.map((input, inputIdx) => ({
587
+ ...input,
588
+ sequence: inputIdx === idx ? input.sequence : 0,
589
+ }));
590
+ }
591
+ let outputs = this.outputs.map(outputBeforeSign);
592
+ if (isNone) outputs = [];
593
+ else if (isSingle) {
594
+ outputs = outputs.slice(0, idx).fill(EMPTY_OUTPUT).concat([outputs[idx]]);
595
+ }
596
+ const tmpTx = RawTx.encode({
597
+ lockTime: this.lockTime,
598
+ version: this.version,
599
+ segwitFlag: false,
600
+ inputs,
601
+ outputs,
602
+ });
603
+ return u.sha256x2(tmpTx, P.I32LE.encode(hashType));
604
+ }
605
+ preimageWitnessV0(idx: number, prevOutScript: Bytes, hashType: number, amount: bigint) {
606
+ const { isAny, isNone, isSingle } = unpackSighash(hashType);
607
+ let inputHash = EMPTY32;
608
+ let sequenceHash = EMPTY32;
609
+ let outputHash = EMPTY32;
610
+ const inputs = this.inputs.map(inputBeforeSign);
611
+ const outputs = this.outputs.map(outputBeforeSign);
612
+ if (!isAny) inputHash = u.sha256x2(...inputs.map(TxHashIdx.encode));
613
+ if (!isAny && !isSingle && !isNone)
614
+ sequenceHash = u.sha256x2(...inputs.map((i) => P.U32LE.encode(i.sequence)));
615
+ if (!isSingle && !isNone) {
616
+ outputHash = u.sha256x2(...outputs.map(RawOutput.encode));
617
+ } else if (isSingle && idx < outputs.length)
618
+ outputHash = u.sha256x2(RawOutput.encode(outputs[idx]));
619
+ const input = inputs[idx];
620
+ return u.sha256x2(
621
+ P.I32LE.encode(this.version),
622
+ inputHash,
623
+ sequenceHash,
624
+ P.bytes(32, true).encode(input.txid),
625
+ P.U32LE.encode(input.index),
626
+ VarBytes.encode(prevOutScript),
627
+ P.U64LE.encode(amount),
628
+ P.U32LE.encode(input.sequence),
629
+ outputHash,
630
+ P.U32LE.encode(this.lockTime),
631
+ P.U32LE.encode(hashType)
632
+ );
633
+ }
634
+ preimageWitnessV1(
635
+ idx: number,
636
+ prevOutScript: Bytes[],
637
+ hashType: number,
638
+ amount: bigint[],
639
+ codeSeparator = -1,
640
+ leafScript?: Bytes,
641
+ leafVer = 0xc0,
642
+ annex?: Bytes
643
+ ) {
644
+ if (!Array.isArray(amount) || this.inputs.length !== amount.length)
645
+ throw new Error(`Invalid amounts array=${amount}`);
646
+ if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
647
+ throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
648
+ const out: Bytes[] = [
649
+ P.U8.encode(0),
650
+ P.U8.encode(hashType), // U8 sigHash
651
+ P.I32LE.encode(this.version),
652
+ P.U32LE.encode(this.lockTime),
653
+ ];
654
+ const outType = hashType === SignatureHash.DEFAULT ? SignatureHash.ALL : hashType & 0b11;
655
+ const inType = hashType & SignatureHash.ANYONECANPAY;
656
+ const inputs = this.inputs.map(inputBeforeSign);
657
+ const outputs = this.outputs.map(outputBeforeSign);
658
+ if (inType !== SignatureHash.ANYONECANPAY) {
659
+ out.push(
660
+ ...[
661
+ inputs.map(TxHashIdx.encode),
662
+ amount.map(P.U64LE.encode),
663
+ prevOutScript.map(VarBytes.encode),
664
+ inputs.map((i) => P.U32LE.encode(i.sequence)),
665
+ ].map((i) => u.sha256(concatBytes(...i)))
666
+ );
667
+ }
668
+ if (outType === SignatureHash.ALL) {
669
+ out.push(u.sha256(concatBytes(...outputs.map(RawOutput.encode))));
670
+ }
671
+ const spendType = (annex ? 1 : 0) | (leafScript ? 2 : 0);
672
+ out.push(new Uint8Array([spendType]));
673
+ if (inType === SignatureHash.ANYONECANPAY) {
674
+ const inp = inputs[idx];
675
+ out.push(
676
+ TxHashIdx.encode(inp),
677
+ P.U64LE.encode(amount[idx]),
678
+ VarBytes.encode(prevOutScript[idx]),
679
+ P.U32LE.encode(inp.sequence)
680
+ );
681
+ } else out.push(P.U32LE.encode(idx));
682
+ if (spendType & 1) out.push(u.sha256(VarBytes.encode(annex || P.EMPTY)));
683
+ if (outType === SignatureHash.SINGLE)
684
+ out.push(idx < outputs.length ? u.sha256(RawOutput.encode(outputs[idx])) : EMPTY32);
685
+ if (leafScript)
686
+ out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
687
+ return u.tagSchnorr('TapSighash', ...out);
688
+ }
689
+ // Signer can be privateKey OR instance of bip32 HD stuff
690
+ signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
691
+ this.checkInputIdx(idx);
692
+ const input = this.inputs[idx];
693
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
694
+ // Handle BIP32 HDKey
695
+ if (!isBytes(privateKey)) {
696
+ if (!input.bip32Derivation || !input.bip32Derivation.length)
697
+ throw new Error('bip32Derivation: empty');
698
+ const signers = input.bip32Derivation
699
+ .filter((i) => i[1].fingerprint == (privateKey as HDKey).fingerprint)
700
+ .map(([pubKey, { path }]) => {
701
+ let s = privateKey as HDKey;
702
+ for (const i of path) s = s.deriveChild(i);
703
+ if (!P.equalBytes(s.publicKey, pubKey)) throw new Error('bip32Derivation: wrong pubKey');
704
+ if (!s.privateKey) throw new Error('bip32Derivation: no privateKey');
705
+ return s;
706
+ });
707
+ if (!signers.length)
708
+ throw new Error(`bip32Derivation: no items with fingerprint=${privateKey.fingerprint}`);
709
+ let signed = false;
710
+ for (const s of signers) if (this.signIdx(s.privateKey, idx)) signed = true;
711
+ return signed;
712
+ }
713
+ // Sighash checks
714
+ // Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
715
+ if (!allowedSighash) allowedSighash = [inputType.defaultSighash as unknown as SigHash];
716
+ else allowedSighash.forEach(validateSigHash);
717
+ const sighash = inputType.sighash;
718
+ if (!allowedSighash.includes(sighash)) {
719
+ throw new Error(
720
+ `Input with not allowed sigHash=${sighash}. Allowed: ${allowedSighash.join(', ')}`
721
+ );
722
+ }
723
+ // It is possible to sign these inputs for legacy/segwit v0 (but no taproot!),
724
+ // however this was because of bug in bitcoin-core, which remains here because of consensus.
725
+ // If this is absolutely neccessary for your case, please open issue.
726
+ // We disable it to avoid complicated workflow where SINGLE will block adding new outputs
727
+ const { sigOutputs } = this.inputSighash(idx);
728
+ if (sigOutputs === SignatureHash.SINGLE && idx >= this.outputs.length) {
729
+ throw new Error(
730
+ `Input with sighash SINGLE, but there is no output with corresponding index=${idx}`
731
+ );
732
+ }
733
+
734
+ // Actual signing
735
+ // Taproot
736
+ const prevOut = getPrevOut(input);
737
+ if (inputType.txType === 'taproot') {
738
+ if (input.tapBip32Derivation) throw new Error('tapBip32Derivation unsupported');
739
+ const prevOuts = this.inputs.map(getPrevOut);
740
+ const prevOutScript = prevOuts.map((i) => i.script);
741
+ const amount = prevOuts.map((i) => i.amount);
742
+ let signed = false;
743
+ let schnorrPub = u.pubSchnorr(privateKey);
744
+ let merkleRoot = input.tapMerkleRoot || P.EMPTY;
745
+ if (input.tapInternalKey) {
746
+ // internal + tweak = tweaked key
747
+ // if internal key == current public key, we need to tweak private key,
748
+ // otherwise sign as is. bitcoinjs implementation always wants tweaked
749
+ // priv key to be provided
750
+ const { pubKey, privKey } = getTaprootKeys(
751
+ privateKey,
752
+ schnorrPub,
753
+ input.tapInternalKey,
754
+ merkleRoot
755
+ );
756
+ const [taprootPubKey, _] = u.taprootTweakPubkey(input.tapInternalKey, merkleRoot);
757
+ if (P.equalBytes(taprootPubKey, pubKey)) {
758
+ const hash = this.preimageWitnessV1(idx, prevOutScript, sighash, amount);
759
+ const sig = concatBytes(
760
+ u.signSchnorr(hash, privKey, _auxRand),
761
+ sighash !== SignatureHash.DEFAULT ? new Uint8Array([sighash]) : P.EMPTY
762
+ );
763
+ this.updateInput(idx, { tapKeySig: sig }, true);
764
+ signed = true;
765
+ }
766
+ }
767
+ if (input.tapLeafScript) {
768
+ input.tapScriptSig = input.tapScriptSig || [];
769
+ for (const [_, _script] of input.tapLeafScript) {
770
+ const script = _script.subarray(0, -1);
771
+ const scriptDecoded = Script.decode(script);
772
+ const ver = _script[_script.length - 1];
773
+ const hash = tapLeafHash(script, ver);
774
+ // NOTE: no need to tweak internal key here, since we don't support nested p2tr
775
+ const pos = scriptDecoded.findIndex((i) => isBytes(i) && P.equalBytes(i, schnorrPub));
776
+ // Skip if there is no public key in tapLeafScript
777
+ if (pos === -1) continue;
778
+ const msg = this.preimageWitnessV1(
779
+ idx,
780
+ prevOutScript,
781
+ sighash,
782
+ amount,
783
+ undefined,
784
+ script,
785
+ ver
786
+ );
787
+ const sig = concatBytes(
788
+ u.signSchnorr(msg, privateKey, _auxRand),
789
+ sighash !== SignatureHash.DEFAULT ? new Uint8Array([sighash]) : P.EMPTY
790
+ );
791
+ this.updateInput(
792
+ idx,
793
+ { tapScriptSig: [[{ pubKey: schnorrPub, leafHash: hash }, sig]] },
794
+ true
795
+ );
796
+ signed = true;
797
+ }
798
+ }
799
+ if (!signed) throw new Error('No taproot scripts signed');
800
+ return true;
801
+ } else {
802
+ // only compressed keys are supported for now
803
+ const pubKey = u.pubECDSA(privateKey);
804
+ // TODO: replace with explicit checks
805
+ // Check if script has public key or its has inside
806
+ let hasPubkey = false;
807
+ const pubKeyHash = u.hash160(pubKey);
808
+ for (const i of Script.decode(inputType.lastScript)) {
809
+ if (isBytes(i) && (P.equalBytes(i, pubKey) || P.equalBytes(i, pubKeyHash)))
810
+ hasPubkey = true;
811
+ }
812
+ if (!hasPubkey) throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
813
+ let hash;
814
+ if (inputType.txType === 'legacy') {
815
+ hash = this.preimageLegacy(idx, inputType.lastScript, sighash);
816
+ } else if (inputType.txType === 'segwit') {
817
+ let script = inputType.lastScript;
818
+ // If wpkh OR sh-wpkh, wsh-wpkh is impossible, so looks ok
819
+ if (inputType.last.type === 'wpkh')
820
+ script = OutScript.encode({ type: 'pkh', hash: inputType.last.hash });
821
+ hash = this.preimageWitnessV0(idx, script, sighash, prevOut.amount);
822
+ } else throw new Error(`Transaction/sign: unknown tx type: ${inputType.txType}`);
823
+ const sig = u.signECDSA(hash, privateKey, this.opts.lowR);
824
+ this.updateInput(
825
+ idx,
826
+ {
827
+ partialSig: [[pubKey, concatBytes(sig, new Uint8Array([sighash]))]],
828
+ },
829
+ true
830
+ );
831
+ }
832
+ return true;
833
+ }
834
+ // This is bad API. Will work if user creates and signs tx, but if
835
+ // there is some complex workflow with exchanging PSBT and signing them,
836
+ // then it is better to validate which output user signs. How could a better API look like?
837
+ // Example: user adds input, sends to another party, then signs received input (mixer etc),
838
+ // another user can add different input for same key and user will sign it.
839
+ // Even worse: another user can add bip32 derivation, and spend money from different address.
840
+ // Better api: signIdx
841
+ sign(privateKey: Signer, allowedSighash?: number[], _auxRand?: Bytes): number {
842
+ let num = 0;
843
+ for (let i = 0; i < this.inputs.length; i++) {
844
+ try {
845
+ if (this.signIdx(privateKey, i, allowedSighash, _auxRand)) num++;
846
+ } catch (e) {}
847
+ }
848
+ if (!num) throw new Error('No inputs signed');
849
+ return num;
850
+ }
851
+
852
+ finalizeIdx(idx: number) {
853
+ this.checkInputIdx(idx);
854
+ if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
855
+ const input = this.inputs[idx];
856
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
857
+ // Taproot finalize
858
+ if (inputType.txType === 'taproot') {
859
+ if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig];
860
+ else if (input.tapLeafScript && input.tapScriptSig) {
861
+ // Sort leafs by control block length.
862
+ const leafs = input.tapLeafScript.sort(
863
+ (a, b) =>
864
+ psbt.TaprootControlBlock.encode(a[0]).length -
865
+ psbt.TaprootControlBlock.encode(b[0]).length
866
+ );
867
+ for (const [cb, _script] of leafs) {
868
+ // Last byte is version
869
+ const script = _script.slice(0, -1);
870
+ const ver = _script[_script.length - 1];
871
+ const outScript = OutScript.decode(script);
872
+ const hash = tapLeafHash(script, ver);
873
+ const scriptSig = input.tapScriptSig.filter((i) => P.equalBytes(i[0].leafHash, hash));
874
+ let signatures: Bytes[] = [];
875
+ if (outScript.type === 'tr_ms') {
876
+ const m = outScript.m;
877
+ const pubkeys = outScript.pubkeys;
878
+ let added = 0;
879
+ for (const pub of pubkeys) {
880
+ const sigIdx = scriptSig.findIndex((i) => P.equalBytes(i[0].pubKey, pub));
881
+ // Should have exact amount of signatures (more -- will fail)
882
+ if (added === m || sigIdx === -1) {
883
+ signatures.push(P.EMPTY);
884
+ continue;
885
+ }
886
+ signatures.push(scriptSig[sigIdx][1]);
887
+ added++;
888
+ }
889
+ // Should be exact same as m
890
+ if (added !== m) continue;
891
+ } else if (outScript.type === 'tr_ns') {
892
+ for (const pub of outScript.pubkeys) {
893
+ const sigIdx = scriptSig.findIndex((i) => P.equalBytes(i[0].pubKey, pub));
894
+ if (sigIdx === -1) continue;
895
+ signatures.push(scriptSig[sigIdx][1]);
896
+ }
897
+ if (signatures.length !== outScript.pubkeys.length) continue;
898
+ } else if (outScript.type === 'unknown' && this.opts.allowUnknownInputs) {
899
+ // Trying our best to sign what we can
900
+ const scriptDecoded = Script.decode(script);
901
+ signatures = scriptSig
902
+ .map(([{ pubKey }, signature]) => {
903
+ const pos = scriptDecoded.findIndex((i) => isBytes(i) && P.equalBytes(i, pubKey));
904
+ if (pos === -1)
905
+ throw new Error('finalize/taproot: cannot find position of pubkey in script');
906
+ return { signature, pos };
907
+ })
908
+ // Reverse order (because witness is stack and we take last element first from it)
909
+ .sort((a, b) => a.pos - b.pos)
910
+ .map((i) => i.signature);
911
+ if (!signatures.length) continue;
912
+ } else {
913
+ const custom = this.opts.customScripts;
914
+ if (custom) {
915
+ for (const c of custom) {
916
+ if (!c.finalizeTaproot) continue;
917
+ const scriptDecoded = Script.decode(script);
918
+ const csEncoded = c.encode(scriptDecoded);
919
+ if (csEncoded === undefined) continue;
920
+ const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
921
+ if (!finalized) continue;
922
+ input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
923
+ input.finalScriptSig = P.EMPTY;
924
+ cleanFinalInput(input);
925
+ return;
926
+ }
927
+ }
928
+ throw new Error('Finalize: Unknown tapLeafScript');
929
+ }
930
+ // Witness is stack, so last element will be used first
931
+ input.finalScriptWitness = signatures
932
+ .reverse()
933
+ .concat([script, psbt.TaprootControlBlock.encode(cb)]);
934
+ break;
935
+ }
936
+ if (!input.finalScriptWitness) throw new Error('finalize/taproot: empty witness');
937
+ } else throw new Error('finalize/taproot: unknown input');
938
+ input.finalScriptSig = P.EMPTY;
939
+ cleanFinalInput(input);
940
+ return;
941
+ }
942
+ if (!input.partialSig || !input.partialSig.length) throw new Error('Not enough partial sign');
943
+
944
+ let inputScript: Bytes = P.EMPTY;
945
+ let witness: Bytes[] = [];
946
+ // TODO: move input scripts closer to payments/output scripts
947
+ // Multisig
948
+ if (inputType.last.type === 'ms') {
949
+ const m = inputType.last.m;
950
+ const pubkeys = inputType.last.pubkeys;
951
+ let signatures = [];
952
+ // partial: [pubkey, sign]
953
+ for (const pub of pubkeys) {
954
+ const sign = input.partialSig.find((s) => P.equalBytes(pub, s[0]));
955
+ if (!sign) continue;
956
+ signatures.push(sign[1]);
957
+ }
958
+ signatures = signatures.slice(0, m);
959
+ if (signatures.length !== m) {
960
+ throw new Error(
961
+ `Multisig: wrong signatures count, m=${m} n=${pubkeys.length} signatures=${signatures.length}`
962
+ );
963
+ }
964
+ inputScript = Script.encode([0, ...signatures]);
965
+ } else if (inputType.last.type === 'pk') {
966
+ inputScript = Script.encode([input.partialSig[0][1]]);
967
+ } else if (inputType.last.type === 'pkh') {
968
+ inputScript = Script.encode([input.partialSig[0][1], input.partialSig[0][0]]);
969
+ } else if (inputType.last.type === 'wpkh') {
970
+ inputScript = P.EMPTY;
971
+ witness = [input.partialSig[0][1], input.partialSig[0][0]];
972
+ } else if (inputType.last.type === 'unknown' && !this.opts.allowUnknownInputs)
973
+ throw new Error('Unknown inputs not allowed');
974
+
975
+ // Create final scripts (generic part)
976
+ let finalScriptSig: Bytes | undefined, finalScriptWitness: Bytes[] | undefined;
977
+ if (inputType.type.includes('wsh-')) {
978
+ // P2WSH
979
+ if (inputScript.length && inputType.lastScript.length) {
980
+ witness = Script.decode(inputScript).map((i) => {
981
+ if (i === 0) return P.EMPTY;
982
+ if (isBytes(i)) return i;
983
+ throw new Error(`Wrong witness op=${i}`);
984
+ });
985
+ }
986
+ witness = witness.concat(inputType.lastScript);
987
+ }
988
+ if (inputType.txType === 'segwit') finalScriptWitness = witness;
989
+ if (inputType.type.startsWith('sh-wsh-')) {
990
+ finalScriptSig = Script.encode([Script.encode([0, u.sha256(inputType.lastScript)])]);
991
+ } else if (inputType.type.startsWith('sh-')) {
992
+ finalScriptSig = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
993
+ } else if (inputType.type.startsWith('wsh-')) {
994
+ } else if (inputType.txType !== 'segwit') finalScriptSig = inputScript;
995
+
996
+ if (!finalScriptSig && !finalScriptWitness) throw new Error('Unknown error finalizing input');
997
+ if (finalScriptSig) input.finalScriptSig = finalScriptSig;
998
+ if (finalScriptWitness) input.finalScriptWitness = finalScriptWitness;
999
+ cleanFinalInput(input);
1000
+ }
1001
+ finalize() {
1002
+ for (let i = 0; i < this.inputs.length; i++) this.finalizeIdx(i);
1003
+ }
1004
+ extract() {
1005
+ if (!this.isFinal) throw new Error('Transaction has unfinalized inputs');
1006
+ if (!this.outputs.length) throw new Error('Transaction has no outputs');
1007
+ if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
1008
+ return this.toBytes(true, true);
1009
+ }
1010
+ combine(other: Transaction): this {
1011
+ for (const k of ['PSBTVersion', 'version', 'lockTime'] as const) {
1012
+ if (this.opts[k] !== other.opts[k]) {
1013
+ throw new Error(
1014
+ `Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`
1015
+ );
1016
+ }
1017
+ }
1018
+ for (const k of ['inputs', 'outputs'] as const) {
1019
+ if (this[k].length !== other[k].length) {
1020
+ throw new Error(
1021
+ `Transaction/combine: different ${k} length this=${this[k].length} other=${other[k].length}`
1022
+ );
1023
+ }
1024
+ }
1025
+ const thisUnsigned = this.global.unsignedTx ? RawTx.encode(this.global.unsignedTx) : P.EMPTY;
1026
+ const otherUnsigned = other.global.unsignedTx ? RawTx.encode(other.global.unsignedTx) : P.EMPTY;
1027
+ if (!P.equalBytes(thisUnsigned, otherUnsigned))
1028
+ throw new Error(`Transaction/combine: different unsigned tx`);
1029
+ this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global);
1030
+ for (let i = 0; i < this.inputs.length; i++) this.updateInput(i, other.inputs[i], true);
1031
+ for (let i = 0; i < this.outputs.length; i++) this.updateOutput(i, other.outputs[i], true);
1032
+ return this;
1033
+ }
1034
+ clone() {
1035
+ // deepClone probably faster, but this enforces that encoding is valid
1036
+ return Transaction.fromPSBT(this.toPSBT(this.opts.PSBTVersion), this.opts);
1037
+ }
1038
+ }
1039
+
1040
+ export function PSBTCombine(psbts: Bytes[]): Bytes {
1041
+ if (!psbts || !Array.isArray(psbts) || !psbts.length)
1042
+ throw new Error('PSBTCombine: wrong PSBT list');
1043
+ const tx = Transaction.fromPSBT(psbts[0]);
1044
+ for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i]));
1045
+ return tx.toPSBT();
1046
+ }
1047
+
1048
+ // Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
1049
+ const HARDENED_OFFSET: number = 0x80000000;
1050
+ export function bip32Path(path: string): number[] {
1051
+ const out: number[] = [];
1052
+ if (!/^[mM]'?/.test(path)) throw new Error('Path must start with "m" or "M"');
1053
+ if (/^[mM]'?$/.test(path)) return out;
1054
+ const parts = path.replace(/^[mM]'?\//, '').split('/');
1055
+ for (const c of parts) {
1056
+ const m = /^(\d+)('?)$/.exec(c);
1057
+ if (!m || m.length !== 3) throw new Error(`Invalid child index: ${c}`);
1058
+ let idx = +m[1];
1059
+ if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) throw new Error('Invalid index');
1060
+ // hardened key
1061
+ if (m[2] === "'") idx += HARDENED_OFFSET;
1062
+ out.push(idx);
1063
+ }
1064
+ return out;
1065
+ }