@bitpay-labs/bitcore-wallet-service 11.11.1 → 11.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -1
- package/package.json +5 -5
- package/src/lib/chain/btc/index.ts +3 -2
- package/src/lib/chain/eth/index.ts +134 -117
- package/src/lib/chain/sol/index.ts +1 -1
- package/src/lib/chain/xrp/index.ts +1 -1
- package/src/lib/common/constants.ts +1 -1
- package/src/lib/common/utils.ts +18 -1
- package/src/lib/model/tsskeygen.ts +16 -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 +7 -1
- package/src/lib/routes/tss.ts +33 -30
- package/src/lib/routes/wallets.ts +3 -1
- package/src/lib/server.ts +198 -105
- package/src/lib/storage.ts +129 -20
- package/src/lib/tss.ts +111 -7
- package/test/chain/eth.test.ts +70 -0
- package/test/chain/sol.test.ts +1 -1
- package/test/chain/xrp.test.ts +1 -1
- package/test/expressapp.test.ts +243 -0
- package/test/integration/addressReads.test.ts +503 -0
- package/test/integration/bcmonitor.test.ts +44 -0
- package/test/integration/server.test.ts +251 -49
- package/test/integration/tss.test.ts +428 -0
- package/test/integration/walletReads.test.ts +488 -0
- package/ts_build/package.json +4 -4
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -2
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +114 -102
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +1 -1
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +1 -1
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +1 -1
- package/ts_build/src/lib/common/constants.js +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -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/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/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 +6 -1
- 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/wallets.d.ts.map +1 -1
- package/ts_build/src/lib/routes/wallets.js +2 -1
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +24 -3
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +148 -97
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +26 -10
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +99 -20
- 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/chain/eth.test.js +59 -0
- package/ts_build/test/chain/eth.test.js.map +1 -1
- package/ts_build/test/chain/sol.test.js +1 -1
- package/ts_build/test/chain/sol.test.js.map +1 -1
- package/ts_build/test/chain/xrp.test.js +1 -1
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/expressapp.test.js +222 -0
- package/ts_build/test/expressapp.test.js.map +1 -1
- package/ts_build/test/integration/addressReads.test.d.ts +2 -0
- package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/addressReads.test.js +465 -0
- package/ts_build/test/integration/addressReads.test.js.map +1 -0
- package/ts_build/test/integration/bcmonitor.test.js +40 -0
- package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +228 -48
- 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
- package/ts_build/test/integration/walletReads.test.d.ts +2 -0
- package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/walletReads.test.js +436 -0
- package/ts_build/test/integration/walletReads.test.js.map +1 -0
package/src/lib/storage.ts
CHANGED
|
@@ -304,8 +304,9 @@ export class Storage {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
// TODO: should be done client-side
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
// Callers that already have the wallet pass it, to save reading it again
|
|
308
|
+
_completeTxData(walletId: string, txs: TxProposal | TxProposal[], cb: (err?: any, txs?: TxProposal | TxProposal[]) => void, wallet?: Wallet) {
|
|
309
|
+
const complete = (err, wallet: Wallet) => {
|
|
309
310
|
if (err) return cb(err);
|
|
310
311
|
for (const tx of [].concat(txs) as TxProposal[]) {
|
|
311
312
|
tx.derivationStrategy = wallet.derivationStrategy || 'BIP45';
|
|
@@ -318,11 +319,13 @@ export class Storage {
|
|
|
318
319
|
tx.raw = tx.getRawTx();
|
|
319
320
|
}
|
|
320
321
|
return cb(null, txs);
|
|
321
|
-
}
|
|
322
|
+
};
|
|
323
|
+
if (wallet) return complete(null, wallet);
|
|
324
|
+
this.fetchWallet(walletId, complete);
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
// TODO: remove walletId from signature
|
|
325
|
-
fetchTx(walletId: string, txProposalId: string, cb: (err?: any, tx?: TxProposal) => void) {
|
|
328
|
+
fetchTx(walletId: string, txProposalId: string, cb: (err?: any, tx?: TxProposal) => void, wallet?: Wallet) {
|
|
326
329
|
if (!this.db) return cb();
|
|
327
330
|
|
|
328
331
|
this.db.collection(collections.TXS).findOne(
|
|
@@ -333,7 +336,7 @@ export class Storage {
|
|
|
333
336
|
(err, result) => {
|
|
334
337
|
if (err) return cb(err);
|
|
335
338
|
if (!result) return cb();
|
|
336
|
-
return this._completeTxData(walletId, TxProposal.fromObj(result), cb);
|
|
339
|
+
return this._completeTxData(walletId, TxProposal.fromObj(result), cb, wallet);
|
|
337
340
|
}
|
|
338
341
|
);
|
|
339
342
|
}
|
|
@@ -356,7 +359,7 @@ export class Storage {
|
|
|
356
359
|
|
|
357
360
|
// Wallet-scoped counterpart to fetchTxByHash, used by the authenticated
|
|
358
361
|
// by-hash API read.
|
|
359
|
-
fetchTxByHashForWallet(walletId: string, hash, cb: (err?: any, tx?: TxProposal) => void) {
|
|
362
|
+
fetchTxByHashForWallet(walletId: string, hash, cb: (err?: any, tx?: TxProposal) => void, wallet?: Wallet) {
|
|
360
363
|
if (!this.db) return cb();
|
|
361
364
|
|
|
362
365
|
this.db.collection(collections.TXS).findOne(
|
|
@@ -368,7 +371,7 @@ export class Storage {
|
|
|
368
371
|
if (err) return cb(err);
|
|
369
372
|
if (!result) return cb();
|
|
370
373
|
|
|
371
|
-
return this._completeTxData(walletId, TxProposal.fromObj(result), cb);
|
|
374
|
+
return this._completeTxData(walletId, TxProposal.fromObj(result), cb, wallet);
|
|
372
375
|
}
|
|
373
376
|
);
|
|
374
377
|
}
|
|
@@ -444,7 +447,7 @@ export class Storage {
|
|
|
444
447
|
});
|
|
445
448
|
}
|
|
446
449
|
|
|
447
|
-
fetchPendingTxs(walletId, cb) {
|
|
450
|
+
fetchPendingTxs(walletId, cb, wallet?: Wallet) {
|
|
448
451
|
this.db
|
|
449
452
|
.collection(collections.TXS)
|
|
450
453
|
.find({
|
|
@@ -460,7 +463,7 @@ export class Storage {
|
|
|
460
463
|
const txs = _.map(result, tx => {
|
|
461
464
|
return TxProposal.fromObj(tx);
|
|
462
465
|
});
|
|
463
|
-
return this._completeTxData(walletId, txs, cb);
|
|
466
|
+
return this._completeTxData(walletId, txs, cb, wallet);
|
|
464
467
|
});
|
|
465
468
|
}
|
|
466
469
|
|
|
@@ -472,7 +475,7 @@ export class Storage {
|
|
|
472
475
|
* @param opts.maxTs
|
|
473
476
|
* @param opts.limit
|
|
474
477
|
*/
|
|
475
|
-
fetchTxs(walletId, opts, cb) {
|
|
478
|
+
fetchTxs(walletId, opts, cb, wallet?: Wallet) {
|
|
476
479
|
opts = opts || {};
|
|
477
480
|
|
|
478
481
|
const tsFilter: { $gte?: number; $lte?: number } = {};
|
|
@@ -499,7 +502,7 @@ export class Storage {
|
|
|
499
502
|
const txs = _.map(result, tx => {
|
|
500
503
|
return TxProposal.fromObj(tx);
|
|
501
504
|
});
|
|
502
|
-
return this._completeTxData(walletId, txs, cb);
|
|
505
|
+
return this._completeTxData(walletId, txs, cb, wallet);
|
|
503
506
|
});
|
|
504
507
|
}
|
|
505
508
|
|
|
@@ -511,7 +514,7 @@ export class Storage {
|
|
|
511
514
|
* @param opts.maxTs
|
|
512
515
|
* @param opts.limit
|
|
513
516
|
*/
|
|
514
|
-
fetchBroadcastedTxs(walletId, opts, cb) {
|
|
517
|
+
fetchBroadcastedTxs(walletId, opts, cb, wallet?: Wallet) {
|
|
515
518
|
opts = opts || {};
|
|
516
519
|
|
|
517
520
|
const tsFilter: { $gte?: number; $lte?: number } = {};
|
|
@@ -543,7 +546,7 @@ export class Storage {
|
|
|
543
546
|
const txs = _.map(result, tx => {
|
|
544
547
|
return TxProposal.fromObj(tx);
|
|
545
548
|
});
|
|
546
|
-
return this._completeTxData(walletId, txs, cb);
|
|
549
|
+
return this._completeTxData(walletId, txs, cb, wallet);
|
|
547
550
|
});
|
|
548
551
|
}
|
|
549
552
|
|
|
@@ -678,6 +681,70 @@ export class Storage {
|
|
|
678
681
|
});
|
|
679
682
|
}
|
|
680
683
|
|
|
684
|
+
/**
|
|
685
|
+
* A page of the wallet's addresses, in fetchAddresses order (createdOn, then path), reading only the addresses the
|
|
686
|
+
* page needs. noChange and addresses filter the list, then reverse, skip and limit apply in that order.
|
|
687
|
+
*
|
|
688
|
+
* @param walletId
|
|
689
|
+
* @param opts.noChange - leave out change addresses
|
|
690
|
+
* @param opts.addresses - only these addresses
|
|
691
|
+
* @param opts.reverse - newest first
|
|
692
|
+
* @param opts.skip
|
|
693
|
+
* @param opts.limit
|
|
694
|
+
*/
|
|
695
|
+
fetchAddressesPage(
|
|
696
|
+
walletId: string,
|
|
697
|
+
opts: { noChange?: boolean; addresses?: string[]; reverse?: boolean; skip?: number; limit?: number },
|
|
698
|
+
cb: (err?: any, addresses?: Address[]) => void
|
|
699
|
+
) {
|
|
700
|
+
const query: { walletId: string; isChange?: { $in: Array<false | null> }; address?: { $in: string[] } } = { walletId };
|
|
701
|
+
// false, null or missing, the same addresses as !isChange
|
|
702
|
+
if (opts.noChange) query.isChange = { $in: [false, null] };
|
|
703
|
+
if (opts.addresses) query.address = { $in: opts.addresses };
|
|
704
|
+
// skip and limit apply only when positive, and as whole numbers, as slice does
|
|
705
|
+
const skip = opts.skip > 0 ? Math.floor(opts.skip) : 0;
|
|
706
|
+
const limit = opts.limit > 0 ? Math.floor(opts.limit) : null;
|
|
707
|
+
const collection = this.db.collection(collections.ADDRESSES);
|
|
708
|
+
|
|
709
|
+
const page = docs => {
|
|
710
|
+
let addresses = Utils.sortAsc(docs.map(Address.fromObj), 'createdOn', 'path');
|
|
711
|
+
if (opts.reverse) addresses.reverse();
|
|
712
|
+
if (skip) addresses = addresses.slice(skip);
|
|
713
|
+
if (limit !== null) addresses = addresses.slice(0, limit);
|
|
714
|
+
return addresses;
|
|
715
|
+
};
|
|
716
|
+
const readAll = () => collection.find(query).sort({ createdOn: 1 }).toArray((err, docs) => {
|
|
717
|
+
if (err) return cb(err);
|
|
718
|
+
return cb(null, page(docs));
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// filtering by address, or reading without a limit or with one too large for a query limit, reads every matching
|
|
722
|
+
// address
|
|
723
|
+
if (opts.addresses || !(limit > 0) || !Number.isSafeInteger(skip + limit + 1)) return readAll();
|
|
724
|
+
|
|
725
|
+
// fetchAddresses sorts an address with a missing createdOn last and one with a null createdOn first, but Mongo sorts
|
|
726
|
+
// both first, so a wallet that has either is read whole
|
|
727
|
+
collection.findOne({ walletId, createdOn: null }, { projection: { _id: 1 } }, (err, unordered) => {
|
|
728
|
+
if (err) return cb(err);
|
|
729
|
+
if (unordered) return readAll();
|
|
730
|
+
|
|
731
|
+
// The page is within the first skip + limit addresses by createdOn plus any others sharing the last one's
|
|
732
|
+
// createdOn, whatever order addresses created in the same second are in. One more address shows whether there are any.
|
|
733
|
+
const count = skip + limit;
|
|
734
|
+
collection.find(query).sort({ createdOn: opts.reverse ? -1 : 1 }).limit(count + 1).toArray((err, docs) => {
|
|
735
|
+
if (err) return cb(err);
|
|
736
|
+
if (docs.length <= count || docs[count].createdOn !== docs[count - 1].createdOn) {
|
|
737
|
+
return cb(null, page(docs.slice(0, count)));
|
|
738
|
+
}
|
|
739
|
+
collection.find({ ...query, createdOn: docs[count - 1].createdOn }).toArray((err, ties) => {
|
|
740
|
+
if (err) return cb(err);
|
|
741
|
+
const read = new Set(docs.slice(0, count).map(doc => String(doc._id)));
|
|
742
|
+
return cb(null, page([...docs.slice(0, count), ...ties.filter(doc => !read.has(String(doc._id)))]));
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
681
748
|
migrateToCashAddr(walletId, cb) {
|
|
682
749
|
const cursor = this.db.collection(collections.ADDRESSES).find({
|
|
683
750
|
walletId
|
|
@@ -768,6 +835,26 @@ export class Storage {
|
|
|
768
835
|
);
|
|
769
836
|
}
|
|
770
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
|
+
|
|
771
858
|
deregisterWallet(walletId, cb) {
|
|
772
859
|
this.db.collection(collections.WALLETS).updateOne(
|
|
773
860
|
{
|
|
@@ -1300,7 +1387,7 @@ export class Storage {
|
|
|
1300
1387
|
});
|
|
1301
1388
|
}
|
|
1302
1389
|
|
|
1303
|
-
fetchTxNote(walletId, txid, cb: (err?: any, note?: TxNote) => void) {
|
|
1390
|
+
fetchTxNote(walletId, txid, cb: (err?: any, note?: TxNote) => void, wallet?: Wallet) {
|
|
1304
1391
|
this.db.collection(collections.TX_NOTES).findOne(
|
|
1305
1392
|
{
|
|
1306
1393
|
walletId,
|
|
@@ -1309,20 +1396,23 @@ export class Storage {
|
|
|
1309
1396
|
(err, result) => {
|
|
1310
1397
|
if (err) return cb(err);
|
|
1311
1398
|
if (!result) return cb();
|
|
1312
|
-
return this._completeTxNotesData(walletId, TxNote.fromObj(result), cb);
|
|
1399
|
+
return this._completeTxNotesData(walletId, TxNote.fromObj(result), cb, wallet);
|
|
1313
1400
|
}
|
|
1314
1401
|
);
|
|
1315
1402
|
}
|
|
1316
1403
|
|
|
1317
1404
|
// TODO: should be done client-side
|
|
1318
|
-
|
|
1319
|
-
|
|
1405
|
+
// Callers that already have the wallet pass it, to save reading it again
|
|
1406
|
+
_completeTxNotesData(walletId, notes: TxNote | TxNote[], cb: (err?: any, notes?: TxNote | TxNote[]) => void, wallet?: Wallet) {
|
|
1407
|
+
const complete = (err, wallet: Wallet) => {
|
|
1320
1408
|
if (err) return cb(err);
|
|
1321
1409
|
for (const note of [].concat(notes) as TxNote[]) {
|
|
1322
1410
|
note.editedByName = wallet.getCopayer(note.editedBy).name;
|
|
1323
1411
|
}
|
|
1324
1412
|
return cb(null, notes);
|
|
1325
|
-
}
|
|
1413
|
+
};
|
|
1414
|
+
if (wallet) return complete(null, wallet);
|
|
1415
|
+
this.fetchWallet(walletId, complete);
|
|
1326
1416
|
}
|
|
1327
1417
|
|
|
1328
1418
|
/**
|
|
@@ -1331,7 +1421,7 @@ export class Storage {
|
|
|
1331
1421
|
* @param walletId
|
|
1332
1422
|
* @param opts.minTs
|
|
1333
1423
|
*/
|
|
1334
|
-
fetchTxNotes(walletId, opts, cb) {
|
|
1424
|
+
fetchTxNotes(walletId, opts, cb, wallet?: Wallet) {
|
|
1335
1425
|
const filter: { walletId: string; editedOn?: { $gte: number } } = {
|
|
1336
1426
|
walletId
|
|
1337
1427
|
};
|
|
@@ -1349,7 +1439,7 @@ export class Storage {
|
|
|
1349
1439
|
return TxNote.fromObj(note);
|
|
1350
1440
|
})
|
|
1351
1441
|
);
|
|
1352
|
-
return this._completeTxNotesData(walletId, notes, cb);
|
|
1442
|
+
return this._completeTxNotesData(walletId, notes, cb, wallet);
|
|
1353
1443
|
});
|
|
1354
1444
|
}
|
|
1355
1445
|
|
|
@@ -1891,6 +1981,10 @@ export class Storage {
|
|
|
1891
1981
|
);
|
|
1892
1982
|
}
|
|
1893
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* Use only for legacy (< v1.3) clients, where bwsJoinSecrets was not available.
|
|
1986
|
+
* @deprecated Use storeTssKeyBwsJoinSecrets instead.
|
|
1987
|
+
*/
|
|
1894
1988
|
async storeTssKeyBwsJoinSecret({ id, secret }: { id: string; secret: string }) {
|
|
1895
1989
|
return this.db.collection(collections.TSS_KEYGEN).updateOne({
|
|
1896
1990
|
id
|
|
@@ -1903,6 +1997,21 @@ export class Storage {
|
|
|
1903
1997
|
{ upsert: false });
|
|
1904
1998
|
}
|
|
1905
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
|
+
|
|
1906
2015
|
async fetchTssSigSession({ id }: { id: string }) {
|
|
1907
2016
|
const doc = await this.db.collection(collections.TSS_SIGN).findOne({ id });
|
|
1908
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 &&
|
package/test/chain/eth.test.ts
CHANGED
|
@@ -19,6 +19,76 @@ describe('Chain ETH', function() {
|
|
|
19
19
|
x.address.should.equal('0x01');
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
describe('#onTx', function() {
|
|
23
|
+
const token = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
|
|
24
|
+
const sender = '0xC877cBCF020A8AA259A1Efab1559B2A3A7259086';
|
|
25
|
+
const recipient = '0x37D3bCDA9d7d5Dc41e32DAd77a5BA89a77aA8BD0';
|
|
26
|
+
const multisig = '0x9527a556f0D9AA753dE0D1f9Ad024a17dd281B1D';
|
|
27
|
+
// a tx event as bitcore-node emits it to the wallets room
|
|
28
|
+
const tx = (fields = {}) => ({ chain: 'ETH', network: 'mainnet', txid: '0x01', to: token, from: sender, value: 0, ...fields });
|
|
29
|
+
// effects as bitcore-node builds them
|
|
30
|
+
const erc20 = { type: 'ERC20:transfer', to: recipient, from: sender, amount: '1000000', contractAddress: token, callStack: '' };
|
|
31
|
+
const internal = { to: recipient, from: sender, amount: '2000', callStack: '0' };
|
|
32
|
+
const multisigEffect = type => ({ type, to: recipient, from: sender, amount: '3000', contractAddress: multisig, callStack: '' });
|
|
33
|
+
|
|
34
|
+
it('notifies the tx recipient and value for a tx without effects', function() {
|
|
35
|
+
ChainService.onTx('eth', tx({ to: recipient, value: 5000 })).should.deep.equal({
|
|
36
|
+
txid: '0x01',
|
|
37
|
+
out: { address: recipient, amount: 5000, tokenAddress: undefined, multisigContractAddress: undefined }
|
|
38
|
+
});
|
|
39
|
+
ChainService.onTx('eth', tx({ to: recipient, value: 5000, effects: [] })).out.should.deep.equal({
|
|
40
|
+
address: recipient, amount: 5000, tokenAddress: undefined, multisigContractAddress: undefined
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('notifies the recipient of an internal value transfer', function() {
|
|
45
|
+
ChainService.onTx('eth', tx({ effects: [internal] })).out.should.deep.equal({
|
|
46
|
+
address: recipient, amount: '2000', tokenAddress: undefined, multisigContractAddress: undefined
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const type of ['MULTISIG:confirmTransaction', 'MULTISIG:submitTransaction']) {
|
|
51
|
+
it(`notifies the recipient of a ${type} with its multisig contract`, function() {
|
|
52
|
+
ChainService.onTx('eth', tx({ effects: [internal, multisigEffect(type)] })).out.should.deep.equal({
|
|
53
|
+
address: recipient, amount: '3000', tokenAddress: undefined, multisigContractAddress: multisig
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it('prefers a multisig effect to an ERC20 transfer', function() {
|
|
59
|
+
ChainService.onTx('eth', tx({ effects: [erc20, multisigEffect('MULTISIG:submitTransaction')] })).out.should.deep.equal({
|
|
60
|
+
address: recipient, amount: '3000', tokenAddress: undefined, multisigContractAddress: multisig
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to the tx recipient for effects it does not know', function() {
|
|
65
|
+
const other = { type: 'OTHER:call', to: recipient, from: sender, amount: '1', contractAddress: token, callStack: '' };
|
|
66
|
+
ChainService.onTx('eth', tx({ effects: [other] })).out.should.deep.equal({
|
|
67
|
+
address: token, amount: 0, tokenAddress: undefined, multisigContractAddress: undefined
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles MATIC txs the same way', function() {
|
|
72
|
+
ChainService.onTx('matic', tx({ chain: 'MATIC', to: recipient, value: 5000 })).out.should.deep.equal({
|
|
73
|
+
address: recipient, amount: 5000, tokenAddress: undefined, multisigContractAddress: undefined
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('notifies the token recipient, amount and token of an ERC20 transfer', function() {
|
|
78
|
+
ChainService.onTx('eth', tx({ effects: [erc20] })).should.deep.equal({
|
|
79
|
+
txid: '0x01',
|
|
80
|
+
out: { address: recipient, amount: '1000000', tokenAddress: token, multisigContractAddress: undefined }
|
|
81
|
+
});
|
|
82
|
+
ChainService.onTx('matic', tx({ chain: 'MATIC', effects: [erc20] })).out.tokenAddress.should.equal(token);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('prefers an ERC20 transfer to an internal value transfer', function() {
|
|
86
|
+
ChainService.onTx('eth', tx({ effects: [internal, erc20] })).out.should.deep.equal({
|
|
87
|
+
address: recipient, amount: '1000000', tokenAddress: token, multisigContractAddress: undefined
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
22
92
|
describe('#selectTxInputs', function() {
|
|
23
93
|
it('should reject a total that a number rounds down to the balance', function(done) {
|
|
24
94
|
const server = {
|
package/test/chain/sol.test.ts
CHANGED
|
@@ -27,7 +27,7 @@ describe('Chain SOL', function() {
|
|
|
27
27
|
getRentMinimum: (_space, cb) => cb(null, Defaults.MIN_SOL_BALANCE)
|
|
28
28
|
}),
|
|
29
29
|
getPendingTxs: (_opts, cb) => cb(null, c.txps),
|
|
30
|
-
storage: {
|
|
30
|
+
storage: { fetchAddressesPage: (_walletId, _opts, cb) => cb(null, []) }
|
|
31
31
|
};
|
|
32
32
|
const wallet = { chain: 'sol', network: 'livenet' };
|
|
33
33
|
|
package/test/chain/xrp.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ describe('Chain XRP', function() {
|
|
|
20
20
|
getReserve: cb => cb(null, 1000000)
|
|
21
21
|
}),
|
|
22
22
|
getPendingTxs: (_opts, cb) => cb(null, [{ amount: '3000000', fee: '12' }]),
|
|
23
|
-
storage: {
|
|
23
|
+
storage: { fetchAddressesPage: (_walletId, _opts, cb) => cb(null, []) }
|
|
24
24
|
};
|
|
25
25
|
const wallet = { chain: 'xrp', network: 'livenet' };
|
|
26
26
|
|