@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.
- package/package.json +4 -4
- package/src/externalservices/simplex.ts +0 -30
- package/src/lib/chain/btc/index.ts +5 -0
- package/src/lib/chain/eth/index.ts +4 -0
- package/src/lib/chain/index.ts +4 -0
- package/src/lib/chain/sol/index.ts +6 -0
- package/src/lib/chain/xrp/index.ts +5 -0
- package/src/lib/common/utils.ts +52 -1
- package/src/lib/routes/middleware/authTssRequest.ts +1 -1
- package/src/lib/routes/services.ts +1 -14
- package/src/lib/server.ts +6 -2
- package/src/types/chain.d.ts +1 -0
- package/test/chain/sol.test.ts +48 -0
- package/test/chain/xrp.test.ts +30 -0
- package/test/integration/externalservices/simplex.test.ts +0 -18
- package/test/integration/server.test.ts +94 -0
- package/test/integration/tss.test.ts +67 -0
- package/ts_build/package.json +3 -3
- package/ts_build/src/externalservices/simplex.d.ts +0 -1
- package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
- package/ts_build/src/externalservices/simplex.js +0 -25
- package/ts_build/src/externalservices/simplex.js.map +1 -1
- package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -0
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +3 -0
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/index.d.ts +1 -0
- package/ts_build/src/lib/chain/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/index.js +3 -0
- package/ts_build/src/lib/chain/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
- package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +5 -0
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
- package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +4 -0
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -0
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +4 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +33 -0
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +1 -13
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +1 -1
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/test/chain/sol.test.d.ts +2 -0
- package/ts_build/test/chain/sol.test.d.ts.map +1 -0
- package/ts_build/test/chain/sol.test.js +78 -0
- package/ts_build/test/chain/sol.test.js.map +1 -0
- package/ts_build/test/chain/xrp.test.js +28 -0
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
- package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +80 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.js +57 -0
- 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.
|
|
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.
|
|
46
|
-
"@bitpay-labs/crypto-wallet-core": "^11.10.
|
|
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": "
|
|
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,
|
package/src/lib/chain/index.ts
CHANGED
|
@@ -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';
|
package/src/lib/common/utils.ts
CHANGED
|
@@ -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]
|
|
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 {
|
|
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
|
-
//
|
|
3124
|
-
if (
|
|
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
|
}
|
package/src/types/chain.d.ts
CHANGED
|
@@ -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
|
+
});
|
package/test/chain/xrp.test.ts
CHANGED
|
@@ -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 => {
|
package/ts_build/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.
|
|
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.
|
|
46
|
-
"@bitpay-labs/crypto-wallet-core": "^11.10.
|
|
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;
|
|
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
|