@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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.d.ts +197 -148
  4. package/dist/index.js +341 -205
  5. package/dist/index.js.map +1 -1
  6. package/package.json +8 -8
  7. package/src/bip322/bip322-utils.ts +1 -1
  8. package/src/bip322/sign-message-bip322-bitcoinjs.ts +8 -3
  9. package/src/bip322/sign-message-bip322.spec.ts +13 -13
  10. package/src/coin-selection/calculate-max-spend.spec.ts +19 -17
  11. package/src/coin-selection/calculate-max-spend.ts +26 -16
  12. package/src/coin-selection/coin-selection.spec.ts +29 -26
  13. package/src/coin-selection/coin-selection.ts +1 -1
  14. package/src/coin-selection/coin-selection.utils.spec.ts +2 -1
  15. package/src/coin-selection/coin-selection.utils.ts +5 -8
  16. package/src/fees/bitcoin-fees.spec.ts +7 -10
  17. package/src/index.ts +21 -9
  18. package/src/mocks/mocks.ts +39 -0
  19. package/src/{p2tr-address-gen.spec.ts → payments/p2tr-address-gen.spec.ts} +1 -1
  20. package/src/{p2tr-address-gen.ts → payments/p2tr-address-gen.ts} +2 -2
  21. package/src/{p2wpkh-address-gen.ts → payments/p2wpkh-address-gen.ts} +2 -2
  22. package/src/psbt/psbt-details.ts +3 -3
  23. package/src/psbt/psbt-inputs.ts +9 -6
  24. package/src/psbt/psbt-outputs.ts +10 -7
  25. package/src/psbt/psbt-totals.ts +3 -3
  26. package/src/{bitcoin-signer.ts → signer/bitcoin-signer.ts} +6 -5
  27. package/src/transactions/generate-unsigned-transaction.spec.ts +1 -1
  28. package/src/transactions/generate-unsigned-transaction.ts +3 -3
  29. package/src/{bitcoin.network.ts → utils/bitcoin.network.ts} +2 -0
  30. package/src/{bitcoin.utils.spec.ts → utils/bitcoin.utils.spec.ts} +19 -14
  31. package/src/{bitcoin.utils.ts → utils/bitcoin.utils.ts} +19 -13
  32. package/src/{lookup-derivation-by-address.spec.ts → utils/lookup-derivation-by-address.spec.ts} +11 -6
  33. package/src/{lookup-derivation-by-address.ts → utils/lookup-derivation-by-address.ts} +4 -3
  34. package/src/validation/address-validation.spec.ts +396 -0
  35. package/src/validation/address-validation.ts +28 -0
  36. package/src/validation/amount-validation.spec.ts +39 -0
  37. package/src/validation/amount-validation.ts +31 -0
  38. package/src/validation/bitcoin-address.ts +23 -0
  39. package/src/{bitcoin-error.ts → validation/bitcoin-error.ts} +4 -2
  40. package/src/validation/transaction-validation.spec.ts +60 -0
  41. package/src/validation/transaction-validation.ts +46 -0
  42. /package/src/{btc-size-fee-estimator.spec.ts → fees/btc-size-fee-estimator.spec.ts} +0 -0
  43. /package/src/{btc-size-fee-estimator.ts → fees/btc-size-fee-estimator.ts} +0 -0
  44. /package/src/{p2wpkh-address-gen.spec.ts → payments/p2wpkh-address-gen.spec.ts} +0 -0
  45. /package/src/{p2wsh-p2sh-address-gen.spec.ts → payments/p2wsh-p2sh-address-gen.spec.ts} +0 -0
  46. /package/src/{p2wsh-p2sh-address-gen.ts → payments/p2wsh-p2sh-address-gen.ts} +0 -0
  47. /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 nativeSegwitAddress = btc.getAddress('wpkh', testVectorKey);
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 (!nativeSegwitAddress) throw new Error('nativeSegwitAddress is undefined');
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('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
48
- expect(payment.address).toEqual('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
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 taprootAddress = btc.getAddress('tr', testVectorKey);
115
+ const rawTaprootAddress = btc.getAddress('tr', testVectorKey);
117
116
 
118
- if (!taprootAddress) throw new Error('Could not generate taproot address');
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 { calculateMaxBitcoinSpend } from './calculate-max-spend';
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
- describe(calculateMaxBitcoinSpend.name, () => {
5
+ const recipient = createBitcoinAddress('');
6
+ describe(calculateMaxSpend.name, () => {
5
7
  test('with 1 sat/vb fee', () => {
6
8
  const fee = 1;
7
- const maxBitcoinSpend = calculateMaxBitcoinSpend({
8
- address: '',
9
+ const maxBitcoinSpend = calculateMaxSpend({
10
+ recipient,
9
11
  utxos: mockUtxos,
10
- fetchedFeeRates: generateMockAverageFee(fee),
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 = calculateMaxBitcoinSpend({
18
- address: '',
19
+ const maxBitcoinSpend = calculateMaxSpend({
20
+ recipient,
19
21
  utxos: mockUtxos,
20
- fetchedFeeRates: generateMockAverageFee(fee),
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 = calculateMaxBitcoinSpend({
28
- address: '',
29
+ const maxBitcoinSpend = calculateMaxSpend({
30
+ recipient,
29
31
  utxos: mockUtxos,
30
- fetchedFeeRates: generateMockAverageFee(fee),
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 = calculateMaxBitcoinSpend({
38
- address: '',
39
+ const maxBitcoinSpend = calculateMaxSpend({
40
+ recipient,
39
41
  utxos: mockUtxos,
40
- fetchedFeeRates: generateMockAverageFee(fee),
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 = calculateMaxBitcoinSpend({
48
- address: '',
49
+ const maxBitcoinSpend = calculateMaxSpend({
50
+ recipient,
49
51
  utxos: mockUtxos,
50
- fetchedFeeRates: generateMockAverageFee(fee),
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 './coin-selection';
7
- import { filterUneconomicalUtxos, getSpendableAmount } from './coin-selection.utils';
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 CalculateMaxBitcoinSpend {
10
- address: string;
13
+ interface CalculateMaxSpendArgs {
14
+ recipient: BitcoinAddress;
11
15
  utxos: CoinSelectionUtxo[];
12
- fetchedFeeRates?: AverageBitcoinFeeRates;
16
+ feeRates?: AverageBitcoinFeeRates;
13
17
  feeRate?: number;
14
18
  }
15
19
 
16
- export function calculateMaxBitcoinSpend({
17
- address,
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
- fetchedFeeRates,
21
- }: CalculateMaxBitcoinSpend) {
22
- if (!utxos.length || !fetchedFeeRates)
29
+ feeRates,
30
+ }: CalculateMaxSpendArgs): CalculateMaxSpendResponse {
31
+ if (!utxos.length || !feeRates)
23
32
  return {
24
33
  spendAllFee: 0,
25
34
  amount: createMoney(0, 'BTC'),
26
- spendableBitcoin: new BigNumber(0),
35
+ spendableBtc: new BigNumber(0),
27
36
  };
28
37
 
29
- const currentFeeRate = feeRate ?? fetchedFeeRates.halfHourFee.toNumber();
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
- spendableBitcoin: satToBtc(spendableAmount),
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
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('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
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('15PyZveQd28E2SHZu2ugkWZBp6iER41vXj');
125
- const segwit = generate10kSpendWithDummyUtxoSet('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH');
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('33SVjoCHJovrXxjDKLFSXo1h3t5KgkPzfH');
131
- const native = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
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('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(1000, 'BTC') },
67
+ { address: recipientAddress, amount: createMoney(1000, 'BTC') },
71
68
  ];
72
69
  const utxos: CoinSelectionUtxo[] = [
73
70
  {
74
- address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
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 './transactions/generate-unsigned-transaction';
10
+ export * from './mocks/mocks';
11
11
 
12
- export * from './bitcoin-error';
13
- export * from './bitcoin-signer';
14
- export * from './bitcoin.network';
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 './bitcoin.utils';
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 './bitcoin.network';
7
+ import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
8
8
  import {
9
9
  BitcoinAccount,
10
10
  deriveAddressIndexZeroFromAccount,
11
11
  ecdsaPublicKeyToSchnorr,
12
12
  getBitcoinCoinTypeIndexByNetwork,
13
- } from './bitcoin.utils';
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 './bitcoin.network';
7
+ import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
8
8
  import {
9
9
  BitcoinAccount,
10
10
  deriveAddressIndexZeroFromAccount,
11
11
  getBitcoinCoinTypeIndexByNetwork,
12
- } from './bitcoin.utils';
12
+ } from '../utils/bitcoin.utils';
13
13
 
14
14
  export function makeNativeSegwitAccountDerivationPath(
15
15
  network: BitcoinNetworkModes,
@@ -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: string[];
13
+ psbtAddresses: BitcoinAddress[];
14
14
  networkMode: BitcoinNetworkModes;
15
15
  indexesToSign?: number[];
16
16
  }
@@ -1,13 +1,15 @@
1
1
  import { bytesToHex } from '@noble/hashes/utils';
2
2
  import type { TransactionInput } from '@scure/btc-signer/psbt';
3
- import { getBtcSignerLibNetworkConfigByMode } from 'bitcoin.network';
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: string;
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: string[];
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 isCurrentAddress = psbtAddresses.includes(inputAddress);
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: inputAddress,
59
+ address: bitcoinAddress,
57
60
  index: input.index,
58
61
  bip32Derivation: input.bip32Derivation,
59
62
  tapBip32Derivation: input.tapBip32Derivation,