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