@bitpay-labs/bitcore-wallet-service 11.10.6 → 11.10.8

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 (103) hide show
  1. package/package.json +3 -3
  2. package/src/externalservices/coingecko.ts +24 -10
  3. package/src/externalservices/moonpay.ts +23 -1
  4. package/src/externalservices/transak.ts +25 -3
  5. package/src/lib/chain/doge/index.ts +1 -1
  6. package/src/lib/chain/eth/abi-invoice.ts +139 -90
  7. package/src/lib/common/constants.ts +5 -2
  8. package/src/lib/common/defaults.ts +11 -2
  9. package/src/lib/errors/errordefinitions.ts +6 -0
  10. package/src/lib/model/copayer.ts +1 -0
  11. package/src/lib/model/tsskeygen.ts +33 -4
  12. package/src/lib/model/tsssign.ts +34 -3
  13. package/src/lib/pushnotificationsservice.ts +3 -1
  14. package/src/lib/routes/middleware/authTssRequest.ts +10 -4
  15. package/src/lib/routes/middleware/verifyTssMessage.ts +13 -8
  16. package/src/lib/routes/tss.ts +11 -4
  17. package/src/lib/server.ts +54 -25
  18. package/src/lib/storage.ts +24 -1
  19. package/src/lib/tss.ts +232 -30
  20. package/src/types/externalservices.d.ts +29 -1
  21. package/test/integration/externalservices/coinGecko.test.ts +71 -7
  22. package/test/integration/externalservices/transak.test.ts +84 -0
  23. package/test/integration/server.test.ts +780 -1
  24. package/test/integration/tss.test.ts +275 -0
  25. package/test/model/copayer.test.ts +128 -0
  26. package/test/storage.test.ts +91 -0
  27. package/ts_build/package.json +1 -1
  28. package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
  29. package/ts_build/src/externalservices/coingecko.js +14 -8
  30. package/ts_build/src/externalservices/coingecko.js.map +1 -1
  31. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  32. package/ts_build/src/externalservices/moonpay.js +38 -2
  33. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  34. package/ts_build/src/externalservices/transak.d.ts +1 -0
  35. package/ts_build/src/externalservices/transak.d.ts.map +1 -1
  36. package/ts_build/src/externalservices/transak.js +15 -3
  37. package/ts_build/src/externalservices/transak.js.map +1 -1
  38. package/ts_build/src/lib/chain/doge/index.js +1 -1
  39. package/ts_build/src/lib/chain/doge/index.js.map +1 -1
  40. package/ts_build/src/lib/chain/eth/abi-invoice.d.ts +254 -41
  41. package/ts_build/src/lib/chain/eth/abi-invoice.d.ts.map +1 -1
  42. package/ts_build/src/lib/chain/eth/abi-invoice.js +138 -89
  43. package/ts_build/src/lib/chain/eth/abi-invoice.js.map +1 -1
  44. package/ts_build/src/lib/common/constants.d.ts +4 -2
  45. package/ts_build/src/lib/common/constants.d.ts.map +1 -1
  46. package/ts_build/src/lib/common/constants.js +4 -2
  47. package/ts_build/src/lib/common/constants.js.map +1 -1
  48. package/ts_build/src/lib/common/defaults.d.ts +4 -2
  49. package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
  50. package/ts_build/src/lib/common/defaults.js +4 -2
  51. package/ts_build/src/lib/common/defaults.js.map +1 -1
  52. package/ts_build/src/lib/common/index.d.ts +8 -4
  53. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  54. package/ts_build/src/lib/errors/errordefinitions.d.ts +3 -0
  55. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  56. package/ts_build/src/lib/errors/errordefinitions.js +3 -0
  57. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  58. package/ts_build/src/lib/model/copayer.d.ts.map +1 -1
  59. package/ts_build/src/lib/model/copayer.js +1 -0
  60. package/ts_build/src/lib/model/copayer.js.map +1 -1
  61. package/ts_build/src/lib/model/tsskeygen.d.ts +3 -0
  62. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  63. package/ts_build/src/lib/model/tsskeygen.js +12 -3
  64. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  65. package/ts_build/src/lib/model/tsssign.d.ts +7 -0
  66. package/ts_build/src/lib/model/tsssign.d.ts.map +1 -1
  67. package/ts_build/src/lib/model/tsssign.js +22 -2
  68. package/ts_build/src/lib/model/tsssign.js.map +1 -1
  69. package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
  70. package/ts_build/src/lib/pushnotificationsservice.js +3 -1
  71. package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
  72. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  73. package/ts_build/src/lib/routes/middleware/authTssRequest.js +8 -2
  74. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  75. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  76. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +11 -6
  77. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  78. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  79. package/ts_build/src/lib/routes/tss.js +4 -4
  80. package/ts_build/src/lib/routes/tss.js.map +1 -1
  81. package/ts_build/src/lib/server.d.ts.map +1 -1
  82. package/ts_build/src/lib/server.js +34 -25
  83. package/ts_build/src/lib/server.js.map +1 -1
  84. package/ts_build/src/lib/storage.d.ts +4 -0
  85. package/ts_build/src/lib/storage.d.ts.map +1 -1
  86. package/ts_build/src/lib/storage.js +18 -1
  87. package/ts_build/src/lib/storage.js.map +1 -1
  88. package/ts_build/src/lib/tss.d.ts +4 -0
  89. package/ts_build/src/lib/tss.d.ts.map +1 -1
  90. package/ts_build/src/lib/tss.js +74 -17
  91. package/ts_build/src/lib/tss.js.map +1 -1
  92. package/ts_build/test/integration/externalservices/coinGecko.test.js +62 -8
  93. package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
  94. package/ts_build/test/integration/externalservices/transak.test.js +78 -0
  95. package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
  96. package/ts_build/test/integration/server.test.js +640 -0
  97. package/ts_build/test/integration/server.test.js.map +1 -1
  98. package/ts_build/test/integration/tss.test.js +245 -0
  99. package/ts_build/test/integration/tss.test.js.map +1 -1
  100. package/ts_build/test/model/copayer.test.js +91 -0
  101. package/ts_build/test/model/copayer.test.js.map +1 -1
  102. package/ts_build/test/storage.test.js +73 -0
  103. package/ts_build/test/storage.test.js.map +1 -1
