@scure/btc-signer 2.3.0 → 2.4.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 +49 -10
- package/index.d.ts +4 -3
- package/index.js +3 -3
- package/musig2.d.ts +17 -3
- package/musig2.js +18 -6
- package/net.js +7 -2
- package/package.json +12 -11
- package/payment.d.ts +16 -5
- package/payment.js +118 -27
- package/psbt.d.ts +680 -9
- package/psbt.js +187 -38
- package/src/_type_test.ts +14 -0
- package/src/index.ts +4 -2
- package/src/musig2.ts +32 -7
- package/src/net.ts +7 -2
- package/src/payment.ts +145 -32
- package/src/psbt.ts +210 -35
- package/src/transaction.ts +790 -136
- package/src/utils.ts +24 -2
- package/src/utxo.ts +185 -71
- package/transaction.d.ts +40 -6
- package/transaction.js +667 -123
- package/utils.d.ts +15 -1
- package/utils.js +21 -2
- package/utxo.d.ts +192 -1
- package/utxo.js +166 -69
package/src/psbt.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
-
import { anumber } from '@noble/hashes/utils.js';
|
|
2
|
+
import { anumber, concatBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import * as P from 'micro-packed';
|
|
4
4
|
import {
|
|
5
5
|
CompactSize,
|
|
@@ -24,6 +24,25 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// PSBT BIP174, BIP370, BIP371
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Policy for PSBT fields this version does not understand.
|
|
29
|
+
*
|
|
30
|
+
* `ignore` preserves data for forward interoperability, but the library may operate without
|
|
31
|
+
* understanding future constraints. `strip` accepts then removes it, which can itself break
|
|
32
|
+
* interoperability with newer participants; `strict` rejects it. Non-`ignore` behavior also
|
|
33
|
+
* fingerprints noble/scure and potentially its version. When the original boolean handling was
|
|
34
|
+
* added, the PSBT registry had no assigned fields unknown to this library; assigned extensions are
|
|
35
|
+
* now explicit table entries and do not use this policy.
|
|
36
|
+
*/
|
|
37
|
+
export type Unknowns = 'ignore' | 'strip' | 'strict';
|
|
38
|
+
type UnknownsArg = Unknowns | boolean;
|
|
39
|
+
const unknowns = (mode: UnknownsArg, name: string): Unknowns => {
|
|
40
|
+
if (mode === true) return 'ignore';
|
|
41
|
+
if (mode === false) return 'strip';
|
|
42
|
+
if (mode === 'ignore' || mode === 'strip' || mode === 'strict') return mode;
|
|
43
|
+
throw new Error(`PSBT: invalid ${name} policy=${mode}`);
|
|
44
|
+
};
|
|
45
|
+
|
|
27
46
|
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
28
47
|
// prettier-ignore
|
|
29
48
|
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
@@ -176,11 +195,67 @@ const tapTree = /* @__PURE__ */ (() =>
|
|
|
176
195
|
// field-specific structure and length checks still live at the individual field definitions.
|
|
177
196
|
// Keep a distinct name here so the byte coder does not collide with the Bytes type alias.
|
|
178
197
|
const BytesInf: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(null);
|
|
198
|
+
// PSBTKeyMap emits the 0xfc type byte itself, so the public value remains only the bytes after it.
|
|
199
|
+
// Validate that raw suffix here: otherwise a caller-supplied leading 0xfc becomes a 252-byte
|
|
200
|
+
// identifier declaration which scure can relay but Bitcoin Core cannot parse.
|
|
201
|
+
const ProprietaryKey = /* @__PURE__ */ (() => {
|
|
202
|
+
const suffix = P.struct({
|
|
203
|
+
identifier: P.bytes(CompactSizeLen),
|
|
204
|
+
subtype: CompactSizeLen,
|
|
205
|
+
data: BytesInf,
|
|
206
|
+
});
|
|
207
|
+
return P.validate(BytesInf, (key) => {
|
|
208
|
+
try {
|
|
209
|
+
suffix.decode(key);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
throw new Error(`Proprietary key: expected BIP174 identifier and subtype, got ${message}`);
|
|
213
|
+
}
|
|
214
|
+
return key;
|
|
215
|
+
});
|
|
216
|
+
})();
|
|
179
217
|
// Shared 20-byte key-data helper for the BIP174 RIPEMD160 and HASH160 preimage maps.
|
|
180
218
|
const Bytes20: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(20);
|
|
181
219
|
// Shared 32-byte helper for fixed-size hash / txid / merkle-root byte fields; any stronger
|
|
182
220
|
// semantics such as x-only pubkey validity still need to be enforced by the field that uses it.
|
|
183
221
|
const Bytes32: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(32);
|
|
222
|
+
const Bytes64: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(64);
|
|
223
|
+
const Bytes66: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(66);
|
|
224
|
+
// BIP373 uses the same aggregate-key participant list in input and output maps.
|
|
225
|
+
const MuSig2Participants = /* @__PURE__ */ (() => P.array(null, PubKeyECDSACompressed))();
|
|
226
|
+
type MuSig2Key = {
|
|
227
|
+
participantPubkey: Bytes;
|
|
228
|
+
aggregatePubkey: Bytes;
|
|
229
|
+
leafHash?: Bytes;
|
|
230
|
+
};
|
|
231
|
+
// BIP373 keys append an optional tapleaf hash to participant || aggregate. A structured key keeps
|
|
232
|
+
// the optional hash distinct while preserving the exact 66/98-byte wire encoding.
|
|
233
|
+
const MuSig2Key = /* @__PURE__ */ (() =>
|
|
234
|
+
P.apply(BytesInf, {
|
|
235
|
+
decode: (key: TArg<MuSig2Key>) => {
|
|
236
|
+
const _key = key as MuSig2Key;
|
|
237
|
+
return concatBytes(
|
|
238
|
+
PubKeyECDSACompressed.encode(_key.participantPubkey),
|
|
239
|
+
PubKeyECDSACompressed.encode(_key.aggregatePubkey),
|
|
240
|
+
_key.leafHash === undefined ? P.EMPTY : Bytes32.encode(_key.leafHash)
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
encode: (raw: TArg<Bytes>): TRet<MuSig2Key> => {
|
|
244
|
+
const _raw = raw as Bytes;
|
|
245
|
+
if (_raw.length !== 66 && _raw.length !== 98)
|
|
246
|
+
throw new Error(`MuSig2 key: expected 66 or 98 bytes, got ${_raw.length}`);
|
|
247
|
+
const key: MuSig2Key = {
|
|
248
|
+
participantPubkey: PubKeyECDSACompressed.decode(_raw.subarray(0, 33)),
|
|
249
|
+
aggregatePubkey: PubKeyECDSACompressed.decode(_raw.subarray(33, 66)),
|
|
250
|
+
};
|
|
251
|
+
if (_raw.length === 98) key.leafHash = Bytes32.decode(_raw.subarray(66));
|
|
252
|
+
return key as TRet<MuSig2Key>;
|
|
253
|
+
},
|
|
254
|
+
}))();
|
|
255
|
+
const SilentPaymentInfo = /* @__PURE__ */ (() =>
|
|
256
|
+
P.struct({ scanKey: PubKeyECDSACompressed, spendKey: PubKeyECDSACompressed }))();
|
|
257
|
+
// BIP353 prefixes the human-readable name with one byte; the RFC9102 proof is opaque here.
|
|
258
|
+
const DNSSECProof = /* @__PURE__ */ (() => P.struct({ name: P.bytes(P.U8), proof: BytesInf }))();
|
|
184
259
|
type PSBTKeyCoder = P.CoderType<any> | false;
|
|
185
260
|
type PSBTKeyMapInfo = Readonly<
|
|
186
261
|
[
|
|
@@ -244,8 +319,12 @@ export const PSBTGlobal = /* @__PURE__ */ (() => Object.freeze({
|
|
|
244
319
|
outputCount: PSBTInfo(0x05, false, CompactSizeLen, [2], [2], false),
|
|
245
320
|
// TODO: bitfield
|
|
246
321
|
txModifiable: PSBTInfo(0x06, false, P.U8, [], [2], false),
|
|
322
|
+
// These assigned extension fields must not fall through the unknown-field policy.
|
|
323
|
+
spEcdhShare: PSBTInfo(0x07, PubKeyECDSACompressed, PubKeyECDSACompressed, [], [2], false),
|
|
324
|
+
spDleq: PSBTInfo(0x08, PubKeyECDSACompressed, Bytes64, [], [2], false),
|
|
325
|
+
genericSignedMessage: PSBTInfo(0x09, false, BytesInf, [], [0, 2], false),
|
|
247
326
|
version: PSBTInfo(0xfb, false, P.U32LE, [], [0, 2], false),
|
|
248
|
-
proprietary: PSBTInfo(0xfc, BytesInf,
|
|
327
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
249
328
|
} as const))();
|
|
250
329
|
// prettier-ignore
|
|
251
330
|
/**
|
|
@@ -293,7 +372,15 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
293
372
|
tapBip32Derivation: PSBTInfo(0x16, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
294
373
|
tapInternalKey: PSBTInfo(0x17, false, PubKeySchnorr, [], [0, 2], false),
|
|
295
374
|
tapMerkleRoot: PSBTInfo(0x18, false, Bytes32, [], [0, 2], false),
|
|
296
|
-
|
|
375
|
+
p2cKeyTweak: PSBTInfo(0x19, PubKeyECDSACompressed, Bytes32, [], [0, 2], false),
|
|
376
|
+
musig2ParticipantPubkeys: PSBTInfo(0x1a, PubKeyECDSACompressed, MuSig2Participants, [], [0, 2], false),
|
|
377
|
+
musig2PubNonce: PSBTInfo(0x1b, MuSig2Key, Bytes66, [], [0, 2], false),
|
|
378
|
+
musig2PartialSig: PSBTInfo(0x1c, MuSig2Key, Bytes32, [], [0, 2], false),
|
|
379
|
+
spEcdhShare: PSBTInfo(0x1d, PubKeyECDSACompressed, PubKeyECDSACompressed, [], [2], false),
|
|
380
|
+
spDleq: PSBTInfo(0x1e, PubKeyECDSACompressed, Bytes64, [], [2], false),
|
|
381
|
+
spSpendBip32Derivation: PSBTInfo(0x1f, PubKeyECDSACompressed, BIP32Der, [], [2], false),
|
|
382
|
+
spTweak: PSBTInfo(0x20, false, Bytes32, [], [2], false),
|
|
383
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
297
384
|
} as const))();
|
|
298
385
|
// All other keys removed when finalizing
|
|
299
386
|
/**
|
|
@@ -308,32 +395,31 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
308
395
|
*/
|
|
309
396
|
export const PSBTInputFinalKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
|
|
310
397
|
// PSBTv2 extractors rebuild the final transaction from per-input fields, so
|
|
311
|
-
// finalized inputs still need txid/index
|
|
398
|
+
// finalized inputs still need txid/index, any non-default sequence, and locktime requirements
|
|
312
399
|
// even though BIP174's generic cleanup is stricter.
|
|
313
400
|
'txid',
|
|
314
401
|
'sequence',
|
|
315
402
|
'index',
|
|
316
403
|
'witnessUtxo',
|
|
317
404
|
'nonWitnessUtxo',
|
|
405
|
+
'requiredTimeLocktime',
|
|
406
|
+
'requiredHeightLocktime',
|
|
318
407
|
'finalScriptSig',
|
|
319
408
|
'finalScriptWitness',
|
|
320
409
|
'unknown',
|
|
321
410
|
]);
|
|
322
411
|
|
|
323
|
-
// Can be modified even on signed input
|
|
324
412
|
/**
|
|
325
|
-
*
|
|
413
|
+
* Signature and final-satisfaction fields used while reopening a finalized input.
|
|
326
414
|
* @example
|
|
327
|
-
*
|
|
415
|
+
* Finalized inputs may remove their existing satisfaction before further mutation.
|
|
328
416
|
* ```ts
|
|
329
|
-
* import {
|
|
330
|
-
* const mutableKeys = new Set(
|
|
417
|
+
* import { PSBTInputSignatureKeys } from '@scure/btc-signer/psbt.js';
|
|
418
|
+
* const mutableKeys = new Set(PSBTInputSignatureKeys);
|
|
331
419
|
* mutableKeys.has('tapScriptSig');
|
|
332
420
|
* ```
|
|
333
421
|
*/
|
|
334
|
-
export const
|
|
335
|
-
// This is the replace/remove allowlist for signed inputs; mergeKeyMap() can still append
|
|
336
|
-
// previously absent metadata or new KV entries for other fields when they don't conflict.
|
|
422
|
+
export const PSBTInputSignatureKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
|
|
337
423
|
'partialSig',
|
|
338
424
|
'finalScriptSig',
|
|
339
425
|
'finalScriptWitness',
|
|
@@ -341,6 +427,14 @@ export const PSBTInputUnsignedKeys = /* @__PURE__ */ Object.freeze<(keyof Transa
|
|
|
341
427
|
'tapScriptSig',
|
|
342
428
|
]);
|
|
343
429
|
|
|
430
|
+
/**
|
|
431
|
+
* A static list cannot describe mutable input fields because that depends on each signature's
|
|
432
|
+
* algorithm, sighash, and target index.
|
|
433
|
+
* @deprecated Use {@link PSBTInputSignatureKeys} only for signature/final-satisfaction records;
|
|
434
|
+
* transaction mutation is enforced by {@link Transaction.updateInput}.
|
|
435
|
+
*/
|
|
436
|
+
export const PSBTInputUnsignedKeys = PSBTInputSignatureKeys;
|
|
437
|
+
|
|
344
438
|
// prettier-ignore
|
|
345
439
|
/**
|
|
346
440
|
* PSBT output key definitions.
|
|
@@ -364,7 +458,11 @@ export const PSBTOutput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
364
458
|
// reconstruct the same Taproot tree, not just an arbitrary list of serialized leaves.
|
|
365
459
|
tapTree: PSBTInfo(0x06, false, tapTree, [], [0, 2], false),
|
|
366
460
|
tapBip32Derivation: PSBTInfo(0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
367
|
-
|
|
461
|
+
musig2ParticipantPubkeys: PSBTInfo(0x08, PubKeyECDSACompressed, MuSig2Participants, [], [0, 2], false),
|
|
462
|
+
spV0Info: PSBTInfo(0x09, false, SilentPaymentInfo, [], [2], false),
|
|
463
|
+
spV0Label: PSBTInfo(0x0a, false, P.U32LE, [], [2], false),
|
|
464
|
+
dnssecProof: PSBTInfo(0x35, false, DNSSECProof, [], [0, 2], false),
|
|
465
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
368
466
|
} as const))();
|
|
369
467
|
|
|
370
468
|
// Can be modified even on signed input
|
|
@@ -764,20 +862,28 @@ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>): P.UnwrapCoder<PSBTRaw> {
|
|
|
764
862
|
for (const o of tx.outputs) validatePSBTFields(version, PSBTOutput, o);
|
|
765
863
|
// BIP174 defines `<psbt> := <magic> <global-map> <input-map>* <output-map>*`, so after decode the
|
|
766
864
|
// number of input/output maps should match the unsigned tx. PSBTv2 makes the same shape explicit
|
|
767
|
-
// through `inputCount` / `outputCount`.
|
|
768
|
-
// keep accepting exactly one trailing empty map because the separate bitcoinjs compatibility PSBT
|
|
769
|
-
// fixture corpus still contains that encoding. Anything non-empty or more than one extra map is
|
|
770
|
-
// still rejected here.
|
|
865
|
+
// through `inputCount` / `outputCount`. Input maps are count-framed and must match exactly.
|
|
771
866
|
const inputCount = !version ? tx.global.unsignedTx!.inputs.length : tx.global.inputCount!;
|
|
772
|
-
if (tx.inputs.length
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
//
|
|
867
|
+
if (tx.inputs.length !== inputCount)
|
|
868
|
+
throw new Error(`Wrong number of input maps=${tx.inputs.length}, expected=${inputCount}`);
|
|
869
|
+
// PSBTv0 compatibility mode may append exactly one empty output map when the unsigned
|
|
870
|
+
// transaction has no outputs. bip174js additionally inserts an empty input map when there are
|
|
871
|
+
// no inputs; count-framing makes that map appear before the real output maps in this array.
|
|
872
|
+
// PSBTv2 map counts remain strict.
|
|
777
873
|
const outputCount = !version ? tx.global.unsignedTx!.outputs.length : tx.global.outputCount!;
|
|
778
874
|
if (tx.outputs.length < outputCount) throw new Error('Not outputs inputs');
|
|
875
|
+
const hasBip174InputMap =
|
|
876
|
+
version === 0 &&
|
|
877
|
+
inputCount === 0 &&
|
|
878
|
+
Object.keys(tx.outputs[0] || {}).length === 0 &&
|
|
879
|
+
((outputCount > 0 && tx.outputs.length === outputCount + 1) ||
|
|
880
|
+
(outputCount === 0 && tx.outputs.length === 2 && Object.keys(tx.outputs[1]).length === 0));
|
|
881
|
+
if (hasBip174InputMap) return tx;
|
|
779
882
|
const outputsLeft = tx.outputs.slice(outputCount);
|
|
780
|
-
if (
|
|
883
|
+
if (
|
|
884
|
+
outputsLeft.length > 1 ||
|
|
885
|
+
(outputsLeft.length && (version !== 0 || Object.keys(outputsLeft[0]).length))
|
|
886
|
+
)
|
|
781
887
|
throw new Error(`Unexpected outputs left in tx=${outputsLeft}`);
|
|
782
888
|
return tx;
|
|
783
889
|
}
|
|
@@ -788,7 +894,8 @@ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>): P.UnwrapCoder<PSBTRaw> {
|
|
|
788
894
|
* @param val - new values to merge in
|
|
789
895
|
* @param cur - existing decoded PSBT key map
|
|
790
896
|
* @param allowedFields - fields still allowed to change
|
|
791
|
-
* @param
|
|
897
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
898
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
792
899
|
* @returns Merged PSBT key map.
|
|
793
900
|
* @throws If keyed PSBT fields conflict or signed fields would be removed. {@link Error}
|
|
794
901
|
* @example
|
|
@@ -811,7 +918,8 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
811
918
|
val: TArg<PSBTKeyMapKeys<T>>,
|
|
812
919
|
cur?: TArg<PSBTKeyMapKeys<T>>,
|
|
813
920
|
allowedFields?: TArg<readonly (keyof PSBTKeyMapKeys<T>)[]>,
|
|
814
|
-
|
|
921
|
+
unknown: UnknownsArg = 'strip',
|
|
922
|
+
proprietary: UnknownsArg = 'strip'
|
|
815
923
|
): TRet<PSBTKeyMapKeys<T>> {
|
|
816
924
|
validateObject(psbtEnum as Record<string, any>, {}, {}, 'psbtEnum');
|
|
817
925
|
validateObject(val as Record<string, any>, {}, {}, 'val');
|
|
@@ -820,10 +928,24 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
820
928
|
const _val = val as PSBTKeyMapKeys<T>;
|
|
821
929
|
const _cur = cur as PSBTKeyMapKeys<T> | undefined;
|
|
822
930
|
const _allowedFields = allowedFields as readonly (keyof PSBTKeyMapKeys<T>)[] | undefined;
|
|
931
|
+
const unknownMode = unknowns(unknown, 'unknown');
|
|
932
|
+
const proprietaryMode = unknowns(proprietary, 'proprietary');
|
|
933
|
+
for (const [name, mode] of [
|
|
934
|
+
['unknown', unknownMode],
|
|
935
|
+
['proprietary', proprietaryMode],
|
|
936
|
+
] as const) {
|
|
937
|
+
if (mode !== 'strict') continue;
|
|
938
|
+
if (
|
|
939
|
+
(_val[name] as unknown[] | undefined)?.length ||
|
|
940
|
+
(_cur?.[name] as unknown[] | undefined)?.length
|
|
941
|
+
)
|
|
942
|
+
throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
|
|
943
|
+
}
|
|
823
944
|
const res: PSBTKeyMapKeys<T> = { ..._cur, ..._val };
|
|
824
945
|
// All arguments can be provided as hex
|
|
825
946
|
for (const k in psbtEnum) {
|
|
826
947
|
const key = k as keyof typeof psbtEnum;
|
|
948
|
+
if (k === 'proprietary' && proprietaryMode !== 'ignore') continue;
|
|
827
949
|
const [_, kC, vC] = psbtEnum[key];
|
|
828
950
|
type _KV = [P.UnwrapCoder<typeof kC>, P.UnwrapCoder<typeof vC>];
|
|
829
951
|
const cannotChange = _allowedFields && !_allowedFields.includes(k);
|
|
@@ -866,19 +988,27 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
866
988
|
if (v === undefined) {
|
|
867
989
|
if (cannotChange) throw new Error(`Cannot remove signed field=${key as string}/${k}`);
|
|
868
990
|
delete map[kStr];
|
|
869
|
-
} else
|
|
991
|
+
} else {
|
|
992
|
+
if (cannotChange && map[kStr] === undefined)
|
|
993
|
+
throw new Error(`Cannot add signed field=${key as string}/${kStr}`);
|
|
994
|
+
add(kStr, k, v);
|
|
995
|
+
}
|
|
870
996
|
}
|
|
871
997
|
(res as any)[key] = Object.values(map) as _KV[];
|
|
872
998
|
}
|
|
873
|
-
} else
|
|
874
|
-
res[k] = vC.decode(hex.decode(res[k] as string));
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
999
|
+
} else {
|
|
1000
|
+
if (typeof res[k] === 'string') res[k] = vC.decode(hex.decode(res[k] as string));
|
|
1001
|
+
if (cannotChange && k in _val) {
|
|
1002
|
+
if (!_cur || _cur[k] === undefined) throw new Error(`Cannot add signed field=${k}`);
|
|
1003
|
+
let current = _cur[k];
|
|
1004
|
+
if (typeof current === 'string') current = vC.decode(hex.decode(current));
|
|
1005
|
+
if (!equalBytes(vC.encode(res[k]), vC.encode(current)))
|
|
1006
|
+
throw new Error(`Cannot change signed field=${k}`);
|
|
1007
|
+
}
|
|
878
1008
|
}
|
|
879
1009
|
}
|
|
880
|
-
if (
|
|
881
|
-
// Unknown PSBT rows are stripped by default here, but explicit
|
|
1010
|
+
if (unknownMode === 'ignore' && _val.unknown) {
|
|
1011
|
+
// Unknown PSBT rows are stripped by default here, but explicit ignore mode is pass-through.
|
|
882
1012
|
// Merge them by full serialized unknown key so repeated updates do not clobber earlier opaque rows.
|
|
883
1013
|
const map: Record<string, [P.UnwrapCoder<typeof PSBTUnknownKey>, Bytes]> = {};
|
|
884
1014
|
for (const [k, v] of _cur?.unknown || []) map[hex.encode(PSBTUnknownKey.encode(k))] = [k, v];
|
|
@@ -895,16 +1025,61 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
895
1025
|
}
|
|
896
1026
|
res.unknown = Object.values(map);
|
|
897
1027
|
}
|
|
898
|
-
// Remove
|
|
1028
|
+
// Remove properties outside the table, except opaque rows in explicit ignore mode.
|
|
899
1029
|
for (const k in res) {
|
|
900
1030
|
if (!psbtEnum[k]) {
|
|
901
|
-
if (
|
|
1031
|
+
if (unknownMode === 'ignore' && k === 'unknown') continue;
|
|
902
1032
|
delete res[k];
|
|
903
1033
|
}
|
|
904
1034
|
}
|
|
1035
|
+
if (unknownMode !== 'ignore') delete res.unknown;
|
|
1036
|
+
if (proprietaryMode !== 'ignore') delete res.proprietary;
|
|
905
1037
|
return res as TRet<PSBTKeyMapKeys<T>>;
|
|
906
1038
|
}
|
|
907
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* Combines two independently produced PSBT maps without choosing between conflicting scalar
|
|
1042
|
+
* values. Keyed fields retain {@link mergeKeyMap}'s union semantics.
|
|
1043
|
+
* @param psbtEnum - PSBT field definition table
|
|
1044
|
+
* @param current - first map
|
|
1045
|
+
* @param other - second map
|
|
1046
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
1047
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
1048
|
+
* @returns The symmetric map union.
|
|
1049
|
+
* @throws If both maps provide different values for the same scalar field. {@link Error}
|
|
1050
|
+
* @example
|
|
1051
|
+
* Combine disjoint global metadata without choosing an operand as authoritative.
|
|
1052
|
+
* ```ts
|
|
1053
|
+
* import { combineKeyMap, PSBTGlobal } from '@scure/btc-signer/psbt.js';
|
|
1054
|
+
* combineKeyMap(PSBTGlobal, { txVersion: 2 }, { fallbackLocktime: 0 });
|
|
1055
|
+
* ```
|
|
1056
|
+
*/
|
|
1057
|
+
export function combineKeyMap<T extends PSBTKeyMap>(
|
|
1058
|
+
psbtEnum: T,
|
|
1059
|
+
current: TArg<PSBTKeyMapKeys<T>>,
|
|
1060
|
+
other: TArg<PSBTKeyMapKeys<T>>,
|
|
1061
|
+
unknown: UnknownsArg = 'strip',
|
|
1062
|
+
proprietary: UnknownsArg = 'strip'
|
|
1063
|
+
): TRet<PSBTKeyMapKeys<T>> {
|
|
1064
|
+
validateObject(psbtEnum as Record<string, any>, {}, {}, 'psbtEnum');
|
|
1065
|
+
validateObject(current as Record<string, any>, {}, {}, 'current');
|
|
1066
|
+
validateObject(other as Record<string, any>, {}, {}, 'other');
|
|
1067
|
+
const _current = current as PSBTKeyMapKeys<T>;
|
|
1068
|
+
const _other = other as PSBTKeyMapKeys<T>;
|
|
1069
|
+
for (const k in psbtEnum) {
|
|
1070
|
+
const key = k as keyof typeof psbtEnum;
|
|
1071
|
+
const [_, keyCoder, valueCoder] = psbtEnum[key];
|
|
1072
|
+
if (keyCoder || _current[key] === undefined || _other[key] === undefined) continue;
|
|
1073
|
+
let a = _current[key];
|
|
1074
|
+
let b = _other[key];
|
|
1075
|
+
if (typeof a === 'string') a = valueCoder.decode(hex.decode(a));
|
|
1076
|
+
if (typeof b === 'string') b = valueCoder.decode(hex.decode(b));
|
|
1077
|
+
if (!equalBytes(valueCoder.encode(a), valueCoder.encode(b)))
|
|
1078
|
+
throw new Error(`Cannot combine conflicting field=${k}`);
|
|
1079
|
+
}
|
|
1080
|
+
return mergeKeyMap(psbtEnum, _other, _current, undefined, unknown, proprietary);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
908
1083
|
/** Validated PSBTv0 coder. */
|
|
909
1084
|
// This wrapper only layers `validatePSBT`'s PSBTv0 field/count reconciliation on top of
|
|
910
1085
|
// `_RawPSBTV0`; field-specific payload invariants still depend on the nested coders/tables.
|