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

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 (152) hide show
  1. package/package.json +4 -4
  2. package/src/config.ts +8 -0
  3. package/src/externalservices/coingecko.ts +6 -1
  4. package/src/externalservices/transak.ts +394 -70
  5. package/src/lib/blockchainexplorer.ts +4 -0
  6. package/src/lib/blockchainexplorers/v8.ts +1 -0
  7. package/src/lib/blockchainmonitor.ts +1 -0
  8. package/src/lib/chain/arc/index.ts +95 -0
  9. package/src/lib/chain/eth/index.ts +41 -23
  10. package/src/lib/chain/index.ts +2 -0
  11. package/src/lib/common/constants.ts +13 -2
  12. package/src/lib/common/defaults.ts +31 -0
  13. package/src/lib/common/utils.ts +18 -1
  14. package/src/lib/emailservice.ts +3 -1
  15. package/src/lib/errors/errordefinitions.ts +4 -0
  16. package/src/lib/model/preferences.ts +4 -0
  17. package/src/lib/model/tsskeygen.ts +16 -0
  18. package/src/lib/model/wallet.ts +1 -0
  19. package/src/lib/pushnotificationsservice.ts +2 -0
  20. package/src/lib/routes/maintenance.ts +11 -2
  21. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  22. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  23. package/src/lib/routes/services.ts +8 -2
  24. package/src/lib/routes/tss.ts +33 -30
  25. package/src/lib/routes/walletdata.ts +2 -0
  26. package/src/lib/server.ts +82 -19
  27. package/src/lib/storage.ts +39 -0
  28. package/src/lib/tss.ts +111 -7
  29. package/test/blockchainexplorer.test.ts +7 -0
  30. package/test/expressapp.test.ts +130 -0
  31. package/test/integration/externalservices/coinGecko.test.ts +42 -2
  32. package/test/integration/externalservices/transak.test.ts +545 -135
  33. package/test/integration/fiatrateservice.test.ts +13 -0
  34. package/test/integration/helpers.ts +1 -0
  35. package/test/integration/server.test.ts +192 -4
  36. package/test/integration/tss.test.ts +428 -0
  37. package/ts_build/package.json +4 -4
  38. package/ts_build/src/config.d.ts.map +1 -1
  39. package/ts_build/src/config.js +8 -0
  40. package/ts_build/src/config.js.map +1 -1
  41. package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
  42. package/ts_build/src/externalservices/coingecko.js +6 -1
  43. package/ts_build/src/externalservices/coingecko.js.map +1 -1
  44. package/ts_build/src/externalservices/transak.d.ts +28 -2
  45. package/ts_build/src/externalservices/transak.d.ts.map +1 -1
  46. package/ts_build/src/externalservices/transak.js +274 -57
  47. package/ts_build/src/externalservices/transak.js.map +1 -1
  48. package/ts_build/src/lib/blockchainexplorer.d.ts.map +1 -1
  49. package/ts_build/src/lib/blockchainexplorer.js +4 -0
  50. package/ts_build/src/lib/blockchainexplorer.js.map +1 -1
  51. package/ts_build/src/lib/blockchainexplorers/v8.d.ts.map +1 -1
  52. package/ts_build/src/lib/blockchainexplorers/v8.js +1 -0
  53. package/ts_build/src/lib/blockchainexplorers/v8.js.map +1 -1
  54. package/ts_build/src/lib/blockchainmonitor.d.ts.map +1 -1
  55. package/ts_build/src/lib/blockchainmonitor.js +1 -0
  56. package/ts_build/src/lib/blockchainmonitor.js.map +1 -1
  57. package/ts_build/src/lib/chain/arc/index.d.ts +20 -0
  58. package/ts_build/src/lib/chain/arc/index.d.ts.map +1 -0
  59. package/ts_build/src/lib/chain/arc/index.js +95 -0
  60. package/ts_build/src/lib/chain/arc/index.js.map +1 -0
  61. package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
  62. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  63. package/ts_build/src/lib/chain/eth/index.js +38 -19
  64. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  65. package/ts_build/src/lib/chain/index.d.ts.map +1 -1
  66. package/ts_build/src/lib/chain/index.js +2 -0
  67. package/ts_build/src/lib/chain/index.js.map +1 -1
  68. package/ts_build/src/lib/common/constants.d.ts +13 -1
  69. package/ts_build/src/lib/common/constants.d.ts.map +1 -1
  70. package/ts_build/src/lib/common/constants.js +13 -2
  71. package/ts_build/src/lib/common/constants.js.map +1 -1
  72. package/ts_build/src/lib/common/defaults.d.ts +24 -0
  73. package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
  74. package/ts_build/src/lib/common/defaults.js +30 -0
  75. package/ts_build/src/lib/common/defaults.js.map +1 -1
  76. package/ts_build/src/lib/common/index.d.ts +38 -1
  77. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  78. package/ts_build/src/lib/common/utils.d.ts +1 -0
  79. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  80. package/ts_build/src/lib/common/utils.js +16 -1
  81. package/ts_build/src/lib/common/utils.js.map +1 -1
  82. package/ts_build/src/lib/emailservice.d.ts.map +1 -1
  83. package/ts_build/src/lib/emailservice.js +2 -0
  84. package/ts_build/src/lib/emailservice.js.map +1 -1
  85. package/ts_build/src/lib/errors/errordefinitions.d.ts +2 -0
  86. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  87. package/ts_build/src/lib/errors/errordefinitions.js +2 -0
  88. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  89. package/ts_build/src/lib/model/preferences.d.ts +2 -0
  90. package/ts_build/src/lib/model/preferences.d.ts.map +1 -1
  91. package/ts_build/src/lib/model/preferences.js +3 -0
  92. package/ts_build/src/lib/model/preferences.js.map +1 -1
  93. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  94. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  95. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  96. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  97. package/ts_build/src/lib/model/wallet.d.ts.map +1 -1
  98. package/ts_build/src/lib/model/wallet.js +1 -0
  99. package/ts_build/src/lib/model/wallet.js.map +1 -1
  100. package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
  101. package/ts_build/src/lib/pushnotificationsservice.js +2 -0
  102. package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
  103. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  104. package/ts_build/src/lib/routes/maintenance.js +12 -4
  105. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  106. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  107. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  108. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  109. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  110. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  111. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  112. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  113. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  114. package/ts_build/src/lib/routes/services.js +7 -2
  115. package/ts_build/src/lib/routes/services.js.map +1 -1
  116. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  117. package/ts_build/src/lib/routes/tss.js +32 -30
  118. package/ts_build/src/lib/routes/tss.js.map +1 -1
  119. package/ts_build/src/lib/routes/walletdata.d.ts.map +1 -1
  120. package/ts_build/src/lib/routes/walletdata.js +2 -0
  121. package/ts_build/src/lib/routes/walletdata.js.map +1 -1
  122. package/ts_build/src/lib/server.d.ts +23 -2
  123. package/ts_build/src/lib/server.d.ts.map +1 -1
  124. package/ts_build/src/lib/server.js +37 -5
  125. package/ts_build/src/lib/server.js.map +1 -1
  126. package/ts_build/src/lib/storage.d.ts +9 -0
  127. package/ts_build/src/lib/storage.d.ts.map +1 -1
  128. package/ts_build/src/lib/storage.js +26 -0
  129. package/ts_build/src/lib/storage.js.map +1 -1
  130. package/ts_build/src/lib/tss.d.ts +12 -0
  131. package/ts_build/src/lib/tss.d.ts.map +1 -1
  132. package/ts_build/src/lib/tss.js +64 -5
  133. package/ts_build/src/lib/tss.js.map +1 -1
  134. package/ts_build/test/blockchainexplorer.test.js +6 -0
  135. package/ts_build/test/blockchainexplorer.test.js.map +1 -1
  136. package/ts_build/test/expressapp.test.js +120 -0
  137. package/ts_build/test/expressapp.test.js.map +1 -1
  138. package/ts_build/test/integration/externalservices/coinGecko.test.js +38 -2
  139. package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
  140. package/ts_build/test/integration/externalservices/transak.test.d.ts.map +1 -1
  141. package/ts_build/test/integration/externalservices/transak.test.js +498 -128
  142. package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
  143. package/ts_build/test/integration/fiatrateservice.test.js +11 -0
  144. package/ts_build/test/integration/fiatrateservice.test.js.map +1 -1
  145. package/ts_build/test/integration/helpers.d.ts.map +1 -1
  146. package/ts_build/test/integration/helpers.js +1 -0
  147. package/ts_build/test/integration/helpers.js.map +1 -1
  148. package/ts_build/test/integration/server.test.js +182 -3
  149. package/ts_build/test/integration/server.test.js.map +1 -1
  150. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  151. package/ts_build/test/integration/tss.test.js +344 -0
  152. package/ts_build/test/integration/tss.test.js.map +1 -1
