@scure/btc-signer 1.2.2 → 1.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.
Files changed (60) hide show
  1. package/README.md +19 -3
  2. package/lib/_type_test.d.ts +2 -0
  3. package/lib/_type_test.d.ts.map +1 -0
  4. package/lib/_type_test.js +59 -0
  5. package/lib/_type_test.js.map +1 -0
  6. package/lib/esm/_type_test.js +57 -0
  7. package/lib/esm/_type_test.js.map +1 -0
  8. package/lib/esm/index.js +13 -3007
  9. package/lib/esm/index.js.map +1 -1
  10. package/lib/esm/payment.js +681 -0
  11. package/lib/esm/payment.js.map +1 -0
  12. package/lib/esm/psbt.js +441 -0
  13. package/lib/esm/psbt.js.map +1 -0
  14. package/lib/esm/script.js +347 -0
  15. package/lib/esm/script.js.map +1 -0
  16. package/lib/esm/transaction.js +1013 -0
  17. package/lib/esm/transaction.js.map +1 -0
  18. package/lib/esm/utils.js +115 -0
  19. package/lib/esm/utils.js.map +1 -0
  20. package/lib/esm/utxo.js +491 -0
  21. package/lib/esm/utxo.js.map +1 -0
  22. package/lib/index.d.ts +18 -1439
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +53 -3042
  25. package/lib/index.js.map +1 -0
  26. package/lib/payment.d.ts +167 -0
  27. package/lib/payment.d.ts.map +1 -0
  28. package/lib/payment.js +704 -0
  29. package/lib/payment.js.map +1 -0
  30. package/lib/psbt.d.ts +834 -0
  31. package/lib/psbt.d.ts.map +1 -0
  32. package/lib/psbt.js +446 -0
  33. package/lib/psbt.js.map +1 -0
  34. package/lib/script.d.ts +154 -0
  35. package/lib/script.d.ts.map +1 -0
  36. package/lib/script.js +353 -0
  37. package/lib/script.js.map +1 -0
  38. package/lib/transaction.d.ts +223 -0
  39. package/lib/transaction.d.ts.map +1 -0
  40. package/lib/transaction.js +1022 -0
  41. package/lib/transaction.js.map +1 -0
  42. package/lib/utils.d.ts +30 -0
  43. package/lib/utils.d.ts.map +1 -0
  44. package/lib/utils.js +128 -0
  45. package/lib/utils.js.map +1 -0
  46. package/lib/utxo.d.ts +251 -0
  47. package/lib/utxo.d.ts.map +1 -0
  48. package/lib/utxo.js +501 -0
  49. package/lib/utxo.js.map +1 -0
  50. package/package.json +40 -10
  51. package/src/_type_test.ts +69 -0
  52. package/src/index.ts +28 -0
  53. package/src/package.json +3 -0
  54. package/src/payment.ts +749 -0
  55. package/src/psbt.ts +512 -0
  56. package/src/script.ts +236 -0
  57. package/src/transaction.ts +1065 -0
  58. package/src/utils.ts +118 -0
  59. package/src/utxo.ts +517 -0
  60. package/index.ts +0 -3067
