@bitpay-labs/bitcore-wallet-service 11.10.11 → 11.10.12

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 (68) hide show
  1. package/package.json +4 -4
  2. package/src/externalservices/simplex.ts +0 -30
  3. package/src/lib/chain/btc/index.ts +5 -0
  4. package/src/lib/chain/eth/index.ts +4 -0
  5. package/src/lib/chain/index.ts +4 -0
  6. package/src/lib/chain/sol/index.ts +6 -0
  7. package/src/lib/chain/xrp/index.ts +5 -0
  8. package/src/lib/common/utils.ts +52 -1
  9. package/src/lib/routes/middleware/authTssRequest.ts +1 -1
  10. package/src/lib/routes/services.ts +1 -14
  11. package/src/lib/server.ts +6 -2
  12. package/src/types/chain.d.ts +1 -0
  13. package/test/chain/sol.test.ts +48 -0
  14. package/test/chain/xrp.test.ts +30 -0
  15. package/test/integration/externalservices/simplex.test.ts +0 -18
  16. package/test/integration/server.test.ts +94 -0
  17. package/test/integration/tss.test.ts +67 -0
  18. package/ts_build/package.json +3 -3
  19. package/ts_build/src/externalservices/simplex.d.ts +0 -1
  20. package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
  21. package/ts_build/src/externalservices/simplex.js +0 -25
  22. package/ts_build/src/externalservices/simplex.js.map +1 -1
  23. package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
  24. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  25. package/ts_build/src/lib/chain/btc/index.js +3 -0
  26. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  27. package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
  28. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  29. package/ts_build/src/lib/chain/eth/index.js +3 -0
  30. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  31. package/ts_build/src/lib/chain/index.d.ts +1 -0
  32. package/ts_build/src/lib/chain/index.d.ts.map +1 -1
  33. package/ts_build/src/lib/chain/index.js +3 -0
  34. package/ts_build/src/lib/chain/index.js.map +1 -1
  35. package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
  36. package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
  37. package/ts_build/src/lib/chain/sol/index.js +5 -0
  38. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  39. package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
  40. package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
  41. package/ts_build/src/lib/chain/xrp/index.js +4 -0
  42. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  43. package/ts_build/src/lib/common/index.d.ts +2 -0
  44. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  45. package/ts_build/src/lib/common/utils.d.ts +4 -0
  46. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  47. package/ts_build/src/lib/common/utils.js +33 -0
  48. package/ts_build/src/lib/common/utils.js.map +1 -1
  49. package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
  50. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  51. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  52. package/ts_build/src/lib/routes/services.js +1 -13
  53. package/ts_build/src/lib/routes/services.js.map +1 -1
  54. package/ts_build/src/lib/server.d.ts.map +1 -1
  55. package/ts_build/src/lib/server.js +1 -1
  56. package/ts_build/src/lib/server.js.map +1 -1
  57. package/ts_build/test/chain/sol.test.d.ts +2 -0
  58. package/ts_build/test/chain/sol.test.d.ts.map +1 -0
  59. package/ts_build/test/chain/sol.test.js +78 -0
  60. package/ts_build/test/chain/sol.test.js.map +1 -0
  61. package/ts_build/test/chain/xrp.test.js +28 -0
  62. package/ts_build/test/chain/xrp.test.js.map +1 -1
  63. package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
  64. package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
  65. package/ts_build/test/integration/server.test.js +80 -0
  66. package/ts_build/test/integration/server.test.js.map +1 -1
  67. package/ts_build/test/integration/tss.test.js +57 -0
  68. package/ts_build/test/integration/tss.test.js.map +1 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@bitpay-labs/bitcore-wallet-service",
3
3
  "description": "A service for Mutisig HD Bitcoin Wallets",
4
4
  "author": "BitPay Inc",
5
- "version": "11.10.11",
5
+ "version": "11.10.12",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@bitpay-labs/bitcore-logging": "^11.10.9",
45
- "@bitpay-labs/bitcore-tss": "^11.10.11",
46
- "@bitpay-labs/crypto-wallet-core": "^11.10.11",
45
+ "@bitpay-labs/bitcore-tss": "^11.10.12",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.10.12",
47
47
  "@sendgrid/mail": "6.5.4",
48
48
  "abi-decoder": "2.4.0",
49
49
  "async": "0.9.2",
@@ -107,5 +107,5 @@
107
107
  "email": "kjoseph@bitpay.com"
108
108
  }
109
109
  ],
