@leather.io/bitcoin 0.36.6 → 0.37.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +233 -171
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -8
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/coin-selection/calculate-max-spend.ts +6 -2
- package/src/coin-selection/coin-selection.mocks.ts +19 -0
- package/src/coin-selection/coin-selection.spec.ts +85 -34
- package/src/coin-selection/coin-selection.ts +10 -5
- package/src/coin-selection/coin-selection.utils.ts +40 -12
- package/src/fees/bitcoin-fees.spec.ts +4 -4
- package/src/fees/bitcoin-fees.ts +4 -2
- package/src/fees/btc-size-fee-estimator.spec.ts +74 -0
- package/src/fees/btc-size-fee-estimator.ts +64 -0
- package/src/index.ts +1 -0
- package/src/transactions/generate-unsigned-transaction.ts +2 -1
- package/src/utils/bitcoin.utils.ts +8 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@leather.io/bitcoin",
|
|
3
3
|
"author": "Leather.io contact@leather.io",
|
|
4
4
|
"description": "Shared bitcoin utilities",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.37.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/leather.io/mono/tree/dev/packages/bitcoin",
|
|
8
8
|
"repository": {
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"just-memoize": "2.2.0",
|
|
33
33
|
"varuint-bitcoin": "1.1.2",
|
|
34
34
|
"zod": "4.0.17",
|
|
35
|
-
"@leather.io/
|
|
36
|
-
"@leather.io/
|
|
37
|
-
"@leather.io/utils": "0.
|
|
38
|
-
"@leather.io/
|
|
35
|
+
"@leather.io/crypto": "1.12.17",
|
|
36
|
+
"@leather.io/models": "0.54.0",
|
|
37
|
+
"@leather.io/utils": "0.50.0",
|
|
38
|
+
"@leather.io/constants": "0.32.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"prettier": "3.5.1",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"tslib": "2.6.2",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vitest": "2.1.9",
|
|
46
|
-
"@leather.io/prettier-config": "0.9.0",
|
|
47
46
|
"@leather.io/test-config": "0.1.3",
|
|
47
|
+
"@leather.io/rpc": "2.21.11",
|
|
48
48
|
"@leather.io/tsconfig-config": "0.11.1",
|
|
49
|
-
"@leather.io/
|
|
49
|
+
"@leather.io/prettier-config": "0.9.0"
|
|
50
50
|
},
|
|
51
51
|
"keywords": [
|
|
52
52
|
"bitcoin",
|
|
@@ -3,11 +3,15 @@ import BigNumber from 'bignumber.js';
|
|
|
3
3
|
import type { Money } from '@leather.io/models';
|
|
4
4
|
import { createMoney, satToBtc } from '@leather.io/utils';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type InputData,
|
|
8
|
+
filterUneconomicalUtxos,
|
|
9
|
+
getSpendableAmount,
|
|
10
|
+
} from './coin-selection.utils';
|
|
7
11
|
|
|
8
12
|
interface CalculateMaxSpendParams {
|
|
9
13
|
recipient: string;
|
|
10
|
-
utxos:
|
|
14
|
+
utxos: InputData[];
|
|
11
15
|
feeRate: number;
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -37,4 +37,23 @@ export const mockUtxos = generateMockTransactions([
|
|
|
37
37
|
600, 600, 1200, 1200, 10000, 10000, 25000, 40000, 50000000,
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
+
function generateMockTaprootUtxo(value: number): OwnedUtxo {
|
|
41
|
+
return {
|
|
42
|
+
address: 'tb1parwmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd',
|
|
43
|
+
path: `m/86'/1'/0'/0/0`,
|
|
44
|
+
keyOrigin: `deadbeef/86'/1'/0'/0/0`,
|
|
45
|
+
txid: sha256(sha256(hexToBytes(generateMockHex()))).toString(),
|
|
46
|
+
value,
|
|
47
|
+
vout: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generateMockTaprootTransactions(values: number[]) {
|
|
52
|
+
return values.map(val => generateMockTaprootUtxo(val));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const mockTaprootUtxos = generateMockTaprootTransactions([
|
|
56
|
+
600, 600, 1200, 1200, 10000, 10000, 25000, 40000, 50000000,
|
|
57
|
+
]);
|
|
58
|
+
|
|
40
59
|
export const mockAverageFee = generateMockAverageFee(10);
|
|
@@ -10,22 +10,23 @@ import {
|
|
|
10
10
|
} from '../mocks/mocks';
|
|
11
11
|
import { isBitcoinAddress } from '../validation/bitcoin-address';
|
|
12
12
|
import { determineUtxosForSpend, determineUtxosForSpendAll } from './coin-selection';
|
|
13
|
+
import { generateMockTaprootTransactions, generateMockTransactions } from './coin-selection.mocks';
|
|
13
14
|
import { filterUneconomicalUtxos, getSizeInfo } from './coin-selection.utils';
|
|
14
15
|
|
|
15
16
|
const demoUtxos = [
|
|
16
|
-
{ value: 8200 },
|
|
17
|
-
{ value: 8490 },
|
|
18
|
-
{ value: 8790 },
|
|
19
|
-
{ value: 19 },
|
|
20
|
-
{ value: 2000 },
|
|
21
|
-
{ value: 2340 },
|
|
22
|
-
{ value: 1230 },
|
|
23
|
-
{ value: 120 },
|
|
24
|
-
{ value: 8 },
|
|
25
|
-
{ value: 1002 },
|
|
26
|
-
{ value: 1382 },
|
|
27
|
-
{ value: 1400 },
|
|
28
|
-
{ value: 909 },
|
|
17
|
+
{ txid: 'a1', value: 8200, address: recipientAddress },
|
|
18
|
+
{ txid: 'a2', value: 8490, address: recipientAddress },
|
|
19
|
+
{ txid: 'a3', value: 8790, address: recipientAddress },
|
|
20
|
+
{ txid: 'a4', value: 19, address: recipientAddress },
|
|
21
|
+
{ txid: 'a5', value: 2000, address: recipientAddress },
|
|
22
|
+
{ txid: 'a6', value: 2340, address: recipientAddress },
|
|
23
|
+
{ txid: 'a7', value: 1230, address: recipientAddress },
|
|
24
|
+
{ txid: 'a8', value: 120, address: recipientAddress },
|
|
25
|
+
{ txid: 'a9', value: 8, address: recipientAddress },
|
|
26
|
+
{ txid: 'a10', value: 1002, address: recipientAddress },
|
|
27
|
+
{ txid: 'a11', value: 1382, address: recipientAddress },
|
|
28
|
+
{ txid: 'a12', value: 1400, address: recipientAddress },
|
|
29
|
+
{ txid: 'a13', value: 909, address: recipientAddress },
|
|
29
30
|
];
|
|
30
31
|
|
|
31
32
|
function generate10kSpendWithDummyUtxoSet(recipient: string) {
|
|
@@ -33,7 +34,7 @@ function generate10kSpendWithDummyUtxoSet(recipient: string) {
|
|
|
33
34
|
throw new Error('Invalid Bitcoin address');
|
|
34
35
|
}
|
|
35
36
|
return determineUtxosForSpend({
|
|
36
|
-
utxos: demoUtxos
|
|
37
|
+
utxos: demoUtxos,
|
|
37
38
|
feeRate: 20,
|
|
38
39
|
recipients: [{ address: recipient, amount: createMoney(10_000, 'BTC') }],
|
|
39
40
|
});
|
|
@@ -43,7 +44,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
43
44
|
describe('Estimated size', () => {
|
|
44
45
|
test('that Native Segwit, 1 input 2 outputs weighs 140 vBytes', () => {
|
|
45
46
|
const estimation = determineUtxosForSpend({
|
|
46
|
-
utxos: [{ value: 50_000
|
|
47
|
+
utxos: [{ txid: 'b1', value: 50_000, address: recipientAddress }],
|
|
47
48
|
recipients: [
|
|
48
49
|
{
|
|
49
50
|
address: recipientAddress,
|
|
@@ -58,7 +59,10 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
58
59
|
|
|
59
60
|
test('that Native Segwit, 2 input 2 outputs weighs 200vBytes', () => {
|
|
60
61
|
const estimation = determineUtxosForSpend({
|
|
61
|
-
utxos: [
|
|
62
|
+
utxos: [
|
|
63
|
+
{ txid: 'c1', value: 50_000, address: recipientAddress },
|
|
64
|
+
{ txid: 'c2', value: 50_000, address: recipientAddress },
|
|
65
|
+
],
|
|
62
66
|
recipients: [
|
|
63
67
|
{
|
|
64
68
|
address: recipientAddress,
|
|
@@ -74,17 +78,17 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
74
78
|
test('that Native Segwit, 10 input 2 outputs weighs 200vBytes', () => {
|
|
75
79
|
const estimation = determineUtxosForSpend({
|
|
76
80
|
utxos: [
|
|
77
|
-
{ value: 20_000 },
|
|
78
|
-
{ value: 20_000 },
|
|
79
|
-
{ value: 10_000 },
|
|
80
|
-
{ value: 10_000 },
|
|
81
|
-
{ value: 10_000 },
|
|
82
|
-
{ value: 10_000 },
|
|
83
|
-
{ value: 10_000 },
|
|
84
|
-
{ value: 10_000 },
|
|
85
|
-
{ value: 10_000 },
|
|
86
|
-
{ value: 10_000 },
|
|
87
|
-
]
|
|
81
|
+
{ txid: 'd1', value: 20_000, address: recipientAddress },
|
|
82
|
+
{ txid: 'd2', value: 20_000, address: recipientAddress },
|
|
83
|
+
{ txid: 'd3', value: 10_000, address: recipientAddress },
|
|
84
|
+
{ txid: 'd4', value: 10_000, address: recipientAddress },
|
|
85
|
+
{ txid: 'd5', value: 10_000, address: recipientAddress },
|
|
86
|
+
{ txid: 'd6', value: 10_000, address: recipientAddress },
|
|
87
|
+
{ txid: 'd7', value: 10_000, address: recipientAddress },
|
|
88
|
+
{ txid: 'd8', value: 10_000, address: recipientAddress },
|
|
89
|
+
{ txid: 'd9', value: 10_000, address: recipientAddress },
|
|
90
|
+
{ txid: 'd10', value: 10_000, address: recipientAddress },
|
|
91
|
+
],
|
|
88
92
|
recipients: [
|
|
89
93
|
{
|
|
90
94
|
address: recipientAddress,
|
|
@@ -147,12 +151,14 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
147
151
|
});
|
|
148
152
|
|
|
149
153
|
test('against a random set of generated utxos', () => {
|
|
150
|
-
const testData = createNullArrayOfLength(50).map(() => ({
|
|
154
|
+
const testData = createNullArrayOfLength(50).map((_, i) => ({
|
|
155
|
+
txid: `rnd${i}`,
|
|
151
156
|
value: Math.ceil(Math.random() * 10000),
|
|
157
|
+
address: recipientAddress,
|
|
152
158
|
}));
|
|
153
159
|
const amount = 29123n;
|
|
154
160
|
const result = determineUtxosForSpend({
|
|
155
|
-
utxos: testData
|
|
161
|
+
utxos: testData,
|
|
156
162
|
recipients: [
|
|
157
163
|
{
|
|
158
164
|
address: recipientAddress,
|
|
@@ -180,7 +186,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
180
186
|
},
|
|
181
187
|
];
|
|
182
188
|
const filteredUtxos = filterUneconomicalUtxos({
|
|
183
|
-
utxos: demoUtxos.sort((a, b) => b.value - a.value)
|
|
189
|
+
utxos: demoUtxos.sort((a, b) => b.value - a.value),
|
|
184
190
|
feeRate,
|
|
185
191
|
recipients,
|
|
186
192
|
});
|
|
@@ -188,7 +194,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
188
194
|
recipients[0].amount = createMoney(amount, 'BTC');
|
|
189
195
|
|
|
190
196
|
const result = determineUtxosForSpend({
|
|
191
|
-
utxos: filteredUtxos
|
|
197
|
+
utxos: filteredUtxos,
|
|
192
198
|
recipients: [
|
|
193
199
|
{
|
|
194
200
|
address: recipientAddress,
|
|
@@ -203,7 +209,11 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
203
209
|
});
|
|
204
210
|
|
|
205
211
|
test('that spending all utxos with sendMax does not result in dust utxos', () => {
|
|
206
|
-
const utxos = [
|
|
212
|
+
const utxos = [
|
|
213
|
+
{ txid: '123', value: 1000, address: recipientAddress },
|
|
214
|
+
{ txid: '1323', value: 2000, address: recipientAddress },
|
|
215
|
+
{ txid: '132355', value: 3000, address: recipientAddress },
|
|
216
|
+
];
|
|
207
217
|
const recipients = [
|
|
208
218
|
{
|
|
209
219
|
address: recipientAddress,
|
|
@@ -211,7 +221,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
211
221
|
},
|
|
212
222
|
];
|
|
213
223
|
const sizeInfo = getSizeInfo({
|
|
214
|
-
|
|
224
|
+
utxos,
|
|
215
225
|
isSendMax: true,
|
|
216
226
|
recipients,
|
|
217
227
|
});
|
|
@@ -221,7 +231,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
221
231
|
recipients[0].amount = createMoney(amount, 'BTC');
|
|
222
232
|
|
|
223
233
|
const result = determineUtxosForSpendAll({
|
|
224
|
-
utxos: utxos
|
|
234
|
+
utxos: utxos,
|
|
225
235
|
recipients,
|
|
226
236
|
feeRate,
|
|
227
237
|
});
|
|
@@ -231,3 +241,44 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
231
241
|
expect(fee).toEqual(735);
|
|
232
242
|
});
|
|
233
243
|
});
|
|
244
|
+
|
|
245
|
+
describe('mixed input types', () => {
|
|
246
|
+
test('that spending from P2TR-only inputs produces correct fee estimation', () => {
|
|
247
|
+
const taprootUtxos = generateMockTaprootTransactions([50_000]);
|
|
248
|
+
const estimation = determineUtxosForSpend({
|
|
249
|
+
utxos: taprootUtxos,
|
|
250
|
+
recipients: [
|
|
251
|
+
{
|
|
252
|
+
address: recipientAddress,
|
|
253
|
+
amount: createMoney(40_000, 'BTC'),
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
feeRate: 20,
|
|
257
|
+
});
|
|
258
|
+
expect(estimation.fee.amount.toNumber()).toBeGreaterThan(0);
|
|
259
|
+
expect(estimation.inputs).toHaveLength(1);
|
|
260
|
+
expect(estimation.txVBytes).toBeGreaterThanOrEqual(130);
|
|
261
|
+
expect(estimation.txVBytes).toBeLessThan(155);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('that spending from mixed P2WPKH + P2TR inputs uses correct vBytes', () => {
|
|
265
|
+
const nativeSegwitUtxos = generateMockTransactions([30_000]);
|
|
266
|
+
const taprootUtxos = generateMockTaprootTransactions([30_000]);
|
|
267
|
+
const mixedUtxos = [...nativeSegwitUtxos, ...taprootUtxos];
|
|
268
|
+
|
|
269
|
+
const estimation = determineUtxosForSpend({
|
|
270
|
+
utxos: mixedUtxos,
|
|
271
|
+
recipients: [
|
|
272
|
+
{
|
|
273
|
+
address: recipientAddress,
|
|
274
|
+
amount: createMoney(50_000, 'BTC'),
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
feeRate: 20,
|
|
278
|
+
});
|
|
279
|
+
expect(estimation.inputs).toHaveLength(2);
|
|
280
|
+
expect(estimation.fee.amount.toNumber()).toBeGreaterThan(0);
|
|
281
|
+
expect(estimation.txVBytes).toBeGreaterThan(170);
|
|
282
|
+
expect(estimation.txVBytes).toBeLessThan(210);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -6,7 +6,12 @@ import { Money } from '@leather.io/models';
|
|
|
6
6
|
import { createMoney, sumMoney } from '@leather.io/utils';
|
|
7
7
|
|
|
8
8
|
import { BitcoinError } from '../validation/bitcoin-error';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type InputData,
|
|
11
|
+
filterUneconomicalUtxos,
|
|
12
|
+
getSizeInfo,
|
|
13
|
+
getUtxoTotal,
|
|
14
|
+
} from './coin-selection.utils';
|
|
10
15
|
|
|
11
16
|
export interface CoinSelectionOutput {
|
|
12
17
|
value: bigint;
|
|
@@ -23,7 +28,7 @@ export interface DetermineUtxosForSpendArgs<T> {
|
|
|
23
28
|
recipients: CoinSelectionRecipient[];
|
|
24
29
|
utxos: T[];
|
|
25
30
|
}
|
|
26
|
-
export function determineUtxosForSpendAll<T extends
|
|
31
|
+
export function determineUtxosForSpendAll<T extends InputData>({
|
|
27
32
|
feeRate,
|
|
28
33
|
recipients,
|
|
29
34
|
utxos,
|
|
@@ -36,7 +41,7 @@ export function determineUtxosForSpendAll<T extends { value: number; txid: strin
|
|
|
36
41
|
if (!filteredUtxos.length) throw new BitcoinError('InsufficientFunds');
|
|
37
42
|
|
|
38
43
|
const sizeInfo = getSizeInfo({
|
|
39
|
-
|
|
44
|
+
utxos: filteredUtxos,
|
|
40
45
|
isSendMax: true,
|
|
41
46
|
recipients,
|
|
42
47
|
});
|
|
@@ -57,7 +62,7 @@ export function determineUtxosForSpendAll<T extends { value: number; txid: strin
|
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
export function determineUtxosForSpend<T extends
|
|
65
|
+
export function determineUtxosForSpend<T extends InputData>({
|
|
61
66
|
feeRate,
|
|
62
67
|
recipients,
|
|
63
68
|
utxos,
|
|
@@ -79,7 +84,7 @@ export function determineUtxosForSpend<T extends { value: number; txid: string }
|
|
|
79
84
|
|
|
80
85
|
function estimateTransactionSize() {
|
|
81
86
|
return getSizeInfo({
|
|
82
|
-
|
|
87
|
+
utxos: neededUtxos,
|
|
83
88
|
recipients,
|
|
84
89
|
});
|
|
85
90
|
}
|
|
@@ -5,18 +5,45 @@ import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
|
5
5
|
import { sumNumbers } from '@leather.io/utils';
|
|
6
6
|
|
|
7
7
|
import { BtcSizeFeeEstimator } from '../fees/btc-size-fee-estimator';
|
|
8
|
+
import { inferPaymentTypeFromAddress } from '../utils/bitcoin.utils';
|
|
9
|
+
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
8
10
|
import { CoinSelectionRecipient } from './coin-selection';
|
|
9
11
|
|
|
10
|
-
export
|
|
12
|
+
export interface InputData {
|
|
13
|
+
value: number;
|
|
14
|
+
txid: string;
|
|
15
|
+
address: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getUtxoTotal<T extends InputData>(utxos: T[]) {
|
|
11
19
|
return sumNumbers(utxos.map(utxo => utxo.value));
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
interface CountInputsByScriptTypeResponse {
|
|
23
|
+
p2wpkh: number;
|
|
24
|
+
p2tr: number;
|
|
25
|
+
}
|
|
26
|
+
export function countInputsByScriptType<T extends InputData>(
|
|
27
|
+
utxos: T[]
|
|
28
|
+
): CountInputsByScriptTypeResponse {
|
|
29
|
+
return utxos.reduce(
|
|
30
|
+
(acc, utxo) => {
|
|
31
|
+
const paymentType = inferPaymentTypeFromAddress(createBitcoinAddress(utxo.address));
|
|
32
|
+
return {
|
|
33
|
+
...acc,
|
|
34
|
+
[paymentType]: acc[paymentType] + 1,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
{ p2tr: 0, p2wpkh: 0 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getSizeInfo<T extends InputData>(payload: {
|
|
42
|
+
utxos: T[];
|
|
16
43
|
recipients: CoinSelectionRecipient[];
|
|
17
44
|
isSendMax?: boolean;
|
|
18
45
|
}) {
|
|
19
|
-
const {
|
|
46
|
+
const { utxos, recipients, isSendMax } = payload;
|
|
20
47
|
|
|
21
48
|
const validAddressesInfo = recipients
|
|
22
49
|
.map(recipient => validate(recipient.address) && getAddressInfo(recipient.address))
|
|
@@ -48,22 +75,23 @@ export function getSizeInfo(payload: {
|
|
|
48
75
|
{} as Record<string, number>
|
|
49
76
|
);
|
|
50
77
|
|
|
78
|
+
const { p2wpkh, p2tr } = countInputsByScriptType(utxos);
|
|
51
79
|
const txSizer = new BtcSizeFeeEstimator();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
|
|
81
|
+
return txSizer.calcMixedInputTxSize({
|
|
82
|
+
p2wpkh_input_count: p2wpkh,
|
|
83
|
+
p2tr_input_count: p2tr,
|
|
55
84
|
...outputsData,
|
|
56
85
|
});
|
|
57
|
-
|
|
58
|
-
return sizeInfo;
|
|
59
86
|
}
|
|
87
|
+
|
|
60
88
|
interface GetSpendableAmountArgs<T> {
|
|
61
89
|
utxos: T[];
|
|
62
90
|
feeRate: number;
|
|
63
91
|
recipients: CoinSelectionRecipient[];
|
|
64
92
|
isSendMax?: boolean;
|
|
65
93
|
}
|
|
66
|
-
export function getSpendableAmount<T extends
|
|
94
|
+
export function getSpendableAmount<T extends InputData>({
|
|
67
95
|
utxos,
|
|
68
96
|
feeRate,
|
|
69
97
|
recipients,
|
|
@@ -71,7 +99,7 @@ export function getSpendableAmount<T extends { value: number }>({
|
|
|
71
99
|
const balance = utxos.map(utxo => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
72
100
|
|
|
73
101
|
const size = getSizeInfo({
|
|
74
|
-
|
|
102
|
+
utxos,
|
|
75
103
|
recipients,
|
|
76
104
|
});
|
|
77
105
|
const fee = Math.ceil(size.txVBytes * feeRate);
|
|
@@ -83,7 +111,7 @@ export function getSpendableAmount<T extends { value: number }>({
|
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
// Check if the spendable amount drops when adding a utxo
|
|
86
|
-
export function filterUneconomicalUtxos<T extends
|
|
114
|
+
export function filterUneconomicalUtxos<T extends InputData>({
|
|
87
115
|
utxos,
|
|
88
116
|
feeRate,
|
|
89
117
|
recipients,
|
|
@@ -4,7 +4,7 @@ import { AverageBitcoinFeeRates } from '@leather.io/models';
|
|
|
4
4
|
import { createMoney } from '@leather.io/utils';
|
|
5
5
|
|
|
6
6
|
import { CoinSelectionRecipient } from '../coin-selection/coin-selection';
|
|
7
|
-
import { recipientAddress
|
|
7
|
+
import { recipientAddress } from '../mocks/mocks';
|
|
8
8
|
import { getBitcoinFees, getBitcoinTransactionFee } from './bitcoin-fees';
|
|
9
9
|
|
|
10
10
|
describe('getBitcoinTransactionFee', () => {
|
|
@@ -13,7 +13,7 @@ describe('getBitcoinTransactionFee', () => {
|
|
|
13
13
|
recipients: [{ address: recipientAddress, amount: createMoney(1000, 'BTC') }],
|
|
14
14
|
utxos: [
|
|
15
15
|
{
|
|
16
|
-
address:
|
|
16
|
+
address: recipientAddress,
|
|
17
17
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
18
18
|
vout: 0,
|
|
19
19
|
value: 2000,
|
|
@@ -32,7 +32,7 @@ describe('getBitcoinTransactionFee', () => {
|
|
|
32
32
|
recipients: [{ address: recipientAddress, amount: createMoney(2000, 'BTC') }],
|
|
33
33
|
utxos: [
|
|
34
34
|
{
|
|
35
|
-
address:
|
|
35
|
+
address: recipientAddress,
|
|
36
36
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
37
37
|
vout: 0,
|
|
38
38
|
value: 2000,
|
|
@@ -68,7 +68,7 @@ describe('getBitcoinFees', () => {
|
|
|
68
68
|
];
|
|
69
69
|
const utxos = [
|
|
70
70
|
{
|
|
71
|
-
address:
|
|
71
|
+
address: recipientAddress,
|
|
72
72
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
73
73
|
vout: 0,
|
|
74
74
|
value: 2000,
|
package/src/fees/bitcoin-fees.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InputData } from 'coin-selection/coin-selection.utils';
|
|
2
|
+
|
|
1
3
|
import { AverageBitcoinFeeRates, Money } from '@leather.io/models';
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -7,7 +9,7 @@ import {
|
|
|
7
9
|
determineUtxosForSpendAll,
|
|
8
10
|
} from '../coin-selection/coin-selection';
|
|
9
11
|
|
|
10
|
-
type GetBitcoinTransactionFeeArgs = DetermineUtxosForSpendArgs<
|
|
12
|
+
type GetBitcoinTransactionFeeArgs = DetermineUtxosForSpendArgs<InputData> & {
|
|
11
13
|
isSendingMax?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -33,7 +35,7 @@ export interface GetBitcoinFeesArgs {
|
|
|
33
35
|
feeRates: AverageBitcoinFeeRates;
|
|
34
36
|
isSendingMax?: boolean;
|
|
35
37
|
recipients: CoinSelectionRecipient[];
|
|
36
|
-
utxos:
|
|
38
|
+
utxos: InputData[];
|
|
37
39
|
}
|
|
38
40
|
export function getBitcoinFees({ feeRates, isSendingMax, recipients, utxos }: GetBitcoinFeesArgs) {
|
|
39
41
|
const defaultArgs = {
|
|
@@ -110,4 +110,78 @@ describe('BtcSizeFeeEstimator', () => {
|
|
|
110
110
|
expect(feeRange).toBe('900 - 1100');
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
describe('calcMixedInputTxSize', () => {
|
|
115
|
+
it('should calculate size for p2wpkh-only inputs', () => {
|
|
116
|
+
const { txVBytes, txBytes, txWeight } = estimator.calcMixedInputTxSize({
|
|
117
|
+
p2wpkh_input_count: 2,
|
|
118
|
+
p2tr_input_count: 0,
|
|
119
|
+
p2wpkh_output_count: 1,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// overhead: 10.75, inputs: 2 * 67.75 = 135.5, outputs: 1 * 31 = 31
|
|
123
|
+
expect(txVBytes).toEqual(177.25);
|
|
124
|
+
// extraRawBytes: 2.25, witnessBytes: 2 * 107 = 214
|
|
125
|
+
expect(txBytes).toEqual(393.5);
|
|
126
|
+
expect(txWeight).toEqual(709);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should calculate size for p2tr-only inputs', () => {
|
|
130
|
+
const { txVBytes, txBytes, txWeight } = estimator.calcMixedInputTxSize({
|
|
131
|
+
p2wpkh_input_count: 0,
|
|
132
|
+
p2tr_input_count: 2,
|
|
133
|
+
p2tr_output_count: 1,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// overhead: 10.75, inputs: 2 * 57.25 = 114.5, outputs: 1 * 43 = 43
|
|
137
|
+
expect(txVBytes).toEqual(168.25);
|
|
138
|
+
// extraRawBytes: 2.25, witnessBytes: 2 * 65 = 130
|
|
139
|
+
expect(txBytes).toEqual(300.5);
|
|
140
|
+
expect(txWeight).toEqual(673);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should calculate size for mixed p2wpkh and p2tr inputs', () => {
|
|
144
|
+
const { txVBytes, txBytes, txWeight } = estimator.calcMixedInputTxSize({
|
|
145
|
+
p2wpkh_input_count: 1,
|
|
146
|
+
p2tr_input_count: 1,
|
|
147
|
+
p2wpkh_output_count: 1,
|
|
148
|
+
p2tr_output_count: 1,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// overhead: 10.75, inputs: 1 * 67.75 + 1 * 57.25 = 125, outputs: 31 + 43 = 74
|
|
152
|
+
expect(txVBytes).toEqual(209.75);
|
|
153
|
+
// extraRawBytes: 2.25, witnessBytes: 107 + 65 = 172
|
|
154
|
+
expect(txBytes).toEqual(384);
|
|
155
|
+
expect(txWeight).toEqual(839);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should default optional output counts to zero', () => {
|
|
159
|
+
const { txVBytes } = estimator.calcMixedInputTxSize({
|
|
160
|
+
p2wpkh_input_count: 1,
|
|
161
|
+
p2tr_input_count: 0,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// overhead: 10.75, inputs: 1 * 67.75, outputs: 0
|
|
165
|
+
expect(txVBytes).toEqual(78.5);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should calculate size with multiple output types', () => {
|
|
169
|
+
const { txVBytes, txBytes, txWeight } = estimator.calcMixedInputTxSize({
|
|
170
|
+
p2wpkh_input_count: 2,
|
|
171
|
+
p2tr_input_count: 1,
|
|
172
|
+
p2pkh_output_count: 1,
|
|
173
|
+
p2sh_output_count: 1,
|
|
174
|
+
p2wpkh_output_count: 1,
|
|
175
|
+
p2tr_output_count: 1,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// overhead: getTxOverheadVBytes('p2wpkh', 3, 4) = 4 + 1 + 1 + 4 + 0.75 = 10.75
|
|
179
|
+
// inputs: 2 * 67.75 + 1 * 57.25 = 192.75
|
|
180
|
+
// outputs: 34 + 32 + 31 + 43 = 140
|
|
181
|
+
expect(txVBytes).toEqual(343.5);
|
|
182
|
+
// extraRawBytes: 2.25, witnessBytes: 2 * 107 + 1 * 65 = 279
|
|
183
|
+
expect(txBytes).toEqual(624.75);
|
|
184
|
+
expect(txWeight).toEqual(1374);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
113
187
|
});
|
|
@@ -26,6 +26,26 @@ export interface TxSizerParams {
|
|
|
26
26
|
p2tr_output_count: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface MixedInputTxSizerParams {
|
|
30
|
+
p2wpkh_input_count: number;
|
|
31
|
+
p2tr_input_count: number;
|
|
32
|
+
p2pkh_input_count?: number;
|
|
33
|
+
p2sh_input_count?: number;
|
|
34
|
+
p2sh_p2wpkh_input_count?: number;
|
|
35
|
+
p2sh_p2wsh_input_count?: number;
|
|
36
|
+
p2wsh_input_count?: number;
|
|
37
|
+
input_m?: number;
|
|
38
|
+
input_n?: number;
|
|
39
|
+
p2pkh_output_count?: number;
|
|
40
|
+
p2sh_output_count?: number;
|
|
41
|
+
p2sh_p2wpkh_output_count?: number;
|
|
42
|
+
p2sh_p2wsh_output_count?: number;
|
|
43
|
+
p2wpkh_output_count?: number;
|
|
44
|
+
p2wsh_output_count?: number;
|
|
45
|
+
p2tr_output_count?: number;
|
|
46
|
+
_forceSegwit?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export class BtcSizeFeeEstimator {
|
|
30
50
|
P2PKH_IN_SIZE = 148;
|
|
31
51
|
P2PKH_OUT_SIZE = 34;
|
|
@@ -310,6 +330,50 @@ export class BtcSizeFeeEstimator {
|
|
|
310
330
|
return { txVBytes, txBytes, txWeight };
|
|
311
331
|
}
|
|
312
332
|
|
|
333
|
+
calcMixedInputTxSize(opts: MixedInputTxSizerParams) {
|
|
334
|
+
const { p2wpkh_input_count, p2tr_input_count } = opts;
|
|
335
|
+
const totalInputCount = p2wpkh_input_count + p2tr_input_count;
|
|
336
|
+
|
|
337
|
+
const p2pkh_output_count = opts.p2pkh_output_count ?? 0;
|
|
338
|
+
const p2sh_output_count = opts.p2sh_output_count ?? 0;
|
|
339
|
+
const p2sh_p2wpkh_output_count = opts.p2sh_p2wpkh_output_count ?? 0;
|
|
340
|
+
const p2sh_p2wsh_output_count = opts.p2sh_p2wsh_output_count ?? 0;
|
|
341
|
+
const p2wpkh_output_count = opts.p2wpkh_output_count ?? 0;
|
|
342
|
+
const p2wsh_output_count = opts.p2wsh_output_count ?? 0;
|
|
343
|
+
const p2tr_output_count = opts.p2tr_output_count ?? 0;
|
|
344
|
+
|
|
345
|
+
const output_count =
|
|
346
|
+
p2pkh_output_count +
|
|
347
|
+
p2sh_output_count +
|
|
348
|
+
p2sh_p2wpkh_output_count +
|
|
349
|
+
p2sh_p2wsh_output_count +
|
|
350
|
+
p2wpkh_output_count +
|
|
351
|
+
p2wsh_output_count +
|
|
352
|
+
p2tr_output_count;
|
|
353
|
+
|
|
354
|
+
const inputVBytes =
|
|
355
|
+
p2wpkh_input_count * this.P2WPKH_IN_SIZE + p2tr_input_count * this.P2TR_IN_SIZE;
|
|
356
|
+
|
|
357
|
+
const inputWitnessBytes = p2wpkh_input_count * 107 + p2tr_input_count * 65;
|
|
358
|
+
|
|
359
|
+
const txVBytes =
|
|
360
|
+
this.getTxOverheadVBytes('p2wpkh', totalInputCount, output_count) +
|
|
361
|
+
inputVBytes +
|
|
362
|
+
this.P2PKH_OUT_SIZE * p2pkh_output_count +
|
|
363
|
+
this.P2SH_OUT_SIZE * p2sh_output_count +
|
|
364
|
+
this.P2SH_P2WPKH_OUT_SIZE * p2sh_p2wpkh_output_count +
|
|
365
|
+
this.P2SH_P2WSH_OUT_SIZE * p2sh_p2wsh_output_count +
|
|
366
|
+
this.P2WPKH_OUT_SIZE * p2wpkh_output_count +
|
|
367
|
+
this.P2WSH_OUT_SIZE * p2wsh_output_count +
|
|
368
|
+
this.P2TR_OUT_SIZE * p2tr_output_count;
|
|
369
|
+
|
|
370
|
+
const txBytes =
|
|
371
|
+
this.getTxOverheadExtraRawBytes('p2wpkh', totalInputCount) + txVBytes + inputWitnessBytes;
|
|
372
|
+
const txWeight = txVBytes * 4;
|
|
373
|
+
|
|
374
|
+
return { txVBytes, txBytes, txWeight };
|
|
375
|
+
}
|
|
376
|
+
|
|
313
377
|
estimateFee(vbyte: number, satVb: number) {
|
|
314
378
|
if (isNaN(vbyte) || isNaN(satVb)) {
|
|
315
379
|
throw new Error('Parameters should be numbers');
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as btc from '@scure/btc-signer';
|
|
2
|
+
import type { InputData } from 'coin-selection/coin-selection.utils';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
CoinSelectionRecipient,
|
|
@@ -24,7 +25,7 @@ export interface GenerateBitcoinUnsignedTransactionArgs<T> {
|
|
|
24
25
|
payerLookup(keyOrigin: string): BitcoinNativeSegwitPayer | BitcoinTaprootPayer | undefined;
|
|
25
26
|
}
|
|
26
27
|
export function generateBitcoinUnsignedTransaction<
|
|
27
|
-
T extends
|
|
28
|
+
T extends InputData & { vout: number; keyOrigin: string },
|
|
28
29
|
>({
|
|
29
30
|
feeRate,
|
|
30
31
|
isSendingMax,
|