@leather.io/bitcoin 0.36.6 → 0.37.1

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/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.36.6",
5
+ "version": "0.37.1",
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/constants": "0.31.0",
36
- "@leather.io/crypto": "1.12.16",
37
- "@leather.io/utils": "0.49.10",
38
- "@leather.io/models": "0.53.0"
35
+ "@leather.io/crypto": "1.12.18",
36
+ "@leather.io/constants": "0.33.0",
37
+ "@leather.io/models": "0.54.0",
38
+ "@leather.io/utils": "0.50.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "prettier": "3.5.1",
@@ -44,9 +44,9 @@
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "2.1.9",
46
46
  "@leather.io/prettier-config": "0.9.0",
47
- "@leather.io/test-config": "0.1.3",
47
+ "@leather.io/rpc": "2.21.12",
48
48
  "@leather.io/tsconfig-config": "0.11.1",
49
- "@leather.io/rpc": "2.21.10"
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(50087948);
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(50085342);
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(50073585);
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(50046950);
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(49969100);
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
  });
@@ -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 { filterUneconomicalUtxos, getSpendableAmount } from './coin-selection.utils';
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: { value: number; txid: string }[];
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 as any,
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 }] as any[],
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: [{ value: 50_000 }, { value: 50_000 }] as any[],
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
- ] as any[],
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 as any,
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) as any,
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 as any,
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 = [{ value: 1000 }, { value: 2000 }, { value: 3000 }];
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
- inputLength: utxos.length,
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 as any,
234
+ utxos: utxos,
225
235
  recipients,
226
236
  feeRate,
227
237
  });
@@ -230,4 +240,121 @@ describe(determineUtxosForSpend.name, () => {
230
240
  expect(result.fee.amount.isEqualTo(735)).toBeTruthy();
231
241
  expect(fee).toEqual(735);
232
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
+ });
266
+ });
267
+
268
+ describe('mixed input types', () => {
269
+ test('that spending from P2TR-only inputs produces correct fee estimation', () => {
270
+ const taprootUtxos = generateMockTaprootTransactions([50_000]);
271
+ const estimation = determineUtxosForSpend({
272
+ utxos: taprootUtxos,
273
+ recipients: [
274
+ {
275
+ address: recipientAddress,
276
+ amount: createMoney(40_000, 'BTC'),
277
+ },
278
+ ],
279
+ feeRate: 20,
280
+ });
281
+ expect(estimation.fee.amount.toNumber()).toBeGreaterThan(0);
282
+ expect(estimation.inputs).toHaveLength(1);
283
+ expect(estimation.txVBytes).toBeGreaterThanOrEqual(130);
284
+ expect(estimation.txVBytes).toBeLessThan(155);
285
+ });
286
+
287
+ test('that spending from mixed P2WPKH + P2TR inputs uses correct vBytes', () => {
288
+ const nativeSegwitUtxos = generateMockTransactions([30_000]);
289
+ const taprootUtxos = generateMockTaprootTransactions([30_000]);
290
+ const mixedUtxos = [...nativeSegwitUtxos, ...taprootUtxos];
291
+
292
+ const estimation = determineUtxosForSpend({
293
+ utxos: mixedUtxos,
294
+ recipients: [
295
+ {
296
+ address: recipientAddress,
297
+ amount: createMoney(50_000, 'BTC'),
298
+ },
299
+ ],
300
+ feeRate: 20,
301
+ });
302
+ expect(estimation.inputs).toHaveLength(2);
303
+ expect(estimation.fee.amount.toNumber()).toBeGreaterThan(0);
304
+ expect(estimation.txVBytes).toBeGreaterThan(170);
305
+ expect(estimation.txVBytes).toBeLessThan(210);
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
+ });
233
360
  });
@@ -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 { filterUneconomicalUtxos, getSizeInfo, getUtxoTotal } from './coin-selection.utils';
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 { value: number; txid: string }>({
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
- inputLength: filteredUtxos.length,
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 { value: number; txid: string }>({
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
- inputLength: neededUtxos.length,
87
+ utxos: neededUtxos,
83
88
  recipients,
84
89
  });
85
90
  }