@bitpay-labs/bitcore-wallet-service 11.10.7 → 11.10.8
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 +3 -3
- package/src/externalservices/coingecko.ts +24 -10
- package/src/externalservices/moonpay.ts +23 -1
- package/src/lib/chain/eth/abi-invoice.ts +139 -90
- package/src/lib/common/constants.ts +5 -2
- package/src/lib/common/defaults.ts +11 -2
- package/src/lib/errors/errordefinitions.ts +6 -0
- package/src/lib/model/copayer.ts +1 -0
- package/src/lib/model/tsskeygen.ts +33 -4
- package/src/lib/model/tsssign.ts +34 -3
- package/src/lib/pushnotificationsservice.ts +3 -1
- package/src/lib/routes/middleware/authTssRequest.ts +10 -4
- package/src/lib/routes/middleware/verifyTssMessage.ts +13 -8
- package/src/lib/routes/tss.ts +11 -4
- package/src/lib/server.ts +53 -25
- package/src/lib/storage.ts +24 -1
- package/src/lib/tss.ts +232 -30
- package/src/types/externalservices.d.ts +29 -1
- package/test/integration/externalservices/coinGecko.test.ts +71 -7
- package/test/integration/server.test.ts +731 -1
- package/test/integration/tss.test.ts +275 -0
- package/test/model/copayer.test.ts +128 -0
- package/test/storage.test.ts +91 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
- package/ts_build/src/externalservices/coingecko.js +14 -8
- package/ts_build/src/externalservices/coingecko.js.map +1 -1
- package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
- package/ts_build/src/externalservices/moonpay.js +38 -2
- package/ts_build/src/externalservices/moonpay.js.map +1 -1
- package/ts_build/src/lib/chain/eth/abi-invoice.d.ts +254 -41
- package/ts_build/src/lib/chain/eth/abi-invoice.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/abi-invoice.js +138 -89
- package/ts_build/src/lib/chain/eth/abi-invoice.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +4 -2
- package/ts_build/src/lib/common/constants.d.ts.map +1 -1
- package/ts_build/src/lib/common/constants.js +4 -2
- package/ts_build/src/lib/common/constants.js.map +1 -1
- package/ts_build/src/lib/common/defaults.d.ts +4 -2
- package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
- package/ts_build/src/lib/common/defaults.js +4 -2
- package/ts_build/src/lib/common/defaults.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +8 -4
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +3 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +3 -0
- package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
- package/ts_build/src/lib/model/copayer.d.ts.map +1 -1
- package/ts_build/src/lib/model/copayer.js +1 -0
- package/ts_build/src/lib/model/copayer.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +3 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +12 -3
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/model/tsssign.d.ts +7 -0
- package/ts_build/src/lib/model/tsssign.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsssign.js +22 -2
- package/ts_build/src/lib/model/tsssign.js.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.js +3 -1
- package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +8 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +11 -6
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +4 -4
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +33 -25
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +4 -0
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +18 -1
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +4 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +74 -17
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.js +62 -8
- package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +597 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.js +245 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
- package/ts_build/test/model/copayer.test.js +91 -0
- package/ts_build/test/model/copayer.test.js.map +1 -1
- package/ts_build/test/storage.test.js +73 -0
- package/ts_build/test/storage.test.js.map +1 -1
|
@@ -4,6 +4,8 @@ import * as chai from 'chai';
|
|
|
4
4
|
import 'chai/register-should';
|
|
5
5
|
import util from 'util';
|
|
6
6
|
import sinon from 'sinon';
|
|
7
|
+
import http from 'http';
|
|
8
|
+
import request from 'request';
|
|
7
9
|
import * as CWC from '@bitpay-labs/crypto-wallet-core';
|
|
8
10
|
import { ChainService } from '../../src/lib/chain/index';
|
|
9
11
|
import config from '../../src/config';
|
|
@@ -11,10 +13,12 @@ import { WalletService, UPGRADES } from '../../src/lib/server';
|
|
|
11
13
|
import { Storage } from '../../src/lib/storage';
|
|
12
14
|
import { Common } from '../../src/lib/common';
|
|
13
15
|
import * as Model from '../../src/lib/model';
|
|
16
|
+
import { TssKeyGenModel } from '../../src/lib/model/tsskeygen';
|
|
14
17
|
import { BCHAddressTranslator } from '../../src/lib/bchaddresstranslator';
|
|
15
18
|
import * as TestData from '../testdata';
|
|
16
19
|
import helpers from './helpers';
|
|
17
20
|
import { ClientError } from '../../src/lib/errors/clienterror';
|
|
21
|
+
import { ExpressApp } from '../../src/lib/expressapp';
|
|
18
22
|
|
|
19
23
|
const should = chai.should();
|
|
20
24
|
config.moralis = config.moralis ?? {
|
|
@@ -1135,6 +1139,271 @@ describe('Wallet service', function() {
|
|
|
1135
1139
|
});
|
|
1136
1140
|
});
|
|
1137
1141
|
|
|
1142
|
+
it('should fail to join with wrong signature even when hardwareSourcePublicKey is present', function(done) {
|
|
1143
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1144
|
+
walletId: walletId,
|
|
1145
|
+
name: 'me',
|
|
1146
|
+
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
1147
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1148
|
+
});
|
|
1149
|
+
copayerOpts.name = 'me2'; // invalidates the signature, same as the plain wrong-signature case above
|
|
1150
|
+
copayerOpts.hardwareSourcePublicKey = 'attacker-controlled-hw-pubkey';
|
|
1151
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1152
|
+
should.not.exist(result);
|
|
1153
|
+
should.exist(err);
|
|
1154
|
+
err.message.should.equal('Bad request');
|
|
1155
|
+
// The rejected join must not have persisted a copayer despite carrying
|
|
1156
|
+
// hardwareSourcePublicKey - a regression that returns an error but still adds the
|
|
1157
|
+
// copayer would pass the assertions above alone.
|
|
1158
|
+
server.getWallet({}, function(err, wallet) {
|
|
1159
|
+
should.not.exist(err);
|
|
1160
|
+
wallet.copayers.length.should.equal(0);
|
|
1161
|
+
done();
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it('should fail to join with wrong signature even when clientDerivedPublicKey is present', function(done) {
|
|
1167
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1168
|
+
walletId: walletId,
|
|
1169
|
+
name: 'me',
|
|
1170
|
+
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
1171
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1172
|
+
});
|
|
1173
|
+
copayerOpts.name = 'me2'; // invalidates the signature, same as the plain wrong-signature case above
|
|
1174
|
+
copayerOpts.clientDerivedPublicKey = 'attacker-controlled-client-derived-pubkey';
|
|
1175
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1176
|
+
should.not.exist(result);
|
|
1177
|
+
should.exist(err);
|
|
1178
|
+
err.message.should.equal('Bad request');
|
|
1179
|
+
// Same persisted-state guard as the hardwareSourcePublicKey case above.
|
|
1180
|
+
server.getWallet({}, function(err, wallet) {
|
|
1181
|
+
should.not.exist(err);
|
|
1182
|
+
wallet.copayers.length.should.equal(0);
|
|
1183
|
+
done();
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('should join existing wallet with a valid signature and hardwareSourcePublicKey present', function(done) {
|
|
1189
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1190
|
+
walletId: walletId,
|
|
1191
|
+
name: 'me',
|
|
1192
|
+
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
1193
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1194
|
+
});
|
|
1195
|
+
copayerOpts.hardwareSourcePublicKey = 'legit-hw-pubkey';
|
|
1196
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1197
|
+
should.not.exist(err);
|
|
1198
|
+
should.exist(result);
|
|
1199
|
+
should.exist(result.copayerId);
|
|
1200
|
+
server.getWallet({}, function(err, wallet) {
|
|
1201
|
+
should.not.exist(err);
|
|
1202
|
+
wallet.copayers.length.should.equal(1);
|
|
1203
|
+
const copayer = wallet.copayers[0];
|
|
1204
|
+
copayer.id.should.equal(result.copayerId);
|
|
1205
|
+
copayer.hardwareSourcePublicKey.should.equal('legit-hw-pubkey');
|
|
1206
|
+
done();
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it('should join existing wallet with a valid signature and clientDerivedPublicKey present', function(done) {
|
|
1212
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1213
|
+
walletId: walletId,
|
|
1214
|
+
name: 'me',
|
|
1215
|
+
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
1216
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1217
|
+
});
|
|
1218
|
+
copayerOpts.clientDerivedPublicKey = 'legit-client-derived-pubkey';
|
|
1219
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1220
|
+
should.not.exist(err);
|
|
1221
|
+
should.exist(result);
|
|
1222
|
+
should.exist(result.copayerId);
|
|
1223
|
+
server.getWallet({}, function(err, wallet) {
|
|
1224
|
+
should.not.exist(err);
|
|
1225
|
+
wallet.copayers.length.should.equal(1);
|
|
1226
|
+
const copayer = wallet.copayers[0];
|
|
1227
|
+
copayer.id.should.equal(result.copayerId);
|
|
1228
|
+
copayer.clientDerivedPublicKey.should.equal('legit-client-derived-pubkey');
|
|
1229
|
+
done();
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('should reject a join with hardwareSourcePublicKey present when xPubKey is absent', function(done) {
|
|
1235
|
+
// xPubKey is the canonical copayer identity for every join; the ancillary
|
|
1236
|
+
// hardwareSourcePublicKey field is metadata and must not stand in for it. The
|
|
1237
|
+
// signature is created while xPubKey is genuinely absent - it covers
|
|
1238
|
+
// [name, undefined, requestPubKey].join('|') === 'me||<requestPubKey>' - so the
|
|
1239
|
+
// rejection below is attributable to the missing identity field, not to a
|
|
1240
|
+
// signature invalidated by deleting an xpub after signing.
|
|
1241
|
+
const copayerOpts: any = helpers.getSignedCopayerOpts({
|
|
1242
|
+
walletId: walletId,
|
|
1243
|
+
name: 'me',
|
|
1244
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1245
|
+
});
|
|
1246
|
+
should.not.exist(copayerOpts.xPubKey);
|
|
1247
|
+
copayerOpts.hardwareSourcePublicKey = 'legit-hw-pubkey-no-xpub';
|
|
1248
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1249
|
+
should.not.exist(result);
|
|
1250
|
+
should.exist(err);
|
|
1251
|
+
err.should.be.instanceof(ClientError);
|
|
1252
|
+
// The rejected join must not have persisted a copayer. The join is rejected
|
|
1253
|
+
// before WalletService.walletId is set, so an unauthenticated getWallet({})
|
|
1254
|
+
// cannot address the wallet, and there is no copayerId to authenticate with;
|
|
1255
|
+
// read persisted state at the storage level instead.
|
|
1256
|
+
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
1257
|
+
should.not.exist(err);
|
|
1258
|
+
should.exist(wallet);
|
|
1259
|
+
wallet.copayers.length.should.equal(0);
|
|
1260
|
+
done();
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// Table-driven rather than one near-identical 'it' per field: the point of each case is
|
|
1266
|
+
// that this *particular* ancillary field must not stand in for xPubKey, so looping over
|
|
1267
|
+
// the field name keeps that assertion identical across cases and makes it obvious if a
|
|
1268
|
+
// future ancillary field (a third credential type) is added without a matching case here.
|
|
1269
|
+
for (const ancillaryField of ['hardwareSourcePublicKey', 'clientDerivedPublicKey']) {
|
|
1270
|
+
it(`should reject an xPubKey-less ${ancillaryField} join without persisting a copayer`, function(done) {
|
|
1271
|
+
const copayerOpts: any = helpers.getSignedCopayerOpts({
|
|
1272
|
+
walletId: walletId,
|
|
1273
|
+
name: 'me',
|
|
1274
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1275
|
+
});
|
|
1276
|
+
should.not.exist(copayerOpts.xPubKey);
|
|
1277
|
+
copayerOpts[ancillaryField] = 'legit-value-no-xpub';
|
|
1278
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1279
|
+
should.not.exist(result);
|
|
1280
|
+
should.exist(err);
|
|
1281
|
+
err.should.be.instanceof(ClientError);
|
|
1282
|
+
// The join is rejected before WalletService.walletId is set, so an unauthenticated
|
|
1283
|
+
// getWallet({}) cannot address the wallet; read persisted state at the storage level.
|
|
1284
|
+
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
1285
|
+
should.not.exist(err);
|
|
1286
|
+
should.exist(wallet);
|
|
1287
|
+
wallet.copayers.length.should.equal(0);
|
|
1288
|
+
done();
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
it('should fail to join with a malformed xPubKey even when hardwareSourcePublicKey is present', function(done) {
|
|
1295
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1296
|
+
walletId: walletId,
|
|
1297
|
+
name: 'me',
|
|
1298
|
+
xPubKey: 'invalid',
|
|
1299
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1300
|
+
});
|
|
1301
|
+
copayerOpts.hardwareSourcePublicKey = 'legit-hw-pubkey';
|
|
1302
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1303
|
+
should.not.exist(result);
|
|
1304
|
+
should.exist(err);
|
|
1305
|
+
err.should.be.instanceof(ClientError);
|
|
1306
|
+
// The rejected join must not have persisted a copayer; read persisted state
|
|
1307
|
+
// at the storage level (the join may be rejected before WalletService.walletId
|
|
1308
|
+
// is set, in which case getWallet({}) cannot address the wallet).
|
|
1309
|
+
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
1310
|
+
should.not.exist(err);
|
|
1311
|
+
should.exist(wallet);
|
|
1312
|
+
wallet.copayers.length.should.equal(0);
|
|
1313
|
+
done();
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('should fail to join with a malformed xPubKey even when clientDerivedPublicKey is present', function(done) {
|
|
1319
|
+
const copayerOpts = helpers.getSignedCopayerOpts({
|
|
1320
|
+
walletId: walletId,
|
|
1321
|
+
name: 'me',
|
|
1322
|
+
xPubKey: 'invalid',
|
|
1323
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1324
|
+
});
|
|
1325
|
+
copayerOpts.clientDerivedPublicKey = 'legit-client-derived-pubkey';
|
|
1326
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1327
|
+
should.not.exist(result);
|
|
1328
|
+
should.exist(err);
|
|
1329
|
+
err.should.be.instanceof(ClientError);
|
|
1330
|
+
// Same persisted-state guard as the hardwareSourcePublicKey case above.
|
|
1331
|
+
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
1332
|
+
should.not.exist(err);
|
|
1333
|
+
should.exist(wallet);
|
|
1334
|
+
wallet.copayers.length.should.equal(0);
|
|
1335
|
+
done();
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
it('should reject a join when xPubKey, hardwareSourcePublicKey and clientDerivedPublicKey are all missing or empty', function(done) {
|
|
1341
|
+
const copayerOpts: any = helpers.getSignedCopayerOpts({
|
|
1342
|
+
walletId: walletId,
|
|
1343
|
+
name: 'me',
|
|
1344
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1345
|
+
});
|
|
1346
|
+
should.not.exist(copayerOpts.xPubKey);
|
|
1347
|
+
copayerOpts.hardwareSourcePublicKey = '';
|
|
1348
|
+
copayerOpts.clientDerivedPublicKey = '';
|
|
1349
|
+
server.joinWallet(copayerOpts, function(err, result) {
|
|
1350
|
+
should.not.exist(result);
|
|
1351
|
+
should.exist(err);
|
|
1352
|
+
err.should.be.instanceof(ClientError);
|
|
1353
|
+
// The rejected join must not have persisted a copayer. The join is rejected
|
|
1354
|
+
// before WalletService.walletId is set, so an unauthenticated getWallet({})
|
|
1355
|
+
// cannot address the wallet, and there is no copayerId to authenticate with;
|
|
1356
|
+
// read persisted state at the storage level instead.
|
|
1357
|
+
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
1358
|
+
should.not.exist(err);
|
|
1359
|
+
should.exist(wallet);
|
|
1360
|
+
wallet.copayers.length.should.equal(0);
|
|
1361
|
+
done();
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
it('should fail to join a full wallet with a valid signature and hardwareSourcePublicKey present', function(done) {
|
|
1367
|
+
// walletId is m=1/n=2 (see beforeEach above); fill both slots with normal joins first
|
|
1368
|
+
// so the third, hardware-flagged join hits WALLET_FULL rather than succeeding.
|
|
1369
|
+
const firstCopayerOpts = helpers.getSignedCopayerOpts({
|
|
1370
|
+
walletId: walletId,
|
|
1371
|
+
name: 'copayer 1',
|
|
1372
|
+
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
1373
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1374
|
+
});
|
|
1375
|
+
server.joinWallet(firstCopayerOpts, function(err) {
|
|
1376
|
+
should.not.exist(err);
|
|
1377
|
+
const secondCopayerOpts = helpers.getSignedCopayerOpts({
|
|
1378
|
+
walletId: walletId,
|
|
1379
|
+
name: 'copayer 2',
|
|
1380
|
+
xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H,
|
|
1381
|
+
requestPubKey: TestData.copayers[1].pubKey_1H_0,
|
|
1382
|
+
});
|
|
1383
|
+
server.joinWallet(secondCopayerOpts, function(err) {
|
|
1384
|
+
should.not.exist(err);
|
|
1385
|
+
const thirdCopayerOpts = helpers.getSignedCopayerOpts({
|
|
1386
|
+
walletId: walletId,
|
|
1387
|
+
name: 'copayer 3',
|
|
1388
|
+
xPubKey: TestData.copayers[2].xPubKey_44H_0H_0H,
|
|
1389
|
+
requestPubKey: TestData.copayers[2].pubKey_1H_0,
|
|
1390
|
+
});
|
|
1391
|
+
thirdCopayerOpts.hardwareSourcePublicKey = 'attacker-or-legit-hw-pubkey';
|
|
1392
|
+
server.joinWallet(thirdCopayerOpts, function(err, result) {
|
|
1393
|
+
should.not.exist(result);
|
|
1394
|
+
should.exist(err);
|
|
1395
|
+
err.should.be.instanceof(ClientError);
|
|
1396
|
+
err.code.should.equal('WALLET_FULL');
|
|
1397
|
+
server.getWallet({}, function(err, wallet) {
|
|
1398
|
+
should.not.exist(err);
|
|
1399
|
+
wallet.copayers.length.should.equal(2);
|
|
1400
|
+
done();
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1138
1407
|
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
|
|
1139
1408
|
helpers.createAndJoinWallet(2, 3).then(function({ server }) {
|
|
1140
1409
|
server.getWallet({}, function(err, wallet) {
|
|
@@ -1376,6 +1645,133 @@ describe('Wallet service', function() {
|
|
|
1376
1645
|
});
|
|
1377
1646
|
});
|
|
1378
1647
|
});
|
|
1648
|
+
|
|
1649
|
+
// SECURITY: this guards against a copayer joining a TSS wallet without ever being checked
|
|
1650
|
+
// against the key generation session's participant list.
|
|
1651
|
+
describe('TSS wallets (non-participant bypass)', function() {
|
|
1652
|
+
it('should reject a join from a copayer who is not a TSS keygen session participant, even with clientDerivedPublicKey supplied', async function() {
|
|
1653
|
+
const server = new WalletService();
|
|
1654
|
+
|
|
1655
|
+
const legitXPubKey = TestData.copayers[0].xPubKey_44H_0H_0H;
|
|
1656
|
+
const legitCopayerId = Model.Copayer.xPubToCopayerId('btc', legitXPubKey);
|
|
1657
|
+
|
|
1658
|
+
// A completed TSS key-gen session whose sole participant is the legit copayer above.
|
|
1659
|
+
// NOTE: helpers.beforeEach()'s collection wipe-list doesn't include tss_keygen (it's
|
|
1660
|
+
// marked // TODO in helpers.ts), so this collection isn't reset between test runs the
|
|
1661
|
+
// way the rest of the suite's state is. Clear our fixture id explicitly so this test
|
|
1662
|
+
// stays repeatable regardless of that pre-existing gap.
|
|
1663
|
+
const session = TssKeyGenModel.create({
|
|
1664
|
+
id: 'tss-non-participant-bypass-test-session',
|
|
1665
|
+
message: { partyId: 0, broadcastMessages: [], p2pMessages: [], publicKey: 'dummy', round: 0 },
|
|
1666
|
+
n: 1,
|
|
1667
|
+
copayerId: legitCopayerId,
|
|
1668
|
+
version: 1.1,
|
|
1669
|
+
});
|
|
1670
|
+
session.sharedPublicKey = 'dummy-shared-public-key';
|
|
1671
|
+
await server.storage.db.collection('tss_keygen').deleteMany({ id: session.id });
|
|
1672
|
+
await server.storage.storeTssKeyGenSession({ doc: session });
|
|
1673
|
+
|
|
1674
|
+
// createWallet forces m=n=1 for any wallet created against a tssKeyId.
|
|
1675
|
+
const walletId = await util.promisify(server.createWallet).call(server, {
|
|
1676
|
+
name: 'tss wallet',
|
|
1677
|
+
m: 1,
|
|
1678
|
+
n: 1,
|
|
1679
|
+
pubKey: TestData.keyPair.pub,
|
|
1680
|
+
coin: 'btc',
|
|
1681
|
+
tssKeyId: session.id,
|
|
1682
|
+
});
|
|
1683
|
+
should.exist(walletId);
|
|
1684
|
+
|
|
1685
|
+
// Attacker knows the wallet's join secret (so they CAN produce a valid
|
|
1686
|
+
// copayerSignature - e.g. they were handed the invite link/secret for this wallet)
|
|
1687
|
+
// but their copayerId is NOT in session.participants: they never actually took part
|
|
1688
|
+
// in the TSS key generation. A valid copayerSignature alone must not be enough to
|
|
1689
|
+
// join a TSS wallet; only listed DKG participants may.
|
|
1690
|
+
const copayerOpts: any = helpers.getSignedCopayerOpts({
|
|
1691
|
+
walletId,
|
|
1692
|
+
name: 'attacker',
|
|
1693
|
+
xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H,
|
|
1694
|
+
clientDerivedPublicKey: 'attacker-controlled-client-derived-pubkey',
|
|
1695
|
+
requestPubKey: TestData.copayers[1].pubKey_1H_0,
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
const { err, result } = await new Promise<{ err: any; result: any }>(resolve => {
|
|
1699
|
+
server.joinWallet(copayerOpts, (err, result) => resolve({ err, result }));
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
should.not.exist(result);
|
|
1703
|
+
should.exist(err);
|
|
1704
|
+
err.code.should.equal('TSS_NON_PARTICIPANT');
|
|
1705
|
+
|
|
1706
|
+
// The rejected join must not have persisted the attacker as a copayer despite
|
|
1707
|
+
// supplying a valid copayerSignature - a regression that returns TSS_NON_PARTICIPANT
|
|
1708
|
+
// but still adds the copayer would pass the assertions above alone.
|
|
1709
|
+
const wallet = await util.promisify(server.getWallet).call(server, {});
|
|
1710
|
+
wallet.copayers.length.should.equal(0);
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
it('should reject a TSS join with clientDerivedPublicKey present when xPubKey is absent, even if the ancillary-derived ID is a keygen session participant', async function() {
|
|
1714
|
+
// xPubKey is the canonical copayer identity for every join, including TSS ones;
|
|
1715
|
+
// the ancillary clientDerivedPublicKey field must not stand in for it. The
|
|
1716
|
+
// session participant list below carries the ID derived from
|
|
1717
|
+
// clientDerivedPublicKey, so this proves the join is rejected for the missing
|
|
1718
|
+
// identity field itself rather than by the TSS participant check. The signature
|
|
1719
|
+
// is created while xPubKey is genuinely absent - it covers
|
|
1720
|
+
// [name, undefined, requestPubKey].join('|') === 'me||<requestPubKey>' - so it is
|
|
1721
|
+
// valid for the request as sent and the rejection is attributable to the missing
|
|
1722
|
+
// xPubKey, not to an invalidated signature.
|
|
1723
|
+
|
|
1724
|
+
// 20 Aug '26: Support for joining a wallet without xPubKey will fast-follow
|
|
1725
|
+
const server = new WalletService();
|
|
1726
|
+
|
|
1727
|
+
const clientDerivedPublicKey = 'legit-client-derived-pubkey-no-xpub';
|
|
1728
|
+
const ancillaryDerivedCopayerId = Model.Copayer.xPubToCopayerId('btc', clientDerivedPublicKey);
|
|
1729
|
+
|
|
1730
|
+
const session = TssKeyGenModel.create({
|
|
1731
|
+
id: 'tss-no-xpubkey-rejected-test-session',
|
|
1732
|
+
message: { partyId: 0, broadcastMessages: [], p2pMessages: [], publicKey: 'dummy', round: 0 },
|
|
1733
|
+
n: 1,
|
|
1734
|
+
copayerId: ancillaryDerivedCopayerId,
|
|
1735
|
+
version: 1.1,
|
|
1736
|
+
});
|
|
1737
|
+
session.sharedPublicKey = 'dummy-shared-public-key';
|
|
1738
|
+
await server.storage.db.collection('tss_keygen').deleteMany({ id: session.id });
|
|
1739
|
+
await server.storage.storeTssKeyGenSession({ doc: session });
|
|
1740
|
+
|
|
1741
|
+
const walletId = await util.promisify(server.createWallet).call(server, {
|
|
1742
|
+
name: 'tss wallet',
|
|
1743
|
+
m: 1,
|
|
1744
|
+
n: 1,
|
|
1745
|
+
pubKey: TestData.keyPair.pub,
|
|
1746
|
+
coin: 'btc',
|
|
1747
|
+
tssKeyId: session.id,
|
|
1748
|
+
});
|
|
1749
|
+
should.exist(walletId);
|
|
1750
|
+
|
|
1751
|
+
const copayerOpts: any = helpers.getSignedCopayerOpts({
|
|
1752
|
+
walletId,
|
|
1753
|
+
name: 'me',
|
|
1754
|
+
clientDerivedPublicKey,
|
|
1755
|
+
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
1756
|
+
});
|
|
1757
|
+
should.not.exist(copayerOpts.xPubKey);
|
|
1758
|
+
|
|
1759
|
+
const { err, result } = await new Promise<{ err: any; result: any }>(resolve => {
|
|
1760
|
+
server.joinWallet(copayerOpts, (err, result) => resolve({ err, result }));
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
should.not.exist(result);
|
|
1764
|
+
should.exist(err);
|
|
1765
|
+
err.should.be.instanceof(ClientError);
|
|
1766
|
+
|
|
1767
|
+
// The rejected join must not have persisted a copayer. The join is rejected
|
|
1768
|
+
// before WalletService.walletId is set, so an unauthenticated getWallet({})
|
|
1769
|
+
// cannot address the wallet; read persisted state at the storage level instead.
|
|
1770
|
+
const wallet = await util.promisify(server.storage.fetchWallet).call(server.storage, walletId);
|
|
1771
|
+
should.exist(wallet);
|
|
1772
|
+
wallet.copayers.length.should.equal(0);
|
|
1773
|
+
});
|
|
1774
|
+
});
|
|
1379
1775
|
});
|
|
1380
1776
|
|
|
1381
1777
|
describe('#removeWallet', function() {
|
|
@@ -2805,6 +3201,112 @@ describe('Wallet service', function() {
|
|
|
2805
3201
|
});
|
|
2806
3202
|
});
|
|
2807
3203
|
});
|
|
3204
|
+
|
|
3205
|
+
describe('#add access with historical copayer state', function() {
|
|
3206
|
+
beforeEach(async function() {
|
|
3207
|
+
({ server, wallet } = await helpers.createAndJoinWallet(1, 1));
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
function snapshotState(cb) {
|
|
3211
|
+
server.storage.db.collection(Storage.collections.WALLETS).findOne(
|
|
3212
|
+
{ id: wallet.id },
|
|
3213
|
+
(err, walletDoc) => {
|
|
3214
|
+
if (err) return cb(err);
|
|
3215
|
+
server.storage.db.collection(Storage.collections.COPAYERS_LOOKUP).findOne(
|
|
3216
|
+
{ copayerId: opts.copayerId },
|
|
3217
|
+
(err2, lookupDoc) => {
|
|
3218
|
+
if (err2) return cb(err2);
|
|
3219
|
+
return cb(null, {
|
|
3220
|
+
wallet: JSON.parse(JSON.stringify(walletDoc)),
|
|
3221
|
+
lookup: JSON.parse(JSON.stringify(lookupDoc))
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
function mutateWalletDoc(mutation, cb) {
|
|
3230
|
+
server.storage.db.collection(Storage.collections.WALLETS).findOne(
|
|
3231
|
+
{ id: wallet.id },
|
|
3232
|
+
(err, walletDoc) => {
|
|
3233
|
+
if (err) return cb(err);
|
|
3234
|
+
mutation(walletDoc);
|
|
3235
|
+
server.storage.db.collection(Storage.collections.WALLETS).replaceOne(
|
|
3236
|
+
{ id: wallet.id },
|
|
3237
|
+
walletDoc,
|
|
3238
|
+
cb
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
it('should return NOT_AUTHORIZED when the target copayer is missing from the wallet', function(done) {
|
|
3245
|
+
mutateWalletDoc(doc => {
|
|
3246
|
+
doc.copayers = doc.copayers.filter(c => c.id !== opts.copayerId);
|
|
3247
|
+
}, (err) => {
|
|
3248
|
+
should.not.exist(err);
|
|
3249
|
+
snapshotState((err, before) => {
|
|
3250
|
+
should.not.exist(err);
|
|
3251
|
+
ws.addAccess(opts, (err, res) => {
|
|
3252
|
+
err.should.be.instanceof(ClientError);
|
|
3253
|
+
err.code.should.equal('NOT_AUTHORIZED');
|
|
3254
|
+
should.not.exist(res);
|
|
3255
|
+
snapshotState((err, after) => {
|
|
3256
|
+
should.not.exist(err);
|
|
3257
|
+
// The rejected attempt must not have persisted anything: no new access key, no other state change.
|
|
3258
|
+
after.should.deep.equal(before);
|
|
3259
|
+
done();
|
|
3260
|
+
});
|
|
3261
|
+
});
|
|
3262
|
+
});
|
|
3263
|
+
});
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
it('should return NOT_AUTHORIZED when the target copayer has no xPubKey', function(done) {
|
|
3267
|
+
mutateWalletDoc(doc => {
|
|
3268
|
+
delete doc.copayers[0].xPubKey;
|
|
3269
|
+
}, (err) => {
|
|
3270
|
+
should.not.exist(err);
|
|
3271
|
+
snapshotState((err, before) => {
|
|
3272
|
+
should.not.exist(err);
|
|
3273
|
+
ws.addAccess(opts, (err, res) => {
|
|
3274
|
+
err.should.be.instanceof(ClientError);
|
|
3275
|
+
err.code.should.equal('NOT_AUTHORIZED');
|
|
3276
|
+
should.not.exist(res);
|
|
3277
|
+
snapshotState((err, after) => {
|
|
3278
|
+
should.not.exist(err);
|
|
3279
|
+
// The rejected attempt must not have persisted anything: no new access key, no other state change.
|
|
3280
|
+
after.should.deep.equal(before);
|
|
3281
|
+
done();
|
|
3282
|
+
});
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
});
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
it('should return NOT_AUTHORIZED when the target copayer has a malformed xPubKey', function(done) {
|
|
3289
|
+
mutateWalletDoc(doc => {
|
|
3290
|
+
doc.copayers[0].xPubKey = 'invalid';
|
|
3291
|
+
}, (err) => {
|
|
3292
|
+
should.not.exist(err);
|
|
3293
|
+
snapshotState((err, before) => {
|
|
3294
|
+
should.not.exist(err);
|
|
3295
|
+
ws.addAccess(opts, (err, res) => {
|
|
3296
|
+
err.should.be.instanceof(ClientError);
|
|
3297
|
+
err.code.should.equal('NOT_AUTHORIZED');
|
|
3298
|
+
should.not.exist(res);
|
|
3299
|
+
snapshotState((err, after) => {
|
|
3300
|
+
should.not.exist(err);
|
|
3301
|
+
// The rejected attempt must not have persisted anything: no new access key, no other state change.
|
|
3302
|
+
after.should.deep.equal(before);
|
|
3303
|
+
done();
|
|
3304
|
+
});
|
|
3305
|
+
});
|
|
3306
|
+
});
|
|
3307
|
+
});
|
|
3308
|
+
});
|
|
3309
|
+
});
|
|
2808
3310
|
});
|
|
2809
3311
|
|
|
2810
3312
|
describe('#getBalance', function() {
|
|
@@ -9030,10 +9532,238 @@ describe('Wallet service', function() {
|
|
|
9030
9532
|
});
|
|
9031
9533
|
|
|
9032
9534
|
it.skip('should get accepted/rejected transaction proposal', function(done) { });
|
|
9033
|
-
|
|
9535
|
+
|
|
9034
9536
|
it.skip('should get broadcasted transaction proposal', function(done) { });
|
|
9035
9537
|
});
|
|
9036
9538
|
|
|
9539
|
+
describe('#getTxByHash', function() {
|
|
9540
|
+
let serverA: WalletService;
|
|
9541
|
+
let walletA: Model.Wallet;
|
|
9542
|
+
let serverB: WalletService;
|
|
9543
|
+
let txp;
|
|
9544
|
+
|
|
9545
|
+
beforeEach(async function() {
|
|
9546
|
+
({ server: serverA, wallet: walletA } = await helpers.createAndJoinWallet(1, 1));
|
|
9547
|
+
await helpers.stubUtxos(serverA, walletA, 1);
|
|
9548
|
+
const txOpts = {
|
|
9549
|
+
outputs: [{
|
|
9550
|
+
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
9551
|
+
amount: 0.5e8
|
|
9552
|
+
}],
|
|
9553
|
+
feePerKb: 100e2,
|
|
9554
|
+
message: 'some message',
|
|
9555
|
+
};
|
|
9556
|
+
txp = await helpers.createAndPublishTx(serverA, txOpts, TestData.copayers[0].privKey_1H_0);
|
|
9557
|
+
should.exist(txp);
|
|
9558
|
+
|
|
9559
|
+
const signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
9560
|
+
txp = await util.promisify(serverA.signTx).call(serverA, {
|
|
9561
|
+
txProposalId: txp.id,
|
|
9562
|
+
signatures,
|
|
9563
|
+
});
|
|
9564
|
+
should.exist(txp.txid);
|
|
9565
|
+
|
|
9566
|
+
({ server: serverB } = await helpers.createAndJoinWallet(1, 1, { offset: 1 }));
|
|
9567
|
+
});
|
|
9568
|
+
|
|
9569
|
+
it('should not disclose another wallet\'s transaction proposal by hash', function(done) {
|
|
9570
|
+
serverB.getTxByHash({
|
|
9571
|
+
txid: txp.txid
|
|
9572
|
+
}, function(err, res) {
|
|
9573
|
+
should.exist(err);
|
|
9574
|
+
should.not.exist(res);
|
|
9575
|
+
err.should.be.instanceof(ClientError);
|
|
9576
|
+
err.code.should.equal('TX_NOT_FOUND');
|
|
9577
|
+
err.message.should.equal('Transaction proposal not found');
|
|
9578
|
+
// Belt-and-suspenders: confirm the error itself carries none of
|
|
9579
|
+
// wallet A's data (no walletId/creatorId/raw leaking via the error).
|
|
9580
|
+
JSON.stringify(err).should.not.include(walletA.id);
|
|
9581
|
+
done();
|
|
9582
|
+
});
|
|
9583
|
+
});
|
|
9584
|
+
|
|
9585
|
+
it('should get own transaction proposal by hash, unchanged from before the fix', function(done) {
|
|
9586
|
+
serverA.editTxNote({
|
|
9587
|
+
txid: txp.txid,
|
|
9588
|
+
body: 'a note from wallet A'
|
|
9589
|
+
}, function(err) {
|
|
9590
|
+
should.not.exist(err);
|
|
9591
|
+
serverA.getTxByHash({
|
|
9592
|
+
txid: txp.txid
|
|
9593
|
+
}, function(err, res) {
|
|
9594
|
+
should.not.exist(err);
|
|
9595
|
+
should.exist(res);
|
|
9596
|
+
res.id.should.equal(txp.id);
|
|
9597
|
+
res.walletId.should.equal(walletA.id);
|
|
9598
|
+
res.txid.should.equal(txp.txid);
|
|
9599
|
+
should.exist(res.raw);
|
|
9600
|
+
should.exist(res.note);
|
|
9601
|
+
res.note.body.should.equal('a note from wallet A');
|
|
9602
|
+
done();
|
|
9603
|
+
});
|
|
9604
|
+
});
|
|
9605
|
+
});
|
|
9606
|
+
|
|
9607
|
+
it('should return the identical not-found error for a foreign txid and an unknown txid', function(done) {
|
|
9608
|
+
serverB.getTxByHash({
|
|
9609
|
+
txid: txp.txid
|
|
9610
|
+
}, function(errForeign, resForeign) {
|
|
9611
|
+
should.exist(errForeign);
|
|
9612
|
+
should.not.exist(resForeign);
|
|
9613
|
+
serverB.getTxByHash({
|
|
9614
|
+
txid: helpers.randomTXID()
|
|
9615
|
+
}, function(errUnknown, resUnknown) {
|
|
9616
|
+
should.exist(errUnknown);
|
|
9617
|
+
should.not.exist(resUnknown);
|
|
9618
|
+
errForeign.code.should.equal(errUnknown.code);
|
|
9619
|
+
errForeign.message.should.equal(errUnknown.message);
|
|
9620
|
+
errForeign.code.should.equal('TX_NOT_FOUND');
|
|
9621
|
+
done();
|
|
9622
|
+
});
|
|
9623
|
+
});
|
|
9624
|
+
});
|
|
9625
|
+
|
|
9626
|
+
it('should return a bad-request error when txid is missing', function(done) {
|
|
9627
|
+
serverA.getTxByHash({}, function(err, res) {
|
|
9628
|
+
should.exist(err);
|
|
9629
|
+
should.not.exist(res);
|
|
9630
|
+
err.should.be.instanceof(ClientError);
|
|
9631
|
+
err.code.should.equal('BADREQUEST');
|
|
9632
|
+
err.message.should.equal('Required argument: txid missing.');
|
|
9633
|
+
done();
|
|
9634
|
+
});
|
|
9635
|
+
});
|
|
9636
|
+
});
|
|
9637
|
+
|
|
9638
|
+
describe('GET /v1/txproposalsbyhash/:id/ (HTTP route)', function() {
|
|
9639
|
+
// Exercises the real Express route (registerTransactionRoutes ->
|
|
9640
|
+
// getServerWithAuth -> WalletService.getInstanceWithAuth), not just the
|
|
9641
|
+
// service method, so route wiring, request-auth plumbing, and HTTP
|
|
9642
|
+
// status/body serialization are covered, not only the underlying
|
|
9643
|
+
// service logic already tested above. Only the cryptographic signature
|
|
9644
|
+
// check is stubbed to return true
|
|
9645
|
+
const testHost = 'http://127.0.0.1';
|
|
9646
|
+
let testPort: number;
|
|
9647
|
+
let httpServer;
|
|
9648
|
+
let verifyStub;
|
|
9649
|
+
let walletA: Model.Wallet;
|
|
9650
|
+
let walletB: Model.Wallet;
|
|
9651
|
+
let txp;
|
|
9652
|
+
|
|
9653
|
+
beforeEach(async function() {
|
|
9654
|
+
httpServer = undefined;
|
|
9655
|
+
verifyStub = undefined;
|
|
9656
|
+
|
|
9657
|
+
let serverA: WalletService;
|
|
9658
|
+
let serverB: WalletService;
|
|
9659
|
+
({ server: serverA, wallet: walletA } = await helpers.createAndJoinWallet(1, 1));
|
|
9660
|
+
await helpers.stubUtxos(serverA, walletA, 1);
|
|
9661
|
+
const txOpts = {
|
|
9662
|
+
outputs: [{
|
|
9663
|
+
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
9664
|
+
amount: 0.5e8
|
|
9665
|
+
}],
|
|
9666
|
+
feePerKb: 100e2,
|
|
9667
|
+
message: 'some message',
|
|
9668
|
+
};
|
|
9669
|
+
txp = await helpers.createAndPublishTx(serverA, txOpts, TestData.copayers[0].privKey_1H_0);
|
|
9670
|
+
const signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
9671
|
+
txp = await util.promisify(serverA.signTx).call(serverA, {
|
|
9672
|
+
txProposalId: txp.id,
|
|
9673
|
+
signatures,
|
|
9674
|
+
});
|
|
9675
|
+
should.exist(txp.txid);
|
|
9676
|
+
|
|
9677
|
+
({ server: serverB, wallet: walletB } = await helpers.createAndJoinWallet(1, 1, { offset: 1 }));
|
|
9678
|
+
|
|
9679
|
+
verifyStub = sinon.stub(WalletService.prototype, '_verifySignature').returns(true);
|
|
9680
|
+
|
|
9681
|
+
const app = new ExpressApp();
|
|
9682
|
+
httpServer = new http.Server(app.app);
|
|
9683
|
+
await util.promisify(app.start).call(app, {
|
|
9684
|
+
storage: helpers.getStorage(),
|
|
9685
|
+
blockchainExplorer: helpers.getBlockchainExplorer(),
|
|
9686
|
+
request: sinon.stub(),
|
|
9687
|
+
disableLogs: true,
|
|
9688
|
+
basePath: config.basePath
|
|
9689
|
+
});
|
|
9690
|
+
|
|
9691
|
+
await new Promise<void>((resolve, reject) => {
|
|
9692
|
+
httpServer.once('error', reject);
|
|
9693
|
+
httpServer.listen(0, '127.0.0.1', () => {
|
|
9694
|
+
httpServer.off('error', reject);
|
|
9695
|
+
const address = httpServer.address();
|
|
9696
|
+
if (!address || typeof address === 'string') {
|
|
9697
|
+
return reject(new Error('HTTP test server did not bind to a TCP port'));
|
|
9698
|
+
}
|
|
9699
|
+
testPort = address.port;
|
|
9700
|
+
resolve();
|
|
9701
|
+
});
|
|
9702
|
+
});
|
|
9703
|
+
});
|
|
9704
|
+
|
|
9705
|
+
afterEach(function(done) {
|
|
9706
|
+
if (verifyStub) {
|
|
9707
|
+
verifyStub.restore();
|
|
9708
|
+
verifyStub = undefined;
|
|
9709
|
+
}
|
|
9710
|
+
if (!httpServer?.listening) {
|
|
9711
|
+
httpServer = undefined;
|
|
9712
|
+
return done();
|
|
9713
|
+
}
|
|
9714
|
+
httpServer.close(err => {
|
|
9715
|
+
httpServer = undefined;
|
|
9716
|
+
done(err);
|
|
9717
|
+
});
|
|
9718
|
+
});
|
|
9719
|
+
|
|
9720
|
+
function getByHash(copayerId, txid, cb) {
|
|
9721
|
+
request({
|
|
9722
|
+
method: 'GET',
|
|
9723
|
+
url: testHost + ':' + testPort + config.basePath + '/v1/txproposalsbyhash/' + txid + '/',
|
|
9724
|
+
headers: {
|
|
9725
|
+
'x-identity': copayerId,
|
|
9726
|
+
'x-signature': 'stubbed'
|
|
9727
|
+
},
|
|
9728
|
+
json: true
|
|
9729
|
+
}, (err, res, body) => cb(err, res, body));
|
|
9730
|
+
}
|
|
9731
|
+
|
|
9732
|
+
it('foreign wallet gets HTTP 400 TX_NOT_FOUND with no owner data in the body', function(done) {
|
|
9733
|
+
getByHash(walletB.copayers[0].id, txp.txid, (err, res, body) => {
|
|
9734
|
+
should.not.exist(err);
|
|
9735
|
+
res.statusCode.should.equal(400);
|
|
9736
|
+
body.code.should.equal('TX_NOT_FOUND');
|
|
9737
|
+
JSON.stringify(body).should.not.include(walletA.id);
|
|
9738
|
+
done();
|
|
9739
|
+
});
|
|
9740
|
+
});
|
|
9741
|
+
|
|
9742
|
+
it('owner wallet gets HTTP 200 with the full proposal, unchanged', function(done) {
|
|
9743
|
+
getByHash(walletA.copayers[0].id, txp.txid, (err, res, body) => {
|
|
9744
|
+
should.not.exist(err);
|
|
9745
|
+
res.statusCode.should.equal(200);
|
|
9746
|
+
body.walletId.should.equal(walletA.id);
|
|
9747
|
+
body.txid.should.equal(txp.txid);
|
|
9748
|
+
should.exist(body.raw);
|
|
9749
|
+
done();
|
|
9750
|
+
});
|
|
9751
|
+
});
|
|
9752
|
+
|
|
9753
|
+
it('foreign and unknown txids produce an identical HTTP 400 TX_NOT_FOUND body', function(done) {
|
|
9754
|
+
getByHash(walletB.copayers[0].id, txp.txid, (err, resForeign, bodyForeign) => {
|
|
9755
|
+
should.not.exist(err);
|
|
9756
|
+
getByHash(walletB.copayers[0].id, helpers.randomTXID(), (err, resUnknown, bodyUnknown) => {
|
|
9757
|
+
should.not.exist(err);
|
|
9758
|
+
resForeign.statusCode.should.equal(resUnknown.statusCode);
|
|
9759
|
+
bodyForeign.code.should.equal(bodyUnknown.code);
|
|
9760
|
+
bodyForeign.code.should.equal('TX_NOT_FOUND');
|
|
9761
|
+
done();
|
|
9762
|
+
});
|
|
9763
|
+
});
|
|
9764
|
+
});
|
|
9765
|
+
});
|
|
9766
|
+
|
|
9037
9767
|
describe('#getTxs', function() {
|
|
9038
9768
|
let server: WalletService;
|
|
9039
9769
|
let wallet: Model.Wallet;
|