@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/README.md +189 -39
- package/index.d.ts +15 -5
- package/index.d.ts.map +1 -1
- package/index.js +16 -6
- package/index.js.map +1 -1
- package/musig2.d.ts +202 -64
- package/musig2.d.ts.map +1 -1
- package/musig2.js +324 -87
- package/musig2.js.map +1 -1
- package/p2p.d.ts +17 -7
- package/p2p.d.ts.map +1 -1
- package/p2p.js +40 -4
- package/p2p.js.map +1 -1
- package/package.json +25 -10
- package/payment.d.ts +407 -38
- package/payment.d.ts.map +1 -1
- package/payment.js +504 -57
- package/payment.js.map +1 -1
- package/psbt.d.ts +2958 -559
- package/psbt.d.ts.map +1 -1
- package/psbt.js +462 -118
- package/psbt.js.map +1 -1
- package/script.d.ts +311 -132
- package/script.d.ts.map +1 -1
- package/script.js +246 -35
- package/script.js.map +1 -1
- package/src/index.ts +34 -11
- package/src/musig2.ts +387 -145
- package/src/p2p.ts +54 -18
- package/src/payment.ts +823 -226
- package/src/psbt.ts +633 -228
- package/src/script.ts +353 -117
- package/src/transaction.ts +593 -169
- package/src/utils.ts +322 -43
- package/src/utxo.ts +154 -51
- package/transaction.d.ts +242 -31
- package/transaction.d.ts.map +1 -1
- package/transaction.js +460 -100
- package/transaction.js.map +1 -1
- package/utils.d.ts +266 -24
- package/utils.d.ts.map +1 -1
- package/utils.js +278 -27
- package/utils.js.map +1 -1
- package/utxo.d.ts +438 -75
- package/utxo.d.ts.map +1 -1
- package/utxo.js +123 -36
- package/utxo.js.map +1 -1
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
|
-
//
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
48
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
360
|
+
add({ type, key: P.EMPTY }, vc.encode(val));
|
|
154
361
|
}
|
|
155
362
|
else {
|
|
156
|
-
//
|
|
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
|
-
|
|
372
|
+
add({ key, type }, value);
|
|
165
373
|
}
|
|
166
374
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
for (const [k, v] of
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) &&
|
|
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
|
|
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] =
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 =
|
|
340
|
-
if (
|
|
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 = (
|
|
347
|
-
let newKV =
|
|
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
|
|
393
|
-
if (!equalBytes(vC.encode(
|
|
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
|
-
|
|
408
|
-
|
|
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
|