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