@@ -0,0 +1,95 @@
1
+ import util from 'util';
2
+ import { Constants, ethers } from '@bitpay-labs/crypto-wallet-core';
3
+ import { ClientError } from '../../errors/clienterror';
4
+ import { Errors } from '../../errors/errordefinitions';
5
+ import { EthChain } from '../eth';
6
+ import { ERC20Abi } from '../eth/abi-erc20';
7
+ import type { TxProposal } from '../../model/txproposal';
8
+
9
+ const usdcInterface = new ethers.Interface(ERC20Abi);
10
+
11
+ export class ArcChain extends EthChain {
12
+ constructor() {
13
+ super();
14
+ this.chain = 'ARC';
15
+ }
16
+
17
+ private isUsdcTokenView(opts) {
18
+ return opts.tokenAddress?.toLowerCase() === Constants.ARC.USDC_CONTRACT_ADDRESS;
19
+ }
20
+
21
+ protected getAdditionalNativeAmount(txp: TxProposal): bigint {
22
+ if (txp.tokenAddress || txp.multisigContractAddress || txp.multiSendContractAddress) return 0n;
23
+
24
+ try {
25
+ return (txp.outputs || []).reduce((total, output, index) => {
26
+ if (output.toAddress?.toLowerCase() !== Constants.ARC.USDC_CONTRACT_ADDRESS) return total;
27
+ const data = index === 0 && txp.data ? txp.data : output.data;
28
+ if (!data || data === '0x') return total;
29
+ const call = usdcInterface.parseTransaction({ data });
30
+ let amount = 0n;
31
+ if (call?.name === 'transfer') {
32
+ amount = call.args[1];
33
+ } else if (call?.name === 'transferFrom' && call.args[0].toLowerCase() === txp.from?.toLowerCase()) {
34
+ amount = call.args[2];
35
+ }
36
+ // The ERC20 interface uses 6 decimals; the same native balance uses 18.
37
+ return total + amount * 10n ** 12n;
38
+ }, 0n);
39
+ } catch {
40
+ throw new ClientError('Invalid Arc USDC contract call data');
41
+ }
42
+ }
43
+
44
+ getWalletBalance(server, wallet, opts, cb) {
45
+ // Native USDC and its ERC-20 view share funds. A separate token balance would
46
+ // use different units and omit the native spend locked by pending proposals.
47
+ if (this.isUsdcTokenView(opts)) {
48
+ return cb(new ClientError('USDC is the native Arc balance; omit tokenAddress'));
49
+ }
50
+ return super.getWalletBalance(server, wallet, opts, cb);
51
+ }
52
+
53
+ getWalletSendMaxInfo(server, wallet, opts, cb) {
54
+ const onFee = (err, feePerKb?) => {
55
+ if (err) return cb(err);
56
+ feePerKb = Number(feePerKb);
57
+ if (!Number.isFinite(feePerKb) || feePerKb < Constants.ARC.MIN_GAS_PRICE) {
58
+ return cb(new ClientError('Arc gas price must be at least 20 gwei'));
59
+ }
60
+ return super.getWalletSendMaxInfo(server, wallet, { ...opts, feePerKb }, cb);
61
+ };
62
+ // Match the type-2 cap reserved when creating the proposal.
63
+ if (Number(opts.txType) === 2) {
64
+ return server.estimateFee({ network: wallet.network, chain: wallet.chain, txType: 2 })
65
+ .then(feePerKb => onFee(null, feePerKb), onFee);
66
+ }
67
+ return server._getFeePerKb(wallet, opts, onFee);
68
+ }
69
+
70
+ async getFee(server, wallet, opts) {
71
+ if (this.isUsdcTokenView(opts)) {
72
+ throw new ClientError('USDC is the native Arc asset; omit tokenAddress');
73
+ }
74
+ const fees = await super.getFee(server, wallet, opts);
75
+ const gasPrice = Number(Number(opts.txType) === 2 ? fees.maxGasFee : fees.gasPrice);
76
+ if (!Number.isFinite(gasPrice) || gasPrice < Constants.ARC.MIN_GAS_PRICE) {
77
+ throw new ClientError('Arc gas price must be at least 20 gwei');
78
+ }
79
+ // Reserve the fee actually encoded in the transaction, including type-2 caps.
80
+ const fee = gasPrice * fees.gasLimit;
81
+ if (opts.sendMax && Number(opts.txType) === 2) {
82
+ const balance = await util.promisify(server.getBalance).call(server, {});
83
+ if (balance.availableAmount <= fee) throw Errors.INSUFFICIENT_FUNDS_FOR_FEE;
84
+ opts.outputs[0].amount = balance.availableAmount - fee;
85
+ }
86
+ return { ...fees, fee };
87
+ }
88
+
89
+ checkDust(output, opts) {
90
+ if (!opts.tokenAddress && output.amount > 0 &&
91
+ output.toAddress.toLowerCase() === '0x0000000000000000000000000000000000000000') {
92
+ return new ClientError('Native USDC cannot be sent to the zero address on Arc');
93
+ }
94
+ }
95
+ }
@@ -84,6 +84,10 @@ export class EthChain implements IChain {
84
84
  return true;
85
85
  }
