@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
@@ -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
@@ -455,7 +461,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
455
461
 
456
462
  router.post('/v1/service/transak/orderDetails', cors(transakCorsOptions), (req, res) => {
457
463
  respondWithAuthServer(req, res, context, server => {
458
- return server.externalServices.transak.transakGetOrderDetails(req);
464
+ return server.externalServices.transak.transakGetOrderDetails(req, server.walletId);
459
465
  });
460
466
  });
461
467
 
@@ -1,7 +1,6 @@
1
1
  import express from 'express';
2
2
  import * as Types from '../../types/expressapp';
3
3
  import { TssKeyGen, TssSign } from '../tss';
4
- import { authRequest } from './middleware/authRequest';
5
4
  import { authTssKeyGenRequest, authTssSignRequest } from './middleware/authTssRequest';
6
5
  import { createWalletLimiter } from './middleware/createWalletLimiter';
7
6
  import { verifyTssMessage } from './middleware/verifyTssMessage';
@@ -49,6 +48,36 @@ export class TssRouter {
49
48
  }
50
49
  });
51
50
 
51
+ // Must be before /keygen/:id/:round so "secret" doesn't get interpreted as a round number
52
+ router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
53
+ try {
54
+ const id = req.params.id;
55
+ const copayerId = req.headers['x-identity'];
56
+ const { chain, network } = req.query as { [key: string]: string };
57
+ const secret = await TssKeyGen.getBwsJoinSecret({ id, chain, network, copayerId });
58
+ return res.json({ secret });
59
+ } catch (err) {
60
+ return returnError(err ?? 'unknown', res, req);
61
+ }
62
+ });
63
+
64
+ router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
65
+ try {
66
+ const id = req.params.id;
67
+ const { chain, network, secrets, secret } = req.body;
68
+ const copayerId = req.headers['x-identity'];
69
+ if (secrets == null && secret != null) {
70
+ // Legacy (< v1.3) clients send a single unencrypted secret
71
+ await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
72
+ } else {
73
+ await TssKeyGen.storeBwsJoinSecrets({ id, chain, network, secrets, copayerId });
74
+ }
75
+ return res.send();
76
+ } catch (err) {
77
+ return returnError(err ?? 'unknown', res, req);
78
+ }
79
+ });
80
+
52
81
  router.get('/v1/tss/keygen/:id/:round', authTssKeyGenRequest(), async function(req, res) {
53
82
  let interval: NodeJS.Timeout;
54
83
  try {
@@ -58,10 +87,6 @@ export class TssRouter {
58
87
  const { id, round } = req.params as { [key: string]: string };
59
88
  const { maxWaitTime } = req.query as { [key: string]: string };
60
89
  const copayerId = req.headers['x-identity'];
61
- if (round === 'secret') {
62
- const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
63
- return res.json({ secret });
64
- }
65
90
 
66
91
  // Validate access and fetch session before committing to a streaming response, so that errors like
67
92
  // "session not found" or "not a participant" can still be returned with a proper
@@ -100,33 +125,10 @@ export class TssRouter {
100
125
  }
101
126
  });
102
127
 
103
- router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
104
- try {
105
- const id = req.params.id;
106
- const { secret } = req.body;
107
- const copayerId = req.headers['x-identity'];
108
- await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
109
- return res.send();
110
- } catch (err) {
111
- return returnError(err ?? 'unknown', res, req);
112
- }
113
- });
114
-
115
- router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
116
- try {
117
- const id = req.params.id;
118
- const copayerId = req.headers['x-identity'];
119
- const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
120
- return res.json({ secret });
121
- } catch (err) {
122
- return returnError(err ?? 'unknown', res, req);
123
- }
124
- });
125
-
126
128
 
127
129
  /** Signature Methods */
128
130
 
