@scure/btc-signer 2.0.0 → 2.2.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/src/psbt.ts CHANGED
@@ -9,109 +9,300 @@ import {
9
9
  RawWitness,
10
10
  VarBytes,
11
11
  } from './script.ts';
12
- import { type Bytes, compareBytes, equalBytes, PubT, validatePubkey } from './utils.ts';
12
+ import {
13
+ type Bytes,
14
+ compareBytes,
15
+ equalBytes,
16
+ PubT,
17
+ type TArg,
18
+ type TRet,
19
+ validatePubkey,
20
+ } from './utils.ts';
13
21
 
14
22
  // PSBT BIP174, BIP370, BIP371
15
23
 
16
- // Can be 33 or 64 bytes
17
- const PubKeyECDSA: P.CoderType<Bytes> = P.validate(P.bytes(null), (pub) =>
18
- validatePubkey(pub, PubT.ecdsa)
19
- );
20
- const PubKeySchnorr: P.CoderType<Bytes> = P.validate(P.bytes(32), (pub) =>
21
- validatePubkey(pub, PubT.schnorr)
22
- );
23
- const SignatureSchnorr: P.CoderType<Bytes> = P.validate(P.bytes(null), (sig) => {
24
- if (sig.length !== 64 && sig.length !== 65)
25
- throw new Error('Schnorr signature should be 64 or 65 bytes long');
26
- return sig;
27
- });
24
+ // BIP174 keydata only says "public key", so legacy PSBT ECDSA fields still accept both
25
+ // compressed (33-byte) and uncompressed (65-byte) SEC1 encodings, but not x-only keys.
26
+ const PubKeyECDSA: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
27
+ P.validate(P.bytes(null), (pub) => validatePubkey(pub, PubT.ecdsa)))();
28
+ // BIP32 serialized xpub payloads specifically store `ser_P(K)`, which is always the 33-byte
29
+ // compressed SEC1 encoding of the public key rather than the looser legacy PSBT "any ECDSA pubkey".
30
+ const PubKeyECDSACompressed: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
31
+ P.validate(P.bytes(33), (pub) => validatePubkey(pub, PubT.ecdsa)))();
32
+ // BIP371 taproot PSBT key fields use 32-byte x-only pubkeys, so this coder keeps the
33
+ // fixed-length check and then reuses the shared Schnorr pubkey validator.
34
+ const PubKeySchnorr: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
35
+ P.validate(P.bytes(32), (pub) => validatePubkey(pub, PubT.schnorr)))();
36
+ // BIP371 taproot signature fields carry the 64-byte Schnorr signature, plus an optional
37
+ // trailing sighash byte when the signer used anything other than the default key-path mode.
38
+ const SignatureSchnorr: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
39
+ P.validate(P.bytes(null), (sig) => {
40
+ if (sig.length !== 64 && sig.length !== 65)
41
+ throw new Error('Schnorr signature should be 64 or 65 bytes long');
42
+ return sig;
43
+ }))();
44
+ // PSBTInput.finalScriptWitness should keep the historical decoded witness-stack shape even though
45
+ // the exported RawWitness coder now uses TRet for declaration stability.
46
+ const RawWitnessWire = RawWitness as unknown as P.CoderType<Bytes[]>;
28
47
 
29
- const BIP32Der = P.struct({
30
- fingerprint: P.U32BE,
31
- path: P.array(null, P.U32LE),
32
- });
33
- const TaprootBIP32Der = P.struct({
34
- hashes: P.array(CompactSizeLen, P.bytes(32)),
35
- der: BIP32Der,
36
- });
37
- // The 78 byte serialized extended public key as defined by BIP 32.
38
- const GlobalXPUB = P.bytes(78);
39
- const tapScriptSigKey = P.struct({ pubKey: PubKeySchnorr, leafHash: P.bytes(32) });
48
+ // BIP174 stores the 4-byte master fingerprint as-is, then appends each child index in
49
+ // 32-bit little-endian order; cross-field checks like xpub depth matching live above this.
50
+ const BIP32Der = /* @__PURE__ */ (() =>
51
+ P.struct({
52
+ fingerprint: P.U32BE,
53
+ path: P.array(null, P.U32LE),
54
+ }))();
55
+ // BIP371 prepends the shared BIP32 derivation payload with the tapleaf-hash list; internal keys
56
+ // use `hashes.length === 0`, while script-path keys list the leaves that actually use that pubkey.
57
+ const TaprootBIP32Der = /* @__PURE__ */ (() =>
58
+ P.struct({
59
+ hashes: P.array(CompactSizeLen, P.bytes(32)),
60
+ der: BIP32Der,
61
+ }))();
62
+ // BIP174 `PSBT_GLOBAL_XPUB` says the key is "The 78 byte serialized extended public key as
63
+ // defined by BIP 32", so decode it to the actual BIP32 field layout instead of preserving an
64
+ // opaque blob. We intentionally do not hardcode version-byte policy here because BIP32 version
65
+ // bytes vary by network / deployment; this layer just parses the field and enforces the BIP32
66
+ // import rules that are independent of network selection.
67
+ const GlobalXPUB = /* @__PURE__ */ (() =>
68
+ P.validate(
69
+ P.struct({
70
+ version: P.U32BE,
71
+ depth: P.U8,
72
+ parentFingerprint: P.U32BE,
73
+ childNumber: P.U32BE,
74
+ chainCode: P.bytes(32),
75
+ // BIP32 serialization stores the public key as the final 33-byte `ser_P(K)` field and says
76
+ // importing an extended public key must verify that point data corresponds to the curve.
77
+ publicKey: PubKeyECDSACompressed,
78
+ }),
79
+ (xpub) => {
80
+ // BIP32 serialization says master nodes use depth 0 with zero parent
81
+ // fingerprint and zero child number. The invalid examples explicitly
82
+ // include zero-depth xpubs with either field non-zero.
83
+ if (xpub.depth === 0 && xpub.parentFingerprint !== 0)
84
+ throw new Error('GlobalXPUB: depth=0 requires parentFingerprint=0');
85
+ if (xpub.depth === 0 && xpub.childNumber !== 0)
86
+ throw new Error('GlobalXPUB: depth=0 requires childNumber=0');
87
+ return xpub;
88
+ }
89
+ ))();
90
+ // BIP371 puts the x-only pubkey and leaf hash into the key side of `PSBT_IN_TAP_SCRIPT_SIG`;
91
+ // the actual 64/65-byte Schnorr signature stays in the value side under `SignatureSchnorr`.
92
+ const tapScriptSigKey = /* @__PURE__ */ (() =>
93
+ P.struct({ pubKey: PubKeySchnorr, leafHash: P.bytes(32) }))();
40
94
 
41
95
  // Complex structure for PSBT fields
42
96
  // <control byte with leaf version and parity bit> <internal key p> <C> <E> <AB>
43
- const _TaprootControlBlock = P.struct({
44
- version: P.U8, // With parity :(
45
- internalKey: P.bytes(32),
46
- merklePath: P.array(null, P.bytes(32)),
47
- });
48
- export const TaprootControlBlock = P.validate(_TaprootControlBlock, (cb) => {
49
- if (cb.merklePath.length > 128)
50
- throw new Error('TaprootControlBlock: merklePath should be of length 0..128 (inclusive)');
51
- return cb;
52
- });
97
+ // Raw BIP341 control-block layout only; the exported TaprootControlBlock wrapper adds the
98
+ // `0..128` Merkle-depth bound, and later taproot logic checks version/parity semantics.
99
+ const _TaprootControlBlock = /* @__PURE__ */ (() =>
100
+ P.struct({
101
+ version: P.U8, // With parity :(
102
+ internalKey: P.bytes(32),
103
+ merklePath: P.array(null, P.bytes(32)),
104
+ }))();
105
+ /**
106
+ * Taproot control block coder.
107
+ * @example
108
+ * Encode the Taproot control block attached to a script-path witness.
109
+ * ```ts
110
+ * import { TaprootControlBlock } from '@scure/btc-signer/psbt.js';
111
+ * import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
112
+ * TaprootControlBlock.encode({
113
+ * version: 0xc0,
114
+ * internalKey: pubSchnorr(randomPrivateKeyBytes()),
115
+ * merklePath: [],
116
+ * });
117
+ * ```
118
+ */
119
+ export const TaprootControlBlock = /* @__PURE__ */ (() =>
120
+ Object.freeze(
121
+ P.validate(_TaprootControlBlock, (cb) => {
122
+ // BIP 341 control blocks are raw 33+32m byte records; this PSBT coder only enforces
123
+ // the length/depth shape here and leaves curve / leaf-version validation to taproot logic.
124
+ if (cb.merklePath.length > 128)
125
+ throw new Error('TaprootControlBlock: merklePath should be of length 0..128 (inclusive)');
126
+ return cb;
127
+ })
128
+ ))();
53
129
 
