@bitpay-labs/bitcore-wallet-service 11.11.2 → 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 (63) hide show
  1. package/package.json +2 -2
  2. package/src/lib/common/constants.ts +1 -1
  3. package/src/lib/common/utils.ts +18 -1
  4. package/src/lib/model/tsskeygen.ts +16 -0
  5. package/src/lib/routes/maintenance.ts +11 -2
  6. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  7. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  8. package/src/lib/routes/services.ts +7 -1
  9. package/src/lib/routes/tss.ts +33 -30
  10. package/src/lib/server.ts +62 -17
  11. package/src/lib/storage.ts +39 -0
  12. package/src/lib/tss.ts +111 -7
  13. package/test/expressapp.test.ts +99 -0
  14. package/test/integration/server.test.ts +53 -1
  15. package/test/integration/tss.test.ts +428 -0
  16. package/ts_build/package.json +4 -4
  17. package/ts_build/src/lib/common/constants.d.ts +1 -1
  18. package/ts_build/src/lib/common/constants.js +1 -1
  19. package/ts_build/src/lib/common/index.d.ts +2 -1
  20. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  21. package/ts_build/src/lib/common/utils.d.ts +1 -0
  22. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  23. package/ts_build/src/lib/common/utils.js +16 -1
  24. package/ts_build/src/lib/common/utils.js.map +1 -1
  25. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  26. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  27. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  28. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  29. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  30. package/ts_build/src/lib/routes/maintenance.js +12 -4
  31. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  32. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  33. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  34. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  35. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  36. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  37. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  38. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  39. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  40. package/ts_build/src/lib/routes/services.js +6 -1
  41. package/ts_build/src/lib/routes/services.js.map +1 -1
  42. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  43. package/ts_build/src/lib/routes/tss.js +32 -30
  44. package/ts_build/src/lib/routes/tss.js.map +1 -1
  45. package/ts_build/src/lib/server.d.ts +23 -2
  46. package/ts_build/src/lib/server.d.ts.map +1 -1
  47. package/ts_build/src/lib/server.js +20 -3
  48. package/ts_build/src/lib/server.js.map +1 -1
  49. package/ts_build/src/lib/storage.d.ts +9 -0
  50. package/ts_build/src/lib/storage.d.ts.map +1 -1
  51. package/ts_build/src/lib/storage.js +26 -0
  52. package/ts_build/src/lib/storage.js.map +1 -1
  53. package/ts_build/src/lib/tss.d.ts +12 -0
  54. package/ts_build/src/lib/tss.d.ts.map +1 -1
  55. package/ts_build/src/lib/tss.js +64 -5
  56. package/ts_build/src/lib/tss.js.map +1 -1
  57. package/ts_build/test/expressapp.test.js +90 -0
  58. package/ts_build/test/expressapp.test.js.map +1 -1
  59. package/ts_build/test/integration/server.test.js +47 -0
  60. package/ts_build/test/integration/server.test.js.map +1 -1
  61. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  62. package/ts_build/test/integration/tss.test.js +344 -0
  63. package/ts_build/test/integration/tss.test.js.map +1 -1
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.2",
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",
@@ -107,5 +107,5 @@
107
107
  "email": "kjoseph@bitpay.com"
108
108
  }
109
109
  ],
110
- "gitHead": "1a044f2c984f6856f569981eebd91b051ab130d8"
110
+ "gitHead": "154120b032bb0cd42bad2559c0ef81873a8c7e8c"
111
111
  }