@@ -54,6 +54,7 @@ export class Copayer {
54
54
  isMarketingStaff?: boolean;
55
55
 
56
56
  static xPubToCopayerId(coin, xpub) {
57
+ $.checkArgument(xpub, 'Missing xPubKey for copayer id derivation');
57
58
  const str = coin == Defaults.COIN ? xpub : coin + xpub;
58
59
  const hash = sjcl.hash.sha256.hash(str);
59
60
  return sjcl.codec.hex.fromBits(hash);
@@ -103,14 +103,36 @@ export class TssKeyGenModel implements ITssKeyGenModel {
103
103
  __v: number;
104
104
 
105
105
 
106
- static create(params: { id: string; message: ITssKeyMessageObject; n: number; copayerId: string; passwordHash?: string }): TssKeyGenModel {
107
- const { id, message, n, copayerId, passwordHash } = params;
106
+ /**
107
+ * Create a new TssKeyGenModel instance. This is used to create a new TSS keygen session.
108
+ */
109
+ static create(params: {
110
+ /** Session ID */
111
+ id: string;
112
+ /** Initial broadcast message from party 0 */
113
+ message: ITssKeyMessageObject;
114
+ /** Number of key participants */
115
+ n: number;
116
+ /** Copayer ID of party 0 */
117
+ copayerId: string;
118
+ /** Password hash for joining the session */
119
+ passwordHash?: string;
120
+ /** TSS keygen version */
121
+ version: number;
122
+ /** Time limit for the session in minutes, after which the session expires */
123
+ timeLimit?: number;
124
+ }): TssKeyGenModel {
125
+ const { id, message, n, copayerId, passwordHash, version } = params;
108
126
  const { partyId } = message;
127
+ const timeLimit = parseFloat(params.timeLimit?.toString());
109
128
  $.checkArgument(partyId === 0, 'Key generation session must be started by partyId 0');
129
+ $.checkArgument(!timeLimit || (Number.isFinite(timeLimit) && timeLimit > 0), 'Time limit must be a positive number');
130
+
131
+ const ONE_MINUTE = 60 * 1000;
110
132
 
111
133
  const x = new TssKeyGenModel();
112
134
  x.id = id;
113
- x.schemeVersion = Defaults.TSS_KEYGEN_SCHEME_VERSION;
135
+ x.schemeVersion = version || Defaults.TSS_KEYGEN_SCHEME_VERSION;
114
136
  x.n = n;
115
137
  x.participants = new Array(n);
116
138
  x.participants[partyId] = copayerId;
@@ -121,7 +143,7 @@ export class TssKeyGenModel implements ITssKeyGenModel {
121
143
  x.joinPassword = passwordHash;
122
144
  x.keyShares = new Array(n);
123
145
  x.createdOn = Date.now();
124
- x.timeLimit = null; // TODO - add a session time limit
146
+ x.timeLimit = Math.min(timeLimit || Defaults.TSS_KEYGEN_TIME_LIMIT, 60) * ONE_MINUTE; // capped at 60 minutes
125
147
  x.__v = 0;
126
148
  return x;
127
149
  }
@@ -151,4 +173,11 @@ export class TssKeyGenModel implements ITssKeyGenModel {
151
173
  }
152
174
  return mostRecentRound;
153
175
  }
176
+
177
+ isExpired(): boolean {
178
+ if (!this.timeLimit) {
179
+ return false;
180
+ }
181
+ return Date.now() > this.createdOn + this.timeLimit;
182
+ }
154
183
  }
@@ -1,5 +1,8 @@
1
+ import { singleton } from 'preconditions';
1
2
  import { Common } from '../common';
2
3
 
4
+ const $ = singleton();
5
+
3
6
  const { Defaults } = Common;
4
7
 
5
8
 
@@ -60,6 +63,14 @@ export interface ITssSigGenModel {
60
63
  fromPartyId: number;
61
64
  messages: ITssSigMessageObject;
62
65
  }>>;
66
+ /**
67
+ * Timestamp the session was created
68
+ */
69
+ createdOn: number;
70
+ /**
71
+ * Session expires after this many milliseconds.
72
+ */
73
+ timeLimit?: number;
63
74
  /**
64
75
  * The signature generated as the result of the TSS signature generation process.
65
76
  * The signature can be generated along a derivation path, which produces a pubKey that's
@@ -90,16 +101,27 @@ export class TssSigGenModel implements ITssSigGenModel {
90
101
  }>>;
91
102
  signature?: ITssSigMessageObject['signature'];
92
103
  schemeVersion: number;
104
+ createdOn: number;
105
+ timeLimit?: number;
93
106
  __v: number;
94
107
 
95
108
 
96
- static create(params: { id: string; message: ITssSigMessageObject; m: number; copayerId: string }): TssSigGenModel {
97
- const { id, message, m, copayerId } = params;
109
+ static create(params: { id: string; message: ITssSigMessageObject; m: number; copayerId: string; version: number; timeLimit?: number }): TssSigGenModel {
110
+ const { id, message, m, copayerId, version } = params;
98
111
  const { partyId } = message;
112
+ const timeLimit = parseFloat(params.timeLimit?.toString());
113
+ $.checkArgument(!timeLimit || (Number.isFinite(timeLimit) && timeLimit > 0), 'Time limit must be a positive number');
114
+
115
+ const MAX_TIME_LIMIT = 7 * 24 * 60; // 7 days in minutes
116
+ const ONE_MINUTE = 60 * 1000;
99
117
 
100
118
  const x = new TssSigGenModel();
101
119
  x.id = id;
102
- x.schemeVersion = Defaults.TSS_SIGGEN_SCHEME_VERSION;
120
+ x.schemeVersion = version || Defaults.TSS_SIGGEN_SCHEME_VERSION;
121
+ x.createdOn = Date.now();
122
+ x.timeLimit = (timeLimit === 0
123
+ ? MAX_TIME_LIMIT
124
+ : Math.min(timeLimit || Defaults.TSS_SIGGEN_TIME_LIMIT, MAX_TIME_LIMIT)) * ONE_MINUTE;
103
125
  x.m = m;
104
126
  x.participants = [{
105
127
  partyId,
@@ -117,6 +139,8 @@ export class TssSigGenModel implements ITssSigGenModel {
117
139
  const x = new TssSigGenModel();
118
140
  x.id = obj.id;
119
141
  x.schemeVersion = obj.schemeVersion;
142
+ x.createdOn = obj.createdOn;
143
+ x.timeLimit = obj.timeLimit;
120
144
  x.m = obj.m;
121
145
  x.participants = obj.participants;
122
146
  x.rounds = obj.rounds;
@@ -133,4 +157,11 @@ export class TssSigGenModel implements ITssSigGenModel {
133
157
  }
134
158
  return mostRecentRound;
135
159
  }
160
+
161
+ isExpired(): boolean {
162
+ if (!this.timeLimit) {
163
+ return false;
164
+ }
165
+ return Date.now() > this.createdOn + this.timeLimit;
166
+ }
136
167
  }
@@ -224,6 +224,7 @@ export class PushNotificationsService {
224
224
  const chain = notification?.data?.chain || notification?.data?.coin;
225
225
  const coin = chain; // backwards compatibility
226
226
  const network = notification?.data?.network ? Utils.getNetworkName(chain, notification.data.network) : null;
227
+ const txid = typeof notification?.data?.txid === 'string' ? notification.data.txid : null;
227
228
 
228
229
  if (sub.token) {
229
230
  notificationData = {
@@ -267,10 +268,11 @@ export class PushNotificationsService {
267
268
  network,
268
269
  tokenAddress,
269
270
  multisigContractAddress,
271
+ txid,
270
272
  title,
271
273
  body
272
274
  };
273
- const custom_uri = `bitpay://wallet?walletId=${walletId}&tokenAddress=${tokenAddress}&multisigContractAddress=${multisigContractAddress}&copayerId=${copayerId}&coin=${coin}&chain=${chain}&network=${network}&notification_type=${notification_type}&title=${title}&body=${body}`;
275
+ const custom_uri = `bitpay://wallet?walletId=${walletId}&tokenAddress=${tokenAddress}&multisigContractAddress=${multisigContractAddress}&copayerId=${copayerId}&coin=${coin}&chain=${chain}&network=${network}&notification_type=${notification_type}&txid=${txid}&title=${title}&body=${body}`;
274
276
  notificationData = {
275
277
  external_user_ids: [sub.externalUserId],
276
278
  messages: {
@@ -5,7 +5,8 @@ import { Errors } from '../../errors/errordefinitions';
5
5
  import { WalletService, checkRequired } from '../../server';
6
6
  import { error } from '../helpers';
7
7
  import { getCredentials, getMessage } from './authRequest';
8
-
8
+ import type { TssKeyGenModel } from '../../model/tsskeygen';
9
+ import type { TssSigGenModel } from '../../model/tsssign';
9
10
 
10
11
  /**
11
12
  * Middleware to authenticate TSS requests and attach the TSS session to the request.
@@ -13,7 +14,6 @@ import { getCredentials, getMessage } from './authRequest';
13
14
  * and retrieves the TSS session based on the request path.
14
15
  */
15
16
  export function authTssRequest(): express.RequestHandler {
16
-
17
17
  return async function(req, res, next) {
18
18
  try {
19
19
  const storage = WalletService.getStorage();
@@ -28,7 +28,8 @@ export function authTssRequest(): express.RequestHandler {
28
28
  } = credentials;
29
29
 
30
30
  const { id } = req.params as { [key: string]: string };
31
- let session;
31
+
32
+ let session: TssKeyGenModel | TssSigGenModel | null = null;
32
33
  let partyId = null;
33
34
  let pubKey = null;
34
35
  if (req.path.includes('/tss/keygen/')) {
@@ -38,7 +39,9 @@ export function authTssRequest(): express.RequestHandler {
38
39
  } else if (req.path.includes('/tss/sign/')) {
39
40
  session = await storage.fetchTssSigSession({ id });
40
41
  partyId = session?.participants.find(p => p.copayerId === copayerId)?.partyId;
41
- pubKey = partyId == null ? null : session.rounds[0].find(r => r.fromPartyId === partyId).messages.publicKey;
42
+ pubKey = partyId == null ? null : session?.rounds[0]?.find(r => r.fromPartyId === partyId)?.messages.publicKey;
43
+ } else {
44
+ throw new Error('Unknown TSS request path: ' + req.path);
42
45
  }
43
46
 
44
47
  if (!session) {
@@ -47,6 +50,9 @@ export function authTssRequest(): express.RequestHandler {
47
50
  if (!pubKey) {
48
51
  throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
49
52
  }
53
+ if (session.isExpired()) {
54
+ throw Errors.TSS_SESSION_EXPIRED;
55
+ }
50
56
 
51
57
  const message = getMessage(req);
52
58
  const isValid = !!Utils.verifyMessage(message, signature, pubKey);
@@ -1,7 +1,9 @@
1
1
  import { utils as tssUtils } from '@bitpay-labs/bitcore-tss';
2
+ import { ClientError } from '../../errors/clienterror';
2
3
  import { Errors } from '../../errors/errordefinitions';
3
- import { ITssKeyMessageObject } from '../../model/tsskeygen';
4
- import { ITssSigMessageObject } from '../../model/tsssign';
4
+ import { error } from '../helpers';
5
+ import type { ITssKeyMessageObject } from '../../model/tsskeygen';
6
+ import type { ITssSigMessageObject } from '../../model/tsssign';
5
7
 
6
8
 
7
9
  export async function verifyTssMessage(req, res, next) {
@@ -9,16 +11,16 @@ export async function verifyTssMessage(req, res, next) {
9
11
  const { message }: { message: ITssKeyMessageObject | ITssSigMessageObject } = req.body;
10
12
  const { publicKey } = message || {};
11
13
  if (!publicKey) {
12
- return res.status(400).send(Errors.TSS_PUBKEY_MISSING);
14
+ throw Errors.TSS_PUBKEY_MISSING;
13
15
  }
14
16
 
15
17
  if (!message.broadcastMessages?.length && !message.p2pMessages?.length) {
16
- return res.status(400).send(Errors.TSS_INVALID_MESSAGE);
18
+ throw Errors.TSS_INVALID_MESSAGE;
17
19
  }
18
20
 
19
21
  for (const m of message.broadcastMessages) {
20
22
  if (!tssUtils.verifySignedData(m.payload, publicKey)) {
21
- return res.status(400).send(Errors.TSS_INVALID_MESSAGE_SIG);
23
+ throw Errors.TSS_INVALID_MESSAGE_SIG;
22
24
  };
23
25
  }
24
26
  for (const m of message.p2pMessages) {
@@ -26,12 +28,15 @@ export async function verifyTssMessage(req, res, next) {
26
28
  // `m.payload.signature` is for the unencrypted message.
27
29
  // Only the recipients can verify on the client side.
28
30
  if (!m.payload || !m.payload.encryptedMessage || !m.payload.signature) {
29
- return res.status(400).send(Errors.TSS_INVALID_MESSAGE_SIG);
31
+ throw Errors.TSS_INVALID_MESSAGE_SIG;
30
32
  }
31
33
  }
32
34
  return next();
33
- } catch {
34
- return res.status(400).send(Errors.TSS_INVALID_MESSAGE);
35
+ } catch (err) {
36
+ if (err instanceof ClientError) {
37
+ return error.returnError(err, res, req);
38
+ }
39
+ return error.returnError(Errors.TSS_INVALID_MESSAGE, res, req);
35
40
  }
36
41
  };
37
42
 
@@ -21,12 +21,15 @@ export class TssRouter {
21
21
  const { returnError, opts } = params;
22
22
  const router = express.Router();
23
23
 
24
+ /** Key generation methods */
25
+
24
26
  router.post('/v1/tss/keygen/:id', createWalletLimiter(opts), verifyTssMessage, async function(req, res) {
25
27
  try {
26
28
  const id = req.params.id;
27
- const { message, n, password } = req.body;
29
+ // version was not given by client until 1.1, so fallback to 1.0
30
+ const { message, n, password, version = 1.0, timeLimit } = req.body;
28
31
  const copayerId = req.headers['x-identity'];
29
- await TssKeyGen.processMessage({ id, message, n, password, copayerId });
32
+ await TssKeyGen.processMessage({ id, message, n, password, copayerId, version, timeLimit });
30
33
  return res.send();
31
34
  } catch (err) {
32
35
  return returnError(err ?? 'unknown', res, req);
@@ -84,12 +87,16 @@ export class TssRouter {
84
87
  }
85
88
  });
86
89
 
90
+
91
+ /** Signature Methods */
92
+
87
93
  router.post('/v1/tss/sign/:id', authRequest(), verifyTssMessage, async function(req, res) {
88
94
  try {
89
95
  const id = req.params.id;
90
- const { message, m } = req.body;
96
+ // version was not given by client until 1.1, so fallback to 1.0
97
+ const { message, m, version = 1.0, timeLimit } = req.body;
91
98
  const copayerId = req.headers['x-identity'];
92
- await TssSign.processMessage({ id, message, m, copayerId });
99
+ await TssSign.processMessage({ id, message, m, copayerId, version, timeLimit });
93
100
  return res.send();
94
101
  } catch (err) {
95
102
  return returnError(err ?? 'unknown', res, req);
package/src/lib/server.ts CHANGED
@@ -668,15 +668,17 @@ export class WalletService implements IWalletService {
668
668
  }
669
669
 
670
670
  if (opts.tssKeyId) {
671
- opts.tssVersion = opts.tssVersion || Defaults.TSS_KEYGEN_SCHEME_VERSION;
672
- if (!(opts.tssVersion > 0 && opts.tssVersion <= Constants.TSS_KEYGEN_SCHEME_VERSION_MAX)) {
673
- return cb(new ClientError('Invalid TSS version'));
674
- }
675
-
676
671
  const keySession = await storage.fetchTssKeyGenSession({ id: opts.tssKeyId });
677
672
  if (!keySession || !keySession.sharedPublicKey) {
673
+ // TODO: support importing a TSS key that was generated outside of this BWS instance.
678
674
  return cb(new ClientError('Invalid TSS key session id'));
679
675
  }
676
+
677
+ opts.tssVersion = opts.tssVersion || keySession.schemeVersion || Defaults.TSS_KEYGEN_SCHEME_VERSION;
678
+ if (!(opts.tssVersion >= Constants.TSS_KEYGEN_SCHEME_VERSION_MIN && opts.tssVersion <= Constants.TSS_KEYGEN_SCHEME_VERSION_MAX)) {
679
+ return cb(new ClientError('Invalid TSS version'));
680
+ }
681
+
680
682
  // TSS wallets behave like a single-sig
681
683
  opts.m = 1;
682
684
  opts.n = 1;
@@ -1144,9 +1146,13 @@ export class WalletService implements IWalletService {
1144
1146
  if (err) return cb(err);
1145
1147
  if (!wallet) return cb(Errors.NOT_AUTHORIZED);
1146
1148
 
1147
- const xPubKey = wallet.copayers.find(c => c.id === opts.copayerId).xPubKey;
1149
+ const target = wallet.copayers.find(c => c.id === opts.copayerId);
1150
+ if (!target?.xPubKey) return cb(Errors.NOT_AUTHORIZED);
1148
1151
 
1149
- if (!this._verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) {
1152
+ try {
1153
+ const isValid = this._verifyRequestPubKey(opts.requestPubKey, opts.signature, target.xPubKey);
1154
+ if (!isValid) return cb(Errors.NOT_AUTHORIZED);
1155
+ } catch (e) {
1150
1156
  return cb(Errors.NOT_AUTHORIZED);
1151
1157
  }
1152
1158
 
@@ -1207,7 +1213,17 @@ export class WalletService implements IWalletService {
1207
1213
  * @param {boolean} [opts.dryRun] Simulate the action but do not change server state.
1208
1214
  */
1209
1215
  joinWallet(opts, cb) {
1210
- if (!checkRequired(opts, ['walletId', 'name', 'requestPubKey', 'copayerSignature'], cb)) return;
1216
+ // xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
1217
+ // fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
1218
+ // end-to-end identity protocol rather than a local identity fallback.
1219
+ // SECURITY: xPubKey is required for every join, including hardwareSourcePublicKey/
1220
+ // clientDerivedPublicKey ones. Copayer.xPubToCopayerId() (called below both for the TSS
1221
+ // participant check and inside Copayer.create()) now throws on a missing xpub for every
1222
+ // coin (see its own guard), so an xPubKey-less join can no longer derive a copayer id at
1223
+ // all. bitcore-wallet-client's only join path (_doJoinWallet) always sends a real
1224
+ // xPubKey regardless of these two fields, so this doesn't restrict any flow this repo's
1225
+ // own client exercises - see the TSS-participant comment below for how that was confirmed.
1226
+ if (!checkRequired(opts, ['walletId', 'name', 'requestPubKey', 'copayerSignature', 'xPubKey'], cb)) return;
1211
1227
  if (!opts.name) return cb(new ClientError('Invalid copayer name'));
1212
1228
 
1213
1229
  opts.coin = opts.coin || Defaults.COIN;
@@ -1216,17 +1232,17 @@ export class WalletService implements IWalletService {
1216
1232
  }
1217
1233
  if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
1218
1234
 
1235
+ // xPubKey is parsed and validated for every join; ancillary fields never suppress it.
1236
+ // A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
1237
+ // or opts.clientDerivedPublicKey to circumvent this requirement
1219
1238
  let xPubKey;
1220
- if (!opts.hardwareSourcePublicKey && !opts.clientDerivedPublicKey) {
1221
- if (!checkRequired(opts, ['xPubKey'], cb)) return;
1222
- try {
1223
- xPubKey = Bitcore_[opts.chain].HDPublicKey(opts.xPubKey);
1224
- } catch {
1225
- return cb(new ClientError('Invalid extended public key'));
1226
- }
1227
- if (xPubKey.network == null) {
1228
- return cb(new ClientError('Invalid extended public key'));
1229
- }
1239
+ try {
1240
+ xPubKey = Bitcore_[opts.chain].HDPublicKey(opts.xPubKey);
1241
+ } catch {
1242
+ return cb(new ClientError('Invalid extended public key'));
1243
+ }
1244
+ if (xPubKey.network == null) {
1245
+ return cb(new ClientError('Invalid extended public key'));
1230
1246
  }
1231
1247
 
1232
1248
  this.walletId = opts.walletId;
@@ -1235,10 +1251,9 @@ export class WalletService implements IWalletService {
1235
1251
  if (err) return cb(err);
1236
1252
  if (!wallet) return cb(Errors.WALLET_NOT_FOUND);
1237
1253
 
1238
- if ((opts.hardwareSourcePublicKey || opts.clientDerivedPublicKey) && !opts.tssKeyId) {
1239
- this._addCopayerToWallet(wallet, opts, cb);
1240
- return;
1241
- }
1254
+ // SECURITY: opts.hardwareSourcePublicKey or opts.clientDerivedPublicKey may not
1255
+ // circumvent the required checks below. A future update may enable either to replace xPubKey
1256
+ // That is currently not supported
1242
1257
 
1243
1258
  if (this._upgradeNeeded(UPGRADES.BCH_bwc_$lt_8_3_multisig, { chain: opts.chain, n: wallet.n })) {
1244
1259
  return cb(Errors.UPGRADE_NEEDED.withMessage('BWC clients < 8.3 are no longer supported for multisig BCH wallets.'));
@@ -2398,6 +2413,7 @@ export class WalletService implements IWalletService {
2398
2413
 
2399
2414
  this.getSendMaxInfo(
2400
2415
  {
2416
+ feeLevel: opts.feeLevel,
2401
2417
  feePerKb: opts.feePerKb,
2402
2418
  excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
2403
2419
  returnInputs: true,
@@ -3138,7 +3154,14 @@ export class WalletService implements IWalletService {
3138
3154
  * @returns {Object} txProposal
3139
3155
  */
3140
3156
  getTxByHash(opts, cb) {
3141
- this.storage.fetchTxByHash(opts.txid, (err, txp) => {
3157
+ if (!checkRequired(opts, 'txid', cb)) return;
3158
+
3159
+ // Scoped to this.walletId: the global storage.fetchTxByHash
3160
+ // would return any wallet's TxProposal for a known txid, disclosing a
3161
+ // foreign wallet's proposal data to any authenticated copayer. The
3162
+ // internal global consumers (BlockchainMonitor, getWalletFromIdentifier)
3163
+ // are unaffected; they keep calling storage.fetchTxByHash directly.
3164
+ this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3142
3165
  if (err) return cb(err);
3143
3166
  if (!txp) return cb(Errors.TX_NOT_FOUND);
3144
3167
 
@@ -3249,7 +3272,7 @@ export class WalletService implements IWalletService {
3249
3272
  {
3250
3273
  txProposalId: opts.txProposalId
3251
3274
  },
3252
- (err, txp) => {
3275
+ (err, txp: TxProposal) => {
3253
3276
  if (err) return cb(err);
3254
3277
 
3255
3278
  if (!txp.isPending()) return cb(Errors.TX_NOT_PENDING);
@@ -3257,7 +3280,13 @@ export class WalletService implements IWalletService {
3257
3280
  const deleteLockTime = this.getRemainingDeleteLockTime(txp);
3258
3281
  if (deleteLockTime > 0) return cb(Errors.TX_CANNOT_REMOVE);
3259
3282
 
3260
- this.storage.removeTx(this.walletId, txp.id, () => {
3283
+ this.storage.removeTx(this.walletId, txp.id, async () => {
3284
+ try {
3285
+ const inputPaths = Array.isArray(txp.inputPaths) && txp.inputPaths?.length ? txp.inputPaths : [txp.inputPaths || 'm/0/0']; // doesn't actually matter what's in the array
3286
+ await Promise.all(inputPaths.map((_, i) => this.storage.removeTssSigSession({ id: `${txp.id}:input${i}` })));
3287
+ } catch (err) {
3288
+ logger.warn('Error removing tss sig session for wallet %s txp %s: %o', this.walletId, txp.id, err);
3289
+ }
3261
3290
  this._notifyTxProposalAction('TxProposalRemoved', txp, cb);
3262
3291
  });
3263
3292
  }
@@ -360,6 +360,25 @@ export class Storage {
360
360
  );
361
361
  }
362
362
 
363
+ // Wallet-scoped counterpart to fetchTxByHash, used by the authenticated
364
+ // by-hash API read.
365
+ fetchTxByHashForWallet(walletId: string, hash, cb: (err?: any, tx?: TxProposal) => void) {
366
+ if (!this.db) return cb();
367
+
368
+ this.db.collection(collections.TXS).findOne(
369
+ {
370
+ walletId,
371
+ txid: hash
372
+ },
373
+ (err, result) => {
374
+ if (err) return cb(err);
375
+ if (!result) return cb();
376
+
377
+ return this._completeTxData(walletId, TxProposal.fromObj(result), cb);
378
+ }
379
+ );
380
+ }
381
+
363
382
  fetchLastTxs(walletId, creatorId, limit, cb) {
364
383
  this.db
365
384
  .collection(collections.TXS)
@@ -1910,7 +1929,7 @@ export class Storage {
1910
1929
 
1911
1930
  async storeTssSignature({ id, signature }: { id: string; signature: ITssSigMessageObject['signature'] }) {
1912
1931
  return this.db.collection(collections.TSS_SIGN).updateOne(
1913
- { id },
1932
+ { id, $or: [{ signature: null }, { signature: { $exists: false } }] },
1914
1933
  {
1915
1934
  $set: {
1916
1935
  signature
@@ -1920,4 +1939,8 @@ export class Storage {
1920
1939
  );
1921
1940
  }
1922
1941
 
1942
+ async removeTssSigSession({ id }: { id: string }) {
1943
+ return this.db.collection(collections.TSS_SIGN).deleteOne({ id }, { w: 1 });
1944
+ }
1945
+
1923
1946
  }