ecash-lib 4.10.0 → 4.12.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 (43) hide show
  1. package/README.md +2 -0
  2. package/dist/consts.d.ts +11 -0
  3. package/dist/consts.d.ts.map +1 -1
  4. package/dist/consts.js +12 -1
  5. package/dist/consts.js.map +1 -1
  6. package/dist/ffi/ecash_lib_wasm_bg_browser.js +20489 -20489
  7. package/dist/ffi/ecash_lib_wasm_bg_browser.wasm +0 -0
  8. package/dist/ffi/ecash_lib_wasm_bg_nodejs.wasm +0 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/psbt.d.ts +115 -0
  14. package/dist/psbt.d.ts.map +1 -0
  15. package/dist/psbt.js +743 -0
  16. package/dist/psbt.js.map +1 -0
  17. package/dist/script.d.ts +11 -1
  18. package/dist/script.d.ts.map +1 -1
  19. package/dist/script.js +24 -0
  20. package/dist/script.js.map +1 -1
  21. package/dist/signatories.d.ts +44 -0
  22. package/dist/signatories.d.ts.map +1 -0
  23. package/dist/signatories.js +206 -0
  24. package/dist/signatories.js.map +1 -0
  25. package/dist/tx.d.ts +60 -0
  26. package/dist/tx.d.ts.map +1 -1
  27. package/dist/tx.js +274 -0
  28. package/dist/tx.js.map +1 -1
  29. package/dist/txBuilder.d.ts +2 -30
  30. package/dist/txBuilder.d.ts.map +1 -1
  31. package/dist/txBuilder.js +1 -45
  32. package/dist/txBuilder.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/consts.ts +13 -0
  35. package/src/ffi/ecash_lib_wasm_bg_browser.js +20489 -20489
  36. package/src/ffi/ecash_lib_wasm_bg_browser.wasm +0 -0
  37. package/src/ffi/ecash_lib_wasm_bg_nodejs.wasm +0 -0
  38. package/src/index.ts +2 -0
  39. package/src/psbt.ts +908 -0
  40. package/src/script.ts +25 -1
  41. package/src/signatories.ts +296 -0
  42. package/src/tx.ts +330 -0
  43. package/src/txBuilder.ts +2 -74
