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