130
+ // BIP371 says PSBT_OUT_TAP_TREE is one or more tuples in DFS order so the Taproot tree can be
131
+ // reconstructed. Validate both the non-empty requirement and that the leaf-depth sequence really
132
+ // describes a complete left-to-right DFS walk of a binary tree, not just arbitrary tuples.
54
133
  // {<8-bit uint depth> <8-bit uint leaf version> <compact size uint scriptlen> <bytes script>}*
55
- const tapTree = P.array(
56
- null,
57
- P.struct({
58
- depth: P.U8,
59
- version: P.U8,
60
- script: VarBytes,
61
- })
62
- );
134
+ const tapTree = /* @__PURE__ */ (() =>
135
+ P.validate(
136
+ P.array(
137
+ null,
138
+ P.struct({
139
+ depth: P.U8,
140
+ version: P.U8,
141
+ script: VarBytes,
142
+ })
143
+ ),
144
+ (tree) => {
145
+ if (tree.length < 1) throw new Error('tapTree: expected at least one tuple');
146
+ let path = Array(tree[0].depth).fill(0);
147
+ let maxDepth = tree[0].depth;
148
+ for (let i = 1; i < tree.length; i++) {
149
+ const { depth } = tree[i];
150
+ if (depth > maxDepth) maxDepth = depth;
151
+ let j = path.length - 1;
152
+ while (j >= 0 && path[j] === 1) j--;
153
+ if (j < 0) throw new Error('tapTree: tuples must be in DFS order');
154
+ const next = path.slice(0, j);
155
+ next.push(1);
156
+ if (depth < next.length) throw new Error('tapTree: tuples must be in DFS order');
157
+ while (next.length < depth) next.push(0);
158
+ path = next;
159
+ }
160
+ let leaves = 0n;
161
+ for (let i = 0; i < tree.length; i++) leaves += 1n << BigInt(maxDepth - tree[i].depth);
162
+ if (leaves !== 1n << BigInt(maxDepth))
163
+ throw new Error('tapTree: tuples must describe a complete binary tree');
164
+ return tree;
165
+ }
166
+ ))();
63
167
 
64
- const BytesInf: P.CoderType<Bytes> = P.bytes(null); // Bytes will conflict with Bytes type
65
- const Bytes20: P.CoderType<Bytes> = P.bytes(20);
66
- const Bytes32: P.CoderType<Bytes> = P.bytes(32);
168
+ // Shared raw PSBT byte payload coder for fields whose BIP174 value format is just opaque bytes;
169
+ // field-specific structure and length checks still live at the individual field definitions.
170
+ // Keep a distinct name here so the byte coder does not collide with the Bytes type alias.
171
+ const BytesInf: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(null);
172
+ // Shared 20-byte key-data helper for the BIP174 RIPEMD160 and HASH160 preimage maps.
173
+ const Bytes20: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(20);
174
+ // Shared 32-byte helper for fixed-size hash / txid / merkle-root byte fields; any stronger
175
+ // semantics such as x-only pubkey validity still need to be enforced by the field that uses it.
176
+ const Bytes32: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(32);
177
+ type PSBTKeyCoder = P.CoderType<any> | false;
178
+ type PSBTKeyMapInfo = Readonly<
179
+ [
180
+ number,
181
+ PSBTKeyCoder,
182
+ any,
183
+ readonly number[], // versionsRequiringInclusion
184
+ readonly number[], // versionsAllowsInclusion
185
+ boolean, // silentIgnore
186
+ ]
187
+ >;
188
+ // jsbt mutate checks exported PSBT tables recursively, so freeze each field tuple and its
189
+ // nested version arrays here while preserving the original coder slot types for local inference.
190
+ const PSBTInfo = <
191
+ K extends PSBTKeyCoder,
192
+ V,
193
+ Req extends readonly number[],
194
+ Allow extends readonly number[],
195
+ S extends boolean,
196
+ >(
197
+ type: number,
198
+ kc: K,
199
+ vc: V,
200
+ reqInc: Req,
201
+ allowInc: Allow,
202
+ silentIgnore: S
203
+ ) =>
204
+ /* @__PURE__ */ Object.freeze([
205
+ type,
206
+ kc && typeof kc === 'object' ? (Object.freeze(kc) as K) : kc,
207
+ vc && typeof vc === 'object' ? (Object.freeze(vc as object) as V) : vc,
208
+ Object.freeze([...reqInc]) as Req,
209
+ Object.freeze([...allowInc]) as Allow,
210
+ silentIgnore,
211
+ ] as const) as readonly [number, K, V, Req, Allow, S];
67
212
  // versionsRequiringExclusing = !versionsAllowsInclusion (as set)
68
- // {name: [tag, keyCoder, valueCoder, versionsRequiringInclusion, versionsRequiringExclusing, versionsAllowsInclusion, silentIgnore]}
69
- // SilentIgnore: we use some v2 fields for v1 representation too, so we just clean them before serialize
213
+ // {name: [tag, keyCoder, valueCoder, versionsRequiringInclusion,
214
+ // versionsRequiringExclusing, versionsAllowsInclusion, silentIgnore]}
215
+ // SilentIgnore: we use some v2 fields for v1 representation too,
216
+ // so we just clean them before serialize.
70
217
 
71
218
  // Tables from BIP-0174 (https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki)
72
219
  // prettier-ignore
73
- export const PSBTGlobal = {
74
- unsignedTx: [0x00, false, RawOldTx, [0], [0], false],
75
- xpub: [0x01, GlobalXPUB, BIP32Der, [], [0, 2], false],
76
- txVersion: [0x02, false, P.U32LE, [2], [2], false],
77
- fallbackLocktime: [0x03, false, P.U32LE, [], [2], false],
78
- inputCount: [0x04, false, CompactSizeLen, [2], [2], false],
79
- outputCount: [0x05, false, CompactSizeLen, [2], [2], false],
80
- txModifiable: [0x06, false, P.U8, [], [2], false], // TODO: bitfield
81
- version: [0xfb, false, P.U32LE, [], [0, 2], false],
82
- proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
83
- } as const;
220
+ /**
221
+ * PSBT global key definitions.
222
+ * @example
223
+ * Keep only the fields that are valid for the target PSBT version before serializing.
224
+ * ```ts
225
+ * import { PSBTGlobal, cleanPSBTFields } from '@scure/btc-signer/psbt.js';
226
+ * cleanPSBTFields(2, PSBTGlobal, { txVersion: 2, inputCount: 1, outputCount: 1 });
227
+ * ```
228
+ */
229
+ export const PSBTGlobal = /* @__PURE__ */ (() => Object.freeze({
230
+ unsignedTx: PSBTInfo(0x00, false, RawOldTx, [0], [0], false),
231
+ // BIP174 also requires the serialized xpub depth to match the number of path elements in the
232
+ // paired derivation value, so callers still need that cross-field check above this raw table.
233
+ xpub: PSBTInfo(0x01, GlobalXPUB, BIP32Der, [], [0, 2], false),
234
+ txVersion: PSBTInfo(0x02, false, P.U32LE, [2], [2], false),
235
+ fallbackLocktime: PSBTInfo(0x03, false, P.U32LE, [], [2], false),
236
+ inputCount: PSBTInfo(0x04, false, CompactSizeLen, [2], [2], false),
237
+ outputCount: PSBTInfo(0x05, false, CompactSizeLen, [2], [2], false),
238
+ // TODO: bitfield
239
+ txModifiable: PSBTInfo(0x06, false, P.U8, [], [2], false),
240
+ version: PSBTInfo(0xfb, false, P.U32LE, [], [0, 2], false),
241
+ proprietary: PSBTInfo(0xfc, BytesInf, BytesInf, [], [0, 2], false),
242
+ } as const))();
84
243
  // prettier-ignore