110
- "gitHead": "10bcfcf7cc6953e2af65d707b35d3e65e1254583"
110
+ "gitHead": "d1992fd744501a1b69b8cb97f07b0896df7223fd"
111
111
  }
@@ -260,34 +260,4 @@ export class SimplexService {
260
260
  );
261
261
  });
262
262
  }
263
-
264
- simplexGetEvents(req): Promise<any> {
265
- return new Promise((resolve, reject) => {
266
- if (!config.simplex) return reject(new Error('Simplex missing credentials'));
267
- if (!req.env || (req.env != 'sandbox' && req.env != 'production'))
268
- return reject(new Error("Simplex's request wrong environment"));
269
-
270
- const API = config.simplex[req.env].api;
271
- const API_KEY = config.simplex[req.env].apiKey;
272
- const headers = {
273
- 'Content-Type': 'application/json',
274
- Authorization: 'ApiKey ' + API_KEY
275
- };
276
-
277
- this.request.get(
278
- API + '/wallet/merchant/v2/events',
279
- {
280
- headers,
281
- json: true
282
- },
283
- (err, data) => {
284
- if (err) {
285
- return reject(err.body ? err.body : null);
286
- } else {
287
- return resolve(data.body ? data.body : null);
288
- }
289
- }
290
- );
291
- });
292
- }
293
263
  }
@@ -349,6 +349,11 @@ export class BtcChain implements IChain {
349
349
  });
350
350
  }
351
351
 
352
+ isPrePublishRawBound(_txp: TxProposal) {
353
+ // UTXO chains never mutate tx data at publish, so they never legitimately carry prePublishRaw.
354
+ return false;
355
+ }
356
+
352
357
  getBitcoreTx(txp, opts = { signed: true }) {
353
358
  const t = new this.bitcoreLib.Transaction();
354
359
 
@@ -287,6 +287,10 @@ export class EthChain implements IChain {
287
287
  });
288
288
  }
289
289
 
