@atomicfinance/bitcoin-wallet-provider 2.5.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,596 +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;
35
- _maxAddressesToDerive: number;
33
+ export enum AddressSearchType {
34
+ EXTERNAL,
35
+ CHANGE,
36
+ EXTERNAL_OR_CHANGE,
37
+ }
36
38
 
37
- constructor(network: BitcoinNetwork) {
38
- super();
39
+ type DerivationCache = { [index: string]: Address };
39
40
 
40
- this._network = network;
41
- this._unusedAddressesBlacklist = {};
42
- this._maxAddressesToDerive = 5000;
43
- }
41
+ type Constructor<T = unknown> = new (...args: any[]) => T;
44
42
 
45
- async buildSweepTransactionWithSetOutputs(
46
- externalChangeAddress: string,
47
- feePerByte: number,
48
- _outputs: Output[],
49
- fixedInputs: Input[],
50
- ) {
51
- return this._buildSweepTransaction(
52
- externalChangeAddress,
53
- feePerByte,
54
- _outputs,
55
- fixedInputs,
56
- );
57
- }
43
+ interface BitcoinWalletProviderOptions {
44
+ network: BitcoinNetwork;
45
+ baseDerivationPath: string;
46
+ addressType?: bT.AddressType;
47
+ }
58
48
 
59
- getUnusedAddressesBlacklist(): UnusedAddressesBlacklist {
60
- return this._unusedAddressesBlacklist;
61
- }
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
+ }
62
71
 
63
- setUnusedAddressesBlacklist(
64
- unusedAddressesBlacklist: UnusedAddressesBlacklist,
65
- ) {
66
- this._unusedAddressesBlacklist = unusedAddressesBlacklist;
67
- }
72
+ super(options);
68
73
 
69
- setMaxAddressesToDerive(maxAddressesToDerive: number) {
70
- this._maxAddressesToDerive = maxAddressesToDerive;
71
- }
74
+ this._baseDerivationPath = baseDerivationPath;
75
+ this._network = network;
76
+ this._addressType = addressType;
77
+ this._derivationCache = {};
78
+ this._unusedAddressesBlacklist = {};
79
+ this._maxAddressesToDerive = 5000;
80
+ }
72
81
 
73
- getMaxAddressesToDerive() {
74
- return this._maxAddressesToDerive;
75
- }
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
+ }
76
109
 
