@atomicfinance/bitcoin-wallet-provider 2.4.2 → 3.0.0

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.
@@ -1,23 +1,26 @@
1
- import Provider from '@atomicfinance/provider';
2
- import {
3
- CreateMultisigResponse,
4
- finalizePSBTResponse,
5
- FinanceWalletProvider,
6
- Input,
7
- Output,
8
- } from '@atomicfinance/types';
9
- import { BitcoinNetwork } from '@liquality/bitcoin-networks';
10
1
  import {
2
+ CoinSelectTarget,
11
3
  decodeRawTransaction,
12
4
  normalizeTransactionObject,
13
5
  selectCoins,
14
- } from '@liquality/bitcoin-utils';
15
- import { Address, bitcoin as bT, Transaction } from '@liquality/types';
16
- import assert from 'assert';
6
+ } from '@atomicfinance/bitcoin-utils';
7
+ import { InsufficientBalanceError } from '@atomicfinance/errors';
8
+ import Provider from '@atomicfinance/provider';
9
+ import {
10
+ Address,
11
+ BigNumber,
12
+ bitcoin as bT,
13
+ ChainProvider,
14
+ SendOptions,
15
+ Transaction,
16
+ WalletProvider,
17
+ } from '@atomicfinance/types';
18
+ import { addressToString } from '@atomicfinance/utils';
19
+ import { BitcoinNetwork } from 'bitcoin-networks';
17
20
  import * as bitcoin from 'bitcoinjs-lib';
18
- import secp256k1 from 'secp256k1';
21
+ import { BIP32Interface } from 'bitcoinjs-lib';
22
+ import memoize from 'memoizee';
19
23
 
20
- const FEE_PER_BYTE_FALLBACK = 5;
21
24
  const ADDRESS_GAP = 20;
22
25
  const NONCHANGE_ADDRESS = 0;
23
26
  const CHANGE_ADDRESS = 1;
@@ -27,516 +30,731 @@ type UnusedAddressesBlacklist = {
27
30
  [address: string]: true;
28
31
  };
29
32
 