@@ -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
@@ -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);
package/src/lib/server.ts CHANGED
@@ -604,6 +604,7 @@ export class WalletService implements IWalletService {
604
604
  * @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
605
  * threshold insead of on-chain multisig. Otherwise, n > 1 and a non-multisig chain will default to TSS_KEYGEN_SCHEME_VERSION
606
606
  * @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.
607
+ * @param {string} [opts.tssKeygenCopayerId] The ID of the copayer creating the wallet. Required for TSS version >= 1.3.
607
608
  */
608
609
  async createWallet(opts, cb) {
609
610
  let pubKey;
@@ -674,6 +675,17 @@ export class WalletService implements IWalletService {
674
675
  return cb(new ClientError('Invalid TSS version'));
675
676
  }
676
677
 
678
+ if (opts.tssVersion >= 1.3) {
679
+ if (!opts.tssKeygenCopayerId) {
680
+ return cb(new ClientError('Copayer ID is required for TSS version >= 1.3'));
681
+ }
682
+ // This is only a sanity check to prevent problems down the line. If this is somehow
683
+ // spoofed, there are more strict authorization checks later in the wallet creation flow.
684
+ if (keySession.participants.indexOf(opts.tssKeygenCopayerId) !== 0) {
685
+ return cb(new ClientError('Only the TSS session leader can create a wallet'));
686
+ }
687
+ }
688
+
677
689
  // TSS wallets behave like a single-sig
678
690
  opts.m = 1;
679
691
  opts.n = 1;
@@ -1038,7 +1050,11 @@ export class WalletService implements IWalletService {
1038
1050
  this._notify(type, data, {}, cb);
1039
1051
  }
1040
1052
 
1041
- _addCopayerToWallet(wallet: Wallet, opts, cb) {
1053
+ _addCopayerToWallet(
1054
+ wallet: Wallet,
1055
+ opts: JoinWalletOpts,
1056
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1057
+ ) {
1042
1058
  const copayer = Copayer.create({
1043
1059
  coin: wallet.coin,
1044
1060
  chain: wallet.chain, // chain === coin for stored clients
@@ -1194,20 +1210,11 @@ export class WalletService implements IWalletService {
1194
1210
 
1195
1211
  /**
1196
1212
  * 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
1213
  */
1210
- joinWallet(opts, cb) {
1214
+ joinWallet(
1215
+ opts: JoinWalletOpts,
1216
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1217
+ ) {
1211
1218
  // xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
1212
1219
  // fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
1213
1220
  // end-to-end identity protocol rather than a local identity fallback.
@@ -1226,6 +1233,9 @@ export class WalletService implements IWalletService {
1226
1233
  opts.chain = opts.coin; // chain === coin for stored clients
1227
1234
  }
1228
1235
  if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
1236
+ if (opts.tssKeygenChain != null && !Utils.checkValueInCollection(opts.tssKeygenChain, Constants.CHAINS)) {
1237
+ return cb(new ClientError('Invalid TSS keygen chain'));
1238
+ }
1229
1239
 
1230
1240
  // xPubKey is parsed and validated for every join; ancillary fields never suppress it.
1231
1241
  // A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
@@ -1286,8 +1296,16 @@ export class WalletService implements IWalletService {
1286
1296
 
1287
1297
  if (wallet.tssKeyId) {
1288
1298
  const keySession = await storage.fetchTssKeyGenSession({ id: wallet.tssKeyId });
1289
- const copayerId = Copayer.xPubToCopayerId(opts.chain, opts.xPubKey);
1290
- if (!keySession.participants.includes(copayerId)) {
1299
+ // A sibling wallet on another network (e.g. livenet -> testnet) has an xPubKey serialized for that network,
1300
+ // but the keygen session's copayer IDs were derived from the xPubKey serialized for the session's network.
1301
+ const idChain = opts.tssKeygenChain || opts.chain;
1302
+ const copayerIds = new Set([
1303
+ Copayer.xPubToCopayerId(idChain, opts.xPubKey), // this is probably a duplicate of one of the next two entries, but just in case.
1304
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'livenet')),
1305
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'testnet'))
1306
+ ]);
1307
+
1308
+ if (!keySession?.participants.some(id => copayerIds.has(id))) {
1291
1309
  return cb(Errors.TSS_NON_PARTICIPANT);
1292
1310
  }
1293
1311
  return this._addCopayerToWallet(wallet, opts, cb);
@@ -4177,7 +4195,7 @@ export class WalletService implements IWalletService {
4177
4195
  return cb(err);
4178
4196
  }
4179
4197
  wallet.beRegistered = true;
4180
- return this.storage.storeWallet(wallet, err => cb(err, true));
4198
+ return this.storage.storeWalletRegistration(wallet, err => cb(err, true));
4181
4199
  });
4182
4200
  }
4183
4201
 
@@ -5838,3 +5856,30 @@ export const UPGRADES = {
5838
5856
  } as const;
5839
5857
 
5840
5858
  type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
5859
+
5860
+ export interface JoinWalletOpts {
5861
+ /** The wallet id. */
5862
+ walletId: string;
5863
+ /** The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5864
+ coin?: string;
5865
+ /** The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5866
+ chain?: string;
5867
+ /** The copayer name. */
5868
+ name: string;
5869
+ /** Extended Public Key for this copayer */
5870
+ xPubKey: string;
5871
+ /** Public key from a hardware device for this copayer */
5872
+ hardwareSourcePublicKey?: string;
5873
+ /** Public key from the client for this wallet */
5874
+ clientDerivedPublicKey?: string;
5875
+ /** Public Key used to check requests from this copayer. */
5876
+ requestPubKey: string;
5877
+ /** Signature proving knowledge of the wallet secret for this copayer. */
5878
+ copayerSignature: string;
5879
+ /** Custom data for this copayer. */
5880
+ customData?: string;
5881
+ /** Simulate the action but do not change server state. */
5882
+ dryRun?: boolean;
5883
+ /** The chain used for TSS key generation. */
5884
+ tssKeygenChain?: string;
5885
+ };
@@ -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) {