77
- _createMultisigPayment(m: number, pubkeys: string[]): bitcoin.Payment {
78
- if (m > pubkeys.length) {
79
- throw new Error(
80
- `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],
81
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;
82
120
  }
83
- // Create m-of-n multisig
84
- const p2ms = bitcoin.payments.p2ms({
85
- m: m,
86
- pubkeys: pubkeys.map((key: string) => Buffer.from(key, 'hex')),
87
- network: this._network,
88
- });
89
-
90
- // Create p2wsh for multisig
91
- const p2wsh = bitcoin.payments.p2wsh({
92
- redeem: p2ms,
93
- network: this._network,
94
- });
95
-
96
- return p2wsh;
97
- }
98
121
 
99
- /**
100
- * Creates a native-segwit multi-signature address (P2MS in P2WSH) with n signatures of m required keys
101
- * https://developer.bitcoin.org/reference/rpc/createmultisig.html
102
- * @param m the number of required signatures
103
- * @param pubkeys n possible pubkeys in total
104
- * @returns a json object containing the `address` and `redeemScript`
105
- */
106
- createMultisig(m: number, pubkeys: string[]): CreateMultisigResponse {
107
- const p2wsh = this._createMultisigPayment(m, pubkeys);
108
- return {
109
- address: p2wsh.address,
110
- redeemScript: p2wsh.redeem?.output?.toString('hex'),
111
- };
112
- }
122
+ sendOptionsToOutputs(transactions: SendOptions[]): bT.OutputTarget[] {
123
+ const targets: bT.OutputTarget[] = [];
113
124
 
114
- /**
115
- * Creates a PSBT of a native-segwit multi-signature address (P2MS in P2WSH) with n signatures of m required keys
116
- * https://developer.bitcoin.org/reference/rpc/createmultisig.html
117
- * https://developer.bitcoin.org/reference/rpc/createpsbt.html
118
- * @param m the number of required signatures
119
- * @param pubkeys n possible pubkeys in total
120
- * @param inputs the Inputs to the PSBT
121
- * @param ouputs the Outputs to the PSBT
122
- * @returns a base64 encoded psbt string
123
- */
124
- buildMultisigPSBT(
125
- m: number,
126
- pubkeys: string[],
127
- inputs: Input[],
128
- outputs: Output[],
129
- ): string {
130
- assert(inputs.length > 0, 'no inputs found');
131
- assert(outputs.length > 0, 'no outputs found');
132
-
133
- const p2wsh = this._createMultisigPayment(m, pubkeys);
134
-
135
- // Verify pubkeyhash for all inputs matches the p2wsh hash
136
- assert(
137
- inputs.every(
138
- (input: Input) => p2wsh.output.toString('hex') === input.scriptPubKey,
139
- ),
140
- 'address pubkeyhash does not match input scriptPubKey',
141
- );
142
-
143
- // creator
144
- const psbt = new bitcoin.Psbt({ network: this._network });
145
-
146
- // updater
147
- inputs.forEach((input: Input) => {
148
- psbt.addInput({
149
- hash: input.txid,
150
- index: input.vout,
151
- witnessUtxo: { script: p2wsh.output, value: input.value },
152
- witnessScript: p2wsh.redeem.output,
153
- });
154
- });
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
+ }
155
132
 
156
- outputs.forEach((output: Output) => {
157
- psbt.addOutput({
158
- address: output.to,
159
- 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
+ }
160
143
  });
161
- });
162
144
 
163
- return psbt.toBase64();
164
- }
145
+ return targets;
146
+ }
165
147
 
166
- /**
167
- * Update a PSBT with input information from our wallet and then sign inputs that we can sign for
168
- * https://developer.bitcoin.org/reference/rpc/walletprocesspsbt.html
169
- * @param psbt a base64 encoded psbt string (P2WSH only)
170
- * @returns a base64 encoded signed psbt string
171
- */
172
- async walletProcessPSBT(psbtString: string): Promise<string> {
173
- const psbt = bitcoin.Psbt.fromBase64(psbtString);
174
-
175
- await Promise.all(
176
- psbt.data.inputs.map(async (input, i: number) => {
177
- assert(
178
- psbt.getInputType(i).slice(0, 5) === 'p2wsh',
179
- 'only accepts P2WSH inputs',
180
- );
148
+ async buildTransaction(output: bT.OutputTarget, feePerByte: number) {
149
+ return this._buildTransaction([output], feePerByte);
150
+ }
181
151
 
182
- const scriptStack = bitcoin.script.decompile(input.witnessScript);
183
- const pubkeys = scriptStack.filter(
184
- (data) => Buffer.isBuffer(data) && secp256k1.publicKeyVerify(data),
185
- );
152
+ async buildBatchTransaction(outputs: bT.OutputTarget[]) {
153
+ return this._buildTransaction(outputs);
154
+ }
186
155
 
187
- await Promise.all(
188
- pubkeys.map(async (key) => {
189
- // create address using pubkey
190
- const { address: addressString } = bitcoin.payments.p2wpkh({
191
- pubkey: key as Buffer,
192
- network: this._network,
193
- });
194
-
195
- // Retrieve address object from wallet using address
196
- const address: Address = await this.quickFindAddress([
197
- addressString,
198
- ]);
199
-
200
- // exit if address doesn't exist in wallet
201
- if (!address) return;
202
-
203
- // derive keypair
204
- const keyPair = await this.getMethod('keyPair')(
205
- address.derivationPath,
206
- );
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
+ }
207
170
 
208
- // sign PSBT using keypair
209
- psbt.signInput(i, keyPair);
210
- }),
211
- );
212
- }),
213
- );
171
+ async sendTransaction(options: SendOptions) {
172
+ return this._sendTransaction(
173
+ this.sendOptionsToOutputs([options]),
174
+ options.fee as number,
175
+ );
176
+ }
214
177
 
215
- psbt.validateSignaturesOfAllInputs(); // ensure all signatures are valid!
216
- return psbt.toBase64();
217
- }
178
+ async sendBatchTransaction(transactions: SendOptions[]) {
179
+ return this._sendTransaction(this.sendOptionsToOutputs(transactions));
180
+ }
218
181
 
219
- /**
220
- * Finalize the inputs of a PSBT. If the transaction is fully signed, it will
221
- * produce a network serialized transaction which can be broadcast with sendrawtransaction
222
- * https://developer.bitcoin.org/reference/rpc/finalizepsbt.html
223
- * @param psbt a base64 encoded psbt string
224
- * @returns a json object containing `psbt` in base64, `hex` for transaction and `complete` for if
225
- * the transaction has a complete set of signatures
226
- */
227
- finalizePSBT(psbtString: string): finalizePSBTResponse {
228
- const psbt = bitcoin.Psbt.fromBase64(psbtString);
229
-
230
- try {
231
- psbt.validateSignaturesOfAllInputs(); // ensure all signatures are valid!
232
- psbt.finalizeAllInputs();
233
- } catch (error) {
234
- return {
235
- psbt: psbt.toBase64(),
236
- complete: false,
237
- };
182
+ async buildSweepTransaction(
183
+ externalChangeAddress: string,
184
+ feePerByte: number,
185
+ ) {
186
+ return this._buildSweepTransaction(externalChangeAddress, feePerByte);
238
187
  }
239
- return {
240
- psbt: psbt.toBase64(),
241
- hex: psbt.extractTransaction().toHex(),
242
- complete: true,
243
- };
244
- }
245
188
 
246
- async getUnusedAddress(change = false, numAddressPerCall = 100) {
247
- const addressType = change ? CHANGE_ADDRESS : NONCHANGE_ADDRESS;
248
- 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
+ }
249
203
 
250
- const address = await this._getUsedUnusedAddresses(
251
- numAddressPerCall,
252
- addressType,
253
- ).then(({ unusedAddress }) => unusedAddress[key]);
254
- this._unusedAddressesBlacklist[address.address] = true;
204
+ getUnusedAddressesBlacklist(): UnusedAddressesBlacklist {
205
+ return this._unusedAddressesBlacklist;
206
+ }
255
207
 
256
- return address;
257
- }
208
+ setUnusedAddressesBlacklist(
209
+ unusedAddressesBlacklist: UnusedAddressesBlacklist,
210
+ ) {
211
+ this._unusedAddressesBlacklist = unusedAddressesBlacklist;
212
+ }
213
+
214
+ setMaxAddressesToDerive(maxAddressesToDerive: number) {
215
+ this._maxAddressesToDerive = maxAddressesToDerive;
216
+ }
217
+
218
+ getMaxAddressesToDerive() {
219
+ return this._maxAddressesToDerive;
220
+ }
258
221
 
259
- async _getUsedUnusedAddresses(numAddressPerCall = 100, addressType) {
260
- const usedAddresses = [];
261
- const addressCountMap = { change: 0, nonChange: 0 };
262
- const unusedAddressMap = { change: null, nonChange: null };
263
-
264
- let addrList;
265
- let addressIndex = 0;
266
- let changeAddresses: Address[] = [];
267
- let nonChangeAddresses: Address[] = [];
268
-
269
- /* eslint-disable no-unmodified-loop-condition */
270
- while (
271
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS &&
272
- (addressCountMap.change < ADDRESS_GAP ||
273
- addressCountMap.nonChange < ADDRESS_GAP)) ||
274
- (addressType === NONCHANGE_ADDRESS &&
275
- addressCountMap.nonChange < ADDRESS_GAP) ||
276
- (addressType === CHANGE_ADDRESS && addressCountMap.change < ADDRESS_GAP)
222
+ async updateTransactionFee(
223
+ tx: Transaction<bitcoin.Transaction> | string,
224
+ newFeePerByte: number,
277
225
  ) {
278
- /* eslint-enable no-unmodified-loop-condition */
279
- 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
+ );
280
239
 
281
- if (
282
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
283
- addressType === CHANGE_ADDRESS) &&
284
- addressCountMap.change < ADDRESS_GAP
285
- ) {
286
- // Scanning for change addr
287
- changeAddresses = await this.client.wallet.getAddresses(
288
- addressIndex,
289
- numAddressPerCall,
290
- 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],
291
246
  );
292
- addrList = addrList.concat(changeAddresses);
293
- } else {
294
- changeAddresses = [];
295
247
  }
296
248
 
297
- if (
298
- (addressType === NONCHANGE_OR_CHANGE_ADDRESS ||
299
- addressType === NONCHANGE_ADDRESS) &&
300
- addressCountMap.nonChange < ADDRESS_GAP
301
- ) {
302
- // Scanning for non change addr
303
- nonChangeAddresses = await this.quickGetAddresses(
304
- addressIndex,
305
- numAddressPerCall,
306
- false,
307
- );
308
- addrList = addrList.concat(nonChangeAddresses);
309
- }
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
+ }
310
265
 
311
- const transactionCounts = await this.getMethod(
312
- 'getAddressTransactionCounts',
313
- )(addrList);
266
+ async getUnusedAddress(change = false, numAddressPerCall = 100) {
267
+ const addressType = change ? CHANGE_ADDRESS : NONCHANGE_ADDRESS;
268
+ const key = change ? 'change' : 'nonChange';
314
269
 
315
- for (const address of addrList) {
316
- const isUsed =
317
- transactionCounts[address.address] > 0 ||
318
- this._unusedAddressesBlacklist[address.address];
319
- const isChangeAddress = changeAddresses.find(
320
- (a) => address.address === a.address,
321
- );
322
- 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;
275
+
276
+ return address;
277
+ }
323
278
 
324
- if (isUsed) {
325
- usedAddresses.push(address);
326
- addressCountMap[key] = 0;
327
- unusedAddressMap[key] = null;
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);
328
313
  } else {
329
- 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
+ }
330
330
 
331
- if (!unusedAddressMap[key]) {
332
- 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
+ }
333
354
  }
334
355
  }
356
+
357
+ addressIndex += numAddressPerCall;
335
358
  }
336
359
 
337
- 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
+ };
338
377
  }
339
378
 
340
- let firstUnusedAddress;
341
- const indexNonChange = unusedAddressMap.nonChange
342
- ? unusedAddressMap.nonChange.index
343
- : Infinity;
344
- const indexChange = unusedAddressMap.change
345
- ? unusedAddressMap.change.index
346
- : Infinity;
347
-
348
- if (indexNonChange <= indexChange)
349
- firstUnusedAddress = unusedAddressMap.nonChange;
350
- else firstUnusedAddress = unusedAddressMap.change;
351
-
352
- return {
353
- usedAddresses,
354
- unusedAddress: unusedAddressMap,
355
- firstUnusedAddress,
356
- };
357
- }
379
+ async getWalletAddress(address: string) {
380
+ const foundAddress = await this.findAddress([address]);
381
+ if (foundAddress) return foundAddress;
358
382
 
359
- async sendSweepTransactionWithSetOutputs(
360
- externalChangeAddress: string,
361
- feePerByte: number,
362
- _outputs: Output[],
363
- fixedInputs: Input[],
364
- ): Promise<Transaction<bT.Transaction>> {
365
- const { hex, fee } = await this._buildSweepTransaction(
366
- externalChangeAddress,
367
- feePerByte,
368
- _outputs,
369
- fixedInputs,
370
- );
371
- await this.getMethod('sendRawTransaction')(hex);
372
- return normalizeTransactionObject(
373
- decodeRawTransaction(hex, this._network),
374
- fee,
375
- );
376
- }
383
+ throw new Error('Wallet does not contain address');
384
+ }
377
385
 
378
- async _buildSweepTransaction(
379
- externalChangeAddress: string,
380
- feePerByte: number,
381
- _outputs: Output[] = [],
382
- fixedInputs: Input[],
383
- ) {
384
- const _feePerByte =
385
- feePerByte ||
386
- (await this.getMethod('getFeePerByte')()) ||
387
- FEE_PER_BYTE_FALLBACK;
388
- const inputs: Input[] = [];
389
- const outputs: Output[] = [];
390
- try {
391
- const inputsForAmount = await this.getMethod('getInputsForAmount')(
392
- _outputs,
393
- _feePerByte,
394
- fixedInputs,
395
- 100,
396
- true,
397
- );
398
- if (inputsForAmount.change) {
399
- 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
+ });
400
409
  }
401
- inputs.push(...(inputsForAmount.inputs || []));
402
- outputs.push(...(inputsForAmount.outputs || []));
403
- } catch (e) {
404
- if (fixedInputs.length === 0) {
405
- throw Error(
406
- `Inputs for amount doesn't exist and no fixedInputs provided`,
407
- );
410
+ }
411
+
412
+ async getDerivationPathAddress(path: string) {
413
+ if (path in this._derivationCache) {
414
+ return this._derivationCache[path];
408
415
  }
409
416
 
410
- const inputsForAmount = await this._getInputForAmountWithoutUtxoCheck(
411
- _outputs,
412
- _feePerByte,
413
- fixedInputs,
414
- );
415
- inputs.push(
416
- ...(inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo)) || []),
417
- );
418
- outputs.push(...(inputsForAmount.outputs || []));
419
- }
420
- _outputs.forEach((output) => {
421
- const spliceIndex = outputs.findIndex(
422
- (sweepOutput: Output) => output.value === sweepOutput.value,
423
- );
424
- outputs.splice(spliceIndex, 1);
425
- });
426
- _outputs.push({
427
- to: externalChangeAddress,
428
- value: outputs[0].value,
429
- });
430
- return this._buildTransactionWithoutUtxoCheck(
431
- _outputs,
432
- _feePerByte,
433
- inputs,
434
- );
435
- }
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
+ });
436
426
 
