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

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 (105) 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/common/constants.ts +1 -1
  8. package/src/lib/common/utils.ts +18 -1
  9. package/src/lib/model/tsskeygen.ts +16 -0
  10. package/src/lib/routes/maintenance.ts +11 -2
  11. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  12. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  13. package/src/lib/routes/services.ts +7 -1
  14. package/src/lib/routes/tss.ts +33 -30
  15. package/src/lib/routes/wallets.ts +3 -1
  16. package/src/lib/server.ts +198 -105
  17. package/src/lib/storage.ts +129 -20
  18. package/src/lib/tss.ts +111 -7
  19. package/test/chain/eth.test.ts +70 -0
  20. package/test/chain/sol.test.ts +1 -1
  21. package/test/chain/xrp.test.ts +1 -1
  22. package/test/expressapp.test.ts +243 -0
  23. package/test/integration/addressReads.test.ts +503 -0
  24. package/test/integration/bcmonitor.test.ts +44 -0
  25. package/test/integration/server.test.ts +251 -49
  26. package/test/integration/tss.test.ts +428 -0
  27. package/test/integration/walletReads.test.ts +488 -0
  28. package/ts_build/package.json +4 -4
  29. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  30. package/ts_build/src/lib/chain/btc/index.js +3 -2
  31. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  32. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  33. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  34. package/ts_build/src/lib/chain/eth/index.js +114 -102
  35. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  36. package/ts_build/src/lib/chain/sol/index.js +1 -1
  37. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  38. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  39. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  40. package/ts_build/src/lib/common/constants.d.ts +1 -1
  41. package/ts_build/src/lib/common/constants.js +1 -1
  42. package/ts_build/src/lib/common/index.d.ts +2 -1
  43. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  44. package/ts_build/src/lib/common/utils.d.ts +1 -0
  45. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  46. package/ts_build/src/lib/common/utils.js +16 -1
  47. package/ts_build/src/lib/common/utils.js.map +1 -1
  48. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  49. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  51. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  52. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  53. package/ts_build/src/lib/routes/maintenance.js +12 -4
  54. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  55. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  56. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  57. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  58. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  59. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  60. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  61. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  62. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  63. package/ts_build/src/lib/routes/services.js +6 -1
  64. package/ts_build/src/lib/routes/services.js.map +1 -1
  65. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  66. package/ts_build/src/lib/routes/tss.js +32 -30
  67. package/ts_build/src/lib/routes/tss.js.map +1 -1
  68. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  69. package/ts_build/src/lib/routes/wallets.js +2 -1
  70. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  71. package/ts_build/src/lib/server.d.ts +24 -3
  72. package/ts_build/src/lib/server.d.ts.map +1 -1
  73. package/ts_build/src/lib/server.js +148 -97
  74. package/ts_build/src/lib/server.js.map +1 -1
  75. package/ts_build/src/lib/storage.d.ts +26 -10
  76. package/ts_build/src/lib/storage.d.ts.map +1 -1
  77. package/ts_build/src/lib/storage.js +99 -20
  78. package/ts_build/src/lib/storage.js.map +1 -1
  79. package/ts_build/src/lib/tss.d.ts +12 -0
  80. package/ts_build/src/lib/tss.d.ts.map +1 -1
  81. package/ts_build/src/lib/tss.js +64 -5
  82. package/ts_build/src/lib/tss.js.map +1 -1
  83. package/ts_build/test/chain/eth.test.js +59 -0
  84. package/ts_build/test/chain/eth.test.js.map +1 -1
  85. package/ts_build/test/chain/sol.test.js +1 -1
  86. package/ts_build/test/chain/sol.test.js.map +1 -1
  87. package/ts_build/test/chain/xrp.test.js +1 -1
  88. package/ts_build/test/chain/xrp.test.js.map +1 -1
  89. package/ts_build/test/expressapp.test.js +222 -0
  90. package/ts_build/test/expressapp.test.js.map +1 -1
  91. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  92. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  93. package/ts_build/test/integration/addressReads.test.js +465 -0
  94. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  95. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  96. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  97. package/ts_build/test/integration/server.test.js +228 -48
  98. package/ts_build/test/integration/server.test.js.map +1 -1
  99. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  100. package/ts_build/test/integration/tss.test.js +344 -0
  101. package/ts_build/test/integration/tss.test.js.map +1 -1
  102. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  103. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  104. package/ts_build/test/integration/walletReads.test.js +436 -0
  105. 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.3",
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": "154120b032bb0cd42bad2559c0ef81873a8c7e8c"
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 = [
@@ -226,7 +226,7 @@ export const Constants = {
226
226
 
227
227
  // May be different from the defaults if we rollback
228
228
  // or want to beta test new versions.
229
- TSS_KEYGEN_SCHEME_VERSION_MAX: 1.2,
229
+ TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3,
230
230
  TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1,
231
231
 
232
232
  TSS_KEYGEN_SCHEME_VERSION_MIN: 1,
@@ -571,5 +571,22 @@ export const Utils = {
571
571
  return retval;
572
572
  },
573
573
 
574
- sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
574
+ sleep(ms) {
575
+ return new Promise(resolve => setTimeout(resolve, ms));
576
+ },
577
+
578
+ convertXPubKeyForNetwork(xPubKey: string, chain: string, network: string) {
579
+ // Convert named network for non-UTXO chains to generic name (e.g. sepolia -> testnet)
580
+ network = !Bitcore_[chain] ? Utils.getGenericName(network) : network;
581
+ const lib = Bitcore_[chain] || BitcoreLib;
582
+ $.checkArgument(xPubKey, 'xPubKey is required at <convertXPubKeyForNetwork()>');
583
+ $.checkArgument(chain, 'chain is required at <convertXPubKeyForNetwork()>');
584
+ $.checkArgument(lib.Networks.get(network), 'invalid network at <convertXPubKeyForNetwork()>: ' + network);
585
+ const hdPubKeyObj = new lib.HDPublicKey(xPubKey).toObject();
586
+ hdPubKeyObj.network = network;
587
+ delete hdPubKeyObj.checksum;
588
+ delete hdPubKeyObj.xpubkey;
589
+ const newHdPubKey = lib.HDPublicKey.fromObject(hdPubKeyObj);
590
+ return newHdPubKey.toString();
591
+ }
575
592
  };
@@ -80,9 +80,18 @@ export interface ITssKeyGenModel {
80
80
  */
81
81
  timeLimit?: number;
82
82
  /**
83
+ * @deprecated Use bwsJoinSecrets instead.
83
84
  * The BWC-generated secret for joining a BWS wallet
84
85
  */
85
86
  bwsJoinSecret?: string;
87
+ /**
88
+ * The BWC-generated secrets for joining a BWS wallet
89
+ */
90
+ bwsJoinSecrets?: {
91
+ [chainNetwork: string]: {
92
+ [partyId: number]: string;
93
+ };
94
+ };
86
95
  /**
87
96
  * The mongo doc version
88
97
  */
@@ -105,6 +114,11 @@ export class TssKeyGenModel implements ITssKeyGenModel {
105
114
  createdOn: number;
106
115
  timeLimit?: number;
107
116
  bwsJoinSecret?: string;
117
+ bwsJoinSecrets?: {
118
+ [chainNetwork: string]: {
119
+ [partyId: number]: string;
120
+ };
121
+ };
108
122
  __v: number;
109
123
 
110
124
 
@@ -151,6 +165,7 @@ export class TssKeyGenModel implements ITssKeyGenModel {
151
165
  x.keyShares = new Array(n);
152
166
  x.createdOn = Date.now();
153
167
  x.timeLimit = Math.min(timeLimit || Defaults.TSS_KEYGEN_TIME_LIMIT, 60) * ONE_MINUTE; // capped at 60 minutes
168
+ x.bwsJoinSecrets = {};
154
169
  x.__v = 0;
155
170
  return x;
156
171
  }
@@ -169,6 +184,7 @@ export class TssKeyGenModel implements ITssKeyGenModel {
169
184
  x.createdOn = obj.createdOn;
170
185
  x.timeLimit = obj.timeLimit;
171
186
  x.bwsJoinSecret = obj.bwsJoinSecret;
187
+ x.bwsJoinSecrets = obj.bwsJoinSecrets;
172
188
  x.__v = obj.__v;
173
189
  return x;
174
190
  }
@@ -1,4 +1,5 @@
1
1
  import express from 'express';
2
+ import { ClientError } from '../errors/clienterror';
2
3
  import { WalletService } from '../server';
3
4
  import { Stats } from '../stats';
4
5
  import type * as Types from '../../types/expressapp';
@@ -27,6 +28,14 @@ export function registerMaintenanceRoutes(router: express.Router, context: Route
27
28
  });
28
29
 
29
30
  router.get('/v1/stats/', (req, res) => {
31
+ const { network, coin } = req.query;
32
+ if (network !== undefined && typeof network !== 'string') {
33
+ return returnError(new ClientError('Invalid network'), res, req);
34
+ }
35
+ if (coin !== undefined && typeof coin !== 'string') {
36
+ return returnError(new ClientError('Invalid coin'), res, req);
37
+ }
38
+
30
39
  setPublicCache(res, 1 * ONE_MINUTE);
31
40
  const opts: {
32
41
  network?: string;
@@ -35,8 +44,8 @@ export function registerMaintenanceRoutes(router: express.Router, context: Route
35
44
  to?: string;
36
45
  } = {};
37
46
 
38
- if (req.query.network) opts.network = req.query.network as string;
39
- if (req.query.coin) opts.coin = req.query.coin as string;
47
+ if (network) opts.network = network;
48
+ if (coin) opts.coin = coin;
40
49
  if (req.query.from) opts.from = req.query.from as string;
41
50
  if (req.query.to) opts.to = req.query.to as string;
42
51
 
@@ -11,9 +11,7 @@ import type { ITssSigMessageObject, TssSigGenModel } from '../../model/tsssign';
11
11
  function verifyMessage(req: express.Request, signature: string, pubKey: string) {
12
12
  const message = getMessage(req);
13
13
  const isValid = !!Utils.verifyMessage(message, signature, pubKey);
14
- if (!isValid) {
15
- throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
16
- }
14
+ return isValid;
17
15
  }
18
16
 
19
17
  /**
@@ -21,7 +19,8 @@ function verifyMessage(req: express.Request, signature: string, pubKey: string)
21
19
  * It checks if the request has valid credentials, verifies the signature,
22
20
  * and retrieves the TSS session based on the request path.
23
21
  */
24
- export function authTssKeyGenRequest(): express.RequestHandler {
22
+ export function authTssKeyGenRequest(params: { bypassExpired?: boolean } = {}): express.RequestHandler {
23
+ const { bypassExpired } = params;
25
24
  return async function(req, res, next) {
26
25
  try {
27
26
  const storage = WalletService.getStorage();
@@ -46,7 +45,10 @@ export function authTssKeyGenRequest(): express.RequestHandler {
46
45
  // This is a brand new session. Caller should be party 0 with the initial round message.
47
46
  if (body?.message?.round === 0 && body?.message?.partyId === 0) {
48
47
  const pubKey = body?.message?.publicKey;
49
- verifyMessage(req, signature, pubKey); // Self verification, but at least ensures that the caller owns the message's private key
48
+ if (!verifyMessage(req, signature, pubKey)) {
49
+ // Self verification, but at least ensures that the caller owns the message's private key
50
+ throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
51
+ }
50
52
  return next();
51
53
  } else {
52
54
  // Session should have been initialized by the client already in BWC > TssKeyGen.newKey()
@@ -54,7 +56,7 @@ export function authTssKeyGenRequest(): express.RequestHandler {
54
56
  }
55
57
  }
56
58
 
57
- if (session.isExpired()) {
59
+ if (!bypassExpired && session.isExpired()) {
58
60
  throw Errors.TSS_SESSION_EXPIRED;
59
61
  }
60
62
 
@@ -103,7 +105,9 @@ export function authTssKeyGenRequest(): express.RequestHandler {
103
105
  throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
104
106
  }
105
107
 
106
- verifyMessage(req, signature, pubKey);
108
+ if (!verifyMessage(req, signature, pubKey)) {
109
+ throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
110
+ }
107
111
 
108
112
  req.session = session;
109
113
  return next();
@@ -116,7 +120,8 @@ export function authTssKeyGenRequest(): express.RequestHandler {
116
120
  };
117
121
  };
118
122
 
119
- export function authTssSignRequest(): express.RequestHandler {
123
+ export function authTssSignRequest(params: { joinable?: boolean } = {}): express.RequestHandler {
124
+ const { joinable } = params;
120
125
  return async function(req, res, next) {
121
126
  try {
122
127
  const storage = WalletService.getStorage();
@@ -132,16 +137,22 @@ export function authTssSignRequest(): express.RequestHandler {
132
137
 
133
138
  const { id } = req.params as { [key: string]: string };
134
139
  const body = req.body as { message: ITssSigMessageObject | undefined };
135
- // Only some requests carry a body.message (e.g. POST /store only sends the final signature).
136
- // Omitting it is safe since routes that don't receive a message don't consume one.
137
- const verifyMsg = !!body?.message;
138
140
 
139
141
  const copayer = await storage.fetchCopayerLookup(copayerId);
140
142
  if (!copayer) {
141
143
  throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found');
142
144
  }
143
- const pubKey = copayer.requestPubKeys[0].key;
144
- if (verifyMsg && pubKey !== body?.message?.publicKey) {
145
+ // I'm not aware of an instance where copayer.requestPubKeys.length > 1, but we iterate over them to find the key for the given signature.
146
+ const pubKey = copayer.requestPubKeys.find(pubKey => verifyMessage(req, signature, pubKey.key))?.key;
147
+ if (!pubKey) {
148
+ throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
149
+ }
150
+
151
+ // Only some requests carry a body.message (e.g. POST /store only sends the final signature).
152
+ // Omitting it is safe since routes that don't receive a message don't change state.
153
+ // `verifyMsgFields` just indicates whether to check the message's fields for expected values (e.g. publicKey, partyId).
154
+ const verifyMsgFields = !!body?.message;
155
+ if (verifyMsgFields && pubKey !== body?.message?.publicKey) {
145
156
  throw Errors.NOT_AUTHORIZED.withMessage('Invalid public key');
146
157
  }
147
158
 
@@ -152,10 +163,12 @@ export function authTssSignRequest(): express.RequestHandler {
152
163
  throw Errors.TSS_SESSION_EXPIRED;
153
164
  }
154
165
  const party = session.participants.find(p => p.copayerId === copayerId);
155
- if (!party) {
166
+ // A copayer joins an existing session by sending its round 0 message (participant is added in TssSign.processMessage)
167
+ const isJoining = !party && body?.message?.round === 0;
168
+ if (!party && !(joinable && isJoining)) {
156
169
  throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
157
170
  }
158
- if (verifyMsg && party.partyId !== body?.message?.partyId) {
171
+ if (verifyMsgFields && party && party.partyId !== body?.message?.partyId) {
159
172
  throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
160
173
  }
161
174
 
@@ -164,7 +177,6 @@ export function authTssSignRequest(): express.RequestHandler {
164
177
  throw Errors.TSS_SESSION_NOT_FOUND;
165
178
  }
166
179
 
167
- verifyMessage(req, signature, pubKey);
168
180
  return next();
169
181
  } catch (err) {
170
182
  if (err instanceof ClientError) {
@@ -14,7 +14,10 @@ export async function verifyTssMessage(req, res, next) {
14
14
  throw Errors.TSS_PUBKEY_MISSING;
15
15
  }
16
16
 
17
- if (!message.broadcastMessages?.length && !message.p2pMessages?.length) {
17
+ // 1-of-N signing sessions won't have messages for other recipients.
18
+ // req.session is unset for a new session, and keygen sessions have no `m`.
19
+ const isSoloSigner = req.session?.m === 1;
20
+ if (!isSoloSigner && !message.broadcastMessages?.length && !message.p2pMessages?.length) {
18
21
  throw Errors.TSS_INVALID_MESSAGE;
19
22
  }
20
23
 
@@ -3,6 +3,7 @@ import express from 'express';
3
3
  import rp from 'request-promise-native';
4
4
  import config from '../../config';
5
5
  import { Common } from '../common';
6
+ import { ClientError } from '../errors/clienterror';
6
7
  import type * as Types from '../../types/expressapp';
7
8
  import type { WalletService } from '../server';
8
9
 
@@ -125,13 +126,18 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
125
126
  });
126
127
 
127
128
  router.get('/v1/fiatrates/:code/', (req, res) => {
129
+ const { coin } = req.query;
130
+ if (coin !== undefined && typeof coin !== 'string') {
131
+ return context.returnError(new ClientError('Invalid coin'), res, req);
132
+ }
133
+
128
134
  setPublicCache(res, 5 * ONE_MINUTE);
129
135
  respondWithPublicServer(req, res, context, server =>
130
136
  callbackResult(resolve => {
131
137
  server.getFiatRate(
132
138
  {
133
139
  code: req.params['code'],
134
- coin: req.query.coin || 'btc',
140
+ coin: coin || 'btc',
135
141
  ts: req.query.ts ? +req.query.ts : null
136
142
  },
137
143
  resolve