85
- export const PSBTInput = {
86
- nonWitnessUtxo: [0x00, false, RawTx, [], [0, 2], false],
87
- witnessUtxo: [0x01, false, RawOutput, [], [0, 2], false],
88
- partialSig: [0x02, PubKeyECDSA, BytesInf, [], [0, 2], false],
89
- sighashType: [0x03, false, P.U32LE, [], [0, 2], false],
90
- redeemScript: [0x04, false, BytesInf, [], [0, 2], false],
91
- witnessScript: [0x05, false, BytesInf, [], [0, 2], false],
92
- bip32Derivation: [0x06, PubKeyECDSA, BIP32Der, [], [0, 2], false],
93
- finalScriptSig: [0x07, false, BytesInf, [], [0, 2], false],
94
- finalScriptWitness: [0x08, false, RawWitness, [], [0, 2], false],
95
- porCommitment: [0x09, false, BytesInf, [], [0, 2], false],
96
- ripemd160: [0x0a, Bytes20, BytesInf, [], [0, 2], false],
97
- sha256: [0x0b, Bytes32, BytesInf, [], [0, 2], false],
98
- hash160: [0x0c, Bytes20, BytesInf, [], [0, 2], false],
99
- hash256: [0x0d, Bytes32, BytesInf, [], [0, 2], false],
100
- txid: [0x0e, false, Bytes32, [2], [2], true],
101
- index: [0x0f, false, P.U32LE, [2], [2], true],
102
- sequence: [0x10, false, P.U32LE, [], [2], true],
103
- requiredTimeLocktime: [0x11, false, P.U32LE, [], [2], false],
104
- requiredHeightLocktime: [0x12, false, P.U32LE, [], [2], false],
105
- tapKeySig: [0x13, false, SignatureSchnorr, [], [0, 2], false],
106
- tapScriptSig: [0x14, tapScriptSigKey, SignatureSchnorr, [], [0, 2], false],
107
- tapLeafScript: [0x15, TaprootControlBlock, BytesInf, [], [0, 2], false],
108
- tapBip32Derivation: [0x16, Bytes32, TaprootBIP32Der, [], [0, 2], false],
109
- tapInternalKey: [0x17, false, PubKeySchnorr, [], [0, 2], false],
110
- tapMerkleRoot: [0x18, false, Bytes32, [], [0, 2], false],
111
- proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
112
- } as const;
244
+ /**
245
+ * PSBT input key definitions.
246
+ * @example
247
+ * Strip input fields that do not belong in the requested PSBT version.
248
+ * ```ts
249
+ * import { hex } from '@scure/base';
250
+ * import { PSBTInput, cleanPSBTFields } from '@scure/btc-signer/psbt.js';
251
+ * cleanPSBTFields(2, PSBTInput, {
252
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
253
+ * index: 0,
254
+ * witnessUtxo: { amount: 2n, script: new Uint8Array([0x51]) },
255
+ * });
256
+ * ```
257
+ */
258
+ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
259
+ nonWitnessUtxo: PSBTInfo(0x00, false, RawTx, [], [0, 2], false),
260
+ witnessUtxo: PSBTInfo(0x01, false, RawOutput, [], [0, 2], false),
261
+ partialSig: PSBTInfo(0x02, PubKeyECDSA, BytesInf, [], [0, 2], false),
262
+ sighashType: PSBTInfo(0x03, false, P.U32LE, [], [0, 2], false),
263
+ redeemScript: PSBTInfo(0x04, false, BytesInf, [], [0, 2], false),
264
+ witnessScript: PSBTInfo(0x05, false, BytesInf, [], [0, 2], false),
265
+ bip32Derivation: PSBTInfo(0x06, PubKeyECDSA, BIP32Der, [], [0, 2], false),
266
+ finalScriptSig: PSBTInfo(0x07, false, BytesInf, [], [0, 2], false),
267
+ finalScriptWitness: PSBTInfo(0x08, false, RawWitnessWire, [], [0, 2], false),
268
+ porCommitment: PSBTInfo(0x09, false, BytesInf, [], [0, 2], false),
269
+ ripemd160: PSBTInfo(0x0a, Bytes20, BytesInf, [], [0, 2], false),
270
+ sha256: PSBTInfo(0x0b, Bytes32, BytesInf, [], [0, 2], false),
271
+ hash160: PSBTInfo(0x0c, Bytes20, BytesInf, [], [0, 2], false),
272
+ hash256: PSBTInfo(0x0d, Bytes32, BytesInf, [], [0, 2], false),
273
+ // BIP174/BIP370 serialize PREVIOUS_TXID in standard byte order, while the rest of this repo
274
+ // historically keeps TransactionInput.txid in display-order bytes matching `Transaction.id`.
275
+ // Reverse at this PSBTv2 boundary so internal txid semantics stay aligned with the raw-tx path.
276
+ txid: PSBTInfo(0x0e, false, P.bytes(32, true), [2], [2], true),
277
+ index: PSBTInfo(0x0f, false, P.U32LE, [2], [2], true),
278
+ sequence: PSBTInfo(0x10, false, P.U32LE, [], [2], true),
279
+ requiredTimeLocktime: PSBTInfo(0x11, false, P.U32LE, [], [2], false),
280
+ requiredHeightLocktime: PSBTInfo(0x12, false, P.U32LE, [], [2], false),
281
+ tapKeySig: PSBTInfo(0x13, false, SignatureSchnorr, [], [0, 2], false),
282
+ tapScriptSig: PSBTInfo(0x14, tapScriptSigKey, SignatureSchnorr, [], [0, 2], false),
283
+ tapLeafScript: PSBTInfo(0x15, TaprootControlBlock, BytesInf, [], [0, 2], false),
284
+ // BIP371 key data here is a 32-byte x-only pubkey, so reuse the shared Schnorr pubkey coder
285
+ // instead of accepting arbitrary 32-byte blobs that only fail much later in taproot flows.
286
+ tapBip32Derivation: PSBTInfo(0x16, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
287
+ tapInternalKey: PSBTInfo(0x17, false, PubKeySchnorr, [], [0, 2], false),
288
+ tapMerkleRoot: PSBTInfo(0x18, false, Bytes32, [], [0, 2], false),
289
+ proprietary: PSBTInfo(0xfc, BytesInf, BytesInf, [], [0, 2], false),
290
+ } as const))();
113
291
  // All other keys removed when finalizing