437
- _getInputForAmountWithoutUtxoCheck(
438
- _outputs: Output[],
439
- _feePerByte: number,
440
- fixedInputs: Input[],
441
- ): {
442
- inputs: bT.UTXO[];
443
- outputs: { value: number; id?: string }[];
444
- fee: number;
445
- change: { value: number; id?: string };
446
- } {
447
- const utxoBalance = fixedInputs.reduce((a, b) => a + (b['value'] || 0), 0);
448
- const outputBalance = _outputs.reduce((a, b) => a + (b['value'] || 0), 0);
449
- const amountToSend =
450
- utxoBalance -
451
- _feePerByte * ((_outputs.length + 1) * 39 + fixedInputs.length * 153); // todo better calculation
452
-
453
- const targets = _outputs.map((target, i) => ({
454
- id: 'main',
455
- value: target.value,
456
- }));
457
- if (amountToSend - outputBalance > 0) {
458
- targets.push({ id: 'main', value: amountToSend - outputBalance });
427
+ this._derivationCache[path] = addressObject;
428
+ return addressObject;
459
429
  }
460
430
 
461
- return selectCoins(
462
- fixedInputs,
463
- targets,
464
- Math.ceil(_feePerByte),
465
- fixedInputs,
466
- );
467
- }
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';
468
452
 
