@bitpay-labs/bitcore-wallet-service 11.11.1 → 11.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/Dockerfile +1 -1
  2. package/package.json +5 -5
  3. package/src/lib/chain/btc/index.ts +3 -2
  4. package/src/lib/chain/eth/index.ts +134 -117
  5. package/src/lib/chain/sol/index.ts +1 -1
  6. package/src/lib/chain/xrp/index.ts +1 -1
  7. package/src/lib/routes/wallets.ts +3 -1
  8. package/src/lib/server.ts +136 -88
  9. package/src/lib/storage.ts +90 -20
  10. package/test/chain/eth.test.ts +70 -0
  11. package/test/chain/sol.test.ts +1 -1
  12. package/test/chain/xrp.test.ts +1 -1
  13. package/test/expressapp.test.ts +144 -0
  14. package/test/integration/addressReads.test.ts +503 -0
  15. package/test/integration/bcmonitor.test.ts +44 -0
  16. package/test/integration/server.test.ts +198 -48
  17. package/test/integration/walletReads.test.ts +488 -0
  18. package/ts_build/package.json +3 -3
  19. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  20. package/ts_build/src/lib/chain/btc/index.js +3 -2
  21. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  22. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  23. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  24. package/ts_build/src/lib/chain/eth/index.js +114 -102
  25. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  26. package/ts_build/src/lib/chain/sol/index.js +1 -1
  27. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  28. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  29. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  30. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  31. package/ts_build/src/lib/routes/wallets.js +2 -1
  32. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  33. package/ts_build/src/lib/server.d.ts +1 -1
  34. package/ts_build/src/lib/server.d.ts.map +1 -1
  35. package/ts_build/src/lib/server.js +128 -94
  36. package/ts_build/src/lib/server.js.map +1 -1
  37. package/ts_build/src/lib/storage.d.ts +17 -10
  38. package/ts_build/src/lib/storage.d.ts.map +1 -1
  39. package/ts_build/src/lib/storage.js +73 -20
  40. package/ts_build/src/lib/storage.js.map +1 -1
  41. package/ts_build/test/chain/eth.test.js +59 -0
  42. package/ts_build/test/chain/eth.test.js.map +1 -1
  43. package/ts_build/test/chain/sol.test.js +1 -1
  44. package/ts_build/test/chain/sol.test.js.map +1 -1
  45. package/ts_build/test/chain/xrp.test.js +1 -1
  46. package/ts_build/test/chain/xrp.test.js.map +1 -1
  47. package/ts_build/test/expressapp.test.js +132 -0
  48. package/ts_build/test/expressapp.test.js.map +1 -1
  49. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  50. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  51. package/ts_build/test/integration/addressReads.test.js +465 -0
  52. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  53. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  54. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  55. package/ts_build/test/integration/server.test.js +181 -48
  56. package/ts_build/test/integration/server.test.js.map +1 -1
  57. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  58. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  59. package/ts_build/test/integration/walletReads.test.js +436 -0
  60. package/ts_build/test/integration/walletReads.test.js.map +1 -0
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:22
1
+ FROM node:24
2
2
  RUN set -x && node -v && npm -v
3
3
  WORKDIR /bitcore
4
4
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@bitpay-labs/bitcore-wallet-service",
3
3
  "description": "A service for Mutisig HD Bitcoin Wallets",
4
4
  "author": "BitPay Inc",
5
- "version": "11.11.1",
5
+ "version": "11.11.2",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -41,9 +41,9 @@
41
41
  "cp:templates": "cp -r ./templates ./ts_build/templates"
42
42
  },
