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