469
- async _buildTransactionWithoutUtxoCheck(
470
- outputs: Output[],
471
- feePerByte: number,
472
- fixedInputs: Input[],
473
- ) {
474
- const network = this._network;
475
-
476
- const { fee } = this._getInputForAmountWithoutUtxoCheck(
477
- outputs,
478
- feePerByte,
479
- fixedInputs,
480
- );
481
- const inputs = fixedInputs;
482
-
483
- const txb = new bitcoin.TransactionBuilder(network);
484
-
485
- for (const output of outputs) {
486
- const to = output.to; // Allow for OP_RETURN
487
- txb.addOutput(to, output.value);
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
+ }
463
+
464
+ return addresses;
488
465
  }
489
466
 
490
- 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
+ }
491
493
 
492
- for (let i = 0; i < inputs.length; i++) {
493
- const wallet = await this.getMethod('getWalletAddress')(
494
- inputs[i].address,
495
- );
496
- const keyPair = await this.getMethod('keyPair')(wallet.derivationPath);
497
- const paymentVariant = this.getMethod('getPaymentVariantFromPublicKey')(
498
- keyPair.publicKey,
499
- );
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
+ }
500
515
 
501
- 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);
502
521
  }
503
522
 
504
- for (let i = 0; i < inputs.length; i++) {
505
- const wallet = await this.getMethod('getWalletAddress')(
506
- 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 },
507
531
  );
