@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/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
|
|
@@ -345,10 +411,32 @@ export function taprootTweakPubkey(pubKey: TArg<Bytes>, h: TArg<Bytes>): TRet<[B
|
|
|
345
411
|
// This is the fixed BIP 341 H example, not the privacy-preserving H + rG variant.
|
|
346
412
|
// Downstream helpers use exact-byte equality with it to recognize
|
|
347
413
|
// library-generated script-only outputs.
|
|
348
|
-
|
|
349
|
-
export
|
|
414
|
+
// Keep the value used by library internals private: exported Uint8Arrays are mutable, so the
|
|
415
|
+
// public compatibility export below cannot safely be a source of cryptographic key material.
|
|
416
|
+
const INTERNAL_TAPROOT_NUMS: TRet<Bytes> = /* @__PURE__ */ (() =>
|
|
350
417
|
sha256(Point.BASE.toBytes(false)) as TRet<Bytes>)();
|
|
351
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Standard unspendable internal key used for script-only Taproot outputs.
|
|
421
|
+
* @deprecated Use {@link taprootNumsKey} to receive an owned copy.
|
|
422
|
+
*/
|
|
423
|
+
export const TAPROOT_UNSPENDABLE_KEY: TRet<Bytes> = /* @__PURE__ */ (() =>
|
|
424
|
+
Uint8Array.from(INTERNAL_TAPROOT_NUMS) as TRet<Bytes>)();
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Returns an owned copy of the library's stable Taproot NUMS key.
|
|
428
|
+
* @returns A new 32-byte NUMS key copy.
|
|
429
|
+
* @example
|
|
430
|
+
* Obtain an internal key without sharing mutable exported storage.
|
|
431
|
+
* ```ts
|
|
432
|
+
* import { taprootNumsKey } from '@scure/btc-signer/utils.js';
|
|
433
|
+
* const internalKey = taprootNumsKey();
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export function taprootNumsKey(): TRet<Bytes> {
|
|
437
|
+
return Uint8Array.from(INTERNAL_TAPROOT_NUMS) as TRet<Bytes>;
|
|
438
|
+
}
|
|
439
|
+
|
|
352
440
|
/** Bitcoin network parameters. */
|
|
353
441
|
export type BTC_NETWORK = {
|
|
354
442
|
/** Human-readable prefix used by Bech32 and Bech32m addresses. */
|
package/src/utxo.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
2
|
import * as P from 'micro-packed';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Address,
|
|
5
|
+
type CustomScript,
|
|
6
|
+
OutScript,
|
|
7
|
+
_WitnessOutScript,
|
|
8
|
+
checkScript,
|
|
9
|
+
tapLeafHash,
|
|
10
|
+
} from './payment.ts';
|
|
4
11
|
import * as psbt from './psbt.ts';
|
|
5
|
-
import { CompactSizeLen, RawWitness, Script
|
|
12
|
+
import { CompactSizeLen, RawWitness, Script } from './script.ts';
|
|
6
13
|
import {
|
|
7
14
|
SignatureHash,
|
|
8
15
|
Transaction,
|
|
@@ -14,16 +21,22 @@ import {
|
|
|
14
21
|
toVsize,
|
|
15
22
|
} from './transaction.ts';
|
|
16
23
|
import {
|
|
24
|
+
abigint,
|
|
25
|
+
aarray,
|
|
26
|
+
astring,
|
|
17
27
|
type Bytes,
|
|
18
28
|
NETWORK,
|
|
19
29
|
PubT,
|
|
20
|
-
TAPROOT_UNSPENDABLE_KEY,
|
|
21
30
|
type TArg,
|
|
31
|
+
type TRet,
|
|
22
32
|
compareBytes,
|
|
23
33
|
equalBytes,
|
|
24
34
|
isBytes,
|
|
25
35
|
sha256,
|
|
36
|
+
taprootNumsKey,
|
|
37
|
+
taprootTweakPubkey,
|
|
26
38
|
validatePubkey,
|
|
39
|
+
validateObject,
|
|
27
40
|
} from './utils.ts';
|
|
28
41
|
|
|
29
42
|
// UTXO Select
|
|
@@ -39,73 +52,174 @@ export type Accumulated =
|
|
|
39
52
|
}
|
|
40
53
|
| undefined;
|
|
41
54
|
type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
|
|
55
|
+
type TapLeaf = NonNullable<TapLeafScript>[number];
|
|
42
56
|
type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
|
|
43
57
|
const encodeTapBlock = (item: TB) => psbt.TaprootControlBlock.encode(item);
|
|
58
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
59
|
+
// prettier-ignore
|
|
60
|
+
const _0n = /* @__PURE__ */ BigInt(0), _3n = /* @__PURE__ */ BigInt(3);
|
|
61
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
62
|
+
const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
63
|
+
|
|
64
|
+
const tapLeafWitness = (
|
|
65
|
+
leaf: TArg<TapLeaf>,
|
|
66
|
+
sigSize: number,
|
|
67
|
+
customScripts?: TArg<CustomScript[]>,
|
|
68
|
+
pubkeys?: Set<string>,
|
|
69
|
+
unknownError = 'Finalize: Unknown tapLeafScript'
|
|
70
|
+
): TRet<Bytes[] | undefined> => {
|
|
71
|
+
const [cb, _script] = leaf as TapLeaf;
|
|
72
|
+
const _customScripts = customScripts as CustomScript[] | undefined;
|
|
73
|
+
// Last byte is version
|
|
74
|
+
const script = _script.slice(0, -1);
|
|
75
|
+
const ver = _script[_script.length - 1];
|
|
76
|
+
const outs = OutScript.decode(script);
|
|
77
|
+
const available = (pubkey: TArg<Bytes>) => !pubkeys || pubkeys.has(hex.encode(pubkey as Bytes));
|
|
78
|
+
const empty = () => new Uint8Array(sigSize);
|
|
79
|
+
let signatures: Bytes[] = [];
|
|
80
|
+
if (outs.type === 'tr_ms') {
|
|
81
|
+
let added = 0;
|
|
82
|
+
for (const pubkey of outs.pubkeys) {
|
|
83
|
+
if (added === outs.m || !available(pubkey)) signatures.push(P.EMPTY);
|
|
84
|
+
else {
|
|
85
|
+
signatures.push(empty());
|
|
86
|
+
added++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (added !== outs.m) return;
|
|
90
|
+
} else if (outs.type === 'tr_ns') {
|
|
91
|
+
for (const pubkey of outs.pubkeys) {
|
|
92
|
+
if (!available(pubkey)) return;
|
|
93
|
+
signatures.push(empty());
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (!_customScripts) throw new Error(unknownError);
|
|
97
|
+
const leafHash = tapLeafHash(script, ver);
|
|
98
|
+
const scriptDecoded = Script.decode(script);
|
|
99
|
+
const scriptPubkeys = scriptDecoded.filter((i) => {
|
|
100
|
+
if (!isBytes(i)) return false;
|
|
101
|
+
try {
|
|
102
|
+
validatePubkey(i, PubT.schnorr);
|
|
103
|
+
return true;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}) as Bytes[];
|
|
108
|
+
const availablePubkeys = scriptPubkeys.filter(available);
|
|
109
|
+
// A custom finalizer may treat an unexpected empty signature list as malformed input. If the
|
|
110
|
+
// script embeds keys but none are owned, the path is unavailable without consulting the hook.
|
|
111
|
+
if (pubkeys && scriptPubkeys.length && !availablePubkeys.length) return;
|
|
112
|
+
let recognized = false;
|
|
113
|
+
for (const c of _customScripts) {
|
|
114
|
+
if (!c.finalizeTaproot) continue;
|
|
115
|
+
const csEncoded = c.encode(scriptDecoded);
|
|
116
|
+
if (csEncoded === undefined) continue;
|
|
117
|
+
recognized = true;
|
|
118
|
+
const finalized = c.finalizeTaproot(
|
|
119
|
+
script,
|
|
120
|
+
csEncoded,
|
|
121
|
+
availablePubkeys.map((pubKey) => [{ pubKey, leafHash }, empty()])
|
|
122
|
+
);
|
|
123
|
+
if (finalized) return finalized.concat(encodeTapBlock(cb)) as TRet<Bytes[]>;
|
|
124
|
+
}
|
|
125
|
+
if (recognized && pubkeys) return;
|
|
126
|
+
throw new Error(unknownError);
|
|
127
|
+
}
|
|
128
|
+
// Witness is stack, so last element will be used first
|
|
129
|
+
return signatures.reverse().concat([script, encodeTapBlock(cb)]) as TRet<Bytes[]>;
|
|
130
|
+
};
|
|
44
131
|
|
|
45
132
|
function iterLeafs(
|
|
46
133
|
tapLeafScript: TArg<TapLeafScript>,
|
|
47
134
|
sigSize: number,
|
|
48
135
|
customScripts?: TArg<CustomScript[]>
|
|
49
|
-
) {
|
|
136
|
+
): TRet<Bytes[]> {
|
|
50
137
|
const _tapLeafScript = tapLeafScript as TapLeafScript;
|
|
51
138
|
const _customScripts = customScripts as CustomScript[] | undefined;
|
|
52
139
|
if (!_tapLeafScript || !_tapLeafScript.length) throw new Error('no leafs');
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
// Start with the old shallowest-path order for stable equal-weight ties. Full witness size can
|
|
141
|
+
// reverse that order when a shallow leaf needs a larger script or more signatures.
|
|
142
|
+
const leafs = _tapLeafScript
|
|
143
|
+
.slice()
|
|
144
|
+
.sort((a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length);
|
|
145
|
+
let smallest: Bytes[] | undefined;
|
|
146
|
+
let smallestSize = Number.POSITIVE_INFINITY;
|
|
147
|
+
for (const leaf of leafs) {
|
|
148
|
+
const witness = tapLeafWitness(leaf, sigSize, _customScripts);
|
|
149
|
+
if (!witness) continue;
|
|
150
|
+
const size = RawWitness.encode(witness).length;
|
|
151
|
+
if (size >= smallestSize) continue;
|
|
152
|
+
smallest = witness;
|
|
153
|
+
smallestSize = size;
|
|
154
|
+
}
|
|
155
|
+
if (!smallest) throw new Error('there was no witness');
|
|
156
|
+
return smallest as TRet<Bytes[]>;
|
|
157
|
+
}
|
|
67
158
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Removes Taproot spend paths that cannot be satisfied by the supplied Schnorr public keys.
|
|
161
|
+
* Non-Taproot inputs are retained, and caller-owned input metadata is never mutated.
|
|
162
|
+
* @param inputs - candidate PSBT input records to filter
|
|
163
|
+
* @param pubkeys - available x-only Schnorr public keys
|
|
164
|
+
* @returns Copies of inputs that retain at least one available path
|
|
165
|
+
* @example
|
|
166
|
+
* Filter wallet UTXOs before selecting coins.
|
|
167
|
+
* ```ts
|
|
168
|
+
* import { filterTaproot } from '@scure/btc-signer/utxo.js';
|
|
169
|
+
* import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
170
|
+
* const spendable = filterTaproot([], [pubSchnorr(randomPrivateKeyBytes())]);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function filterTaproot(
|
|
174
|
+
inputs: TArg<psbt.TransactionInputUpdate[]>,
|
|
175
|
+
pubkeys: TArg<Bytes[]>
|
|
176
|
+
): TRet<psbt.TransactionInputUpdate[]> {
|
|
177
|
+
aarray(inputs, 'inputs');
|
|
178
|
+
aarray(pubkeys, 'pubkeys');
|
|
179
|
+
const _inputs = inputs as psbt.TransactionInputUpdate[];
|
|
180
|
+
const _pubkeys = pubkeys as Bytes[];
|
|
181
|
+
const keys = new Set<string>();
|
|
182
|
+
for (const pubkey of _pubkeys) keys.add(hex.encode(validatePubkey(pubkey, PubT.schnorr)));
|
|
183
|
+
const res: psbt.TransactionInputUpdate[] = [];
|
|
184
|
+
for (const input of _inputs) {
|
|
185
|
+
const filtered = { ...input };
|
|
186
|
+
// BIP371 path fields are optional and empty keyed lists encode as absence, so the committed
|
|
187
|
+
// previous-output script is the authoritative input type.
|
|
188
|
+
const prevScript = _WitnessOutScript.decode(
|
|
189
|
+
getPrevOut(filtered as TArg<psbt.TransactionInput>).script
|
|
190
|
+
);
|
|
191
|
+
if (prevScript.type !== 'tr') {
|
|
192
|
+
res.push(filtered);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (filtered.tapInternalKey) {
|
|
196
|
+
// A control block can reconstruct the root, but BIP371 does not require that inference.
|
|
197
|
+
// Omission may be broken data or an earlier filter deliberately disabling the key path.
|
|
198
|
+
// Recognize root omission only when the prevout proves this is an empty-tree commitment.
|
|
199
|
+
const hasRoot =
|
|
200
|
+
filtered.tapMerkleRoot !== undefined ||
|
|
201
|
+
equalBytes(taprootTweakPubkey(filtered.tapInternalKey, P.EMPTY)[0], prevScript.pubkey);
|
|
202
|
+
if (
|
|
203
|
+
!hasRoot ||
|
|
204
|
+
equalBytes(filtered.tapInternalKey, taprootNumsKey()) ||
|
|
205
|
+
(!keys.has(hex.encode(filtered.tapInternalKey)) && !keys.has(hex.encode(prevScript.pubkey)))
|
|
206
|
+
)
|
|
207
|
+
delete filtered.tapInternalKey;
|
|
208
|
+
}
|
|
209
|
+
if (filtered.tapLeafScript) {
|
|
210
|
+
const sigSize =
|
|
211
|
+
filtered.sighashType !== undefined && filtered.sighashType !== SignatureHash.DEFAULT
|
|
212
|
+
? 65
|
|
213
|
+
: 64;
|
|
214
|
+
const leafs = filtered.tapLeafScript.filter((leaf) =>
|
|
215
|
+
tapLeafWitness(leaf, sigSize, undefined, keys, 'filterTaproot: unknown Taproot leaf')
|
|
216
|
+
);
|
|
217
|
+
if (leafs.length) filtered.tapLeafScript = leafs;
|
|
218
|
+
else delete filtered.tapLeafScript;
|
|
104
219
|
}
|
|
105
|
-
|
|
106
|
-
return signatures.reverse().concat([script, encodeTapBlock(cb)]);
|
|
220
|
+
if (filtered.tapInternalKey || filtered.tapLeafScript) res.push(filtered);
|
|
107
221
|
}
|
|
108
|
-
|
|
222
|
+
return res as TRet<psbt.TransactionInputUpdate[]>;
|
|
109
223
|
}
|
|
110
224
|
|
|
111
225
|
function estimateInput(
|
|
@@ -121,13 +235,9 @@ function estimateInput(
|
|
|
121
235
|
// schnorr sig is always 64 bytes. except for cases when sighash is not default!
|
|
122
236
|
if (inputType.txType === 'taproot') {
|
|
123
237
|
const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// on the online side. Callers that know only script-path signing is
|
|
128
|
-
// possible should omit `tapInternalKey` or pre-filter `tapLeafScript`
|
|
129
|
-
// before estimation.
|
|
130
|
-
if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
|
|
238
|
+
// A real internal key and every retained leaf declare paths available to this caller. Use
|
|
239
|
+
// filterTaproot before estimation when the supplied input contains unavailable paths.
|
|
240
|
+
if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, taprootNumsKey())) {
|
|
131
241
|
witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
|
|
132
242
|
} else if (_input.tapLeafScript) {
|
|
133
243
|
witness = iterLeafs(_input.tapLeafScript, SCHNORR_SIG_SIZE, _opts.customScripts);
|
|
@@ -175,10 +285,11 @@ function estimateInput(
|
|
|
175
285
|
} else if (inputType.type.startsWith('wsh-')) {
|
|
176
286
|
} else if (inputType.txType !== 'segwit') script = inputScript;
|
|
177
287
|
}
|
|
178
|
-
let weight = 160 + 4 *
|
|
288
|
+
let weight = 160 + 4 * varLen(script.length);
|
|
179
289
|
let hasWitnesses = false;
|
|
180
290
|
if (witness) {
|
|
181
|
-
weight +=
|
|
291
|
+
weight += CompactSizeLen.encode(witness.length).length;
|
|
292
|
+
for (const w of witness) weight += varLen(w.length);
|
|
182
293
|
hasWitnesses = true;
|
|
183
294
|
}
|
|
184
295
|
return { weight, hasWitnesses };
|
|
@@ -189,8 +300,8 @@ export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
|
|
|
189
300
|
// Array.sort comparators must return a number, so normalize bigint comparisons to -1/0/1
|
|
190
301
|
// instead of coercing large differences through Number(...) and losing ordering precision.
|
|
191
302
|
const n = a - b;
|
|
192
|
-
if (n <
|
|
193
|
-
else if (n >
|
|
303
|
+
if (n < _0n) return -1;
|
|
304
|
+
else if (n > _0n) return 1;
|
|
194
305
|
return 0;
|
|
195
306
|
};
|
|
196
307
|
|
|
@@ -209,9 +320,12 @@ export type EstimatorOpts = TxOpts & {
|
|
|
209
320
|
createTx?: boolean; // Create tx inside selection
|
|
210
321
|
requiredInputs?: psbt.TransactionInputUpdate[]; // these inputs always will be used
|
|
211
322
|
allowSameUtxo?: boolean; // allow using UTXO multiple times (for test purposes)
|
|
323
|
+
/** Filter Taproot candidate paths to those satisfiable by these Schnorr public keys. */
|
|
324
|
+
filterTaproot?: Bytes[];
|
|
212
325
|
};
|
|
213
326
|
|
|
214
327
|
function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK) {
|
|
328
|
+
validateObject(o as Record<string, any>, {}, {}, 'output');
|
|
215
329
|
const _o = o as Output;
|
|
216
330
|
const _opts = opts as TxOpts;
|
|
217
331
|
let script;
|
|
@@ -219,8 +333,7 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
|
|
|
219
333
|
script = _o.script;
|
|
220
334
|
}
|
|
221
335
|
if ('address' in _o) {
|
|
222
|
-
|
|
223
|
-
throw new Error(`Estimator: wrong output address=${_o.address}`);
|
|
336
|
+
astring(_o.address, 'output.address');
|
|
224
337
|
// Address.decode() only yields known descriptors for valid output addresses, but the wrapped
|
|
225
338
|
// coder type still includes `undefined`, so narrow before re-encoding the script template.
|
|
226
339
|
script = OutScript.encode(
|
|
@@ -228,16 +341,10 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
|
|
|
228
341
|
);
|
|
229
342
|
}
|
|
230
343
|
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
344
|
// Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
|
|
238
345
|
// satoshi-denominated outputs are not allowed to go negative.
|
|
239
|
-
|
|
240
|
-
if (script && !_opts.allowUnknownOutputs &&
|
|
346
|
+
abigint(_o.amount, 'output.amount');
|
|
347
|
+
if (script && !_opts.allowUnknownOutputs && _WitnessOutScript.decode(script).type === 'unknown') {
|
|
241
348
|
throw new Error(
|
|
242
349
|
'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
|
|
243
350
|
);
|
|
@@ -254,10 +361,7 @@ type AccumStrategy = `accum${SortStrategy}`;
|
|
|
254
361
|
|
|
255
362
|
/** Supported UTXO selection strategies. */
|
|
256
363
|
export type SelectionStrategy =
|
|
257
|
-
| '
|
|
258
|
-
| 'default'
|
|
259
|
-
| AccumStrategy
|
|
260
|
-
| `${ExactStrategy}/${AccumStrategy}`;
|
|
364
|
+
'all' | 'default' | AccumStrategy | `${ExactStrategy}/${AccumStrategy}`;
|
|
261
365
|
|
|
262
366
|
// class, because we need to re-use normalized inputs, instead of parsing each time
|
|
263
367
|
// internal stuff, exported for tests only
|
|
@@ -280,18 +384,14 @@ export class _Estimator {
|
|
|
280
384
|
private outputs: Output[];
|
|
281
385
|
private opts: EstimatorOpts;
|
|
282
386
|
constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
|
|
387
|
+
// EstimatorOpts extends TxOpts, so resolve the complete transaction policy once even when
|
|
388
|
+
// createTx=false. This keeps selection normalization and a returned Transaction identical.
|
|
389
|
+
opts = new Transaction(opts).opts as EstimatorOpts;
|
|
283
390
|
this.outputs = outputs;
|
|
284
391
|
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
392
|
// Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
|
|
292
393
|
// `getSatoshi(...)` produce nonsensical negative fees throughout selection.
|
|
293
|
-
|
|
294
|
-
throw new Error(`Estimator: feePerByte must be >= 0 satoshi per vbyte`);
|
|
394
|
+
abigint(opts.feePerByte, 'opts.feePerByte');
|
|
295
395
|
// Dust stuff
|
|
296
396
|
// TODO: think about this more:
|
|
297
397
|
// - current dust filters tx which cannot be relayed by core
|
|
@@ -304,46 +404,37 @@ export class _Estimator {
|
|
|
304
404
|
const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
|
|
305
405
|
const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
|
|
306
406
|
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
|
-
}
|
|
407
|
+
abigint(dustBytes, 'opts.dust');
|
|
312
408
|
// 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
|
|
313
409
|
// 3000 sat/kvb -> 3 sat/vb.
|
|
314
410
|
// 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
|
-
}
|
|
411
|
+
const dustFee = opts.dustRelayFeeRate === undefined ? _3n : opts.dustRelayFeeRate;
|
|
412
|
+
abigint(dustFee, 'opts.dustRelayFeeRate');
|
|
321
413
|
// Dust uses feePerbyte by default, but we allow separate dust fee if needed
|
|
322
414
|
this.dust = dustBytes * dustFee;
|
|
323
415
|
if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
|
|
324
416
|
throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
|
|
325
417
|
const network = opts.network || NETWORK;
|
|
326
|
-
let amount =
|
|
418
|
+
let amount = _0n;
|
|
327
419
|
// Base weight: tx with outputs, no inputs
|
|
328
420
|
let baseWeight = 32;
|
|
329
421
|
for (const o of outputs) {
|
|
330
422
|
const script = getScript(o, opts, opts.network);
|
|
331
|
-
baseWeight += 32 + 4 *
|
|
423
|
+
baseWeight += 32 + 4 * varLen(script.length);
|
|
332
424
|
amount += o.amount;
|
|
333
425
|
}
|
|
334
|
-
|
|
335
|
-
throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
|
|
426
|
+
astring(opts.changeAddress, 'opts.changeAddress');
|
|
336
427
|
let changeWeight =
|
|
337
428
|
baseWeight +
|
|
338
429
|
32 +
|
|
339
430
|
// Same Address.decode() narrowing as above: the estimator only reaches this path for a
|
|
340
431
|
// concrete change output address, not an unknown descriptor.
|
|
341
432
|
4 *
|
|
342
|
-
|
|
433
|
+
varLen(
|
|
343
434
|
OutScript.encode(
|
|
344
435
|
Address(network).decode(opts.changeAddress) as Parameters<typeof OutScript.encode>[0]
|
|
345
|
-
)
|
|
346
|
-
)
|
|
436
|
+
).length
|
|
437
|
+
);
|
|
347
438
|
baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
|
|
348
439
|
// If there a lot of outputs change can change fee
|
|
349
440
|
changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
|
|
@@ -362,7 +453,8 @@ export class _Estimator {
|
|
|
362
453
|
undefined,
|
|
363
454
|
undefined,
|
|
364
455
|
opts.disableScriptCheck,
|
|
365
|
-
opts.
|
|
456
|
+
opts.unknown!,
|
|
457
|
+
opts.proprietary!
|
|
366
458
|
);
|
|
367
459
|
inputBeforeSign(normalized as TArg<psbt.TransactionInput>); // check fields
|
|
368
460
|
const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
|
|
@@ -441,7 +533,7 @@ export class _Estimator {
|
|
|
441
533
|
let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
|
|
442
534
|
let hasWitnesses = false;
|
|
443
535
|
let num = 0;
|
|
444
|
-
let inputsAmount =
|
|
536
|
+
let inputsAmount = _0n;
|
|
445
537
|
const targetAmount = this.amount;
|
|
446
538
|
const res: Set<number> = new Set();
|
|
447
539
|
let fee;
|
|
@@ -483,7 +575,7 @@ export class _Estimator {
|
|
|
483
575
|
// Negative: cost of using input is more than value provided (negative)
|
|
484
576
|
// By default 'blackjack' mode in coinselect doesn't use that, which means
|
|
485
577
|
// it will use negative output if sorted by 'smallest'
|
|
486
|
-
if (skipNegative && value <=
|
|
578
|
+
if (skipNegative && value <= _0n) continue;
|
|
487
579
|
weight = newWeight;
|
|
488
580
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
489
581
|
num = newNum;
|
|
@@ -495,6 +587,10 @@ export class _Estimator {
|
|
|
495
587
|
}
|
|
496
588
|
if (all) {
|
|
497
589
|
const total = getTotal(weight, num);
|
|
590
|
+
// 'all' accumulates unconditionally, so sufficiency must be checked here; otherwise
|
|
591
|
+
// result() would report a negative fee (or throw its internal negative-change error).
|
|
592
|
+
// Insufficient funds are a selection failure, same as for accumulation strategies.
|
|
593
|
+
if (targetAmount + total.fee > inputsAmount) return undefined;
|
|
498
594
|
return {
|
|
499
595
|
indices: Array.from(res),
|
|
500
596
|
fee: total.fee,
|
|
@@ -572,7 +668,7 @@ export class _Estimator {
|
|
|
572
668
|
if (needChange) {
|
|
573
669
|
fee = changeFee;
|
|
574
670
|
// this shouldn't happen!
|
|
575
|
-
if (change <
|
|
671
|
+
if (change < _0n) throw new Error(`Estimator.result: negative change=${change}`);
|
|
576
672
|
outputs.push({ address: this.opts.changeAddress, amount: change });
|
|
577
673
|
}
|
|
578
674
|
if (this.opts.bip69) {
|
|
@@ -634,8 +730,22 @@ export function selectUTXO(
|
|
|
634
730
|
strategy: SelectionStrategy,
|
|
635
731
|
opts: TArg<EstimatorOpts>
|
|
636
732
|
) {
|
|
733
|
+
aarray(inputs, 'inputs');
|
|
734
|
+
aarray(outputs, 'outputs');
|
|
735
|
+
validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
736
|
+
astring(strategy, 'strategy');
|
|
637
737
|
// Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
|
|
638
738
|
const _opts = { createTx: true, bip69: true, ...(opts as EstimatorOpts) };
|
|
639
|
-
|
|
739
|
+
let candidates = inputs as psbt.TransactionInputUpdate[];
|
|
740
|
+
if (_opts.filterTaproot !== undefined) {
|
|
741
|
+
candidates = filterTaproot(candidates, _opts.filterTaproot);
|
|
742
|
+
if (_opts.requiredInputs) {
|
|
743
|
+
const requiredInputs = filterTaproot(_opts.requiredInputs, _opts.filterTaproot);
|
|
744
|
+
if (requiredInputs.length !== _opts.requiredInputs.length)
|
|
745
|
+
throw new Error('filterTaproot: required input has no available Taproot path');
|
|
746
|
+
_opts.requiredInputs = requiredInputs;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const est = new _Estimator(candidates, outputs as Output[], _opts);
|
|
640
750
|
return est.result(strategy);
|
|
641
751
|
}
|