86
86
 
87
+ protected getAdditionalNativeAmount(_txp: TxProposal): bigint {
88
+ return 0n;
89
+ }
90
+
87
91
  getWalletBalance(server, wallet, opts, cb) {
88
92
  const bc = server._getBlockchainExplorer(wallet.chain || wallet.coin, wallet.network);
89
93
 
@@ -106,18 +110,22 @@ export class EthChain implements IChain {
106
110
  let fees = 0n;
107
111
  let amounts = 0n;
108
112
 
109
- txps.filter(txp => {
110
- // Add gas used for tokens when getting native balance
111
- if (!opts.tokenAddress) {
112
- fees += txp.fee ? BigInt(txp.fee) : 0n;
113
- }
114
- // Filter tokens when getting native balance
115
- if (txp.tokenAddress && !opts.tokenAddress) {
116
- return false;
117
- }
118
- amounts += txp.amount ? BigInt(txp.amount) : 0n;
119
- return true;
120
- });
113
+ try {
114
+ txps.filter(txp => {
115
+ // Add gas used for tokens when getting native balance
116
+ if (!opts.tokenAddress) {
117
+ fees += txp.fee ? BigInt(txp.fee) : 0n;
118
+ }
119
+ // Filter tokens when getting native balance
120
+ if (txp.tokenAddress && !opts.tokenAddress) {
121
+ return false;
122
+ }
123
+ amounts += BigInt(txp.amount || 0) + this.getAdditionalNativeAmount(txp);
124
+ return true;
125
+ });
126
+ } catch (err) {
127
+ return cb(err);
128
+ }
121
129
 
122
130
  // TODO support big int
123
131
  const lockedSum = Number(amounts + fees) || 0; // previously set to 0 if opts.multisigContractAddress
@@ -175,8 +183,15 @@ export class EthChain implements IChain {
175
183
  checkScriptOutput(_output) { }
176
184
 
177
185
  async getFee(server, wallet, opts) {
186
+ const { from, txType, priorityFeePercentile, gasLimitBuffer } = opts;
187
+ const { coin, network, chain } = wallet;
178
188
  const hasType2FeeOverride = opts.maxGasFee != null || opts.priorityGasFee != null;
179
189
  let { maxGasFee, priorityGasFee } = opts;
190
+ if (Number(txType) === 2 && !hasType2FeeOverride) {
191
+ maxGasFee = await server.estimateFee({ network, chain: chain || coin, txType: 2 });
192
+ priorityGasFee = await server.estimatePriorityFee({ network, chain: chain || coin, percentile: priorityFeePercentile || 15 });
193
+ maxGasFee = Math.max(maxGasFee, priorityGasFee);
194
+ }
180
195
  if (hasType2FeeOverride) {
181
196
  const chain = wallet.chain || wallet.coin;
182
197
  // Keep overrides consistent with CWC's chain-specific minimum priority fee.
@@ -185,8 +200,8 @@ export class EthChain implements IChain {
185
200
  throw new ClientError(`priorityGasFee must be at least ${minimum} wei for ${chain}`);
186
201
  }
187
202
  }
188
- // Use the explicit rate for both construction and the fee reserved from the wallet balance.
189
- const feeOpts = opts.gasPrice != null || hasType2FeeOverride
203
+ // Reserve the same rate used for construction, including the automatic type-2 fee cap.
204
+ const feeOpts = opts.gasPrice != null || maxGasFee != null
190
205
  ? { ...opts, feePerKb: opts.gasPrice ?? maxGasFee }
191
206
  : opts;
192
207
  // Fixed fees are converted to a gas price below, once the gas limits are known.
@@ -196,8 +211,6 @@ export class EthChain implements IChain {
196
211
  : await util.promisify(server._getFeePerKb).call(server, wallet, feeOpts);
197
212
  let feePerKb = inFeePerKb;
198
213
  let gasPrice = inFeePerKb;
199
- const { from, txType, priorityFeePercentile, gasLimitBuffer } = opts;
200
- const { coin, network, chain } = wallet;
201
214
  let inGasLimit = 0; // Per recepient gas limit
202
215
  let gasLimit = 0; // Gas limit for all recepients. used for contract interactions that rollup recepients
203
216
  let fee = 0;
@@ -297,9 +310,9 @@ export class EthChain implements IChain {
297
310
  fee += feePerKb * gasLimit;
298
311
  logger.info(`[${from}] Top level gas limit with buffer: ${gasLimit}`);
299
312
  }
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 });
313
+ // BWC verifies an explicit feePerKb against the request, separately from the type-2 fee cap.
314
+ if (Number(txType) === 2 && Common.Utils.isNumber(opts.feePerKb)) {
315
+ feePerKb = opts.feePerKb;
303
316
  }
304
317
  return { feePerKb, gasPrice, gasLimit, maxGasFee, priorityGasFee, fee };
305
318
  }
@@ -482,10 +495,15 @@ export class EthChain implements IChain {
482
495
  "gasLimit":200000
483
496
  }]
484
497
  */
485
- const txpTotalAmount =
486
- (opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl
487
- ? getInvoiceValue(txp)
488
- : txp.getTotalAmountBigInt();
498
+ let txpTotalAmount;
499
+ try {
500
+ txpTotalAmount =
501
+ (opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl
502
+ ? getInvoiceValue(txp)
503
+ : txp.getTotalAmountBigInt() + this.getAdditionalNativeAmount(txp);
504
+ } catch (err) {
505
+ return cb(err);
506
+ }
489
507
 
490
508
  if (totalAmount < txpTotalAmount) {
491
509
  return cb(Errors.INSUFFICIENT_FUNDS);
@@ -4,6 +4,7 @@ import { IWallet, TxProposal } from '../model';
4
4
  import { WalletService } from '../server';
5
5
  import logger from './../logger';
6
6
  import { ArbChain } from './arb';
7
+ import { ArcChain } from './arc';
7
8
  import { BaseChain } from './base';
8
9
  import { BchChain } from './bch';
9
10
  import { BtcChain } from './btc';
@@ -24,6 +25,7 @@ const chains: { [chain: string]: IChain } = {
24
25
  ETH: new EthChain(),
25
26
  MATIC: new MaticChain(),
26
27
  ARB: new ArbChain(),
28
+ ARC: new ArcChain(),
27
29
  BASE: new BaseChain(),
28
30
  OP: new OpChain(),
29
31
  XRP: new XrpChain(),
@@ -10,6 +10,7 @@ export const Constants = {
10
10
  ARB: 'arb',
11
11
  BASE: 'base',
12
12
  OP: 'op',
13
+ ARC: 'arc',
13
14
  XRP: 'xrp',
14
15
  DOGE: 'doge',
15
16
  LTC: 'ltc',
@@ -38,7 +39,8 @@ export const Constants = {
38
39
  EUROC: 'euroc',
39
40
  USDT: 'usdt',
40
41
  WETH: 'weth',
41
- SOL: 'sol'
42
+ SOL: 'sol',
43
+ ARC: 'arc'
42
44
  },
43
45
 
44
46
  BITPAY_SUPPORTED_ETH_ERC20: {
@@ -60,6 +62,7 @@ export const Constants = {
60
62
 
61
63
  BITPAY_USD_STABLECOINS: {
62
64
  // used for rates
65
+ ARC: 'arc',
63
66
  USDC: 'usdc',
64
67
  PYUSD: 'pyusd',
65
68
  USDP: 'usdp',
@@ -88,6 +91,7 @@ export const Constants = {
88
91
  ARB: 'arb',
89
92
  BASE: 'base',
90
93
  OP: 'op',
94
+ ARC: 'arc',
91
95
  },
92
96
 
93
97
  EVM_CHAINS_WITH_ETH_GAS: {
@@ -112,6 +116,7 @@ export const Constants = {
112
116
  arb: ['livenet', 'sepolia', 'holesky', 'regtest'],
113
117
  base: ['livenet', 'sepolia', 'holesky', 'regtest'],
114
118
  op: ['livenet', 'sepolia', 'holesky', 'regtest'],
119
+ arc: ['livenet', 'testnet'],
115
120
  sol: ['livenet', 'testnet', 'devnet', 'regtest']
116
121
  } as { [chain: string]: Array<string> },
117
122
 
@@ -156,6 +161,9 @@ export const Constants = {
156
161
  mainnet: 'livenet',
157
162
  testnet: 'sepolia'
158
163
  },
164
+ arc: {
165
+ mainnet: 'livenet'
166
+ },
159
167
  sol: {
160
168
  mainnet: 'livenet',
161
169
  testnet: 'devnet'
@@ -200,6 +208,8 @@ export const Constants = {
200
208
  ARB_TOKEN_OPTS: CWC.Constants.ARB_TOKEN_OPTS,
201
209
  OP_TOKEN_OPTS: CWC.Constants.OP_TOKEN_OPTS,
202
210
  BASE_TOKEN_OPTS: CWC.Constants.BASE_TOKEN_OPTS,
211
+ ARC_TOKEN_OPTS: CWC.Constants.ARC_TOKEN_OPTS,
212
+ ARC: CWC.Constants.ARC,
203
213
  SOL_TOKEN_OPTS: CWC.Constants.SOL_TOKEN_OPTS,
204
214
 
205
215
  BITPAY_CONTRACTS: {
@@ -219,6 +229,7 @@ export const Constants = {
219
229
  bch: { testnet: 300, livenet: 0 },
220
230
  eth: { testnet: 300, livenet: 0 },
221
231
  matic: { testnet: 300, livenet: 12 }, // MATIC set to 12 because blocks normally occur every 1-2 seconds
232
+ arc: { testnet: 0, livenet: 0 },
222
233
  xrp: { testnet: 300, livenet: 0 },
223
234
  doge: { testnet: 300, livenet: 0 },
224
235
  ltc: { testnet: 300, livenet: 0 }
@@ -226,7 +237,7 @@ export const Constants = {
226
237
 
227
238
  // May be different from the defaults if we rollback
228
239
  // or want to beta test new versions.
229
- TSS_KEYGEN_SCHEME_VERSION_MAX: 1.2,
240
+ TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3,
230
241
  TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1,
231
242
 
232
243
  TSS_KEYGEN_SCHEME_VERSION_MIN: 1,
@@ -194,6 +194,34 @@ export const Defaults = {
194
194
  defaultValue: 2000000000
195
195
  }
196
196
  ],
197
+ // Fallbacks must meet Arc's testnet gas-price floor. Live estimates still take precedence.
198
+ arc: [
199
+ {
200
+ name: 'urgent',
201
+ nbBlocks: 1,
202
+ defaultValue: 20000000000
203
+ },
204
+ {
205
+ name: 'priority',
206
+ nbBlocks: 2,
207
+ defaultValue: 20000000000
208
+ },
209
+ {
210
+ name: 'normal',
211
+ nbBlocks: 3,
212
+ defaultValue: 20000000000
213
+ },
214
+ {
215
+ name: 'economy',
216
+ nbBlocks: 4,
217
+ defaultValue: 20000000000
218
+ },
219
+ {
220
+ name: 'superEconomy',
221
+ nbBlocks: 4,
222
+ defaultValue: 20000000000
223
+ }
224
+ ],
197
225
  xrp: [
198
226
  {
199
227
  name: 'normal',
@@ -362,6 +390,7 @@ export const Defaults = {
362
390
  arb: 1000000000000, // 50 Gwei,
363
391
  base: 1000000000000, // 50 Gwei,
364
392
  op: 1000000000000, // 50 Gwei,
393
+ arc: 1000000000000,
365
394
  xrp: 1000000000000,
366
395
  doge: 100000000 * 100,
367
396
  ltc: 10000 * 1000, // 10k sat/b
@@ -376,6 +405,7 @@ export const Defaults = {
376
405
  arb: 0,
377
406
  base: 0,
378
407
  op: 0,
408
+ arc: 0,
379
409
  xrp: 0,
380
410
  doge: 0,
381
411
  ltc: 0,
@@ -390,6 +420,7 @@ export const Defaults = {
390
420
  arb: 1 * 1e18, // 1 eth
391
421
  base: 1 * 1e18, // 1 eth
392
422
  op: 1 * 1e18, // 1 eth
423
+ arc: 1 * 1e18, // 1 USDC
393
424
  xrp: 1 * 1e6, // 1 xrp
394
425
  doge: 400 * 1e8,
395
426
  ltc: 0.05 * 1e8
@@ -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
  };
@@ -337,6 +337,7 @@ export class EmailService {
337
337
  bch: 'BCH',
338
338
  eth: 'ETH',
339
339
  matic: 'MATIC',
340
+ arc: 'USDC',
340
341
  xrp: 'XRP',
341
342
  doge: 'DOGE',
342
343
  ltc: 'LTC',
@@ -685,6 +686,7 @@ export class EmailService {
685
686
  arb: 'arbitrum-one',
686
687
  base: 'base',
687
688
  op: 'optimistic-ethereum',
689
+ arc: 'arc',
688
690
  sol: 'solana',
689
691
  };
690
692
 
@@ -739,4 +741,4 @@ export class EmailService {
739
741
  });
740
742
  });
741
743
  }
742
- }
744
+ }
@@ -37,6 +37,8 @@ interface Errors<T> {
37
37
  // Solana Errors
38
38
  INSUFFICIENT_SOL_FEE: T;
39
39
  LOCKED_SOL_FEE: T;
40
+ INSUFFICIENT_ARC_FEE: T;
41
+ LOCKED_ARC_FEE: T;
40
42
  HISTORY_LIMIT_EXCEEDED: T;
41
43
  MAIN_ADDRESS_GAP_REACHED: T;
42
44
  MULTI_TX_UNSUPPORTED: T;
@@ -114,6 +116,8 @@ const errors: Errors<string> = {
114
116
  LOCKED_OP_FEE: 'Your linked OP wallet does not have enough ETH for fee',
115
117
  INSUFFICIENT_SOL_FEE: 'Your linked SOL wallet does not have enough SOL for fee',
116
118
  LOCKED_SOL_FEE: 'Your linked SOL wallet does not have enough SOL for fee',
119
+ INSUFFICIENT_ARC_FEE: 'Your linked ARC wallet does not have enough USDC for fee',
120
+ LOCKED_ARC_FEE: 'Your linked ARC wallet USDC is locked by pending spend proposals',
117
121
  INVALID_ADDRESS: 'Invalid address',
118
122
  INVALID_CHANGE_ADDRESS: 'Invalid change address',
119
123
  KEY_IN_COPAYER: 'Key already registered',
@@ -12,6 +12,7 @@ export interface IPreferences {
12
12
  opTokenAddresses?: string[];
13
13
  baseTokenAddresses?: string[];
14
14
  arbTokenAddresses?: string[];
15
+ arcTokenAddresses?: string[];
15
16
  solTokenAddresses?: string[];
16
17
  multisigMaticInfo: object[];
17
18
  }
@@ -29,6 +30,7 @@ export class Preferences {
29
30
  opTokenAddresses: string[];
30
31
  baseTokenAddresses: string[];
31
32
  arbTokenAddresses: string[];
33
+ arcTokenAddresses: string[];
32
34
  solTokenAddresses: string[];
33
35
  multisigMaticInfo: object[];
34
36
 
@@ -50,6 +52,7 @@ export class Preferences {
50
52
  x.opTokenAddresses = opts.opTokenAddresses;
51
53
  x.baseTokenAddresses = opts.baseTokenAddresses;
52
54
  x.arbTokenAddresses = opts.arbTokenAddresses;
55
+ x.arcTokenAddresses = opts.arcTokenAddresses;
53
56
  x.solTokenAddresses = opts.solTokenAddresses;
54
57
  x.multisigMaticInfo = opts.multisigMaticInfo;
55
58
  // you can't put useDust here since this is copayer's specific.
@@ -72,6 +75,7 @@ export class Preferences {
72
75
  x.opTokenAddresses = obj.opTokenAddresses;
73
76
  x.baseTokenAddresses = obj.baseTokenAddresses;
74
77
  x.arbTokenAddresses = obj.arbTokenAddresses;
78
+ x.arcTokenAddresses = obj.arcTokenAddresses;
75
79
  x.solTokenAddresses = obj.solTokenAddresses;
76
80
  x.multisigMaticInfo = obj.multisigMaticInfo;
77
81
  return x;
@@ -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
  }
@@ -24,6 +24,7 @@ const Bitcore = {
24
24
  arb: BitcoreLib,
25
25
  base: BitcoreLib,
26
26
  op: BitcoreLib,
27
+ arc: BitcoreLib,
27
28
  xrp: BitcoreLib,
28
29
  doge: BitcoreLibDoge,
29
30
  ltc: BitcoreLibLtc,
@@ -474,6 +474,7 @@ export class PushNotificationsService {
474
474
  bch: 'BCH',
475
475
  eth: 'ETH',
476
476
  matic: 'MATIC',
477
+ arc: 'USDC',
477
478
  xrp: 'XRP',
478
479
  doge: 'DOGE',
479
480
  ltc: 'LTC',
@@ -748,6 +749,7 @@ export class PushNotificationsService {
748
749
  arb: 'arbitrum-one',
749
750
  base: 'base',
750
751
  op: 'optimistic-ethereum',
752
+ arc: 'arc',
751
753
  sol: 'solana',
752
754
  };
753
755
 
@@ -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) {