30
- export default class BitcoinWalletProvider
31
- extends Provider
32
- implements Partial<FinanceWalletProvider> {
33
- _network: BitcoinNetwork;
34
- _unusedAddressesBlacklist: UnusedAddressesBlacklist;
33
+ export enum AddressSearchType {
34
+ EXTERNAL,
35
+ CHANGE,
36
+ EXTERNAL_OR_CHANGE,
37
+ }
35
38
 
36
- constructor(network: BitcoinNetwork) {
37
- super();
39
+ type DerivationCache = { [index: string]: Address };
38
40
 
39
- this._network = network;
40
- this._unusedAddressesBlacklist = {};
41
- }
41
+ type Constructor<T = unknown> = new (...args: any[]) => T;
42
42
 
43
- async buildSweepTransactionWithSetOutputs(
44
- externalChangeAddress: string,
45
- feePerByte: number,
46
- _outputs: Output[],
47
- fixedInputs: Input[],
48
- ) {
49
- return this._buildSweepTransaction(
50
- externalChangeAddress,
51
- feePerByte,
52
- _outputs,
53
- fixedInputs,
54
- );
55
- }
43
+ interface BitcoinWalletProviderOptions {
44
+ network: BitcoinNetwork;
45
+ baseDerivationPath: string;
46
+ addressType?: bT.AddressType;
47
+ }
56
48
 
57
- getUnusedAddressesBlacklist(): UnusedAddressesBlacklist {
58
- return this._unusedAddressesBlacklist;
59
- }
49
+ export default <T extends Constructor<Provider>>(superclass: T) => {
50
+ abstract class BitcoinWalletProvider
51
+ extends superclass
52
+ implements Partial<ChainProvider>, Partial<WalletProvider> {
53
+ _network: BitcoinNetwork;
54
+ _unusedAddressesBlacklist: UnusedAddressesBlacklist;
55
+ _maxAddressesToDerive: number;
56
+ _baseDerivationPath: string;
57
+ _addressType: bT.AddressType;
58
+ _derivationCache: DerivationCache;
59
+
60
+ constructor(...args: any[]) {
61
+ const options = args[0] as BitcoinWalletProviderOptions;
62
+ const {
63
+ network,
64
+ baseDerivationPath,
65
+ addressType = bT.AddressType.BECH32,
66
+ } = options;
67
+ const addressTypes = Object.values(bT.AddressType);
68
+ if (!addressTypes.includes(addressType)) {
69
+ throw new Error(`addressType must be one of ${addressTypes.join(',')}`);
70
+ }
60
71
 
61
- setUnusedAddressesBlacklist(
62
- unusedAddressesBlacklist: UnusedAddressesBlacklist,
63
- ) {
64
- this._unusedAddressesBlacklist = unusedAddressesBlacklist;
65
- }
72
+ super(options);
73
+
74
+ this._baseDerivationPath = baseDerivationPath;
75
+ this._network = network;
76
+ this._addressType = addressType;
77
+ this._derivationCache = {};
78
+ this._unusedAddressesBlacklist = {};
79
+ this._maxAddressesToDerive = 5000;
80
+ }
81
+
82
+ abstract baseDerivationNode(): Promise<BIP32Interface>;
83
+ abstract _buildTransaction(
84
+ targets: bT.OutputTarget[],
85
+ feePerByte?: number,
86
+ fixedInputs?: bT.Input[],
87
+ ): Promise<{ hex: string; fee: number }>;
88
+ abstract _buildSweepTransaction(
89
+ externalChangeAddress: string,
90
+ feePerByte?: number,
91
+ ): Promise<{ hex: string; fee: number }>;
92
+ abstract signPSBT(
93
+ data: string,
94
+ inputs: bT.PsbtInputTarget[],
95
+ ): Promise<string>;
96
+ abstract signBatchP2SHTransaction(
97
+ inputs: [
98
+ { inputTxHex: string; index: number; vout: any; outputScript: Buffer },
99
+ ],
100
+ addresses: string,
101
+ tx: any,
102
+ lockTime?: number,
103
+ segwit?: boolean,
104
+ ): Promise<Buffer[]>;
105
+
106
+ getDerivationCache() {
107
+ return this._derivationCache;
108
+ }
66
109
 
67
- _createMultisigPayment(m: number, pubkeys: string[]): bitcoin.Payment {
68
- if (m > pubkeys.length) {
69
- throw new Error(
70
- `not enough keys supplied (got ${pubkeys.length} keys, but need at least ${m} to redeem)`,
110
+ async setDerivationCache(derivationCache: DerivationCache) {
111
+ const address = await this.getDerivationPathAddress(
112
+ Object.keys(derivationCache)[0],
71
113
  );
114
+ if (derivationCache[address.derivationPath].address !== address.address) {
115
+ throw new Error(
116
+ `derivationCache at ${address.derivationPath} does not match`,
117
+ );
118
+ }
119
+ this._derivationCache = derivationCache;
72
120
  }
73
- // Create m-of-n multisig
74
- const p2ms = bitcoin.payments.p2ms({
75
- m: m,
76
- pubkeys: pubkeys.map((key: string) => Buffer.from(key, 'hex')),
77
- network: this._network,
78
- });
79
-
80
- // Create p2wsh for multisig
81
- const p2wsh = bitcoin.payments.p2wsh({
82
- redeem: p2ms,
83
- network: this._network,
84
- });
85
-
86
- return p2wsh;
87
- }
88
121
 
89
- /**
90
- * Creates a native-segwit multi-signature address (P2MS in P2WSH) with n signatures of m required keys
91
- * https://developer.bitcoin.org/reference/rpc/createmultisig.html
92
- * @param m the number of required signatures
93
- * @param pubkeys n possible pubkeys in total
94
- * @returns a json object containing the `address` and `redeemScript`
95
- */
96
- createMultisig(m: number, pubkeys: string[]): CreateMultisigResponse {
97
- const p2wsh = this._createMultisigPayment(m, pubkeys);
98
- return {
99
- address: p2wsh.address,
100
- redeemScript: p2wsh.redeem?.output?.toString('hex'),
101
- };
102
- }
122
+ sendOptionsToOutputs(transactions: SendOptions[]): bT.OutputTarget[] {
123
+ const targets: bT.OutputTarget[] = [];
103
124
 
104
- /**
105
- * Creates a PSBT of a native-segwit multi-signature address (P2MS in P2WSH) with n signatures of m required keys
106
- * https://developer.bitcoin.org/reference/rpc/createmultisig.html
107
- * https://developer.bitcoin.org/reference/rpc/createpsbt.html
108
- * @param m the number of required signatures
109
- * @param pubkeys n possible pubkeys in total
110
- * @param inputs the Inputs to the PSBT
111
- * @param ouputs the Outputs to the PSBT
112
- * @returns a base64 encoded psbt string
113
- */
114
- buildMultisigPSBT(
115
- m: number,
116
- pubkeys: string[],
117
- inputs: Input[],
118
- outputs: Output[],
119
- ): string {
120
- assert(inputs.length > 0, 'no inputs found');
121
- assert(outputs.length > 0, 'no outputs found');
122
-
123
- const p2wsh = this._createMultisigPayment(m, pubkeys);
124
-
125
- // Verify pubkeyhash for all inputs matches the p2wsh hash
126
- assert(
127
- inputs.every(
128
- (input: Input) => p2wsh.output.toString('hex') === input.scriptPubKey,
129
- ),
130
- 'address pubkeyhash does not match input scriptPubKey',
131
- );
132
-
133
- // creator
134
- const psbt = new bitcoin.Psbt({ network: this._network });
135
-
136
- // updater
137
- inputs.forEach((input: Input) => {
138
- psbt.addInput({
139
- hash: input.txid,
140
- index: input.vout,
141
- witnessUtxo: { script: p2wsh.output, value: input.value },
142
- witnessScript: p2wsh.redeem.output,
143
- });
144
- });
125
+ transactions.forEach((tx) => {
126
+ if (tx.to && tx.value && tx.value.gt(0)) {
127
+ targets.push({
128
+ address: addressToString(tx.to),
129
+ value: tx.value.toNumber(),
130
+ });
131
+ }
145
132
 
146
- outputs.forEach((output: Output) => {
147
- psbt.addOutput({
148
- address: output.to,
149
- value: output.value,
133
+ if (tx.data) {
134
+ const scriptBuffer = bitcoin.script.compile([
135
+ bitcoin.script.OPS.OP_RETURN,
136
+ Buffer.from(tx.data, 'hex'),
137
+ ]);
138
+ targets.push({
139
+ value: 0,
140
+ script: scriptBuffer,
141
+ });
142
+ }
150
143
  });
151
- });
152
144
 
153
- return psbt.toBase64();
154
- }
145
+ return targets;
146
+ }
155
147
 
156
- /**
157
- * Update a PSBT with input information from our wallet and then sign inputs that we can sign for
158
- * https://developer.bitcoin.org/reference/rpc/walletprocesspsbt.html
159
- * @param psbt a base64 encoded psbt string (P2WSH only)
160
- * @returns a base64 encoded signed psbt string
161
- */
162
- async walletProcessPSBT(psbtString: string): Promise<string> {
163
- const psbt = bitcoin.Psbt.fromBase64(psbtString);
164
-
165
- await Promise.all(
166
- psbt.data.inputs.map(async (input, i: number) => {
167
- assert(
168
- psbt.getInputType(i).slice(0, 5) === 'p2wsh',
169
- 'only accepts P2WSH inputs',
170
- );
148
+ async buildTransaction(output: bT.OutputTarget, feePerByte: number) {
149
+ return this._buildTransaction([output], feePerByte);
150
+ }
171
151
 
172
- const scriptStack = bitcoin.script.decompile(input.witnessScript);
173
- const pubkeys = scriptStack.filter(
174
- (data) => Buffer.isBuffer(data) && secp256k1.publicKeyVerify(data),
175
- );
152
+ async buildBatchTransaction(outputs: bT.OutputTarget[]) {
153
+ return this._buildTransaction(outputs);
154
+ }
176
155
 
177
- await Promise.all(
178
- pubkeys.map(async (key) => {
179
- // create address using pubkey
180
- const { address: addressString } = bitcoin.payments.p2wpkh({
181
- pubkey: key as Buffer,
182
- network: this._network,
183
- });
184
-
185
- // Retrieve address object from wallet using address
186
- const address: Address = await this.quickFindAddress([
187
- addressString,
188
- ]);
189
-
190
- // exit if address doesn't exist in wallet
191
- if (!address) return;
192
-
193
- // derive keypair
194
- const keyPair = await this.getMethod('keyPair')(
195
- address.derivationPath,
196
- );
156
+ async _sendTransaction(
157
+ transactions: bT.OutputTarget[],
158
+ feePerByte?: number,
159
+ ) {
160
+ const { hex, fee } = await this._buildTransaction(
161
+ transactions,
162
+ feePerByte,
163
+ );
164
+ await this.getMethod('sendRawTransaction')(hex);
165
+ return normalizeTransactionObject(
166
+ decodeRawTransaction(hex, this._network),
167
+ fee,
168
+ );
169
+ }
197
170
 
198
- // sign PSBT using keypair
199
- psbt.signInput(i, keyPair);
200
- }),
201
- );
202
- }),
203
- );
171
+ async sendTransaction(options: SendOptions) {
172
+ return this._sendTransaction(
173
+ this.sendOptionsToOutputs([options]),
174
+ options.fee as number,
175
+ );
176
+ }
204
177
 
205
- psbt.validateSignaturesOfAllInputs(); // ensure all signatures are valid!
206
- return psbt.toBase64();
207
- }
178
+ async sendBatchTransaction(transactions: SendOptions[]) {
179
+ return this._sendTransaction(this.sendOptionsToOutputs(transactions));
180
+ }
208
181
 
209
- /**
210
- * Finalize the inputs of a PSBT. If the transaction is fully signed, it will
211
- * produce a network serialized transaction which can be broadcast with sendrawtransaction
212
- * https://developer.bitcoin.org/reference/rpc/finalizepsbt.html
213
- * @param psbt a base64 encoded psbt string
214
- * @returns a json object containing `psbt` in base64, `hex` for transaction and `complete` for if
215
- * the transaction has a complete set of signatures
216
- */
217
- finalizePSBT(psbtString: string): finalizePSBTResponse {
218
- const psbt = bitcoin.Psbt.fromBase64(psbtString);
219
-
220
- try {
221
- psbt.validateSignaturesOfAllInputs(); // ensure all signatures are valid!
222
- psbt.finalizeAllInputs();
223
- } catch (error) {
224
- return {
225
- psbt: psbt.toBase64(),
226
- complete: false,
227
- };
182
+ async buildSweepTransaction(
183
+ externalChangeAddress: string,
184
+ feePerByte: number,
185
+ ) {
186
+ return this._buildSweepTransaction(externalChangeAddress, feePerByte);
228
187
  }
229
- return {
230
- psbt: psbt.toBase64(),
231
- hex: psbt.extractTransaction().toHex(),
232
- complete: true,
233
- };
234
- }
235
188
 
236
- async getUnusedAddress(change = false, numAddressPerCall = 100) {
237
- const addressType = change ? CHANGE_ADDRESS : NONCHANGE_ADDRESS;
238
- const key = change ? 'change' : 'nonChange';
189
+ async sendSweepTransaction(
190
+ externalChangeAddress: Address | string,
191
+ feePerByte: number,
192
+ ) {
193
+ const { hex, fee } = await this._buildSweepTransaction(
194
+ addressToString(externalChangeAddress),
195
+ feePerByte,
196
+ );
197
+ await this.getMethod('sendRawTransaction')(hex);
198
+ return normalizeTransactionObject(
199
+ decodeRawTransaction(hex, this._network),
200
+ fee,
201
+ );
202
+ }
239
203
 
240
- const address = await this._getUsedUnusedAddresses(
241
- numAddressPerCall,
242
- addressType,
243
- ).then(({ unusedAddress }) => unusedAddress[key]);
244
- this._unusedAddressesBlacklist[address.address] = true;
204
+ getUnusedAddressesBlacklist(): UnusedAddressesBlacklist {
205
+ return this._unusedAddressesBlacklist;
206
+ }
245
207
 
246
- return address;
247
- }
208
+ setUnusedAddressesBlacklist(
209
+ unusedAddressesBlacklist: UnusedAddressesBlacklist,
210
+ ) {
211
+ this._unusedAddressesBlacklist = unusedAddressesBlacklist;
212
+ }
248
213
 
249
- async _getUsedUnusedAddresses(numAddressPerCall = 100, addressType) {
250
- const usedAddresses = [];
251
- const addressCountMap = { change: 0, nonChange: 0 };
252
- const unusedAddressMap = { change: null, nonChange: null };
253
-
254
- let addrList;
255
- let addressIndex = 0;
256
- let changeAddresses: Address[] = [];
257
- let nonChangeAddresses: Address[] = [];
258
-
259
- /* eslint-disable no-unmodified-loop-condition */
260
- while (
261
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS &&
262
- (addressCountMap.change < ADDRESS_GAP ||
263
- addressCountMap.nonChange < ADDRESS_GAP)) ||
264
- (addressType === NONCHANGE_ADDRESS &&
265
- addressCountMap.nonChange < ADDRESS_GAP) ||
266
- (addressType === CHANGE_ADDRESS && addressCountMap.change < ADDRESS_GAP)
214
+ setMaxAddressesToDerive(maxAddressesToDerive: number) {
215
+ this._maxAddressesToDerive = maxAddressesToDerive;
216
+ }
217
+
218
+ getMaxAddressesToDerive() {
219
+ return this._maxAddressesToDerive;
220
+ }
221
+
222
+ async updateTransactionFee(
223
+ tx: Transaction<bitcoin.Transaction> | string,
224
+ newFeePerByte: number,
267
225
  ) {
268
- /* eslint-enable no-unmodified-loop-condition */
269
- addrList = [];
226
+ const txHash = typeof tx === 'string' ? tx : tx.hash;
227
+ const transaction: bT.Transaction = (
228
+ await this.getMethod('getTransactionByHash')(txHash)
229
+ )._raw;
230
+ const fixedInputs = [transaction.vin[0]]; // TODO: should this pick more than 1 input? RBF doesn't mandate it
231
+
232
+ const lookupAddresses = transaction.vout.map(
233
+ (vout) => vout.scriptPubKey.addresses[0],
234
+ );
235
+ const changeAddress = await this.findAddress(lookupAddresses, true);
236
+ const changeOutput = transaction.vout.find(
237
+ (vout) => vout.scriptPubKey.addresses[0] === changeAddress.address,
238
+ );
270
239
 
271
- if (
272
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
273
- addressType === CHANGE_ADDRESS) &&
274
- addressCountMap.change < ADDRESS_GAP
275
- ) {
276
- // Scanning for change addr
277
- changeAddresses = await this.client.wallet.getAddresses(
278
- addressIndex,
279
- numAddressPerCall,
280
- true,
240
+ let outputs = transaction.vout;
241
+ if (changeOutput) {
242
+ outputs = outputs.filter(
243
+ (vout) =>
244
+ vout.scriptPubKey.addresses[0] !==
245
+ changeOutput.scriptPubKey.addresses[0],
281
246
  );
282
- addrList = addrList.concat(changeAddresses);
283
- } else {
284
- changeAddresses = [];
285
247
  }
286
248
 
287
- if (
288
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
289
- addressType === NONCHANGE_ADDRESS) &&
290
- addressCountMap.nonChange < ADDRESS_GAP
291
- ) {
292
- // Scanning for non change addr
293
- nonChangeAddresses = await this.client.wallet.getAddresses(
294
- addressIndex,
295
- numAddressPerCall,
296
- false,
297
- );
298
- addrList = addrList.concat(nonChangeAddresses);
299
- }
249
+ // TODO more checks?
250
+ const transactions = outputs.map((output) => ({
251
+ address: output.scriptPubKey.addresses[0],
252
+ value: new BigNumber(output.value).times(1e8).toNumber(),
253
+ }));
254
+ const { hex, fee } = await this._buildTransaction(
255
+ transactions,
256
+ newFeePerByte,
257
+ fixedInputs,
258
+ );
259
+ await this.getMethod('sendRawTransaction')(hex);
260
+ return normalizeTransactionObject(
261
+ decodeRawTransaction(hex, this._network),
262
+ fee,
263
+ );
264
+ }
300
265
 
301
- const transactionCounts = await this.getMethod(
302
- 'getAddressTransactionCounts',
303
- )(addrList);
266
+ async getUnusedAddress(change = false, numAddressPerCall = 100) {
267
+ const addressType = change ? CHANGE_ADDRESS : NONCHANGE_ADDRESS;
268
+ const key = change ? 'change' : 'nonChange';
304
269
 
305
- for (const address of addrList) {
306
- const isUsed =
307
- transactionCounts[address.address] > 0 ||
308
- this._unusedAddressesBlacklist[address.address];
309
- const isChangeAddress = changeAddresses.find(
310
- (a) => address.address === a.address,
311
- );
312
- const key = isChangeAddress ? 'change' : 'nonChange';
270
+ const address = await this._getUsedUnusedAddresses(
271
+ numAddressPerCall,
272
+ addressType,
273
+ ).then(({ unusedAddress }) => unusedAddress[key]);
274
+ this._unusedAddressesBlacklist[address.address] = true;
313
275
 
314
- if (isUsed) {
315
- usedAddresses.push(address);
316
- addressCountMap[key] = 0;
317
- unusedAddressMap[key] = null;
276
+ return address;
277
+ }
278
+
279
+ async _getUsedUnusedAddresses(numAddressPerCall = 100, addressType) {
280
+ const usedAddresses = [];
281
+ const addressCountMap = { change: 0, nonChange: 0 };
282
+ const unusedAddressMap = { change: null, nonChange: null };
283
+
284
+ let addrList;
285
+ let addressIndex = 0;
286
+ let changeAddresses: Address[] = [];
287
+ let nonChangeAddresses: Address[] = [];
288
+
289
+ /* eslint-disable no-unmodified-loop-condition */
290
+ while (
291
+ (addressType === NONCHANGE_OR_CHANGE_ADDRESS &&
292
+ (addressCountMap.change < ADDRESS_GAP ||
293
+ addressCountMap.nonChange < ADDRESS_GAP)) ||
294
+ (addressType === NONCHANGE_ADDRESS &&
295
+ addressCountMap.nonChange < ADDRESS_GAP) ||
296
+ (addressType === CHANGE_ADDRESS && addressCountMap.change < ADDRESS_GAP)
297
+ ) {
298
+ /* eslint-enable no-unmodified-loop-condition */
299
+ addrList = [];
300
+
301
+ if (
302
+ (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
303
+ addressType === CHANGE_ADDRESS) &&
304
+ addressCountMap.change < ADDRESS_GAP
305
+ ) {
306
+ // Scanning for change addr
307
+ changeAddresses = await this.client.wallet.getAddresses(
308
+ addressIndex,
309
+ numAddressPerCall,
310
+ true,
311
+ );
312
+ addrList = addrList.concat(changeAddresses);
318
313
  } else {
319
- addressCountMap[key]++;
314
+ changeAddresses = [];
315
+ }
316
+
317
+ if (
318
+ (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
319
+ addressType === NONCHANGE_ADDRESS) &&
320
+ addressCountMap.nonChange < ADDRESS_GAP
321
+ ) {
322
+ // Scanning for non change addr
323
+ nonChangeAddresses = await this.getAddresses(
324
+ addressIndex,
325
+ numAddressPerCall,
326
+ false,
327
+ );
328
+ addrList = addrList.concat(nonChangeAddresses);
329
+ }
320
330
 
321
- if (!unusedAddressMap[key]) {
322
- unusedAddressMap[key] = address;
331
+ const transactionCounts = await this.getMethod(
332
+ 'getAddressTransactionCounts',
333
+ )(addrList);
334
+
335
+ for (const address of addrList) {
336
+ const isUsed =
337
+ transactionCounts[address.address] > 0 ||
338
+ this._unusedAddressesBlacklist[address.address];
339
+ const isChangeAddress = changeAddresses.find(
340
+ (a) => address.address === a.address,
341
+ );
342
+ const key = isChangeAddress ? 'change' : 'nonChange';
343
+
344
+ if (isUsed) {
345
+ usedAddresses.push(address);
346
+ addressCountMap[key] = 0;
347
+ unusedAddressMap[key] = null;
348
+ } else {
349
+ addressCountMap[key]++;
350
+
351
+ if (!unusedAddressMap[key]) {
352
+ unusedAddressMap[key] = address;
353
+ }
323
354
  }
324
355
  }
356
+
357
+ addressIndex += numAddressPerCall;
325
358
  }
326
359
 
327
- addressIndex += numAddressPerCall;
360
+ let firstUnusedAddress;
361
+ const indexNonChange = unusedAddressMap.nonChange
362
+ ? unusedAddressMap.nonChange.index
363
+ : Infinity;
364
+ const indexChange = unusedAddressMap.change
365
+ ? unusedAddressMap.change.index
366
+ : Infinity;
367
+
368
+ if (indexNonChange <= indexChange)
369
+ firstUnusedAddress = unusedAddressMap.nonChange;
370
+ else firstUnusedAddress = unusedAddressMap.change;
371
+
372
+ return {
373
+ usedAddresses,
374
+ unusedAddress: unusedAddressMap,
375
+ firstUnusedAddress,
376
+ };
328
377
  }
329
378
 
330
- let firstUnusedAddress;
331
- const indexNonChange = unusedAddressMap.nonChange
332
- ? unusedAddressMap.nonChange.index
333
- : Infinity;
334
- const indexChange = unusedAddressMap.change
335
- ? unusedAddressMap.change.index
336
- : Infinity;
337
-
338
- if (indexNonChange <= indexChange)
339
- firstUnusedAddress = unusedAddressMap.nonChange;
340
- else firstUnusedAddress = unusedAddressMap.change;
341
-
342
- return {
343
- usedAddresses,
344
- unusedAddress: unusedAddressMap,
345
- firstUnusedAddress,
346
- };
347
- }
379
+ async getWalletAddress(address: string) {
380
+ const foundAddress = await this.findAddress([address]);
381
+ if (foundAddress) return foundAddress;
348
382
 
349
- async sendSweepTransactionWithSetOutputs(
350
- externalChangeAddress: string,
351
- feePerByte: number,
352
- _outputs: Output[],
353
- fixedInputs: Input[],
354
- ): Promise<Transaction<bT.Transaction>> {
355
- const { hex, fee } = await this._buildSweepTransaction(
356
- externalChangeAddress,
357
- feePerByte,
358
- _outputs,
359
- fixedInputs,
360
- );
361
- await this.getMethod('sendRawTransaction')(hex);
362
- return normalizeTransactionObject(
363
- decodeRawTransaction(hex, this._network),
364
- fee,
365
- );
366
- }
383
+ throw new Error('Wallet does not contain address');
384
+ }
367
385
 
368
- async _buildSweepTransaction(
369
- externalChangeAddress: string,
370
- feePerByte: number,
371
- _outputs: Output[] = [],
372
- fixedInputs: Input[],
373
- ) {
374
- const _feePerByte =
375
- feePerByte ||
376
- (await this.getMethod('getFeePerByte')()) ||
377
- FEE_PER_BYTE_FALLBACK;
378
- const inputs: Input[] = [];
379
- const outputs: Output[] = [];
380
- try {
381
- const inputsForAmount = await this.getMethod('getInputsForAmount')(
382
- _outputs,
383
- _feePerByte,
384
- fixedInputs,
385
- 100,
386
- true,
387
- );
388
- if (inputsForAmount.change) {
389
- throw Error('There should not be any change for sweeping transaction');
386
+ getAddressFromPublicKey(publicKey: Buffer) {
387
+ return this.getPaymentVariantFromPublicKey(publicKey).address;
388
+ }
389
+
390
+ getPaymentVariantFromPublicKey(publicKey: Buffer) {
391
+ if (this._addressType === bT.AddressType.LEGACY) {
392
+ return bitcoin.payments.p2pkh({
393
+ pubkey: publicKey,
394
+ network: this._network,
395
+ });
396
+ } else if (this._addressType === bT.AddressType.P2SH_SEGWIT) {
397
+ return bitcoin.payments.p2sh({
398
+ redeem: bitcoin.payments.p2wpkh({
399
+ pubkey: publicKey,
400
+ network: this._network,
401
+ }),
402
+ network: this._network,
403
+ });
404
+ } else if (this._addressType === bT.AddressType.BECH32) {
405
+ return bitcoin.payments.p2wpkh({
406
+ pubkey: publicKey,
407
+ network: this._network,
408
+ });
390
409
  }
391
- inputs.push(...(inputsForAmount.inputs || []));
392
- outputs.push(...(inputsForAmount.outputs || []));
393
- } catch (e) {
394
- if (fixedInputs.length === 0) {
395
- throw Error(
396
- `Inputs for amount doesn't exist and no fixedInputs provided`,
397
- );
410
+ }
411
+
412
+ async getDerivationPathAddress(path: string) {
413
+ if (path in this._derivationCache) {
414
+ return this._derivationCache[path];
398
415
  }
399
416
 
400
- const inputsForAmount = await this._getInputForAmountWithoutUtxoCheck(
401
- _outputs,
402
- _feePerByte,
403
- fixedInputs,
404
- );
405
- inputs.push(
406
- ...(inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo)) || []),
407
- );
408
- outputs.push(...(inputsForAmount.outputs || []));
409
- }
410
- _outputs.forEach((output) => {
411
- const spliceIndex = outputs.findIndex(
412
- (sweepOutput: Output) => output.value === sweepOutput.value,
413
- );
414
- outputs.splice(spliceIndex, 1);
415
- });
416
- _outputs.push({
417
- to: externalChangeAddress,
418
- value: outputs[0].value,
419
- });
420
- return this._buildTransactionWithoutUtxoCheck(
421
- _outputs,
422
- _feePerByte,
423
- inputs,
424
- );
425
- }
417
+ const baseDerivationNode = await this.baseDerivationNode();
418
+ const subPath = path.replace(this._baseDerivationPath + '/', '');
419
+ const publicKey = baseDerivationNode.derivePath(subPath).publicKey;
420
+ const address = this.getAddressFromPublicKey(publicKey);
421
+ const addressObject = new Address({
422
+ address,
423
+ publicKey: publicKey.toString('hex'),
424
+ derivationPath: path,
425
+ });
426
426
 
427
- _getInputForAmountWithoutUtxoCheck(
428
- _outputs: Output[],
429
- _feePerByte: number,
430
- fixedInputs: Input[],
431
- ) {
432
- const utxoBalance = fixedInputs.reduce((a, b) => a + (b['value'] || 0), 0);
433
- const outputBalance = _outputs.reduce((a, b) => a + (b['value'] || 0), 0);
434
- const amountToSend =
435
- utxoBalance -
436
- _feePerByte * ((_outputs.length + 1) * 39 + fixedInputs.length * 153); // todo better calculation
437
-
438
- const targets = _outputs.map((target, i) => ({
439
- id: 'main',
440
- value: target.value,
441
- }));
442
- if (amountToSend - outputBalance > 0) {
443
- targets.push({ id: 'main', value: amountToSend - outputBalance });
427
+ this._derivationCache[path] = addressObject;
428
+ return addressObject;
444
429
  }
445
430
 
446
- return selectCoins(
447
- fixedInputs,
448
- targets,
449
- Math.ceil(_feePerByte),
450
- fixedInputs,
451
- );
452
- }
431
+ /**
432
+ * getAddresses is an optimized version of upstream CAL's getAddresses.
433
+ * It removes the call to `asyncSetImmediate()`, speeding up the function by a factor of 6x.
434
+ *
435
+ * @param startingIndex
436
+ * @param numAddresses
437
+ * @param change
438
+ * @returns {Promise<Address[]>}
439
+ */
440
+ async getAddresses(
441
+ startingIndex = 0,
442
+ numAddresses = 1,
443
+ change = false,
444
+ ): Promise<Address[]> {
445
+ if (numAddresses < 1) {
446
+ throw new Error('You must return at least one address');
447
+ }
448
+
449
+ const addresses = [];
450
+ const lastIndex = startingIndex + numAddresses;
451
+ const changeVal = change ? '1' : '0';
452
+
453
+ for (
454
+ let currentIndex = startingIndex;
455
+ currentIndex < lastIndex;
456
+ currentIndex++
457
+ ) {
458
+ const subPath = changeVal + '/' + currentIndex;
459
+ const path = this._baseDerivationPath + '/' + subPath;
460
+ const addressObject = await this.getDerivationPathAddress(path);
461
+ addresses.push(addressObject);
462
+ }
453
463
 
454
- async _buildTransactionWithoutUtxoCheck(
455
- outputs: Output[],
456
- feePerByte: number,
457
- fixedInputs: Input[],
458
- ) {
459
- const network = this._network;
460
-
461
- const { fee } = this._getInputForAmountWithoutUtxoCheck(
462
- outputs,
463
- feePerByte,
464
- fixedInputs,
465
- );
466
- const inputs = fixedInputs;
467
-
468
- const txb = new bitcoin.TransactionBuilder(network);
469
-
470
- for (const output of outputs) {
471
- const to = output.to; // Allow for OP_RETURN
472
- txb.addOutput(to, output.value);
464
+ return addresses;
473
465
  }
474
466
 
475
- const prevOutScriptType = 'p2wpkh';
467
+ /**
468
+ * findAddress is an optimized version of upstream CAL's findAddress.
469
+ *
470
+ * It searches through both change and non-change addresses (if change arg is not provided) each iteration.
471
+ *
472
+ * This is in contrast to the original findAddress function which searches
473
+ * through all non-change addresses before moving on to change addresses.
474
+ *
475
+ * @param addresses
476
+ * @returns {Promise<Address>}
477
+ */
478
+ async findAddress(
479
+ addresses: string[],
480
+ change: boolean | null = null,
481
+ ): Promise<Address> {
482
+ const addressesPerCall = 20;
483
+ let index = 0;
484
+
485
+ while (index < this._maxAddressesToDerive) {
486
+ const walletAddresses = [];
487
+
488
+ if (change === null || change === false) {
489
+ walletAddresses.push(
490
+ ...(await this.getAddresses(index, addressesPerCall, false)),
491
+ );
492
+ }
476
493
 
477
- for (let i = 0; i < inputs.length; i++) {
478
- const wallet = await this.getMethod('getWalletAddress')(
479
- inputs[i].address,
480
- );
481
- const keyPair = await this.getMethod('keyPair')(wallet.derivationPath);
482
- const paymentVariant = this.getMethod('getPaymentVariantFromPublicKey')(
483
- keyPair.publicKey,
484
- );
494
+ if (change === null || change === true) {
495
+ walletAddresses.push(
496
+ ...(await this.getAddresses(index, addressesPerCall, true)),
497
+ );
498
+ }
499
+
500
+ const walletAddress = walletAddresses.find((walletAddr) =>
501
+ addresses.find((addr) => walletAddr.address === addr),
502
+ );
503
+
504
+ if (walletAddress) {
505
+ // Increment max addresses to derive by 100 if found within 100 addresses of maxAddressesToDerive
506
+ this._maxAddressesToDerive = Math.max(
507
+ this._maxAddressesToDerive,
508
+ index + 100,
509
+ );
510
+ return walletAddress;
511
+ }
512
+ index += addressesPerCall;
513
+ }
514
+ }
485
515
 
486
- txb.addInput(inputs[i].txid, inputs[i].vout, 0, paymentVariant.output);
516
+ async getUsedAddresses(numAddressPerCall = 100) {
517
+ return this._getUsedUnusedAddresses(
518
+ numAddressPerCall,
519
+ AddressSearchType.EXTERNAL_OR_CHANGE,
520
+ ).then(({ usedAddresses }) => usedAddresses);
487
521
  }
488
522
 
489
- for (let i = 0; i < inputs.length; i++) {
490
- const wallet = await this.getMethod('getWalletAddress')(
491
- inputs[i].address,
523
+ async withCachedUtxos(func: () => any) {
524
+ const originalGetMethod = this.getMethod;
525
+ const memoizedGetFeePerByte = memoize(this.getMethod('getFeePerByte'), {
526
+ primitive: true,
527
+ });
528
+ const memoizedGetUnspentTransactions = memoize(
529
+ this.getMethod('getUnspentTransactions'),
530
+ { primitive: true },
492
531
  );
493
- const keyPair = await this.getMethod('keyPair')(wallet.derivationPath);
494
- const paymentVariant = this.getMethod('getPaymentVariantFromPublicKey')(
495
- keyPair.publicKey,
532
+ const memoizedGetAddressTransactionCounts = memoize(
533
+ this.getMethod('getAddressTransactionCounts'),
534
+ {
535
+ primitive: true,
536
+ },
496
537
  );
497
- const needsWitness = true;
498
-
499
- const signParams = {
500
- prevOutScriptType,
501
- vin: i,
502
- keyPair,
503
- witnessValue: 0,
538
+ this.getMethod = (method: string, requestor: any = this) => {
539
+ if (method === 'getFeePerByte') return memoizedGetFeePerByte;
540
+ if (method === 'getUnspentTransactions')
541
+ return memoizedGetUnspentTransactions;
542
+ else if (method === 'getAddressTransactionCounts')
543
+ return memoizedGetAddressTransactionCounts;
544
+ else return originalGetMethod.bind(this)(method, requestor);
504
545
  };
505
546
 
506
- if (needsWitness) {
507
- signParams.witnessValue = inputs[i].value;
547
+ const result = await func.bind(this)();
548
+
549
+ this.getMethod = originalGetMethod;
550
+
551
+ return result;
552
+ }
553
+
554
+ async getTotalFee(opts: SendOptions, max: boolean) {
555
+ const targets = this.sendOptionsToOutputs([opts]);
556
+ if (!max) {
557
+ const { fee } = await this.getInputsForAmount(
558
+ targets,
559
+ opts.fee as number,
560
+ );
561
+ return fee;
562
+ } else {
563
+ const { fee } = await this.getInputsForAmount(
564
+ targets.filter((t) => !t.value),
565
+ opts.fee as number,
566
+ [],
567
+ 100,
568
+ true,
569
+ );
570
+ return fee;
508
571
  }
572
+ }
509
573
 
510
- txb.sign(signParams);
574
+ async getTotalFees(transactions: SendOptions[], max: boolean) {
575
+ const fees = await this.withCachedUtxos(async () => {
576
+ const fees: { [index: number]: BigNumber } = {};
577
+ for (const tx of transactions) {
578
+ const fee = await this.getTotalFee(tx, max);
579
+ fees[tx.fee as number] = new BigNumber(fee);
580
+ }
581
+ return fees;
582
+ });
583
+ return fees;
511
584
  }
512
585
 
513
- return { hex: txb.build().toHex(), fee };
514
- }
586
+ async getInputsForAmount(
587
+ _targets: bT.OutputTarget[],
588
+ feePerByte?: number,
589
+ fixedInputs: bT.Input[] = [],
590
+ numAddressPerCall = 100,
591
+ sweep = false,
592
+ ) {
593
+ let addressIndex = 0;
594
+ let changeAddresses: Address[] = [];
595
+ let externalAddresses: Address[] = [];
596
+ const addressCountMap = {
597
+ change: 0,
598
+ nonChange: 0,
599
+ };
515
600
 
516
- async quickFindAddress(addresses: string[]): Promise<Address> {
517
- const maxAddresses = 5000;
518
- const addressesPerCall = 5;
519
- let index = 0;
520
- while (index < maxAddresses) {
521
- const walletNonChangeAddresses = await this.getMethod('getAddresses')(
522
- index,
523
- addressesPerCall,
524
- true,
525
- );
526
- const walletChangeAddresses = await this.getMethod('getAddresses')(
527
- index,
528
- addressesPerCall,
529
- false,
530
- );
531
- const walletAddresses = [
532
- ...walletNonChangeAddresses,
533
- ...walletChangeAddresses,
534
- ];
535
- const walletAddress = walletAddresses.find((walletAddr) =>
536
- addresses.find((addr) => walletAddr.address === addr),
537
- );
538
- if (walletAddress) return walletAddress;
539
- index += addressesPerCall;
601
+ const feePerBytePromise = this.getMethod('getFeePerByte')();
602
+ let utxos: bT.UTXO[] = [];
603
+
604
+ while (
605
+ addressCountMap.change < ADDRESS_GAP ||
606
+ addressCountMap.nonChange < ADDRESS_GAP
607
+ ) {
608
+ let addrList: Address[] = [];
609
+
610
+ if (addressCountMap.change < ADDRESS_GAP) {
611
+ // Scanning for change addr
612
+ changeAddresses = await this.getAddresses(
613
+ addressIndex,
614
+ numAddressPerCall,
615
+ true,
616
+ );
617
+ addrList = addrList.concat(changeAddresses);
618
+ } else {
619
+ changeAddresses = [];
620
+ }
621
+
622
+ if (addressCountMap.nonChange < ADDRESS_GAP) {
623
+ // Scanning for non change addr
624
+ externalAddresses = await this.getAddresses(
625
+ addressIndex,
626
+ numAddressPerCall,
627
+ false,
628
+ );
629
+ addrList = addrList.concat(externalAddresses);
630
+ }
631
+
632
+ const fixedUtxos: bT.UTXO[] = [];
633
+ if (fixedInputs.length > 0) {
634
+ for (const input of fixedInputs) {
635
+ const txHex = await this.getMethod('getRawTransactionByHash')(
636
+ input.txid,
637
+ );
638
+ const tx = decodeRawTransaction(txHex, this._network);
639
+ const value = new BigNumber(tx.vout[input.vout].value)
640
+ .times(1e8)
641
+ .toNumber();
642
+ const address = tx.vout[input.vout].scriptPubKey.addresses[0];
643
+ const walletAddress = await this.getWalletAddress(address);
644
+ const utxo = {
645
+ ...input,
646
+ value,
647
+ address,
648
+ derivationPath: walletAddress.derivationPath,
649
+ };
650
+ fixedUtxos.push(utxo);
651
+ }
652
+ }
653
+
654
+ if (!sweep || fixedUtxos.length === 0) {
655
+ const _utxos: bT.UTXO[] = await this.getMethod(
656
+ 'getUnspentTransactions',
657
+ )(addrList);
658
+ utxos.push(
659
+ ..._utxos.map((utxo) => {
660
+ const addr = addrList.find((a) => a.address === utxo.address);
661
+ return {
662
+ ...utxo,
663
+ derivationPath: addr.derivationPath,
664
+ };
665
+ }),
666
+ );
667
+ } else {
668
+ utxos = fixedUtxos;
669
+ }
670
+
671
+ const utxoBalance = utxos.reduce((a, b) => a + (b.value || 0), 0);
672
+
673
+ const transactionCounts: bT.AddressTxCounts = await this.getMethod(
674
+ 'getAddressTransactionCounts',
675
+ )(addrList);
676
+
677
+ if (!feePerByte) feePerByte = await feePerBytePromise;
678
+ const minRelayFee = await this.getMethod('getMinRelayFee')();
679
+ if (feePerByte < minRelayFee) {
680
+ throw new Error(
681
+ `Fee supplied (${feePerByte} sat/b) too low. Minimum relay fee is ${minRelayFee} sat/b`,
682
+ );
683
+ }
684
+
685
+ let targets: CoinSelectTarget[];
686
+ if (sweep) {
687
+ const outputBalance = _targets.reduce(
688
+ (a, b) => a + (b['value'] || 0),
689
+ 0,
690
+ );
691
+
692
+ const sweepOutputSize = 39;
693
+ const paymentOutputSize =
694
+ _targets.filter((t) => t.value && t.address).length * 39;
695
+ const scriptOutputSize = _targets
696
+ .filter((t) => !t.value && t.script)
697
+ .reduce((size, t) => size + 39 + t.script.byteLength, 0);
698
+
699
+ const outputSize =
700
+ sweepOutputSize + paymentOutputSize + scriptOutputSize;
701
+ const inputSize = utxos.length * 153;
702
+
703
+ const sweepFee = feePerByte * (inputSize + outputSize);
704
+ const amountToSend = new BigNumber(utxoBalance).minus(sweepFee);
705
+
706
+ targets = _targets.map((target) => ({
707
+ id: 'main',
708
+ value: target.value,
709
+ script: target.script,
710
+ }));
711
+ targets.push({
712
+ id: 'main',
713
+ value: amountToSend.minus(outputBalance).toNumber(),
714
+ });
715
+ } else {
716
+ targets = _targets.map((target) => ({
717
+ id: 'main',
718
+ value: target.value,
719
+ script: target.script,
720
+ }));
721
+ }
722
+
723
+ const { inputs, outputs, change, fee } = selectCoins(
724
+ utxos,
725
+ targets,
726
+ Math.ceil(feePerByte),
727
+ fixedUtxos,
728
+ );
729
+
730
+ if (inputs && outputs) {
731
+ return {
732
+ inputs,
733
+ change,
734
+ outputs,
735
+ fee,
736
+ };
737
+ }
738
+
739
+ for (const address of addrList) {
740
+ const isUsed = transactionCounts[address.address];
741
+ const isChangeAddress = changeAddresses.find(
742
+ (a) => address.address === a.address,
743
+ );
744
+ const key = isChangeAddress ? 'change' : 'nonChange';
745
+
746
+ if (isUsed) {
747
+ addressCountMap[key] = 0;
748
+ } else {
749
+ addressCountMap[key]++;
750
+ }
751
+ }
752
+
753
+ addressIndex += numAddressPerCall;
754
+ }
755
+
756
+ throw new InsufficientBalanceError('Not enough balance');
540
757
  }
541
758
  }
542
- }
759
+ return BitcoinWalletProvider;
760
+ };