@leather.io/bitcoin 0.37.0 → 0.37.2
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 +31 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/coin-selection/calculate-max-spend.spec.ts +125 -6
- package/src/coin-selection/coin-selection.spec.ts +76 -0
- package/src/coin-selection/coin-selection.utils.spec.ts +113 -2
- package/src/coin-selection/coin-selection.utils.ts +2 -0
- package/src/fees/bitcoin-fees.spec.ts +69 -1
- package/src/transactions/generate-unsigned-transaction.spec.ts +351 -13
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.37.
|
|
5
|
+
"version": "0.37.2",
|
|
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/
|
|
35
|
+
"@leather.io/constants": "0.34.0",
|
|
36
36
|
"@leather.io/models": "0.54.0",
|
|
37
|
-
"@leather.io/utils": "0.
|
|
38
|
-
"@leather.io/
|
|
37
|
+
"@leather.io/utils": "0.51.0",
|
|
38
|
+
"@leather.io/crypto": "1.12.19"
|
|
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/
|
|
47
|
-
"@leather.io/rpc": "2.21.
|
|
46
|
+
"@leather.io/prettier-config": "0.9.0",
|
|
47
|
+
"@leather.io/rpc": "2.21.13",
|
|
48
48
|
"@leather.io/tsconfig-config": "0.11.1",
|
|
49
|
-
"@leather.io/
|
|
49
|
+
"@leather.io/test-config": "0.1.3"
|
|
50
50
|
},
|
|
51
51
|
"keywords": [
|
|
52
52
|
"bitcoin",
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { createMoney } from '@leather.io/utils';
|
|
2
|
+
|
|
3
|
+
import { recipientAddress, taprootAddress } from '../mocks/mocks';
|
|
1
4
|
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
2
5
|
import { calculateMaxSpend } from './calculate-max-spend';
|
|
3
|
-
import { mockUtxos } from './coin-selection.mocks';
|
|
6
|
+
import { mockTaprootUtxos, mockUtxos } from './coin-selection.mocks';
|
|
7
|
+
import { getSizeInfo } from './coin-selection.utils';
|
|
4
8
|
|
|
5
9
|
const recipient = createBitcoinAddress('');
|
|
6
10
|
describe(calculateMaxSpend.name, () => {
|
|
11
|
+
test('that empty utxos returns zero', () => {
|
|
12
|
+
const result = calculateMaxSpend({ recipient: recipientAddress, utxos: [], feeRate: 1 });
|
|
13
|
+
expect(result.spendAllFee).toEqual(0);
|
|
14
|
+
expect(result.amount.amount.toNumber()).toEqual(0);
|
|
15
|
+
expect(result.spendableBitcoin.toNumber()).toEqual(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
7
18
|
test('with 1 sat/vb fee', () => {
|
|
8
19
|
const maxBitcoinSpend = calculateMaxSpend({
|
|
9
20
|
recipient,
|
|
10
21
|
utxos: mockUtxos,
|
|
11
22
|
feeRate: 1,
|
|
12
23
|
});
|
|
13
|
-
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(
|
|
24
|
+
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50087979);
|
|
25
|
+
expect(maxBitcoinSpend.spendAllFee).toEqual(621);
|
|
14
26
|
});
|
|
15
27
|
|
|
16
28
|
test('with 5 sat/vb fee', () => {
|
|
@@ -19,7 +31,8 @@ describe(calculateMaxSpend.name, () => {
|
|
|
19
31
|
utxos: mockUtxos,
|
|
20
32
|
feeRate: 5,
|
|
21
33
|
});
|
|
22
|
-
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(
|
|
34
|
+
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50085497);
|
|
35
|
+
expect(maxBitcoinSpend.spendAllFee).toEqual(3103);
|
|
23
36
|
});
|
|
24
37
|
|
|
25
38
|
test('with 30 sat/vb fee', () => {
|
|
@@ -28,7 +41,8 @@ describe(calculateMaxSpend.name, () => {
|
|
|
28
41
|
utxos: mockUtxos,
|
|
29
42
|
feeRate: 30,
|
|
30
43
|
});
|
|
31
|
-
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(
|
|
44
|
+
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50074515);
|
|
45
|
+
expect(maxBitcoinSpend.spendAllFee).toEqual(10485);
|
|
32
46
|
});
|
|
33
47
|
|
|
34
48
|
test('with 100 sat/vb fee', () => {
|
|
@@ -37,7 +51,8 @@ describe(calculateMaxSpend.name, () => {
|
|
|
37
51
|
utxos: mockUtxos,
|
|
38
52
|
feeRate: 100,
|
|
39
53
|
});
|
|
40
|
-
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(
|
|
54
|
+
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50050050);
|
|
55
|
+
expect(maxBitcoinSpend.spendAllFee).toEqual(34950);
|
|
41
56
|
});
|
|
42
57
|
|
|
43
58
|
test('with 400 sat/vb fee', () => {
|
|
@@ -46,6 +61,110 @@ describe(calculateMaxSpend.name, () => {
|
|
|
46
61
|
utxos: mockUtxos,
|
|
47
62
|
feeRate: 400,
|
|
48
63
|
});
|
|
49
|
-
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(
|
|
64
|
+
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(49981500);
|
|
65
|
+
expect(maxBitcoinSpend.spendAllFee).toEqual(58500);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('with taproot UTXOs', () => {
|
|
69
|
+
const taprootRecipient = recipientAddress;
|
|
70
|
+
|
|
71
|
+
test('with 1 sat/vb fee', () => {
|
|
72
|
+
const result = calculateMaxSpend({
|
|
73
|
+
recipient: taprootRecipient,
|
|
74
|
+
utxos: mockTaprootUtxos,
|
|
75
|
+
feeRate: 1,
|
|
76
|
+
});
|
|
77
|
+
expect(result.amount.amount.toNumber()).toEqual(50088043);
|
|
78
|
+
expect(result.spendAllFee).toEqual(557);
|
|
79
|
+
expect(result.spendableBitcoin.toNumber()).toEqual(0.50088043);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('with 5 sat/vb fee', () => {
|
|
83
|
+
const result = calculateMaxSpend({
|
|
84
|
+
recipient: taprootRecipient,
|
|
85
|
+
utxos: mockTaprootUtxos,
|
|
86
|
+
feeRate: 5,
|
|
87
|
+
});
|
|
88
|
+
expect(result.amount.amount.toNumber()).toEqual(50085815);
|
|
89
|
+
expect(result.spendAllFee).toEqual(2785);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('with 30 sat/vb fee', () => {
|
|
93
|
+
const result = calculateMaxSpend({
|
|
94
|
+
recipient: taprootRecipient,
|
|
95
|
+
utxos: mockTaprootUtxos,
|
|
96
|
+
feeRate: 30,
|
|
97
|
+
});
|
|
98
|
+
expect(result.amount.amount.toNumber()).toEqual(50075160);
|
|
99
|
+
expect(result.spendAllFee).toEqual(9840);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('with 100 sat/vb fee', () => {
|
|
103
|
+
const result = calculateMaxSpend({
|
|
104
|
+
recipient: taprootRecipient,
|
|
105
|
+
utxos: mockTaprootUtxos,
|
|
106
|
+
feeRate: 100,
|
|
107
|
+
});
|
|
108
|
+
expect(result.amount.amount.toNumber()).toEqual(50052200);
|
|
109
|
+
expect(result.spendAllFee).toEqual(32800);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('with 400 sat/vb fee', () => {
|
|
113
|
+
const result = calculateMaxSpend({
|
|
114
|
+
recipient: taprootRecipient,
|
|
115
|
+
utxos: mockTaprootUtxos,
|
|
116
|
+
feeRate: 400,
|
|
117
|
+
});
|
|
118
|
+
expect(result.amount.amount.toNumber()).toEqual(49979600);
|
|
119
|
+
expect(result.spendAllFee).toEqual(85400);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('taproot inputs have lower fees than native segwit at same fee rate', () => {
|
|
123
|
+
const segwitResult = calculateMaxSpend({
|
|
124
|
+
recipient: taprootRecipient,
|
|
125
|
+
utxos: mockUtxos,
|
|
126
|
+
feeRate: 30,
|
|
127
|
+
});
|
|
128
|
+
const taprootResult = calculateMaxSpend({
|
|
129
|
+
recipient: taprootRecipient,
|
|
130
|
+
utxos: mockTaprootUtxos,
|
|
131
|
+
feeRate: 30,
|
|
132
|
+
});
|
|
133
|
+
expect(taprootResult.spendAllFee).toBeLessThan(segwitResult.spendAllFee);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('send-max fee should not include a change output', () => {
|
|
138
|
+
const feeRate = 10;
|
|
139
|
+
const utxos = mockTaprootUtxos;
|
|
140
|
+
|
|
141
|
+
const result = calculateMaxSpend({ recipient: recipientAddress, utxos, feeRate });
|
|
142
|
+
|
|
143
|
+
const correctSize = getSizeInfo({
|
|
144
|
+
utxos,
|
|
145
|
+
recipients: [{ address: recipientAddress, amount: createMoney(0, 'BTC') }],
|
|
146
|
+
isSendMax: true,
|
|
147
|
+
});
|
|
148
|
+
const correctFee = Math.ceil(correctSize.txVBytes * feeRate);
|
|
149
|
+
|
|
150
|
+
expect(result.spendAllFee).toEqual(correctFee);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('with taproot recipient', () => {
|
|
154
|
+
test('P2TR output costs more than P2WPKH output', () => {
|
|
155
|
+
const segwitRecipientResult = calculateMaxSpend({
|
|
156
|
+
recipient: recipientAddress,
|
|
157
|
+
utxos: mockUtxos,
|
|
158
|
+
feeRate: 30,
|
|
159
|
+
});
|
|
160
|
+
const taprootRecipientResult = calculateMaxSpend({
|
|
161
|
+
recipient: taprootAddress,
|
|
162
|
+
utxos: mockUtxos,
|
|
163
|
+
feeRate: 30,
|
|
164
|
+
});
|
|
165
|
+
expect(segwitRecipientResult.amount.amount.toNumber()).toEqual(50073585);
|
|
166
|
+
expect(taprootRecipientResult.amount.amount.toNumber()).toEqual(50073225);
|
|
167
|
+
expect(taprootRecipientResult.spendAllFee).toBeGreaterThan(segwitRecipientResult.spendAllFee);
|
|
168
|
+
});
|
|
50
169
|
});
|
|
51
170
|
});
|
|
@@ -240,6 +240,29 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
240
240
|
expect(result.fee.amount.isEqualTo(735)).toBeTruthy();
|
|
241
241
|
expect(fee).toEqual(735);
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
test('that spending all taproot utxos with sendMax produces lower fee than native segwit', () => {
|
|
245
|
+
const taprootUtxos = generateMockTaprootTransactions([1000, 2000, 3000]);
|
|
246
|
+
const segwitUtxos = generateMockTransactions([1000, 2000, 3000]);
|
|
247
|
+
const feeRate = 2;
|
|
248
|
+
|
|
249
|
+
const taprootResult = determineUtxosForSpendAll({
|
|
250
|
+
utxos: taprootUtxos,
|
|
251
|
+
recipients: [{ address: recipientAddress, amount: createMoney(5000, 'BTC') }],
|
|
252
|
+
feeRate,
|
|
253
|
+
});
|
|
254
|
+
const segwitResult = determineUtxosForSpendAll({
|
|
255
|
+
utxos: segwitUtxos,
|
|
256
|
+
recipients: [{ address: recipientAddress, amount: createMoney(5000, 'BTC') }],
|
|
257
|
+
feeRate,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(taprootResult.inputs.length).toEqual(3);
|
|
261
|
+
expect(taprootResult.outputs.length).toEqual(1);
|
|
262
|
+
expect(taprootResult.fee.amount.isLessThan(segwitResult.fee.amount)).toBeTruthy();
|
|
263
|
+
expect(taprootResult.size).toBeGreaterThan(200);
|
|
264
|
+
expect(taprootResult.size).toBeLessThan(220);
|
|
265
|
+
});
|
|
243
266
|
});
|
|
244
267
|
|
|
245
268
|
describe('mixed input types', () => {
|
|
@@ -281,4 +304,57 @@ describe('mixed input types', () => {
|
|
|
281
304
|
expect(estimation.txVBytes).toBeGreaterThan(170);
|
|
282
305
|
expect(estimation.txVBytes).toBeLessThan(210);
|
|
283
306
|
});
|
|
307
|
+
|
|
308
|
+
test('that spending all mixed P2WPKH + P2TR utxos includes inputs from both address types', () => {
|
|
309
|
+
const nativeSegwitUtxos = generateMockTransactions([2000, 3000]);
|
|
310
|
+
const taprootUtxos = generateMockTaprootTransactions([2000, 3000]);
|
|
311
|
+
const allTaprootUtxos = generateMockTaprootTransactions([2000, 3000, 2000, 3000]);
|
|
312
|
+
const mixedUtxos = [...nativeSegwitUtxos, ...taprootUtxos];
|
|
313
|
+
const feeRate = 4;
|
|
314
|
+
|
|
315
|
+
const mixedResult = determineUtxosForSpendAll({
|
|
316
|
+
utxos: mixedUtxos,
|
|
317
|
+
recipients: [{ address: recipientAddress, amount: createMoney(8000, 'BTC') }],
|
|
318
|
+
feeRate,
|
|
319
|
+
});
|
|
320
|
+
const allTaprootResult = determineUtxosForSpendAll({
|
|
321
|
+
utxos: allTaprootUtxos,
|
|
322
|
+
recipients: [{ address: recipientAddress, amount: createMoney(8000, 'BTC') }],
|
|
323
|
+
feeRate,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(mixedResult.inputs.length).toEqual(4);
|
|
327
|
+
expect(mixedResult.outputs.length).toEqual(1);
|
|
328
|
+
|
|
329
|
+
const hasSegwit = mixedResult.inputs.some(u => u.address.startsWith('tb1q'));
|
|
330
|
+
const hasTaproot = mixedResult.inputs.some(u => u.address.startsWith('tb1p'));
|
|
331
|
+
expect(hasSegwit).toBeTruthy();
|
|
332
|
+
expect(hasTaproot).toBeTruthy();
|
|
333
|
+
|
|
334
|
+
expect(mixedResult.size).toBeGreaterThan(280);
|
|
335
|
+
expect(mixedResult.size).toBeLessThan(300);
|
|
336
|
+
expect(mixedResult.fee.amount.isGreaterThan(allTaprootResult.fee.amount)).toBeTruthy();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('that mixed P2WPKH + P2TR inputs are sorted by value descending', () => {
|
|
340
|
+
const nativeSegwitUtxos = generateMockTransactions([15_000, 40_000]);
|
|
341
|
+
const taprootUtxos = generateMockTaprootTransactions([25_000, 30_000]);
|
|
342
|
+
const utxos = [...nativeSegwitUtxos, ...taprootUtxos];
|
|
343
|
+
|
|
344
|
+
const result = determineUtxosForSpend({
|
|
345
|
+
utxos,
|
|
346
|
+
recipients: [
|
|
347
|
+
{
|
|
348
|
+
address: recipientAddress,
|
|
349
|
+
amount: createMoney(50_000, 'BTC'),
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
feeRate: 20,
|
|
353
|
+
});
|
|
354
|
+
result.inputs.forEach((input, i) => {
|
|
355
|
+
const nextInput = result.inputs[i + 1];
|
|
356
|
+
if (!nextInput) return;
|
|
357
|
+
expect(input.value >= nextInput.value).toEqual(true);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
284
360
|
});
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { createMoney } from '@leather.io/utils';
|
|
2
2
|
|
|
3
3
|
import { recipientAddress } from '../mocks/mocks';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
generateMockTaprootTransactions,
|
|
6
|
+
generateMockTransactions,
|
|
7
|
+
mockTaprootUtxos,
|
|
8
|
+
mockUtxos,
|
|
9
|
+
} from './coin-selection.mocks';
|
|
10
|
+
import {
|
|
11
|
+
countInputsByScriptType,
|
|
12
|
+
filterUneconomicalUtxos,
|
|
13
|
+
getSpendableAmount,
|
|
14
|
+
} from './coin-selection.utils';
|
|
6
15
|
|
|
7
16
|
describe(filterUneconomicalUtxos.name, () => {
|
|
8
17
|
const recipients = [
|
|
@@ -62,4 +71,106 @@ describe(filterUneconomicalUtxos.name, () => {
|
|
|
62
71
|
});
|
|
63
72
|
expect(filteredUtxos.length).toEqual(2);
|
|
64
73
|
});
|
|
74
|
+
|
|
75
|
+
describe('with taproot utxos', () => {
|
|
76
|
+
test('that every surviving taproot utxo increases spendable amount', () => {
|
|
77
|
+
const feeRate = 200;
|
|
78
|
+
const survivors = filterUneconomicalUtxos({
|
|
79
|
+
utxos: mockTaprootUtxos,
|
|
80
|
+
feeRate,
|
|
81
|
+
recipients,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { spendableAmount: fullAmount } = getSpendableAmount({
|
|
85
|
+
utxos: mockTaprootUtxos,
|
|
86
|
+
feeRate,
|
|
87
|
+
recipients,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
for (const utxo of survivors) {
|
|
91
|
+
const { spendableAmount: withoutUtxo } = getSpendableAmount({
|
|
92
|
+
utxos: mockTaprootUtxos.filter(u => u.txid !== utxo.txid),
|
|
93
|
+
feeRate,
|
|
94
|
+
recipients,
|
|
95
|
+
});
|
|
96
|
+
expect(fullAmount.toNumber()).toBeGreaterThan(withoutUtxo.toNumber());
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('that every filtered taproot utxo does not increase spendable amount', () => {
|
|
101
|
+
const feeRate = 200;
|
|
102
|
+
const survivors = filterUneconomicalUtxos({
|
|
103
|
+
utxos: mockTaprootUtxos,
|
|
104
|
+
feeRate,
|
|
105
|
+
recipients,
|
|
106
|
+
});
|
|
107
|
+
const filtered = mockTaprootUtxos.filter(u => !survivors.some(s => s.txid === u.txid));
|
|
108
|
+
|
|
109
|
+
expect(filtered.length).toBeGreaterThan(0);
|
|
110
|
+
|
|
111
|
+
const { spendableAmount: fullAmount } = getSpendableAmount({
|
|
112
|
+
utxos: mockTaprootUtxos,
|
|
113
|
+
feeRate,
|
|
114
|
+
recipients,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const utxo of filtered) {
|
|
118
|
+
const { spendableAmount: withoutUtxo } = getSpendableAmount({
|
|
119
|
+
utxos: mockTaprootUtxos.filter(u => u.txid !== utxo.txid),
|
|
120
|
+
feeRate,
|
|
121
|
+
recipients,
|
|
122
|
+
});
|
|
123
|
+
expect(withoutUtxo.toNumber()).toBeGreaterThanOrEqual(fullAmount.toNumber());
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('that taproot inputs are cheaper so more survive than native segwit', () => {
|
|
128
|
+
const feeRate = 10;
|
|
129
|
+
const segwitResult = filterUneconomicalUtxos({
|
|
130
|
+
utxos: mockUtxos,
|
|
131
|
+
feeRate,
|
|
132
|
+
recipients,
|
|
133
|
+
});
|
|
134
|
+
const taprootResult = filterUneconomicalUtxos({
|
|
135
|
+
utxos: mockTaprootUtxos,
|
|
136
|
+
feeRate,
|
|
137
|
+
recipients,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(taprootResult.length).toBeGreaterThan(segwitResult.length);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('that taproot utxo below dust threshold is filtered even at low fee rate', () => {
|
|
144
|
+
const utxosWithDust = generateMockTaprootTransactions([200, 50000000]);
|
|
145
|
+
const result = filterUneconomicalUtxos({
|
|
146
|
+
utxos: utxosWithDust,
|
|
147
|
+
feeRate: 1,
|
|
148
|
+
recipients,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.length).toEqual(1);
|
|
152
|
+
expect(result[0].value).toEqual(50000000);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe(countInputsByScriptType.name, () => {
|
|
158
|
+
test('all native segwit utxos', () => {
|
|
159
|
+
const result = countInputsByScriptType(mockUtxos);
|
|
160
|
+
expect(result).toEqual({ p2wpkh: 9, p2tr: 0 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('all taproot utxos', () => {
|
|
164
|
+
const result = countInputsByScriptType(mockTaprootUtxos);
|
|
165
|
+
expect(result).toEqual({ p2wpkh: 0, p2tr: 9 });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('mixed utxos', () => {
|
|
169
|
+
const mixed = [
|
|
170
|
+
...generateMockTransactions([1000, 2000]),
|
|
171
|
+
...generateMockTaprootTransactions([3000, 4000, 5000]),
|
|
172
|
+
];
|
|
173
|
+
const result = countInputsByScriptType(mixed);
|
|
174
|
+
expect(result).toEqual({ p2wpkh: 2, p2tr: 3 });
|
|
175
|
+
});
|
|
65
176
|
});
|
|
@@ -95,12 +95,14 @@ export function getSpendableAmount<T extends InputData>({
|
|
|
95
95
|
utxos,
|
|
96
96
|
feeRate,
|
|
97
97
|
recipients,
|
|
98
|
+
isSendMax,
|
|
98
99
|
}: GetSpendableAmountArgs<T>) {
|
|
99
100
|
const balance = utxos.map(utxo => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
100
101
|
|
|
101
102
|
const size = getSizeInfo({
|
|
102
103
|
utxos,
|
|
103
104
|
recipients,
|
|
105
|
+
isSendMax,
|
|
104
106
|
});
|
|
105
107
|
const fee = Math.ceil(size.txVBytes * feeRate);
|
|
106
108
|
const bigNumberBalance = BigNumber(balance);
|
|
@@ -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 } from '../mocks/mocks';
|
|
7
|
+
import { recipientAddress, taprootAddress } from '../mocks/mocks';
|
|
8
8
|
import { getBitcoinFees, getBitcoinTransactionFee } from './bitcoin-fees';
|
|
9
9
|
|
|
10
10
|
describe('getBitcoinTransactionFee', () => {
|
|
@@ -56,6 +56,74 @@ describe('getBitcoinTransactionFee', () => {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
describe('getBitcoinTransactionFee with taproot', () => {
|
|
60
|
+
const mockTxid = '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6';
|
|
61
|
+
const defaultRecipients = [{ address: recipientAddress, amount: createMoney(1000, 'BTC') }];
|
|
62
|
+
|
|
63
|
+
it('should return a lower fee for a taproot UTXO than native segwit', () => {
|
|
64
|
+
const segwitFee = getBitcoinTransactionFee({
|
|
65
|
+
recipients: defaultRecipients,
|
|
66
|
+
utxos: [{ address: recipientAddress, txid: mockTxid, value: 2000 }],
|
|
67
|
+
feeRate: 1,
|
|
68
|
+
});
|
|
69
|
+
const taprootFee = getBitcoinTransactionFee({
|
|
70
|
+
recipients: defaultRecipients,
|
|
71
|
+
utxos: [{ address: taprootAddress, txid: mockTxid, value: 2000 }],
|
|
72
|
+
feeRate: 1,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(segwitFee).not.toBeNull();
|
|
76
|
+
expect(taprootFee).not.toBeNull();
|
|
77
|
+
expect(taprootFee!.amount.toNumber()).toBeLessThan(segwitFee!.amount.toNumber());
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return a lower fee for taproot max send than native segwit', () => {
|
|
81
|
+
const maxSendRecipients = [{ address: recipientAddress, amount: createMoney(2000, 'BTC') }];
|
|
82
|
+
|
|
83
|
+
const segwitFee = getBitcoinTransactionFee({
|
|
84
|
+
isSendingMax: true,
|
|
85
|
+
recipients: maxSendRecipients,
|
|
86
|
+
utxos: [{ address: recipientAddress, txid: mockTxid, value: 2000 }],
|
|
87
|
+
feeRate: 2,
|
|
88
|
+
});
|
|
89
|
+
const taprootFee = getBitcoinTransactionFee({
|
|
90
|
+
isSendingMax: true,
|
|
91
|
+
recipients: maxSendRecipients,
|
|
92
|
+
utxos: [{ address: taprootAddress, txid: mockTxid, value: 2000 }],
|
|
93
|
+
feeRate: 2,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(segwitFee).not.toBeNull();
|
|
97
|
+
expect(taprootFee).not.toBeNull();
|
|
98
|
+
expect(taprootFee!.amount.toNumber()).toBeLessThan(segwitFee!.amount.toNumber());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return a lower fee for mixed UTXOs than all native segwit', () => {
|
|
102
|
+
const mixedRecipients = [{ address: recipientAddress, amount: createMoney(15000, 'BTC') }];
|
|
103
|
+
|
|
104
|
+
const allSegwitFee = getBitcoinTransactionFee({
|
|
105
|
+
recipients: mixedRecipients,
|
|
106
|
+
utxos: [
|
|
107
|
+
{ address: recipientAddress, txid: mockTxid, value: 10000 },
|
|
108
|
+
{ address: recipientAddress, txid: mockTxid, value: 10000 },
|
|
109
|
+
],
|
|
110
|
+
feeRate: 1,
|
|
111
|
+
});
|
|
112
|
+
const mixedFee = getBitcoinTransactionFee({
|
|
113
|
+
recipients: mixedRecipients,
|
|
114
|
+
utxos: [
|
|
115
|
+
{ address: recipientAddress, txid: mockTxid, value: 10000 },
|
|
116
|
+
{ address: taprootAddress, txid: mockTxid, value: 10000 },
|
|
117
|
+
],
|
|
118
|
+
feeRate: 1,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(allSegwitFee).not.toBeNull();
|
|
122
|
+
expect(mixedFee).not.toBeNull();
|
|
123
|
+
expect(mixedFee!.amount.toNumber()).toBeLessThan(allSegwitFee!.amount.toNumber());
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
59
127
|
describe('getBitcoinFees', () => {
|
|
60
128
|
it('should return the fees for different fee rates', () => {
|
|
61
129
|
const feeRates: AverageBitcoinFeeRates = {
|