129
- router.post('/v1/tss/sign/:id', authRequest(), verifyTssMessage, async function(req, res) {
131
+ router.post('/v1/tss/sign/:id', authTssSignRequest({ joinable: true }), verifyTssMessage, async function(req, res) {
130
132
  try {
131
133
  const id = req.params.id;
132
134
  // version was not given by client until 1.1, so fallback to 1.0
@@ -178,7 +180,8 @@ export class TssRouter {
178
180
  try {
179
181
  const id = req.params.id;
180
182
  const { signature } = req.body;
181
- await TssSign.storeSignature({ id, signature });
183
+ const copayerId = req.headers['x-identity'];
184
+ await TssSign.storeSignature({ id, signature, copayerId });
182
185
  return res.send();
183
186
  } catch (err) {
184
187
  return returnError(err ?? 'unknown', res, req);
@@ -125,11 +125,13 @@ export function registerWalletDataRoutes(router: express.Router, context: RouteC
125
125
  const opts: {
126
126
  feePerKb?: number;
127
127
  feeLevel?: number;
128
+ txType?: number;
128
129
  returnInputs?: boolean;
129
130
  excludeUnconfirmedUtxos?: boolean;
130
131
  } = {};
131
132
  if (query.feePerKb) opts.feePerKb = +query.feePerKb;
132
133
  if (query.feeLevel) opts.feeLevel = Number(query.feeLevel);
134
+ if (query.txType != null) opts.txType = Number(query.txType);
133
135
  if (query.excludeUnconfirmedUtxos == '1') opts.excludeUnconfirmedUtxos = true;
134
136
  if (query.returnInputs == '1') opts.returnInputs = true;
135
137
 
package/src/lib/server.ts CHANGED
@@ -71,6 +71,7 @@ const Bitcore_ = {
71
71
  arb: Bitcore,
72
72
  base: Bitcore,
73
73
  op: Bitcore,
74
+ arc: Bitcore,
74
75
  xrp: Bitcore,
75
76
  doge: BitcoreDoge,
76
77
  ltc: BitcoreLtc,
@@ -187,7 +188,7 @@ export class WalletService implements IWalletService {
187
188
  sardine: new SardineService(),
188
189
  simplex: new SimplexService(),
189
190
  thorswap: new ThorswapService(),
190
- transak: new TransakService(),
191
+ transak: new TransakService(this.storage, this.lock),
191
192
  coinGecko: new CoinGeckoService(this.storage),
192
193
  };
193
194
  }
@@ -604,6 +605,7 @@ export class WalletService implements IWalletService {
604
605
  * @param {number} [opts.tssVersion] TSS version to use. Supplying this with n > 1 and a multisig chain (e.g. btc) will tell the wallet to use
605
606
  * threshold insead of on-chain multisig. Otherwise, n > 1 and a non-multisig chain will default to TSS_KEYGEN_SCHEME_VERSION
606
607
  * @param {string} [opts.tssKeyId] TSS key session id. This is the id of the TSS key generation session that will be used to create the wallet.
608
+ * @param {string} [opts.tssKeygenCopayerId] The ID of the copayer creating the wallet. Required for TSS version >= 1.3.
607
609
  */
608
610
  async createWallet(opts, cb) {
609
611
  let pubKey;
@@ -674,6 +676,17 @@ export class WalletService implements IWalletService {
674
676
  return cb(new ClientError('Invalid TSS version'));
675
677
  }
676
678
 
679
+ if (opts.tssVersion >= 1.3) {
680
+ if (!opts.tssKeygenCopayerId) {
681
+ return cb(new ClientError('Copayer ID is required for TSS version >= 1.3'));
682
+ }
683
+ // This is only a sanity check to prevent problems down the line. If this is somehow
684
+ // spoofed, there are more strict authorization checks later in the wallet creation flow.
685
+ if (keySession.participants.indexOf(opts.tssKeygenCopayerId) !== 0) {
686
+ return cb(new ClientError('Only the TSS session leader can create a wallet'));
687
+ }
688
+ }
689
+
677
690
  // TSS wallets behave like a single-sig
678
691
  opts.m = 1;
679
692
  opts.n = 1;
@@ -1038,7 +1051,11 @@ export class WalletService implements IWalletService {
1038
1051
  this._notify(type, data, {}, cb);
1039
1052
  }
1040
1053
 
1041
- _addCopayerToWallet(wallet: Wallet, opts, cb) {
1054
+ _addCopayerToWallet(
1055
+ wallet: Wallet,
1056
+ opts: JoinWalletOpts,
1057
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1058
+ ) {
1042
1059
  const copayer = Copayer.create({
1043
1060
  coin: wallet.coin,
1044
1061
  chain: wallet.chain, // chain === coin for stored clients
@@ -1194,20 +1211,11 @@ export class WalletService implements IWalletService {
1194
1211
 
1195
1212
  /**
1196
1213
  * Joins a wallet in creation.
1197
- * @param {Object} opts
1198
- * @param {string} opts.walletId The wallet id.
1199
- * @param {string} opts.coin The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc
1200
- * @param {string} opts.chain The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc
1201
- * @param {string} opts.name The copayer name.
1202
- * @param {string} opts.xPubKey Extended Public Key for this copayer
1203
- * @param {string} opts.hardwareSourcePublicKey Public key from a hardware device for this copayer
1204
- * @param {string} opts.clientDerivedPublicKey Public key from the client for this wallet
1205
- * @param {string} opts.requestPubKey Public Key used to check requests from this copayer.
1206
- * @param {string} opts.copayerSignature S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
1207
- * @param {string} [opts.customData] Custom data for this copayer.
1208
- * @param {boolean} [opts.dryRun] Simulate the action but do not change server state.
1209
1214
  */
1210
- joinWallet(opts, cb) {
1215
+ joinWallet(
1216
+ opts: JoinWalletOpts,
1217
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1218
+ ) {
1211
1219
  // xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
1212
1220
  // fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
1213
1221
  // end-to-end identity protocol rather than a local identity fallback.
@@ -1226,6 +1234,9 @@ export class WalletService implements IWalletService {
1226
1234
  opts.chain = opts.coin; // chain === coin for stored clients
1227
1235
  }
1228
1236
  if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
1237
+ if (opts.tssKeygenChain != null && !Utils.checkValueInCollection(opts.tssKeygenChain, Constants.CHAINS)) {
1238
+ return cb(new ClientError('Invalid TSS keygen chain'));
1239
+ }
1229
1240
 
1230
1241
  // xPubKey is parsed and validated for every join; ancillary fields never suppress it.
1231
1242
  // A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
@@ -1286,8 +1297,16 @@ export class WalletService implements IWalletService {
1286
1297
 
1287
1298
  if (wallet.tssKeyId) {
1288
1299
  const keySession = await storage.fetchTssKeyGenSession({ id: wallet.tssKeyId });
1289
- const copayerId = Copayer.xPubToCopayerId(opts.chain, opts.xPubKey);
1290
- if (!keySession.participants.includes(copayerId)) {
1300
+ // A sibling wallet on another network (e.g. livenet -> testnet) has an xPubKey serialized for that network,
1301
+ // but the keygen session's copayer IDs were derived from the xPubKey serialized for the session's network.
1302
+ const idChain = opts.tssKeygenChain || opts.chain;
1303
+ const copayerIds = new Set([
1304
+ Copayer.xPubToCopayerId(idChain, opts.xPubKey), // this is probably a duplicate of one of the next two entries, but just in case.
1305
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'livenet')),
1306
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'testnet'))
1307
+ ]);
1308
+
1309
+ if (!keySession?.participants.some(id => copayerIds.has(id))) {
1291
1310
  return cb(Errors.TSS_NON_PARTICIPANT);
1292
1311
  }
1293
1312
  return this._addCopayerToWallet(wallet, opts, cb);
@@ -1312,6 +1331,7 @@ export class WalletService implements IWalletService {
1312
1331
  * @param {string} opts.opTokenAddresses - Linked token addresses
1313
1332
  * @param {string} opts.baseTokenAddresses - Linked token addresses
1314
1333
  * @param {string} opts.arbTokenAddresses - Linked token addresses
1334
+ * @param {string[]} opts.arcTokenAddresses - Linked Arc ERC20 token addresses
1315
1335
  * @param {string} opts.solTokenAddresses - Linked token addresses
1316
1336
  * @param {string} opts.multisigMaticInfo - Linked multisig eth wallet info
1317
1337
  *
@@ -1386,6 +1406,14 @@ export class WalletService implements IWalletService {
1386
1406
  return Array.isArray(value) && value.every(x => Validation.validateAddress('arb', 'mainnet', x));
1387
1407
  }
1388
1408
  },
1409
+ {
1410
+ name: 'arcTokenAddresses',
1411
+ isValid(value) {
1412
+ return Array.isArray(value) && value.every(address =>
1413
+ Validation.validateAddress('arc', 'mainnet', address) &&
1414
+ address.toLowerCase() !== Constants.ARC.USDC_CONTRACT_ADDRESS);
1415
+ }
1416
+ },
1389
1417
  {
1390
1418
  name: 'solTokenAddresses',
1391
1419
  isValid(value) {
@@ -1487,6 +1515,13 @@ export class WalletService implements IWalletService {
1487
1515
  preferences.arbTokenAddresses = _.uniq(oldPref.arbTokenAddresses.concat(opts.arbTokenAddresses));
1488
1516
  }
1489
1517
 
1518
+ if (opts.arcTokenAddresses) {
1519
+ oldPref = oldPref || {} as Preferences;
1520
+ oldPref.arcTokenAddresses = oldPref.arcTokenAddresses || [];
1521
+ preferences.arcTokenAddresses = _.uniq(oldPref.arcTokenAddresses.concat(opts.arcTokenAddresses)
1522
+ .map(address => address.toLowerCase()));
1523
+ }
1524
+
1490
1525
  if (opts.solTokenAddresses) {
1491
1526
  oldPref = oldPref || {} as Preferences;
1492
1527
  oldPref.solTokenAddresses = oldPref.solTokenAddresses || [];
@@ -2484,7 +2519,8 @@ export class WalletService implements IWalletService {
2484
2519
  feePerKb: opts.feePerKb,
2485
2520
  excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
2486
2521
  returnInputs: true,
2487
- numSignatures: opts.numSignatures
2522
+ numSignatures: opts.numSignatures,
2523
+ txType: opts.txType
2488
2524
  },
2489
2525
  (err, info) => {
2490
2526
  if (err) return next(err);
@@ -4177,7 +4213,7 @@ export class WalletService implements IWalletService {
4177
4213
  return cb(err);
4178
4214
  }
4179
4215
  wallet.beRegistered = true;
4180
- return this.storage.storeWallet(wallet, err => cb(err, true));
4216
+ return this.storage.storeWalletRegistration(wallet, err => cb(err, true));
4181
4217
  });
4182
4218
  }
4183
4219
 
@@ -5838,3 +5874,30 @@ export const UPGRADES = {
5838
5874
  } as const;
5839
5875
 
5840
5876
  type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
5877
+
5878
+ export interface JoinWalletOpts {
5879
+ /** The wallet id. */
5880
+ walletId: string;
5881
+ /** The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5882
+ coin?: string;
5883
+ /** The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5884
+ chain?: string;
5885
+ /** The copayer name. */
5886
+ name: string;
5887
+ /** Extended Public Key for this copayer */
5888
+ xPubKey: string;
5889
+ /** Public key from a hardware device for this copayer */
5890
+ hardwareSourcePublicKey?: string;
5891
+ /** Public key from the client for this wallet */
5892
+ clientDerivedPublicKey?: string;
5893
+ /** Public Key used to check requests from this copayer. */
5894
+ requestPubKey: string;
5895
+ /** Signature proving knowledge of the wallet secret for this copayer. */
5896
+ copayerSignature: string;
5897
+ /** Custom data for this copayer. */
5898
+ customData?: string;
5899
+ /** Simulate the action but do not change server state. */
5900
+ dryRun?: boolean;
5901
+ /** The chain used for TSS key generation. */
5902
+ tssKeygenChain?: string;
5903
+ };
@@ -835,6 +835,26 @@ export class Storage {
835
835
  );
836
836
  }
837
837
 
838
+ storeWalletRegistration(wallet, cb) {
839
+ this.db.collection(collections.WALLETS).updateOne(
840
+ {
841
+ id: wallet.id
842
+ },
843
+ {
844
+ $set: {
845
+ beRegistered: wallet.beRegistered,
846
+ beAuthPrivateKey2: wallet.beAuthPrivateKey2,
847
+ beAuthPublicKey2: wallet.beAuthPublicKey2
848
+ }
849
+ },
850
+ {
851
+ w: 1,
852
+ upsert: false
853
+ },
854
+ cb
855
+ );
856
+ }
857
+
838
858
  deregisterWallet(walletId, cb) {
839
859
  this.db.collection(collections.WALLETS).updateOne(
840
860
  {
@@ -1961,6 +1981,10 @@ export class Storage {
1961
1981
  );
1962
1982
  }
1963
1983
 
1984
+ /**
1985
+ * Use only for legacy (< v1.3) clients, where bwsJoinSecrets was not available.
1986
+ * @deprecated Use storeTssKeyBwsJoinSecrets instead.
1987
+ */
1964
1988
  async storeTssKeyBwsJoinSecret({ id, secret }: { id: string; secret: string }) {
1965
1989
  return this.db.collection(collections.TSS_KEYGEN).updateOne({
1966
1990
  id
@@ -1973,6 +1997,21 @@ export class Storage {
1973
1997
  { upsert: false });
1974
1998
  }
1975
1999
 
2000
+ async storeTssKeyBwsJoinSecrets(params: { id: string; chain: string; network: string; secrets: { [partyId: number]: string } }) {
2001
+ const { id, chain, network, secrets } = params;
2002
+ const _chain = chain.toLowerCase();
2003
+ const _network = network.toLowerCase();
2004
+ return this.db.collection(collections.TSS_KEYGEN).updateOne({
2005
+ id
2006
+ },
2007
+ {
2008
+ $set: {
2009
+ [`bwsJoinSecrets.${_chain}:${_network}`]: secrets
2010
+ }
2011
+ },
2012
+ { upsert: false });
2013
+ }
2014
+
1976
2015
  async fetchTssSigSession({ id }: { id: string }) {
1977
2016
  const doc = await this.db.collection(collections.TSS_SIGN).findOne({ id });
1978
2017
  if (!doc) {
package/src/lib/tss.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { BitcoreLib } from '@bitpay-labs/crypto-wallet-core';
3
3
  import { Constants } from './common/constants';
4
+ import { Utils } from './common/utils';
4
5
  import { Errors } from './errors/errordefinitions';
5
6
  import logger from './logger';
6
7
  import { TssKeyGenModel } from './model/tsskeygen';
@@ -553,7 +554,10 @@ class TssKeyGenClass {
553
554
  // }
554
555
  }
555
556
 
556
- /** Store the BWS join secret for the TSS keygen session */
557
+ /**
558
+ * Store the BWS join secret for the TSS keygen session
559
+ * @deprecated Use storeBwsJoinSecrets instead.
560
+ */
557
561
  async storeBwsJoinSecret(params: {
558
562
  /** Session ID */
559
563
  id: string;
@@ -587,10 +591,77 @@ class TssKeyGenClass {
587
591
  }
588
592
  }
589
593
 
590
- /** Get the BWS join secret for the TSS keygen session */
594
+ /**
595
+ * Store the party-encrypted BWS join secrets for a wallet on `chain`:`network` created with the TSS key.
596
+ * This can be called after the key generation session has expired to create additional wallets (e.g. other EVM chains) for the same key.
597
+ */
598
+ async storeBwsJoinSecrets(params: {
599
+ /** Session ID */
600
+ id: string;
601
+ /** Blockchain (e.g., 'btc', 'eth') */
602
+ chain: string;
603
+ /** Network (e.g., 'livenet', 'testnet') */
604
+ network: string;
605
+ /** Party encrypted BWS join secrets, keyed by partyId or by the party's keygen session copayer ID */
606
+ secrets: { [party: string]: string };
607
+ /** Copayer ID of the submitting party (must be the session creator) */
608
+ copayerId: string;
609
+ }) {
610
+ const { id, copayerId } = params;
611
+ const { chain, network } = getSecretChainNetwork(params.chain, params.network);
612
+ if (!params.secrets || typeof params.secrets !== 'object' || Array.isArray(params.secrets)) {
613
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid BWS join secrets provided');
614
+ }
615
+ const storage = WalletService.getStorage();
616
+ const session = await storage.fetchTssKeyGenSession({ id });
617
+
618
+ if (!session) {
619
+ throw Errors.TSS_SESSION_NOT_FOUND;
620
+ }
621
+
622
+ if (session.participants.indexOf(copayerId) !== 0) {
623
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Only the session creator can store the BWS join secret');
624
+ }
625
+
626
+ // Only store secrets keyed by partyId for the other parties (1..n-1)
627
+ const secrets: { [partyId: number]: string } = {};
628
+ for (const [party, secret] of Object.entries(params.secrets)) {
629
+ const partyId = /^\d+$/.test(party) ? parseInt(party) : session.participants.indexOf(party);
630
+ if (!(partyId > 0 && partyId < session.n) || typeof secret !== 'string' || !secret) {
631
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid BWS join secret for party ' + party);
632
+ }
633
+ if (secrets[partyId]) {
634
+ // Malformed secrets objects may be keyed by both partyId (number) and copayerId (string).
635
+ // While this technically won't break anything (only 1 secret will be stored), it indicates
636
+ // a malformation which could signal a broader issue with the client.
637
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Multiple BWS join secret for party ' + party);
638
+ }
639
+ secrets[partyId] = secret;
640
+ }
641
+
642
+ if (Object.keys(secrets).length !== session.n - 1) {
643
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Missing BWS join secrets for some parties');
644
+ }
645
+
646
+ const result = await storage.storeTssKeyBwsJoinSecrets({ id, chain, network, secrets });
647
+ if (!result.result.ok) {
648
+ logger.error('Failed to store TSS key generation BWS join secrets %o %o', id, result);
649
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Failed to store TSS key generation BWS join secrets');
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Get the BWS join secret for the TSS keygen session.
655
+ * If chain & network are given, returns the requesting party's encrypted secret for that chain & network.
656
+ * Otherwise, returns the legacy (< v1.3) unencrypted secret.
657
+ */
591
658
  async getBwsJoinSecret(params: {
592
659
  /** Session ID */
593
660
  id: string;
661
+ /** Blockchain (e.g., 'btc', 'eth'). Omitted by legacy clients */
662
+ chain?: string;
663
+ /** Network (e.g., 'livenet', 'testnet'). Omitted by legacy clients */
664
+ network?: string;
594
665
  /** Copayer ID of the requesting party */
595
666
  copayerId: string;
596
667
  }): Promise<string> {
@@ -600,16 +671,42 @@ class TssKeyGenClass {
600
671
  if (!session) {
601
672
  throw Errors.TSS_SESSION_NOT_FOUND;
602
673
  }
603
- if (session.participants.indexOf(copayerId) === -1) {
674
+ const partyId = session.participants.indexOf(copayerId);
675
+ if (partyId === -1) {
604
676
  throw Errors.TSS_NON_PARTICIPANT;
605
677
  }
606
- if (!session.bwsJoinSecret) {
678
+
679
+ if (params.chain == null && params.network == null) {
680
+ // Legacy clients don't send chain & network
681
+ if (!session.bwsJoinSecret) {
682
+ throw Errors.TSS_BWS_JOIN_SECRET_NOT_FOUND;
683
+ }
684
+ return session.bwsJoinSecret;
685
+ }
686
+
687
+ // Note: sessions < v1.3 can have chain-specific secrets too (i.e. wallets created with an existing TSS key)
688
+ const { chain, network } = getSecretChainNetwork(params.chain, params.network);
689
+ const secret = session.bwsJoinSecrets?.[`${chain}:${network}`]?.[partyId];
690
+ if (typeof secret !== 'string') {
607
691
  throw Errors.TSS_BWS_JOIN_SECRET_NOT_FOUND;
608
692
  }
609
- return session.bwsJoinSecret;
693
+ return secret;
610
694
  }
611
695
  };
612
696
 
697
+ /** Validate and normalize the chain & network used to key the BWS join secrets */
698
+ function getSecretChainNetwork(chain: string, network: string) {
699
+ chain = typeof chain === 'string' ? chain.toLowerCase() : chain;
700
+ if (!Utils.checkValueInCollection(chain, Constants.CHAINS)) {
701
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid chain provided');
702
+ }
703
+ network = typeof network === 'string' ? Utils.getNetworkName(chain, Utils.getGenericName(network.toLowerCase())) : network;
704
+ if (!Utils.checkValueInCollection(network, Constants.NETWORKS[chain])) {
705
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid network provided');
706
+ }
707
+ return { chain, network };
708
+ }
709
+
613
710
  export const TssKeyGen = new TssKeyGenClass();
614
711
 
615
712
  class TssSignClass {
@@ -748,7 +845,8 @@ class TssSignClass {
748
845
  if (session.isExpired()) {
749
846
  throw Errors.TSS_SESSION_EXPIRED;
750
847
  }
751
- if (!this._isValidBroadcastMessage({ message }) && !this._isValidP2pMessage({ message })) {
848
+ // 1-of-N sessions won't have messages for other recipients
849
+ if (session.m > 1 && !this._isValidBroadcastMessage({ message }) && !this._isValidP2pMessage({ message })) {
752
850
  throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid message provided');
753
851
  }
754
852
 
@@ -903,8 +1001,10 @@ class TssSignClass {
903
1001
  id: string;
904
1002
  /** Signature to store */
905
1003
  signature: ITssSigMessageObject['signature'];
1004
+ /** Copayer ID of the sender */
1005
+ copayerId: string;
906
1006
  }): Promise<void> {
907
- const { id, signature } = params;
1007
+ const { id, signature, copayerId } = params;
908
1008
  if (!signature) {
909
1009
  throw Errors.TSS_NO_FINAL_SIGNATURE;
910
1010
  }
@@ -917,6 +1017,10 @@ class TssSignClass {
917
1017
  if (!session) {
918
1018
  throw Errors.TSS_SESSION_NOT_FOUND;
919
1019
  }
1020
+ const party = session.participants.find(p => p.copayerId === copayerId);
1021
+ if (!party) {
1022
+ throw Errors.TSS_NON_PARTICIPANT;
1023
+ }
920
1024
 
921
1025
  const verifySig = (sig1, sig2) => {
922
1026
  return sig1.r === sig2.r &&
@@ -39,6 +39,13 @@ describe('BlockChain explorer', function() {
39
39
  exp2.should.respondTo('initSocket');
40
40
  exp2.should.respondTo('register');
41
41
  exp2.should.respondTo('addAddresses');
42
+
43
+ for (const [network, apiNetwork] of [['livenet', 'mainnet'], ['testnet', 'testnet']]) {
44
+ const opts = { coin: 'arc', network };
45
+ BlockChainExplorer(opts).baseUrl.should.equal(`https://api-eth.bitcore.io/api/ARC/${apiNetwork}`);
46
+ BlockChainExplorer({ ...opts, url: 'http://localhost:3000' })
47
+ .baseUrl.should.equal(`http://localhost:3000/api/ARC/${apiNetwork}`);
48
+ }
42
49
  });
43
50
  it('should fail on unsupported provider', function() {
44
51
  try {