@bitpay-labs/bitcore-wallet-service 11.10.7 → 11.10.9
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.
- package/.mocharc.json +6 -0
- package/mcr.config.js +17 -0
- package/package.json +6 -6
- package/src/externalservices/coingecko.ts +24 -10
- package/src/externalservices/moonpay.ts +23 -1
- package/src/lib/chain/eth/abi-invoice.ts +139 -90
- package/src/lib/common/constants.ts +5 -2
- package/src/lib/common/defaults.ts +11 -2
- package/src/lib/errors/errordefinitions.ts +6 -0
- package/src/lib/model/copayer.ts +1 -0
- package/src/lib/model/tsskeygen.ts +33 -4
- package/src/lib/model/tsssign.ts +34 -3
- package/src/lib/pushnotificationsservice.ts +3 -1
- package/src/lib/routes/middleware/authTssRequest.ts +10 -4
- package/src/lib/routes/middleware/verifyTssMessage.ts +13 -8
- package/src/lib/routes/tss.ts +11 -4
- package/src/lib/server.ts +53 -25
- package/src/lib/storage.ts +24 -1
- package/src/lib/tss.ts +232 -30
- package/src/types/externalservices.d.ts +29 -1
- package/test/integration/externalservices/coinGecko.test.ts +71 -7
- package/test/integration/server.test.ts +731 -1
- package/test/integration/tss.test.ts +275 -0
- package/test/model/copayer.test.ts +128 -0
- package/test/storage.test.ts +91 -0
- package/ts_build/package.json +3 -3
- package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
- package/ts_build/src/externalservices/coingecko.js +14 -8
- package/ts_build/src/externalservices/coingecko.js.map +1 -1
- package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
- package/ts_build/src/externalservices/moonpay.js +38 -2
- package/ts_build/src/externalservices/moonpay.js.map +1 -1
- package/ts_build/src/lib/chain/eth/abi-invoice.d.ts +254 -41
- package/ts_build/src/lib/chain/eth/abi-invoice.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/abi-invoice.js +138 -89
- package/ts_build/src/lib/chain/eth/abi-invoice.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +4 -2
- package/ts_build/src/lib/common/constants.d.ts.map +1 -1
- package/ts_build/src/lib/common/constants.js +4 -2
- package/ts_build/src/lib/common/constants.js.map +1 -1
- package/ts_build/src/lib/common/defaults.d.ts +4 -2
- package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
- package/ts_build/src/lib/common/defaults.js +4 -2
- package/ts_build/src/lib/common/defaults.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +8 -4
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +3 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +3 -0
- package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
- package/ts_build/src/lib/model/copayer.d.ts.map +1 -1
- package/ts_build/src/lib/model/copayer.js +1 -0
- package/ts_build/src/lib/model/copayer.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +3 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +12 -3
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/model/tsssign.d.ts +7 -0
- package/ts_build/src/lib/model/tsssign.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsssign.js +22 -2
- package/ts_build/src/lib/model/tsssign.js.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.js +3 -1
- package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +8 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +11 -6
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +4 -4
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +33 -25
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +4 -0
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +18 -1
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +4 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +74 -17
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.js +62 -8
- package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +597 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.js +245 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
- package/ts_build/test/model/copayer.test.js +91 -0
- package/ts_build/test/model/copayer.test.js.map +1 -1
- package/ts_build/test/storage.test.js +73 -0
- package/ts_build/test/storage.test.js.map +1 -1
- package/.mocharc.yml +0 -2
- package/.nycrc.yml +0 -13
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
272
|
+
/** Unused - removed support for saving keychain */
|
|
189
273
|
encryptedKeyChain: string;
|
|
190
274
|
};
|
|
191
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
447
|
-
r
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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?:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
});
|