@scure/btc-signer 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -28
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/musig2.d.ts +11 -6
- package/musig2.js +28 -12
- package/net.d.ts +355 -0
- package/net.js +875 -0
- package/p2p.d.ts +0 -1
- package/p2p.js +23 -7
- package/package.json +15 -19
- package/payment.d.ts +2 -6
- package/payment.js +85 -31
- package/psbt.d.ts +0 -1
- package/psbt.js +18 -6
- package/script.d.ts +1 -2
- package/script.js +71 -59
- package/src/_type_test.ts +69 -0
- package/src/musig2.ts +39 -12
- package/src/net.ts +1106 -0
- package/src/p2p.ts +22 -6
- package/src/payment.ts +88 -38
- package/src/psbt.ts +19 -5
- package/src/script.ts +57 -35
- package/src/transaction.ts +81 -34
- package/src/utils.ts +68 -2
- package/src/utxo.ts +39 -43
- package/transaction.d.ts +0 -1
- package/transaction.js +81 -31
- package/utils.d.ts +30 -1
- package/utils.js +59 -3
- package/utxo.d.ts +0 -1
- package/utxo.js +36 -32
- 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/transaction.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
+
import { anumber } from '@noble/hashes/utils.js';
|
|
2
3
|
import * as P from 'micro-packed';
|
|
3
4
|
import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
|
|
4
5
|
import * as psbt from './psbt.ts';
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
RawInput,
|
|
10
11
|
RawOutput,
|
|
11
12
|
RawTx,
|
|
12
|
-
RawWitness,
|
|
13
13
|
Script,
|
|
14
14
|
scriptPushLen,
|
|
15
15
|
VarBytes,
|
|
@@ -18,16 +18,22 @@ import * as u from './utils.ts';
|
|
|
18
18
|
import {
|
|
19
19
|
type Bytes,
|
|
20
20
|
NETWORK,
|
|
21
|
+
abigint,
|
|
21
22
|
concatBytes,
|
|
22
23
|
equalBytes,
|
|
23
24
|
isBytes,
|
|
24
25
|
type TArg,
|
|
25
26
|
type TRet,
|
|
27
|
+
validateObject,
|
|
26
28
|
} from './utils.ts';
|
|
27
29
|
|
|
30
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
31
|
+
// prettier-ignore
|
|
32
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
33
|
+
const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
|
|
28
34
|
const EMPTY32: Uint8Array = /* @__PURE__ */ new Uint8Array(32);
|
|
29
35
|
const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
|
|
30
|
-
amount:
|
|
36
|
+
amount: U64_MAX,
|
|
31
37
|
script: P.EMPTY,
|
|
32
38
|
};
|
|
33
39
|
/**
|
|
@@ -301,6 +307,7 @@ function outputBeforeSign(i: TArg<psbt.TransactionOutput>): TRet<psbt.Transactio
|
|
|
301
307
|
* ```
|
|
302
308
|
*/
|
|
303
309
|
export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<TransactionInputRequired> {
|
|
310
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
304
311
|
if (i.txid === undefined || i.index === undefined)
|
|
305
312
|
throw new Error('Transaction/input: txid and index required');
|
|
306
313
|
const res = {
|
|
@@ -344,8 +351,7 @@ function unpackSighash(hashType: number) {
|
|
|
344
351
|
}
|
|
345
352
|
|
|
346
353
|
function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
347
|
-
if (opts !== undefined
|
|
348
|
-
throw new Error(`Wrong object type for transaction options: ${opts}`);
|
|
354
|
+
if (opts !== undefined) validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
349
355
|
|
|
350
356
|
const _opts = {
|
|
351
357
|
...opts,
|
|
@@ -382,12 +388,15 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
382
388
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
383
389
|
}
|
|
384
390
|
// 0 and -1 happens in tests
|
|
391
|
+
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
392
|
+
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
385
393
|
if (
|
|
386
394
|
_opts.allowUnknownVersion
|
|
387
|
-
? typeof _opts.version
|
|
395
|
+
? typeof _opts.version !== 'number'
|
|
388
396
|
: ![-1, 0, 1, 2, 3].includes(_opts.version)
|
|
389
397
|
)
|
|
390
398
|
throw new Error(`Unknown version: ${_opts.version}`);
|
|
399
|
+
P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
|
|
391
400
|
if (_opts.customScripts !== undefined) {
|
|
392
401
|
const cs = _opts.customScripts;
|
|
393
402
|
if (!Array.isArray(cs)) {
|
|
@@ -407,6 +416,7 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
407
416
|
|
|
408
417
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
409
418
|
function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
419
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
410
420
|
const _i = i as PSBTInputs;
|
|
411
421
|
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
412
422
|
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
@@ -433,6 +443,9 @@ function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
|
433
443
|
allowUnknownOutputs: true,
|
|
434
444
|
disableScriptCheck: true,
|
|
435
445
|
allowUnknownInputs: true,
|
|
446
|
+
// Consensus does not restrict nVersion; a previous tx with a non-standard
|
|
447
|
+
// version is still spendable and its txid must still be verifiable.
|
|
448
|
+
allowUnknownVersion: true,
|
|
436
449
|
});
|
|
437
450
|
const txid = hex.encode(_i.txid);
|
|
438
451
|
// BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
|
|
@@ -465,6 +478,7 @@ export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
|
|
|
465
478
|
* ```
|
|
466
479
|
*/
|
|
467
480
|
export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<typeof RawOutput> {
|
|
481
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
468
482
|
const _input = input as PSBTInputs;
|
|
469
483
|
if (_input.nonWitnessUtxo) {
|
|
470
484
|
if (_input.index === undefined) throw new Error('Unknown input index');
|
|
@@ -478,8 +492,17 @@ export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<ty
|
|
|
478
492
|
)
|
|
479
493
|
throw new Error(`Wrong input index=${_input.index}`);
|
|
480
494
|
return _input.nonWitnessUtxo.outputs[_input.index];
|
|
481
|
-
} else if (
|
|
482
|
-
|
|
495
|
+
} else if ('witnessUtxo' in _input) {
|
|
496
|
+
// The presence check catches malformed provided values; narrow after the guard for TS.
|
|
497
|
+
const prev = _input.witnessUtxo as P.UnwrapCoder<typeof RawOutput>;
|
|
498
|
+
validateObject(prev as Record<string, any>, {}, {}, 'input.witnessUtxo');
|
|
499
|
+
abigint(prev.amount, 'input.witnessUtxo.amount');
|
|
500
|
+
if (!isBytes(prev.script))
|
|
501
|
+
throw new TypeError(
|
|
502
|
+
'"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script
|
|
503
|
+
);
|
|
504
|
+
return prev;
|
|
505
|
+
} else throw new Error('Cannot find previous output info');
|
|
483
506
|
}
|
|
484
507
|
|
|
485
508
|
/**
|
|
@@ -510,6 +533,9 @@ export function normalizeInput(
|
|
|
510
533
|
disableScriptCheck = false,
|
|
511
534
|
allowUnknown = false
|
|
512
535
|
): TRet<PSBTInputs> {
|
|
536
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
537
|
+
if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
|
|
538
|
+
if (allowedFields !== undefined) u.aarray(allowedFields, 'allowedFields');
|
|
513
539
|
const _i = i as psbt.TransactionInputUpdate;
|
|
514
540
|
const _cur = cur as PSBTInputs | undefined;
|
|
515
541
|
const _allowedFields = allowedFields as readonly (keyof PSBTInputs)[] | undefined;
|
|
@@ -718,6 +744,7 @@ export class Transaction {
|
|
|
718
744
|
toPSBT(
|
|
719
745
|
PSBTVersion: number | undefined = this.global.version || this.opts.PSBTVersion
|
|
720
746
|
): Uint8Array {
|
|
747
|
+
if (PSBTVersion !== undefined) anumber(PSBTVersion, 'PSBTVersion');
|
|
721
748
|
if (PSBTVersion !== 0 && PSBTVersion !== 2)
|
|
722
749
|
throw new Error(`Wrong PSBT version=${PSBTVersion}`);
|
|
723
750
|
// if (PSBTVersion === 0 && this.inputs.length === 0) {
|
|
@@ -878,27 +905,33 @@ export class Transaction {
|
|
|
878
905
|
|
|
879
906
|
// Info utils
|
|
880
907
|
get hasWitnesses(): boolean {
|
|
881
|
-
let out = false;
|
|
882
908
|
for (const i of this.inputs)
|
|
883
|
-
if (i.finalScriptWitness && i.finalScriptWitness.length)
|
|
884
|
-
return
|
|
909
|
+
if (i.finalScriptWitness && i.finalScriptWitness.length) return true;
|
|
910
|
+
return false;
|
|
885
911
|
}
|
|
886
912
|
// https://en.bitcoin.it/wiki/Weight_units
|
|
887
913
|
get weight(): number {
|
|
888
914
|
if (!this.isFinal) throw new Error('Transaction is not finalized');
|
|
915
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
916
|
+
const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
917
|
+
const hasWitnesses = this.hasWitnesses;
|
|
889
918
|
let out = 32;
|
|
890
919
|
// Outputs
|
|
891
920
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
892
921
|
out += 4 * CompactSizeLen.encode(this.outputs.length).length;
|
|
893
|
-
for (const o of outputs) out += 32 + 4 *
|
|
922
|
+
for (const o of outputs) out += 32 + 4 * varLen(o.script.length);
|
|
894
923
|
// Inputs
|
|
895
|
-
if (
|
|
924
|
+
if (hasWitnesses) out += 2;
|
|
896
925
|
out += 4 * CompactSizeLen.encode(this.inputs.length).length;
|
|
897
926
|
for (const i of this.inputs) {
|
|
898
|
-
out += 160 + 4 *
|
|
927
|
+
out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
|
|
899
928
|
// Once segwit serialization is active, every input contributes one witness vector, including
|
|
900
929
|
// legacy inputs whose empty vector still encodes as a single zero-item-count byte.
|
|
901
|
-
if (
|
|
930
|
+
if (hasWitnesses) {
|
|
931
|
+
const witness = i.finalScriptWitness || [];
|
|
932
|
+
out += CompactSizeLen.encode(witness.length).length;
|
|
933
|
+
for (const w of witness) out += varLen(w.length);
|
|
934
|
+
}
|
|
902
935
|
}
|
|
903
936
|
return out;
|
|
904
937
|
}
|
|
@@ -933,8 +966,8 @@ export class Transaction {
|
|
|
933
966
|
}
|
|
934
967
|
// Input stuff
|
|
935
968
|
private checkInputIdx(idx: number) {
|
|
936
|
-
|
|
937
|
-
|
|
969
|
+
anumber(idx, 'idx');
|
|
970
|
+
if (idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`);
|
|
938
971
|
}
|
|
939
972
|
getInput(idx: number): psbt.TransactionInput {
|
|
940
973
|
this.checkInputIdx(idx);
|
|
@@ -945,6 +978,7 @@ export class Transaction {
|
|
|
945
978
|
}
|
|
946
979
|
// Modification
|
|
947
980
|
addInput(input: TArg<psbt.TransactionInputUpdate>, _ignoreSignStatus = false): number {
|
|
981
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
948
982
|
if (!_ignoreSignStatus && !this.signStatus().addInput)
|
|
949
983
|
throw new Error('Tx has signed inputs, cannot add new one');
|
|
950
984
|
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
@@ -982,8 +1016,8 @@ export class Transaction {
|
|
|
982
1016
|
}
|
|
983
1017
|
// Output stuff
|
|
984
1018
|
private checkOutputIdx(idx: number) {
|
|
985
|
-
|
|
986
|
-
|
|
1019
|
+
anumber(idx, 'idx');
|
|
1020
|
+
if (idx >= this.outputs.length) throw new Error(`Wrong output index=${idx}`);
|
|
987
1021
|
}
|
|
988
1022
|
getOutput(idx: number): psbt.TransactionOutput {
|
|
989
1023
|
this.checkOutputIdx(idx);
|
|
@@ -1005,12 +1039,10 @@ export class Transaction {
|
|
|
1005
1039
|
cur?: PSBTOutputs,
|
|
1006
1040
|
allowedFields?: readonly (keyof typeof psbt.PSBTOutput)[]
|
|
1007
1041
|
): PSBTOutputs {
|
|
1042
|
+
validateObject(o as Record<string, any>, {}, {}, 'o');
|
|
1008
1043
|
let { amount, script } = o;
|
|
1009
1044
|
if (amount === undefined) amount = cur?.amount;
|
|
1010
|
-
|
|
1011
|
-
throw new Error(
|
|
1012
|
-
`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`
|
|
1013
|
-
);
|
|
1045
|
+
amount = abigint(amount, 'o.amount');
|
|
1014
1046
|
if (typeof script === 'string') script = hex.decode(script);
|
|
1015
1047
|
if (script === undefined) script = cur?.script;
|
|
1016
1048
|
let res: PSBTOutputs = { ...cur, ...(o as PSBTOutputs & { script?: string }), amount, script };
|
|
@@ -1065,7 +1097,7 @@ export class Transaction {
|
|
|
1065
1097
|
}
|
|
1066
1098
|
// Utils
|
|
1067
1099
|
get fee(): bigint {
|
|
1068
|
-
let res =
|
|
1100
|
+
let res = _0n;
|
|
1069
1101
|
for (const i of this.inputs) {
|
|
1070
1102
|
const prevOut = getPrevOut(i);
|
|
1071
1103
|
if (!prevOut) throw new Error('Empty input amount');
|
|
@@ -1084,7 +1116,7 @@ export class Transaction {
|
|
|
1084
1116
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
1085
1117
|
if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
|
|
1086
1118
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
1087
|
-
return P.U256BE.encode(
|
|
1119
|
+
return P.U256BE.encode(_1n);
|
|
1088
1120
|
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
1089
1121
|
let inputs: TransactionInputRequired[] = this.inputs
|
|
1090
1122
|
.map(inputBeforeSign)
|
|
@@ -1124,8 +1156,8 @@ export class Transaction {
|
|
|
1124
1156
|
): Uint8Array {
|
|
1125
1157
|
// BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
|
|
1126
1158
|
// nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
|
|
1127
|
-
|
|
1128
|
-
|
|
1159
|
+
anumber(idx, 'idx');
|
|
1160
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
1129
1161
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
1130
1162
|
let inputHash = EMPTY32;
|
|
1131
1163
|
let sequenceHash = EMPTY32;
|
|
@@ -1164,15 +1196,16 @@ export class Transaction {
|
|
|
1164
1196
|
leafVer = 0xc0,
|
|
1165
1197
|
annex?: Bytes
|
|
1166
1198
|
): Uint8Array {
|
|
1167
|
-
if (!Array.isArray(amount) || this.inputs.length !== amount.length)
|
|
1168
|
-
throw new Error(`Invalid amounts array=${amount}`);
|
|
1169
|
-
if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
|
|
1170
|
-
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
1171
1199
|
// BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
|
|
1172
1200
|
// sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
|
|
1173
1201
|
// nonexistent input or leaking a later integer-encoding RangeError for negative idx.
|
|
1174
|
-
|
|
1175
|
-
|
|
1202
|
+
anumber(idx, 'idx');
|
|
1203
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
1204
|
+
u.aarray(amount, 'amount');
|
|
1205
|
+
u.aarray(prevOutScript, 'prevOutScript');
|
|
1206
|
+
if (this.inputs.length !== amount.length) throw new Error(`Invalid amounts array=${amount}`);
|
|
1207
|
+
if (this.inputs.length !== prevOutScript.length)
|
|
1208
|
+
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
1176
1209
|
const out: Bytes[] = [
|
|
1177
1210
|
P.U8.encode(0),
|
|
1178
1211
|
P.U8.encode(hashType), // U8 sigHash
|
|
@@ -1216,6 +1249,18 @@ export class Transaction {
|
|
|
1216
1249
|
}
|
|
1217
1250
|
// Signer can be privateKey OR instance of bip32 HD stuff
|
|
1218
1251
|
signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
|
|
1252
|
+
if (!isBytes(privateKey)) {
|
|
1253
|
+
// HDKey is a structural external instance, so plain-object validation would
|
|
1254
|
+
// reject valid signers.
|
|
1255
|
+
if (
|
|
1256
|
+
!privateKey ||
|
|
1257
|
+
typeof privateKey !== 'object' ||
|
|
1258
|
+
typeof (privateKey as HDKey).deriveChild !== 'function'
|
|
1259
|
+
)
|
|
1260
|
+
throw new TypeError(
|
|
1261
|
+
'"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1219
1264
|
this.checkInputIdx(idx);
|
|
1220
1265
|
const input = this.inputs[idx];
|
|
1221
1266
|
const inputType = getInputType(
|
|
@@ -1438,7 +1483,7 @@ export class Transaction {
|
|
|
1438
1483
|
|
|
1439
1484
|
finalizeIdx(idx: number): void {
|
|
1440
1485
|
this.checkInputIdx(idx);
|
|
1441
|
-
if (this.fee <
|
|
1486
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1442
1487
|
const input = this.inputs[idx];
|
|
1443
1488
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1444
1489
|
// Taproot finalize
|
|
@@ -1592,10 +1637,12 @@ export class Transaction {
|
|
|
1592
1637
|
extract(): Uint8Array {
|
|
1593
1638
|
if (!this.isFinal) throw new Error('Transaction has unfinalized inputs');
|
|
1594
1639
|
if (!this.outputs.length) throw new Error('Transaction has no outputs');
|
|
1595
|
-
if (this.fee <
|
|
1640
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1596
1641
|
return this.toBytes(true, true);
|
|
1597
1642
|
}
|
|
1598
1643
|
combine(other: Transaction): this {
|
|
1644
|
+
if (!(other instanceof Transaction))
|
|
1645
|
+
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
1599
1646
|
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1600
1647
|
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1601
1648
|
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
package/src/utils.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ripemd160 } from '@noble/hashes/legacy.js';
|
|
|
4
4
|
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
|
5
5
|
import { type TArg, type TRet } from '@noble/hashes/utils.js';
|
|
6
6
|
import { utils as packedUtils, U32LE } from 'micro-packed';
|
|
7
|
+
export { abytes, validateObject as vld } from '@noble/curves/utils.js';
|
|
7
8
|
export { type TArg, type TRet } from '@noble/hashes/utils.js';
|
|
8
9
|
|
|
9
10
|
/** Hex-like input accepted by helpers in this module. */
|
|
@@ -11,9 +12,71 @@ export type Hex = string | Uint8Array;
|
|
|
11
12
|
/** Byte array alias used across the library. */
|
|
12
13
|
export type Bytes = Uint8Array;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a value is a non-negative bigint.
|
|
17
|
+
* @param n - Value to validate.
|
|
18
|
+
* @param title - Label included in thrown errors.
|
|
19
|
+
* @returns The same bigint.
|
|
20
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
21
|
+
* @example
|
|
22
|
+
* Validate a satoshi amount before transaction encoding.
|
|
23
|
+
* ```ts
|
|
24
|
+
* abigint(1n, 'amount');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function abigint(n: unknown, title: string = 'value'): bigint {
|
|
28
|
+
if (typeof n !== 'bigint')
|
|
29
|
+
throw new TypeError(`"${title}" expected bigint, got type=${typeof n}`);
|
|
30
|
+
if (n < _0n) throw new RangeError(`"${title}" expected non-negative bigint, got ${n}`);
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
import { validateObject as vld } from '@noble/curves/utils.js';
|
|
35
|
+
|
|
36
|
+
export function aarray<T>(
|
|
37
|
+
item: unknown,
|
|
38
|
+
title: string,
|
|
39
|
+
inner: (elm: T, title: string) => void = () => {}
|
|
40
|
+
): T[] {
|
|
41
|
+
if (!Array.isArray(item))
|
|
42
|
+
throw new TypeError(`"${title}" expected array, got type=${typeof item}`);
|
|
43
|
+
for (let i = 0; i < item.length; i++) inner(item[i], `${title}[${i}]`);
|
|
44
|
+
return item;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Asserts something is a string.
|
|
48
|
+
* @param value - Value to validate.
|
|
49
|
+
* @param title - Label included in thrown errors.
|
|
50
|
+
* @returns The validated string.
|
|
51
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
52
|
+
* @example
|
|
53
|
+
* Validate a label string.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* astring('example', 'label');
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function astring(value: unknown, title: string = ''): string {
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
const prefix = title && `"${title}" `;
|
|
62
|
+
throw new TypeError(prefix + 'expected string, got type=' + typeof value);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
export function validateObject(
|
|
67
|
+
object: Record<string, any>,
|
|
68
|
+
fields: Record<string, string> = {},
|
|
69
|
+
optFields: Record<string, string> = {},
|
|
70
|
+
_title = 'object'
|
|
71
|
+
) {
|
|
72
|
+
return vld(object, fields, optFields);
|
|
73
|
+
}
|
|
14
74
|
const Point = /* @__PURE__ */ (() => secp.Point)();
|
|
15
75
|
const Fn = /* @__PURE__ */ (() => Point.Fn)();
|
|
16
76
|
const CURVE_ORDER = /* @__PURE__ */ (() => Point.Fn.ORDER)();
|
|
77
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
78
|
+
// prettier-ignore
|
|
79
|
+
const _0n = /* @__PURE__ */ BigInt(0), _2n = /* @__PURE__ */ BigInt(2);
|
|
17
80
|
/**
|
|
18
81
|
* Checks whether a curve y-coordinate is even.
|
|
19
82
|
* @param y - y-coordinate to inspect
|
|
@@ -24,7 +87,7 @@ const CURVE_ORDER = /* @__PURE__ */ (() => Point.Fn.ORDER)();
|
|
|
24
87
|
* hasEven(2n);
|
|
25
88
|
* ```
|
|
26
89
|
*/
|
|
27
|
-
export const hasEven = (y: bigint) => y %
|
|
90
|
+
export const hasEven = (y: bigint) => y % _2n === _0n;
|
|
28
91
|
|
|
29
92
|
/**
|
|
30
93
|
* Checks whether a value is a Uint8Array.
|
|
@@ -144,7 +207,10 @@ export const pubECDSA = (privateKey: TArg<Uint8Array>, isCompressed?: boolean):
|
|
|
144
207
|
// noble/secp256k1 does not support the feature: it is not used outside of BTC.
|
|
145
208
|
// We implement it manually, because in BTC it's common.
|
|
146
209
|
// Not best way, but closest to bitcoin implementation (easier to check)
|
|
147
|
-
|
|
210
|
+
// Hoisted: the bound is constant; no need to redo the bigint division on every
|
|
211
|
+
// grinding-loop iteration. n/2 < 2^255, so r < n/2 guarantees the 32-byte DER r.
|
|
212
|
+
const LOW_R_BOUND = /* @__PURE__ */ (() => CURVE_ORDER / _2n)();
|
|
213
|
+
const hasLowR = (sig: { r: bigint; s: bigint }) => sig.r < LOW_R_BOUND;
|
|
148
214
|
/**
|
|
149
215
|
* Signs a 32-byte hash with ECDSA and returns DER encoding.
|
|
150
216
|
* @param hash - message hash to sign
|
package/src/utxo.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { hex } from '@scure/base';
|
|
|
2
2
|
import * as P from 'micro-packed';
|
|
3
3
|
import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
|
|
4
4
|
import * as psbt from './psbt.ts';
|
|
5
|
-
import { CompactSizeLen,
|
|
5
|
+
import { CompactSizeLen, Script } from './script.ts';
|
|
6
6
|
import {
|
|
7
7
|
SignatureHash,
|
|
8
8
|
Transaction,
|
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
toVsize,
|
|
15
15
|
} from './transaction.ts';
|
|
16
16
|
import {
|
|
17
|
+
abigint,
|
|
18
|
+
aarray,
|
|
19
|
+
astring,
|
|
17
20
|
type Bytes,
|
|
18
21
|
NETWORK,
|
|
19
22
|
PubT,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
isBytes,
|
|
25
28
|
sha256,
|
|
26
29
|
validatePubkey,
|
|
30
|
+
validateObject,
|
|
27
31
|
} from './utils.ts';
|
|
28
32
|
|
|
29
33
|
// UTXO Select
|
|
@@ -41,6 +45,11 @@ export type Accumulated =
|
|
|
41
45
|
type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
|
|
42
46
|
type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
|
|
43
47
|
const encodeTapBlock = (item: TB) => psbt.TaprootControlBlock.encode(item);
|
|
48
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
49
|
+
// prettier-ignore
|
|
50
|
+
const _0n = /* @__PURE__ */ BigInt(0), _3n = /* @__PURE__ */ BigInt(3);
|
|
51
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
52
|
+
const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
44
53
|
|
|
45
54
|
function iterLeafs(
|
|
46
55
|
tapLeafScript: TArg<TapLeafScript>,
|
|
@@ -175,10 +184,11 @@ function estimateInput(
|
|
|
175
184
|
} else if (inputType.type.startsWith('wsh-')) {
|
|
176
185
|
} else if (inputType.txType !== 'segwit') script = inputScript;
|
|
177
186
|
}
|
|
178
|
-
let weight = 160 + 4 *
|
|
187
|
+
let weight = 160 + 4 * varLen(script.length);
|
|
179
188
|
let hasWitnesses = false;
|
|
180
189
|
if (witness) {
|
|
181
|
-
weight +=
|
|
190
|
+
weight += CompactSizeLen.encode(witness.length).length;
|
|
191
|
+
for (const w of witness) weight += varLen(w.length);
|
|
182
192
|
hasWitnesses = true;
|
|
183
193
|
}
|
|
184
194
|
return { weight, hasWitnesses };
|
|
@@ -189,8 +199,8 @@ export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
|
|
|
189
199
|
// Array.sort comparators must return a number, so normalize bigint comparisons to -1/0/1
|
|
190
200
|
// instead of coercing large differences through Number(...) and losing ordering precision.
|
|
191
201
|
const n = a - b;
|
|
192
|
-
if (n <
|
|
193
|
-
else if (n >
|
|
202
|
+
if (n < _0n) return -1;
|
|
203
|
+
else if (n > _0n) return 1;
|
|
194
204
|
return 0;
|
|
195
205
|
};
|
|
196
206
|
|
|
@@ -212,6 +222,7 @@ export type EstimatorOpts = TxOpts & {
|
|
|
212
222
|
};
|
|
213
223
|
|
|
214
224
|
function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK) {
|
|
225
|
+
validateObject(o as Record<string, any>, {}, {}, 'output');
|
|
215
226
|
const _o = o as Output;
|
|
216
227
|
const _opts = opts as TxOpts;
|
|
217
228
|
let script;
|
|
@@ -219,8 +230,7 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
|
|
|
219
230
|
script = _o.script;
|
|
220
231
|
}
|
|
221
232
|
if ('address' in _o) {
|
|
222
|
-
|
|
223
|
-
throw new Error(`Estimator: wrong output address=${_o.address}`);
|
|
233
|
+
astring(_o.address, 'output.address');
|
|
224
234
|
// Address.decode() only yields known descriptors for valid output addresses, but the wrapped
|
|
225
235
|
// coder type still includes `undefined`, so narrow before re-encoding the script template.
|
|
226
236
|
script = OutScript.encode(
|
|
@@ -228,15 +238,9 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
|
|
|
228
238
|
);
|
|
229
239
|
}
|
|
230
240
|
if (!script) throw new Error('Estimator: wrong output script');
|
|
231
|
-
if (typeof _o.amount !== 'bigint')
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Estimator: wrong output amount=${
|
|
234
|
-
_o.amount
|
|
235
|
-
}, should be of type bigint but got ${typeof _o.amount}.`
|
|
236
|
-
);
|
|
237
241
|
// Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
|
|
238
242
|
// satoshi-denominated outputs are not allowed to go negative.
|
|
239
|
-
|
|
243
|
+
abigint(_o.amount, 'output.amount');
|
|
240
244
|
if (script && !_opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
|
|
241
245
|
throw new Error(
|
|
242
246
|
'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
|
|
@@ -282,16 +286,9 @@ export class _Estimator {
|
|
|
282
286
|
constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
|
|
283
287
|
this.outputs = outputs;
|
|
284
288
|
this.opts = opts;
|
|
285
|
-
if (typeof opts.feePerByte !== 'bigint')
|
|
286
|
-
throw new Error(
|
|
287
|
-
`Estimator: wrong feePerByte=${
|
|
288
|
-
opts.feePerByte
|
|
289
|
-
}, should be of type bigint but got ${typeof opts.feePerByte}.`
|
|
290
|
-
);
|
|
291
289
|
// Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
|
|
292
290
|
// `getSatoshi(...)` produce nonsensical negative fees throughout selection.
|
|
293
|
-
|
|
294
|
-
throw new Error(`Estimator: feePerByte must be >= 0 satoshi per vbyte`);
|
|
291
|
+
abigint(opts.feePerByte, 'opts.feePerByte');
|
|
295
292
|
// Dust stuff
|
|
296
293
|
// TODO: think about this more:
|
|
297
294
|
// - current dust filters tx which cannot be relayed by core
|
|
@@ -304,46 +301,37 @@ export class _Estimator {
|
|
|
304
301
|
const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
|
|
305
302
|
const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
|
|
306
303
|
const dustBytes = opts.dust === undefined ? BigInt(inputsDust + outputDust) : opts.dust;
|
|
307
|
-
|
|
308
|
-
throw new Error(
|
|
309
|
-
`Estimator: wrong dust=${opts.dust}, should be of type bigint but got ${typeof opts.dust}.`
|
|
310
|
-
);
|
|
311
|
-
}
|
|
304
|
+
abigint(dustBytes, 'opts.dust');
|
|
312
305
|
// 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
|
|
313
306
|
// 3000 sat/kvb -> 3 sat/vb.
|
|
314
307
|
// https://github.com/bitcoin/bitcoin/blob/27a770b34b8f1dbb84760f442edb3e23a0c2420b/src/policy/policy.h#L55
|
|
315
|
-
const dustFee = opts.dustRelayFeeRate === undefined ?
|
|
316
|
-
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Estimator: wrong dustRelayFeeRate=${opts.dustRelayFeeRate}, should be of type bigint but got ${typeof opts.dustRelayFeeRate}.`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
308
|
+
const dustFee = opts.dustRelayFeeRate === undefined ? _3n : opts.dustRelayFeeRate;
|
|
309
|
+
abigint(dustFee, 'opts.dustRelayFeeRate');
|
|
321
310
|
// Dust uses feePerbyte by default, but we allow separate dust fee if needed
|
|
322
311
|
this.dust = dustBytes * dustFee;
|
|
323
312
|
if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
|
|
324
313
|
throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
|
|
325
314
|
const network = opts.network || NETWORK;
|
|
326
|
-
let amount =
|
|
315
|
+
let amount = _0n;
|
|
327
316
|
// Base weight: tx with outputs, no inputs
|
|
328
317
|
let baseWeight = 32;
|
|
329
318
|
for (const o of outputs) {
|
|
330
319
|
const script = getScript(o, opts, opts.network);
|
|
331
|
-
baseWeight += 32 + 4 *
|
|
320
|
+
baseWeight += 32 + 4 * varLen(script.length);
|
|
332
321
|
amount += o.amount;
|
|
333
322
|
}
|
|
334
|
-
|
|
335
|
-
throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
|
|
323
|
+
astring(opts.changeAddress, 'opts.changeAddress');
|
|
336
324
|
let changeWeight =
|
|
337
325
|
baseWeight +
|
|
338
326
|
32 +
|
|
339
327
|
// Same Address.decode() narrowing as above: the estimator only reaches this path for a
|
|
340
328
|
// concrete change output address, not an unknown descriptor.
|
|
341
329
|
4 *
|
|
342
|
-
|
|
330
|
+
varLen(
|
|
343
331
|
OutScript.encode(
|
|
344
332
|
Address(network).decode(opts.changeAddress) as Parameters<typeof OutScript.encode>[0]
|
|
345
|
-
)
|
|
346
|
-
)
|
|
333
|
+
).length
|
|
334
|
+
);
|
|
347
335
|
baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
|
|
348
336
|
// If there a lot of outputs change can change fee
|
|
349
337
|
changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
|
|
@@ -441,7 +429,7 @@ export class _Estimator {
|
|
|
441
429
|
let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
|
|
442
430
|
let hasWitnesses = false;
|
|
443
431
|
let num = 0;
|
|
444
|
-
let inputsAmount =
|
|
432
|
+
let inputsAmount = _0n;
|
|
445
433
|
const targetAmount = this.amount;
|
|
446
434
|
const res: Set<number> = new Set();
|
|
447
435
|
let fee;
|
|
@@ -483,7 +471,7 @@ export class _Estimator {
|
|
|
483
471
|
// Negative: cost of using input is more than value provided (negative)
|
|
484
472
|
// By default 'blackjack' mode in coinselect doesn't use that, which means
|
|
485
473
|
// it will use negative output if sorted by 'smallest'
|
|
486
|
-
if (skipNegative && value <=
|
|
474
|
+
if (skipNegative && value <= _0n) continue;
|
|
487
475
|
weight = newWeight;
|
|
488
476
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
489
477
|
num = newNum;
|
|
@@ -495,6 +483,10 @@ export class _Estimator {
|
|
|
495
483
|
}
|
|
496
484
|
if (all) {
|
|
497
485
|
const total = getTotal(weight, num);
|
|
486
|
+
// 'all' accumulates unconditionally, so sufficiency must be checked here; otherwise
|
|
487
|
+
// result() would report a negative fee (or throw its internal negative-change error).
|
|
488
|
+
// Insufficient funds are a selection failure, same as for accumulation strategies.
|
|
489
|
+
if (targetAmount + total.fee > inputsAmount) return undefined;
|
|
498
490
|
return {
|
|
499
491
|
indices: Array.from(res),
|
|
500
492
|
fee: total.fee,
|
|
@@ -572,7 +564,7 @@ export class _Estimator {
|
|
|
572
564
|
if (needChange) {
|
|
573
565
|
fee = changeFee;
|
|
574
566
|
// this shouldn't happen!
|
|
575
|
-
if (change <
|
|
567
|
+
if (change < _0n) throw new Error(`Estimator.result: negative change=${change}`);
|
|
576
568
|
outputs.push({ address: this.opts.changeAddress, amount: change });
|
|
577
569
|
}
|
|
578
570
|
if (this.opts.bip69) {
|
|
@@ -634,6 +626,10 @@ export function selectUTXO(
|
|
|
634
626
|
strategy: SelectionStrategy,
|
|
635
627
|
opts: TArg<EstimatorOpts>
|
|
636
628
|
) {
|
|
629
|
+
aarray(inputs, 'inputs');
|
|
630
|
+
aarray(outputs, 'outputs');
|
|
631
|
+
validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
632
|
+
astring(strategy, 'strategy');
|
|
637
633
|
// Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
|
|
638
634
|
const _opts = { createTx: true, bip69: true, ...(opts as EstimatorOpts) };
|
|
639
635
|
const est = new _Estimator(inputs as psbt.TransactionInputUpdate[], outputs as Output[], _opts);
|
package/transaction.d.ts
CHANGED