@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.
Files changed (105) hide show
  1. package/Dockerfile +1 -1
  2. package/package.json +5 -5
  3. package/src/lib/chain/btc/index.ts +3 -2
  4. package/src/lib/chain/eth/index.ts +134 -117
  5. package/src/lib/chain/sol/index.ts +1 -1
  6. package/src/lib/chain/xrp/index.ts +1 -1
  7. package/src/lib/common/constants.ts +1 -1
  8. package/src/lib/common/utils.ts +18 -1
  9. package/src/lib/model/tsskeygen.ts +16 -0
  10. package/src/lib/routes/maintenance.ts +11 -2
  11. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  12. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  13. package/src/lib/routes/services.ts +7 -1
  14. package/src/lib/routes/tss.ts +33 -30
  15. package/src/lib/routes/wallets.ts +3 -1
  16. package/src/lib/server.ts +198 -105
  17. package/src/lib/storage.ts +129 -20
  18. package/src/lib/tss.ts +111 -7
  19. package/test/chain/eth.test.ts +70 -0
  20. package/test/chain/sol.test.ts +1 -1
  21. package/test/chain/xrp.test.ts +1 -1
  22. package/test/expressapp.test.ts +243 -0
  23. package/test/integration/addressReads.test.ts +503 -0
  24. package/test/integration/bcmonitor.test.ts +44 -0
  25. package/test/integration/server.test.ts +251 -49
  26. package/test/integration/tss.test.ts +428 -0
  27. package/test/integration/walletReads.test.ts +488 -0
  28. package/ts_build/package.json +4 -4
  29. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  30. package/ts_build/src/lib/chain/btc/index.js +3 -2
  31. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  32. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  33. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  34. package/ts_build/src/lib/chain/eth/index.js +114 -102
  35. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  36. package/ts_build/src/lib/chain/sol/index.js +1 -1
  37. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  38. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  39. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  40. package/ts_build/src/lib/common/constants.d.ts +1 -1
  41. package/ts_build/src/lib/common/constants.js +1 -1
  42. package/ts_build/src/lib/common/index.d.ts +2 -1
  43. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  44. package/ts_build/src/lib/common/utils.d.ts +1 -0
  45. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  46. package/ts_build/src/lib/common/utils.js +16 -1
  47. package/ts_build/src/lib/common/utils.js.map +1 -1
  48. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  49. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  51. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  52. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  53. package/ts_build/src/lib/routes/maintenance.js +12 -4
  54. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  55. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  56. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  57. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  58. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  59. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  60. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  61. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  62. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  63. package/ts_build/src/lib/routes/services.js +6 -1
  64. package/ts_build/src/lib/routes/services.js.map +1 -1
  65. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  66. package/ts_build/src/lib/routes/tss.js +32 -30
  67. package/ts_build/src/lib/routes/tss.js.map +1 -1
  68. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  69. package/ts_build/src/lib/routes/wallets.js +2 -1
  70. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  71. package/ts_build/src/lib/server.d.ts +24 -3
  72. package/ts_build/src/lib/server.d.ts.map +1 -1
  73. package/ts_build/src/lib/server.js +148 -97
  74. package/ts_build/src/lib/server.js.map +1 -1
  75. package/ts_build/src/lib/storage.d.ts +26 -10
  76. package/ts_build/src/lib/storage.d.ts.map +1 -1
  77. package/ts_build/src/lib/storage.js +99 -20
  78. package/ts_build/src/lib/storage.js.map +1 -1
  79. package/ts_build/src/lib/tss.d.ts +12 -0
  80. package/ts_build/src/lib/tss.d.ts.map +1 -1
  81. package/ts_build/src/lib/tss.js +64 -5
  82. package/ts_build/src/lib/tss.js.map +1 -1
  83. package/ts_build/test/chain/eth.test.js +59 -0
  84. package/ts_build/test/chain/eth.test.js.map +1 -1
  85. package/ts_build/test/chain/sol.test.js +1 -1
  86. package/ts_build/test/chain/sol.test.js.map +1 -1
  87. package/ts_build/test/chain/xrp.test.js +1 -1
  88. package/ts_build/test/chain/xrp.test.js.map +1 -1
  89. package/ts_build/test/expressapp.test.js +222 -0
  90. package/ts_build/test/expressapp.test.js.map +1 -1
  91. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  92. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  93. package/ts_build/test/integration/addressReads.test.js +465 -0
  94. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  95. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  96. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  97. package/ts_build/test/integration/server.test.js +228 -48
  98. package/ts_build/test/integration/server.test.js.map +1 -1
  99. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  100. package/ts_build/test/integration/tss.test.js +344 -0
  101. package/ts_build/test/integration/tss.test.js.map +1 -1
  102. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  103. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  104. package/ts_build/test/integration/walletReads.test.js +436 -0
  105. package/ts_build/test/integration/walletReads.test.js.map +1 -0
@@ -304,8 +304,9 @@ export class Storage {
304
304
  }
305
305
 
306
306
  // TODO: should be done client-side
307
- _completeTxData(walletId: string, txs: TxProposal | TxProposal[], cb: (err?: any, txs?: TxProposal | TxProposal[]) => void) {
308
- this.fetchWallet(walletId, (err, wallet) => {
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
- _completeTxNotesData(walletId, notes: TxNote | TxNote[], cb: (err?: any, notes?: TxNote | TxNote[]) => void) {
1319
- this.fetchWallet(walletId, (err, wallet) => {
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
- /** Store the BWS join secret for the TSS keygen session */
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
- /** Get the BWS join secret for the TSS keygen session */
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
- if (session.participants.indexOf(copayerId) === -1) {
674
+ const partyId = session.participants.indexOf(copayerId);
675
+ if (partyId === -1) {
604
676
  throw Errors.TSS_NON_PARTICIPANT;
605
677
  }
606
- if (!session.bwsJoinSecret) {
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 session.bwsJoinSecret;
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
- if (!this._isValidBroadcastMessage({ message }) && !this._isValidP2pMessage({ message })) {
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 &&
@@ -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 = {
@@ -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: { fetchAddresses: (_walletId, cb) => cb(null, []) }
30
+ storage: { fetchAddressesPage: (_walletId, _opts, cb) => cb(null, []) }
31
31
  };
32
32
  const wallet = { chain: 'sol', network: 'livenet' };
33
33
 
@@ -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: { fetchAddresses: (_walletId, cb) => cb(null, []) }
23
+ storage: { fetchAddressesPage: (_walletId, _opts, cb) => cb(null, []) }
24
24
  };
25
25
  const wallet = { chain: 'xrp', network: 'livenet' };
26
26