@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +37 -0
- package/dist/index.d.ts +233 -170
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -10
- 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/calculate-max-spend.ts +6 -2
- package/src/coin-selection/coin-selection.mocks.ts +19 -0
- package/src/coin-selection/coin-selection.spec.ts +161 -34
- package/src/coin-selection/coin-selection.ts +10 -5
- package/src/coin-selection/coin-selection.utils.spec.ts +113 -2
- package/src/coin-selection/coin-selection.utils.ts +42 -12
- package/src/fees/bitcoin-fees.spec.ts +71 -3
- 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.spec.ts +351 -13
- package/src/transactions/generate-unsigned-transaction.ts +2 -1
- package/src/utils/bitcoin.utils.ts +8 -0
|
@@ -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
|
});
|
|
@@ -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,31 +75,34 @@ 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,
|
|
98
|
+
isSendMax,
|
|
70
99
|
}: GetSpendableAmountArgs<T>) {
|
|
71
100
|
const balance = utxos.map(utxo => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
72
101
|
|
|
73
102
|
const size = getSizeInfo({
|
|
74
|
-
|
|
103
|
+
utxos,
|
|
75
104
|
recipients,
|
|
105
|
+
isSendMax,
|
|
76
106
|
});
|
|
77
107
|
const fee = Math.ceil(size.txVBytes * feeRate);
|
|
78
108
|
const bigNumberBalance = BigNumber(balance);
|
|
@@ -83,7 +113,7 @@ export function getSpendableAmount<T extends { value: number }>({
|
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
// Check if the spendable amount drops when adding a utxo
|
|
86
|
-
export function filterUneconomicalUtxos<T extends
|
|
116
|
+
export function filterUneconomicalUtxos<T extends InputData>({
|
|
87
117
|
utxos,
|
|
88
118
|
feeRate,
|
|
89
119
|
recipients,
|
|
@@ -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,
|
|
@@ -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 = {
|
|
@@ -68,7 +136,7 @@ describe('getBitcoinFees', () => {
|
|
|
68
136
|
];
|
|
69
137
|
const utxos = [
|
|
70
138
|
{
|
|
71
|
-
address:
|
|
139
|
+
address: recipientAddress,
|
|
72
140
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
73
141
|
vout: 0,
|
|
74
142
|
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');
|