43
43
  "dependencies": {
44
- "@bitpay-labs/bitcore-logging": "^11.10.9",
45
- "@bitpay-labs/bitcore-tss": "^11.11.1",
46
- "@bitpay-labs/crypto-wallet-core": "^11.11.1",
44
+ "@bitpay-labs/bitcore-logging": "^11.11.2",
45
+ "@bitpay-labs/bitcore-tss": "^11.11.2",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.11.2",
47
47
  "@sendgrid/mail": "6.5.4",
48
48
  "abi-decoder": "2.4.0",
49
49
  "async": "0.9.2",
@@ -107,5 +107,5 @@
107
107
  "email": "kjoseph@bitpay.com"
108
108
  }
109
109
  ],
110
- "gitHead": "2d26cb6cfc59aab5415c9ce32863a9b9d832e5bb"
110
+ "gitHead": "1a044f2c984f6856f569981eebd91b051ab130d8"
111
111
  }
@@ -41,7 +41,8 @@ export class BtcChain implements IChain {
41
41
  server.getUtxosForCurrentWallet(
42
42
  {
43
43
  coin: opts.coin,
44
- addresses: opts.addresses
44
+ addresses: opts.addresses,
45
+ wallet
45
46
  },
46
47
  (err, utxos) => {
47
48
  if (err) return cb(err);
@@ -173,7 +174,7 @@ export class BtcChain implements IChain {
173
174
  return new Promise((resolve, reject) => {
174
175
  const getChangeAddress = (wallet, cb) => {
175
176
  if (wallet.singleAddress) {
176
- server.storage.fetchAddresses(server.walletId, (err, addresses) => {
177
+ server.storage.fetchAddressesPage(server.walletId, { limit: 1 }, (err, addresses) => {
177
178
  if (err) return cb(err);
178
179
  if (!addresses?.length) return cb(new ClientError('The wallet has no addresses'));
179
180
  return cb(null, addresses[0]);
@@ -1,4 +1,5 @@
1
- import { Transactions, Utils, Validation, Web3 } from '@bitpay-labs/crypto-wallet-core';
1
+ import util from 'util';
2
+ import { Constants as CWCConstants, Transactions, Utils, Validation, Web3 } from '@bitpay-labs/crypto-wallet-core';
2
3
  import _ from 'lodash';
3
4
  import { IWallet } from 'src/lib/model';
4
5
  import { IAddress } from 'src/lib/model/address';
@@ -23,6 +24,9 @@ function requireUncached(module) {
23
24
  return require(module);
24
25
  }
25
26
 
27
+ // the effect type bitcore-node gives ERC20 transfers in tx events
28
+ const ERC20_TRANSFER_EFFECT = 'ERC20:transfer';
29
+
26
30
  const Erc20Decoder = requireUncached('abi-decoder');
27
31
  Erc20Decoder.addABI(ERC20Abi);
28
32
 
@@ -118,7 +122,7 @@ export class EthChain implements IChain {
118
122
  // TODO support big int
119
123
  const lockedSum = Number(amounts + fees) || 0; // previously set to 0 if opts.multisigContractAddress
120
124
  const convertedBalance = this.convertBitcoreBalance(balance, lockedSum);
121
- server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => {
125
+ server.storage.fetchAddressesPage(server.walletId, { limit: 1 }, (err, addresses: IAddress[]) => {
122
126
  if (err) return cb(err);
123
127
  if (addresses.length > 0) {
124
128
  const byAddress = [
@@ -170,121 +174,134 @@ export class EthChain implements IChain {
170
174
 
171
175
  checkScriptOutput(_output) { }
172
176
 
173
- getFee(server, wallet, opts) {
174
- return new Promise(resolve => {
175
- server._getFeePerKb(wallet, opts, async (err, inFeePerKb) => {
176
- let feePerKb = inFeePerKb;
177
- let gasPrice = inFeePerKb;
178
- let maxGasFee;
179
- let priorityGasFee;
180
- const { from, txType, priorityFeePercentile, gasLimitBuffer } = opts;
181
- const { coin, network, chain } = wallet;
182
- let inGasLimit = 0; // Per recepient gas limit
183
- let gasLimit = 0; // Gas limit for all recepients. used for contract interactions that rollup recepients
184
- let fee = 0;
185
- const defaultGasLimit = this.getDefaultGasLimit(opts);
186
- const outputAddresses = []; // Parameter for MuliSend contract
187
- const outputAmounts: bigint[] = []; // Parameter for MuliSend contract
188
- let totalValue = 0n; // Parameter for MuliSend contract
189
- logger.info(`getFee for address ${from} on network ${network} and chain ${chain}`);
190
- logger.info('getFee.opts: %o', { from, txType, priorityFeePercentile, gasLimitBuffer });
191
- logger.info(`[${from}] Add gas limit buffer?: ${!!gasLimitBuffer}`);
192
- for (const output of opts.outputs) {
193
- // Multisend txs build contract fn parameters (addresses, amounts) and bypass output level gas estimations
194
- if (opts.multiSendContractAddress) {
195
- outputAddresses.push(output.toAddress);
196
- outputAmounts.push(BigInt(output.amount));
197
- if (!opts.tokenAddress) {
198
- totalValue += BigInt(output.amount);
199
- }
200
- // Used as a fallback value if estimateGas fails for multisend
201
- inGasLimit += output.gasLimit ? output.gasLimit : defaultGasLimit;
202
- continue;
203
- }
204
- // Estimate a standard transfer
205
- if (!output.gasLimit) {
206
- try {
207
- const to = opts.payProUrl
208
- ? output.toAddress
209
- : opts.tokenAddress
210
- ? opts.tokenAddress
211
- : opts.multisigContractAddress
212
- ? opts.multisigContractAddress
213
- : output.toAddress;
214
- const value = opts.tokenAddress || opts.multisigContractAddress ? 0 : output.amount;
215
- // output.gasLimit used as the gasLimit in getBitcoreTx for non multisend transactions
216
- const gasLimitEstimate = await server.estimateGas({
217
- coin,
218
- chain: this.chain,
219
- network,
220
- from,
221
- to,
222
- value,
223
- data: output.data,
224
- gasPrice
225
- });
226
- output.gasLimit = gasLimitEstimate || defaultGasLimit;
227
- } catch {
228
- output.gasLimit = defaultGasLimit;
229
- }
230
- }
231
- inGasLimit += output.gasLimit;
232
- logger.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output level gas limit: ${output.gasLimit}`);
233
- // Add gas Limit buffer to output level gasLimit
234
- if (gasLimitBuffer) {
235
- const gasBuffer = Math.ceil(output.gasLimit * (gasLimitBuffer / 100));
236
- output.gasLimit += gasBuffer;
237
- inGasLimit += gasBuffer;
238
- logger.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output gas limit with buffer: ${output.gasLimit}`);
239
- }
240
- if (_.isNumber(opts.fee)) {
241
- // This is used for sendmax
242
- gasPrice = feePerKb = Number((opts.fee / (inGasLimit || defaultGasLimit)).toFixed());
243
- }
244
- fee += feePerKb * output.gasLimit;
245
- }
246
- // gasLimit == sum of internal gasLimits (for non Multisend)
247
- gasLimit = inGasLimit;
248
- logger.info(`[${from}] Current top level gas limit: ${gasLimit}`);
249
- if (opts.multiSendContractAddress) {
250
- // Calculate gas limit for top level of txp based on multisend tx
251
- let _gasLimit;
252
- try {
253
- const data = this.encodeContractParameters(
254
- Constants.BITPAY_CONTRACTS.MULTISEND,
255
- { addresses: outputAddresses, amounts: outputAmounts },
256
- opts
257
- );
258
-
259
- _gasLimit = await server.estimateGas({
260
- coin,
261
- chain: this.chain,
262
- network,
263
- from,
264
- to: opts.multiSendContractAddress,
265
- value: totalValue.toString(),
266
- data,
267
- gasPrice
268
- });
269
- logger.info(`[${from}] Estimated multisend gas limit: ${_gasLimit}`);
270
- } catch (error) {
271
- logger.error('Error estimating gas for MultiSend contract: %o', error);
272
- }
273
- // Add gas limit buffer to top level gas limit
274
- const buffer = gasLimitBuffer ? gasLimitBuffer / 100 : Defaults.MS_GAS_LIMIT_BUFFER_PERCENT;
275
- // If gas estimation fails, fallback to sum of internal gasLimits
276
- gasLimit = _gasLimit || gasLimit;
277
- gasLimit += Math.ceil(gasLimit * buffer); // add gas limit buffer
278
- fee += feePerKb * gasLimit;
279
- logger.info(`[${from}] Top level gas limit with buffer: ${gasLimit}`);
177
+ async getFee(server, wallet, opts) {
178
+ const hasType2FeeOverride = opts.maxGasFee != null || opts.priorityGasFee != null;
179
+ let { maxGasFee, priorityGasFee } = opts;
180
+ if (hasType2FeeOverride) {
181
+ const chain = wallet.chain || wallet.coin;
182
+ // Keep overrides consistent with CWC's chain-specific minimum priority fee.
183
+ const minimum = CWCConstants.FEE_MINIMUMS[chain.toUpperCase()]?.priority ?? 0;
184
+ if (priorityGasFee < minimum) {
185
+ throw new ClientError(`priorityGasFee must be at least ${minimum} wei for ${chain}`);
186
+ }
187
+ }
188
+ // Use the explicit rate for both construction and the fee reserved from the wallet balance.
189
+ const feeOpts = opts.gasPrice != null || hasType2FeeOverride
190
+ ? { ...opts, feePerKb: opts.gasPrice ?? maxGasFee }
191
+ : opts;
192
+ // Fixed fees are converted to a gas price below, once the gas limits are known.
193
+ const fixedFee = Common.Utils.isNumber(opts.fee) ? Number(opts.fee) : undefined;
194
+ const inFeePerKb = fixedFee != null
195
+ ? undefined
196
+ : await util.promisify(server._getFeePerKb).call(server, wallet, feeOpts);
197
+ let feePerKb = inFeePerKb;
198
+ let gasPrice = inFeePerKb;
199
+ const { from, txType, priorityFeePercentile, gasLimitBuffer } = opts;
200
+ const { coin, network, chain } = wallet;
201
+ let inGasLimit = 0; // Per recepient gas limit
202
+ let gasLimit = 0; // Gas limit for all recepients. used for contract interactions that rollup recepients
203
+ let fee = 0;
204
+ const defaultGasLimit = this.getDefaultGasLimit(opts);
205
+ const outputAddresses = []; // Parameter for MuliSend contract
206
+ const outputAmounts: bigint[] = []; // Parameter for MuliSend contract
207
+ let totalValue = 0n; // Parameter for MuliSend contract
208
+ logger.info(`getFee for address ${from} on network ${network} and chain ${chain}`);
209
+ logger.info('getFee.opts: %o', { from, txType, priorityFeePercentile, gasLimitBuffer });
210
+ logger.info(`[${from}] Add gas limit buffer?: ${!!gasLimitBuffer}`);
211
+ for (const output of opts.outputs) {
212
+ // Multisend txs build contract fn parameters (addresses, amounts) and bypass output level gas estimations
213
+ if (opts.multiSendContractAddress) {
214
+ outputAddresses.push(output.toAddress);
215
+ outputAmounts.push(BigInt(output.amount));
216
+ if (!opts.tokenAddress) {
217
+ totalValue += BigInt(output.amount);
280
218
  }
281
- if (Number(txType) === 2) {
282
- maxGasFee = await server.estimateFee({ network, chain: wallet.chain || coin, txType: 2 });
283
- priorityGasFee = await server.estimatePriorityFee({ network, chain: wallet.chain || coin, percentile: priorityFeePercentile || 15 });
219
+ // Used as a fallback value if estimateGas fails for multisend
220
+ inGasLimit += output.gasLimit ? output.gasLimit : defaultGasLimit;
221
+ continue;
222
+ }
223
+ // Estimate a standard transfer
224
+ if (!output.gasLimit) {
225
+ try {
226
+ const to = opts.payProUrl
227
+ ? output.toAddress
228
+ : opts.tokenAddress
229
+ ? opts.tokenAddress
230
+ : opts.multisigContractAddress
231
+ ? opts.multisigContractAddress
232
+ : output.toAddress;
233
+ const value = opts.tokenAddress || opts.multisigContractAddress ? 0 : output.amount;
234
+ // output.gasLimit used as the gasLimit in getBitcoreTx for non multisend transactions
235
+ const gasLimitEstimate = await server.estimateGas({
236
+ coin,
237
+ chain: this.chain,
238
+ network,
239
+ from,
240
+ to,
241
+ value,
242
+ data: output.data,
243
+ gasPrice
244
+ });
245
+ output.gasLimit = gasLimitEstimate || defaultGasLimit;
246
+ } catch {
247
+ output.gasLimit = defaultGasLimit;
284
248
  }
285
- return resolve({ feePerKb, gasPrice, gasLimit, maxGasFee, priorityGasFee, fee });
286
- });
287
- });
249
+ }
250
+ inGasLimit += output.gasLimit;
251
+ logger.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output level gas limit: ${output.gasLimit}`);
252
+ // Add gas Limit buffer to output level gasLimit
253
+ if (gasLimitBuffer) {
254
+ const gasBuffer = Math.ceil(output.gasLimit * (gasLimitBuffer / 100));
255
+ output.gasLimit += gasBuffer;
256
+ inGasLimit += gasBuffer;
257
+ logger.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output gas limit with buffer: ${output.gasLimit}`);
258
+ }
259
+ if (fixedFee != null) {
260
+ // This is used for sendmax
261
+ gasPrice = feePerKb = Number((fixedFee / (inGasLimit || defaultGasLimit)).toFixed());
262
+ }
263
+ fee += feePerKb * output.gasLimit;
264
+ }
265
+ // gasLimit == sum of internal gasLimits (for non Multisend)
266
+ gasLimit = inGasLimit;
267
+ logger.info(`[${from}] Current top level gas limit: ${gasLimit}`);
268
+ if (opts.multiSendContractAddress) {
269
+ // Calculate gas limit for top level of txp based on multisend tx
270
+ let _gasLimit;
271
+ try {
272
+ const data = this.encodeContractParameters(
273
+ Constants.BITPAY_CONTRACTS.MULTISEND,
274
+ { addresses: outputAddresses, amounts: outputAmounts },
275
+ opts
276
+ );
277
+
278
+ _gasLimit = await server.estimateGas({
279
+ coin,
280
+ chain: this.chain,
281
+ network,
282
+ from,
283
+ to: opts.multiSendContractAddress,
284
+ value: totalValue.toString(),
285
+ data,
286
+ gasPrice
287
+ });
288
+ logger.info(`[${from}] Estimated multisend gas limit: ${_gasLimit}`);
289
+ } catch (error) {
290
+ logger.error('Error estimating gas for MultiSend contract: %o', error);
291
+ }
292
+ // Add gas limit buffer to top level gas limit
293
+ const buffer = gasLimitBuffer ? gasLimitBuffer / 100 : Defaults.MS_GAS_LIMIT_BUFFER_PERCENT;
294
+ // If gas estimation fails, fallback to sum of internal gasLimits
295
+ gasLimit = _gasLimit || gasLimit;
296
+ gasLimit += Math.ceil(gasLimit * buffer); // add gas limit buffer
297
+ fee += feePerKb * gasLimit;
298
+ logger.info(`[${from}] Top level gas limit with buffer: ${gasLimit}`);
299
+ }
300
+ if (Number(txType) === 2 && !hasType2FeeOverride) {
301
+ maxGasFee = await server.estimateFee({ network, chain: wallet.chain || coin, txType: 2 });
302
+ priorityGasFee = await server.estimatePriorityFee({ network, chain: wallet.chain || coin, percentile: priorityFeePercentile || 15 });
303
+ }
304
+ return { feePerKb, gasPrice, gasLimit, maxGasFee, priorityGasFee, fee };
288
305
  }
289
306
 
290
307
  isPrePublishRawBound(txp: TxProposal) {
@@ -613,7 +630,7 @@ export class EthChain implements IChain {
613
630
  if (tx.effects && tx.effects.length) {
614
631
  const multisigConfirm = tx.effects.findIndex(e => e.type === 'MULTISIG:confirmTransaction');
615
632
  const multisigSubmit = tx.effects.findIndex(e => e.type === 'MULTISIG:submitTransaction');
616
- const erc20Transfer = tx.effects.findIndex(e => e.type === 'ERC20:transer');
633
+ const erc20Transfer = tx.effects.findIndex(e => e.type === ERC20_TRANSFER_EFFECT);
617
634
  const internalTransfer = tx.effects.findIndex(e => !e.type && !e.contractAddress);
618
635
 
619
636
  const exists = [multisigConfirm, multisigSubmit, erc20Transfer, internalTransfer].filter(i => i > -1);
@@ -625,7 +642,7 @@ export class EthChain implements IChain {
625
642
  multisigContractAddress = best.contractAddress;
626
643
  address = best.to;
627
644
  amount = best.amount;
628
- } else if (best.type == 'ERC20:transfer') {
645
+ } else if (best.type == ERC20_TRANSFER_EFFECT) {
629
646
  tokenAddress = best.contractAddress;
630
647
  address = best.to;
631
648
  amount = best.amount;
@@ -73,7 +73,7 @@ export class SolChain implements IChain {
73
73
  const lockedSum = Number(amounts + fees) || 0; // previously set to 0 if opts.multisigContractAddress
74
74
  const reserveAmount = opts.tokenAddress ? 0 : reserve;
75
75
  const convertedBalance = this.convertBitcoreBalance(balance, lockedSum, reserveAmount);
76
- server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => {
76
+ server.storage.fetchAddressesPage(server.walletId, { limit: 1 }, (err, addresses: IAddress[]) => {
77
77
  if (err) return cb(err);
78
78
  if (addresses.length > 0) {
79
79
  const byAddress = [
@@ -69,7 +69,7 @@ export class XrpChain implements IChain {
69
69
  return sum + BigInt(txp.amount || 0) + BigInt(txp.fee || 0);
70
70
  }, 0n) || 0n;
71
71
  const convertedBalance = this.convertBitcoreBalance(balance, Number(lockedSum), reserve);
72
- server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => {
72
+ server.storage.fetchAddressesPage(server.walletId, { limit: 1 }, (err, addresses: IAddress[]) => {
73
73
  if (err) return cb(err);
74
74
  if (addresses.length > 0) {
75
75
  const byAddress = [
@@ -228,8 +228,10 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
228
228
  return res.json([]);
229
229
  }
230
230
 
231
+ // a copayer that isn't an object with a string id can't exist, so it isn't looked up
232
+ const wellFormed = copayers.filter(c => typeof c?.copayerId === 'string');
231
233
  const storage = WalletService.getStorage();
232
- const existing = await Promise.all<{ copayerId: string; verified: boolean }>(copayers.map(async c => {
234
+ const existing = await Promise.all<{ copayerId: string; verified: boolean }>(wellFormed.map(async c => {
233
235
  const copayer = await storage.fetchCopayerLookup(c.copayerId).catch(() => null);
234
236
  if (!copayer) {
235
237
  return { copayerId: c.copayerId, verified: false };