package/src/psbt.ts ADDED
@@ -0,0 +1,908 @@
1
+ // Copyright (c) 2026 The Bitcoin developers
2
+ // Distributed under the MIT software license, see the accompanying
3
+ // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
+
5
+ /**
6
+ * Partially Signed Bitcoin Transaction (PSBT) per **BIP 174**:
7
+ * - Spec: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
8
+ *
9
+ * Decode/encode aligned with Bitcoin ABC (`{@link Psbt.fromBytes}`,
10
+ * `{@link Psbt.toBytes}`), plus multisig workflows: per-input `PSBT_IN_UTXO`
11
+ * (`0x00`), redeem script, partial signatures; unknown pairs preserved (BIP 174).
12
+ *
13
+ * **Input key `0x00` (`PSBT_IN_UTXO`):** BIP 174 also defines type `0x01` (“witness
14
+ * UTXO”) for the same *value* shape. **Bitcoin ABC only implements `0x00`:** value
15
+ * is `CTxOut` or full previous tx (non-witness UTXO). We match the node; `0x01`
16
+ * entries are preserved as unknown keys on round-trip.
17
+ *
18
+ * High-level merge/sign helpers: {@link Tx.addMultisigSignature},
19
+ * {@link Tx.addMultisigSignatureFromKey}; {@link Psbt.fromTx} builds a PSBT from a
20
+ * partially signed {@link Tx}; {@link Psbt.toTx} rebuilds scriptSigs from partial sigs.
21
+ */
22
+
23
+ import { Bytes } from './io/bytes.js';
24
+ import { fromHex, toHex, toHexRev } from './io/hex.js';
25
+ import { readVarSize, writeVarSize } from './io/varsize.js';
26
+ import { WriterBytes } from './io/writerbytes.js';
27
+ import { WriterLength } from './io/writerlength.js';
28
+ import { Writer } from './io/writer.js';
29
+ import { Ecc } from './ecc.js';
30
+ import { shaRmd160, sha256d } from './hash.js';
31
+ import { Script } from './script.js';
32
+ import { ALL_BIP143, type SigHashType } from './sigHashType.js';
33
+ import {
34
+ copyTxInput,
35
+ copyTxOutput,
36
+ OutPoint,
37
+ SignData,
38
+ Tx,
39
+ TxInput,
40
+ } from './tx.js';
41
+ import { UnsignedTx } from './unsignedTx.js';
42
+
43
+ /**
44
+ * BIP 174 **magic bytes** for PSBT version 0: ASCII `psbt` + `0xff`.
45
+ * Defined in BIP 174 “Specification > Version 0” (must be first bytes of a `.psbt`).
46
+ * - https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#specification
47
+ * - https://bips.dev/174/#specification
48
+ */
49
+ export const PSBT_MAGIC = new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff]);
50
+
51
+ const PSBT_GLOBAL_UNSIGNED_TX = 0x00;
52
+ /**
53
+ * Bitcoin ABC `PSBT_IN_UTXO` (BIP 174 input type `0x00`): value is either the full
54
+ * previous transaction (BIP “non-witness UTXO”) or a serialized `CTxOut` (amount +
55
+ * scriptPubKey). This is the **only** key we use for spent-output data; BIP type
56
+ * `0x01` is not handled like the node (see module TSDoc). Resolved in
57
+ * {@link resolveWitnessFromKey00}.
58
+ */
59
+ export const PSBT_IN_UTXO = 0x00;
60
+ const PSBT_IN_PARTIAL_SIG = 0x02;
61
+ const PSBT_IN_SIGHASH = 0x03;
62
+ const PSBT_IN_REDEEM_SCRIPT = 0x04;
63
+ /** BIP 174 / Bitcoin ABC — input HD keypaths */
64
+ const PSBT_IN_BIP32_DERIVATION = 0x06;
65
+ const PSBT_IN_SCRIPTSIG = 0x07;
66
+ /** Bitcoin ABC — output redeem script (same first byte as global unsigned tx key type). */
67
+ const PSBT_OUT_REDEEMSCRIPT = 0x00;
68
+ /** Bitcoin ABC — output HD keypaths */
69
+ const PSBT_OUT_BIP32_DERIVATION = 0x02;
70
+
71
+ function compareLex(a: Uint8Array, b: Uint8Array): number {
72
+ const n = Math.min(a.length, b.length);
73
+ for (let i = 0; i < n; i++) {
74
+ if (a[i] !== b[i]) return a[i]! - b[i]!;
75
+ }
76
+ return a.length - b.length;
77
+ }
78
+
79
+ function sortPairs(pairs: { key: Uint8Array; value: Uint8Array }[]): void {
80
+ pairs.sort((x, y) => compareLex(x.key, y.key));
81
+ }
82
+
83
+ function writePsbtKeyValue(writer: Writer, key: Uint8Array, value: Uint8Array) {
84
+ writeVarSize(key.length, writer);
85
+ writer.putBytes(key);
86
+ writeVarSize(value.length, writer);
87
+ writer.putBytes(value);
88
+ }
89
+
90
+ function serializeMap(
91
+ pairs: { key: Uint8Array; value: Uint8Array }[],
92
+ ): Uint8Array {
93
+ sortPairs(pairs);
94
+ const wl = new WriterLength();
95
+ for (const p of pairs) {
96
+ writePsbtKeyValue(wl, p.key, p.value);
97
+ }
98
+ writeVarSize(0, wl);
99
+ const out = new WriterBytes(wl.length);
100
+ for (const p of pairs) {
101
+ writePsbtKeyValue(out, p.key, p.value);
102
+ }
103
+ writeVarSize(0, out);
104
+ return out.data;
105
+ }
106
+
107
+ function parseMap(bytes: Bytes): { key: Uint8Array; value: Uint8Array }[] {
108
+ const pairs: { key: Uint8Array; value: Uint8Array }[] = [];
109
+ while (true) {
110
+ const keyLen = readVarSize(bytes);
111
+ if (keyLen === 0) break;
112
+ const key = bytes.readBytes(Number(keyLen));
113
+ const valLen = readVarSize(bytes);
114
+ const value = bytes.readBytes(Number(valLen));
115
+ pairs.push({ key, value });
116
+ }
117
+ return pairs;
118
+ }
119
+
120
+ /** BIP 174 / Bitcoin ABC: duplicate keys in a map are forbidden (see `PSBTInput::Unserialize`). */
121
+ function assertUniquePsbtKeys(
122
+ pairs: { key: Uint8Array; value: Uint8Array }[],
123
+ mapLabel: string,
124
+ ): void {
125
+ const seen = new Set<string>();
126
+ for (const { key } of pairs) {
127
+ if (key.length === 0) continue;
128
+ const id = toHex(key);
129
+ if (seen.has(id)) {
130
+ throw new Error(`PSBT: duplicate key in ${mapLabel} map`);
131
+ }
132
+ seen.add(id);
133
+ }
134
+ }
135
+
136
+ /** Global unsigned transaction must have empty scriptSigs (Bitcoin ABC `PartiallySignedTransaction::Unserialize`). */
137
+ function assertUnsignedTxEmptyScriptSigs(tx: Tx): void {
138
+ for (let i = 0; i < tx.inputs.length; i++) {
139
+ const sc = tx.inputs[i]?.script;
140
+ const len = sc?.bytecode.length ?? 0;
141
+ if (len > 0) {
142
+ throw new Error('PSBT: unsigned tx must have empty scriptSigs');
143
+ }
144
+ }
145
+ }
146
+
147
+ /** Partial signature key: 1 byte type + 33 (compressed) or 65 (uncompressed) byte pubkey. */
148
+ function isValidPartialSigKeyLength(keyLen: number): boolean {
149
+ return keyLen === 34 || keyLen === 66;
150
+ }
151
+
152
+ /** CPubKey-style prefix check (matches `DeserializeHDKeypaths` length rules; not full curve validation). */
153
+ function pubkeyBytesLookPlausible(pk: Uint8Array): boolean {
154
+ if (pk.length === 33) {
155
+ return pk[0] === 0x02 || pk[0] === 0x03;
156
+ }
157
+ if (pk.length === 65) {
158
+ return pk[0] === 0x04;
159
+ }
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * `DeserializeHDKeypaths` in `src/script/sign.h` (Bitcoin ABC) reads a leading
165
+ * compact size `value_len`, then exactly `value_len` bytes (fingerprint + path).
166
+ *
167
+ * Some `rpc_psbt.json` valid vectors use the same logical payload **without** that
168
+ * length prefix (fingerprint + uint32 path only), matching BIP 174’s description
169
+ * of the value bytes; we accept both shapes so `decodepsbt`-valid PSBTs parse.
170
+ */
171
+ function validateBip32DerivationKeyValue(
172
+ key: Uint8Array,
173
+ value: Uint8Array,
174
+ ): void {
175
+ if (key.length !== 34 && key.length !== 66) {
176
+ throw new Error(
177
+ 'PSBT: size of key was not the expected size for the type BIP32 keypath',
178
+ );
179
+ }
180
+ const pk = key.slice(1);
181
+ if (!pubkeyBytesLookPlausible(pk)) {
182
+ throw new Error('PSBT: invalid pubkey in BIP32 derivation key');
183
+ }
184
+ const tryPrefixedLength = (): boolean => {
185
+ const b = new Bytes(value);
186
+ let valueLen: number;
187
+ try {
188
+ valueLen = Number(readVarSize(b));
189
+ } catch {
190
+ return false;
191
+ }
192
+ if (valueLen === 0 || valueLen % 4 !== 0) {
193
+ return false;
194
+ }
195
+ if (value.length - b.idx < valueLen) {
196
+ return false;
197
+ }
198
+ b.readBytes(4);
199
+ for (let i = 4; i < valueLen; i += 4) {
200
+ b.readU32();
201
+ }
202
+ return b.idx === value.length;
203
+ };
204
+
205
+ if (tryPrefixedLength()) {
206
+ return;
207
+ }
208
+
209
+ if (value.length < 4 || value.length % 4 !== 0) {
210
+ throw new Error('PSBT: invalid length for HD key path');
211
+ }
212
+ const b = new Bytes(value);
213
+ b.readBytes(4);
214
+ for (let i = 4; i < value.length; i += 4) {
215
+ b.readU32();
216
+ }
217
+ if (b.idx !== value.length) {
218
+ throw new Error('PSBT: invalid BIP32 derivation value');
219
+ }
220
+ }
221
+
222
+ /** Strip input scripts for PSBT global unsigned transaction (BIP 174). */
223
+ export function txToUnsigned(tx: Tx): Tx {
224
+ return new Tx({
225
+ version: tx.version,
226
+ inputs: tx.inputs.map(inp => ({
227
+ prevOut: inp.prevOut,
228
+ script: new Script(),
229
+ sequence: inp.sequence,
230
+ })),
231
+ outputs: tx.outputs.map(o => copyTxOutput(o)),
232
+ locktime: tx.locktime,
233
+ });
234
+ }
235
+
236
+ /** `CTxOut` bytes for a {@link PSBT_IN_UTXO} map value (same layout BIP 174 labels “witness UTXO”). */
237
+ function encodeWitnessUtxo(sats: bigint, scriptPubKey: Uint8Array): Uint8Array {
238
+ const wl = new WriterLength();
239
+ wl.putU64(sats);
240
+ writeVarSize(scriptPubKey.length, wl);
241
+ wl.putBytes(scriptPubKey);
242
+ const w = new WriterBytes(wl.length);
243
+ w.putU64(sats);
244
+ writeVarSize(scriptPubKey.length, w);
245
+ w.putBytes(scriptPubKey);
246
+ return w.data;
247
+ }
248
+
249
+ function decodeWitnessUtxo(data: Uint8Array): {
250
+ sats: bigint;
251
+ scriptPubKey: Uint8Array;
252
+ } {
253
+ const b = new Bytes(data);
254
+ const sats = b.readU64();
255
+ const sl = readVarSize(b);
256
+ const scriptPubKey = b.readBytes(Number(sl));
257
+ return { sats, scriptPubKey };
258
+ }
259
+
260
+ function prevOutTxidHex(po: OutPoint): string {
261
+ return typeof po.txid === 'string' ? po.txid : toHexRev(po.txid);
262
+ }
263
+
264
+ /**
265
+ * Value for input key type `0x00` (`PSBT_IN_UTXO` in Bitcoin ABC): either BIP 174 non-witness
266
+ * UTXO (full prev tx) or a `CTxOut`-shaped blob (amount + scriptPubKey), as in
267
+ * Bitcoin ABC.
268
+ */
269
+ function resolveWitnessFromKey00(
270
+ value: Uint8Array,
271
+ prevOut: OutPoint,
272
+ ): { sats: bigint; scriptPubKey: Uint8Array } | undefined {
273
+ if (value.length >= 50) {
274
+ const tx = Tx.tryDeserExact(value);
275
+ if (tx !== undefined && tx.inputs.length > 0) {
276
+ if (tx.txid() === prevOutTxidHex(prevOut)) {
277
+ const out = tx.outputs[prevOut.outIdx];
278
+ if (out !== undefined) {
279
+ return {
280
+ sats: out.sats,
281
+ scriptPubKey: out.script.bytecode,
282
+ };
283
+ }
284
+ }
285
+ }
286
+ }
287
+ try {
288
+ return decodeWitnessUtxo(value);
289
+ } catch {
290
+ return undefined;
291
+ }
292
+ }
293
+
294
+ function pubkeyHex(pk: Uint8Array): string {
295
+ return toHex(pk);
296
+ }
297
+
298
+ function scriptPubKeyFromSignData(signData: SignData): Uint8Array {
299
+ if (signData.redeemScript !== undefined) {
300
+ const h = shaRmd160(signData.redeemScript.bytecode);
301
+ return Script.p2sh(h).bytecode;
302
+ }
303
+ if (signData.outputScript !== undefined) {
304
+ return signData.outputScript.bytecode;
305
+ }
306
+ throw new Error('SignData needs redeemScript or outputScript for PSBT');
307
+ }
308
+
309
+ /**
310
+ * The multisig **redeem** script used to interpret partial sigs: P2SH `redeemScript`,
311
+ * or bare `outputScript` (the locking script is the multisig template itself).
312
+ */
313
+ function multisigLockingScript(signData: SignData): Script {
314
+ if (signData.redeemScript !== undefined) {
315
+ return signData.redeemScript;
316
+ }
317
+ if (signData.outputScript !== undefined) {
318
+ return signData.outputScript;
319
+ }
320
+ throw new Error('SignData needs redeemScript or outputScript');
321
+ }
322
+
323
+ /**
324
+ * Build the input `scriptSig` for a multisig spend from the PSBT partial-signature
325
+ * map (pubkey hex → signature with sighash byte). Used by {@link Psbt.toTx} only.
326
+ *
327
+ * Matches pubkey order from {@link multisigLockingScript}'s `parseMultisigRedeemScript`,
328
+ * then dispatches:
329
+ * - **Schnorr** if any partial sig has length 65 (Schnorr + sighash): uses
330
+ * {@link Script.multisigSpend} with `pubkeyIndices` derived from which pubkeys
331
+ * have sigs (bare passes `numPubkeys`; P2SH passes `redeemScript`).
332
+ * - **ECDSA** otherwise: fills `m` slots left-to-right from present sigs (same
333
+ * convention as {@link BareMultisigSignatory} / {@link P2SHMultisigSignatory}).
334
+ */
335
+ function buildScriptSigFromPartialSigs(
336
+ signData: SignData,
337
+ partialSigs: Map<string, Uint8Array>,
338
+ ): Script {
339
+ const lock = multisigLockingScript(signData);
340
+ const { numSignatures, numPubkeys, pubkeys } =
341
+ lock.parseMultisigRedeemScript();
342
+ const perPk = pubkeys.map(pk => partialSigs.get(pubkeyHex(pk)));
343
+ const signatures = perPk.filter((s): s is Uint8Array => s !== undefined);
344
+
345
+ const anySchnorr = [...partialSigs.values()].some(s => s.length === 65);
346
+ if (anySchnorr) {
347
+ const pubkeyIndices = new Set(
348
+ perPk.flatMap((s, i) => (s !== undefined ? [i] : [])),
349
+ );
350
+ if (signData.redeemScript !== undefined) {
351
+ return Script.multisigSpend({
352
+ signatures,
353
+ redeemScript: signData.redeemScript,
354
+ pubkeyIndices,
355
+ });
356
+ }
357
+ return Script.multisigSpend({
358
+ signatures,
359
+ pubkeyIndices,
360
+ numPubkeys,
361
+ });
362
+ }
363
+
364
+ const sigsForScript: (Uint8Array | undefined)[] =
365
+ signatures.length >= numSignatures
366
+ ? signatures.slice(0, numSignatures)
367
+ : [
368
+ ...signatures,
369
+ ...Array(numSignatures - signatures.length).fill(undefined),
370
+ ];
371
+
372
+ return Script.multisigSpend({
373
+ signatures: sigsForScript,
374
+ redeemScript: signData.redeemScript,
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Extract pubkey → signature (with sighash byte) from a multisig scriptSig.
380
+ */
381
+ function extractPartialSigsFromInput(
382
+ script: Script,
383
+ signData: SignData,
384
+ unsignedTx: Tx,
385
+ inputIdx: number,
386
+ ecc: Ecc,
387
+ ): Map<string, Uint8Array> {
388
+ const map = new Map<string, Uint8Array>();
389
+ if (!script.bytecode.length) return map;
390
+
391
+ const inputs: TxInput[] = unsignedTx.inputs.map((inp, i) => ({
392
+ ...copyTxInput(inp),
393
+ signData: i === inputIdx ? signData : undefined,
394
+ }));
395
+ inputs[inputIdx] = {
396
+ ...copyTxInput(unsignedTx.inputs[inputIdx]!),
397
+ script,
398
+ signData,
399
+ };
400
+
401
+ const txForPreimage = new Tx({
402
+ version: unsignedTx.version,
403
+ inputs,
404
+ outputs: unsignedTx.outputs.map(copyTxOutput),
405
+ locktime: unsignedTx.locktime,
406
+ });
407
+
408
+ const msgHash = sha256d(
409
+ UnsignedTx.fromTx(txForPreimage)
410
+ .inputAt(inputIdx)
411
+ .sigHashPreimage(ALL_BIP143).bytes,
412
+ );
413
+
414
+ if (signData.redeemScript !== undefined) {
415
+ const parsed = script.parseP2shMultisigSpend();
416
+ if (parsed.isSchnorr) {
417
+ const sorted = [...(parsed.pubkeyIndices ?? [])].sort(
418
+ (a, b) => a - b,
419
+ );
420
+ for (let i = 0; i < sorted.length; i++) {
421
+ const pkIdx = sorted[i]!;
422
+ const pk = parsed.pubkeys[pkIdx]!;
423
+ const sig = parsed.signatures[i];
424
+ if (sig !== undefined) map.set(pubkeyHex(pk), sig);
425
+ }
426
+ return map;
427
+ }
428
+ for (const sig of parsed.signatures) {
429
+ if (sig === undefined) continue;
430
+ const sigNo = sig.slice(0, -1);
431
+ for (const pk of parsed.pubkeys) {
432
+ try {
433
+ ecc.ecdsaVerify(sigNo, msgHash, pk);
434
+ map.set(pubkeyHex(pk), sig);
435
+ break;
436
+ } catch {
437
+ /* next */
438
+ }
439
+ }
440
+ }
441
+ return map;
442
+ }
443
+
444
+ if (signData.outputScript !== undefined) {
445
+ const parsed = script.parseBareMultisigSpend(signData.outputScript);
446
+ if (parsed.isSchnorr) {
447
+ const sorted = [...(parsed.pubkeyIndices ?? [])].sort(
448
+ (a, b) => a - b,
449
+ );
450
+ for (let i = 0; i < sorted.length; i++) {
451
+ const pkIdx = sorted[i]!;
452
+ const pk = parsed.pubkeys[pkIdx]!;
453
+ const sig = parsed.signatures[i];
454
+ if (sig !== undefined) map.set(pubkeyHex(pk), sig);
455
+ }
456
+ return map;
457
+ }
458
+ for (const sig of parsed.signatures) {
459
+ if (sig === undefined) continue;
460
+ const sigNo = sig.slice(0, -1);
461
+ for (const pk of parsed.pubkeys) {
462
+ try {
463
+ ecc.ecdsaVerify(sigNo, msgHash, pk);
464
+ map.set(pubkeyHex(pk), sig);
465
+ break;
466
+ } catch {
467
+ /* next */
468
+ }
469
+ }
470
+ }
471
+ }
472
+ return map;
473
+ }
474
+
475
+ export interface PsbtInputMaps {
476
+ witnessUtxo: { sats: bigint; scriptPubKey: Uint8Array };
477
+ redeemScript?: Script;
478
+ partialSigs: Map<string, Uint8Array>;
479
+ }
480
+
481
+ /** One PSBT key-value pair (BIP 174). */
482
+ export type PsbtKeyValue = { key: Uint8Array; value: Uint8Array };
483
+
484
+ function parseInputMapPairs(
485
+ inPairs: PsbtKeyValue[],
486
+ prevOut: OutPoint,
487
+ ): {
488
+ witness: { sats: bigint; scriptPubKey: Uint8Array } | undefined;
489
+ redeemScript?: Script;
490
+ partialSigs: Map<string, Uint8Array>;
491
+ unknown: PsbtKeyValue[];
492
+ } {
493
+ const partialSigs = new Map<string, Uint8Array>();
494
+ const unknown: PsbtKeyValue[] = [];
495
+ let redeemScript: Script | undefined;
496
+ let pair00: PsbtKeyValue | undefined;
497
+
498
+ for (const p of inPairs) {
499
+ if (p.key.length === 0) continue;
500
+ const t = p.key[0]!;
501
+ // Bitcoin ABC PSBTInput::Unserialize: these types require a 1-byte key only.
502
+ if (t === PSBT_IN_SIGHASH) {
503
+ if (p.key.length !== 1) {
504
+ throw new Error(
505
+ 'PSBT: sighash type key must be exactly one byte',
506
+ );
507
+ }
508
+ unknown.push(p);
509
+ continue;
510
+ }
511
+ if (t === PSBT_IN_SCRIPTSIG) {
512
+ if (p.key.length !== 1) {
513
+ throw new Error(
514
+ 'PSBT: final scriptSig key must be exactly one byte',
515
+ );
516
+ }
517
+ unknown.push(p);
518
+ continue;
519
+ }
520
+ if (t === PSBT_IN_REDEEM_SCRIPT) {
521
+ if (p.key.length !== 1) {
522
+ throw new Error(
523
+ 'PSBT: redeemScript key must be exactly one byte',
524
+ );
525
+ }
526
+ redeemScript = new Script(p.value);
527
+ continue;
528
+ }
529
+ // Bitcoin ABC PSBT_IN_UTXO: key is type byte only (see psbt.h).
530
+ if (t === PSBT_IN_UTXO && p.key.length !== 1) {
531
+ throw new Error('PSBT: input UTXO key must be exactly one byte');
532
+ }
533
+ if (t === PSBT_IN_UTXO && p.key.length === 1) {
534
+ pair00 = p;
535
+ } else if (t === PSBT_IN_PARTIAL_SIG) {
536
+ if (!isValidPartialSigKeyLength(p.key.length)) {
537
+ throw new Error(
538
+ 'PSBT: invalid partial signature pubkey key size',
539
+ );
540
+ }
541
+ partialSigs.set(pubkeyHex(p.key.slice(1)), p.value);
542
+ } else if (t === PSBT_IN_BIP32_DERIVATION) {
543
+ validateBip32DerivationKeyValue(p.key, p.value);
544
+ unknown.push(p);
545
+ } else {
546
+ unknown.push(p);
547
+ }
548
+ }
549
+
550
+ let witness: { sats: bigint; scriptPubKey: Uint8Array } | undefined;
551
+ if (pair00 !== undefined) {
552
+ const w = resolveWitnessFromKey00(pair00.value, prevOut);
553
+ if (w === undefined) {
554
+ throw new Error('PSBT input: invalid PSBT_IN_UTXO (0x00) value');
555
+ }
556
+ witness = w;
557
+ }
558
+
559
+ return { witness, redeemScript, partialSigs, unknown };
560
+ }
561
+
562
+ /**
563
+ * BIP 174 PSBT for eCash multisig and ABC-aligned decode/encode.
564
+ * See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#serialization
565
+ *
566
+ * Typical flow: {@link Psbt.fromTx} from a partially signed {@link Tx} plus
567
+ * {@link SignData} per input → {@link Psbt.toBytes} → share → {@link Psbt.fromBytes}.
568
+ * Unknown key-value pairs are preserved (BIP 174).
569
+ */
570
+ export class Psbt {
571
+ /** Unsigned transaction (empty scriptSigs). */
572
+ public readonly unsignedTx: Tx;
573
+ /** Per-input signing data (amount, scripts, partial sig maps). */
574
+ public readonly signDataPerInput: SignData[];
575
+ /** Per-input partial signatures: hex(pubkey) → signature incl. sighash byte. */
576
+ public readonly inputPartialSigs: Map<string, Uint8Array>[];
577
+ /**
578
+ * Unknown global key-value pairs (BIP 174: implementations must preserve
579
+ * unknown keys on round-trip).
580
+ */
581
+ public readonly unknownGlobalPairs: PsbtKeyValue[];
582
+ /** Unknown per-input pairs (excluding consumed `PSBT_IN_UTXO` / `0x00` entries). */
583
+ public readonly unknownInputPairs: PsbtKeyValue[][];
584
+ /** Unknown per-output pairs. */
585
+ public readonly unknownOutputPairs: PsbtKeyValue[][];
586
+ /**
587
+ * When true, this input had no `PSBT_IN_UTXO` (`0x00`) field in the PSBT (e.g.
588
+ * creator-only PSBT or finalized script fields only). {@link toBytes} will
589
+ * not emit that entry for the input.
590
+ */
591
+ public readonly inputWitnessIncomplete: boolean[];
592
+
593
+ public constructor(params: {
594
+ unsignedTx: Tx;
595
+ signDataPerInput: SignData[];
596
+ inputPartialSigs: Map<string, Uint8Array>[];
597
+ unknownGlobalPairs?: PsbtKeyValue[];
598
+ unknownInputPairs?: PsbtKeyValue[][];
599
+ unknownOutputPairs?: PsbtKeyValue[][];
600
+ inputWitnessIncomplete?: boolean[];
601
+ }) {
602
+ if (
603
+ params.signDataPerInput.length !== params.unsignedTx.inputs.length
604
+ ) {
605
+ throw new Error('signDataPerInput length must match inputs');
606
+ }
607
+ if (
608
+ params.inputPartialSigs.length !== params.unsignedTx.inputs.length
609
+ ) {
610
+ throw new Error('inputPartialSigs length must match inputs');
611
+ }
612
+ this.unsignedTx = params.unsignedTx;
613
+ this.signDataPerInput = params.signDataPerInput;
614
+ this.inputPartialSigs = params.inputPartialSigs;
615
+ this.unknownGlobalPairs = params.unknownGlobalPairs ?? [];
616
+ this.unknownInputPairs =
617
+ params.unknownInputPairs ?? params.unsignedTx.inputs.map(() => []);
618
+ this.unknownOutputPairs =
619
+ params.unknownOutputPairs ??
620
+ params.unsignedTx.outputs.map(() => []);
621
+ if (this.unknownInputPairs.length !== this.unsignedTx.inputs.length) {
622
+ throw new Error('unknownInputPairs length must match inputs');
623
+ }
624
+ if (this.unknownOutputPairs.length !== this.unsignedTx.outputs.length) {
625
+ throw new Error('unknownOutputPairs length must match outputs');
626
+ }
627
+ this.inputWitnessIncomplete =
628
+ params.inputWitnessIncomplete ??
629
+ params.unsignedTx.inputs.map(() => false);
630
+ if (
631
+ this.inputWitnessIncomplete.length !== this.unsignedTx.inputs.length
632
+ ) {
633
+ throw new Error('inputWitnessIncomplete length must match inputs');
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Build a PSBT from a transaction that may already include partial scriptSigs.
639
+ * Populates `PSBT_IN_UTXO` (`0x00`) + redeem script + partial signatures.
640
+ */
641
+ public static fromTx(tx: Tx, signDataPerInput: SignData[], ecc: Ecc): Psbt {
642
+ const unsignedTx = txToUnsigned(tx);
643
+ const inputPartialSigs: Map<string, Uint8Array>[] = [];
644
+ for (let i = 0; i < tx.inputs.length; i++) {
645
+ const script = tx.inputs[i]?.script ?? new Script();
646
+ const sigs = extractPartialSigsFromInput(
647
+ script,
648
+ signDataPerInput[i]!,
649
+ unsignedTx,
650
+ i,
651
+ ecc,
652
+ );
653
+ inputPartialSigs.push(sigs);
654
+ }
655
+ return new Psbt({
656
+ unsignedTx,
657
+ signDataPerInput,
658
+ inputPartialSigs,
659
+ unknownGlobalPairs: [],
660
+ unknownInputPairs: unsignedTx.inputs.map(() => []),
661
+ unknownOutputPairs: unsignedTx.outputs.map(() => []),
662
+ inputWitnessIncomplete: unsignedTx.inputs.map(() => false),
663
+ });
664
+ }
665
+
666
+ /** Deserialize PSBT bytes (BIP 174). */
667
+ public static fromBytes(data: Uint8Array): Psbt {
668
+ const bytes = new Bytes(data);
669
+ for (let i = 0; i < PSBT_MAGIC.length; i++) {
670
+ if (bytes.readU8() !== PSBT_MAGIC[i]) {
671
+ throw new Error('Invalid PSBT: bad magic');
672
+ }
673
+ }
674
+
675
+ const globalPairs = parseMap(bytes);
676
+ assertUniquePsbtKeys(globalPairs, 'global');
677
+ let unsignedRaw: Uint8Array | undefined;
678
+ const unknownGlobalPairs: PsbtKeyValue[] = [];
679
+ for (const { key, value } of globalPairs) {
680
+ if (key.length === 0) continue;
681
+ if (key[0] === PSBT_GLOBAL_UNSIGNED_TX && key.length === 1) {
682
+ unsignedRaw = value;
683
+ } else {
684
+ unknownGlobalPairs.push({ key, value });
685
+ }
686
+ }
687
+ if (unsignedRaw === undefined) {
688
+ throw new Error('PSBT missing global unsigned transaction');
689
+ }
690
+
691
+ const unsignedTx = Tx.deser(unsignedRaw);
692
+ assertUnsignedTxEmptyScriptSigs(unsignedTx);
693
+ const inputPartialSigs: Map<string, Uint8Array>[] = [];
694
+ const signDataPerInput: SignData[] = [];
695
+ const unknownInputPairs: PsbtKeyValue[][] = [];
696
+ const inputWitnessIncomplete: boolean[] = [];
697
+
698
+ for (let i = 0; i < unsignedTx.inputs.length; i++) {
699
+ const inPairs = parseMap(bytes);
700
+ assertUniquePsbtKeys(inPairs, `input ${i}`);
701
+ const prevOut = unsignedTx.inputs[i]!.prevOut;
702
+ const { witness, redeemScript, partialSigs, unknown } =
703
+ parseInputMapPairs(inPairs, prevOut);
704
+ unknownInputPairs.push(unknown);
705
+ const incomplete = witness === undefined;
706
+ inputWitnessIncomplete.push(incomplete);
707
+
708
+ let signData: SignData;
709
+ if (incomplete) {
710
+ signData =
711
+ redeemScript !== undefined
712
+ ? { sats: 0n, redeemScript }
713
+ : { sats: 0n, outputScript: new Script() };
714
+ } else if (redeemScript !== undefined) {
715
+ signData = {
716
+ sats: witness.sats,
717
+ redeemScript,
718
+ };
719
+ } else {
720
+ signData = {
721
+ sats: witness.sats,
722
+ outputScript: new Script(witness.scriptPubKey),
723
+ };
724
+ }
725
+ signDataPerInput.push(signData);
726
+ inputPartialSigs.push(partialSigs);
727
+ }
728
+
729
+ const unknownOutputPairs: PsbtKeyValue[][] = [];
730
+ for (let o = 0; o < unsignedTx.outputs.length; o++) {
731
+ const outPairs = parseMap(bytes);
732
+ assertUniquePsbtKeys(outPairs, `output ${o}`);
733
+ for (const p of outPairs) {
734
+ if (p.key.length === 0) continue;
735
+ const ot = p.key[0]!;
736
+ // PSBTOutput::Unserialize (Bitcoin ABC): redeem script key is type byte only.
737
+ if (ot === PSBT_OUT_REDEEMSCRIPT && p.key.length !== 1) {
738
+ throw new Error(
739
+ 'PSBT: output redeemScript key must be exactly one byte',
740
+ );
741
+ }
742
+ if (ot === PSBT_OUT_BIP32_DERIVATION) {
743
+ validateBip32DerivationKeyValue(p.key, p.value);
744
+ }
745
+ }
746
+ unknownOutputPairs.push(outPairs.filter(p => p.key.length > 0));
747
+ }
748
+
749
+ if (bytes.idx !== data.length) {
750
+ throw new Error('PSBT: trailing bytes after output maps');
751
+ }
752
+
753
+ return new Psbt({
754
+ unsignedTx,
755
+ signDataPerInput,
756
+ inputPartialSigs,
757
+ unknownGlobalPairs,
758
+ unknownInputPairs,
759
+ unknownOutputPairs,
760
+ inputWitnessIncomplete,
761
+ });
762
+ }
763
+
764
+ /** Serialize to BIP 174 bytes. */
765
+ public toBytes(): Uint8Array {
766
+ const unsignedSer = this.unsignedTx.ser();
767
+ const globalPairs: PsbtKeyValue[] = [
768
+ {
769
+ key: new Uint8Array([PSBT_GLOBAL_UNSIGNED_TX]),
770
+ value: unsignedSer,
771
+ },
772
+ ...this.unknownGlobalPairs,
773
+ ];
774
+
775
+ const parts: Uint8Array[] = [PSBT_MAGIC, serializeMap(globalPairs)];
776
+
777
+ for (let i = 0; i < this.unsignedTx.inputs.length; i++) {
778
+ const sd = this.signDataPerInput[i]!;
779
+ const inPairs: PsbtKeyValue[] = [];
780
+ if (!this.inputWitnessIncomplete[i]) {
781
+ const spk = scriptPubKeyFromSignData(sd);
782
+ inPairs.push({
783
+ key: new Uint8Array([PSBT_IN_UTXO]),
784
+ value: encodeWitnessUtxo(sd.sats, spk),
785
+ });
786
+ }
787
+ if (sd.redeemScript !== undefined) {
788
+ inPairs.push({
789
+ key: new Uint8Array([PSBT_IN_REDEEM_SCRIPT]),
790
+ value: sd.redeemScript.bytecode,
791
+ });
792
+ }
793
+ const pSig = this.inputPartialSigs[i]!;
794
+ for (const [pkHex, sig] of pSig) {
795
+ const pk = fromHex(pkHex);
796
+ const key = new Uint8Array(1 + pk.length);
797
+ key[0] = PSBT_IN_PARTIAL_SIG;
798
+ key.set(pk, 1);
799
+ inPairs.push({ key, value: sig });
800
+ }
801
+ inPairs.push(...(this.unknownInputPairs[i] ?? []));
802
+ parts.push(serializeMap(inPairs));
803
+ }
804
+
805
+ for (let o = 0; o < this.unsignedTx.outputs.length; o++) {
806
+ parts.push(serializeMap(this.unknownOutputPairs[o] ?? []));
807
+ }
808
+
809
+ const wl = new WriterLength();
810
+ for (const p of parts) {
811
+ wl.putBytes(p);
812
+ }
813
+ const w = new WriterBytes(wl.length);
814
+ for (const p of parts) {
815
+ w.putBytes(p);
816
+ }
817
+ return w.data;
818
+ }
819
+
820
+ /**
821
+ * Current transaction with scriptSigs built from partial signatures.
822
+ * Attach each input's `signData` for signing and validation helpers.
823
+ */
824
+ public toTx(): Tx {
825
+ return new Tx({
826
+ version: this.unsignedTx.version,
827
+ inputs: this.unsignedTx.inputs.map((inp, i) => {
828
+ const sd = this.signDataPerInput[i]!;
829
+ const script = buildScriptSigFromPartialSigs(
830
+ sd,
831
+ this.inputPartialSigs[i]!,
832
+ );
833
+ return {
834
+ ...copyTxInput(inp),
835
+ script,
836
+ signData: sd,
837
+ };
838
+ }),
839
+ outputs: this.unsignedTx.outputs.map(copyTxOutput),
840
+ locktime: this.unsignedTx.locktime,
841
+ });
842
+ }
843
+
844
+ /**
845
+ * Add or merge a multisig signature on an input (same semantics as
846
+ * {@link Tx.addMultisigSignature}).
847
+ */
848
+ public addMultisigSignature(params: {
849
+ inputIdx: number;
850
+ signature: Uint8Array;
851
+ signData: SignData;
852
+ ecc?: Ecc;
853
+ }): Psbt {
854
+ const tx = this.toTx();
855
+ const nextTx = tx.addMultisigSignature(params);
856
+ const next = Psbt.fromTx(
857
+ nextTx,
858
+ this.signDataPerInput,
859
+ params.ecc ?? new Ecc(),
860
+ );
861
+ return new Psbt({
862
+ unsignedTx: next.unsignedTx,
863
+ signDataPerInput: next.signDataPerInput,
864
+ inputPartialSigs: next.inputPartialSigs,
865
+ unknownGlobalPairs: this.unknownGlobalPairs,
866
+ unknownInputPairs: this.unknownInputPairs,
867
+ unknownOutputPairs: this.unknownOutputPairs,
868
+ inputWitnessIncomplete: this.inputWitnessIncomplete,
869
+ });
870
+ }
871
+
872
+ /**
873
+ * Like {@link addMultisigSignature}, but signs with a secret key (see
874
+ * {@link Tx.addMultisigSignatureFromKey}).
875
+ */
876
+ public addMultisigSignatureFromKey(params: {
877
+ inputIdx: number;
878
+ sk: Uint8Array;
879
+ signData: SignData;
880
+ sigHashType?: SigHashType;
881
+ ecc?: Ecc;
882
+ }): Psbt {
883
+ const tx = this.toTx();
884
+ const nextTx = tx.addMultisigSignatureFromKey(params);
885
+ const next = Psbt.fromTx(
886
+ nextTx,
887
+ this.signDataPerInput,
888
+ params.ecc ?? new Ecc(),
889
+ );
890
+ return new Psbt({
891
+ unsignedTx: next.unsignedTx,
892
+ signDataPerInput: next.signDataPerInput,
893
+ inputPartialSigs: next.inputPartialSigs,
894
+ unknownGlobalPairs: this.unknownGlobalPairs,
895
+ unknownInputPairs: this.unknownInputPairs,
896
+ unknownOutputPairs: this.unknownOutputPairs,
897
+ inputWitnessIncomplete: this.inputWitnessIncomplete,
898
+ });
899
+ }
900
+
901
+ /**
902
+ * Same as {@link Tx.isFullySignedMultisig} on {@link toTx} (including
903
+ * vacuous `true` when there are no multisig inputs).
904
+ */
905
+ public isFullySignedMultisig(): boolean {
906
+ return this.toTx().isFullySignedMultisig();
907
+ }
908
+ }