508
- const keyPair = await this.getMethod('keyPair')(wallet.derivationPath);
509
- const paymentVariant = this.getMethod('getPaymentVariantFromPublicKey')(
510
- keyPair.publicKey,
532
+ const memoizedGetAddressTransactionCounts = memoize(
533
+ this.getMethod('getAddressTransactionCounts'),
534
+ {
535
+ primitive: true,
536
+ },
511
537
  );
512
- const needsWitness = true;
513
-
514
- const signParams = {
515
- prevOutScriptType,
516
- vin: i,
517
- keyPair,
518
- 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);
519
545
  };
520
546
 
521
- if (needsWitness) {
522
- signParams.witnessValue = inputs[i].value;
523
- }
547
+ const result = await func.bind(this)();
548
+
549
+ this.getMethod = originalGetMethod;
524
550
 
525
- txb.sign(signParams);
551
+ return result;
526
552
  }
527
553
 
528
- return { hex: txb.build().toHex(), fee };
529
- }
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;
571
+ }
572
+ }
530
573
 
531
- /**
532
- * quickGetAddresses is an optimized version of getAddresses.
533
- * It removes the call to `asyncSetImmediate()`, speeding up the function by a factor of 6x.
534
- *
535
- * @param startingIndex
536
- * @param numAddresses
537
- * @param change
538
- * @returns {Promise<Address[]>}
539
- */
540
- async quickGetAddresses(
541
- startingIndex = 0,
542
- numAddresses = 1,
543
- change = false,
544
- ): Promise<Address[]> {
545
- if (numAddresses < 1) {
546
- throw new Error('You must return at least one address');
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;
547
584
  }
548
585
 
549
- const addresses = [];
550
- const lastIndex = startingIndex + numAddresses;
551
- const changeVal = change ? '1' : '0';
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
+ };
552
600
 
