@scure/btc-signer 2.2.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 +196 -37
- package/index.d.ts +4 -4
- package/index.js +3 -4
- package/musig2.d.ts +28 -9
- package/musig2.js +46 -18
- package/net.d.ts +355 -0
- package/net.js +880 -0
- package/p2p.d.ts +0 -1
- package/p2p.js +23 -7
- package/package.json +18 -21
- package/payment.d.ts +18 -11
- package/payment.js +203 -58
- package/psbt.d.ts +680 -10
- package/psbt.js +204 -43
- package/script.d.ts +1 -2
- package/script.js +71 -59
- package/src/_type_test.ts +83 -0
- package/src/index.ts +4 -2
- package/src/musig2.ts +71 -19
- package/src/net.ts +1111 -0
- package/src/p2p.ts +22 -6
- package/src/payment.ts +233 -70
- package/src/psbt.ts +228 -39
- package/src/script.ts +57 -35
- package/src/transaction.ts +869 -168
- package/src/utils.ts +92 -4
- package/src/utxo.ts +223 -113
- package/transaction.d.ts +40 -7
- package/transaction.js +745 -151
- package/utils.d.ts +45 -2
- package/utils.js +80 -5
- package/utxo.d.ts +192 -2
- package/utxo.js +200 -99
- 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/src/psbt.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
+
import { anumber, concatBytes } from '@noble/hashes/utils.js';
|
|
2
3
|
import * as P from 'micro-packed';
|
|
3
4
|
import {
|
|
4
5
|
CompactSize,
|
|
@@ -10,17 +11,42 @@ import {
|
|
|
10
11
|
VarBytes,
|
|
11
12
|
} from './script.ts';
|
|
12
13
|
import {
|
|
14
|
+
aarray,
|
|
13
15
|
type Bytes,
|
|
14
16
|
compareBytes,
|
|
15
17
|
equalBytes,
|
|
16
18
|
PubT,
|
|
17
19
|
type TArg,
|
|
18
20
|
type TRet,
|
|
21
|
+
validateObject,
|
|
19
22
|
validatePubkey,
|
|
20
23
|
} from './utils.ts';
|
|
21
24
|
|
|
22
25
|
// PSBT BIP174, BIP370, BIP371
|
|
23
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
|
+
|
|
46
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
47
|
+
// prettier-ignore
|
|
48
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
49
|
+
|
|
24
50
|
// BIP174 keydata only says "public key", so legacy PSBT ECDSA fields still accept both
|
|
25
51
|
// compressed (33-byte) and uncompressed (65-byte) SEC1 encodings, but not x-only keys.
|
|
26
52
|
const PubKeyECDSA: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
|
|
@@ -157,9 +183,9 @@ const tapTree = /* @__PURE__ */ (() =>
|
|
|
157
183
|
while (next.length < depth) next.push(0);
|
|
158
184
|
path = next;
|
|
159
185
|
}
|
|
160
|
-
let leaves =
|
|
161
|
-
for (let i = 0; i < tree.length; i++) leaves +=
|
|
162
|
-
if (leaves !==
|
|
186
|
+
let leaves = _0n;
|
|
187
|
+
for (let i = 0; i < tree.length; i++) leaves += _1n << BigInt(maxDepth - tree[i].depth);
|
|
188
|
+
if (leaves !== _1n << BigInt(maxDepth))
|
|
163
189
|
throw new Error('tapTree: tuples must describe a complete binary tree');
|
|
164
190
|
return tree;
|
|
165
191
|
}
|
|
@@ -169,11 +195,67 @@ const tapTree = /* @__PURE__ */ (() =>
|
|
|
169
195
|
// field-specific structure and length checks still live at the individual field definitions.
|
|
170
196
|
// Keep a distinct name here so the byte coder does not collide with the Bytes type alias.
|
|
171
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
|
+
})();
|
|
172
217
|
// Shared 20-byte key-data helper for the BIP174 RIPEMD160 and HASH160 preimage maps.
|
|
173
218
|
const Bytes20: P.CoderType<Bytes> = /* @__PURE__ */ P.bytes(20);
|
|
174
219
|
// Shared 32-byte helper for fixed-size hash / txid / merkle-root byte fields; any stronger
|
|
175
220
|
// semantics such as x-only pubkey validity still need to be enforced by the field that uses it.
|
|
176
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 }))();
|
|
177
259
|
type PSBTKeyCoder = P.CoderType<any> | false;
|
|
178
260
|
type PSBTKeyMapInfo = Readonly<
|
|
179
261
|
[
|
|
@@ -237,8 +319,12 @@ export const PSBTGlobal = /* @__PURE__ */ (() => Object.freeze({
|
|
|
237
319
|
outputCount: PSBTInfo(0x05, false, CompactSizeLen, [2], [2], false),
|
|
238
320
|
// TODO: bitfield
|
|
239
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),
|
|
240
326
|
version: PSBTInfo(0xfb, false, P.U32LE, [], [0, 2], false),
|
|
241
|
-
proprietary: PSBTInfo(0xfc, BytesInf,
|
|
327
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
242
328
|
} as const))();
|
|
243
329
|
// prettier-ignore
|
|
244
330
|
/**
|
|
@@ -286,7 +372,15 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
286
372
|
tapBip32Derivation: PSBTInfo(0x16, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
287
373
|
tapInternalKey: PSBTInfo(0x17, false, PubKeySchnorr, [], [0, 2], false),
|
|
288
374
|
tapMerkleRoot: PSBTInfo(0x18, false, Bytes32, [], [0, 2], false),
|
|
289
|
-
|
|
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),
|
|
290
384
|
} as const))();
|
|
291
385
|
// All other keys removed when finalizing
|
|
292
386
|
/**
|
|
@@ -301,32 +395,31 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
301
395
|
*/
|
|
302
396
|
export const PSBTInputFinalKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
|
|
303
397
|
// PSBTv2 extractors rebuild the final transaction from per-input fields, so
|
|
304
|
-
// finalized inputs still need txid/index
|
|
398
|
+
// finalized inputs still need txid/index, any non-default sequence, and locktime requirements
|
|
305
399
|
// even though BIP174's generic cleanup is stricter.
|
|
306
400
|
'txid',
|
|
307
401
|
'sequence',
|
|
308
402
|
'index',
|
|
309
403
|
'witnessUtxo',
|
|
310
404
|
'nonWitnessUtxo',
|
|
405
|
+
'requiredTimeLocktime',
|
|
406
|
+
'requiredHeightLocktime',
|
|
311
407
|
'finalScriptSig',
|
|
312
408
|
'finalScriptWitness',
|
|
313
409
|
'unknown',
|
|
314
410
|
]);
|
|
315
411
|
|
|
316
|
-
// Can be modified even on signed input
|
|
317
412
|
/**
|
|
318
|
-
*
|
|
413
|
+
* Signature and final-satisfaction fields used while reopening a finalized input.
|
|
319
414
|
* @example
|
|
320
|
-
*
|
|
415
|
+
* Finalized inputs may remove their existing satisfaction before further mutation.
|
|
321
416
|
* ```ts
|
|
322
|
-
* import {
|
|
323
|
-
* const mutableKeys = new Set(
|
|
417
|
+
* import { PSBTInputSignatureKeys } from '@scure/btc-signer/psbt.js';
|
|
418
|
+
* const mutableKeys = new Set(PSBTInputSignatureKeys);
|
|
324
419
|
* mutableKeys.has('tapScriptSig');
|
|
325
420
|
* ```
|
|
326
421
|
*/
|
|
327
|
-
export const
|
|
328
|
-
// This is the replace/remove allowlist for signed inputs; mergeKeyMap() can still append
|
|
329
|
-
// previously absent metadata or new KV entries for other fields when they don't conflict.
|
|
422
|
+
export const PSBTInputSignatureKeys = /* @__PURE__ */ Object.freeze<(keyof TransactionInput)[]>([
|
|
330
423
|
'partialSig',
|
|
331
424
|
'finalScriptSig',
|
|
332
425
|
'finalScriptWitness',
|
|
@@ -334,6 +427,14 @@ export const PSBTInputUnsignedKeys = /* @__PURE__ */ Object.freeze<(keyof Transa
|
|
|
334
427
|
'tapScriptSig',
|
|
335
428
|
]);
|
|
336
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
|
+
|
|
337
438
|
// prettier-ignore
|
|
338
439
|
/**
|
|
339
440
|
* PSBT output key definitions.
|
|
@@ -357,7 +458,11 @@ export const PSBTOutput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
357
458
|
// reconstruct the same Taproot tree, not just an arbitrary list of serialized leaves.
|
|
358
459
|
tapTree: PSBTInfo(0x06, false, tapTree, [], [0, 2], false),
|
|
359
460
|
tapBip32Derivation: PSBTInfo(0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
360
|
-
|
|
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),
|
|
361
466
|
} as const))();
|
|
362
467
|
|
|
363
468
|
// Can be modified even on signed input
|
|
@@ -593,7 +698,7 @@ export const PSBTOutputCoder = /* @__PURE__ */ (() =>
|
|
|
593
698
|
// in the field coder itself because BIP371 constrains the tuple value, not just the row shape.
|
|
594
699
|
// BIP174/BIP370 define PSBT_OUT_AMOUNT as a signed int64 transport field, but it still
|
|
595
700
|
// represents the transaction output amount in satoshis, so negative output values are invalid.
|
|
596
|
-
if (o.amount !== undefined && o.amount <
|
|
701
|
+
if (o.amount !== undefined && o.amount < _0n)
|
|
597
702
|
throw new Error(`validateOutput: wrong amount=${o.amount}`);
|
|
598
703
|
if (o.bip32Derivation) for (const [k] of o.bip32Derivation) validatePubkey(k, PubT.ecdsa);
|
|
599
704
|
return o;
|
|
@@ -726,6 +831,9 @@ export function cleanPSBTFields<T extends PSBTKeyMap>(
|
|
|
726
831
|
info: T,
|
|
727
832
|
lst: TArg<PSBTKeyMapKeys<T>>
|
|
728
833
|
): TRet<PSBTKeyMapKeys<T>> {
|
|
834
|
+
anumber(version, 'version');
|
|
835
|
+
validateObject(info as Record<string, any>, {}, {}, 'info');
|
|
836
|
+
validateObject(lst as Record<string, any>, {}, {}, 'lst');
|
|
729
837
|
const _lst = lst as PSBTKeyMapKeys<T>;
|
|
730
838
|
const out: PSBTKeyMapKeys<T> = {};
|
|
731
839
|
for (const _k in _lst) {
|
|
@@ -747,27 +855,35 @@ export function cleanPSBTFields<T extends PSBTKeyMap>(
|
|
|
747
855
|
return out as TRet<PSBTKeyMapKeys<T>>;
|
|
748
856
|
}
|
|
749
857
|
|
|
750
|
-
function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
|
|
858
|
+
function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>): P.UnwrapCoder<PSBTRaw> {
|
|
751
859
|
const version = (tx && tx.global && tx.global.version) || 0;
|
|
752
860
|
validatePSBTFields(version, PSBTGlobal, tx.global);
|
|
753
861
|
for (const i of tx.inputs) validatePSBTFields(version, PSBTInput, i);
|
|
754
862
|
for (const o of tx.outputs) validatePSBTFields(version, PSBTOutput, o);
|
|
755
863
|
// BIP174 defines `<psbt> := <magic> <global-map> <input-map>* <output-map>*`, so after decode the
|
|
756
864
|
// number of input/output maps should match the unsigned tx. PSBTv2 makes the same shape explicit
|
|
757
|
-
// through `inputCount` / `outputCount`.
|
|
758
|
-
// keep accepting exactly one trailing empty map because the separate bitcoinjs compatibility PSBT
|
|
759
|
-
// fixture corpus still contains that encoding. Anything non-empty or more than one extra map is
|
|
760
|
-
// still rejected here.
|
|
865
|
+
// through `inputCount` / `outputCount`. Input maps are count-framed and must match exactly.
|
|
761
866
|
const inputCount = !version ? tx.global.unsignedTx!.inputs.length : tx.global.inputCount!;
|
|
762
|
-
if (tx.inputs.length
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
//
|
|
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.
|
|
767
873
|
const outputCount = !version ? tx.global.unsignedTx!.outputs.length : tx.global.outputCount!;
|
|
768
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;
|
|
769
882
|
const outputsLeft = tx.outputs.slice(outputCount);
|
|
770
|
-
if (
|
|
883
|
+
if (
|
|
884
|
+
outputsLeft.length > 1 ||
|
|
885
|
+
(outputsLeft.length && (version !== 0 || Object.keys(outputsLeft[0]).length))
|
|
886
|
+
)
|
|
771
887
|
throw new Error(`Unexpected outputs left in tx=${outputsLeft}`);
|
|
772
888
|
return tx;
|
|
773
889
|
}
|
|
@@ -778,7 +894,8 @@ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
|
|
|
778
894
|
* @param val - new values to merge in
|
|
779
895
|
* @param cur - existing decoded PSBT key map
|
|
780
896
|
* @param allowedFields - fields still allowed to change
|
|
781
|
-
* @param
|
|
897
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
898
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
782
899
|
* @returns Merged PSBT key map.
|
|
783
900
|
* @throws If keyed PSBT fields conflict or signed fields would be removed. {@link Error}
|
|
784
901
|
* @example
|
|
@@ -801,15 +918,34 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
801
918
|
val: TArg<PSBTKeyMapKeys<T>>,
|
|
802
919
|
cur?: TArg<PSBTKeyMapKeys<T>>,
|
|
803
920
|
allowedFields?: TArg<readonly (keyof PSBTKeyMapKeys<T>)[]>,
|
|
804
|
-
|
|
921
|
+
unknown: UnknownsArg = 'strip',
|
|
922
|
+
proprietary: UnknownsArg = 'strip'
|
|
805
923
|
): TRet<PSBTKeyMapKeys<T>> {
|
|
924
|
+
validateObject(psbtEnum as Record<string, any>, {}, {}, 'psbtEnum');
|
|
925
|
+
validateObject(val as Record<string, any>, {}, {}, 'val');
|
|
926
|
+
if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
|
|
927
|
+
if (allowedFields !== undefined) aarray(allowedFields, 'allowedFields');
|
|
806
928
|
const _val = val as PSBTKeyMapKeys<T>;
|
|
807
929
|
const _cur = cur as PSBTKeyMapKeys<T> | undefined;
|
|
808
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
|
+
}
|
|
809
944
|
const res: PSBTKeyMapKeys<T> = { ..._cur, ..._val };
|
|
810
945
|
// All arguments can be provided as hex
|
|
811
946
|
for (const k in psbtEnum) {
|
|
812
947
|
const key = k as keyof typeof psbtEnum;
|
|
948
|
+
if (k === 'proprietary' && proprietaryMode !== 'ignore') continue;
|
|
813
949
|
const [_, kC, vC] = psbtEnum[key];
|
|
814
950
|
type _KV = [P.UnwrapCoder<typeof kC>, P.UnwrapCoder<typeof vC>];
|
|
815
951
|
const cannotChange = _allowedFields && !_allowedFields.includes(k);
|
|
@@ -852,19 +988,27 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
852
988
|
if (v === undefined) {
|
|
853
989
|
if (cannotChange) throw new Error(`Cannot remove signed field=${key as string}/${k}`);
|
|
854
990
|
delete map[kStr];
|
|
855
|
-
} 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
|
+
}
|
|
856
996
|
}
|
|
857
997
|
(res as any)[key] = Object.values(map) as _KV[];
|
|
858
998
|
}
|
|
859
|
-
} else
|
|
860
|
-
res[k] = vC.decode(hex.decode(res[k] as string));
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
+
}
|
|
864
1008
|
}
|
|
865
1009
|
}
|
|
866
|
-
if (
|
|
867
|
-
// 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.
|
|
868
1012
|
// Merge them by full serialized unknown key so repeated updates do not clobber earlier opaque rows.
|
|
869
1013
|
const map: Record<string, [P.UnwrapCoder<typeof PSBTUnknownKey>, Bytes]> = {};
|
|
870
1014
|
for (const [k, v] of _cur?.unknown || []) map[hex.encode(PSBTUnknownKey.encode(k))] = [k, v];
|
|
@@ -881,16 +1025,61 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
881
1025
|
}
|
|
882
1026
|
res.unknown = Object.values(map);
|
|
883
1027
|
}
|
|
884
|
-
// Remove
|
|
1028
|
+
// Remove properties outside the table, except opaque rows in explicit ignore mode.
|
|
885
1029
|
for (const k in res) {
|
|
886
1030
|
if (!psbtEnum[k]) {
|
|
887
|
-
if (
|
|
1031
|
+
if (unknownMode === 'ignore' && k === 'unknown') continue;
|
|
888
1032
|
delete res[k];
|
|
889
1033
|
}
|
|
890
1034
|
}
|
|
1035
|
+
if (unknownMode !== 'ignore') delete res.unknown;
|
|
1036
|
+
if (proprietaryMode !== 'ignore') delete res.proprietary;
|
|
891
1037
|
return res as TRet<PSBTKeyMapKeys<T>>;
|
|
892
1038
|
}
|
|
893
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
|
+
|
|
894
1083
|
/** Validated PSBTv0 coder. */
|
|
895
1084
|
// This wrapper only layers `validatePSBT`'s PSBTv0 field/count reconciliation on top of
|
|
896
1085
|
// `_RawPSBTV0`; field-specific payload invariants still depend on the nested coders/tables.
|
package/src/script.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import * as P from 'micro-packed';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
aarray,
|
|
4
|
+
abytes,
|
|
5
|
+
isBytes,
|
|
6
|
+
reverseObject,
|
|
7
|
+
type Bytes,
|
|
8
|
+
type TArg,
|
|
9
|
+
type TRet,
|
|
10
|
+
type ValueOf,
|
|
11
|
+
} from './utils.ts';
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* Maximum byte size allowed for a single pushed script element.
|
|
@@ -8,6 +17,12 @@ import { isBytes, reverseObject, type ValueOf, type Bytes, type TArg, type TRet
|
|
|
8
17
|
*/
|
|
9
18
|
export const MAX_SCRIPT_BYTE_LENGTH = 520;
|
|
10
19
|
|
|
20
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1), _2n = /* @__PURE__ */ BigInt(2), _8n = /* @__PURE__ */ BigInt(8);
|
|
23
|
+
const U8_MAX = /* @__PURE__ */ BigInt(0xff);
|
|
24
|
+
const COMPACT_DIRECT_MAX = /* @__PURE__ */ BigInt(0xfc);
|
|
25
|
+
|
|
11
26
|
// prettier-ignore
|
|
12
27
|
/**
|
|
13
28
|
* Bitcoin Script opcode table.
|
|
@@ -87,11 +102,11 @@ export type ScriptType = ScriptOP[];
|
|
|
87
102
|
export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<bigint> {
|
|
88
103
|
return P.wrap({
|
|
89
104
|
encodeStream: (w: P.Writer, value: bigint) => {
|
|
90
|
-
if (value ===
|
|
105
|
+
if (value === _0n) return;
|
|
91
106
|
const neg = value < 0;
|
|
92
107
|
const val = BigInt(value);
|
|
93
108
|
const nums = [];
|
|
94
|
-
for (let abs = neg ? -val : val; abs; abs >>=
|
|
109
|
+
for (let abs = neg ? -val : val; abs; abs >>= _8n) nums.push(Number(abs & U8_MAX));
|
|
95
110
|
if (nums[nums.length - 1] >= 0x80) nums.push(neg ? 0x80 : 0);
|
|
96
111
|
else if (neg) nums[nums.length - 1] |= 0x80;
|
|
97
112
|
w.bytes(new Uint8Array(nums));
|
|
@@ -100,24 +115,22 @@ export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<big
|
|
|
100
115
|
const len = r.leftBytes;
|
|
101
116
|
if (len > bytesLimit)
|
|
102
117
|
throw new Error(`ScriptNum: number (${len}) bigger than limit=${bytesLimit}`);
|
|
103
|
-
if (len === 0) return
|
|
118
|
+
if (len === 0) return _0n;
|
|
119
|
+
// Read the payload once instead of peeking for the minimality check and
|
|
120
|
+
// then re-reading it byte-by-byte through the Reader.
|
|
121
|
+
const data = r.bytes(len);
|
|
104
122
|
if (forceMinimal) {
|
|
105
|
-
const data = r.bytes(len, true);
|
|
106
123
|
// MSB is zero (without sign bit) -> not minimally encoded
|
|
107
|
-
if ((data[
|
|
124
|
+
if ((data[len - 1] & 0x7f) === 0) {
|
|
108
125
|
// exception
|
|
109
|
-
if (len <= 1 || (data[
|
|
126
|
+
if (len <= 1 || (data[len - 2] & 0x80) === 0)
|
|
110
127
|
throw new Error('Non-minimally encoded ScriptNum');
|
|
111
128
|
}
|
|
112
129
|
}
|
|
113
|
-
let
|
|
114
|
-
let
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
res |= BigInt(last) << (8n * BigInt(i));
|
|
118
|
-
}
|
|
119
|
-
if (last >= 0x80) {
|
|
120
|
-
res &= (2n ** BigInt(len * 8) - 1n) >> 1n;
|
|
130
|
+
let res = _0n;
|
|
131
|
+
for (let i = 0; i < len; ++i) res |= BigInt(data[i]) << (_8n * BigInt(i));
|
|
132
|
+
if (data[len - 1] >= 0x80) {
|
|
133
|
+
res &= (_2n ** BigInt(len * 8) - _1n) >> _1n;
|
|
121
134
|
res = -res;
|
|
122
135
|
}
|
|
123
136
|
return res;
|
|
@@ -148,7 +161,9 @@ export function OpToNum(
|
|
|
148
161
|
if (isBytes(op)) {
|
|
149
162
|
try {
|
|
150
163
|
const val = ScriptNum(bytesLimit, forceMinimal).decode(op);
|
|
151
|
-
|
|
164
|
+
// Symmetric safe-integer bound: large negative values would otherwise
|
|
165
|
+
// coerce through Number() with silent precision loss.
|
|
166
|
+
if (val > Number.MAX_SAFE_INTEGER || val < -Number.MAX_SAFE_INTEGER) return;
|
|
152
167
|
return Number(val);
|
|
153
168
|
} catch (e) {
|
|
154
169
|
return;
|
|
@@ -203,10 +218,14 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
|
|
|
203
218
|
Object.freeze(
|
|
204
219
|
P.wrap({
|
|
205
220
|
encodeStream: (w: P.Writer, value: TArg<ScriptType>) => {
|
|
221
|
+
aarray(value, 'value');
|
|
206
222
|
for (let o of value) {
|
|
207
223
|
if (typeof o === 'string') {
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
const op = OP[o];
|
|
225
|
+
// OP is a plain object, so inherited Object.prototype keys ('toString',
|
|
226
|
+
// 'constructor', ...) are not opcodes and must be rejected here too.
|
|
227
|
+
if (typeof op !== 'number') throw new Error(`Unknown opcode=${o}`);
|
|
228
|
+
w.byte(op);
|
|
210
229
|
continue;
|
|
211
230
|
} else if (typeof o === 'number') {
|
|
212
231
|
if (o === 0x00) {
|
|
@@ -224,7 +243,7 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
|
|
|
224
243
|
}
|
|
225
244
|
// Encode big numbers
|
|
226
245
|
if (typeof o === 'number') o = ScriptNum().encode(BigInt(o));
|
|
227
|
-
|
|
246
|
+
abytes(o, undefined, 'value');
|
|
228
247
|
// Bytes
|
|
229
248
|
const len = o.length;
|
|
230
249
|
if (len < OP.PUSHDATA1) w.byte(len);
|
|
@@ -267,13 +286,6 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
|
|
|
267
286
|
})
|
|
268
287
|
))() as TRet<P.CoderType<ScriptType>>;
|
|
269
288
|
|
|
270
|
-
// BTC specific variable length integer encoding
|
|
271
|
-
// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
|
|
272
|
-
const CSLimits: Record<number, [number, number, bigint, bigint]> = {
|
|
273
|
-
0xfd: [0xfd, 2, 253n, 65535n],
|
|
274
|
-
0xfe: [0xfe, 4, 65536n, 4294967295n],
|
|
275
|
-
0xff: [0xff, 8, 4294967296n, 18446744073709551615n],
|
|
276
|
-
};
|
|
277
289
|
/**
|
|
278
290
|
* Bitcoin CompactSize integer coder.
|
|
279
291
|
* @example
|
|
@@ -282,16 +294,25 @@ const CSLimits: Record<number, [number, number, bigint, bigint]> = {
|
|
|
282
294
|
* CompactSize.encode(1n);
|
|
283
295
|
* ```
|
|
284
296
|
*/
|
|
285
|
-
export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() =>
|
|
286
|
-
|
|
297
|
+
export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() => {
|
|
298
|
+
// BTC specific variable length integer encoding
|
|
299
|
+
// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
|
|
300
|
+
const limits: Record<number, [number, number, bigint, bigint]> = {
|
|
301
|
+
0xfd: [0xfd, 2, BigInt(0xfd), BigInt(0xffff)],
|
|
302
|
+
0xfe: [0xfe, 4, BigInt(0x10000), BigInt(0xffffffff)],
|
|
303
|
+
0xff: [0xff, 8, BigInt(0x100000000), BigInt('0xffffffffffffffff')],
|
|
304
|
+
};
|
|
305
|
+
// Hoisted: Object.values() would otherwise allocate a fresh array on every encode.
|
|
306
|
+
const limitsList = Object.values(limits);
|
|
307
|
+
return Object.freeze(
|
|
287
308
|
P.wrap({
|
|
288
309
|
encodeStream: (w: P.Writer, value: bigint) => {
|
|
289
310
|
if (typeof value === 'number') value = BigInt(value);
|
|
290
|
-
if (
|
|
291
|
-
for (const [flag, bytes, start, stop] of
|
|
311
|
+
if (_0n <= value && value <= COMPACT_DIRECT_MAX) return w.byte(Number(value));
|
|
312
|
+
for (const [flag, bytes, start, stop] of limitsList) {
|
|
292
313
|
if (start > value || value > stop) continue;
|
|
293
314
|
w.byte(flag);
|
|
294
|
-
for (let i = 0; i < bytes; i++) w.byte(Number((value >> (
|
|
315
|
+
for (let i = 0; i < bytes; i++) w.byte(Number((value >> (_8n * BigInt(i))) & U8_MAX));
|
|
295
316
|
return;
|
|
296
317
|
}
|
|
297
318
|
throw w.err(`VarInt too big: ${value}`);
|
|
@@ -299,16 +320,17 @@ export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() =>
|
|
|
299
320
|
decodeStream: (r: P.Reader): bigint => {
|
|
300
321
|
const b0 = r.byte();
|
|
301
322
|
if (b0 <= 0xfc) return BigInt(b0);
|
|
302
|
-
const [_, bytes, start] =
|
|
303
|
-
let num =
|
|
304
|
-
for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (
|
|
323
|
+
const [_, bytes, start] = limits[b0];
|
|
324
|
+
let num = _0n;
|
|
325
|
+
for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (_8n * BigInt(i));
|
|
305
326
|
// BIP 152 / BIP 174: CompactSize fields must use the shortest encoding,
|
|
306
327
|
// so wider prefixes for smaller values are rejected here.
|
|
307
328
|
if (num < start) throw r.err(`Wrong CompactSize(${8 * bytes})`);
|
|
308
329
|
return num;
|
|
309
330
|
},
|
|
310
331
|
})
|
|
311
|
-
)
|
|
332
|
+
);
|
|
333
|
+
})();
|
|
312
334
|
|
|
313
335
|
// Same thing, but in number instead of bigint. Checks for safe integer inside
|
|
314
336
|
/**
|