@leather.io/bitcoin 0.19.29 → 0.19.30
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 +6 -6
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +197 -148
- package/dist/index.js +341 -205
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/bip322/bip322-utils.ts +1 -1
- package/src/bip322/sign-message-bip322-bitcoinjs.ts +8 -3
- package/src/bip322/sign-message-bip322.spec.ts +13 -13
- package/src/coin-selection/calculate-max-spend.spec.ts +19 -17
- package/src/coin-selection/calculate-max-spend.ts +26 -16
- package/src/coin-selection/coin-selection.spec.ts +29 -26
- package/src/coin-selection/coin-selection.ts +1 -1
- package/src/coin-selection/coin-selection.utils.spec.ts +2 -1
- package/src/coin-selection/coin-selection.utils.ts +5 -8
- package/src/fees/bitcoin-fees.spec.ts +7 -10
- package/src/index.ts +21 -9
- package/src/mocks/mocks.ts +39 -0
- package/src/{p2tr-address-gen.spec.ts → payments/p2tr-address-gen.spec.ts} +1 -1
- package/src/{p2tr-address-gen.ts → payments/p2tr-address-gen.ts} +2 -2
- package/src/{p2wpkh-address-gen.ts → payments/p2wpkh-address-gen.ts} +2 -2
- package/src/psbt/psbt-details.ts +3 -3
- package/src/psbt/psbt-inputs.ts +9 -6
- package/src/psbt/psbt-outputs.ts +10 -7
- package/src/psbt/psbt-totals.ts +3 -3
- package/src/{bitcoin-signer.ts → signer/bitcoin-signer.ts} +6 -5
- package/src/transactions/generate-unsigned-transaction.spec.ts +1 -1
- package/src/transactions/generate-unsigned-transaction.ts +3 -3
- package/src/{bitcoin.network.ts → utils/bitcoin.network.ts} +2 -0
- package/src/{bitcoin.utils.spec.ts → utils/bitcoin.utils.spec.ts} +19 -14
- package/src/{bitcoin.utils.ts → utils/bitcoin.utils.ts} +19 -13
- package/src/{lookup-derivation-by-address.spec.ts → utils/lookup-derivation-by-address.spec.ts} +11 -6
- package/src/{lookup-derivation-by-address.ts → utils/lookup-derivation-by-address.ts} +4 -3
- package/src/validation/address-validation.spec.ts +396 -0
- package/src/validation/address-validation.ts +28 -0
- package/src/validation/amount-validation.spec.ts +39 -0
- package/src/validation/amount-validation.ts +31 -0
- package/src/validation/bitcoin-address.ts +23 -0
- package/src/{bitcoin-error.ts → validation/bitcoin-error.ts} +4 -2
- package/src/validation/transaction-validation.spec.ts +60 -0
- package/src/validation/transaction-validation.ts +46 -0
- /package/src/{btc-size-fee-estimator.spec.ts → fees/btc-size-fee-estimator.spec.ts} +0 -0
- /package/src/{btc-size-fee-estimator.ts → fees/btc-size-fee-estimator.ts} +0 -0
- /package/src/{p2wpkh-address-gen.spec.ts → payments/p2wpkh-address-gen.spec.ts} +0 -0
- /package/src/{p2wsh-p2sh-address-gen.spec.ts → payments/p2wsh-p2sh-address-gen.spec.ts} +0 -0
- /package/src/{p2wsh-p2sh-address-gen.ts → payments/p2wsh-p2sh-address-gen.ts} +0 -0
- /package/src/{bitcoin-signer.spec.ts → signer/bitcoin-signer.spec.ts} +0 -0
|
@@ -3,7 +3,8 @@ import * as secp from '@noble/secp256k1';
|
|
|
3
3
|
import * as btc from '@scure/btc-signer';
|
|
4
4
|
import * as bitcoin from 'bitcoinjs-lib';
|
|
5
5
|
|
|
6
|
-
import { ecdsaPublicKeyToSchnorr } from '../bitcoin.utils';
|
|
6
|
+
import { ecdsaPublicKeyToSchnorr } from '../utils/bitcoin.utils';
|
|
7
|
+
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
7
8
|
import {
|
|
8
9
|
createNativeSegwitBitcoinJsSigner,
|
|
9
10
|
createTaprootBitcoinJsSigner,
|
|
@@ -11,13 +12,11 @@ import {
|
|
|
11
12
|
signBip322MessageSimple,
|
|
12
13
|
} from './sign-message-bip322-bitcoinjs';
|
|
13
14
|
|
|
15
|
+
const address = createBitcoinAddress('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
|
|
16
|
+
|
|
14
17
|
describe(createToSpendTx.name, () => {
|
|
15
18
|
test('bitcoinjs example', () => {
|
|
16
|
-
const result = createToSpendTx(
|
|
17
|
-
'bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l',
|
|
18
|
-
'generatedWithBitcoinJs',
|
|
19
|
-
'mainnet'
|
|
20
|
-
);
|
|
19
|
+
const result = createToSpendTx(address, 'generatedWithBitcoinJs', 'mainnet');
|
|
21
20
|
|
|
22
21
|
expect(result.script.toString('hex')).toEqual('00142b05d564e6a7a33c087f16e0f730d1440123799d');
|
|
23
22
|
|
|
@@ -33,7 +32,7 @@ describe(signBip322MessageSimple.name, () => {
|
|
|
33
32
|
const testVectorKey = btc.WIF().decode('L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k');
|
|
34
33
|
|
|
35
34
|
describe('Message signing, Native Segwit', () => {
|
|
36
|
-
const
|
|
35
|
+
const rawNativeSegwitAddress = btc.getAddress('wpkh', testVectorKey);
|
|
37
36
|
const payment = btc.p2wpkh(secp.getPublicKey(testVectorKey, true));
|
|
38
37
|
|
|
39
38
|
function signPsbt(psbt: bitcoin.Psbt) {
|
|
@@ -41,11 +40,11 @@ describe(signBip322MessageSimple.name, () => {
|
|
|
41
40
|
return Promise.resolve(btc.Transaction.fromPSBT(psbt.toBuffer()));
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
if (!
|
|
45
|
-
|
|
43
|
+
if (!rawNativeSegwitAddress) throw new Error('nativeSegwitAddress is undefined');
|
|
44
|
+
const nativeSegwitAddress = createBitcoinAddress(rawNativeSegwitAddress);
|
|
46
45
|
test('Addresses against native segwit test vectors', () => {
|
|
47
|
-
expect(nativeSegwitAddress).toEqual(
|
|
48
|
-
expect(payment.address).toEqual(
|
|
46
|
+
expect(nativeSegwitAddress).toEqual(address);
|
|
47
|
+
expect(payment.address).toEqual(address);
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
test('Signature: "" (empty string)', async () => {
|
|
@@ -113,10 +112,11 @@ describe(signBip322MessageSimple.name, () => {
|
|
|
113
112
|
});
|
|
114
113
|
|
|
115
114
|
describe('Message Signing, Taproot', () => {
|
|
116
|
-
const
|
|
115
|
+
const rawTaprootAddress = btc.getAddress('tr', testVectorKey);
|
|
117
116
|
|
|
118
|
-
if (!
|
|
117
|
+
if (!rawTaprootAddress) throw new Error('Could not generate taproot address');
|
|
119
118
|
|
|
119
|
+
const taprootAddress = createBitcoinAddress(rawTaprootAddress);
|
|
120
120
|
const payment = btc.p2tr(
|
|
121
121
|
ecdsaPublicKeyToSchnorr(secp.getPublicKey(Buffer.from(testVectorKey), true))
|
|
122
122
|
);
|
|
@@ -1,53 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
2
|
+
import { calculateMaxSpend } from './calculate-max-spend';
|
|
2
3
|
import { generateMockAverageFee, mockUtxos } from './coin-selection.mocks';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const recipient = createBitcoinAddress('');
|
|
6
|
+
describe(calculateMaxSpend.name, () => {
|
|
5
7
|
test('with 1 sat/vb fee', () => {
|
|
6
8
|
const fee = 1;
|
|
7
|
-
const maxBitcoinSpend =
|
|
8
|
-
|
|
9
|
+
const maxBitcoinSpend = calculateMaxSpend({
|
|
10
|
+
recipient,
|
|
9
11
|
utxos: mockUtxos,
|
|
10
|
-
|
|
12
|
+
feeRates: generateMockAverageFee(fee),
|
|
11
13
|
});
|
|
12
14
|
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50087948);
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
test('with 5 sat/vb fee', () => {
|
|
16
18
|
const fee = 5;
|
|
17
|
-
const maxBitcoinSpend =
|
|
18
|
-
|
|
19
|
+
const maxBitcoinSpend = calculateMaxSpend({
|
|
20
|
+
recipient,
|
|
19
21
|
utxos: mockUtxos,
|
|
20
|
-
|
|
22
|
+
feeRates: generateMockAverageFee(fee),
|
|
21
23
|
});
|
|
22
24
|
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50085342);
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
test('with 30 sat/vb fee', () => {
|
|
26
28
|
const fee = 30;
|
|
27
|
-
const maxBitcoinSpend =
|
|
28
|
-
|
|
29
|
+
const maxBitcoinSpend = calculateMaxSpend({
|
|
30
|
+
recipient,
|
|
29
31
|
utxos: mockUtxos,
|
|
30
|
-
|
|
32
|
+
feeRates: generateMockAverageFee(fee),
|
|
31
33
|
});
|
|
32
34
|
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50073585);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
test('with 100 sat/vb fee', () => {
|
|
36
38
|
const fee = 100;
|
|
37
|
-
const maxBitcoinSpend =
|
|
38
|
-
|
|
39
|
+
const maxBitcoinSpend = calculateMaxSpend({
|
|
40
|
+
recipient,
|
|
39
41
|
utxos: mockUtxos,
|
|
40
|
-
|
|
42
|
+
feeRates: generateMockAverageFee(fee),
|
|
41
43
|
});
|
|
42
44
|
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50046950);
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
test('with 400 sat/vb fee', () => {
|
|
46
48
|
const fee = 400;
|
|
47
|
-
const maxBitcoinSpend =
|
|
48
|
-
|
|
49
|
+
const maxBitcoinSpend = calculateMaxSpend({
|
|
50
|
+
recipient,
|
|
49
51
|
utxos: mockUtxos,
|
|
50
|
-
|
|
52
|
+
feeRates: generateMockAverageFee(fee),
|
|
51
53
|
});
|
|
52
54
|
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(49969100);
|
|
53
55
|
});
|
|
@@ -1,48 +1,58 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
|
|
3
|
-
import type { AverageBitcoinFeeRates } from '@leather.io/models';
|
|
3
|
+
import type { AverageBitcoinFeeRates, Money } from '@leather.io/models';
|
|
4
4
|
import { createMoney, satToBtc } from '@leather.io/utils';
|
|
5
5
|
|
|
6
|
-
import { CoinSelectionUtxo } from '
|
|
7
|
-
import {
|
|
6
|
+
import { CoinSelectionUtxo } from '../coin-selection/coin-selection';
|
|
7
|
+
import {
|
|
8
|
+
filterUneconomicalUtxos,
|
|
9
|
+
getSpendableAmount,
|
|
10
|
+
} from '../coin-selection/coin-selection.utils';
|
|
11
|
+
import { BitcoinAddress } from '../validation/bitcoin-address';
|
|
8
12
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
13
|
+
interface CalculateMaxSpendArgs {
|
|
14
|
+
recipient: BitcoinAddress;
|
|
11
15
|
utxos: CoinSelectionUtxo[];
|
|
12
|
-
|
|
16
|
+
feeRates?: AverageBitcoinFeeRates;
|
|
13
17
|
feeRate?: number;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
interface CalculateMaxSpendResponse {
|
|
21
|
+
spendAllFee: number;
|
|
22
|
+
amount: Money;
|
|
23
|
+
spendableBtc: BigNumber;
|
|
24
|
+
}
|
|
25
|
+
export function calculateMaxSpend({
|
|
26
|
+
recipient,
|
|
18
27
|
utxos,
|
|
19
28
|
feeRate,
|
|
20
|
-
|
|
21
|
-
}:
|
|
22
|
-
if (!utxos.length || !
|
|
29
|
+
feeRates,
|
|
30
|
+
}: CalculateMaxSpendArgs): CalculateMaxSpendResponse {
|
|
31
|
+
if (!utxos.length || !feeRates)
|
|
23
32
|
return {
|
|
24
33
|
spendAllFee: 0,
|
|
25
34
|
amount: createMoney(0, 'BTC'),
|
|
26
|
-
|
|
35
|
+
spendableBtc: new BigNumber(0),
|
|
27
36
|
};
|
|
28
37
|
|
|
29
|
-
const currentFeeRate = feeRate ??
|
|
38
|
+
const currentFeeRate = feeRate ?? feeRates.halfHourFee.toNumber();
|
|
30
39
|
|
|
31
40
|
const filteredUtxos = filterUneconomicalUtxos({
|
|
32
41
|
utxos,
|
|
33
42
|
feeRate: currentFeeRate,
|
|
34
|
-
recipients: [{ address, amount: createMoney(0, 'BTC') }],
|
|
43
|
+
recipients: [{ address: recipient, amount: createMoney(0, 'BTC') }],
|
|
35
44
|
});
|
|
36
45
|
|
|
37
46
|
const { spendableAmount, fee } = getSpendableAmount({
|
|
38
47
|
utxos: filteredUtxos,
|
|
39
48
|
feeRate: currentFeeRate,
|
|
40
|
-
recipients: [{ address, amount: createMoney(0, 'BTC') }],
|
|
49
|
+
recipients: [{ address: recipient, amount: createMoney(0, 'BTC') }],
|
|
50
|
+
isSendMax: true,
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
return {
|
|
44
54
|
spendAllFee: fee,
|
|
45
55
|
amount: createMoney(spendableAmount, 'BTC'),
|
|
46
|
-
|
|
56
|
+
spendableBtc: satToBtc(spendableAmount),
|
|
47
57
|
};
|
|
48
58
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
2
2
|
import { createMoney, createNullArrayOfLength, sumNumbers } from '@leather.io/utils';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
invalidAddress,
|
|
6
|
+
legacyAddress,
|
|
7
|
+
recipientAddress,
|
|
8
|
+
segwitAddress,
|
|
9
|
+
taprootAddress,
|
|
10
|
+
} from '../mocks/mocks';
|
|
11
|
+
import { isBitcoinAddress } from '../validation/bitcoin-address';
|
|
4
12
|
import { determineUtxosForSpend, determineUtxosForSpendAll } from './coin-selection';
|
|
5
13
|
import { filterUneconomicalUtxos, getSizeInfo } from './coin-selection.utils';
|
|
6
14
|
|
|
@@ -21,6 +29,9 @@ const demoUtxos = [
|
|
|
21
29
|
];
|
|
22
30
|
|
|
23
31
|
function generate10kSpendWithDummyUtxoSet(recipient: string) {
|
|
32
|
+
if (!isBitcoinAddress(recipient)) {
|
|
33
|
+
throw new Error('Invalid Bitcoin address');
|
|
34
|
+
}
|
|
24
35
|
return determineUtxosForSpend({
|
|
25
36
|
utxos: demoUtxos as any,
|
|
26
37
|
feeRate: 20,
|
|
@@ -35,7 +46,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
35
46
|
utxos: [{ value: 50_000 }] as any[],
|
|
36
47
|
recipients: [
|
|
37
48
|
{
|
|
38
|
-
address:
|
|
49
|
+
address: recipientAddress,
|
|
39
50
|
amount: createMoney(40_000, 'BTC'),
|
|
40
51
|
},
|
|
41
52
|
],
|
|
@@ -50,7 +61,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
50
61
|
utxos: [{ value: 50_000 }, { value: 50_000 }] as any[],
|
|
51
62
|
recipients: [
|
|
52
63
|
{
|
|
53
|
-
address:
|
|
64
|
+
address: recipientAddress,
|
|
54
65
|
amount: createMoney(60_000, 'BTC'),
|
|
55
66
|
},
|
|
56
67
|
],
|
|
@@ -76,7 +87,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
76
87
|
] as any[],
|
|
77
88
|
recipients: [
|
|
78
89
|
{
|
|
79
|
-
address:
|
|
90
|
+
address: recipientAddress,
|
|
80
91
|
amount: createMoney(100_000, 'BTC'),
|
|
81
92
|
},
|
|
82
93
|
],
|
|
@@ -89,13 +100,13 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
89
100
|
|
|
90
101
|
describe('sorting algorithm', () => {
|
|
91
102
|
test('that it filters out dust utxos', () => {
|
|
92
|
-
const result = generate10kSpendWithDummyUtxoSet(
|
|
103
|
+
const result = generate10kSpendWithDummyUtxoSet(recipientAddress);
|
|
93
104
|
const hasDust = result.filteredUtxos.some(utxo => utxo.value <= BTC_P2WPKH_DUST_AMOUNT);
|
|
94
105
|
expect(hasDust).toBeFalsy();
|
|
95
106
|
});
|
|
96
107
|
|
|
97
108
|
test('that it sorts utxos in decending order', () => {
|
|
98
|
-
const result = generate10kSpendWithDummyUtxoSet(
|
|
109
|
+
const result = generate10kSpendWithDummyUtxoSet(recipientAddress);
|
|
99
110
|
result.inputs.forEach((u, i) => {
|
|
100
111
|
const nextUtxo = result.inputs[i + 1];
|
|
101
112
|
if (!nextUtxo) return;
|
|
@@ -105,30 +116,24 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
105
116
|
});
|
|
106
117
|
|
|
107
118
|
test('that it accepts a wrapped segwit address', () =>
|
|
108
|
-
expect(() =>
|
|
109
|
-
generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH')
|
|
110
|
-
).not.toThrowError());
|
|
119
|
+
expect(() => generate10kSpendWithDummyUtxoSet(segwitAddress)).not.toThrowError());
|
|
111
120
|
|
|
112
121
|
test('that it accepts a legacy addresses', () =>
|
|
113
|
-
expect(() =>
|
|
114
|
-
generate10kSpendWithDummyUtxoSet('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj')
|
|
115
|
-
).not.toThrowError());
|
|
122
|
+
expect(() => generate10kSpendWithDummyUtxoSet(legacyAddress)).not.toThrowError());
|
|
116
123
|
|
|
117
124
|
test('that it throws an error with non-legit address', () => {
|
|
118
|
-
expect(() =>
|
|
119
|
-
generate10kSpendWithDummyUtxoSet('whoop-de-da-boop-da-de-not-a-bitcoin-address')
|
|
120
|
-
).toThrowError();
|
|
125
|
+
expect(() => generate10kSpendWithDummyUtxoSet(invalidAddress)).toThrowError();
|
|
121
126
|
});
|
|
122
127
|
|
|
123
128
|
test('that given a set of utxos, legacy is more expensive', () => {
|
|
124
|
-
const legacy = generate10kSpendWithDummyUtxoSet(
|
|
125
|
-
const segwit = generate10kSpendWithDummyUtxoSet(
|
|
129
|
+
const legacy = generate10kSpendWithDummyUtxoSet(legacyAddress);
|
|
130
|
+
const segwit = generate10kSpendWithDummyUtxoSet(segwitAddress);
|
|
126
131
|
expect(legacy.fee.amount.isGreaterThan(segwit.fee.amount)).toBeTruthy();
|
|
127
132
|
});
|
|
128
133
|
|
|
129
134
|
test('that given a set of utxos, wrapped segwit is more expensive than native', () => {
|
|
130
|
-
const segwit = generate10kSpendWithDummyUtxoSet(
|
|
131
|
-
const native = generate10kSpendWithDummyUtxoSet(
|
|
135
|
+
const segwit = generate10kSpendWithDummyUtxoSet(segwitAddress);
|
|
136
|
+
const native = generate10kSpendWithDummyUtxoSet(recipientAddress);
|
|
132
137
|
expect(segwit.fee.amount.isGreaterThan(native.fee.amount)).toBeTruthy();
|
|
133
138
|
});
|
|
134
139
|
|
|
@@ -136,10 +141,8 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
136
141
|
// Non-obvious behaviour.
|
|
137
142
|
// P2TR outputs = 34 vBytes
|
|
138
143
|
// P2WPKH outputs = 22 vBytes
|
|
139
|
-
const native = generate10kSpendWithDummyUtxoSet(
|
|
140
|
-
const taproot = generate10kSpendWithDummyUtxoSet(
|
|
141
|
-
'tb1parwmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd'
|
|
142
|
-
);
|
|
144
|
+
const native = generate10kSpendWithDummyUtxoSet(recipientAddress);
|
|
145
|
+
const taproot = generate10kSpendWithDummyUtxoSet(taprootAddress);
|
|
143
146
|
expect(taproot.fee.amount.isGreaterThan(native.fee.amount)).toBeTruthy();
|
|
144
147
|
});
|
|
145
148
|
|
|
@@ -152,7 +155,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
152
155
|
utxos: testData as any,
|
|
153
156
|
recipients: [
|
|
154
157
|
{
|
|
155
|
-
address:
|
|
158
|
+
address: recipientAddress,
|
|
156
159
|
amount: createMoney(Number(amount), 'BTC'),
|
|
157
160
|
},
|
|
158
161
|
],
|
|
@@ -172,7 +175,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
172
175
|
const feeRate = 3;
|
|
173
176
|
const recipients = [
|
|
174
177
|
{
|
|
175
|
-
address:
|
|
178
|
+
address: recipientAddress,
|
|
176
179
|
amount: createMoney(1, 'BTC'),
|
|
177
180
|
},
|
|
178
181
|
];
|
|
@@ -188,7 +191,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
188
191
|
utxos: filteredUtxos as any,
|
|
189
192
|
recipients: [
|
|
190
193
|
{
|
|
191
|
-
address:
|
|
194
|
+
address: recipientAddress,
|
|
192
195
|
amount: createMoney(amount, 'BTC'),
|
|
193
196
|
},
|
|
194
197
|
],
|
|
@@ -203,7 +206,7 @@ describe(determineUtxosForSpend.name, () => {
|
|
|
203
206
|
const utxos = [{ value: 1000 }, { value: 2000 }, { value: 3000 }];
|
|
204
207
|
const recipients = [
|
|
205
208
|
{
|
|
206
|
-
address:
|
|
209
|
+
address: recipientAddress,
|
|
207
210
|
amount: createMoney(Number(1), 'BTC'),
|
|
208
211
|
},
|
|
209
212
|
];
|
|
@@ -5,7 +5,7 @@ import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
|
5
5
|
import { Money } from '@leather.io/models';
|
|
6
6
|
import { createMoney, sumMoney } from '@leather.io/utils';
|
|
7
7
|
|
|
8
|
-
import { BitcoinError } from '../bitcoin-error';
|
|
8
|
+
import { BitcoinError } from '../validation/bitcoin-error';
|
|
9
9
|
import { filterUneconomicalUtxos, getSizeInfo, getUtxoTotal } from './coin-selection.utils';
|
|
10
10
|
|
|
11
11
|
export interface CoinSelectionOutput {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createMoney } from '@leather.io/utils';
|
|
2
2
|
|
|
3
|
+
import { recipientAddress } from '../mocks/mocks';
|
|
3
4
|
import { mockUtxos } from './coin-selection.mocks';
|
|
4
5
|
import { filterUneconomicalUtxos } from './coin-selection.utils';
|
|
5
6
|
|
|
6
7
|
describe(filterUneconomicalUtxos.name, () => {
|
|
7
8
|
const recipients = [
|
|
8
9
|
{
|
|
9
|
-
address:
|
|
10
|
+
address: recipientAddress,
|
|
10
11
|
amount: createMoney(0, 'BTC'),
|
|
11
12
|
},
|
|
12
13
|
];
|
|
@@ -4,7 +4,7 @@ import validate, { AddressInfo, AddressType, getAddressInfo } from 'bitcoin-addr
|
|
|
4
4
|
import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
5
5
|
import { sumNumbers } from '@leather.io/utils';
|
|
6
6
|
|
|
7
|
-
import { BtcSizeFeeEstimator } from '../btc-size-fee-estimator';
|
|
7
|
+
import { BtcSizeFeeEstimator } from '../fees/btc-size-fee-estimator';
|
|
8
8
|
import { CoinSelectionRecipient, CoinSelectionUtxo } from './coin-selection';
|
|
9
9
|
|
|
10
10
|
export function getUtxoTotal(utxos: CoinSelectionUtxo[]) {
|
|
@@ -57,16 +57,13 @@ export function getSizeInfo(payload: {
|
|
|
57
57
|
|
|
58
58
|
return sizeInfo;
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
export function getSpendableAmount({
|
|
62
|
-
utxos,
|
|
63
|
-
feeRate,
|
|
64
|
-
recipients,
|
|
65
|
-
}: {
|
|
60
|
+
interface GetSpendableAmountArgs {
|
|
66
61
|
utxos: CoinSelectionUtxo[];
|
|
67
62
|
feeRate: number;
|
|
68
63
|
recipients: CoinSelectionRecipient[];
|
|
69
|
-
|
|
64
|
+
isSendMax?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export function getSpendableAmount({ utxos, feeRate, recipients }: GetSpendableAmountArgs) {
|
|
70
67
|
const balance = utxos
|
|
71
68
|
.map(utxo => Number(utxo.value))
|
|
72
69
|
.reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
@@ -4,17 +4,16 @@ import { AverageBitcoinFeeRates } from '@leather.io/models';
|
|
|
4
4
|
import { createMoney } from '@leather.io/utils';
|
|
5
5
|
|
|
6
6
|
import { CoinSelectionRecipient, CoinSelectionUtxo } from '../coin-selection/coin-selection';
|
|
7
|
+
import { recipientAddress, taprootAddress } from '../mocks/mocks';
|
|
7
8
|
import { getBitcoinFees, getBitcoinTransactionFee } from './bitcoin-fees';
|
|
8
9
|
|
|
9
10
|
describe('getBitcoinTransactionFee', () => {
|
|
10
11
|
it('should return the fee for a normal transaction', () => {
|
|
11
12
|
const args = {
|
|
12
|
-
recipients: [
|
|
13
|
-
{ address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(1000, 'BTC') },
|
|
14
|
-
],
|
|
13
|
+
recipients: [{ address: recipientAddress, amount: createMoney(1000, 'BTC') }],
|
|
15
14
|
utxos: [
|
|
16
15
|
{
|
|
17
|
-
address:
|
|
16
|
+
address: taprootAddress,
|
|
18
17
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
19
18
|
vout: 0,
|
|
20
19
|
value: 2000,
|
|
@@ -30,12 +29,10 @@ describe('getBitcoinTransactionFee', () => {
|
|
|
30
29
|
it('should return the fee for a max send transaction', () => {
|
|
31
30
|
const args = {
|
|
32
31
|
isSendingMax: true,
|
|
33
|
-
recipients: [
|
|
34
|
-
{ address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(2000, 'BTC') },
|
|
35
|
-
],
|
|
32
|
+
recipients: [{ address: recipientAddress, amount: createMoney(2000, 'BTC') }],
|
|
36
33
|
utxos: [
|
|
37
34
|
{
|
|
38
|
-
address:
|
|
35
|
+
address: taprootAddress,
|
|
39
36
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
40
37
|
vout: 0,
|
|
41
38
|
value: 2000,
|
|
@@ -67,11 +64,11 @@ describe('getBitcoinFees', () => {
|
|
|
67
64
|
hourFee: new BigNumber(1),
|
|
68
65
|
};
|
|
69
66
|
const recipients: CoinSelectionRecipient[] = [
|
|
70
|
-
{ address:
|
|
67
|
+
{ address: recipientAddress, amount: createMoney(1000, 'BTC') },
|
|
71
68
|
];
|
|
72
69
|
const utxos: CoinSelectionUtxo[] = [
|
|
73
70
|
{
|
|
74
|
-
address:
|
|
71
|
+
address: taprootAddress,
|
|
75
72
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
76
73
|
vout: 0,
|
|
77
74
|
value: 2000,
|
package/src/index.ts
CHANGED
|
@@ -7,19 +7,31 @@ export * from './coin-selection/coin-selection.utils';
|
|
|
7
7
|
|
|
8
8
|
export * from './fees/bitcoin-fees';
|
|
9
9
|
|
|
10
|
-
export * from './
|
|
10
|
+
export * from './mocks/mocks';
|
|
11
11
|
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './bitcoin.utils';
|
|
16
|
-
export * from './p2tr-address-gen';
|
|
17
|
-
export * from './p2wpkh-address-gen';
|
|
18
|
-
export * from './p2wsh-p2sh-address-gen';
|
|
19
|
-
export * from './lookup-derivation-by-address';
|
|
12
|
+
export * from './payments/p2tr-address-gen';
|
|
13
|
+
export * from './payments/p2wpkh-address-gen';
|
|
14
|
+
export * from './payments/p2wsh-p2sh-address-gen';
|
|
20
15
|
export * from './psbt/psbt-totals';
|
|
21
16
|
export * from './psbt/psbt-inputs';
|
|
22
17
|
export * from './psbt/psbt-outputs';
|
|
23
18
|
export * from './psbt/psbt-totals';
|
|
24
19
|
export * from './psbt/psbt-details';
|
|
25
20
|
export * from './psbt/utils';
|
|
21
|
+
|
|
22
|
+
export * from './signer/bitcoin-signer';
|
|
23
|
+
|
|
24
|
+
export * from './transactions/generate-unsigned-transaction';
|
|
25
|
+
|
|
26
|
+
export * from './validation/address-validation';
|
|
27
|
+
export * from './validation/amount-validation';
|
|
28
|
+
export * from './validation/bitcoin-address';
|
|
29
|
+
export * from './validation/bitcoin-error';
|
|
30
|
+
export * from './validation/transaction-validation';
|
|
31
|
+
|
|
32
|
+
export * from './utils/bitcoin.network';
|
|
33
|
+
export * from './utils/bitcoin.utils';
|
|
34
|
+
export * from './utils/lookup-derivation-by-address';
|
|
35
|
+
export * from './utils/bitcoin.network';
|
|
36
|
+
export * from './utils/bitcoin.utils';
|
|
37
|
+
export * from './utils/lookup-derivation-by-address';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
2
|
+
|
|
3
|
+
// maybe these should be in mono/config?
|
|
4
|
+
// from extension/tests/mocks/constants
|
|
5
|
+
export const TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS = createBitcoinAddress(
|
|
6
|
+
'bc1q530dz4h80kwlzywlhx2qn0k6vdtftd93c499yq'
|
|
7
|
+
);
|
|
8
|
+
export const TEST_ACCOUNT_1_TAPROOT_ADDRESS = createBitcoinAddress(
|
|
9
|
+
'bc1putuzj9lyfcm8fef9jpy85nmh33cxuq9u6wyuk536t9kemdk37yjqmkc0pg'
|
|
10
|
+
);
|
|
11
|
+
export const TEST_ACCOUNT_2_TAPROOT_ADDRESS = createBitcoinAddress(
|
|
12
|
+
'bc1pmk2sacpfyy4v5phl8tq6eggu4e8laztep7fsgkkx0nc6m9vydjesaw0g2r'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const TEST_TESNET_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS = createBitcoinAddress(
|
|
16
|
+
'tb1q4qgnjewwun2llgken94zqjrx5kpqqycaz5522d'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS = createBitcoinAddress(
|
|
20
|
+
'tb1qr8me8t9gu9g6fu926ry5v44yp0wyljrespjtnz'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const TEST_TESTNET_ACCOUNT_2_TAPROOT_ADDRESS = createBitcoinAddress(
|
|
24
|
+
'tb1pve00jmp43whpqj2wpcxtc7m8wqhz0azq689y4r7h8tmj8ltaj87qj2nj6w'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// coin-selection.spec
|
|
28
|
+
export const recipientAddress = createBitcoinAddress('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
|
|
29
|
+
export const legacyAddress = createBitcoinAddress('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj');
|
|
30
|
+
export const segwitAddress = createBitcoinAddress('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH');
|
|
31
|
+
export const taprootAddress = createBitcoinAddress(
|
|
32
|
+
'tb1parwmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd'
|
|
33
|
+
);
|
|
34
|
+
export const invalidAddress = 'whoop-de-da-boop-da-de-not-a-bitcoin-address';
|
|
35
|
+
|
|
36
|
+
export const inValidCharactersAddress = createBitcoinAddress(
|
|
37
|
+
'tb1&*%wmj7533de3k2fw2kntyqacspvhm67qnjcmpqnnpfvzu05l69nsczdywd'
|
|
38
|
+
);
|
|
39
|
+
export const inValidLengthAddress = createBitcoinAddress('tb1parwmj7533de3k2fw2kntyqacspvhm67wd');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HDKey } from '@scure/bip32';
|
|
2
2
|
import { mnemonicToSeedSync } from '@scure/bip39';
|
|
3
3
|
|
|
4
|
-
import { deriveAddressIndexKeychainFromAccount } from '
|
|
4
|
+
import { deriveAddressIndexKeychainFromAccount } from '../utils/bitcoin.utils';
|
|
5
5
|
import { deriveTaprootAccount, getTaprootPaymentFromAddressIndex } from './p2tr-address-gen';
|
|
6
6
|
|
|
7
7
|
// TODO: this is a SECRET_KEY from @tests/mocks folder.
|
|
@@ -4,13 +4,13 @@ import * as btc from '@scure/btc-signer';
|
|
|
4
4
|
import { DerivationPathDepth } from '@leather.io/crypto';
|
|
5
5
|
import { BitcoinNetworkModes } from '@leather.io/models';
|
|
6
6
|
|
|
7
|
-
import { getBtcSignerLibNetworkConfigByMode } from '
|
|
7
|
+
import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
|
|
8
8
|
import {
|
|
9
9
|
BitcoinAccount,
|
|
10
10
|
deriveAddressIndexZeroFromAccount,
|
|
11
11
|
ecdsaPublicKeyToSchnorr,
|
|
12
12
|
getBitcoinCoinTypeIndexByNetwork,
|
|
13
|
-
} from '
|
|
13
|
+
} from '../utils/bitcoin.utils';
|
|
14
14
|
|
|
15
15
|
export function makeTaprootAccountDerivationPath(
|
|
16
16
|
network: BitcoinNetworkModes,
|
|
@@ -4,12 +4,12 @@ import * as btc from '@scure/btc-signer';
|
|
|
4
4
|
import { DerivationPathDepth } from '@leather.io/crypto';
|
|
5
5
|
import { BitcoinNetworkModes } from '@leather.io/models';
|
|
6
6
|
|
|
7
|
-
import { getBtcSignerLibNetworkConfigByMode } from '
|
|
7
|
+
import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
|
|
8
8
|
import {
|
|
9
9
|
BitcoinAccount,
|
|
10
10
|
deriveAddressIndexZeroFromAccount,
|
|
11
11
|
getBitcoinCoinTypeIndexByNetwork,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../utils/bitcoin.utils';
|
|
13
13
|
|
|
14
14
|
export function makeNativeSegwitAccountDerivationPath(
|
|
15
15
|
network: BitcoinNetworkModes,
|
package/src/psbt/psbt-details.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { getPsbtTxInputs, getPsbtTxOutputs } from 'bitcoin.utils';
|
|
2
|
-
|
|
3
1
|
import { BitcoinNetworkModes } from '@leather.io/models';
|
|
4
2
|
import { createMoney, subtractMoney } from '@leather.io/utils';
|
|
5
3
|
|
|
4
|
+
import { getPsbtTxInputs, getPsbtTxOutputs } from '../utils/bitcoin.utils';
|
|
5
|
+
import { BitcoinAddress } from '../validation/bitcoin-address';
|
|
6
6
|
import { getParsedInputs } from './psbt-inputs';
|
|
7
7
|
import { getParsedOutputs } from './psbt-outputs';
|
|
8
8
|
import { getPsbtTotals } from './psbt-totals';
|
|
@@ -10,7 +10,7 @@ import { getPsbtAsTransaction } from './utils';
|
|
|
10
10
|
|
|
11
11
|
interface GetPsbtDetailsArgs {
|
|
12
12
|
psbtHex: string;
|
|
13
|
-
psbtAddresses:
|
|
13
|
+
psbtAddresses: BitcoinAddress[];
|
|
14
14
|
networkMode: BitcoinNetworkModes;
|
|
15
15
|
indexesToSign?: number[];
|
|
16
16
|
}
|
package/src/psbt/psbt-inputs.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
2
|
import type { TransactionInput } from '@scure/btc-signer/psbt';
|
|
3
|
-
import {
|
|
4
|
-
import { getBitcoinInputAddress, getBitcoinInputValue } from 'bitcoin.utils';
|
|
3
|
+
import { BitcoinAddress, createBitcoinAddress } from 'validation/bitcoin-address';
|
|
5
4
|
|
|
6
5
|
import type { BitcoinNetworkModes, Inscription } from '@leather.io/models';
|
|
7
6
|
import { isDefined, isUndefined } from '@leather.io/utils';
|
|
8
7
|
|
|
8
|
+
import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
|
|
9
|
+
import { getBitcoinInputAddress, getBitcoinInputValue } from '../utils/bitcoin.utils';
|
|
10
|
+
|
|
9
11
|
export interface PsbtInput {
|
|
10
|
-
address:
|
|
12
|
+
address: BitcoinAddress;
|
|
11
13
|
index?: number;
|
|
12
14
|
// TODO: inject inscription later on. getParsedInputs should be a pure function
|
|
13
15
|
inscription?: Inscription;
|
|
@@ -23,7 +25,7 @@ interface GetParsedInputsArgs {
|
|
|
23
25
|
inputs: TransactionInput[];
|
|
24
26
|
indexesToSign?: number[];
|
|
25
27
|
networkMode: BitcoinNetworkModes;
|
|
26
|
-
psbtAddresses:
|
|
28
|
+
psbtAddresses: BitcoinAddress[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
interface GetParsedInputsResponse {
|
|
@@ -43,7 +45,8 @@ export function getParsedInputs({
|
|
|
43
45
|
const inputAddress = isDefined(input.index)
|
|
44
46
|
? getBitcoinInputAddress(input, bitcoinNetwork)
|
|
45
47
|
: '';
|
|
46
|
-
const
|
|
48
|
+
const bitcoinAddress = createBitcoinAddress(inputAddress);
|
|
49
|
+
const isCurrentAddress = psbtAddresses.includes(bitcoinAddress);
|
|
47
50
|
// Flags when not signing ALL inputs/outputs (NONE, SINGLE, and ANYONECANPAY)
|
|
48
51
|
const canChange =
|
|
49
52
|
isCurrentAddress &&
|
|
@@ -53,7 +56,7 @@ export function getParsedInputs({
|
|
|
53
56
|
const toSignIndex = isCurrentAddress && !signAll && indexesToSign.includes(i);
|
|
54
57
|
|
|
55
58
|
return {
|
|
56
|
-
address:
|
|
59
|
+
address: bitcoinAddress,
|
|
57
60
|
index: input.index,
|
|
58
61
|
bip32Derivation: input.bip32Derivation,
|
|
59
62
|
tapBip32Derivation: input.tapBip32Derivation,
|