@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
package/src/lib/tss.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BitcoreLib } from '@bitpay-labs/crypto-wallet-core';
2
+ import { Constants } from './common/constants';
2
3
  import { Errors } from './errors/errordefinitions';
3
4
  import logger from './logger';
4
5
  import { ITssKeyMessageObject, TssKeyGenModel } from './model/tsskeygen';
@@ -7,9 +8,16 @@ import { WalletService, checkRequired } from './server';
7
8
  import { Storage } from './storage';
8
9
 
9
10
  class TssKeyGenClass {
11
+ /**
12
+ * Get messages for a given party in a TSS keygen session.
13
+ * Only returns messages if all other parties have sent their messages for the round.
14
+ */
10
15
  async getMessagesForParty(params: {
16
+ /** Session ID */
11
17
  id: string;
18
+ /** Round number */
12
19
  round: number;
20
+ /** Copayer ID of the requesting party */
13
21
  copayerId: string;
14
22
  }): Promise<{
15
23
  messages?: ITssKeyMessageObject[];
@@ -48,8 +56,26 @@ class TssKeyGenClass {
48
56
  return { messages, publicKey: session.sharedPublicKey, hasKeyBackup: !!session.keyShares?.[partyId] };
49
57
  }
50
58
 
51
- async processMessage(params: { id: string; message: ITssKeyMessageObject; n?: string | number; password?: string; copayerId: string }) {
52
- const { id, message, n, password, copayerId } = params;
59
+ /**
60
+ * Process a TSS keygen message. This will create a new session if the message is from party 0 and is for round 0.
61
+ */
62
+ async processMessage(params: {
63
+ /** Session ID */
64
+ id: string;
65
+ /** Submitted message to send to others */
66
+ message: ITssKeyMessageObject;
67
+ /** Number of participants (only required for the initial message from party 0) */
68
+ n?: string | number;
69
+ /** Password for the session */
70
+ password?: string;
71
+ /** Copayer ID of the submitting party */
72
+ copayerId: string;
73
+ /** TSS keygen version */
74
+ version: number;
75
+ /** Time limit for the session in minutes, after which the session expires. Only applies on session initialization */
76
+ timeLimit?: number;
77
+ }): Promise<void> {
78
+ const { id, message, n, password, copayerId, timeLimit } = params;
53
79
  if (!id || typeof id !== 'string') {
54
80
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid id provided: ' + id);
55
81
  }
@@ -57,14 +83,35 @@ class TssKeyGenClass {
57
83
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid copayerId provided: ' + copayerId);
58
84
  }
59
85
 
86
+ // version was not given by client until 1.1, so fallback to 1.0
87
+ const version = Number(params.version ?? 1.0);
88
+ if (!Number.isFinite(version)) {
89
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid version provided: ' + params.version);
90
+ }
91
+ if (version < Constants.TSS_KEYGEN_SCHEME_VERSION_MIN) {
92
+ throw Errors.UPGRADE_NEEDED;
93
+ }
94
+ if (version > Constants.TSS_KEYGEN_SCHEME_VERSION_MAX) {
95
+ throw Errors.UPGRADE_NEEDED.withMessage('TSS version too new: ' + version);
96
+ }
97
+
60
98
  const storage = WalletService.getStorage();
61
99
  let session = await storage.fetchTssKeyGenSession({ id });
62
100
 
63
101
  if (session) {
102
+ if (session.isExpired()) {
103
+ throw Errors.TSS_SESSION_EXPIRED;
104
+ }
105
+
64
106
  if (!this._isValidBroadcastMessage({ message }) && !this._isValidP2pMessage({ message })) {
65
107
  throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid message provided');
66
108
  }
67
109
 
110
+ if (session.schemeVersion != version) {
111
+ throw Errors.TSS_MISMATCH_VERSION.withMessage(`TSS version (${version}) does not match session version (${session.schemeVersion})`);
112
+ }
113
+
114
+
68
115
  if (!session.participants[message.partyId]) {
69
116
  if (!this._checkPassword({ session, password })) {
70
117
  throw Errors.TSS_INVALID_PASSWORD;
@@ -83,13 +130,21 @@ class TssKeyGenClass {
83
130
  if (!this._isValidBroadcastMessage({ message })) {
84
131
  throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid broadcast message provided');
85
132
  }
86
- await this._initSession({ id, message, n, password, storage, copayerId });
133
+ await this._initSession({ id, message, n, password, storage, copayerId, version, timeLimit });
87
134
  } else {
88
135
  throw Errors.TSS_SESSION_NOT_FOUND;
89
136
  }
90
137
  }
91
138
 
92
- private _checkPassword(params: { session: TssKeyGenModel; password: string }) {
139
+ /**
140
+ * Check the password for a TSS keygen session. Returns true if the password is valid or if no password is set for the session.
141
+ */
142
+ private _checkPassword(params: {
143
+ /** TSS keygen session fetched from BWS storage */
144
+ session: TssKeyGenModel;
145
+ /** Password to check */
146
+ password: string;
147
+ }): boolean {
93
148
  const { session, password } = params;
94
149
  if (!session.joinPassword) {
95
150
  return true;
@@ -101,6 +156,7 @@ class TssKeyGenClass {
101
156
  return session.joinPassword === passwordHash;
102
157
  }
103
158
 
159
+ /** Validate if the message is a valid broadcast message */
104
160
  private _isValidBroadcastMessage(params: { message: ITssKeyMessageObject }) {
105
161
  const { message } = params;
106
162
  return typeof message?.broadcastMessages?.[0]?.from === 'number' &&
@@ -108,6 +164,7 @@ class TssKeyGenClass {
108
164
  typeof message?.broadcastMessages?.[0]?.payload?.signature === 'string';
109
165
  }
110
166
 
167
+ /** Validate if the message is a valid P2P message */
111
168
  private _isValidP2pMessage(params: { message: ITssKeyMessageObject }) {
112
169
  const { message } = params;
113
170
  return typeof message?.p2pMessages?.[0]?.from === 'number' &&
@@ -117,15 +174,26 @@ class TssKeyGenClass {
117
174
  typeof message?.p2pMessages?.[0]?.commitment === 'string';
118
175
  }
119
176
 
177
+ /** Initialize a new TSS keygen session */
120
178
  private async _initSession(params: {
179
+ /** Session ID */
121
180
  id: string;
181
+ /** Broadcast message from party 0 */
122
182
  message: ITssKeyMessageObject;
183
+ /** Number of participants */
123
184
  n: number | string;
185
+ /** Password needed to join the session */
124
186
  password?: string;
187
+ /** TSS keygen version */
188
+ version: number;
189
+ /** BWS storage instance */
125
190
  storage: Storage;
191
+ /** Copayer ID of the submitting party (party 0) */
126
192
  copayerId: string;
193
+ /** Time limit for the session in minutes, after which the session expires */
194
+ timeLimit?: number;
127
195
  }) {
128
- const { id, message, password, storage, copayerId } = params;
196
+ const { id, message, password, storage, copayerId, version, timeLimit } = params;
129
197
  const n = parseInt(params.n as string);
130
198
  if (!n || n < 1) {
131
199
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid n provided: ' + n);
@@ -141,7 +209,9 @@ class TssKeyGenClass {
141
209
  message,
142
210
  n,
143
211
  copayerId,
144
- passwordHash
212
+ passwordHash,
213
+ version: version || 1.0, // Initial version of TSS keygen was 1.0, but BWC didn't pass it in
214
+ timeLimit
145
215
  });
146
216
  const result = await storage.storeTssKeyGenSession({ doc });
147
217
  if (!result.result.ok) {
@@ -150,7 +220,20 @@ class TssKeyGenClass {
150
220
  }
151
221
  }
152
222
 
153
- private async _pushMessage(params: { id: string; session: TssKeyGenModel; message: ITssKeyMessageObject; storage: Storage }) {
223
+ /**
224
+ * Push a TSS keygen message to the session.
225
+ * This will fail if the round is already complete or if the message is from a party that has already sent a message for the round.
226
+ */
227
+ private async _pushMessage(params: {
228
+ /** Session ID */
229
+ id: string;
230
+ /** TSS keygen session fetched from BWS storage */
231
+ session: TssKeyGenModel;
232
+ /** Message to push to the session */
233
+ message: ITssKeyMessageObject;
234
+ /** BWS storage instance */
235
+ storage: Storage;
236
+ }) {
154
237
  const { id, session, message, storage } = params;
155
238
  const { round } = message;
156
239
 
@@ -181,14 +264,15 @@ class TssKeyGenClass {
181
264
  }
182
265
  }
183
266
 
267
+ /** Save the generated public key to the TSS keygen session */
184
268
  async storeKey(params: {
185
269
  id: string;
186
270
  message: {
187
271
  publicKey: ITssKeyMessageObject['publicKey'];
188
- /** unused */
272
+ /** Unused - removed support for saving keychain */
189
273
  encryptedKeyChain: string;
190
274
  };
191
- /** unused - only needed for storing the encryptedKeyChain */
275
+ /** Unused - only needed for storing the encryptedKeyChain */
192
276
  copayerId: string;
193
277
  }) {
194
278
  const { id, message } = params;
@@ -229,9 +313,13 @@ class TssKeyGenClass {
229
313
  // }
230
314
  }
231
315
 
316
+ /** Store the BWS join secret for the TSS keygen session */
232
317
  async storeBwsJoinSecret(params: {
318
+ /** Session ID */
233
319
  id: string;
320
+ /** BWS join secret */
234
321
  secret: string;
322
+ /** Copayer ID of the submitting party (must be the session creator) */
235
323
  copayerId: string;
236
324
  }) {
237
325
  const { id, secret, copayerId } = params;
@@ -259,7 +347,13 @@ class TssKeyGenClass {
259
347
  }
260
348
  }
261
349
 
262
- async getBwsJoinSecret(params: { id: string; copayerId: string }) {
350
+ /** Get the BWS join secret for the TSS keygen session */
351
+ async getBwsJoinSecret(params: {
352
+ /** Session ID */
353
+ id: string;
354
+ /** Copayer ID of the requesting party */
355
+ copayerId: string;
356
+ }): Promise<string> {
263
357
  const { id, copayerId } = params;
264
358
  const storage = WalletService.getStorage();
265
359
  const session = await storage.fetchTssKeyGenSession({ id });
@@ -279,7 +373,18 @@ class TssKeyGenClass {
279
373
  export const TssKeyGen = new TssKeyGenClass();
280
374
 
281
375
  class TssSignClass {
282
- async getMessagesForParty(params: { id: string; round: number; copayerId: string }): Promise<{ messages?: ITssSigMessageObject[]; signature?: ITssSigMessageObject['signature']; participants?: string[] }> {
376
+ /**
377
+ * Get messages for a given party in a TSS signature session.
378
+ * Only returns messages if all other parties have sent their messages for the round.
379
+ */
380
+ async getMessagesForParty(params: {
381
+ /** Session ID */
382
+ id: string;
383
+ /** Round number */
384
+ round: number;
385
+ /** Copayer ID of the requesting party */
386
+ copayerId: string;
387
+ }): Promise<{ messages?: ITssSigMessageObject[]; signature?: ITssSigMessageObject['signature']; participants?: string[] }> {
283
388
  const { id, round, copayerId } = params;
284
389
 
285
390
  const storage = WalletService.getStorage();
@@ -312,8 +417,24 @@ class TssSignClass {
312
417
  return { participants };
313
418
  }
314
419
 
315
- async processMessage(params: { id: string; message: ITssSigMessageObject; m?: string | number; copayerId: string }) {
316
- const { id, message, m, copayerId } = params;
420
+ /**
421
+ * Process a TSS signature message. This will create a new session if the message is from party 0 and is for round 0.
422
+ */
423
+ async processMessage(params: {
424
+ /** Session ID */
425
+ id: string;
426
+ /** Submitted message to send to others */
427
+ message: ITssSigMessageObject;
428
+ /** Number of signers (only required for the initial message from party 0) */
429
+ m?: string | number;
430
+ /** Copayer ID of the sender */
431
+ copayerId: string;
432
+ /** TSS sig generation version */
433
+ version: number;
434
+ /** Time limit for the session in minutes, after which the session expires. Only applies on session initialization */
435
+ timeLimit?: number;
436
+ }) {
437
+ const { id, message, m, copayerId, timeLimit } = params;
317
438
  if (!id || typeof id !== 'string') {
318
439
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid id provided: ' + id);
319
440
  }
@@ -321,13 +442,32 @@ class TssSignClass {
321
442
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid copayerId provided: ' + copayerId);
322
443
  }
323
444
 
445
+ // version was not given by client until 1.1, so fallback to 1.0
446
+ const version = Number(params.version ?? 1.0);
447
+ if (!Number.isFinite(version)) {
448
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid version provided: ' + params.version);
449
+ }
450
+ if (version < Constants.TSS_SIGGEN_SCHEME_VERSION_MIN) {
451
+ throw Errors.UPGRADE_NEEDED;
452
+ }
453
+ if (version > Constants.TSS_SIGGEN_SCHEME_VERSION_MAX) {
454
+ throw Errors.UPGRADE_NEEDED.withMessage('TSS version too new: ' + version);
455
+ }
456
+
324
457
  const storage = WalletService.getStorage();
325
458
  let session = await storage.fetchTssSigSession({ id });
326
459
 
327
460
  if (session) {
461
+ if (session.isExpired()) {
462
+ throw Errors.TSS_SESSION_EXPIRED;
463
+ }
328
464
  if (!this._isValidBroadcastMessage({ message }) && !this._isValidP2pMessage({ message })) {
329
465
  throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid message provided');
330
466
  }
467
+
468
+ if (session.schemeVersion != version) {
469
+ throw Errors.TSS_MISMATCH_VERSION.withMessage(`TSS version (${version}) does not match session version (${session.schemeVersion})`);
470
+ }
331
471
 
332
472
  const isParticipant = session.participants.some(p => p.copayerId === copayerId && p.partyId === message.partyId);
333
473
  if (!isParticipant) {
@@ -357,20 +497,22 @@ class TssSignClass {
357
497
  if (!this._isValidBroadcastMessage({ message })) {
358
498
  throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid broadcast message provided');
359
499
  }
360
- await this._initSession({ id, message, m, storage, copayerId });
500
+ await this._initSession({ id, message, m, storage, copayerId, version, timeLimit });
361
501
  } else {
362
502
  throw Errors.TSS_SESSION_NOT_FOUND;
363
503
  }
364
504
  }
365
505
 
366
- private _isValidBroadcastMessage(params: { message: ITssSigMessageObject }) {
506
+ /** Checks if a broadcast message is in a valid format */
507
+ private _isValidBroadcastMessage(params: { message: ITssSigMessageObject }): boolean {
367
508
  const { message } = params;
368
509
  return typeof message?.broadcastMessages?.[0]?.from === 'number' &&
369
510
  typeof message?.broadcastMessages?.[0]?.payload?.message === 'string' &&
370
511
  typeof message?.broadcastMessages?.[0]?.payload?.signature === 'string';
371
512
  }
372
513
 
373
- private _isValidP2pMessage(params: { message: ITssSigMessageObject }) {
514
+ /** Checks if a P2P message is in a valid format */
515
+ private _isValidP2pMessage(params: { message: ITssSigMessageObject }): boolean {
374
516
  const { message } = params;
375
517
  return typeof message?.p2pMessages?.[0]?.from === 'number' &&
376
518
  typeof message?.p2pMessages?.[0]?.to === 'number' &&
@@ -378,8 +520,26 @@ class TssSignClass {
378
520
  typeof message?.p2pMessages?.[0]?.payload?.signature === 'string';
379
521
  }
380
522
 
381
- private async _initSession(params: { id: string; message: ITssSigMessageObject; m: number | string; storage: Storage; copayerId: string }) {
382
- const { id, message, storage, copayerId } = params;
523
+ /**
524
+ * Initialize a new TSS signature session. This is only called when the first message from party 0 for round 0 is received.
525
+ */
526
+ private async _initSession(params: {
527
+ /** Session ID */
528
+ id: string;
529
+ /** Initial broadcast message by party 0 */
530
+ message: ITssSigMessageObject;
531
+ /** Number of signers */
532
+ m: number | string;
533
+ /** BWS storage instance */
534
+ storage: Storage;
535
+ /** Party 0's copayer ID */
536
+ copayerId: string;
537
+ /** TSS sig generation version given by client to ensure compatibility with others */
538
+ version: number;
539
+ /** Time limit for the session in minutes, after which the session expires. Only applies on session initialization */
540
+ timeLimit?: number;
541
+ }): Promise<void> {
542
+ const { id, message, storage, copayerId, version, timeLimit } = params;
383
543
  const m = parseInt(params.m as string);
384
544
  if (!m || m < 1) {
385
545
  throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid m provided: ' + m);
@@ -388,7 +548,9 @@ class TssSignClass {
388
548
  id,
389
549
  message,
390
550
  m,
391
- copayerId
551
+ copayerId,
552
+ version,
553
+ timeLimit
392
554
  });
393
555
  const result = await storage.storeTssSigSession({ doc });
394
556
  if (!result.result.ok) {
@@ -397,7 +559,19 @@ class TssSignClass {
397
559
  }
398
560
  }
399
561
 
400
- private async _pushMessage(params: { id: string; session: TssSigGenModel; message: ITssSigMessageObject; storage: Storage }) {
562
+ /**
563
+ * Push a TSS signature message to the session.
564
+ */
565
+ private async _pushMessage(params: {
566
+ /** Session ID */
567
+ id: string;
568
+ /** TSS sig generation session fetched from BWS storage */
569
+ session: TssSigGenModel;
570
+ /** TSS signature message to be pushed */
571
+ message: ITssSigMessageObject;
572
+ /** BWS storage instance */
573
+ storage: Storage;
574
+ }): Promise<boolean> {
401
575
  const { id, session, message, storage } = params;
402
576
  const { round } = message;
403
577
 
@@ -428,7 +602,15 @@ class TssSignClass {
428
602
  }
429
603
  }
430
604
 
431
- async storeSignature(params: { id: string; signature: ITssSigMessageObject['signature'] }) {
605
+ /**
606
+ * Stores the signature to the session
607
+ */
608
+ async storeSignature(params: {
609
+ /** Session ID */
610
+ id: string;
611
+ /** Signature to store */
612
+ signature: ITssSigMessageObject['signature'];
613
+ }): Promise<void> {
432
614
  const { id, signature } = params;
433
615
  if (!signature) {
434
616
  throw Errors.TSS_NO_FINAL_SIGNATURE;
@@ -443,15 +625,35 @@ class TssSignClass {
443
625
  throw Errors.TSS_SESSION_NOT_FOUND;
444
626
  }
445
627
 
446
- const result = await storage.storeTssSignature({ id, signature: {
447
- r: signature.r,
448
- s: signature.s,
449
- v: signature.v,
450
- pubKey: signature.pubKey,
451
- } });
452
- if (!result.result.ok) {
453
- logger.error('Failed to store TSS signature %o %o', id, result);
454
- throw Errors.TSS_GENERIC_ERROR.withMessage('Failed to store TSS signature');
628
+ const verifySig = (sig1, sig2) => {
629
+ return sig1.r === sig2.r &&
630
+ sig1.s === sig2.s &&
631
+ sig1.v === sig2.v &&
632
+ sig1.pubKey === sig2.pubKey;
633
+ };
634
+
635
+ if (!session.signature) {
636
+ const result = await storage.storeTssSignature({ id, signature: {
637
+ r: signature.r,
638
+ s: signature.s,
639
+ v: signature.v,
640
+ pubKey: signature.pubKey,
641
+ } });
642
+ if (!result.result.ok) {
643
+ logger.error('Failed to store TSS signature %o %o', id, result);
644
+ throw Errors.TSS_GENERIC_ERROR.withMessage('Failed to store TSS signature');
645
+ }
646
+ if (result.modifiedCount === 0) {
647
+ const updatedSesh = await storage.fetchTssSigSession({ id });
648
+ if (!updatedSesh.signature) {
649
+ throw Errors.TSS_FINAL_SIGNATURE_MISMATCH.withMessage('Unable to fetch stored TSS signature');
650
+ }
651
+ if (updatedSesh?.signature && !verifySig(updatedSesh.signature, signature)) {
652
+ throw Errors.TSS_FINAL_SIGNATURE_MISMATCH;
653
+ }
654
+ }
655
+ } else if (!verifySig(session.signature, signature)) {
656
+ throw Errors.TSS_FINAL_SIGNATURE_MISMATCH;
455
657
  }
456
658
  }
457
659
  };
@@ -3,13 +3,17 @@ interface ExchangeConfig {
3
3
  removed?: boolean;
4
4
  disabledTitle?: string;
5
5
  disabledMessage?: string;
6
- config?: any;
6
+ config?: {
7
+ paymentMethods?: PaymentMethodsConfig;
8
+ [key: string]: any; // Other partner-specific configuration properties can be added here
9
+ };
7
10
  }
8
11
 
9
12
  export interface BuyCryptoConfig {
10
13
  disabled?: boolean;
11
14
  disabledTitle?: string;
12
15
  disabledMessage?: string;
16
+ paymentMethods?: PaymentMethodsConfig;
13
17
  banxa?: ExchangeConfig;
14
18
  moonpay?: ExchangeConfig;
15
19
  ramp?: ExchangeConfig;
@@ -23,6 +27,7 @@ export interface SellCryptoConfig {
23
27
  disabled?: boolean;
24
28
  disabledTitle?: string;
25
29
  disabledMessage?: string;
30
+ paymentMethods?: PaymentMethodsConfig;
26
31
  moonpay?: ExchangeConfig;
27
32
  }
28
33
 
@@ -34,6 +39,29 @@ export interface SwapCryptoConfig {
34
39
  thorswap?: ExchangeConfig;
35
40
  }
36
41
 
42
+ export type PaymentMethodKey =
43
+ | 'ach'
44
+ | 'applePay'
45
+ | 'cashApp'
46
+ | 'creditCard'
47
+ | 'debitCard'
48
+ | 'googlePay'
49
+ | 'sepaBankTransfer'
50
+ | 'gbpBankTransfer'
51
+ | 'other'
52
+ | 'paypal'
53
+ | 'pisp'
54
+ | 'pix'
55
+ | 'venmo';
56
+
57
+ export interface PaymentMethodConfig {
58
+ disabled?: boolean;
59
+ }
60
+
61
+ export type PaymentMethodsConfig = {
62
+ [key in PaymentMethodKey]?: PaymentMethodConfig;
63
+ };
64
+
37
65
  export interface ExternalServicesConfig {
38
66
  buyCrypto?: BuyCryptoConfig;
39
67
  sellCrypto?: SellCryptoConfig;
@@ -150,6 +150,22 @@ describe('CoinGecko integration', function() {
150
150
  sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
151
151
  }
152
152
 
153
+ /** Wraps the active fakeRequest so every outgoing market_chart URL is recorded, in order. */
154
+ function captureMarketChartUrls(): string[] {
155
+ const urls: string[] = [];
156
+ const baseGet = fakeRequest.get;
157
+ fakeRequest.get = (url, opts, cb) => {
158
+ if (matchCoinIdFromMarketChartUrl(url)) urls.push(url);
159
+ return baseGet(url, opts, cb);
160
+ };
161
+ return urls;
162
+ }
163
+
164
+ function daysAndInterval(url: string): { days: string | null; interval: string | null } {
165
+ const u = toURL(url);
166
+ return { days: u.searchParams.get('days'), interval: u.searchParams.get('interval') };
167
+ }
168
+
153
169
  before(async () => {
154
170
  await helpers.before();
155
171
  });
@@ -840,9 +856,9 @@ describe('CoinGecko integration', function() {
840
856
 
841
857
  should.exist(data);
842
858
  checkCacheStub.callCount.should.equal(1);
843
- checkCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:100000');
859
+ checkCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:max');
844
860
  storeCacheStub.callCount.should.equal(1);
845
- storeCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:100000');
861
+ storeCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:max');
846
862
  });
847
863
 
848
864
  it('should bypass DB cache for token fiatrates requests', async () => {
@@ -881,12 +897,20 @@ describe('CoinGecko integration', function() {
881
897
  });
882
898
 
883
899
  it('should return error on invalid days', async () => {
884
- try {
885
- await getFiatRates({ coin: 'BTC', days: 0 });
886
- should.fail('should have thrown');
887
- } catch (err) {
888
- err.message.should.equal('Invalid days');
900
+ const getSpy = sandbox.spy();
901
+ cg().request = { get: getSpy };
902
+
903
+ for (const days of [0, 100000, 'max', 'MAX']) {
904
+ try {
905
+ await getFiatRates({ coin: 'BTC', days });
906
+ should.fail(`should have thrown for days=${JSON.stringify(days)}`);
907
+ } catch (err) {
908
+ err.message.should.equal('Invalid days');
909
+ err.statusCode.should.equal(400);
910
+ }
889
911
  }
912
+
913
+ getSpy.callCount.should.equal(0);
890
914
  });
891
915
 
892
916
  it('should return rates for default coins if missing coin', async () => {
@@ -935,5 +959,45 @@ describe('CoinGecko integration', function() {
935
959
  .filter(args => args?.[0]?.toString().includes('CoinGecko fiat rates fetch failed') && args?.[1]?.coin === 'bch');
936
960
  bchWarns.length.should.be.greaterThan(0);
937
961
  });
962
+
963
+ describe('all-time window', () => {
964
+ it('should request days=max when days is omitted', async () => {
965
+ forceGlobalCacheMisses();
966
+ const urls = captureMarketChartUrls();
967
+
968
+ const data = await getFiatRates({ coin: 'BTC' });
969
+
970
+ data.should.deep.equal([
971
+ { ts: 1, rate: 100 },
972
+ { ts: 2, rate: 110 }
973
+ ]);
974
+ urls.should.have.length(1);
975
+ daysAndInterval(urls[0]).should.deep.equal({ days: 'max', interval: 'daily' });
976
+ });
977
+ });
978
+
979
+ describe('numeric day windows', () => {
980
+ it('should keep every allowed numeric window accepted and unchanged', async () => {
981
+ forceGlobalCacheMisses();
982
+ const urls = captureMarketChartUrls();
983
+
984
+ for (const days of [1, 7, 30, 90, 365, 1825]) {
985
+ const data = await getFiatRates({ coin: 'BTC', days });
986
+ data.should.deep.equal([
987
+ { ts: 1, rate: 100 },
988
+ { ts: 2, rate: 110 }
989
+ ]);
990
+ }
991
+
992
+ urls.map(daysAndInterval).should.deep.equal([
993
+ { days: '1', interval: null },
994
+ { days: '7', interval: null },
995
+ { days: '30', interval: null },
996
+ { days: '90', interval: 'daily' },
997
+ { days: '365', interval: 'daily' },
998
+ { days: '1825', interval: 'daily' }
999
+ ]);
1000
+ });
1001
+ });
938
1002
  });
939
1003
  });