553
- // Original wallet provider is fetched to get the base derivation path
554
- const originalProvider = this.client.getProviderForMethod('getAddresses');
555
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
556
- const baseDerivationPath = (originalProvider as any)
557
- ._baseDerivationPath as string;
601
+ const feePerBytePromise = this.getMethod('getFeePerByte')();
602
+ let utxos: bT.UTXO[] = [];
558
603
 
559
- const getDerivationPathAddressFn: (
560
- path: string,
561
- ) => Promise<Address> = this.client.getMethod('getDerivationPathAddress');
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
+ }
562
621
 
563
- for (
564
- let currentIndex = startingIndex;
565
- currentIndex < lastIndex;
566
- currentIndex++
567
- ) {
568
- const subPath = changeVal + '/' + currentIndex;
569
- const path = baseDerivationPath + '/' + subPath;
570
- const addressObject = await getDerivationPathAddressFn(path);
571
- addresses.push(addressObject);
572
- }
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
+ }
573
631
 
574
- return addresses;
575
- }
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
+ }
576
653
 
577
- /**
578
- * quickFindAddress is an optimized version of findAddress.
579
- *
580
- * It searches through both change and non-change addresses each iteration.
581
- *
582
- * This is in contrast to the original findAddress function which searches
583
- * through all non-change addresses before moving on to change addresses.
584
- *
585
- * @param addresses
586
- * @returns {Promise<Address[]>}
587
- */
588
- async quickFindAddress(addresses: string[]): Promise<Address> {
589
- const addressesPerCall = 20;
590
- let index = 0;
591
-
592
- while (index < this._maxAddressesToDerive) {
593
- const walletNonChangeAddresses = await this.quickGetAddresses(
594
- index,
595
- addressesPerCall,
596
- true,
597
- );
598
- const walletChangeAddresses = await this.quickGetAddresses(
599
- index,
600
- addressesPerCall,
601
- false,
602
- );
603
- const walletAddresses = [
604
- ...walletNonChangeAddresses,
605
- ...walletChangeAddresses,
606
- ];
607
- const walletAddress = walletAddresses.find((walletAddr) =>
608
- addresses.find((addr) => walletAddr.address === addr),
609
- );
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
+ }
610
722
 
611
- if (walletAddress) {
612
- // Increment max addresses to derive by 100 if found within 100 addresses of maxAddressesToDerive
613
- this._maxAddressesToDerive = Math.max(
614
- this._maxAddressesToDerive,
615
- index + 100,
723
+ const { inputs, outputs, change, fee } = selectCoins(
724
+ utxos,
725
+ targets,
726
+ Math.ceil(feePerByte),
727
+ fixedUtxos,
616
728
  );
617
- return walletAddress;
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;
618
754
  }
619
- index += addressesPerCall;
755
+
756
+ throw new InsufficientBalanceError('Not enough balance');
620
757
  }
621
758
  }
622
- }
759
+ return BitcoinWalletProvider;
760
+ };