@scure/btc-signer 2.0.1 → 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 +334 -64
- package/index.d.ts +15 -6
- package/index.js +16 -7
- package/musig2.d.ts +212 -69
- package/musig2.js +352 -99
- package/net.d.ts +355 -0
- package/net.js +875 -0
- package/p2p.d.ts +17 -8
- package/p2p.js +63 -11
- package/package.json +17 -17
- package/payment.d.ts +406 -41
- package/payment.js +570 -69
- package/psbt.d.ts +2958 -560
- package/psbt.js +475 -119
- package/script.d.ts +311 -133
- package/script.js +313 -90
- package/src/_type_test.ts +69 -0
- package/src/index.ts +34 -11
- package/src/musig2.ts +424 -155
- package/src/net.ts +1106 -0
- package/src/p2p.ts +76 -24
- package/src/payment.ts +882 -235
- package/src/psbt.ts +648 -229
- package/src/script.ts +397 -139
- package/src/transaction.ts +667 -196
- package/src/utils.ts +392 -47
- package/src/utxo.ts +182 -83
- package/transaction.d.ts +242 -32
- package/transaction.js +531 -121
- package/utils.d.ts +296 -25
- package/utils.js +337 -30
- package/utxo.d.ts +438 -76
- package/utxo.js +150 -59
- 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/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,19 +14,26 @@ 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,
|
|
20
23
|
TAPROOT_UNSPENDABLE_KEY,
|
|
24
|
+
type TArg,
|
|
21
25
|
compareBytes,
|
|
22
26
|
equalBytes,
|
|
23
27
|
isBytes,
|
|
24
28
|
sha256,
|
|
25
29
|
validatePubkey,
|
|
30
|
+
validateObject,
|
|
26
31
|
} from './utils.ts';
|
|
27
32
|
|
|
28
33
|
// UTXO Select
|
|
34
|
+
/** Minimal output target accepted by the UTXO selector. */
|
|
29
35
|
export type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint };
|
|
36
|
+
/** Intermediate accumulation result returned by selection heuristics. */
|
|
30
37
|
export type Accumulated =
|
|
31
38
|
| {
|
|
32
39
|
indices: number[];
|
|
@@ -38,14 +45,27 @@ export type Accumulated =
|
|
|
38
45
|
type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
|
|
39
46
|
type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
|
|
40
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;
|
|
41
53
|
|
|
42
|
-
function iterLeafs(
|
|
43
|
-
|
|
54
|
+
function iterLeafs(
|
|
55
|
+
tapLeafScript: TArg<TapLeafScript>,
|
|
56
|
+
sigSize: number,
|
|
57
|
+
customScripts?: TArg<CustomScript[]>
|
|
58
|
+
) {
|
|
59
|
+
const _tapLeafScript = tapLeafScript as TapLeafScript;
|
|
60
|
+
const _customScripts = customScripts as CustomScript[] | undefined;
|
|
61
|
+
if (!_tapLeafScript || !_tapLeafScript.length) throw new Error('no leafs');
|
|
62
|
+
// Dummy non-empty Schnorr signature bytes for weight estimation.
|
|
63
|
+
// Unsigned tr_ms slots use P.EMPTY below.
|
|
44
64
|
const empty = () => new Uint8Array(sigSize);
|
|
45
65
|
// If user want to select specific leaf, which can signed,
|
|
46
66
|
// it is possible to remove all other leafs manually.
|
|
47
67
|
// Sort leafs by control block length.
|
|
48
|
-
const leafs =
|
|
68
|
+
const leafs = _tapLeafScript.sort(
|
|
49
69
|
(a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length
|
|
50
70
|
);
|
|
51
71
|
for (const [cb, _script] of leafs) {
|
|
@@ -63,9 +83,9 @@ function iterLeafs(tapLeafScript: TapLeafScript, sigSize: number, customScripts?
|
|
|
63
83
|
} else if (outs.type === 'tr_ns') {
|
|
64
84
|
for (const _pub of outs.pubkeys) signatures.push(empty());
|
|
65
85
|
} else {
|
|
66
|
-
if (!
|
|
86
|
+
if (!_customScripts) throw new Error('Finalize: Unknown tapLeafScript');
|
|
67
87
|
const leafHash = tapLeafHash(script, ver);
|
|
68
|
-
for (const c of
|
|
88
|
+
for (const c of _customScripts) {
|
|
69
89
|
if (!c.finalizeTaproot) continue;
|
|
70
90
|
const scriptDecoded = Script.decode(script);
|
|
71
91
|
const csEncoded = c.encode(scriptDecoded);
|
|
@@ -87,6 +107,9 @@ function iterLeafs(tapLeafScript: TapLeafScript, sigSize: number, customScripts?
|
|
|
87
107
|
if (!finalized) continue;
|
|
88
108
|
return finalized.concat(encodeTapBlock(cb));
|
|
89
109
|
}
|
|
110
|
+
// UTXO selection may run without the real signer/finalizer process. When no matching local
|
|
111
|
+
// finalizeTaproot hook exists, keep a minimal script-path witness lower bound here instead of
|
|
112
|
+
// failing selection; callers that need exact fee estimates must provide the matching hook.
|
|
90
113
|
}
|
|
91
114
|
// Witness is stack, so last element will be used first
|
|
92
115
|
return signatures.reverse().concat([script, encodeTapBlock(cb)]);
|
|
@@ -96,23 +119,32 @@ function iterLeafs(tapLeafScript: TapLeafScript, sigSize: number, customScripts?
|
|
|
96
119
|
|
|
97
120
|
function estimateInput(
|
|
98
121
|
inputType: ReturnType<typeof getInputType>,
|
|
99
|
-
input: psbt.TransactionInput
|
|
100
|
-
opts: TxOpts
|
|
122
|
+
input: TArg<psbt.TransactionInput>,
|
|
123
|
+
opts: TArg<TxOpts>
|
|
101
124
|
) {
|
|
125
|
+
const _input = input as psbt.TransactionInput;
|
|
126
|
+
const _opts = opts as TxOpts;
|
|
102
127
|
let script: Bytes = P.EMPTY;
|
|
103
128
|
let witness: Bytes[] | undefined;
|
|
104
129
|
|
|
105
130
|
// schnorr sig is always 64 bytes. except for cases when sighash is not default!
|
|
106
131
|
if (inputType.txType === 'taproot') {
|
|
107
132
|
const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
|
|
108
|
-
|
|
133
|
+
// BIP371 `PSBT_IN_TAP_INTERNAL_KEY` is signer metadata, but UTXO selection
|
|
134
|
+
// runs before signer availability is known. We intentionally treat a
|
|
135
|
+
// present internal key as a key-path hint here to avoid overestimating fees
|
|
136
|
+
// on the online side. Callers that know only script-path signing is
|
|
137
|
+
// possible should omit `tapInternalKey` or pre-filter `tapLeafScript`
|
|
138
|
+
// before estimation.
|
|
139
|
+
if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
|
|
109
140
|
witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
|
|
110
|
-
} else if (
|
|
111
|
-
witness = iterLeafs(
|
|
141
|
+
} else if (_input.tapLeafScript) {
|
|
142
|
+
witness = iterLeafs(_input.tapLeafScript, SCHNORR_SIG_SIZE, _opts.customScripts);
|
|
112
143
|
} else throw new Error('estimateInput/taproot: unknown input');
|
|
113
144
|
} else {
|
|
114
|
-
// It is possible to grind signatures until
|
|
115
|
-
//
|
|
145
|
+
// It is possible to grind signatures until they have minimal size, but
|
|
146
|
+
// that changes the fee by +N satoshi. It would make estimation exact, but
|
|
147
|
+
// is very hard for multisig because every signature would need to stay small.
|
|
116
148
|
const empty = () => new Uint8Array(72); // max size of sigs
|
|
117
149
|
const emptyPub = () => new Uint8Array(33); // size of pubkey
|
|
118
150
|
let inputScript = P.EMPTY;
|
|
@@ -131,7 +163,7 @@ function estimateInput(
|
|
|
131
163
|
} else if (ltype === 'wpkh') {
|
|
132
164
|
inputScript = P.EMPTY;
|
|
133
165
|
inputWitness = [empty(), emptyPub()];
|
|
134
|
-
} else if (ltype === 'unknown' && !
|
|
166
|
+
} else if (ltype === 'unknown' && !_opts.allowUnknownInputs)
|
|
135
167
|
throw new Error('Unknown inputs are not allowed');
|
|
136
168
|
if (inputType.type.includes('wsh-')) {
|
|
137
169
|
// P2WSH
|
|
@@ -152,10 +184,11 @@ function estimateInput(
|
|
|
152
184
|
} else if (inputType.type.startsWith('wsh-')) {
|
|
153
185
|
} else if (inputType.txType !== 'segwit') script = inputScript;
|
|
154
186
|
}
|
|
155
|
-
let weight = 160 + 4 *
|
|
187
|
+
let weight = 160 + 4 * varLen(script.length);
|
|
156
188
|
let hasWitnesses = false;
|
|
157
189
|
if (witness) {
|
|
158
|
-
weight +=
|
|
190
|
+
weight += CompactSizeLen.encode(witness.length).length;
|
|
191
|
+
for (const w of witness) weight += varLen(w.length);
|
|
159
192
|
hasWitnesses = true;
|
|
160
193
|
}
|
|
161
194
|
return { weight, hasWitnesses };
|
|
@@ -163,50 +196,57 @@ function estimateInput(
|
|
|
163
196
|
|
|
164
197
|
// Exported for tests, internal method
|
|
165
198
|
export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
|
|
199
|
+
// Array.sort comparators must return a number, so normalize bigint comparisons to -1/0/1
|
|
200
|
+
// instead of coercing large differences through Number(...) and losing ordering precision.
|
|
166
201
|
const n = a - b;
|
|
167
|
-
if (n <
|
|
168
|
-
else if (n >
|
|
202
|
+
if (n < _0n) return -1;
|
|
203
|
+
else if (n > _0n) return 1;
|
|
169
204
|
return 0;
|
|
170
205
|
};
|
|
171
206
|
|
|
207
|
+
/** Options for fee estimation and UTXO selection. */
|
|
172
208
|
export type EstimatorOpts = TxOpts & {
|
|
173
|
-
// NOTE:
|
|
209
|
+
// NOTE: feePerByte is an integer sat/vbyte bigint, so fractional rates are impossible here.
|
|
210
|
+
// Zero is useful on regtest/in tests, but negative rates are not supported.
|
|
174
211
|
feePerByte: bigint; // satoshi per vbyte
|
|
175
212
|
changeAddress: string; // address where change will be sent
|
|
176
213
|
// Optional
|
|
177
214
|
alwaysChange?: boolean; // always create change, even if less than dust threshold
|
|
178
215
|
bip69?: boolean; // https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
|
|
179
216
|
network?: typeof NETWORK;
|
|
180
|
-
dust?:
|
|
217
|
+
dust?: bigint; // how much vbytes considered dust?
|
|
181
218
|
dustRelayFeeRate?: bigint; // fee per dust byte (DUST_RELAY_TX_FEE)
|
|
182
219
|
createTx?: boolean; // Create tx inside selection
|
|
183
220
|
requiredInputs?: psbt.TransactionInputUpdate[]; // these inputs always will be used
|
|
184
221
|
allowSameUtxo?: boolean; // allow using UTXO multiple times (for test purposes)
|
|
185
222
|
};
|
|
186
223
|
|
|
187
|
-
function getScript(o: Output
|
|
224
|
+
function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK) {
|
|
225
|
+
validateObject(o as Record<string, any>, {}, {}, 'output');
|
|
226
|
+
const _o = o as Output;
|
|
227
|
+
const _opts = opts as TxOpts;
|
|
188
228
|
let script;
|
|
189
|
-
if ('script' in
|
|
190
|
-
script =
|
|
229
|
+
if ('script' in _o && isBytes(_o.script)) {
|
|
230
|
+
script = _o.script;
|
|
191
231
|
}
|
|
192
|
-
if ('address' in
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
script
|
|
232
|
+
if ('address' in _o) {
|
|
233
|
+
astring(_o.address, 'output.address');
|
|
234
|
+
// Address.decode() only yields known descriptors for valid output addresses, but the wrapped
|
|
235
|
+
// coder type still includes `undefined`, so narrow before re-encoding the script template.
|
|
236
|
+
script = OutScript.encode(
|
|
237
|
+
Address(network).decode(_o.address) as Parameters<typeof OutScript.encode>[0]
|
|
238
|
+
);
|
|
196
239
|
}
|
|
197
240
|
if (!script) throw new Error('Estimator: wrong output script');
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}, should be of type bigint but got ${typeof o.amount}.`
|
|
203
|
-
);
|
|
204
|
-
if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
|
|
241
|
+
// Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
|
|
242
|
+
// satoshi-denominated outputs are not allowed to go negative.
|
|
243
|
+
abigint(_o.amount, 'output.amount');
|
|
244
|
+
if (script && !_opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
|
|
205
245
|
throw new Error(
|
|
206
246
|
'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
|
|
207
247
|
);
|
|
208
248
|
}
|
|
209
|
-
if (!
|
|
249
|
+
if (!_opts.disableScriptCheck) checkScript(script);
|
|
210
250
|
return script;
|
|
211
251
|
}
|
|
212
252
|
|
|
@@ -216,6 +256,7 @@ type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
|
|
|
216
256
|
type ExactStrategy = `exact${SortStrategy}`;
|
|
217
257
|
type AccumStrategy = `accum${SortStrategy}`;
|
|
218
258
|
|
|
259
|
+
/** Supported UTXO selection strategies. */
|
|
219
260
|
export type SelectionStrategy =
|
|
220
261
|
| 'all'
|
|
221
262
|
| 'default'
|
|
@@ -245,12 +286,9 @@ export class _Estimator {
|
|
|
245
286
|
constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
|
|
246
287
|
this.outputs = outputs;
|
|
247
288
|
this.opts = opts;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
opts.feePerByte
|
|
252
|
-
}, should be of type bigint but got ${typeof opts.feePerByte}.`
|
|
253
|
-
);
|
|
289
|
+
// Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
|
|
290
|
+
// `getSatoshi(...)` produce nonsensical negative fees throughout selection.
|
|
291
|
+
abigint(opts.feePerByte, 'opts.feePerByte');
|
|
254
292
|
// Dust stuff
|
|
255
293
|
// TODO: think about this more:
|
|
256
294
|
// - current dust filters tx which cannot be relayed by core
|
|
@@ -263,39 +301,37 @@ export class _Estimator {
|
|
|
263
301
|
const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
|
|
264
302
|
const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
|
|
265
303
|
const dustBytes = opts.dust === undefined ? BigInt(inputsDust + outputDust) : opts.dust;
|
|
266
|
-
|
|
267
|
-
throw new Error(
|
|
268
|
-
`Estimator: wrong dust=${opts.dust}, should be of type bigint but got ${typeof opts.dust}.`
|
|
269
|
-
);
|
|
270
|
-
}
|
|
304
|
+
abigint(dustBytes, 'opts.dust');
|
|
271
305
|
// 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
|
|
272
306
|
// 3000 sat/kvb -> 3 sat/vb.
|
|
273
307
|
// https://github.com/bitcoin/bitcoin/blob/27a770b34b8f1dbb84760f442edb3e23a0c2420b/src/policy/policy.h#L55
|
|
274
|
-
const dustFee = opts.dustRelayFeeRate === undefined ?
|
|
275
|
-
|
|
276
|
-
throw new Error(
|
|
277
|
-
`Estimator: wrong dustRelayFeeRate=${opts.dustRelayFeeRate}, should be of type bigint but got ${typeof opts.dustRelayFeeRate}.`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
308
|
+
const dustFee = opts.dustRelayFeeRate === undefined ? _3n : opts.dustRelayFeeRate;
|
|
309
|
+
abigint(dustFee, 'opts.dustRelayFeeRate');
|
|
280
310
|
// Dust uses feePerbyte by default, but we allow separate dust fee if needed
|
|
281
311
|
this.dust = dustBytes * dustFee;
|
|
282
312
|
if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
|
|
283
313
|
throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
|
|
284
314
|
const network = opts.network || NETWORK;
|
|
285
|
-
let amount =
|
|
315
|
+
let amount = _0n;
|
|
286
316
|
// Base weight: tx with outputs, no inputs
|
|
287
317
|
let baseWeight = 32;
|
|
288
318
|
for (const o of outputs) {
|
|
289
319
|
const script = getScript(o, opts, opts.network);
|
|
290
|
-
baseWeight += 32 + 4 *
|
|
320
|
+
baseWeight += 32 + 4 * varLen(script.length);
|
|
291
321
|
amount += o.amount;
|
|
292
322
|
}
|
|
293
|
-
|
|
294
|
-
throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
|
|
323
|
+
astring(opts.changeAddress, 'opts.changeAddress');
|
|
295
324
|
let changeWeight =
|
|
296
325
|
baseWeight +
|
|
297
326
|
32 +
|
|
298
|
-
|
|
327
|
+
// Same Address.decode() narrowing as above: the estimator only reaches this path for a
|
|
328
|
+
// concrete change output address, not an unknown descriptor.
|
|
329
|
+
4 *
|
|
330
|
+
varLen(
|
|
331
|
+
OutScript.encode(
|
|
332
|
+
Address(network).decode(opts.changeAddress) as Parameters<typeof OutScript.encode>[0]
|
|
333
|
+
).length
|
|
334
|
+
);
|
|
299
335
|
baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
|
|
300
336
|
// If there a lot of outputs change can change fee
|
|
301
337
|
changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
|
|
@@ -316,13 +352,16 @@ export class _Estimator {
|
|
|
316
352
|
opts.disableScriptCheck,
|
|
317
353
|
opts.allowUnknown
|
|
318
354
|
);
|
|
319
|
-
inputBeforeSign(normalized); // check fields
|
|
355
|
+
inputBeforeSign(normalized as TArg<psbt.TransactionInput>); // check fields
|
|
320
356
|
const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
|
|
321
357
|
if (!opts.allowSameUtxo && inputKeys.has(key))
|
|
322
358
|
throw new Error(`Estimator: same input passed multiple times: ${key}`);
|
|
323
359
|
inputKeys.add(key);
|
|
324
|
-
const inputType = getInputType(
|
|
325
|
-
|
|
360
|
+
const inputType = getInputType(
|
|
361
|
+
normalized as TArg<psbt.TransactionInput>,
|
|
362
|
+
opts.allowLegacyWitnessUtxo
|
|
363
|
+
);
|
|
364
|
+
const prev = getPrevOut(normalized as TArg<psbt.TransactionInput>);
|
|
326
365
|
const estimate = estimateInput(inputType, normalized, this.opts);
|
|
327
366
|
const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
|
|
328
367
|
return { inputType, normalized, amount: prev.amount, value, estimate };
|
|
@@ -353,8 +392,8 @@ export class _Estimator {
|
|
|
353
392
|
return compareBytes(scripts[a], scripts[b]);
|
|
354
393
|
});
|
|
355
394
|
}
|
|
356
|
-
private getSatoshi(
|
|
357
|
-
return this.opts.feePerByte * BigInt(toVsize(
|
|
395
|
+
private getSatoshi(weight: number) {
|
|
396
|
+
return this.opts.feePerByte * BigInt(toVsize(weight));
|
|
358
397
|
}
|
|
359
398
|
|
|
360
399
|
// Sort by value instead of amount
|
|
@@ -390,26 +429,33 @@ export class _Estimator {
|
|
|
390
429
|
let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
|
|
391
430
|
let hasWitnesses = false;
|
|
392
431
|
let num = 0;
|
|
393
|
-
let inputsAmount =
|
|
432
|
+
let inputsAmount = _0n;
|
|
394
433
|
const targetAmount = this.amount;
|
|
395
434
|
const res: Set<number> = new Set();
|
|
396
435
|
let fee;
|
|
436
|
+
// BIP144 serialization uses a var_int `txin_count`, so fee accounting must use the post-add
|
|
437
|
+
// input count here; the CompactSize prefix grows from 1 to 3 bytes at 253 inputs.
|
|
438
|
+
const getTotal = (newWeight: number, newNum: number) => {
|
|
439
|
+
const totalWeight = newWeight + 4 * CompactSizeLen.encode(newNum).length;
|
|
440
|
+
return { totalWeight, fee: this.getSatoshi(totalWeight) };
|
|
441
|
+
};
|
|
397
442
|
for (const idx of this.requiredIndices) {
|
|
398
443
|
this.checkInputIdx(idx);
|
|
399
444
|
if (res.has(idx)) throw new Error('required input encountered multiple times'); // should not happen
|
|
400
445
|
const { estimate, amount } = this.normalizedInputs[idx];
|
|
401
446
|
let newWeight = weight + estimate.weight;
|
|
402
447
|
if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
|
|
403
|
-
const
|
|
404
|
-
|
|
448
|
+
const newNum = num + 1;
|
|
449
|
+
const total = getTotal(newWeight, newNum);
|
|
450
|
+
fee = total.fee;
|
|
405
451
|
weight = newWeight;
|
|
406
452
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
407
|
-
num
|
|
453
|
+
num = newNum;
|
|
408
454
|
inputsAmount += amount;
|
|
409
455
|
res.add(idx);
|
|
410
456
|
// inputsAmount is enough to cover cost of tx
|
|
411
457
|
if (!all && targetAmount + fee <= inputsAmount && num >= this.requiredIndices.length)
|
|
412
|
-
return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
|
|
458
|
+
return { indices: Array.from(res), fee, weight: total.totalWeight, total: inputsAmount };
|
|
413
459
|
}
|
|
414
460
|
for (const idx of indices) {
|
|
415
461
|
this.checkInputIdx(idx);
|
|
@@ -417,26 +463,36 @@ export class _Estimator {
|
|
|
417
463
|
const { estimate, amount, value } = this.normalizedInputs[idx];
|
|
418
464
|
let newWeight = weight + estimate.weight;
|
|
419
465
|
if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
|
|
420
|
-
const
|
|
421
|
-
|
|
466
|
+
const newNum = num + 1;
|
|
467
|
+
const total = getTotal(newWeight, newNum);
|
|
468
|
+
fee = total.fee;
|
|
422
469
|
// Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
|
|
423
470
|
if (exact && amount + inputsAmount > targetAmount + fee + this.dust) continue; // skip if added value is bigger than dust
|
|
424
471
|
// Negative: cost of using input is more than value provided (negative)
|
|
425
472
|
// By default 'blackjack' mode in coinselect doesn't use that, which means
|
|
426
473
|
// it will use negative output if sorted by 'smallest'
|
|
427
|
-
if (skipNegative && value <=
|
|
474
|
+
if (skipNegative && value <= _0n) continue;
|
|
428
475
|
weight = newWeight;
|
|
429
476
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
430
|
-
num
|
|
477
|
+
num = newNum;
|
|
431
478
|
inputsAmount += amount;
|
|
432
479
|
res.add(idx);
|
|
433
480
|
// inputsAmount is enough to cover cost of tx
|
|
434
481
|
if (!all && targetAmount + fee <= inputsAmount)
|
|
435
|
-
return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
|
|
482
|
+
return { indices: Array.from(res), fee, weight: total.totalWeight, total: inputsAmount };
|
|
436
483
|
}
|
|
437
484
|
if (all) {
|
|
438
|
-
const
|
|
439
|
-
|
|
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;
|
|
490
|
+
return {
|
|
491
|
+
indices: Array.from(res),
|
|
492
|
+
fee: total.fee,
|
|
493
|
+
weight: total.totalWeight,
|
|
494
|
+
total: inputsAmount,
|
|
495
|
+
};
|
|
440
496
|
}
|
|
441
497
|
return undefined;
|
|
442
498
|
}
|
|
@@ -466,8 +522,15 @@ export class _Estimator {
|
|
|
466
522
|
Biggest: () => this.biggest,
|
|
467
523
|
};
|
|
468
524
|
if (strategy.startsWith('exact')) {
|
|
469
|
-
|
|
525
|
+
// Reject malformed `exact...` strings up front so a successful exact match cannot hide
|
|
526
|
+
// a missing or garbage `/accum...` fallback suffix.
|
|
527
|
+
const parts = strategy.split('/');
|
|
528
|
+
if (parts.length !== 2) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
|
|
529
|
+
const [exactStrategy, left] = parts as [ExactStrategy, AccumStrategy];
|
|
530
|
+
const exactData = exactStrategy.slice(5) as SortStrategy;
|
|
470
531
|
if (!data[exactData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
|
|
532
|
+
if (!left.startsWith('accum'))
|
|
533
|
+
throw new Error(`Estimator.select: wrong strategy=${strategy}`);
|
|
471
534
|
strategy = left;
|
|
472
535
|
const exact = this.accumulate(data[exactData](), true, true);
|
|
473
536
|
if (exact) return exact;
|
|
@@ -491,14 +554,17 @@ export class _Estimator {
|
|
|
491
554
|
|
|
492
555
|
const changeFee = this.getSatoshi(changeWeight);
|
|
493
556
|
let fee = s.fee;
|
|
557
|
+
// If dust suppresses the change output, the leftover becomes additional miner fee, so
|
|
558
|
+
// the returned fee/weight need to follow the no-change transaction shape instead of changeWeight.
|
|
494
559
|
const change = total - this.amount - changeFee;
|
|
495
560
|
if (change > this.dust) needChange = true;
|
|
561
|
+
else if (!needChange) fee = total - this.amount;
|
|
496
562
|
let inputs = indices;
|
|
497
563
|
let outputs = Array.from(this.outputs);
|
|
498
564
|
if (needChange) {
|
|
499
565
|
fee = changeFee;
|
|
500
566
|
// this shouldn't happen!
|
|
501
|
-
if (change <
|
|
567
|
+
if (change < _0n) throw new Error(`Estimator.result: negative change=${change}`);
|
|
502
568
|
outputs.push({ address: this.opts.changeAddress, amount: change });
|
|
503
569
|
}
|
|
504
570
|
if (this.opts.bip69) {
|
|
@@ -509,7 +575,7 @@ export class _Estimator {
|
|
|
509
575
|
inputs: inputs.map((i) => this.normalizedInputs[i].normalized),
|
|
510
576
|
outputs,
|
|
511
577
|
fee,
|
|
512
|
-
weight:
|
|
578
|
+
weight: needChange ? changeWeight : s.weight,
|
|
513
579
|
change: !!needChange,
|
|
514
580
|
};
|
|
515
581
|
let tx;
|
|
@@ -525,14 +591,47 @@ export class _Estimator {
|
|
|
525
591
|
}
|
|
526
592
|
}
|
|
527
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Selects inputs for the requested outputs using the configured strategy.
|
|
596
|
+
* @param inputs - candidate inputs that may be selected
|
|
597
|
+
* @param outputs - desired transaction outputs
|
|
598
|
+
* @param strategy - selection heuristic to use
|
|
599
|
+
* @param opts - Fee-estimation and transaction-construction options. See {@link EstimatorOpts}.
|
|
600
|
+
* @returns Selection result, optionally including a constructed transaction.
|
|
601
|
+
* @throws If the UTXO set, outputs, or estimator options are invalid. {@link Error}
|
|
602
|
+
* @example
|
|
603
|
+
* Estimate fees, pick inputs, and build a transaction for the selected set.
|
|
604
|
+
* ```ts
|
|
605
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
606
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
607
|
+
* import { selectUTXO } from '@scure/btc-signer/utxo.js';
|
|
608
|
+
* import { hex } from '@scure/base';
|
|
609
|
+
* const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
610
|
+
* const change = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
611
|
+
* selectUTXO(
|
|
612
|
+
* [{
|
|
613
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
614
|
+
* index: 0,
|
|
615
|
+
* witnessUtxo: { amount: 50_000n, script: spend.script },
|
|
616
|
+
* }],
|
|
617
|
+
* [{ address: spend.address!, amount: 10_000n }],
|
|
618
|
+
* 'default',
|
|
619
|
+
* { feePerByte: 1n, changeAddress: change.address! }
|
|
620
|
+
* );
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
528
623
|
export function selectUTXO(
|
|
529
|
-
inputs: psbt.TransactionInputUpdate[]
|
|
530
|
-
outputs: Output[]
|
|
624
|
+
inputs: TArg<psbt.TransactionInputUpdate[]>,
|
|
625
|
+
outputs: TArg<Output[]>,
|
|
531
626
|
strategy: SelectionStrategy,
|
|
532
|
-
opts: EstimatorOpts
|
|
627
|
+
opts: TArg<EstimatorOpts>
|
|
533
628
|
) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
629
|
+
aarray(inputs, 'inputs');
|
|
630
|
+
aarray(outputs, 'outputs');
|
|
631
|
+
validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
632
|
+
astring(strategy, 'strategy');
|
|
633
|
+
// Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
|
|
634
|
+
const _opts = { createTx: true, bip69: true, ...(opts as EstimatorOpts) };
|
|
635
|
+
const est = new _Estimator(inputs as psbt.TransactionInputUpdate[], outputs as Output[], _opts);
|
|
537
636
|
return est.result(strategy);
|
|
538
637
|
}
|