@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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { Network } from 'bitcoin-address-validation';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS,
|
|
5
|
+
TEST_ACCOUNT_1_TAPROOT_ADDRESS,
|
|
6
|
+
TEST_TESNET_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS,
|
|
7
|
+
TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS,
|
|
8
|
+
inValidCharactersAddress,
|
|
9
|
+
inValidLengthAddress,
|
|
10
|
+
} from '../mocks/mocks';
|
|
11
|
+
import {
|
|
12
|
+
getBitcoinAddressNetworkType,
|
|
13
|
+
isValidBitcoinAddress,
|
|
14
|
+
isValidBitcoinNetworkAddress,
|
|
15
|
+
} from './address-validation';
|
|
16
|
+
|
|
17
|
+
describe('getBitcoinAddressNetworkType', () => {
|
|
18
|
+
it('returns the correct network type', () => {
|
|
19
|
+
expect(getBitcoinAddressNetworkType('mainnet')).toEqual(Network.mainnet);
|
|
20
|
+
expect(getBitcoinAddressNetworkType('testnet')).toEqual(Network.testnet);
|
|
21
|
+
expect(getBitcoinAddressNetworkType('regtest')).toEqual(Network.regtest);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns Network.testnet type for signet', () => {
|
|
25
|
+
expect(getBitcoinAddressNetworkType('signet')).toEqual(Network.testnet);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns false for invalid bitcoin addresses', () => {
|
|
29
|
+
// expect(() => isValidBitcoinAddress(inValidLengthAddress)).toThrow(BitcoinError);
|
|
30
|
+
expect(isValidBitcoinAddress('')).toBe(false);
|
|
31
|
+
|
|
32
|
+
// @ts-expect-error arg type is invalid
|
|
33
|
+
expect(isValidBitcoinAddress(null)).toBe(false);
|
|
34
|
+
// @ts-expect-error arg type is invalid
|
|
35
|
+
expect(isValidBitcoinAddress(undefined)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false for addresses with invalid characters', () => {
|
|
39
|
+
expect(isValidBitcoinAddress(inValidCharactersAddress)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns false for addresses with invalid length', () => {
|
|
43
|
+
expect(isValidBitcoinAddress(inValidLengthAddress)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('isValidBitcoinAddress', () => {
|
|
48
|
+
it('returns true for valid bitcoin addresses', () => {
|
|
49
|
+
expect(isValidBitcoinAddress(TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS)).toBe(true);
|
|
50
|
+
expect(isValidBitcoinAddress(TEST_ACCOUNT_1_TAPROOT_ADDRESS)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns false for invalid bitcoin addresses', () => {
|
|
54
|
+
// expect(() => isValidBitcoinAddress(inValidLengthAddress)).toThrow(BitcoinError);
|
|
55
|
+
expect(isValidBitcoinAddress('')).toBe(false);
|
|
56
|
+
|
|
57
|
+
// @ts-expect-error arg type is invalid
|
|
58
|
+
expect(isValidBitcoinAddress(null)).toBe(false);
|
|
59
|
+
// @ts-expect-error arg type is invalid
|
|
60
|
+
expect(isValidBitcoinAddress(undefined)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns false for addresses with invalid characters', () => {
|
|
64
|
+
expect(isValidBitcoinAddress(inValidCharactersAddress)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns false for addresses with invalid length', () => {
|
|
68
|
+
expect(isValidBitcoinAddress(inValidLengthAddress)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('isValidBitcoinNetworkAddress', () => {
|
|
73
|
+
it('returns true for valid bitcoin network addresses', () => {
|
|
74
|
+
expect(isValidBitcoinNetworkAddress(TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'mainnet')).toBe(
|
|
75
|
+
true
|
|
76
|
+
);
|
|
77
|
+
expect(isValidBitcoinNetworkAddress(TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'mainnet')).toBe(
|
|
78
|
+
true
|
|
79
|
+
);
|
|
80
|
+
expect(isValidBitcoinNetworkAddress(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS, 'testnet')).toBe(true);
|
|
81
|
+
expect(
|
|
82
|
+
isValidBitcoinNetworkAddress(TEST_TESNET_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'testnet')
|
|
83
|
+
).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns false for invalid bitcoin network addresses', () => {
|
|
87
|
+
expect(isValidBitcoinNetworkAddress(TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'testnet')).toBe(
|
|
88
|
+
false
|
|
89
|
+
);
|
|
90
|
+
expect(isValidBitcoinNetworkAddress(TEST_ACCOUNT_1_TAPROOT_ADDRESS, 'testnet')).toBe(false);
|
|
91
|
+
expect(
|
|
92
|
+
isValidBitcoinNetworkAddress(TEST_TESNET_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'mainnet')
|
|
93
|
+
).toBe(false);
|
|
94
|
+
expect(isValidBitcoinNetworkAddress(TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS, 'mainnet')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false for addresses with invalid characters', () => {
|
|
98
|
+
expect(isValidBitcoinNetworkAddress(inValidCharactersAddress, 'mainnet')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns false for addresses with invalid length', () => {
|
|
102
|
+
expect(isValidBitcoinNetworkAddress(inValidLengthAddress, 'mainnet')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns false for invalid network types', () => {
|
|
106
|
+
expect(
|
|
107
|
+
// @ts-expect-error arg type is invalid
|
|
108
|
+
isValidBitcoinNetworkAddress(TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS, 'invalid-network')
|
|
109
|
+
).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// taken from bitcoin-address-validation/blob/master/tests/index.spec.ts
|
|
114
|
+
// adapted to our code to ensure completeness
|
|
115
|
+
|
|
116
|
+
describe('bitcoin-address-validation address tests', () => {
|
|
117
|
+
describe('Validation and parsing', () => {
|
|
118
|
+
it('validates Mainnet P2PKH', () => {
|
|
119
|
+
const address = '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem';
|
|
120
|
+
|
|
121
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('validates Testnet P2PKH', () => {
|
|
124
|
+
const address = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn';
|
|
125
|
+
|
|
126
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('validates Mainnet P2SH', () => {
|
|
130
|
+
const address = '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy';
|
|
131
|
+
|
|
132
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('validates Testnet P2SH', () => {
|
|
136
|
+
const address = '2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc';
|
|
137
|
+
|
|
138
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('validates Mainnet Bech32 P2WPKH', () => {
|
|
142
|
+
const addresses = [
|
|
143
|
+
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
|
|
144
|
+
'bc1q973xrrgje6etkkn9q9azzsgpxeddats8ckvp5s',
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
expect(isValidBitcoinAddress(addresses[0])).not.toBe(false);
|
|
148
|
+
expect(isValidBitcoinAddress(addresses[1])).not.toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('validates Testnet Bech32 P2WPKH', () => {
|
|
152
|
+
const address = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
|
|
153
|
+
|
|
154
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('validates Regtest Bech32 P2WPKH', () => {
|
|
158
|
+
const address = 'bcrt1q6z64a43mjgkcq0ul2znwneq3spghrlau9slefp';
|
|
159
|
+
|
|
160
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('validates Mainnet Bech32 P2WSH', () => {
|
|
164
|
+
const address = 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3';
|
|
165
|
+
|
|
166
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('validates Testnet Bech32 P2WSH', () => {
|
|
170
|
+
const address = 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7';
|
|
171
|
+
|
|
172
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('validates Regtest Bech32 P2WSH', () => {
|
|
176
|
+
const address = 'bcrt1q5n2k3frgpxces3dsw4qfpqk4kksv0cz96pldxdwxrrw0d5ud5hcqzzx7zt';
|
|
177
|
+
|
|
178
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('validates Signet Bech32 P2WKH', () => {
|
|
182
|
+
const address = 'bcrt1qc7evl8kdgp69h7qmm8cndaq07xkhj6ulyck0x5';
|
|
183
|
+
|
|
184
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('validates Testnet P2PKH', () => {
|
|
188
|
+
const address = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn';
|
|
189
|
+
|
|
190
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('fails on invalid P2PKH', () => {
|
|
194
|
+
const address = '17VZNX1SN5NtKa8UFFxwQbFeFc3iqRYhem';
|
|
195
|
+
|
|
196
|
+
expect(isValidBitcoinAddress(address)).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('validates Mainnet P2SH', () => {
|
|
200
|
+
const address = '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy';
|
|
201
|
+
|
|
202
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('validates Testnet P2SH', () => {
|
|
206
|
+
const address = '2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc';
|
|
207
|
+
|
|
208
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('fails on invalid P2SH', () => {
|
|
212
|
+
const address = '17VZNX1SN5NtKa8UFFxwQbFFFc3iqRYhem';
|
|
213
|
+
|
|
214
|
+
expect(isValidBitcoinAddress(address)).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles bogus address', () => {
|
|
218
|
+
const address = 'x';
|
|
219
|
+
|
|
220
|
+
expect(isValidBitcoinAddress(address)).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('validates Mainnet Bech32 P2WPKH', () => {
|
|
224
|
+
const addresses = [
|
|
225
|
+
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
|
|
226
|
+
'bc1q973xrrgje6etkkn9q9azzsgpxeddats8ckvp5s',
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
expect(isValidBitcoinAddress(addresses[0])).not.toBe(false);
|
|
230
|
+
|
|
231
|
+
expect(isValidBitcoinAddress(addresses[1])).not.toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('validates uppercase Bech32 P2WPKH', () => {
|
|
235
|
+
const addresses = [
|
|
236
|
+
'BC1Q973XRRGJE6ETKKN9Q9AZZSGPXEDDATS8CKVP5S',
|
|
237
|
+
'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4',
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
expect(isValidBitcoinAddress(addresses[0])).not.toBe(false);
|
|
241
|
+
|
|
242
|
+
expect(isValidBitcoinAddress(addresses[1])).not.toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('validates Testnet Bech32 P2WPKH', () => {
|
|
246
|
+
const address = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
|
|
247
|
+
|
|
248
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('validates Regtest Bech32 P2WPKH', () => {
|
|
252
|
+
const address = 'bcrt1q6z64a43mjgkcq0ul2znwneq3spghrlau9slefp';
|
|
253
|
+
|
|
254
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('validates Mainnet Bech32 P2TR', () => {
|
|
258
|
+
const address = 'bc1ptxs597p3fnpd8gwut5p467ulsydae3rp9z75hd99w8k3ljr9g9rqx6ynaw';
|
|
259
|
+
|
|
260
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('validates Testnet Bech32 P2TR', () => {
|
|
264
|
+
const address = 'tb1p84x2ryuyfevgnlpnxt9f39gm7r68gwtvllxqe5w2n5ru00s9aquslzggwq';
|
|
265
|
+
|
|
266
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('validates Regtest Bech32 P2TR', () => {
|
|
270
|
+
const address = 'bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6';
|
|
271
|
+
|
|
272
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('validates Mainnet Bech32 P2WSH', () => {
|
|
276
|
+
const address = 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3';
|
|
277
|
+
|
|
278
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('validates Testnet Bech32 P2WSH', () => {
|
|
282
|
+
const address = 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7';
|
|
283
|
+
|
|
284
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('validates Regtest Bech32 P2WSH', () => {
|
|
288
|
+
const address = 'bcrt1q5n2k3frgpxces3dsw4qfpqk4kksv0cz96pldxdwxrrw0d5ud5hcqzzx7zt';
|
|
289
|
+
|
|
290
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('validates Signet Bech32 P2WKH', () => {
|
|
294
|
+
const address = 'bcrt1qc7evl8kdgp69h7qmm8cndaq07xkhj6ulyck0x5';
|
|
295
|
+
|
|
296
|
+
expect(isValidBitcoinAddress(address)).not.toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('fails on invalid Bech32', () => {
|
|
300
|
+
const address = 'bc1qw508d6qejxtdg4y5r3zrrvary0c5xw7kv8f3t4';
|
|
301
|
+
|
|
302
|
+
expect(isValidBitcoinAddress(address)).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Validation & network', () => {
|
|
307
|
+
it('validates Mainnet P2PKH', () => {
|
|
308
|
+
const address = '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem';
|
|
309
|
+
|
|
310
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('validates Testnet P2PKH', () => {
|
|
314
|
+
const address = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn';
|
|
315
|
+
|
|
316
|
+
expect(isValidBitcoinNetworkAddress(address, Network.testnet)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('fails on invalid P2PKH', () => {
|
|
320
|
+
const address = '17VZNX1SN5NtKa8UFFxwQbFeFc3iqRYhem';
|
|
321
|
+
|
|
322
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('validates Mainnet P2SH', () => {
|
|
326
|
+
const address = '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy';
|
|
327
|
+
|
|
328
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('validates Testnet P2SH', () => {
|
|
332
|
+
const address = '2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc';
|
|
333
|
+
|
|
334
|
+
expect(isValidBitcoinNetworkAddress(address, Network.testnet)).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('fails on invalid P2SH', () => {
|
|
338
|
+
const address = '17VZNX1SN5NtKa8UFFxwQbFFFc3iqRYhem';
|
|
339
|
+
|
|
340
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('handles bogus address', () => {
|
|
344
|
+
const address = 'x';
|
|
345
|
+
|
|
346
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('validates Mainnet Bech32 P2WPKH', () => {
|
|
350
|
+
const addresses = [
|
|
351
|
+
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
|
|
352
|
+
'bc1q973xrrgje6etkkn9q9azzsgpxeddats8ckvp5s',
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
expect(isValidBitcoinNetworkAddress(addresses[0], Network.mainnet)).toBe(true);
|
|
356
|
+
|
|
357
|
+
expect(isValidBitcoinNetworkAddress(addresses[1], Network.mainnet)).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('validates Testnet Bech32 P2WPKH', () => {
|
|
361
|
+
const address = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
|
|
362
|
+
|
|
363
|
+
expect(isValidBitcoinNetworkAddress(address, Network.testnet)).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('validates Regtest Bech32 P2WPKH', () => {
|
|
367
|
+
const address = 'bcrt1q6z64a43mjgkcq0ul2znwneq3spghrlau9slefp';
|
|
368
|
+
|
|
369
|
+
expect(isValidBitcoinNetworkAddress(address, Network.regtest)).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('validates Mainnet Bech32 P2WSH', () => {
|
|
373
|
+
const address = 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3';
|
|
374
|
+
|
|
375
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('validates Testnet Bech32 P2WSH', () => {
|
|
379
|
+
const address = 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7';
|
|
380
|
+
|
|
381
|
+
expect(isValidBitcoinNetworkAddress(address, Network.testnet)).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('validates Regtest Bech32 P2WSH', () => {
|
|
385
|
+
const address = 'bcrt1q5n2k3frgpxces3dsw4qfpqk4kksv0cz96pldxdwxrrw0d5ud5hcqzzx7zt';
|
|
386
|
+
|
|
387
|
+
expect(isValidBitcoinNetworkAddress(address, Network.regtest)).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('fails on invalid Bech32', () => {
|
|
391
|
+
const address = 'bc1qw508d6qejxtdg4y5r3zrrvary0c5xw7kv8f3t4';
|
|
392
|
+
|
|
393
|
+
expect(isValidBitcoinNetworkAddress(address, Network.mainnet)).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Network, validate } from 'bitcoin-address-validation';
|
|
2
|
+
|
|
3
|
+
import { BitcoinNetworkModes } from '@leather.io/models';
|
|
4
|
+
import { isEmptyString, isUndefined } from '@leather.io/utils';
|
|
5
|
+
|
|
6
|
+
// todo investigate handling this in bitcoinNetworkToNetworkMode
|
|
7
|
+
export function getBitcoinAddressNetworkType(network: BitcoinNetworkModes): Network {
|
|
8
|
+
// Signet uses testnet address format, this parsing is to please the
|
|
9
|
+
// validation library - 'bitcoin-address-validation'
|
|
10
|
+
if (network === 'signet') return Network.testnet;
|
|
11
|
+
return network as Network;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isValidBitcoinAddress(address: string) {
|
|
15
|
+
if (isUndefined(address) || isEmptyString(address)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return validate(address);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isValidBitcoinNetworkAddress(address: string, network: BitcoinNetworkModes) {
|
|
23
|
+
if (!isValidBitcoinAddress(address) || !network) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return validate(address, getBitcoinAddressNetworkType(network));
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { createMoney } from '@leather.io/utils';
|
|
4
|
+
|
|
5
|
+
import { isBtcBalanceSufficient, isBtcMinimumSpend } from './amount-validation';
|
|
6
|
+
|
|
7
|
+
describe('isBtcBalanceSufficient', () => {
|
|
8
|
+
it('returns true if the balance is sufficient', () => {
|
|
9
|
+
const args = {
|
|
10
|
+
amount: createMoney(1, 'BTC'),
|
|
11
|
+
spendableBtc: new BigNumber(2),
|
|
12
|
+
};
|
|
13
|
+
expect(isBtcBalanceSufficient(args)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns false if the balance is not sufficient', () => {
|
|
17
|
+
const args = {
|
|
18
|
+
amount: createMoney(2, 'BTC'),
|
|
19
|
+
spendableBtc: new BigNumber(1),
|
|
20
|
+
};
|
|
21
|
+
expect(isBtcBalanceSufficient(args)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('isBtcMinimumSpend', () => {
|
|
26
|
+
it('returns true if the amount is greater than or equal to the minimum spend', () => {
|
|
27
|
+
const args = {
|
|
28
|
+
amount: createMoney(1, 'BTC'),
|
|
29
|
+
};
|
|
30
|
+
expect(isBtcMinimumSpend(args)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns false if the amount is less than the minimum spend', () => {
|
|
34
|
+
const args = {
|
|
35
|
+
amount: createMoney(0.000005, 'BTC'),
|
|
36
|
+
};
|
|
37
|
+
expect(isBtcMinimumSpend(args)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { Money } from '@leather.io/models';
|
|
4
|
+
import { btcToSat } from '@leather.io/utils';
|
|
5
|
+
|
|
6
|
+
export const minSpendAmountInSats = 546;
|
|
7
|
+
|
|
8
|
+
interface isBtcBalanceSufficientArgs {
|
|
9
|
+
amount: Money;
|
|
10
|
+
spendableBtc: BigNumber;
|
|
11
|
+
}
|
|
12
|
+
export function isBtcBalanceSufficient({
|
|
13
|
+
amount: { amount },
|
|
14
|
+
spendableBtc,
|
|
15
|
+
}: isBtcBalanceSufficientArgs) {
|
|
16
|
+
if (!spendableBtc) return false;
|
|
17
|
+
const desiredSpend = new BigNumber(amount);
|
|
18
|
+
if (desiredSpend.isGreaterThan(spendableBtc)) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IsBtcMinimumSpendArgs {
|
|
23
|
+
amount: Money;
|
|
24
|
+
}
|
|
25
|
+
export function isBtcMinimumSpend({ amount: { amount } }: IsBtcMinimumSpendArgs) {
|
|
26
|
+
if (!amount) return false;
|
|
27
|
+
|
|
28
|
+
const desiredSpend = btcToSat(amount);
|
|
29
|
+
if (desiredSpend.isLessThan(minSpendAmountInSats)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isValidBitcoinAddress } from './address-validation';
|
|
2
|
+
import { BitcoinError } from './bitcoin-error';
|
|
3
|
+
|
|
4
|
+
// Branded type for Bitcoin addresses
|
|
5
|
+
export type BitcoinAddress = string & { readonly __brand: unique symbol };
|
|
6
|
+
|
|
7
|
+
export function isBitcoinAddress(value: string): value is BitcoinAddress {
|
|
8
|
+
try {
|
|
9
|
+
isValidBitcoinAddress(value);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Function to create a BitcoinAddress
|
|
17
|
+
export function createBitcoinAddress(value: string): BitcoinAddress {
|
|
18
|
+
if (!isBitcoinAddress(value)) {
|
|
19
|
+
throw new BitcoinError('InvalidAddress');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
type TransactionErrorKey = 'InvalidAddress' | 'InsufficientFunds' | 'InvalidNetworkAddress';
|
|
2
|
+
|
|
1
3
|
export class BitcoinError extends Error {
|
|
2
4
|
public message: BitcoinErrorKey;
|
|
3
5
|
constructor(message: BitcoinErrorKey) {
|
|
@@ -11,7 +13,7 @@ export class BitcoinError extends Error {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export type BitcoinErrorKey =
|
|
14
|
-
|
|
|
15
|
-
| '
|
|
16
|
+
| TransactionErrorKey
|
|
17
|
+
| 'InsufficientAmount'
|
|
16
18
|
| 'NoInputsToSign'
|
|
17
19
|
| 'NoOutputsToSign';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { BitcoinNetworkModes } from '@leather.io/models';
|
|
4
|
+
import { createMoney } from '@leather.io/utils';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS,
|
|
8
|
+
TEST_ACCOUNT_1_TAPROOT_ADDRESS,
|
|
9
|
+
inValidCharactersAddress,
|
|
10
|
+
} from '../mocks/mocks';
|
|
11
|
+
import { BitcoinError } from '../validation/bitcoin-error';
|
|
12
|
+
import { isValidBitcoinTransaction } from './transaction-validation';
|
|
13
|
+
|
|
14
|
+
const mockTransaction = {
|
|
15
|
+
amount: createMoney(1, 'USD'),
|
|
16
|
+
payer: TEST_ACCOUNT_1_TAPROOT_ADDRESS,
|
|
17
|
+
recipient: TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS,
|
|
18
|
+
network: 'testnet' as BitcoinNetworkModes,
|
|
19
|
+
utxos: [],
|
|
20
|
+
feeRate: 1,
|
|
21
|
+
feeRates: {
|
|
22
|
+
fastestFee: new BigNumber(3),
|
|
23
|
+
halfHourFee: new BigNumber(2),
|
|
24
|
+
hourFee: new BigNumber(1),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('isValidBitcoinTransaction', () => {
|
|
29
|
+
it('should throw an error if the payer address is invalid', () => {
|
|
30
|
+
const transaction = {
|
|
31
|
+
...mockTransaction,
|
|
32
|
+
payer: inValidCharactersAddress,
|
|
33
|
+
};
|
|
34
|
+
expect(() => isValidBitcoinTransaction(transaction)).toThrow(BitcoinError);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should throw an error if the recipient address is invalid', () => {
|
|
38
|
+
const transaction = {
|
|
39
|
+
...mockTransaction,
|
|
40
|
+
recipient: inValidCharactersAddress,
|
|
41
|
+
};
|
|
42
|
+
expect(() => isValidBitcoinTransaction(transaction)).toThrow(BitcoinError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw an error if the amount is insufficient', () => {
|
|
46
|
+
const transaction = {
|
|
47
|
+
...mockTransaction,
|
|
48
|
+
amount: createMoney(0, 'USD'),
|
|
49
|
+
};
|
|
50
|
+
expect(() => isValidBitcoinTransaction(transaction)).toThrow(BitcoinError);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw an error if the balance is insufficient', () => {
|
|
54
|
+
const transaction = {
|
|
55
|
+
...mockTransaction,
|
|
56
|
+
amount: createMoney(200, 'USD'),
|
|
57
|
+
};
|
|
58
|
+
expect(() => isValidBitcoinTransaction(transaction)).toThrow(BitcoinError);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type BitcoinNetworkModes, Money } from '@leather.io/models';
|
|
2
|
+
|
|
3
|
+
import { calculateMaxSpend } from '../coin-selection/calculate-max-spend';
|
|
4
|
+
import { GetBitcoinFeesArgs } from '../fees/bitcoin-fees';
|
|
5
|
+
import { BitcoinError } from '../validation/bitcoin-error';
|
|
6
|
+
import { isValidBitcoinAddress, isValidBitcoinNetworkAddress } from './address-validation';
|
|
7
|
+
import { isBtcBalanceSufficient, isBtcMinimumSpend } from './amount-validation';
|
|
8
|
+
import { BitcoinAddress } from './bitcoin-address';
|
|
9
|
+
|
|
10
|
+
interface BitcoinTransaction extends Omit<GetBitcoinFeesArgs, 'recipients'> {
|
|
11
|
+
amount: Money;
|
|
12
|
+
payer: BitcoinAddress;
|
|
13
|
+
recipient: BitcoinAddress;
|
|
14
|
+
network: BitcoinNetworkModes;
|
|
15
|
+
feeRate: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isValidBitcoinTransaction({
|
|
19
|
+
amount,
|
|
20
|
+
payer,
|
|
21
|
+
recipient,
|
|
22
|
+
network,
|
|
23
|
+
utxos,
|
|
24
|
+
feeRate,
|
|
25
|
+
feeRates,
|
|
26
|
+
}: BitcoinTransaction) {
|
|
27
|
+
if (!isValidBitcoinAddress(payer) || !isValidBitcoinAddress(recipient)) {
|
|
28
|
+
throw new BitcoinError('InvalidAddress');
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
!isValidBitcoinNetworkAddress(payer, network) ||
|
|
32
|
+
!isValidBitcoinNetworkAddress(recipient, network)
|
|
33
|
+
) {
|
|
34
|
+
throw new BitcoinError('InvalidNetworkAddress');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isBtcMinimumSpend({ amount })) {
|
|
38
|
+
throw new BitcoinError('InsufficientAmount');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { spendableBtc } = calculateMaxSpend({ recipient, utxos, feeRate, feeRates });
|
|
42
|
+
|
|
43
|
+
if (!isBtcBalanceSufficient({ amount, spendableBtc })) {
|
|
44
|
+
throw new BitcoinError('InsufficientFunds');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|