package/src/utils.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { isBytes, concatBytes, U32LE } from 'micro-packed';
2
+ import { ripemd160 } from '@noble/hashes/ripemd160';
3
+ import { sha256 } from '@noble/hashes/sha256';
4
+ import { secp256k1 as secp, schnorr } from '@noble/curves/secp256k1';
5
+
6
+ export type Bytes = Uint8Array;
7
+ const Point = secp.ProjectivePoint;
8
+ const CURVE_ORDER = secp.CURVE.n;
9
+
10
+ export { sha256, isBytes, concatBytes };
11
+
12
+ export const hash160 = (msg: Bytes) => ripemd160(sha256(msg));
13
+ export const sha256x2 = (...msgs: Bytes[]) => sha256(sha256(concatBytes(...msgs)));
14
+ export const randomPrivateKeyBytes = schnorr.utils.randomPrivateKey;
15
+ export const pubSchnorr = schnorr.getPublicKey as (priv: string | Uint8Array) => Uint8Array;
16
+ export const pubECDSA = secp.getPublicKey;
17
+
18
+ // low-r signature grinding. Used to reduce tx size by 1 byte.
19
+ // noble/secp256k1 does not support the feature: it is not used outside of BTC.
20
+ // We implement it manually, because in BTC it's common.
21
+ // Not best way, but closest to bitcoin implementation (easier to check)
22
+ const hasLowR = (sig: { r: bigint; s: bigint }) => sig.r < CURVE_ORDER / 2n;
23
+ export function signECDSA(hash: Bytes, privateKey: Bytes, lowR = false): Bytes {
24
+ let sig = secp.sign(hash, privateKey);
25
+ if (lowR && !hasLowR(sig)) {
26
+ const extraEntropy = new Uint8Array(32);
27
+ let counter = 0;
28
+ while (!hasLowR(sig)) {
29
+ extraEntropy.set(U32LE.encode(counter++));
30
+ sig = secp.sign(hash, privateKey, { extraEntropy });
31
+ if (counter > 4294967295) throw new Error('lowR counter overflow: report the error');
32
+ }
33
+ }
34
+ return sig.toDERRawBytes();
35
+ }
36
+
37
+ export const signSchnorr = schnorr.sign;
38
+ export const tagSchnorr = schnorr.utils.taggedHash;
39
+
40
+ export enum PubT {
41
+ ecdsa,
42
+ schnorr,
43
+ }
44
+ export function validatePubkey(pub: Bytes, type: PubT): Bytes {
45
+ const len = pub.length;
46
+ if (type === PubT.ecdsa) {
47
+ if (len === 32) throw new Error('Expected non-Schnorr key');
48
+ Point.fromHex(pub); // does assertValidity
49
+ return pub;
50
+ } else if (type === PubT.schnorr) {
51
+ if (len !== 32) throw new Error('Expected 32-byte Schnorr key');
52
+ schnorr.utils.lift_x(schnorr.utils.bytesToNumberBE(pub));
53
+ return pub;
54
+ } else {
55
+ throw new Error('Unknown key type');
56
+ }
57
+ }
58
+
59
+ export function tapTweak(a: Bytes, b: Bytes): bigint {
60
+ const u = schnorr.utils;
61
+ const t = u.taggedHash('TapTweak', a, b);
62
+ const tn = u.bytesToNumberBE(t);
63
+ if (tn >= CURVE_ORDER) throw new Error('tweak higher than curve order');
64
+ return tn;
65
+ }
66
+
67
+ export function taprootTweakPrivKey(privKey: Uint8Array, merkleRoot = new Uint8Array()) {
68
+ const u = schnorr.utils;
69
+ const seckey0 = u.bytesToNumberBE(privKey); // seckey0 = int_from_bytes(seckey0)
70
+ const P = Point.fromPrivateKey(seckey0); // P = point_mul(G, seckey0)
71
+ // seckey = seckey0 if has_even_y(P) else SECP256K1_ORDER - seckey0
72
+ const seckey = P.hasEvenY() ? seckey0 : u.mod(-seckey0, CURVE_ORDER);
73
+ const xP = u.pointToBytes(P);
74
+ // t = int_from_bytes(tagged_hash("TapTweak", bytes_from_int(x(P)) + h)); >= SECP256K1_ORDER check
75
+ const t = tapTweak(xP, merkleRoot);
76
+ // bytes_from_int((seckey + t) % SECP256K1_ORDER)
77
+ return u.numberToBytesBE(u.mod(seckey + t, CURVE_ORDER), 32);
78
+ }
79
+
80
+ export function taprootTweakPubkey(pubKey: Uint8Array, h: Uint8Array): [Uint8Array, number] {
81
+ const u = schnorr.utils;
82
+ const t = tapTweak(pubKey, h); // t = int_from_bytes(tagged_hash("TapTweak", pubkey + h))
83
+ const P = u.lift_x(u.bytesToNumberBE(pubKey)); // P = lift_x(int_from_bytes(pubkey))
84
+ const Q = P.add(Point.fromPrivateKey(t)); // Q = point_add(P, point_mul(G, t))
85
+ const parity = Q.hasEvenY() ? 0 : 1; // 0 if has_even_y(Q) else 1
86
+ return [u.pointToBytes(Q), parity]; // bytes_from_int(x(Q))
87
+ }
88
+
89
+ // Another stupid decision, where lack of standard affects security.
90
+ // Multisig needs to be generated with some key.
91
+ // We are using approach from BIP 341/bitcoinjs-lib: SHA256(uncompressedDER(SECP256K1_GENERATOR_POINT))
92
+ // It is possible to switch SECP256K1_GENERATOR_POINT with some random point;
93
+ // but it's too complex to prove.
94
+ // Also used by bitcoin-core and bitcoinjs-lib
95
+ export const TAPROOT_UNSPENDABLE_KEY = sha256(Point.BASE.toRawBytes(false));
96
+
97
+ export const NETWORK = {
98
+ bech32: 'bc',
99
+ pubKeyHash: 0x00,
100
+ scriptHash: 0x05,
101
+ wif: 0x80,
102
+ };
103
+
104
+ export const TEST_NETWORK: typeof NETWORK = {
105
+ bech32: 'tb',
106
+ pubKeyHash: 0x6f,
107
+ scriptHash: 0xc4,
108
+ wif: 0xef,
109
+ };
110
+
111
+ // Exported for tests, internal method
112
+ export function compareBytes(a: Bytes, b: Bytes) {
113
+ if (!isBytes(a) || !isBytes(b)) throw new Error(`cmp: wrong type a=${typeof a} b=${typeof b}`);
114
+ // -1 -> a<b, 0 -> a==b, 1 -> a>b
115
+ const len = Math.min(a.length, b.length);
116
+ for (let i = 0; i < len; i++) if (a[i] != b[i]) return Math.sign(a[i] - b[i]);
117
+ return Math.sign(a.length - b.length);
118
+ }
package/src/utxo.ts ADDED
@@ -0,0 +1,517 @@
1
+ import { hex } from '@scure/base';
2
+ import * as P from 'micro-packed';
3
+ import { Address, OutScript, checkScript } from './payment.js';
4
+ import * as psbt from './psbt.js';
5
+ import { CompactSizeLen, RawOutput, RawTx, RawWitness, Script, VarBytes } from './script.js';
6
+ import {
7
+ DEFAULT_SEQUENCE,
8
+ TxOpts,
9
+ inputBeforeSign,
10
+ SignatureHash,
11
+ Transaction,
12
+ } from './transaction.js'; // circular
13
+ import { NETWORK, Bytes, compareBytes, isBytes, TAPROOT_UNSPENDABLE_KEY, sha256 } from './utils.js';
14
+
15
+ // Normalizes input
16
+ export function getPrevOut(input: psbt.TransactionInput): P.UnwrapCoder<typeof RawOutput> {
17
+ if (input.nonWitnessUtxo) {
18
+ if (input.index === undefined) throw new Error('Unknown input index');
19
+ return input.nonWitnessUtxo.outputs[input.index];
20
+ } else if (input.witnessUtxo) return input.witnessUtxo;
21
+ else throw new Error('Cannot find previous output info');
22
+ }
23
+
24
+ export function normalizeInput(
25
+ i: psbt.TransactionInputUpdate,
26
+ cur?: psbt.TransactionInput,
27
+ allowedFields?: (keyof psbt.TransactionInput)[],
28
+ disableScriptCheck = false
29
+ ): psbt.TransactionInput {
30
+ let { nonWitnessUtxo, txid } = i;
31
+ // String support for common fields. We usually prefer Uint8Array to avoid errors
32
+ // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
33
+ // it is better to expect string, since constructing this complex object will be
34
+ // difficult for user
35
+ if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
36
+ if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
37
+ if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
38
+ nonWitnessUtxo = cur?.nonWitnessUtxo;
39
+ if (typeof txid === 'string') txid = hex.decode(txid);
40
+ // TODO: if we have nonWitnessUtxo, we can extract txId from here
41
+ if (txid === undefined) txid = cur?.txid;
42
+ let res: psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput> = { ...cur, ...i, nonWitnessUtxo, txid };
43
+ if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
44
+ if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
45
+ if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
46
+ res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields);
47
+ psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
48
+
49
+ let prevOut;
50
+ if (res.nonWitnessUtxo && res.index !== undefined)
51
+ prevOut = res.nonWitnessUtxo.outputs[res.index];
52
+ else if (res.witnessUtxo) prevOut = res.witnessUtxo;
53
+ if (prevOut && !disableScriptCheck)
54
+ checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
55
+ return res;
56
+ }
57
+
58
+ export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtxo = false) {
59
+ let txType = 'legacy';
60
+ let defaultSighash = SignatureHash.ALL;
61
+ const prevOut = getPrevOut(input);
62
+ const first = OutScript.decode(prevOut.script);
63
+ let type = first.type;
64
+ let cur = first;
65
+ const stack = [first];
66
+ if (first.type === 'tr') {
67
+ defaultSighash = SignatureHash.DEFAULT;
68
+ return {
69
+ txType: 'taproot',
70
+ type: 'tr',
71
+ last: first,
72
+ lastScript: prevOut.script,
73
+ defaultSighash,
74
+ sighash: input.sighashType || defaultSighash,
75
+ };
76
+ } else {
77
+ if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
78
+ if (first.type === 'sh') {
79
+ if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
80
+ let child = OutScript.decode(input.redeemScript);
81
+ if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
82
+ stack.push(child);
83
+ cur = child;
84
+ type += `-${child.type}`;
85
+ }
86
+ // wsh can be inside sh
87
+ if (cur.type === 'wsh') {
88
+ if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
89
+ let child = OutScript.decode(input.witnessScript);
90
+ if (child.type === 'wsh') txType = 'segwit';
91
+ stack.push(child);
92
+ cur = child;
93
+ type += `-${child.type}`;
94
+ }
95
+ const last = stack[stack.length - 1];
96
+ if (last.type === 'sh' || last.type === 'wsh')
97
+ throw new Error('inputType: sh/wsh cannot be terminal type');
98
+ const lastScript = OutScript.encode(last);
99
+ const res = {
100
+ type,
101
+ txType,
102
+ last,
103
+ lastScript,
104
+ defaultSighash,
105
+ sighash: input.sighashType || defaultSighash,
106
+ };
107
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
108
+ throw new Error(
109
+ `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
110
+ );
111
+ }
112
+ return res;
113
+ }
114
+ }
115
+
116
+ export const toVsize = (weight: number) => Math.ceil(weight / 4);
117
+ // UTXO Select
118
+ type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint };
119
+
120
+ function estimateInput(
121
+ inputType: ReturnType<typeof getInputType>,
122
+ input: psbt.TransactionInput,
123
+ opts: TxOpts
124
+ ) {
125
+ let script: Bytes = P.EMPTY,
126
+ witness: Bytes[] | undefined;
127
+
128
+ // schnorr sig is always 64 bytes. except for cases when sighash is not default!
129
+ if (inputType.txType === 'taproot') {
130
+ const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
131
+ if (input.tapInternalKey && !P.equalBytes(input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
132
+ witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
133
+ } else if (input.tapLeafScript) {
134
+ // If user want to select specific leaf (which can signed, it is possible to remove all other leafs manually);
135
+ // Sort leafs by control block length.
136
+ const leafs = input.tapLeafScript.sort(
137
+ (a, b) =>
138
+ psbt.TaprootControlBlock.encode(a[0]).length -
139
+ psbt.TaprootControlBlock.encode(b[0]).length
140
+ );
141
+ for (const [cb, _script] of leafs) {
142
+ // Last byte is version
143
+ const script = _script.slice(0, -1);
144
+ const outScript = OutScript.decode(script);
145
+ let signatures: Bytes[] = [];
146
+ if (outScript.type === 'tr_ms') {
147
+ const m = outScript.m;
148
+ for (let i = 0; i < m; i++) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
149
+ const n = outScript.pubkeys.length - m;
150
+ for (let i = 0; i < n; i++) signatures.push(P.EMPTY);
151
+ } else if (outScript.type === 'tr_ns') {
152
+ for (const _pub of outScript.pubkeys) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
153
+ } else throw new Error('Finalize: Unknown tapLeafScript');
154
+ // Witness is stack, so last element will be used first
155
+ witness = signatures.reverse().concat([script, psbt.TaprootControlBlock.encode(cb)]);
156
+ break;
157
+ }
158
+ } else throw new Error('estimateInput/taproot: unknown input');
159
+ } else {
160
+ // It is possible to grind signatures until it has minimal size (but changing fee value +N satoshi),
161
+ // which will make estimations exact. But will be very hard for multi sig (need to make sure all signatures has small size).
162
+ const SIG_SIZE = 72; // Maximum size of signatures
163
+ const PUB_KEY_SIZE = 33;
164
+ let inputScript = P.EMPTY;
165
+ let inputWitness: Uint8Array[] = [];
166
+ if (inputType.last.type === 'ms') {
167
+ const m = inputType.last.m;
168
+ const sig: (number | Uint8Array)[] = [0];
169
+ for (let i = 0; i < m; i++) sig.push(new Uint8Array(SIG_SIZE));
170
+ inputScript = Script.encode(sig);
171
+ } else if (inputType.last.type === 'pk') {
172
+ // 71 sig + 1 sighash
173
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE)]);
174
+ } else if (inputType.last.type === 'pkh') {
175
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)]);
176
+ } else if (inputType.last.type === 'wpkh') {
177
+ inputScript = P.EMPTY;
178
+ inputWitness = [new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)];
179
+ } else if (inputType.last.type === 'unknown' && !opts.allowUnknownInputs)
180
+ throw new Error('Unknown inputs not allowed');
181
+ if (inputType.type.includes('wsh-')) {
182
+ // P2WSH
183
+ if (inputScript.length && inputType.lastScript.length) {
184
+ inputWitness = Script.decode(inputScript).map((i) => {
185
+ if (i === 0) return P.EMPTY;
186
+ if (isBytes(i)) return i;
187
+ throw new Error(`Wrong witness op=${i}`);
188
+ });
189
+ }
190
+ inputWitness = inputWitness.concat(inputType.lastScript);
191
+ }
192
+ if (inputType.txType === 'segwit') witness = inputWitness;
193
+ if (inputType.type.startsWith('sh-wsh-')) {
194
+ script = Script.encode([Script.encode([0, new Uint8Array(sha256.outputLen)])]);
195
+ } else if (inputType.type.startsWith('sh-')) {
196
+ script = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
197
+ } else if (inputType.type.startsWith('wsh-')) {
198
+ } else if (inputType.txType !== 'segwit') script = inputScript;
199
+ }
200
+ let weight = 160 + 4 * VarBytes.encode(script).length;
201
+ let hasWitnesses = false;
202
+ if (witness) {
203
+ weight += RawWitness.encode(witness).length;
204
+ hasWitnesses = true;
205
+ }
206
+ return { weight, hasWitnesses };
207
+ }
208
+
209
+ // Exported for tests, internal method
210
+ export const _cmpBig = (a: bigint, b: bigint) => {
211
+ const n = a - b;
212
+ if (n < 0n) return -1;
213
+ else if (n > 0n) return 1;
214
+ return 0;
215
+ };
216
+
217
+ export type EstimatorOpts = TxOpts & {
218
+ // NOTE: fees less than 1 satoshi per vbyte is not supported. Please create issue if you have valid use case for that.
219
+ feePerByte: bigint; // satoshi per vbyte
220
+ changeAddress: string; // address where change will be sent
221
+ // Optional
222
+ alwaysChange?: boolean; // always create change, even if less than dust threshold
223
+ bip69?: boolean; // https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
224
+ network?: typeof NETWORK;
225
+ dust?: number; // how much vbytes considered dust?
226
+ createTx?: boolean; // Create tx inside selection
227
+ };
228
+
229
+ function getScript(o: Output, opts: TxOpts = {}, network = NETWORK) {
230
+ let script;
231
+ if ('script' in o && o.script instanceof Uint8Array) {
232
+ script = o.script;
233
+ }
234
+ if ('address' in o) {
235
+ if (typeof o.address !== 'string')
236
+ throw new Error(`Estimator: wrong output address=${o.address}`);
237
+ script = OutScript.encode(Address(network).decode(o.address));
238
+ }
239
+ if (!script) throw new Error('Estimator: wrong output script');
240
+ if (typeof o.amount !== 'bigint') throw new Error(`Estimator: wrong output amount=${o.amount}`);
241
+ if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
242
+ throw new Error(
243
+ 'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
244
+ );
245
+ }
246
+ if (!opts.disableScriptCheck) checkScript(script);
247
+ return script;
248
+ }
249
+
250
+ // exact is meaningless without additional accum (will often fail if not possible to find right utxo)
251
+ // -> we support only exact+accum or accum
252
+ type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
253
+ type ExactStrategy = `exact${SortStrategy}`;
254
+ type AccumStrategy = `accum${SortStrategy}`;
255
+
256
+ export type SelectionStrategy =
257
+ | 'all'
258
+ | 'default'
259
+ | AccumStrategy
260
+ | `${ExactStrategy}/${AccumStrategy}`;
261
+
262
+ // class, because we need to re-use normalized inputs, instead of parsing each time
263
+ // internal stuff, exported for tests only
264
+ export class _Estimator {
265
+ private baseWeight: number;
266
+ private changeWeight: number;
267
+ private amount: bigint;
268
+ private normalizedInputs: {
269
+ inputType: ReturnType<typeof getInputType>;
270
+ normalized: ReturnType<typeof normalizeInput>;
271
+ amount: bigint;
272
+ value: bigint;
273
+ estimate: { weight: number; hasWitnesses: boolean };
274
+ }[];
275
+ // https://github.com/bitcoin/bitcoin/blob/f90603ac6d24f5263649675d51233f1fce8b2ecd/src/policy/policy.cpp#L44
276
+ // 32 + 4 + 1 + 107 + 4
277
+ // Dust used in accumExact + change address algo
278
+ // - change address: can be smaller for segwit
279
+ // - accumExact: ???
280
+ private dust = 148n; // compat with coinselect
281
+
282
+ constructor(
283
+ private inputs: psbt.TransactionInputUpdate[],
284
+ private outputs: Output[],
285
+ private opts: EstimatorOpts
286
+ ) {
287
+ if (typeof opts.feePerByte !== 'bigint')
288
+ throw new Error(`Estimator: wrong feePerByte=${opts.feePerByte}`);
289
+ if (opts.dust) {
290
+ if (typeof opts.dust !== 'bigint') throw new Error(`Estimator: wrong dust=${opts.dust}`);
291
+ this.dust = opts.dust;
292
+ }
293
+ const network = opts.network || NETWORK;
294
+ let amount = 0n;
295
+ // Base weight: tx with outputs, no inputs
296
+ let baseWeight = 32;
297
+ for (const o of outputs) {
298
+ const script = getScript(o, opts, opts.network);
299
+ baseWeight += 32 + 4 * VarBytes.encode(script).length;
300
+ amount += o.amount;
301
+ }
302
+ if (typeof opts.changeAddress !== 'string')
303
+ throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
304
+ let changeWeight =
305
+ baseWeight +
306
+ 32 +
307
+ 4 * VarBytes.encode(OutScript.encode(Address(network).decode(opts.changeAddress))).length;
308
+ baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
309
+ // If there a lot of outputs change can change fee
310
+ changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
311
+ this.baseWeight = baseWeight;
312
+ this.changeWeight = changeWeight;
313
+ this.amount = amount;
314
+ this.normalizedInputs = this.inputs.map((i) => {
315
+ const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
316
+ inputBeforeSign(normalized); // check fields
317
+ const inputType = getInputType(normalized, opts.allowLegacyWitnessUtxo);
318
+ const prev = getPrevOut(normalized);
319
+ const estimate = estimateInput(inputType, normalized, this.opts);
320
+ const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
321
+ return { inputType, normalized, amount: prev.amount, value, estimate };
322
+ });
323
+ }
324
+ private checkInputIdx(idx: number) {
325
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
326
+ throw new Error(`Wrong input index=${idx}`);
327
+ return idx;
328
+ }
329
+ private sortIndices(indices: number[]) {
330
+ return indices.slice().sort((a, b) => {
331
+ const ai = this.normalizedInputs[this.checkInputIdx(a)];
332
+ const bi = this.normalizedInputs[this.checkInputIdx(b)];
333
+ const out = compareBytes(ai.normalized.txid!, bi.normalized.txid!);
334
+ if (out !== 0) return out;
335
+ return ai.normalized.index! - bi.normalized.index!;
336
+ });
337
+ }
338
+ private sortOutputs(outputs: Output[]) {
339
+ const scripts = outputs.map((o) => getScript(o, this.opts, this.opts.network));
340
+ const indices = outputs.map((_, j) => j);
341
+ return indices.sort((a, b) => {
342
+ const aa = outputs[a].amount;
343
+ const ba = outputs[b].amount;
344
+ const out = _cmpBig(aa, ba);
345
+ if (out !== 0) return out;
346
+ return compareBytes(scripts[a], scripts[b]);
347
+ });
348
+ }
349
+ private getSatoshi(weigth: number) {
350
+ return this.opts.feePerByte * BigInt(toVsize(weigth));
351
+ }
352
+
353
+ // Sort by value instead of amount
354
+ get biggest() {
355
+ return this.inputs
356
+ .map((_i, j) => j)
357
+ .sort((a, b) => _cmpBig(this.normalizedInputs[b].value, this.normalizedInputs[a].value));
358
+ }
359
+ get smallest() {
360
+ return this.biggest.reverse();
361
+ }
362
+ // These assume that UTXO array has historical order.
363
+ // Otherwise, we have no way to know which tx is oldest
364
+ // Explorers usually give UTXO in this order.
365
+ get oldest() {
366
+ return this.inputs.map((_i, j) => j);
367
+ }
368
+ get newest() {
369
+ return this.oldest.reverse();
370
+ }
371
+ // exact - like blackjack from coinselect.
372
+ // exact(biggest) will select one big utxo which is closer to targetValue+dust, if possible.
373
+ // If not, it will accumulate largest utxo until value is close to targetValue+dust.
374
+ accumulate(indices: number[], exact = false, skipNegative = true, all = false) {
375
+ const { feePerByte } = this.opts;
376
+ // TODO: how to handle change addresses?
377
+ // - cost of input
378
+ // - cost of change output (if input requires change)
379
+ // - cost of output spending
380
+ // Dust threshold should be significantly bigger, no point in
381
+ // creating an output, which cannot be spent.
382
+ // coinselect doesn't consider cost of output address for dust.
383
+ // Changing that can actually reduce privacy
384
+ let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
385
+ let hasWitnesses = false;
386
+ let num = 0;
387
+ let inputsAmount = 0n;
388
+ const targetAmount = this.amount;
389
+ const res = [];
390
+ let fee;
391
+ for (const idx of indices) {
392
+ this.checkInputIdx(idx);
393
+ const { estimate, amount, value } = this.normalizedInputs[idx];
394
+ let newWeight = weight + estimate.weight;
395
+ if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
396
+ const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
397
+ fee = this.getSatoshi(totalWeight);
398
+ // Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
399
+ if (exact) {
400
+ const dust = this.dust * feePerByte;
401
+ // skip if added value is bigger than dust
402
+ if (amount + inputsAmount > targetAmount + fee + dust) continue;
403
+ }
404
+ // Negative: cost of using input is more than value provided (negative)
405
+ // By default 'blackjack' mode in coinselect doesn't use that, which means
406
+ // it will use negative output if sorted by 'smallest'
407
+ if (skipNegative && value <= 0n) continue;
408
+ weight = newWeight;
409
+ if (estimate.hasWitnesses) hasWitnesses = true;
410
+ num++;
411
+ inputsAmount += amount;
412
+ res.push(idx);
413
+ // inputsAmount is enough to cover cost of tx
414
+ if (!all && targetAmount + fee < inputsAmount)
415
+ return { indices: res, fee, weight: totalWeight, total: inputsAmount };
416
+ }
417
+ if (all) {
418
+ const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
419
+ return { indices: res, fee, weight: newWeight, total: inputsAmount };
420
+ }
421
+ return undefined;
422
+ }
423
+
424
+ // Works like coinselect default method
425
+ default() {
426
+ const { biggest } = this;
427
+ const exact = this.accumulate(biggest, true, false);
428
+ if (exact) return exact;
429
+ return this.accumulate(biggest);
430
+ }
431
+
432
+ private select(strategy: SelectionStrategy) {
433
+ if (strategy === 'all') {
434
+ return this.accumulate(
435
+ this.inputs.map((_, j) => j),
436
+ false,
437
+ true,
438
+ true
439
+ );
440
+ }
441
+ if (strategy === 'default') return this.default();
442
+ const data: Record<SortStrategy, () => number[]> = {
443
+ Oldest: () => this.oldest,
444
+ Newest: () => this.newest,
445
+ Smallest: () => this.smallest,
446
+ Biggest: () => this.biggest,
447
+ };
448
+ if (strategy.startsWith('exact')) {
449
+ const [exactData, left] = strategy.slice(5).split('/') as [SortStrategy, SelectionStrategy];
450
+ if (!data[exactData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
451
+ strategy = left;
452
+ const exact = this.accumulate(data[exactData](), true, true);
453
+ if (exact) return exact;
454
+ }
455
+ if (strategy.startsWith('accum')) {
456
+ const accumData = strategy.slice(5) as SortStrategy;
457
+ if (!data[accumData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
458
+ return this.accumulate(data[accumData]());
459
+ }
460
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
461
+ }
462
+
463
+ result(strategy: SelectionStrategy) {
464
+ const s = this.select(strategy);
465
+ if (!s) return;
466
+ const { indices, weight, total } = s;
467
+ let needChange = this.opts.alwaysChange;
468
+ const changeWeight = this.opts.alwaysChange
469
+ ? weight
470
+ : weight + (this.changeWeight - this.baseWeight);
471
+
472
+ const changeFee = this.getSatoshi(changeWeight);
473
+ let fee = s.fee;
474
+ const change = total - this.amount - changeFee;
475
+ if (change > this.dust) needChange = true;
476
+ let inputs = indices;
477
+ let outputs = Array.from(this.outputs);
478
+ if (needChange) {
479
+ fee = changeFee;
480
+ // this shouldn't happen!
481
+ if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
482
+ outputs.push({ address: this.opts.changeAddress, amount: change });
483
+ }
484
+ if (this.opts.bip69) {
485
+ inputs = this.sortIndices(inputs);
486
+ outputs = this.sortOutputs(outputs).map((i) => outputs[i]);
487
+ }
488
+ const res = {
489
+ inputs: inputs.map((i) => this.inputs[i]),
490
+ outputs,
491
+ fee,
492
+ weight: this.opts.alwaysChange ? s.weight : changeWeight,
493
+ change: !!needChange,
494
+ };
495
+ let tx;
496
+ if (this.opts.createTx) {
497
+ const { inputs, outputs } = res;
498
+ tx = new Transaction(this.opts);
499
+ for (const i of inputs) tx.addInput(i);
500
+ for (const o of outputs)
501
+ tx.addOutput({ ...o, script: getScript(o, this.opts, this.opts.network) });
502
+ }
503
+ return { ...res, tx };
504
+ }
505
+ }
506
+
507
+ export function selectUTXO(
508
+ inputs: psbt.TransactionInputUpdate[],
509
+ outputs: Output[],
510
+ strategy: SelectionStrategy,
511
+ opts: EstimatorOpts
512
+ ) {
513
+ // Defaults: do we want bip69 by default?
514
+ const _opts = { createTx: true, bip69: true, ...opts };
515
+ const est = new _Estimator(inputs, outputs, _opts);
516
+ return est.result(strategy);
517
+ }