@bitpay-labs/bitcore-wallet-service 11.10.7 → 11.10.9
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/.mocharc.json +6 -0
- package/mcr.config.js +17 -0
- package/package.json +6 -6
- 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 +3 -3
- 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
- package/.mocharc.yml +0 -2
- package/.nycrc.yml +0 -13
|
@@ -7,10 +7,16 @@ import * as twoOfThree from './data/tss/2of3';
|
|
|
7
7
|
import BWS from '../../src/index';
|
|
8
8
|
import helpers from './helpers';
|
|
9
9
|
import { TssKeyGen } from '../../src/lib/tss';
|
|
10
|
+
import { TssSigGenModel } from '../../src/lib/model/tsssign';
|
|
11
|
+
import { TssKeyGenModel } from '../../src/lib/model/tsskeygen';
|
|
12
|
+
import { Common } from '../../src/lib/common/index';
|
|
10
13
|
|
|
11
14
|
const should = chai.should();
|
|
15
|
+
const { Utils } = Common;
|
|
12
16
|
|
|
13
17
|
describe('TSS', function() {
|
|
18
|
+
this.timeout(Math.max(this['_timeout'], 2000));
|
|
19
|
+
|
|
14
20
|
const vector = {
|
|
15
21
|
m: 2,
|
|
16
22
|
n: 3,
|
|
@@ -250,6 +256,11 @@ describe('TSS', function() {
|
|
|
250
256
|
describe('POST /v1/tss/keygen/:id', function() {
|
|
251
257
|
const url = id => `${urlPrefix}/v1/tss/keygen/${id}`;
|
|
252
258
|
|
|
259
|
+
beforeEach(async function() {
|
|
260
|
+
const storage = helpers.getStorage();
|
|
261
|
+
await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
|
|
262
|
+
});
|
|
263
|
+
|
|
253
264
|
describe('middleware', function() {
|
|
254
265
|
beforeEach(function() {
|
|
255
266
|
sandbox.stub(TssKeyGen, 'processMessage').resolves();
|
|
@@ -336,5 +347,269 @@ describe('TSS', function() {
|
|
|
336
347
|
});
|
|
337
348
|
});
|
|
338
349
|
});
|
|
350
|
+
|
|
351
|
+
it('should error if session is expired', function(done) {
|
|
352
|
+
const storage = helpers.getStorage();
|
|
353
|
+
const publicKey = vector.party0.authKey.publicKey.toString();
|
|
354
|
+
const keyGen = TssKeyGenModel.create({
|
|
355
|
+
id: 'test',
|
|
356
|
+
message: { partyId: 0, publicKey } as any,
|
|
357
|
+
n: 2,
|
|
358
|
+
copayerId: publicKey,
|
|
359
|
+
version: 1.1,
|
|
360
|
+
timeLimit: 1, // 1 minute
|
|
361
|
+
});
|
|
362
|
+
storage.storeTssKeyGenSession({ doc: keyGen }).then(() => {
|
|
363
|
+
// advance time by 2 minutes
|
|
364
|
+
const now = Date.now() + 2 * 60 * 1000;
|
|
365
|
+
sandbox.stub(Date, 'now').returns(now);
|
|
366
|
+
|
|
367
|
+
const message = JSON.parse(JSON.stringify(vector.keygen.messages.round1.party0));
|
|
368
|
+
message.publicKey = publicKey;
|
|
369
|
+
|
|
370
|
+
app.post(url('test'))
|
|
371
|
+
.set('x-identity', publicKey)
|
|
372
|
+
.send({ message })
|
|
373
|
+
.expect((res) => {
|
|
374
|
+
res.status.should.equal(400);
|
|
375
|
+
res.body.code.should.equal('TSS_SESSION_EXPIRED');
|
|
376
|
+
})
|
|
377
|
+
.end(done);
|
|
378
|
+
}).catch(done);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('POST /v1/tss/sign/:id', function() {
|
|
383
|
+
const url = id => `${urlPrefix}/v1/tss/sign/${id}`;
|
|
384
|
+
const signRequest = body => {
|
|
385
|
+
const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
|
|
386
|
+
const hash = Utils.hashMessage(message, false); // BWC hashes the message with reversal, then signs
|
|
387
|
+
const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
|
|
388
|
+
return xSignature;
|
|
389
|
+
};
|
|
390
|
+
const privateKey = new BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
|
|
391
|
+
const publicKey = privateKey.publicKey.toString();
|
|
392
|
+
|
|
393
|
+
beforeEach(async function() {
|
|
394
|
+
const storage = helpers.getStorage();
|
|
395
|
+
await storage.db.collection(BWS.Storage.collections.TSS_SIGN).deleteMany({});
|
|
396
|
+
await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).deleteMany({});
|
|
397
|
+
await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).insertOne({
|
|
398
|
+
copayerId: publicKey,
|
|
399
|
+
walletId: 'test',
|
|
400
|
+
requestPubKey: publicKey,
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should error if session is expired', function(done) {
|
|
405
|
+
const storage = helpers.getStorage();
|
|
406
|
+
const sigGen = TssSigGenModel.create({
|
|
407
|
+
id: 'test',
|
|
408
|
+
message: { partyId: 0, publicKey } as any,
|
|
409
|
+
m: 2,
|
|
410
|
+
copayerId: publicKey,
|
|
411
|
+
version: 1.1,
|
|
412
|
+
timeLimit: 1, // 1 minute
|
|
413
|
+
});
|
|
414
|
+
storage.storeTssSigSession({ doc: sigGen }).then(() => {
|
|
415
|
+
// advance time by 2 minutes
|
|
416
|
+
const now = Date.now() + 2 * 60 * 1000;
|
|
417
|
+
sandbox.stub(Date, 'now').returns(now);
|
|
418
|
+
|
|
419
|
+
const message = { publicKey, p2pMessages: [{ payload: { encryptedMessage: 'encryptie', signature: 'siggie' } }], broadcastMessages: [] };
|
|
420
|
+
const xSignature = signRequest({ message });
|
|
421
|
+
|
|
422
|
+
app.post(url('test'))
|
|
423
|
+
.set('x-identity', publicKey)
|
|
424
|
+
.set('x-signature', xSignature)
|
|
425
|
+
.send({ message })
|
|
426
|
+
.expect((res) => {
|
|
427
|
+
res.status.should.equal(400);
|
|
428
|
+
res.body.code.should.equal('TSS_SESSION_EXPIRED');
|
|
429
|
+
})
|
|
430
|
+
.end(done);
|
|
431
|
+
}).catch(done);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('POST /v1/tss/sign/:id/store', function() {
|
|
436
|
+
const url = id => `${urlPrefix}/v1/tss/sign/${id}/store`;
|
|
437
|
+
const signRequest = body => {
|
|
438
|
+
const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
|
|
439
|
+
const hash = Utils.hashMessage(message, false); // BWC hashes the message with reversal, then signs
|
|
440
|
+
const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
|
|
441
|
+
return xSignature;
|
|
442
|
+
};
|
|
443
|
+
const privateKey = new BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
|
|
444
|
+
const publicKey = privateKey.publicKey.toString();
|
|
445
|
+
const signature = {
|
|
446
|
+
r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f74',
|
|
447
|
+
s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9257',
|
|
448
|
+
v: 1,
|
|
449
|
+
pubKey: '0267c2c572fcc1a5026d0e5d8e60a04ff6adeaf1a3a996c9c69f3b1714cff1db85',
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
beforeEach(async function() {
|
|
454
|
+
const storage = helpers.getStorage();
|
|
455
|
+
await storage.db.collection(BWS.Storage.collections.TSS_SIGN).deleteMany({});
|
|
456
|
+
const sigGen = TssSigGenModel.create({
|
|
457
|
+
id: 'test',
|
|
458
|
+
message: { partyId: 0, publicKey } as any,
|
|
459
|
+
m: 2,
|
|
460
|
+
copayerId: publicKey,
|
|
461
|
+
version: 1.1,
|
|
462
|
+
});
|
|
463
|
+
await storage.storeTssSigSession({ doc: sigGen });
|
|
464
|
+
await storage.storeTssSignature({ id: 'test', signature });
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should throw a mismatch error for a mismatched signature: r', function(done) {
|
|
468
|
+
const modifiedSignature = {
|
|
469
|
+
...signature,
|
|
470
|
+
r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75', // modified r value to create a mismatch
|
|
471
|
+
};
|
|
472
|
+
const xSignature = signRequest({ signature: modifiedSignature });
|
|
473
|
+
|
|
474
|
+
app.post(url('test'))
|
|
475
|
+
.set('x-identity', publicKey)
|
|
476
|
+
.set('x-signature', xSignature)
|
|
477
|
+
.send({
|
|
478
|
+
signature: modifiedSignature
|
|
479
|
+
})
|
|
480
|
+
.expect(res => {
|
|
481
|
+
res.status.should.equal(400);
|
|
482
|
+
res.body.should.have.property('code');
|
|
483
|
+
res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
|
|
484
|
+
res.body.message.should.equal('Final signature does not match the one generated by the session');
|
|
485
|
+
})
|
|
486
|
+
.end(done);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should throw a mismatch error for a mismatched signature: s', function(done) {
|
|
490
|
+
const modifiedSignature = {
|
|
491
|
+
...signature,
|
|
492
|
+
s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9256', // modified s value to create a mismatch
|
|
493
|
+
};
|
|
494
|
+
const xSignature = signRequest({ signature: modifiedSignature });
|
|
495
|
+
|
|
496
|
+
app.post(url('test'))
|
|
497
|
+
.set('x-identity', publicKey)
|
|
498
|
+
.set('x-signature', xSignature)
|
|
499
|
+
.send({
|
|
500
|
+
signature: modifiedSignature
|
|
501
|
+
})
|
|
502
|
+
.expect(res => {
|
|
503
|
+
res.status.should.equal(400);
|
|
504
|
+
res.body.should.have.property('code');
|
|
505
|
+
res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
|
|
506
|
+
res.body.message.should.equal('Final signature does not match the one generated by the session');
|
|
507
|
+
})
|
|
508
|
+
.end(done);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should throw a mismatch error for a mismatched signature: v', function(done) {
|
|
512
|
+
const modifiedSignature = {
|
|
513
|
+
...signature,
|
|
514
|
+
v: 123, // modified v value to create a mismatch
|
|
515
|
+
};
|
|
516
|
+
const xSignature = signRequest({ signature: modifiedSignature });
|
|
517
|
+
|
|
518
|
+
app.post(url('test'))
|
|
519
|
+
.set('x-identity', publicKey)
|
|
520
|
+
.set('x-signature', xSignature)
|
|
521
|
+
.send({
|
|
522
|
+
signature: modifiedSignature
|
|
523
|
+
})
|
|
524
|
+
.expect(res => {
|
|
525
|
+
res.status.should.equal(400);
|
|
526
|
+
res.body.should.have.property('code');
|
|
527
|
+
res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
|
|
528
|
+
res.body.message.should.equal('Final signature does not match the one generated by the session');
|
|
529
|
+
})
|
|
530
|
+
.end(done);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should throw a mismatch error for a mismatched signature: pubKey', function(done) {
|
|
534
|
+
const modifiedSignature = {
|
|
535
|
+
...signature,
|
|
536
|
+
pubKey: 'not-equal-pub-key', // modified pubKey value to create a mismatch
|
|
537
|
+
};
|
|
538
|
+
const xSignature = signRequest({ signature: modifiedSignature });
|
|
539
|
+
|
|
540
|
+
app.post(url('test'))
|
|
541
|
+
.set('x-identity', publicKey)
|
|
542
|
+
.set('x-signature', xSignature)
|
|
543
|
+
.send({
|
|
544
|
+
signature: modifiedSignature
|
|
545
|
+
})
|
|
546
|
+
.expect(res => {
|
|
547
|
+
res.status.should.equal(400);
|
|
548
|
+
res.body.should.have.property('code');
|
|
549
|
+
res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
|
|
550
|
+
res.body.message.should.equal('Final signature does not match the one generated by the session');
|
|
551
|
+
})
|
|
552
|
+
.end(done);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should cleanly handle race condition when saving signature', function(done) {
|
|
556
|
+
const fetchSessionStub = sandbox.stub(BWS.Storage.prototype, 'fetchTssSigSession');
|
|
557
|
+
fetchSessionStub.callsFake(async function({ id }) {
|
|
558
|
+
const session = await fetchSessionStub.wrappedMethod.call(this, { id });
|
|
559
|
+
session.signature = null;
|
|
560
|
+
return session;
|
|
561
|
+
});
|
|
562
|
+
fetchSessionStub.onThirdCall().callsFake(async function({ id }) {
|
|
563
|
+
const session = await fetchSessionStub.wrappedMethod.call(this, { id });
|
|
564
|
+
return session;
|
|
565
|
+
});
|
|
566
|
+
const xSignature = signRequest({ signature });
|
|
567
|
+
|
|
568
|
+
app.post(url('test'))
|
|
569
|
+
.set('x-identity', publicKey)
|
|
570
|
+
.set('x-signature', xSignature)
|
|
571
|
+
.send({
|
|
572
|
+
signature
|
|
573
|
+
})
|
|
574
|
+
.expect(res => {
|
|
575
|
+
res.status.should.equal(200);
|
|
576
|
+
})
|
|
577
|
+
.end(done);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should handle race condition when saving signature but mismatched signature', function(done) {
|
|
581
|
+
const fetchSessionStub = sandbox.stub(BWS.Storage.prototype, 'fetchTssSigSession');
|
|
582
|
+
fetchSessionStub.callsFake(async function({ id }) {
|
|
583
|
+
const session = await fetchSessionStub.wrappedMethod.call(this, { id });
|
|
584
|
+
session.signature = null;
|
|
585
|
+
return session;
|
|
586
|
+
});
|
|
587
|
+
fetchSessionStub.onThirdCall().callsFake(async function({ id }) {
|
|
588
|
+
const session = await fetchSessionStub.wrappedMethod.call(this, { id });
|
|
589
|
+
return session;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const modifiedSignature = {
|
|
593
|
+
...signature,
|
|
594
|
+
r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75', // modified r value to create a mismatch
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const xSignature = signRequest({ signature: modifiedSignature });
|
|
598
|
+
|
|
599
|
+
app.post(url('test'))
|
|
600
|
+
.set('x-identity', publicKey)
|
|
601
|
+
.set('x-signature', xSignature)
|
|
602
|
+
.send({
|
|
603
|
+
signature: modifiedSignature
|
|
604
|
+
})
|
|
605
|
+
.expect(res => {
|
|
606
|
+
res.status.should.equal(400);
|
|
607
|
+
res.body.should.have.property('code');
|
|
608
|
+
res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
|
|
609
|
+
res.body.message.should.equal('Final signature does not match the one generated by the session');
|
|
610
|
+
})
|
|
611
|
+
.end(done);
|
|
612
|
+
});
|
|
339
613
|
});
|
|
614
|
+
|
|
340
615
|
});
|
|
@@ -8,6 +8,134 @@ import { Copayer } from '../../src/lib/model/copayer';
|
|
|
8
8
|
const should = chai.should();
|
|
9
9
|
|
|
10
10
|
describe('Copayer', function() {
|
|
11
|
+
// xPubKey is the canonical copayer identity: the ID is always derived from it, and
|
|
12
|
+
// hardwareSourcePublicKey / clientDerivedPublicKey are persisted ancillary metadata
|
|
13
|
+
// that never select identity.
|
|
14
|
+
|
|
15
|
+
// 20 Aug '26 - support for hardware wallets lacking xPubKey will
|
|
16
|
+
// change the above statement and change the nature of these tests
|
|
17
|
+
describe('#xPubToCopayerId', function() {
|
|
18
|
+
it('should reject a missing xPubKey for the default coin', function() {
|
|
19
|
+
should.throw(() => Copayer.xPubToCopayerId('btc', undefined), 'Missing xPubKey for copayer id derivation');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should reject a missing xPubKey for a non-default coin instead of hashing "<coin>undefined"', function() {
|
|
23
|
+
should.throw(() => Copayer.xPubToCopayerId('bch', undefined), 'Missing xPubKey for copayer id derivation');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject null and empty xPubKey values', function() {
|
|
27
|
+
should.throw(() => Copayer.xPubToCopayerId('btc', null), 'Missing xPubKey for copayer id derivation');
|
|
28
|
+
should.throw(() => Copayer.xPubToCopayerId('btc', ''), 'Missing xPubKey for copayer id derivation');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should preserve existing copayer IDs for valid xPubKey inputs', function() {
|
|
32
|
+
const xPubKey = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
|
|
33
|
+
// Pinned hash so a change to the derivation (e.g. algorithm, encoding, or the
|
|
34
|
+
// coin == Defaults.COIN branch) would break existing wallets' copayer ids silently.
|
|
35
|
+
Copayer.xPubToCopayerId('btc', xPubKey).should.equal(
|
|
36
|
+
'77a8752fdddbe10fb9c290bd55ea8efbb1ad72adb4e68e8b57bf04adc3cc3ebb'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('#create identity', function() {
|
|
42
|
+
// `chain` is passed explicitly below because the service layer
|
|
43
|
+
// (joinWallet / _addCopayerToWallet) always supplies a resolved chain (backfilled
|
|
44
|
+
// from coin via Wallet.fromObj); these model tests encode the identity contract
|
|
45
|
+
// under that precondition rather than pinning a coin-only input shape.
|
|
46
|
+
const baseOpts = {
|
|
47
|
+
coin: 'btc',
|
|
48
|
+
chain: 'btc',
|
|
49
|
+
name: 'me',
|
|
50
|
+
requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc',
|
|
51
|
+
signature: 'dummy-signature',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
it('should derive the ID from xPubKey alone', function() {
|
|
55
|
+
const xPubKey = 'xpub-abc';
|
|
56
|
+
const c = Copayer.create({ ...baseOpts, xPubKey });
|
|
57
|
+
c.id.should.equal(Copayer.xPubToCopayerId('btc', xPubKey));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should prefer xPubKey when hardwareSourcePublicKey is also present', function() {
|
|
61
|
+
const xPubKey = 'xpub-abc';
|
|
62
|
+
const hardwareSourcePublicKey = 'hw-key';
|
|
63
|
+
|
|
64
|
+
const c = Copayer.create({ ...baseOpts, xPubKey, hardwareSourcePublicKey });
|
|
65
|
+
c.id.should.equal(Copayer.xPubToCopayerId('btc', xPubKey));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should prefer xPubKey when both alternate credentials are also present', function() {
|
|
69
|
+
const xPubKey = 'xpub-abc';
|
|
70
|
+
const hardwareSourcePublicKey = 'hw-key';
|
|
71
|
+
const clientDerivedPublicKey = 'cdk-key';
|
|
72
|
+
|
|
73
|
+
const c = Copayer.create({
|
|
74
|
+
...baseOpts,
|
|
75
|
+
xPubKey,
|
|
76
|
+
hardwareSourcePublicKey,
|
|
77
|
+
clientDerivedPublicKey,
|
|
78
|
+
});
|
|
79
|
+
c.id.should.equal(Copayer.xPubToCopayerId('btc', xPubKey));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should derive the same xpub-based ID on a non-default chain', function() {
|
|
83
|
+
const xPubKey = 'xpub-abc';
|
|
84
|
+
const opts = { ...baseOpts, coin: 'bch', chain: 'bch' };
|
|
85
|
+
const c = Copayer.create({ ...opts, xPubKey });
|
|
86
|
+
c.id.should.equal(Copayer.xPubToCopayerId('bch', xPubKey));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should reject creation without xPubKey and without ancillary fields', function() {
|
|
90
|
+
should.throw(() => Copayer.create({ ...baseOpts }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should reject an xPubKey-less copayer with hardwareSourcePublicKey until alternative ID derivation is implemented', function() {
|
|
94
|
+
// Copayer.create's own xPubKey guard is skipped when hardwareSourcePublicKey is present
|
|
95
|
+
// (that's the carve-out left for a future hardware-derived identity), but there is no
|
|
96
|
+
// alternative ID derivation yet, so the id assignment below falls through to
|
|
97
|
+
// xPubToCopayerId(chain, undefined), which still throws.
|
|
98
|
+
should.throw(() => Copayer.create({ ...baseOpts, hardwareSourcePublicKey: 'hw-key' }));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should reject an xPubKey-less copayer with clientDerivedPublicKey until alternative ID derivation is implemented', function() {
|
|
102
|
+
should.throw(() => Copayer.create({ ...baseOpts, clientDerivedPublicKey: 'cdk-key' }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should continue deriving the copayer ID from xPubKey when ancillary public keys are also present', function() {
|
|
106
|
+
const xPubKey = 'xpub-abc';
|
|
107
|
+
const c = Copayer.create({
|
|
108
|
+
...baseOpts,
|
|
109
|
+
xPubKey,
|
|
110
|
+
hardwareSourcePublicKey: 'hw-key',
|
|
111
|
+
clientDerivedPublicKey: 'cdk-key',
|
|
112
|
+
});
|
|
113
|
+
c.id.should.equal(Copayer.xPubToCopayerId('btc', xPubKey));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should persist the supplied ancillary fields and leave unsupplied ones absent', function() {
|
|
117
|
+
const xPubKey = 'xpub-abc';
|
|
118
|
+
const hardwareSourcePublicKey = 'hw-key';
|
|
119
|
+
|
|
120
|
+
const c = Copayer.create({ ...baseOpts, xPubKey, hardwareSourcePublicKey });
|
|
121
|
+
c.xPubKey.should.equal(xPubKey);
|
|
122
|
+
c.hardwareSourcePublicKey.should.equal(hardwareSourcePublicKey);
|
|
123
|
+
should.not.exist(c.clientDerivedPublicKey);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should persist both ancillary fields without changing the xpub-derived ID', function() {
|
|
127
|
+
const xPubKey = 'xpub-abc';
|
|
128
|
+
const hardwareSourcePublicKey = 'hw-key';
|
|
129
|
+
const clientDerivedPublicKey = 'cdk-key';
|
|
130
|
+
|
|
131
|
+
const c = Copayer.create({ ...baseOpts, xPubKey, hardwareSourcePublicKey, clientDerivedPublicKey });
|
|
132
|
+
c.id.should.equal(Copayer.xPubToCopayerId('btc', xPubKey));
|
|
133
|
+
c.xPubKey.should.equal(xPubKey);
|
|
134
|
+
c.hardwareSourcePublicKey.should.equal(hardwareSourcePublicKey);
|
|
135
|
+
c.clientDerivedPublicKey.should.equal(clientDerivedPublicKey);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
11
139
|
describe('#fromObj', function() {
|
|
12
140
|
it('read a copayer', function() {
|
|
13
141
|
const c = Copayer.fromObj(testWallet.copayers[0]);
|
package/test/storage.test.ts
CHANGED
|
@@ -200,6 +200,97 @@ describe('Storage', function() {
|
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it('should fetch tx by hash scoped to wallet, disambiguating duplicate txids across wallets', async function() {
|
|
204
|
+
// A second, unrelated wallet with its own proposal reusing 'txid0'. The
|
|
205
|
+
// txid index is non-unique, so this can happen in production; the
|
|
206
|
+
// scoped method must still resolve deterministically per walletId
|
|
207
|
+
// while the legacy global fetchTxByHash contract stays unchanged.
|
|
208
|
+
const otherWallet = Model.Wallet.create({
|
|
209
|
+
id: '456',
|
|
210
|
+
name: 'other wallet',
|
|
211
|
+
m: 1,
|
|
212
|
+
n: 1,
|
|
213
|
+
coin: 'btc',
|
|
214
|
+
chain: 'btc',
|
|
215
|
+
network: 'livenet',
|
|
216
|
+
pubKey: '',
|
|
217
|
+
singleAddress: false,
|
|
218
|
+
derivationStrategy: 'BIP45',
|
|
219
|
+
addressType: 'P2SH',
|
|
220
|
+
});
|
|
221
|
+
const otherCopayer = Model.Copayer.create({
|
|
222
|
+
coin: 'btc',
|
|
223
|
+
name: 'other copayer',
|
|
224
|
+
xPubKey: 'other xPubKey',
|
|
225
|
+
requestPubKey: 'other requestPubKey',
|
|
226
|
+
signature: 'other signature',
|
|
227
|
+
});
|
|
228
|
+
otherWallet.addCopayer(otherCopayer);
|
|
229
|
+
await util.promisify(storage.storeWalletAndUpdateCopayersLookup).call(storage, otherWallet);
|
|
230
|
+
|
|
231
|
+
const otherTx = Model.TxProposal.create({
|
|
232
|
+
walletId: '456',
|
|
233
|
+
coin: 'btc',
|
|
234
|
+
network: 'livenet',
|
|
235
|
+
outputs: [{
|
|
236
|
+
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
237
|
+
amount: 999,
|
|
238
|
+
}],
|
|
239
|
+
feePerKb: 100e2,
|
|
240
|
+
creatorId: otherWallet.copayers[0].id,
|
|
241
|
+
});
|
|
242
|
+
otherTx.status = 'pending';
|
|
243
|
+
otherTx.txid = 'txid0';
|
|
244
|
+
await util.promisify(storage.storeTx).call(storage, '456', otherTx);
|
|
245
|
+
|
|
246
|
+
// Global lookup is unchanged: it still finds *a* proposal with this
|
|
247
|
+
// txid (which one is not guaranteed with duplicates), proving the
|
|
248
|
+
// existing internal-caller contract was not altered.
|
|
249
|
+
const globalTx = await util.promisify(storage.fetchTxByHash).call(storage, 'txid0');
|
|
250
|
+
should.exist(globalTx);
|
|
251
|
+
globalTx.txid.should.equal('txid0');
|
|
252
|
+
|
|
253
|
+
// The scoped method resolves the correct wallet's proposal regardless
|
|
254
|
+
// of the duplicate.
|
|
255
|
+
const txForWalletA = await util.promisify(storage.fetchTxByHashForWallet).call(storage, '123', 'txid0');
|
|
256
|
+
should.exist(txForWalletA);
|
|
257
|
+
txForWalletA.walletId.should.equal('123');
|
|
258
|
+
txForWalletA.id.should.equal(proposals[0].id);
|
|
259
|
+
|
|
260
|
+
const txForWalletB = await util.promisify(storage.fetchTxByHashForWallet).call(storage, '456', 'txid0');
|
|
261
|
+
should.exist(txForWalletB);
|
|
262
|
+
txForWalletB.walletId.should.equal('456');
|
|
263
|
+
txForWalletB.id.should.equal(otherTx.id);
|
|
264
|
+
|
|
265
|
+
// A wallet with no proposal for this txid gets nothing (not the other
|
|
266
|
+
// wallet's data) -- proving the query is scoped, not just filtered
|
|
267
|
+
// client-side.
|
|
268
|
+
const otherWallet2 = Model.Wallet.create({
|
|
269
|
+
id: '789',
|
|
270
|
+
name: 'third wallet',
|
|
271
|
+
m: 1,
|
|
272
|
+
n: 1,
|
|
273
|
+
coin: 'btc',
|
|
274
|
+
chain: 'btc',
|
|
275
|
+
network: 'livenet',
|
|
276
|
+
pubKey: '',
|
|
277
|
+
singleAddress: false,
|
|
278
|
+
derivationStrategy: 'BIP45',
|
|
279
|
+
addressType: 'P2SH',
|
|
280
|
+
});
|
|
281
|
+
const thirdCopayer = Model.Copayer.create({
|
|
282
|
+
coin: 'btc',
|
|
283
|
+
name: 'third copayer',
|
|
284
|
+
xPubKey: 'third xPubKey',
|
|
285
|
+
requestPubKey: 'third requestPubKey',
|
|
286
|
+
signature: 'third signature',
|
|
287
|
+
});
|
|
288
|
+
otherWallet2.addCopayer(thirdCopayer);
|
|
289
|
+
await util.promisify(storage.storeWalletAndUpdateCopayersLookup).call(storage, otherWallet2);
|
|
290
|
+
const noTx = await util.promisify(storage.fetchTxByHashForWallet).call(storage, '789', 'txid0');
|
|
291
|
+
should.not.exist(noTx);
|
|
292
|
+
});
|
|
293
|
+
|
|
203
294
|
it('should fetch all pending txs', function(done) {
|
|
204
295
|
storage.fetchPendingTxs('123', function(err, txs) {
|
|
205
296
|
should.not.exist(err);
|
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.8",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "ts_build/src/index.js",
|
|
8
8
|
"types": "./ts_build/src/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "npm run clean && npm run build && ./start.sh",
|
|
26
26
|
"stop": "./stop.sh",
|
|
27
|
-
"test": "npm run compile &&
|
|
27
|
+
"test": "npm run compile && mcr mocha ts_build/test",
|
|
28
28
|
"build": "tsc && npm run cp:templates",
|
|
29
29
|
"build:prod": "tsc -p tsconfig.prod.json && npm run cp:templates",
|
|
30
30
|
"build:docker": "docker build -t bitpay/bitcore-wallet-service:latest -f ./Dockerfile ../..",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@bitpay-labs/bitcore-logging": "^11.10.4",
|
|
45
|
-
"@bitpay-labs/bitcore-tss": "^11.10.
|
|
45
|
+
"@bitpay-labs/bitcore-tss": "^11.10.8",
|
|
46
46
|
"@bitpay-labs/crypto-wallet-core": "^11.10.4",
|
|
47
47
|
"@sendgrid/mail": "6.5.4",
|
|
48
48
|
"abi-decoder": "2.4.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coingecko.d.ts","sourceRoot":"","sources":["../../../src/externalservices/coingecko.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"coingecko.d.ts","sourceRoot":"","sources":["../../../src/externalservices/coingecko.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC;AA0I1D,qBAAa,gBAAgB;IAC3B,OAAO,EAAE,GAAG,CAAW;IACvB,OAAO,EAAE,OAAO,CAAC;gBAEL,OAAO,EAAE,OAAO;IAI5B,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,KAAK;YAaC,eAAe;IA6B7B,OAAO,CAAC,gBAAgB;YAqCV,kBAAkB;YAiElB,mBAAmB;YAkBnB,iBAAiB;YAcjB,gBAAgB;IA8ExB,uBAAuB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA2B1E,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC;IA6CxG,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CA2B3E"}
|
|
@@ -44,8 +44,8 @@ const constants_1 = require("../lib/common/constants");
|
|
|
44
44
|
const defaults_1 = require("../lib/common/defaults");
|
|
45
45
|
const logger_1 = __importDefault(require("../lib/logger"));
|
|
46
46
|
const SUPPORTED_FIAT_CODES = new Set(defaults_1.Defaults.FIAT_CURRENCIES.map(f => f.code));
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
const ALLOWED_NUMERIC_DAYS = new Set([1, 7, 30, 90, 365, 1825]);
|
|
48
|
+
const ALL_TIME_DAYS = 'max';
|
|
49
49
|
const MAX_QUERY_PARAM_LENGTH = 64;
|
|
50
50
|
const CHAIN_PARAM_PATTERN = /^[a-z0-9-_]+$/i;
|
|
51
51
|
const COIN_GECKO_REQUEST_TIMEOUT_MS = 15000;
|
|
@@ -106,6 +106,15 @@ function validateQueryParam(query, key) {
|
|
|
106
106
|
throw badRequest(`Unsupported ${key}`);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
function isAllowedNumericDays(value) {
|
|
110
|
+
return ALLOWED_NUMERIC_DAYS.has(value);
|
|
111
|
+
}
|
|
112
|
+
function parseFiatRateNumericDays(raw) {
|
|
113
|
+
const parsed = +raw;
|
|
114
|
+
if (!isAllowedNumericDays(parsed))
|
|
115
|
+
throw badRequest('Invalid days');
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
109
118
|
function parseCoinChainTokenAddress(query) {
|
|
110
119
|
return {
|
|
111
120
|
coin: validateQueryParam(query, 'coin'),
|
|
@@ -287,7 +296,7 @@ class CoinGeckoService {
|
|
|
287
296
|
const { API_KEY } = this.coinGeckoGetCredentials();
|
|
288
297
|
const headers = this.coinGeckoGetHeaders(API_KEY);
|
|
289
298
|
const params = { vs_currency: vsCurrency, days };
|
|
290
|
-
if (days >= 90)
|
|
299
|
+
if (days === ALL_TIME_DAYS || days >= 90)
|
|
291
300
|
params.interval = 'daily';
|
|
292
301
|
const url = this.cgUrl(`/v3/coins/${encodeURIComponent(id)}/market_chart`, params);
|
|
293
302
|
const body = await this.coinGeckoGetJson(url, headers);
|
|
@@ -402,14 +411,11 @@ class CoinGeckoService {
|
|
|
402
411
|
throw badRequest(`Unsupported fiat currency code '${currency}'`);
|
|
403
412
|
const vsCurrency = currency.toLowerCase();
|
|
404
413
|
const { coin: coinParam, chain: chainParam, tokenAddress: tokenAddressParam } = parseCoinChainTokenAddress(req.query);
|
|
405
|
-
let days =
|
|
414
|
+
let days = ALL_TIME_DAYS;
|
|
406
415
|
try {
|
|
407
416
|
const daysParam = getQueryString(req.query, 'days');
|
|
408
417
|
if (daysParam !== undefined) {
|
|
409
|
-
|
|
410
|
-
if (!ALLOWED_DAYS.has(parsed))
|
|
411
|
-
throw badRequest('Invalid days');
|
|
412
|
-
days = parsed;
|
|
418
|
+
days = parseFiatRateNumericDays(daysParam);
|
|
413
419
|
}
|
|
414
420
|
}
|
|
415
421
|
catch {
|