114
- export const PSBTInputFinalKeys: (keyof TransactionInput)[] = [
292
+ /**
293
+ * Input fields preserved after finalization.
294
+ * @example
295
+ * Use the allowlist when stripping transient signing fields after finalization.
296
+ * ```ts
297
+ * import { PSBTInputFinalKeys } from '@scure/btc-signer/psbt.js';
298
+ * const finalKeys = new Set(PSBTInputFinalKeys);
299
+ * finalKeys.has('finalScriptWitness');
300
+ * ```
301
+ */
302
+ export const PSBTInputFinalKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
303
+ // PSBTv2 extractors rebuild the final transaction from per-input fields, so
304
+ // finalized inputs still need txid/index (and any non-default sequence)
305
+ // even though BIP174's generic cleanup is stricter.
115
306
  'txid',
116
307
  'sequence',
117
308
  'index',
@@ -120,57 +311,88 @@ export const PSBTInputFinalKeys: (keyof TransactionInput)[] = [
120
311
  'finalScriptSig',
121
312
  'finalScriptWitness',
122
313
  'unknown',
123
- ];
314
+ ]);
124
315
 
125
316
  // Can be modified even on signed input
126
- export const PSBTInputUnsignedKeys: (keyof TransactionInput)[] = [
317
+ /**
318
+ * Input fields that may still change after signing starts.
319
+ * @example
320
+ * Signed inputs may still update these fields while new signatures are being added.
321
+ * ```ts
322
+ * import { PSBTInputUnsignedKeys } from '@scure/btc-signer/psbt.js';
323
+ * const mutableKeys = new Set(PSBTInputUnsignedKeys);
324
+ * mutableKeys.has('tapScriptSig');
325
+ * ```
326
+ */
327
+ export const PSBTInputUnsignedKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
328
+ // This is the replace/remove allowlist for signed inputs; mergeKeyMap() can still append
329
+ // previously absent metadata or new KV entries for other fields when they don't conflict.
127
330
  'partialSig',
128
331
  'finalScriptSig',
129
332
  'finalScriptWitness',
130
333
  'tapKeySig',
131
334
  'tapScriptSig',
132
- ];
335
+ ]);
133
336
 
134
337
  // prettier-ignore