290
+ isPrePublishRawBound(txp: TxProposal) {
291
+ return Common.Utils.isPrePublishRawBound(this, txp);
292
+ }
293
+
290
294
  getBitcoreTx(txp: TxProposal, opts = { signed: true }) {
291
295
  const {
292
296
  data,
@@ -98,6 +98,10 @@ class ChainProxy {
98
98
  return this.get(txp.chain).getBitcoreTx(txp, { signed: opts.signed });
99
99
  }
100
100
 
101
+ isPrePublishRawBound(txp: TxProposal<any>) {
102
+ return this.get(txp.chain).isPrePublishRawBound(txp);
103
+ }
104
+
101
105
  convertFeePerKb(chain, p, feePerKb) {
102
106
  return this.get(chain).convertFeePerKb(p, feePerKb);
103
107
  }
@@ -2,6 +2,7 @@ import { Transactions, Utils, Validation } from '@bitpay-labs/crypto-wallet-core
2
2
  import _ from 'lodash';
3
3
  import { IChain } from '../../../types/chain';
4
4
  import { WalletWithOpts } from '../../blockchainexplorers/v8';
5
+ import { Common } from '../../common';
5
6
  import { Defaults } from '../../common/defaults';
6
7
  import { Errors } from '../../errors/errordefinitions';
7
8
  import logger from '../../logger';
@@ -28,6 +29,7 @@ export class SolChain implements IChain {
28
29
  totalConfirmedAmount: confirmed,
29
30
  lockedAmount: activatedLocked,
30
31
  lockedConfirmedAmount: activatedLocked,
32
+ reserve: balance > 0 ? minRent : 0,
31
33
  availableAmount: balance - activatedLocked,
32
34
  availableConfirmedAmount: confirmed - activatedLocked,
33
35
  byAddress: []
@@ -98,6 +100,10 @@ export class SolChain implements IChain {
98
100
  });
99
101
  }
100
102
 
103
+ isPrePublishRawBound(txp) {
104
+ return Common.Utils.isPrePublishRawBound(this, txp);
105
+ }
106
+
101
107
  getBitcoreTx(txp, opts = { signed: true }) {
102
108
  const {
103
109
  data,
@@ -29,6 +29,7 @@ export class XrpChain implements IChain {
29
29
  totalConfirmedAmount: confirmed,
30
30
  lockedAmount: activatedLocked,
31
31
  lockedConfirmedAmount: activatedLocked,
32
+ reserve: balance > 0 ? reserve : 0,
32
33
  availableAmount: balance - activatedLocked,
33
34
  availableConfirmedAmount: confirmed - activatedLocked,
34
35
  byAddress: []
@@ -143,6 +144,10 @@ export class XrpChain implements IChain {
143
144
  });
144
145
  }
145
146
 
147
+ isPrePublishRawBound(txp) {
148
+ return Common.Utils.isPrePublishRawBound(this, txp);
149
+ }
150
+
146
151
  getBitcoreTx(txp, opts = { signed: true }) {
147
152
  const { destinationTag, outputs, outputOrder, multiTx } = txp;
148
153
  const chain = 'XRP';
@@ -3,7 +3,8 @@ import {
3
3
  BitcoreLibCash,
4
4
  BitcoreLibDoge,
5
5
  BitcoreLibLtc,
6
- Constants as CWConstants
6
+ Constants as CWConstants,
7
+ Transactions
7
8
  } from '@bitpay-labs/crypto-wallet-core';
8
9
  import _ from 'lodash';
9
10
  import { singleton } from 'preconditions';
@@ -12,6 +13,8 @@ import Config from '../../config';
12
13
  import { logger } from '../logger';
13
14
  import { Constants } from './constants';
14
15
  import { Defaults } from './defaults';
16
+ import type { IChain } from '../../types/chain';
17
+ import type { TxProposal } from '../model/txproposal';
15
18
 
16
19
  const $ = singleton();
17
20
  const Bitcore_ = {
@@ -23,6 +26,54 @@ const Bitcore_ = {
23
26
 
24
27
  export const Utils = {
25
28
 
29
+ /**
30
+ * txp.prePublishRaw as a list of raw transactions, or null when it isn't one. An empty list is truthy and
31
+ * hashes to nothing, so without this the value reaches the tx provider and fails as a thrown error rather
32
+ * than a refused check. Mirrors the client-side Verifier helper so both ends agree on a usable fallback.
33
+ *
34
+ * @param txp - the transaction proposal
35
+ */
36
+ prePublishRawParts(txp: TxProposal<any>): string[] | null {
37
+ if (!txp?.prePublishRaw) {
38
+ return null;
39
+ }
40
+ const parts = Array.isArray(txp.prePublishRaw) ? txp.prePublishRaw : [txp.prePublishRaw];
41
+ if (!parts.length || parts.some(raw => !raw || typeof raw !== 'string')) {
42
+ return null;
43
+ }
44
+ return parts;
45
+ },
46
+
47
+ /**
48
+ * Shared implementation of IChain.isPrePublishRawBound for account-based chains (SVM/EVM/XRP). True only if
49
+ * txp.prePublishRaw is the same transaction as the current proposal, differing solely in the field BWS
50
+ * mutates at publish (blockhash on SVM, nonce on EVM/XRP). Recovers that field from prePublishRaw, rebuilds
51
+ * the proposal via the chain's own getBitcoreTx, and requires byte-for-byte equality, so a tampered stored
52
+ * proposal cannot reuse an old, still-valid proposalSignature. Fails closed on any error.
53
+ *
54
+ * @param chain - the IChain implementation (provides chain + getBitcoreTx)
55
+ * @param txp - the transaction proposal (must carry prePublishRaw)
56
+ */
57
+ isPrePublishRawBound(chain: IChain, txp: TxProposal<any>): boolean {
58
+ try {
59
+ const prePublishRaw = Utils.prePublishRawParts(txp);
60
+ if (!prePublishRaw) return false;
61
+ const provider = Transactions.get({ chain: txp.chain }) as any;
62
+ if (typeof provider?.getMutableFields !== 'function') return false;
63
+ const mutableFields = provider.getMutableFields(prePublishRaw[0]);
64
+ if (!mutableFields || Object.values(mutableFields).every(v => v == null)) return false;
65
+ const cloned = Object.assign(Object.create(Object.getPrototypeOf(txp)), txp, mutableFields);
66
+ // signed: false to match prePublishRaw, which is captured before any copayer signature exists.
67
+ const rebuilt = chain.getBitcoreTx(cloned, { signed: false }).uncheckedSerialize();
68
+ const rebuiltArr = Array.isArray(rebuilt) ? rebuilt : [rebuilt];
69
+ if (rebuiltArr.length !== prePublishRaw.length) return false;
70
+ return rebuiltArr.every((raw, i) => raw === prePublishRaw[i]);
71
+ } catch (err) {
72
+ logger.warn('prePublishRaw binding check failed for txp %s on %s: %o', txp?.id, txp?.chain, err.stack || err.message || err);
73
+ return false;
74
+ }
75
+ },
76
+
26
77
  /**
27
78
  * @deprecated
28
79
  * Moved from ChainService to prevent circular dependency
@@ -35,7 +35,7 @@ export function authTssRequest(): express.RequestHandler {
35
35
  if (req.path.includes('/tss/keygen/')) {
36
36
  session = await storage.fetchTssKeyGenSession({ id });
37
37
  partyId = session?.participants.indexOf(copayerId);
38
- pubKey = partyId > -1 ? session.rounds[0][partyId].messages.publicKey : null;
38
+ pubKey = partyId > -1 ? session.rounds[0].find(r => r.fromPartyId === partyId)?.messages.publicKey : null;
39
39
  } else if (req.path.includes('/tss/sign/')) {
40
40
  session = await storage.fetchTssSigSession({ id });
41
41
  partyId = session?.participants.find(p => p.copayerId === copayerId)?.partyId;
@@ -65,7 +65,7 @@ function respondWithAuthServer(req, res, context: RouteContext, handler: RouteHa
65
65
  }
66
66
 
67
67
  export function registerServiceRoutes(router: express.Router, context: RouteContext) {
68
- const { getServerWithAuth, setPublicCache, returnError } = context;
68
+ const { setPublicCache } = context;
69
69
 
70
70
  const transakCorsOptions = {
71
71
  origin: (origin, cb) => {
@@ -399,19 +399,6 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
399
399
  });
400
400
  });
401
401
 
402
- router.get('/v1/service/simplex/events', (req, res) => {
403
- getServerWithAuth(req, res, server => {
404
- Promise.resolve()
405
- .then(() => server.externalServices.simplex.simplexGetEvents({ env: req.query.env }))
406
- .then(response => {
407
- res.json(response);
408
- })
409
- .catch(err => {
410
- returnError(err ?? 'unknown', res, req);
411
- });
412
- });
413
- });
414
-
415
402
  router.post('/v1/service/thorswap/supportedChains', (req, res) => {
416
403
  respondWithPublicServer(req, res, context, server => {
417
404
  return server.externalServices.thorswap.thorswapGetSupportedChains(req);
package/src/lib/server.ts CHANGED
@@ -3120,8 +3120,12 @@ export class WalletService implements IWalletService {
3120
3120
 
3121
3121
  let signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
3122
3122
  if (!signingKey) {
3123
- // If the txp has been published previously, we will verify the signature against the previously published raw tx
3124
- if (txp.hasMutableTxData() && txp.prePublishRaw) {
3123
+ // The signature didn't match the current (refreshed) raw tx. Fall back to the previously-published
3124
+ // raw tx -- but only if it is bound to THIS proposal (differs from the current tx solely in the
3125
+ // field BWS mutates at publish: blockhash/nonce). Without that check a tampered stored proposal
3126
+ // could reuse an old, still-valid proposalSignature over prePublishRaw. Mirrors the client
3127
+ // Verifier.checkPrePublishRaw guard.
3128
+ if (txp.hasMutableTxData() && txp.prePublishRaw && ChainService.isPrePublishRawBound(txp)) {
3125
3129
  raw = txp.prePublishRaw;
3126
3130
  signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
3127
3131
  }
@@ -33,6 +33,7 @@ export interface IChain {
33
33
  checkScriptOutput(output: { script: string; amount: number });
34
34
  getFee(server: WalletService, wallet: IWallet, opts: { fee: number; feePerKb: number; signatures?: number } & any);
35
35
  getBitcoreTx(txp: TxProposal, opts: { signed: boolean });
36
+ isPrePublishRawBound(txp: TxProposal): boolean;
36
37
  convertFeePerKb(p: number, feePerKb: number);
37
38
  checkTx(server: WalletService, txp: ITxProposal);
38
39
  checkTxUTXOs(server: WalletService, txp: ITxProposal, opts: { noCashAddr: boolean } & any, cb);
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ import * as chai from 'chai';
4
+ import { ChainService } from '../../src/lib/chain';
5
+ import { Defaults } from '../../src/lib/common/defaults';
6
+
7
+ const should = chai.should();
8
+
9
+ describe('Chain SOL', function() {
10
+ describe('#getWalletBalance', function() {
11
+ const cases = [
12
+ { name: 'pending amounts and fees', amount: 1000000000, txps: [{ amount: '2000000000', fee: '5000' }], reserve: 1002240, locked: 2001007240, available: -1001007240 },
13
+ { name: 'dust below rent', amount: 10000, txps: [], reserve: 1002240, locked: 1002240, available: -992240 },
14
+ { name: 'an empty account with a pending proposal', amount: 0, txps: [{ amount: '2000000000', fee: '5000' }], reserve: 0, locked: 2000005000, available: -2000005000 },
15
+ { name: 'an SPL balance', amount: 1000000, txps: [{ tokenAddress: 'mint', amount: '500000', fee: '5000' }], tokenAddress: 'mint', reserve: 0, locked: 500000, available: 500000 }
16
+ ];
17
+
18
+ for (const c of cases) {
19
+ it('should expose the reserve separately for ' + c.name, function(done) {
20
+ const server = {
21
+ walletId: '1',
22
+ _getBlockchainExplorer: () => ({
23
+ getBalance: (_wallet, cb) => cb(null, { balance: c.amount, confirmed: c.amount }),
24
+ getRentMinimum: (_space, cb) => cb(null, Defaults.MIN_SOL_BALANCE)
25
+ }),
26
+ getPendingTxs: (_opts, cb) => cb(null, c.txps),
27
+ storage: { fetchAddresses: (_walletId, cb) => cb(null, []) }
28
+ };
29
+ const wallet = { chain: 'sol', network: 'livenet' };
30
+
31
+ ChainService.get('sol').getWalletBalance(server as any, wallet as any, { tokenAddress: (c as { tokenAddress?: string }).tokenAddress }, (err, balance) => {
32
+ should.not.exist(err);
33
+ balance.should.deep.equal({
34
+ totalAmount: c.amount,
35
+ totalConfirmedAmount: c.amount,
36
+ lockedAmount: c.locked,
37
+ lockedConfirmedAmount: c.locked,
38
+ reserve: c.reserve,
39
+ availableAmount: c.available,
40
+ availableConfirmedAmount: c.available,
41
+ byAddress: []
42
+ });
43
+ done();
44
+ });
45
+ });
46
+ }
47
+ });
48
+ });
@@ -10,6 +10,36 @@ const should = chai.should();
10
10
  const xpub = 'tpubDD7tYYerLNNm65Ez7pRjxQ2NpRHDoyRWoLWudnQ8agXjr7qs9BsPsRXk8Z6spPPJodnaY158YqeCKT5oXuZvbuNLfm1R4kXGJ2vPd9pUxDT';
11
11
 
12
12
  describe('Chain XRP', function() {
13
+ describe('#getWalletBalance', function() {
14
+ it('should expose the reserve separately from pending amounts and fees', function(done) {
15
+ const server = {
16
+ walletId: '1',
17
+ _getBlockchainExplorer: () => ({
18
+ getBalance: (_wallet, cb) => cb(null, { balance: 2000000, confirmed: 2000000 }),
19
+ getReserve: cb => cb(null, 1000000)
20
+ }),
21
+ getPendingTxs: (_opts, cb) => cb(null, [{ amount: '3000000', fee: '12' }]),
22
+ storage: { fetchAddresses: (_walletId, cb) => cb(null, []) }
23
+ };
24
+ const wallet = { chain: 'xrp', network: 'livenet' };
25
+
26
+ ChainService.get('xrp').getWalletBalance(server as any, wallet as any, {}, (err, balance) => {
27
+ should.not.exist(err);
28
+ balance.should.deep.equal({
29
+ totalAmount: 2000000,
30
+ totalConfirmedAmount: 2000000,
31
+ lockedAmount: 4000012,
32
+ lockedConfirmedAmount: 4000012,
33
+ reserve: 1000000,
34
+ availableAmount: -2000012,
35
+ availableConfirmedAmount: -2000012,
36
+ byAddress: []
37
+ });
38
+ done();
39
+ });
40
+ });
41
+ });
42
+
13
43
  describe('#getWalletSendMaxInfo', function() {
14
44
  it('should resolve fee level before calculating sendMax amount', function(done) {
15
45
  const server = {
@@ -503,22 +503,4 @@ describe('Simplex integration', () => {
503
503
  }
504
504
  });
505
505
  });
506
-
507
- describe('#simplexGetEvents', () => {
508
- beforeEach(() => {
509
- req = {
510
- env: 'production'
511
- };
512
-
513
- fakeRequest = {
514
- get: (_url, _opts, _cb) => { return _cb(null, { body: {} }); },
515
- };
516
- server.externalServices.simplex.request = fakeRequest;
517
- });
518
-
519
- it('should work properly if req is OK', async () => {
520
- const data = await server.externalServices.simplex.simplexGetEvents(req);
521
- should.exist(data);
522
- });
523
- });
524
506
  });
@@ -12,6 +12,7 @@ import config from '../../src/config';
12
12
  import { WalletService, UPGRADES } from '../../src/lib/server';
13
13
  import { Storage } from '../../src/lib/storage';
14
14
  import { Common } from '../../src/lib/common';
15
+ import logger from '../../src/lib/logger';
15
16
  import * as Model from '../../src/lib/model';
16
17
  import { TssKeyGenModel } from '../../src/lib/model/tsskeygen';
17
18
  import { BCHAddressTranslator } from '../../src/lib/bchaddresstranslator';
@@ -8821,6 +8822,85 @@ describe('Wallet service', function() {
8821
8822
  result.nonce.should.equal(5);
8822
8823
  // future: result.gasPrice, result.maxFee would also be set here
8823
8824
  });
8825
+
8826
+ describe('prePublishRaw binding (isPrePublishRawBound)', function() {
8827
+ const ATTACKER_ADDR = '0x1111111111111111111111111111111111111111';
8828
+
8829
+ it('accepts prePublishRaw when only the mutable field (nonce) changed', async function() {
8830
+ blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '42');
8831
+ const created = await helpers.createAndPublishTx(server, {
8832
+ outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
8833
+ feePerKb: 123e2, from: fromAddr, deferNonce: true
8834
+ }, TestData.copayers[0].privKey_1H_0);
8835
+
8836
+ // Server assigns the JIT nonce -> the stored tx now differs from prePublishRaw only in the nonce.
8837
+ await util.promisify(server.prepareTx).call(server, { txProposalId: created.id });
8838
+ const withNonce = await util.promisify(server.getTx).call(server, { txProposalId: created.id });
8839
+ should.exist(withNonce.prePublishRaw);
8840
+ withNonce.nonce.should.equal(42);
8841
+
8842
+ ChainService.isPrePublishRawBound(withNonce).should.equal(true);
8843
+ });
8844
+
8845
+ it('rejects prePublishRaw when the destination was tampered', async function() {
8846
+ const created = await helpers.createAndPublishTx(server, {
8847
+ outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
8848
+ feePerKb: 123e2, from: fromAddr, deferNonce: true
8849
+ }, TestData.copayers[0].privKey_1H_0);
8850
+
8851
+ const txp = await util.promisify(server.getTx).call(server, { txProposalId: created.id });
8852
+ should.exist(txp.prePublishRaw);
8853
+ ChainService.isPrePublishRawBound(txp).should.equal(true); // bound before tampering
8854
+ txp.outputs[0].toAddress = ATTACKER_ADDR; // swap payee, keep prePublishRaw + proposalSignature
8855
+ ChainService.isPrePublishRawBound(txp).should.equal(false);
8856
+ });
8857
+
8858
+ it('rejects a prePublishRaw that holds no usable raw tx', async function() {
8859
+ const created = await helpers.createAndPublishTx(server, {
8860
+ outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
8861
+ feePerKb: 123e2, from: fromAddr, deferNonce: true
8862
+ }, TestData.copayers[0].privKey_1H_0);
8863
+
8864
+ const txp = await util.promisify(server.getTx).call(server, { txProposalId: created.id });
8865
+ const warn = sinon.spy(logger, 'warn');
8866
+ try {
8867
+ // an empty list is truthy, so it has to be refused up front rather than handed to the tx
8868
+ // provider, which would fail the same check by throwing
8869
+ for (const prePublishRaw of [[], [''], ['', '']]) {
8870
+ txp.prePublishRaw = prePublishRaw;
8871
+ ChainService.isPrePublishRawBound(txp).should.equal(false);
8872
+ }
8873
+ warn.called.should.equal(false);
8874
+ } finally {
8875
+ warn.restore();
8876
+ }
8877
+ });
8878
+
8879
+ it('rejects the publishTx fallback when the stored proposal was tampered', async function() {
8880
+ blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '7');
8881
+ const created = await util.promisify(server.createTx).call(server, {
8882
+ outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
8883
+ feePerKb: 123e2, from: fromAddr, deferNonce: true
8884
+ });
8885
+ const publishOpts = helpers.getProposalSignatureOpts(created, TestData.copayers[0].privKey_1H_0);
8886
+ await util.promisify(server.publishTx).call(server, publishOpts);
8887
+
8888
+ // Assign the nonce so a re-publish must fall back to prePublishRaw...
8889
+ await util.promisify(server.prepareTx).call(server, { txProposalId: created.id });
8890
+ // ...then tamper the stored destination and replay the original (still-valid) proposalSignature.
8891
+ const stored = await util.promisify(server.storage.fetchTx).call(server.storage, wallet.id, created.id);
8892
+ should.exist(stored.prePublishRaw);
8893
+ stored.outputs[0].toAddress = ATTACKER_ADDR;
8894
+ await util.promisify(server.storage.storeTx).call(server.storage, wallet.id, stored);
8895
+
8896
+ let err;
8897
+ try {
8898
+ await util.promisify(server.publishTx).call(server, publishOpts);
8899
+ } catch (e) { err = e; }
8900
+ should.exist(err);
8901
+ err.message.should.contain('Invalid proposal signature');
8902
+ });
8903
+ });
8824
8904
  });
8825
8905
 
8826
8906
  describe('#prepareTx XRP (deferred nonce)', function() {
@@ -8912,6 +8992,20 @@ describe('Wallet service', function() {
8912
8992
  });
8913
8993
  signed.status.should.equal('accepted');
8914
8994
  });
8995
+
8996
+ it('rejects prePublishRaw binding when an XRP destination was tampered', async function() {
8997
+ const ATTACKER_XRP = 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe';
8998
+ const created = await helpers.createAndPublishTx(server, {
8999
+ outputs: [{ toAddress: XRP_ADDR, amount: 8000 }],
9000
+ feePerKb: 123e2, from: fromAddr, deferNonce: true
9001
+ }, TestData.copayers[0].privKey_1H_0);
9002
+
9003
+ const txp = await util.promisify(server.getTx).call(server, { txProposalId: created.id });
9004
+ should.exist(txp.prePublishRaw);
9005
+ ChainService.isPrePublishRawBound(txp).should.equal(true); // untampered baseline
9006
+ txp.outputs[0].toAddress = ATTACKER_XRP;
9007
+ ChainService.isPrePublishRawBound(txp).should.equal(false);
9008
+ });
8915
9009
  });
8916
9010
 
8917
9011
  describe('#signTx nonce override', function() {
@@ -379,6 +379,73 @@ describe('TSS', function() {
379
379
  });
380
380
  });
381
381
 
382
+ describe('GET /v1/tss/keygen/:id/:round', function() {
383
+ const roundUrl = '/v1/tss/keygen/test/0?maxWaitTime=0';
384
+ const parties = [vector.party0, vector.party1, vector.party2];
385
+ const round0Messages = [vector.keygen.messages.round0.party0, vector.keygen.messages.round0.party1, vector.keygen.messages.round0.party2];
386
+
387
+ const joinRound0 = async (partyId: number) => {
388
+ const publicKey = parties[partyId].authKey.publicKey.toString();
389
+ const message = { ...round0Messages[partyId], partyId, round: 0, publicKey };
390
+ await app.post(`${urlPrefix}/v1/tss/keygen/test`)
391
+ .set('x-identity', publicKey)
392
+ .send({ message, n: 3, version: 1.1 })
393
+ .expect(200);
394
+ };
395
+
396
+ const getRound0 = (partyId: number) => {
397
+ const { authKey } = parties[partyId];
398
+ const hash = Utils.hashMessage(`get|${roundUrl}|{}`, false);
399
+ const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, authKey, { endian: 'little' }).toString();
400
+ return app.get(urlPrefix + roundUrl)
401
+ .set('x-identity', authKey.publicKey.toString())
402
+ .set('x-signature', xSignature);
403
+ };
404
+
405
+ beforeEach(async function() {
406
+ const storage = helpers.getStorage();
407
+ await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
408
+ });
409
+
410
+ it('should authenticate every party when round 0 messages arrive in partyId order', async function() {
411
+ await joinRound0(0);
412
+ await joinRound0(1);
413
+ await joinRound0(2);
414
+
415
+ for (const partyId of [0, 1, 2]) {
416
+ const res = await getRound0(partyId);
417
+ res.status.should.equal(200);
418
+ JSON.parse(res.text).messages.length.should.equal(2);
419
+ }
420
+ });
421
+
422
+ it('should authenticate every party when round 0 messages arrive out of partyId order', async function() {
423
+ await joinRound0(0);
424
+ await joinRound0(2);
425
+ await joinRound0(1);
426
+
427
+ for (const partyId of [0, 1, 2]) {
428
+ const res = await getRound0(partyId);
429
+ res.status.should.equal(200);
430
+ JSON.parse(res.text).messages.length.should.equal(2);
431
+ }
432
+ });
433
+
434
+ it('should reject another party identity when round 0 messages arrive out of partyId order', async function() {
435
+ await joinRound0(0);
436
+ await joinRound0(2);
437
+ await joinRound0(1);
438
+
439
+ for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
440
+ const res = await getRound0(partyId)
441
+ .set('x-identity', parties[otherPartyId].authKey.publicKey.toString());
442
+ res.status.should.equal(401);
443
+ res.body.code.should.equal('NOT_AUTHORIZED');
444
+ res.body.message.should.equal('Invalid signature');
445
+ }
446
+ });
447
+ });
448
+
382
449
  describe('POST /v1/tss/sign/:id', function() {
383
450
  const url = id => `${urlPrefix}/v1/tss/sign/${id}`;
384
451
  const signRequest = body => {
@@ -2,7 +2,7 @@
2
2
  "name": "@bitpay-labs/bitcore-wallet-service",
3
3
  "description": "A service for Mutisig HD Bitcoin Wallets",
4
4
  "author": "BitPay Inc",
5
- "version": "11.10.10",
5
+ "version": "11.10.11",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@bitpay-labs/bitcore-logging": "^11.10.9",
45
- "@bitpay-labs/bitcore-tss": "^11.10.9",
46
- "@bitpay-labs/crypto-wallet-core": "^11.10.9",
45
+ "@bitpay-labs/bitcore-tss": "^11.10.11",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.10.11",
47
47
  "@sendgrid/mail": "6.5.4",
48
48
  "abi-decoder": "2.4.0",
49
49
  "async": "0.9.2",
@@ -7,6 +7,5 @@ export declare class SimplexService {
7
7
  simplexGetSellQuote(req: any): Promise<any>;
8
8
  simplexPaymentRequest(req: any): Promise<any>;
9
9
  simplexSellPaymentRequest(req: any): Promise<any>;
10
- simplexGetEvents(req: any): Promise<any>;
11
10
  }
12
11
  //# sourceMappingURL=simplex.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"simplex.d.ts","sourceRoot":"","sources":["../../../src/externalservices/simplex.ts"],"names":[],"mappings":"AAOA,qBAAa,cAAc;IACzB,OAAO,EAAE,GAAG,CAAW;IAEvB,OAAO,CAAC,cAAc;IAwBtB,OAAO,CAAC,gBAAgB;IAYxB,oBAAoB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6BvC,eAAe,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IAuClC,mBAAmB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6CtC,qBAAqB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6DxC,yBAAyB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA2C5C,gBAAgB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;CA6BpC"}
1
+ {"version":3,"file":"simplex.d.ts","sourceRoot":"","sources":["../../../src/externalservices/simplex.ts"],"names":[],"mappings":"AAOA,qBAAa,cAAc;IACzB,OAAO,EAAE,GAAG,CAAW;IAEvB,OAAO,CAAC,cAAc;IAwBtB,OAAO,CAAC,gBAAgB;IAYxB,oBAAoB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6BvC,eAAe,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IAuClC,mBAAmB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6CtC,qBAAqB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;IA6DxC,yBAAyB,CAAC,GAAG,KAAA,GAAG,OAAO,CAAC,GAAG,CAAC;CA0C7C"}
@@ -240,31 +240,6 @@ class SimplexService {
240
240
  });
241
241
  });
242
242
  }
243
- simplexGetEvents(req) {
244
- return new Promise((resolve, reject) => {
245
- if (!config_1.default.simplex)
246
- return reject(new Error('Simplex missing credentials'));
247
- if (!req.env || (req.env != 'sandbox' && req.env != 'production'))
248
- return reject(new Error("Simplex's request wrong environment"));
249
- const API = config_1.default.simplex[req.env].api;
250
- const API_KEY = config_1.default.simplex[req.env].apiKey;
251
- const headers = {
252
- 'Content-Type': 'application/json',
253
- Authorization: 'ApiKey ' + API_KEY
254
- };
255
- this.request.get(API + '/wallet/merchant/v2/events', {
256
- headers,
257
- json: true
258
- }, (err, data) => {
259
- if (err) {
260
- return reject(err.body ? err.body : null);
261
- }
262
- else {
263
- return resolve(data.body ? data.body : null);
264
- }
265
- });
266
- });
267
- }
268
243
  }
269
244
  exports.SimplexService = SimplexService;
270
245
  //# sourceMappingURL=simplex.js.map