@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.
Files changed (92) hide show
  1. package/package.json +3 -3
  2. package/src/externalservices/coingecko.ts +24 -10
  3. package/src/externalservices/moonpay.ts +23 -1
  4. package/src/lib/chain/eth/abi-invoice.ts +139 -90
  5. package/src/lib/common/constants.ts +5 -2
  6. package/src/lib/common/defaults.ts +11 -2
  7. package/src/lib/errors/errordefinitions.ts +6 -0
  8. package/src/lib/model/copayer.ts +1 -0
  9. package/src/lib/model/tsskeygen.ts +33 -4
  10. package/src/lib/model/tsssign.ts +34 -3
  11. package/src/lib/pushnotificationsservice.ts +3 -1
  12. package/src/lib/routes/middleware/authTssRequest.ts +10 -4
  13. package/src/lib/routes/middleware/verifyTssMessage.ts +13 -8
  14. package/src/lib/routes/tss.ts +11 -4
  15. package/src/lib/server.ts +53 -25
  16. package/src/lib/storage.ts +24 -1
  17. package/src/lib/tss.ts +232 -30
  18. package/src/types/externalservices.d.ts +29 -1
  19. package/test/integration/externalservices/coinGecko.test.ts +71 -7
  20. package/test/integration/server.test.ts +731 -1
  21. package/test/integration/tss.test.ts +275 -0
  22. package/test/model/copayer.test.ts +128 -0
  23. package/test/storage.test.ts +91 -0
  24. package/ts_build/package.json +1 -1
  25. package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
  26. package/ts_build/src/externalservices/coingecko.js +14 -8
  27. package/ts_build/src/externalservices/coingecko.js.map +1 -1
  28. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  29. package/ts_build/src/externalservices/moonpay.js +38 -2
  30. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  31. package/ts_build/src/lib/chain/eth/abi-invoice.d.ts +254 -41
  32. package/ts_build/src/lib/chain/eth/abi-invoice.d.ts.map +1 -1
  33. package/ts_build/src/lib/chain/eth/abi-invoice.js +138 -89
  34. package/ts_build/src/lib/chain/eth/abi-invoice.js.map +1 -1
  35. package/ts_build/src/lib/common/constants.d.ts +4 -2
  36. package/ts_build/src/lib/common/constants.d.ts.map +1 -1
  37. package/ts_build/src/lib/common/constants.js +4 -2
  38. package/ts_build/src/lib/common/constants.js.map +1 -1
  39. package/ts_build/src/lib/common/defaults.d.ts +4 -2
  40. package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
  41. package/ts_build/src/lib/common/defaults.js +4 -2
  42. package/ts_build/src/lib/common/defaults.js.map +1 -1
  43. package/ts_build/src/lib/common/index.d.ts +8 -4
  44. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  45. package/ts_build/src/lib/errors/errordefinitions.d.ts +3 -0
  46. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  47. package/ts_build/src/lib/errors/errordefinitions.js +3 -0
  48. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  49. package/ts_build/src/lib/model/copayer.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/copayer.js +1 -0
  51. package/ts_build/src/lib/model/copayer.js.map +1 -1
  52. package/ts_build/src/lib/model/tsskeygen.d.ts +3 -0
  53. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  54. package/ts_build/src/lib/model/tsskeygen.js +12 -3
  55. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  56. package/ts_build/src/lib/model/tsssign.d.ts +7 -0
  57. package/ts_build/src/lib/model/tsssign.d.ts.map +1 -1
  58. package/ts_build/src/lib/model/tsssign.js +22 -2
  59. package/ts_build/src/lib/model/tsssign.js.map +1 -1
  60. package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
  61. package/ts_build/src/lib/pushnotificationsservice.js +3 -1
  62. package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
  63. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  64. package/ts_build/src/lib/routes/middleware/authTssRequest.js +8 -2
  65. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  66. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  67. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +11 -6
  68. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  69. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  70. package/ts_build/src/lib/routes/tss.js +4 -4
  71. package/ts_build/src/lib/routes/tss.js.map +1 -1
  72. package/ts_build/src/lib/server.d.ts.map +1 -1
  73. package/ts_build/src/lib/server.js +33 -25
  74. package/ts_build/src/lib/server.js.map +1 -1
  75. package/ts_build/src/lib/storage.d.ts +4 -0
  76. package/ts_build/src/lib/storage.d.ts.map +1 -1
  77. package/ts_build/src/lib/storage.js +18 -1
  78. package/ts_build/src/lib/storage.js.map +1 -1
  79. package/ts_build/src/lib/tss.d.ts +4 -0
  80. package/ts_build/src/lib/tss.d.ts.map +1 -1
  81. package/ts_build/src/lib/tss.js +74 -17
  82. package/ts_build/src/lib/tss.js.map +1 -1
  83. package/ts_build/test/integration/externalservices/coinGecko.test.js +62 -8
  84. package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
  85. package/ts_build/test/integration/server.test.js +597 -0
  86. package/ts_build/test/integration/server.test.js.map +1 -1
  87. package/ts_build/test/integration/tss.test.js +245 -0
  88. package/ts_build/test/integration/tss.test.js.map +1 -1
  89. package/ts_build/test/model/copayer.test.js +91 -0
  90. package/ts_build/test/model/copayer.test.js.map +1 -1
  91. package/ts_build/test/storage.test.js +73 -0
  92. package/ts_build/test/storage.test.js.map +1 -1
@@ -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]);
@@ -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);
@@ -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.6",
5
+ "version": "11.10.7",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -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;AA2H1D,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;IA8CxG,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CA2B3E"}
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 ALLOWED_DAYS = new Set([1, 7, 30, 90, 365, 1825]);
48
- const DEFAULT_ALL_TIME_DAYS = 100000;
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 = DEFAULT_ALL_TIME_DAYS;
414
+ let days = ALL_TIME_DAYS;
406
415
  try {
407
416
  const daysParam = getQueryString(req.query, 'days');
408
417
  if (daysParam !== undefined) {
409
- const parsed = +daysParam;
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 {