135
- export const PSBTOutput = {
136
- redeemScript: [0x00, false, BytesInf, [], [0, 2], false],
137
- witnessScript: [0x01, false, BytesInf, [], [0, 2], false],
138
- bip32Derivation: [0x02, PubKeyECDSA, BIP32Der, [], [0, 2], false],
139
- amount: [0x03, false, P.I64LE, [2], [2], true],
140
- script: [0x04, false, BytesInf, [2], [2], true],
141
- tapInternalKey: [0x05, false, PubKeySchnorr, [], [0, 2], false],
142
- tapTree: [0x06, false, tapTree, [], [0, 2], false],
143
- tapBip32Derivation: [0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false],
144
- proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
145
- } as const;
338
+ /**
339
+ * PSBT output key definitions.
340
+ * @example
341
+ * Strip output fields that are not valid for the target PSBT version.
342
+ * ```ts
343
+ * import { PSBTOutput, cleanPSBTFields } from '@scure/btc-signer/psbt.js';
344
+ * cleanPSBTFields(2, PSBTOutput, { amount: 2n, script: new Uint8Array([0x51]) });
345
+ * ```
346
+ */
347
+ export const PSBTOutput = /* @__PURE__ */ (() => Object.freeze({
348
+ redeemScript: PSBTInfo(0x00, false, BytesInf, [], [0, 2], false),
349
+ witnessScript: PSBTInfo(0x01, false, BytesInf, [], [0, 2], false),
350
+ bip32Derivation: PSBTInfo(0x02, PubKeyECDSA, BIP32Der, [], [0, 2], false),
351
+ // BIP174/BIP370 serialize PSBT_OUT_AMOUNT as a signed int64 on the wire; semantic output
352
+ // validity still rejects negative transaction amounts in `PSBTOutputCoder` below.
353
+ amount: PSBTInfo(0x03, false, P.I64LE, [2], [2], true),
354
+ script: PSBTInfo(0x04, false, BytesInf, [2], [2], true),
355
+ tapInternalKey: PSBTInfo(0x05, false, PubKeySchnorr, [], [0, 2], false),
356
+ // BIP371 expects a non-empty DFS-ordered list of tapleaf tuples here so wallets can
357
+ // reconstruct the same Taproot tree, not just an arbitrary list of serialized leaves.
358
+ tapTree: PSBTInfo(0x06, false, tapTree, [], [0, 2], false),
359
+ tapBip32Derivation: PSBTInfo(0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
360
+ proprietary: PSBTInfo(0xfc, BytesInf, BytesInf, [], [0, 2], false),
361
+ } as const))();
146
362
 
147
363
  // Can be modified even on signed input
148
- export const PSBTOutputUnsignedKeys: (keyof typeof PSBTOutput)[] = [];
149
-
150
- const PSBTKeyPair = P.array(
151
- P.NULL,
152
- P.struct({
153
- // <key> := <keylen> <keytype> <keydata> WHERE keylen = len(keytype)+len(keydata)
154
- key: P.prefix(CompactSizeLen, P.struct({ type: CompactSizeLen, key: P.bytes(null) })),
155
- // <value> := <valuelen> <valuedata>
156
- value: P.bytes(CompactSizeLen),
157
- })
364
+ /**
365
+ * Output fields that may still change after signing starts.
366
+ * @example
367
+ * PSBTv2 outputs are fully committed once signing starts, so the set stays empty.
368
+ * ```ts
369
+ * import { PSBTOutputUnsignedKeys } from '@scure/btc-signer/psbt.js';
370
+ * const mutableKeys = new Set(PSBTOutputUnsignedKeys);
371
+ * mutableKeys.size; // 0
372
+ * ```
373
+ */
374
+ export const PSBTOutputUnsignedKeys = /* @__PURE__ */ Object.freeze<(keyof typeof PSBTOutput)[]>(
375
+ []
158
376
  );
377
+ // Signed outputs have no replace/remove exceptions: once a signature actually commits to a given
378
+ // output, every field on that output is frozen. SIGHASH_NONE leaves outputs fully mutable, and
379
+ // SIGHASH_SINGLE only freezes the matching output index.
159
380
 
160
- type PSBTKeyCoder = P.CoderType<any> | false;
161
-
162
- type PSBTKeyMapInfo = Readonly<
163
- [
164
- number,
165
- PSBTKeyCoder,
166
- any,
167
- readonly number[], // versionsRequiringInclusion
168
- readonly number[], // versionsAllowsInclusion
169
- boolean, // silentIgnore
170
- ]
171
- >;
381
+ // Raw BIP174 keypair framing only: `<key><value>` records terminated by `0x00`.
382
+ // Uniqueness, keyed-vs-unkeyed rules, and per-type decoding live one layer up in `PSBTKeyMap`.
383
+ const PSBTKeyPair = /* @__PURE__ */ (() =>
384
+ P.array(
385
+ P.NULL,
386
+ P.struct({
387
+ // <key> := <keylen> <keytype> <keydata> WHERE keylen = len(keytype)+len(keydata)
388
+ key: P.prefix(CompactSizeLen, P.struct({ type: CompactSizeLen, key: P.bytes(null) })),
389
+ // <value> := <valuelen> <valuedata>
390
+ value: P.bytes(CompactSizeLen),
391
+ })
392
+ ))();
172
393
 
173
394
  function PSBTKeyInfo(info: PSBTKeyMapInfo) {
395
+ // Name the tuple slots once so version-filter helpers do not depend on raw positional indexing.
174
396
  const [type, kc, vc, reqInc, allowInc, silentIgnore] = info;
175
397
  return { type, kc, vc, reqInc, allowInc, silentIgnore };
176
398
  }
@@ -182,15 +404,18 @@ const PSBTUnknownKey: P.CoderType<
182
404
  type: number;
183
405
  key: Bytes;
184
406
  }>
185
- > = P.struct({ type: CompactSizeLen, key: P.bytes(null) });
407
+ > = /* @__PURE__ */ (() =>
408
+ // Raw unknown/proprietary field key: compact-size keytype plus opaque keydata for pass-through.
409
+ P.struct({ type: CompactSizeLen, key: P.bytes(null) }))();
186
410
  type PSBTUnknownFields = { unknown?: [P.UnwrapCoder<typeof PSBTUnknownKey>, Bytes][] };
411
+ /** Maps a PSBT key-definition table to the decoded key-map shape. */
187
412
  export type PSBTKeyMapKeys<T extends PSBTKeyMap> = {
188
413
  -readonly [K in keyof T]?: T[K][1] extends false
189
414
  ? P.UnwrapCoder<T[K][2]>
190
415
  : [P.UnwrapCoder<T[K][1]>, P.UnwrapCoder<T[K][2]>][];
191
416
  } & PSBTUnknownFields;
192
417
  // Key cannot be 'unknown', value coder cannot be array for elements with empty key
193
- function PSBTKeyMap<T extends PSBTKeyMap>(psbtEnum: T): P.CoderType<PSBTKeyMapKeys<T>> {
418
+ function PSBTKeyMap<T extends PSBTKeyMap>(psbtEnum: T): TRet<P.CoderType<PSBTKeyMapKeys<T>>> {
194
419
  // -> Record<type, [keyName, ...coders]>
195
420
  const byType: Record<number, [string, PSBTKeyCoder, P.CoderType<any>]> = {};
196
421
  for (const k in psbtEnum) {
@@ -198,17 +423,30 @@ function PSBTKeyMap<T extends PSBTKeyMap>(psbtEnum: T): P.CoderType<PSBTKeyMapKe
198
423
  byType[num] = [k, kc, vc];
199
424
  }
200
425
  return P.wrap({
201
- encodeStream: (w: P.Writer, value: PSBTKeyMapKeys<T>) => {
426
+ encodeStream: (w: P.Writer, value: TArg<PSBTKeyMapKeys<T>>) => {
427
+ const _value = value as PSBTKeyMapKeys<T>;
202
428
  let out: P.UnwrapCoder<typeof PSBTKeyPair> = [];
429
+ const seen: Record<string, true> = {};
430
+ const add = (key: P.UnwrapCoder<typeof PSBTUnknownKey>, value: TArg<Bytes>) => {
431
+ const _value = value as Bytes;
432
+ // BIP174 defines `<key> := <keylen> <keytype> <keydata>` and says repeated `<keytype>`
433
+ // entries are allowed within one `<map>` as long as the full `<key>` stays unique.
434
+ // `<keylen>` is derived from `<keytype><keydata>`, so `PSBTUnknownKey` is enough here.
435
+ const kStr = hex.encode(PSBTUnknownKey.encode(key));
436
+ if (seen[kStr]) throw new Error(`PSBT: duplicate key=${kStr}`);
437
+ seen[kStr] = true;
438
+ out.push({ key, value: _value });
439
+ };
203
440
  // Because we use order of psbtEnum, keymap is sorted here
204
441
  for (const name in psbtEnum) {
205
- const val = value[name];
442
+ const val = _value[name];
206
443
  if (val === undefined) continue;
207
444
  const [type, kc, vc] = psbtEnum[name];
208
445
  if (!kc) {
209
- out.push({ key: { type, key: P.EMPTY }, value: vc.encode(val) });
446
+ add({ type, key: P.EMPTY }, vc.encode(val));
210
447
  } else {
211
- // Low level interface, returns keys as is (with duplicates). Useful for debug
448
+ // BIP174 allows repeated `<keytype>` values inside one `<map>`, but the full `<key>`
449
+ // must stay unique, so keyed rows are sorted and then deduped by serialized key bytes.
212
450
  const kv: [Bytes, Bytes][] = val!.map(
213
451
  ([k, v]: [P.UnwrapCoder<typeof kc>, P.UnwrapCoder<typeof vc>]) => [
214
452
  kc.encode(k),
@@ -217,20 +455,24 @@ function PSBTKeyMap<T extends PSBTKeyMap>(psbtEnum: T): P.CoderType<PSBTKeyMapKe
217
455
  );
218
456
  // sort by keys
219
457
  kv.sort((a, b) => compareBytes(a[0], b[0]));
220
- for (const [key, value] of kv) out.push({ key: { key, type }, value });
458
+ for (const [key, value] of kv) add({ key, type }, value);
221
459
  }
222
460
  }
223
- if (value.unknown) {
224
- value.unknown.sort((a, b) => compareBytes(a[0].key, b[0].key));
225
- for (const [k, v] of value.unknown) out.push({ key: k, value: v });
461
+ if (_value.unknown) {
462
+ _value.unknown.sort((a, b) => compareBytes(a[0].key, b[0].key));
463
+ for (const [k, v] of _value.unknown) add(k, v);
226
464
  }
227
465
  PSBTKeyPair.encodeStream(w, out);
228
466
  },
229
- decodeStream: (r: P.Reader): PSBTKeyMapKeys<T> => {
467
+ decodeStream: (r: P.Reader): TRet<PSBTKeyMapKeys<T>> => {
230
468
  const raw = PSBTKeyPair.decodeStream(r);
231
469
  const out: any = {};
232
470
  const noKey: Record<string, true> = {};
471
+ const seen: Record<string, true> = {};
233
472
  for (const elm of raw) {
473
+ const kStr = hex.encode(PSBTUnknownKey.encode(elm.key));
474
+ if (seen[kStr]) throw new Error(`PSBT: duplicate key=${kStr}`);
475
+ seen[kStr] = true;
234
476
  let name = 'unknown';
235
477
  let key: any = elm.key.key;
236
478
  let value = elm.value;
@@ -254,117 +496,201 @@ function PSBTKeyMap<T extends PSBTKeyMap>(psbtEnum: T): P.CoderType<PSBTKeyMapKe
254
496
  // For unknown: add key type inside key
255
497
  key = { type: elm.key.type, key: elm.key.key };
256
498
  }
257
- // Only keyed elements at this point
499
+ // Only keyed elements at this point.
500
+ // BIP174 uniqueness is over the full serialized `<key>` bytes within one map, not only keytype.
501
+ // Empty-key rows are rejected above; keyed duplicates need an explicit check before this append path.
258
502
  if (noKey[name])
259
503
  throw new Error(`PSBT: Key type with empty key and no key=${name} val=${value}`);
260
504
  if (!out[name]) out[name] = [];
261
505
  out[name].push([key, value]);
262
506
  }
263
- return out;
507
+ return out as TRet<PSBTKeyMapKeys<T>>;
264
508
  },
265
- });
509
+ }) as TRet<P.CoderType<PSBTKeyMapKeys<T>>>;
266
510
  }
267
511
 
268
- export const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
269
- if (i.finalScriptWitness && !i.finalScriptWitness.length)
270
- throw new Error('validateInput: empty finalScriptWitness');
271
- //if (i.finalScriptSig && !i.finalScriptSig.length) throw new Error('validateInput: empty finalScriptSig');
272
- if (i.partialSig && !i.partialSig.length) throw new Error('Empty partialSig');
273
- if (i.partialSig) for (const [k] of i.partialSig) validatePubkey(k, PubT.ecdsa);
274
- if (i.bip32Derivation) for (const [k] of i.bip32Derivation) validatePubkey(k, PubT.ecdsa);
275
- // Locktime = unsigned little endian integer greater than or equal to 500000000 representing
276
- if (i.requiredTimeLocktime !== undefined && i.requiredTimeLocktime < 500000000)
277
- throw new Error(`validateInput: wrong timeLocktime=${i.requiredTimeLocktime}`);
278
- // unsigned little endian integer greater than 0 and less than 500000000
279
- if (
280
- i.requiredHeightLocktime !== undefined &&
281
- (i.requiredHeightLocktime <= 0 || i.requiredHeightLocktime >= 500000000)
282
- )
283
- throw new Error(`validateInput: wrong heighLocktime=${i.requiredHeightLocktime}`);
284
- if (i.tapLeafScript) {
285
- // tap leaf version appears here twice: in control block and at the end of script
286
- for (const [k, v] of i.tapLeafScript) {
287
- if ((k.version & 0b1111_1110) !== v[v.length - 1])
288
- throw new Error('validateInput: tapLeafScript version mimatch');
289
- if (v[v.length - 1] & 1)
290
- throw new Error('validateInput: tapLeafScript version has parity bit!');
291
- }
292
- }
293
- return i;
294
- });
512
+ /**
513
+ * Validated PSBT input coder.
514
+ * @example
515
+ * Validate a decoded PSBT input before serializing it back to bytes.
516
+ * ```ts
517
+ * import { hex } from '@scure/base';
518
+ * import { PSBTInputCoder } from '@scure/btc-signer/psbt.js';
519
+ * PSBTInputCoder.encode({
520
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
521
+ * index: 0,
522
+ * witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
523
+ * });
524
+ * ```
525
+ */
526
+ export const PSBTInputCoder = /* @__PURE__ */ (() =>
527
+ Object.freeze(
528
+ P.validate(PSBTKeyMap(PSBTInput), (i) => {
529
+ // This wrapper adds input-level invariants after raw PSBT key-map decoding.
530
+ // Row-level key validation and duplicate-key rejection still depend on the underlying table/map helpers.
531
+ if (i.finalScriptWitness && !i.finalScriptWitness.length)
532
+ throw new Error('validateInput: empty finalScriptWitness');
533
+ //if (i.finalScriptSig && !i.finalScriptSig.length) throw new Error('validateInput: empty finalScriptSig');
534
+ if (i.partialSig && !i.partialSig.length) throw new Error('Empty partialSig');
535
+ if (i.partialSig) for (const [k] of i.partialSig) validatePubkey(k, PubT.ecdsa);
536
+ if (i.bip32Derivation) for (const [k] of i.bip32Derivation) validatePubkey(k, PubT.ecdsa);
537
+ // Locktime = unsigned little endian integer greater than or equal to 500000000 representing
538
+ if (i.requiredTimeLocktime !== undefined && i.requiredTimeLocktime < 500000000)
539
+ throw new Error(`validateInput: wrong timeLocktime=${i.requiredTimeLocktime}`);
540
+ // unsigned little endian integer greater than 0 and less than 500000000
541
+ if (
542
+ i.requiredHeightLocktime !== undefined &&
543
+ (i.requiredHeightLocktime <= 0 || i.requiredHeightLocktime >= 500000000)
544
+ )
545
+ throw new Error(`validateInput: wrong heighLocktime=${i.requiredHeightLocktime}`);
546
+ if (i.tapLeafScript) {
547
+ // tap leaf version appears here twice: in control block and at the end of script
548
+ for (const [k, v] of i.tapLeafScript) {
549
+ if ((k.version & 0b1111_1110) !== v[v.length - 1])
550
+ throw new Error('validateInput: tapLeafScript version mimatch');
551
+ if (v[v.length - 1] & 1)
552
+ throw new Error('validateInput: tapLeafScript version has parity bit!');
553
+ }
554
+ }
555
+ return i;
556
+ })
557
+ ))();
295
558
 
559
+ /** Replaces selected keys in `T` with widened update shapes from `E`. */
296
560
  export type ExtendType<T, E> = {
297
561
  [K in keyof T]: K extends keyof E ? E[K] | T[K] : T[K];
298
562
  };
563
+ /** Marks selected keys in `T` as required. */
299
564
  export type RequireType<T, K extends keyof T> = T & {
300
565
  [P in K]-?: T[P];
301
566
  };
302
567
 
303
- export type TransactionInput = P.UnwrapCoder<typeof PSBTInputCoder>;
568
+ /** Fully decoded PSBT input. */
569
+ export type TransactionInput = PSBTKeyMapKeys<typeof PSBTInput>;
570
+ /** PSBT input update shape accepted by mutation helpers. */
304
571
  export type TransactionInputUpdate = ExtendType<
305
- TransactionInput,
572
+ PSBTKeyMapKeys<typeof PSBTInput>,
306
573
  {
307
574
  nonWitnessUtxo?: string | Bytes;
308
575
  txid?: string;
309
576
  }
310
577
  >;
311
578
 
312
- export const PSBTOutputCoder = P.validate(PSBTKeyMap(PSBTOutput), (o) => {
313
- if (o.bip32Derivation) for (const [k] of o.bip32Derivation) validatePubkey(k, PubT.ecdsa);
314
- return o;
315
- });
579
+ /**
580
+ * Validated PSBT output coder.
581
+ * @example
582
+ * Validate a decoded PSBT output before serializing it back to bytes.
583
+ * ```ts
584
+ * import { PSBTOutputCoder } from '@scure/btc-signer/psbt.js';
585
+ * PSBTOutputCoder.encode({ amount: 1n, script: new Uint8Array([0x51]) });
586
+ * ```
587
+ */
588
+ export const PSBTOutputCoder = /* @__PURE__ */ (() =>
589
+ Object.freeze(
590
+ P.validate(PSBTKeyMap(PSBTOutput), (o) => {
591
+ // This wrapper only adds output-level invariants after raw key-map decoding.
592
+ // Duplicate-key rejection still depends on the key-map helper; tapTree structure is validated
593
+ // in the field coder itself because BIP371 constrains the tuple value, not just the row shape.
594
+ // BIP174/BIP370 define PSBT_OUT_AMOUNT as a signed int64 transport field, but it still
595
+ // represents the transaction output amount in satoshis, so negative output values are invalid.
596
+ if (o.amount !== undefined && o.amount < 0n)
597
+ throw new Error(`validateOutput: wrong amount=${o.amount}`);
598
+ if (o.bip32Derivation) for (const [k] of o.bip32Derivation) validatePubkey(k, PubT.ecdsa);
599
+ return o;
600
+ })
601
+ ))();
316
602
 
317
- export type TransactionOutput = P.UnwrapCoder<typeof PSBTOutputCoder>;
318
- export type TransactionOutputUpdate = ExtendType<TransactionOutput, { script?: string }>;
603
+ /** Fully decoded PSBT output. */
604
+ export type TransactionOutput = PSBTKeyMapKeys<typeof PSBTOutput>;
605
+ /** PSBT output update shape accepted by mutation helpers. */
606
+ export type TransactionOutputUpdate = ExtendType<
607
+ PSBTKeyMapKeys<typeof PSBTOutput>,
608
+ { script?: string }
609
+ >;
610
+ /** Transaction output fields required for serialization. */
319
611
  export type TransactionOutputRequired = {
612
+ /** Serialized scriptPubKey bytes. */
320
613
  script: Bytes;
614
+ /** Output value in satoshis. */
321
615
  amount: bigint;
322
616
  };
323
617
 
324
- const PSBTGlobalCoder = P.validate(PSBTKeyMap(PSBTGlobal), (g) => {
325
- const version = g.version || 0;
326
- if (version === 0) {
327
- if (!g.unsignedTx) throw new Error('PSBTv0: missing unsignedTx');
328
- for (const inp of g.unsignedTx.inputs)
329
- if (inp.finalScriptSig && inp.finalScriptSig.length)
330
- throw new Error('PSBTv0: input scriptSig found in unsignedTx');
331
- }
332
- return g;
333
- });
618
+ const PSBTGlobalCoder = /* @__PURE__ */ (() =>
619
+ P.validate(PSBTKeyMap(PSBTGlobal), (g) => {
620
+ // This wrapper adds the BIP174/BIP370 cross-field invariants after raw global key-map decoding.
621
+ // `PSBT_GLOBAL_XPUB` stores the serialized xpub plus a separate derivation value, and BIP174
622
+ // says the number of 32-bit indexes in that derivation path must match the xpub depth.
623
+ const version = g.version || 0;
624
+ if (version === 0) {
625
+ if (!g.unsignedTx) throw new Error('PSBTv0: missing unsignedTx');
626
+ for (const inp of g.unsignedTx.inputs)
627
+ if (inp.finalScriptSig && inp.finalScriptSig.length)
628
+ throw new Error('PSBTv0: input scriptSig found in unsignedTx');
629
+ }
630
+ for (const [xpub, der] of g.xpub || []) {
631
+ if (xpub.depth !== der.path.length)
632
+ throw new Error(
633
+ `PSBT_GLOBAL_XPUB: xpub depth=${xpub.depth} must match derivation path length=${der.path.length}`
634
+ );
635
+ }
636
+ return g;
637
+ }))();
334
638
 
335
- export const _RawPSBTV0 = P.struct({
336
- magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
337
- global: PSBTGlobalCoder,
338
- inputs: P.array('global/unsignedTx/inputs/length', PSBTInputCoder),
339
- outputs: P.array(null, PSBTOutputCoder),
340
- });
639
+ export const _RawPSBTV0 = /* @__PURE__ */ (() =>
640
+ Object.freeze(
641
+ P.struct({
642
+ magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
643
+ global: PSBTGlobalCoder,
644
+ // Raw v0 framing follows the unsigned transaction for input-map count; the stricter
645
+ // one-map-per-input/output reconciliation happens in `RawPSBTV0` / `validatePSBT`.
646
+ inputs: P.array('global/unsignedTx/inputs/length', PSBTInputCoder),
647
+ outputs: P.array(null, PSBTOutputCoder),
648
+ })
649
+ ))();
341
650
 
342
- export const _RawPSBTV2 = P.struct({
343
- magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
344
- global: PSBTGlobalCoder,
345
- inputs: P.array('global/inputCount', PSBTInputCoder),
346
- outputs: P.array('global/outputCount', PSBTOutputCoder),
347
- });
651
+ export const _RawPSBTV2 = /* @__PURE__ */ (() =>
652
+ Object.freeze(
653
+ P.struct({
654
+ magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
655
+ global: PSBTGlobalCoder,
656
+ // Raw v2 framing takes map counts from the global PSBTv2 count fields; deeper version
657
+ // and per-field validation still happens in `RawPSBTV2` / `validatePSBT`.
658
+ inputs: P.array('global/inputCount', PSBTInputCoder),
659
+ outputs: P.array('global/outputCount', PSBTOutputCoder),
660
+ })
661
+ ))();
348
662
 
663
+ /** Raw PSBT coder type. */
349
664
  export type PSBTRaw = typeof _RawPSBTV0 | typeof _RawPSBTV2;
350
665
 
351
- export const _DebugPSBT = P.struct({
352
- magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
353
- items: P.array(
354
- null,
355
- P.apply(
356
- P.array(P.NULL, P.tuple([P.hex(CompactSizeLen), P.bytes(CompactSize)])),
357
- P.coders.dict()
358
- )
359
- ),
360
- });
666
+ export const _DebugPSBT = /* @__PURE__ */ (() =>
667
+ Object.freeze(
668
+ P.struct({
669
+ magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
670
+ // Debug-only normalized view: maps become plain objects, so key order is intentionally ignored
671
+ // and duplicate keys fail while decoding instead of being preserved for byte-level diagnostics.
672
+ // Each `items[i]` is one raw PSBT map (`global`, then inputs, then outputs), keyed by the
673
+ // full serialized PSBT key bytes as hex rather than decoded field names.
674
+ items: P.array(
675
+ null,
676
+ P.apply(
677
+ P.array(P.NULL, P.tuple([P.hex(CompactSizeLen), P.bytes(CompactSize)])),
678
+ P.coders.dict()
679
+ )
680
+ ),
681
+ })
682
+ ))();
361
683
 
362
684
  function validatePSBTFields<T extends PSBTKeyMap>(
363
685
  version: number,
364
686
  info: T,
365
- lst: PSBTKeyMapKeys<T>
687
+ lst: TArg<PSBTKeyMapKeys<T>>
366
688
  ) {
367
- for (const k in lst) {
689
+ const _lst = lst as PSBTKeyMapKeys<T>;
690
+ // Enforce the BIP174/BIP370 field-table columns directly: reject rows whose
691
+ // "Versions Allowing Inclusion" excludes this version and require rows whose
692
+ // "Versions Requiring Inclusion" includes it.
693
+ for (const k in _lst) {
368
694
  if (k === 'unknown') continue;
369
695
  if (!info[k]) continue;
370
696
  const { allowInc } = PSBTKeyInfo(info[k]);
@@ -372,19 +698,40 @@ function validatePSBTFields<T extends PSBTKeyMap>(
372
698
  }
373
699
  for (const k in info) {
374
700
  const { reqInc } = PSBTKeyInfo(info[k]);
375
- if (reqInc.includes(version) && lst[k] === undefined)
701
+ if (reqInc.includes(version) && _lst[k] === undefined)
376
702
  throw new Error(`PSBTv${version}: missing required field ${k}`);
377
703
  }
378
704
  }
379
705
 
706
+ /**
707
+ * Removes fields that are not valid for the requested PSBT version.
708
+ * @param version - target PSBT version
709
+ * @param info - PSBT field definition table
710
+ * @param lst - decoded PSBT key map
711
+ * @returns Filtered PSBT key map.
712
+ * @throws If a field cannot be serialized in the requested PSBT version. {@link Error}
713
+ * @example
714
+ * Drop fields that are not allowed in the target PSBT version before encoding.
715
+ * ```ts
716
+ * import { hex } from '@scure/base';
717
+ * import { PSBTInput, cleanPSBTFields } from '@scure/btc-signer/psbt.js';
718
+ * cleanPSBTFields(2, PSBTInput, {
719
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
720
+ * index: 0,
721
+ * });
722
+ * ```
723
+ */
380
724
  export function cleanPSBTFields<T extends PSBTKeyMap>(
381
725
  version: number,
382
726
  info: T,
383
- lst: PSBTKeyMapKeys<T>
384
- ): PSBTKeyMapKeys<T> {
727
+ lst: TArg<PSBTKeyMapKeys<T>>
728
+ ): TRet<PSBTKeyMapKeys<T>> {
729
+ const _lst = lst as PSBTKeyMapKeys<T>;
385
730
  const out: PSBTKeyMapKeys<T> = {};
386
- for (const _k in lst) {
731
+ for (const _k in _lst) {
387
732
  const k = _k as string & keyof PSBTKeyMapKeys<T>;
733
+ // Serializer-side compatibility filter: preserve unknown pass-through fields, silently drop
734
+ // rows explicitly marked `silentIgnore`, and throw on other rows the target version forbids.
388
735
  if (k !== 'unknown') {
389
736
  if (!info[k]) continue;
390
737
  const { allowInc, silentIgnore } = PSBTKeyInfo(info[k]);
@@ -395,9 +742,9 @@ export function cleanPSBTFields<T extends PSBTKeyMap>(
395
742
  );
396
743
  }
397
744
  }
398
- out[k] = lst[k];
745
+ out[k] = _lst[k];
399
746
  }
400
- return out;
747
+ return out as TRet<PSBTKeyMapKeys<T>>;
401
748
  }
402
749
 
403
750
  function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
@@ -405,13 +752,18 @@ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
405
752
  validatePSBTFields(version, PSBTGlobal, tx.global);
406
753
  for (const i of tx.inputs) validatePSBTFields(version, PSBTInput, i);
407
754
  for (const o of tx.outputs) validatePSBTFields(version, PSBTOutput, o);
408
- // We allow only one empty element at the end of map (compat with bitcoinjs-lib bug)
755
+ // BIP174 defines `<psbt> := <magic> <global-map> <input-map>* <output-map>*`, so after decode the
756
+ // number of input/output maps should match the unsigned tx. PSBTv2 makes the same shape explicit
757
+ // through `inputCount` / `outputCount`. We intentionally violate that strict reading for one case:
758
+ // keep accepting exactly one trailing empty map because the separate bitcoinjs compatibility PSBT
759
+ // fixture corpus still contains that encoding. Anything non-empty or more than one extra map is
760
+ // still rejected here.
409
761
  const inputCount = !version ? tx.global.unsignedTx!.inputs.length : tx.global.inputCount!;
410
762
  if (tx.inputs.length < inputCount) throw new Error('Not enough inputs');
411
763
  const inputsLeft = tx.inputs.slice(inputCount);
412
764
  if (inputsLeft.length > 1 || (inputsLeft.length && Object.keys(inputsLeft[0]).length))
413
765
  throw new Error(`Unexpected inputs left in tx=${inputsLeft}`);
414
- // Same for inputs
766
+ // Same carve-out for outputs.
415
767
  const outputCount = !version ? tx.global.unsignedTx!.outputs.length : tx.global.outputCount!;
416
768
  if (tx.outputs.length < outputCount) throw new Error('Not outputs inputs');
417
769
  const outputsLeft = tx.outputs.slice(outputCount);
@@ -420,26 +772,53 @@ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
420
772
  return tx;
421
773
  }
422
774
 
775
+ /**
776
+ * Merges two PSBT key maps while preserving keyed-field uniqueness.
777
+ * @param psbtEnum - PSBT field definition table
778
+ * @param val - new values to merge in
779
+ * @param cur - existing decoded PSBT key map
780
+ * @param allowedFields - fields still allowed to change
781
+ * @param allowUnknown - whether to preserve unknown PSBT fields
782
+ * @returns Merged PSBT key map.
783
+ * @throws If keyed PSBT fields conflict or signed fields would be removed. {@link Error}
784
+ * @example
785
+ * Merge an updated `witnessUtxo` into an existing decoded input map.
786
+ * ```ts
787
+ * import { hex } from '@scure/base';
788
+ * import { PSBTInput, mergeKeyMap } from '@scure/btc-signer/psbt.js';
789
+ * mergeKeyMap(
790
+ * PSBTInput,
791
+ * { witnessUtxo: { amount: 2n, script: new Uint8Array([0x51]) } },
792
+ * {
793
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
794
+ * index: 0,
795
+ * }
796
+ * );
797
+ * ```
798
+ */
423
799
  export function mergeKeyMap<T extends PSBTKeyMap>(
424
800
  psbtEnum: T,
425
- val: PSBTKeyMapKeys<T>,
426
- cur?: PSBTKeyMapKeys<T>,
427
- allowedFields?: (keyof PSBTKeyMapKeys<T>)[],
801
+ val: TArg<PSBTKeyMapKeys<T>>,
802
+ cur?: TArg<PSBTKeyMapKeys<T>>,
803
+ allowedFields?: TArg<readonly (keyof PSBTKeyMapKeys<T>)[]>,
428
804
  allowUnknown?: boolean
429
- ): PSBTKeyMapKeys<T> {
430
- const res: PSBTKeyMapKeys<T> = { ...cur, ...val };
805
+ ): TRet<PSBTKeyMapKeys<T>> {
806
+ const _val = val as PSBTKeyMapKeys<T>;
807
+ const _cur = cur as PSBTKeyMapKeys<T> | undefined;
808
+ const _allowedFields = allowedFields as readonly (keyof PSBTKeyMapKeys<T>)[] | undefined;
809
+ const res: PSBTKeyMapKeys<T> = { ..._cur, ..._val };
431
810
  // All arguments can be provided as hex
432
811
  for (const k in psbtEnum) {
433
812
  const key = k as keyof typeof psbtEnum;
434
813
  const [_, kC, vC] = psbtEnum[key];
435
814
  type _KV = [P.UnwrapCoder<typeof kC>, P.UnwrapCoder<typeof vC>];
436
- const cannotChange = allowedFields && !allowedFields.includes(k);
437
- if (val[k] === undefined && k in val) {
815
+ const cannotChange = _allowedFields && !_allowedFields.includes(k);
816
+ if (_val[k] === undefined && k in _val) {
438
817
  if (cannotChange) throw new Error(`Cannot remove signed field=${k}`);
439
818
  delete res[k];
440
819
  } else if (kC) {
441
- const oldKV = (cur && cur[k] ? cur[k] : []) as _KV[];
442
- let newKV = val[key] as _KV[];
820
+ const oldKV = (_cur && _cur[k] ? _cur[k] : []) as _KV[];
821
+ let newKV = _val[key] as _KV[];
443
822
  if (newKV) {
444
823
  if (!Array.isArray(newKV)) throw new Error(`keyMap(${k}): KV pairs should be [k, v][]`);
445
824
  // Decode hex in k-v
@@ -479,11 +858,29 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
479
858
  }
480
859
  } else if (typeof res[k] === 'string') {
481
860
  res[k] = vC.decode(hex.decode(res[k] as string));
482
- } else if (cannotChange && k in val && cur && cur[k] !== undefined) {
483
- if (!equalBytes(vC.encode(val[k]), vC.encode(cur[k])))
861
+ } else if (cannotChange && k in _val && _cur && _cur[k] !== undefined) {
862
+ if (!equalBytes(vC.encode(_val[k]), vC.encode(_cur[k])))
484
863
  throw new Error(`Cannot change signed field=${k}`);
485
864
  }
486
865
  }
866
+ if (allowUnknown && _val.unknown) {
867
+ // Unknown PSBT rows are stripped by default here, but explicit allowUnknown mode is pass-through.
868
+ // Merge them by full serialized unknown key so repeated updates do not clobber earlier opaque rows.
869
+ const map: Record<string, [P.UnwrapCoder<typeof PSBTUnknownKey>, Bytes]> = {};
870
+ for (const [k, v] of _cur?.unknown || []) map[hex.encode(PSBTUnknownKey.encode(k))] = [k, v];
871
+ for (const [k, v] of _val.unknown) {
872
+ const kStr = hex.encode(PSBTUnknownKey.encode(k));
873
+ if (map[kStr] === undefined) {
874
+ map[kStr] = [k, v];
875
+ continue;
876
+ }
877
+ const oldVal = hex.encode(BytesInf.encode(map[kStr][1]));
878
+ const newVal = hex.encode(BytesInf.encode(v));
879
+ if (oldVal !== newVal)
880
+ throw new Error(`keyMap(unknown): same key=${kStr} oldVal=${oldVal} newVal=${newVal}`);
881
+ }
882
+ res.unknown = Object.values(map);
883
+ }
487
884
  // Remove unknown keys except the "unknown" array if allowUnknown is true
488
885
  for (const k in res) {
489
886
  if (!psbtEnum[k]) {
@@ -491,8 +888,16 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
491
888
  delete res[k];
492
889
  }
493
890
  }
494
- return res;
891
+ return res as TRet<PSBTKeyMapKeys<T>>;
495
892
  }
496
893
 
497
- export const RawPSBTV0 = P.validate(_RawPSBTV0, validatePSBT);
498
- export const RawPSBTV2 = P.validate(_RawPSBTV2, validatePSBT);
894
+ /** Validated PSBTv0 coder. */
895
+ // This wrapper only layers `validatePSBT`'s PSBTv0 field/count reconciliation on top of
896
+ // `_RawPSBTV0`; field-specific payload invariants still depend on the nested coders/tables.
897
+ export const RawPSBTV0 = /* @__PURE__ */ (() =>
898
+ Object.freeze(P.validate(_RawPSBTV0, validatePSBT)))();
899
+ /** Validated PSBTv2 coder. */
900
+ // This wrapper only layers `validatePSBT`'s PSBTv2 required-field/count reconciliation on top
901
+ // of `_RawPSBTV2`; nested input/output/global field invariants still depend on the coders below.
902
+ export const RawPSBTV2 = /* @__PURE__ */ (() =>
903
+ Object.freeze(P.validate(_RawPSBTV2, validatePSBT)))();