@bitpay-labs/bitcore-wallet-service 11.11.1 → 11.11.2

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 (60) hide show
  1. package/Dockerfile +1 -1
  2. package/package.json +5 -5
  3. package/src/lib/chain/btc/index.ts +3 -2
  4. package/src/lib/chain/eth/index.ts +134 -117
  5. package/src/lib/chain/sol/index.ts +1 -1
  6. package/src/lib/chain/xrp/index.ts +1 -1
  7. package/src/lib/routes/wallets.ts +3 -1
  8. package/src/lib/server.ts +136 -88
  9. package/src/lib/storage.ts +90 -20
  10. package/test/chain/eth.test.ts +70 -0
  11. package/test/chain/sol.test.ts +1 -1
  12. package/test/chain/xrp.test.ts +1 -1
  13. package/test/expressapp.test.ts +144 -0
  14. package/test/integration/addressReads.test.ts +503 -0
  15. package/test/integration/bcmonitor.test.ts +44 -0
  16. package/test/integration/server.test.ts +198 -48
  17. package/test/integration/walletReads.test.ts +488 -0
  18. package/ts_build/package.json +3 -3
  19. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  20. package/ts_build/src/lib/chain/btc/index.js +3 -2
  21. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  22. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  23. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  24. package/ts_build/src/lib/chain/eth/index.js +114 -102
  25. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  26. package/ts_build/src/lib/chain/sol/index.js +1 -1
  27. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  28. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  29. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  30. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  31. package/ts_build/src/lib/routes/wallets.js +2 -1
  32. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  33. package/ts_build/src/lib/server.d.ts +1 -1
  34. package/ts_build/src/lib/server.d.ts.map +1 -1
  35. package/ts_build/src/lib/server.js +128 -94
  36. package/ts_build/src/lib/server.js.map +1 -1
  37. package/ts_build/src/lib/storage.d.ts +17 -10
  38. package/ts_build/src/lib/storage.d.ts.map +1 -1
  39. package/ts_build/src/lib/storage.js +73 -20
  40. package/ts_build/src/lib/storage.js.map +1 -1
  41. package/ts_build/test/chain/eth.test.js +59 -0
  42. package/ts_build/test/chain/eth.test.js.map +1 -1
  43. package/ts_build/test/chain/sol.test.js +1 -1
  44. package/ts_build/test/chain/sol.test.js.map +1 -1
  45. package/ts_build/test/chain/xrp.test.js +1 -1
  46. package/ts_build/test/chain/xrp.test.js.map +1 -1
  47. package/ts_build/test/expressapp.test.js +132 -0
  48. package/ts_build/test/expressapp.test.js.map +1 -1
  49. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  50. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  51. package/ts_build/test/integration/addressReads.test.js +465 -0
  52. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  53. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  54. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  55. package/ts_build/test/integration/server.test.js +181 -48
  56. package/ts_build/test/integration/server.test.js.map +1 -1
  57. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  58. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  59. package/ts_build/test/integration/walletReads.test.js +436 -0
  60. package/ts_build/test/integration/walletReads.test.js.map +1 -0
@@ -0,0 +1,488 @@
1
+ 'use strict';
2
+
3
+ import * as chai from 'chai';
4
+ import 'chai/register-should';
5
+ import util from 'util';
6
+ import sinon from 'sinon';
7
+ import { WalletService } from '../../src/lib/server';
8
+ import { Storage } from '../../src/lib/storage';
9
+ import * as TestData from '../testdata';
10
+ import helpers from './helpers';
11
+
12
+ const should = chai.should();
13
+
14
+ const TOKEN_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
15
+ const TO_ADDRESS = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
16
+ // The fields getStatus leaves out of the wallet and of each copayer unless includeExtendedInfo is set
17
+ const WALLET_EXTENDED_KEYS = ['publicKeyRing', 'pubKey', 'addressManager'];
18
+ const COPAYER_EXTENDED_KEYS = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
19
+ const omit = (obj, keys: string[]) => Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key)));
20
+
21
+ describe('Wallet reads', function() {
22
+ let blockchainExplorer;
23
+ let storage: Storage;
24
+
25
+ before(async function() {
26
+ ({ blockchainExplorer, storage } = await helpers.before());
27
+ });
28
+
29
+ beforeEach(async function() {
30
+ await helpers.beforeEach();
31
+ });
32
+
33
+ afterEach(function() {
34
+ sinon.restore();
35
+ });
36
+
37
+ after(async function() {
38
+ await helpers.after();
39
+ });
40
+
41
+ const call = (server: WalletService, method: string, opts = {}) => util.promisify(server[method]).call(server, opts);
42
+ // The wallet document as stored, as plain JSON
43
+ const storedWallet = async (walletId: string) => JSON.parse(JSON.stringify(await util.promisify(storage.fetchWallet).call(storage, walletId)));
44
+ const createTx = (server, amount, message = 'some message') => helpers.createAndPublishTx(
45
+ server,
46
+ { outputs: [{ toAddress: TO_ADDRESS, amount }], feePerKb: 100e2, message },
47
+ TestData.copayers[0].privKey_1H_0
48
+ );
49
+ const sign = (server, txp, copayer = 0) => call(server, 'signTx', {
50
+ txProposalId: txp.id,
51
+ signatures: helpers.clientSign(txp, TestData.copayers[copayer].xPrivKey_44H_0H_0H)
52
+ });
53
+
54
+ // Returns a function that counts the wallet's reads since this was called
55
+ const countWalletReads = (walletId: string) => {
56
+ const fetchWallet = sinon.spy(storage, 'fetchWallet');
57
+ return () => fetchWallet.getCalls().filter(c => c.args[0] === walletId).length;
58
+ };
59
+
60
+ describe('getStatus', function() {
61
+ it('returns the stored wallet without the extended fields', async function() {
62
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 2);
63
+ await call(server, 'getStatus'); // registers the wallet
64
+ const status = await call(server, 'getStatus');
65
+
66
+ const expected: any = omit(await storedWallet(wallet.id), WALLET_EXTENDED_KEYS);
67
+ expected.copayers = expected.copayers.map(copayer => omit(copayer, COPAYER_EXTENDED_KEYS));
68
+ JSON.parse(JSON.stringify(status.wallet)).should.deep.equal(expected);
69
+ });
70
+
71
+ it('returns the stored wallet with the extended fields, and only the caller\'s customData', async function() {
72
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 2);
73
+ await call(server, 'getStatus');
74
+ const status = await call(server, 'getStatus', { includeExtendedInfo: true });
75
+
76
+ const expected = await storedWallet(wallet.id);
77
+ expected.copayers = expected.copayers.map(copayer => copayer.id == server.copayerId ? copayer : omit(copayer, ['customData']));
78
+ JSON.parse(JSON.stringify(status.wallet)).should.deep.equal(expected);
79
+ status.wallet.copayers.find(c => c.id == server.copayerId).customData.should.equal('custom data 1');
80
+ });
81
+
82
+ for (const includeExtendedInfo of [false, true]) {
83
+ it(`keeps every copayer's customData in storage when a status registers the wallet (includeExtendedInfo: ${includeExtendedInfo})`, async function() {
84
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 2);
85
+ (await storedWallet(wallet.id)).beRegistered.should.equal(false);
86
+
87
+ await call(server, 'getStatus', { includeExtendedInfo });
88
+
89
+ blockchainExplorer.register.calledOnce.should.equal(true);
90
+ const stored = await storedWallet(wallet.id);
91
+ stored.beRegistered.should.equal(true);
92
+ stored.copayers.map(c => c.customData).should.deep.equal(['custom data 1', 'custom data 2']);
93
+ stored.should.not.have.property('tokenAddress');
94
+ stored.should.not.have.property('multisigContractAddress');
95
+ });
96
+ }
97
+
98
+ it('keeps a token status and a native status of one wallet apart when they run together', async function() {
99
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'eth' });
100
+ await call(server, 'createAddress');
101
+ await helpers.stubUtxos(server, wallet, [1], { tokenAddress: TOKEN_ADDRESS });
102
+ // the token address each balance call saw, read when it was made
103
+ const seen = [];
104
+ const getBalance = blockchainExplorer.getBalance;
105
+ sinon.stub(blockchainExplorer, 'getBalance').callsFake((w, cb) => {
106
+ seen.push(w.tokenAddress || null);
107
+ return getBalance(w, cb);
108
+ });
109
+
110
+ const [native, token] = await Promise.all([
111
+ call(server, 'getStatus'),
112
+ call(server, 'getStatus', { tokenAddress: TOKEN_ADDRESS })
113
+ ]);
114
+
115
+ native.balance.totalAmount.should.equal(1e18);
116
+ token.balance.totalAmount.should.equal(2e6);
117
+ seen.sort().should.deep.equal([null, TOKEN_ADDRESS].sort());
118
+ });
119
+
120
+ it('keeps the token address the balance step sets out of the returned and the stored wallet', async function() {
121
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'eth' });
122
+ await call(server, 'createAddress');
123
+ await helpers.stubUtxos(server, wallet, [1], { tokenAddress: TOKEN_ADDRESS });
124
+ // an unregistered wallet, so the balance step stores its copy
125
+ await storage.db.collection(Storage.collections.WALLETS).updateOne({ id: wallet.id }, { $set: { beRegistered: false } });
126
+
127
+ const status = await call(server, 'getStatus', { tokenAddress: TOKEN_ADDRESS, includeExtendedInfo: true });
128
+
129
+ status.balance.totalAmount.should.equal(2e6);
130
+ status.wallet.should.not.have.property('tokenAddress');
131
+ status.wallet.should.not.have.property('multisigContractAddress');
132
+ const stored = await storedWallet(wallet.id);
133
+ stored.beRegistered.should.equal(true);
134
+ stored.should.not.have.property('tokenAddress');
135
+ JSON.parse(JSON.stringify(status.wallet)).should.deep.equal({ ...stored, beRegistered: status.wallet.beRegistered });
136
+ });
137
+
138
+ it('counts pending txps as locked and the inputs of broadcast txps as spent', async function() {
139
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
140
+ await helpers.stubUtxos(server, wallet, [1, 2]);
141
+ const broadcast = await sign(server, await createTx(server, 1.5e8));
142
+ helpers.stubBroadcast(broadcast.txid);
143
+ await call(server, 'broadcastTx', { txProposalId: broadcast.id });
144
+ const pending = await createTx(server, 0.5e8);
145
+
146
+ const status = await call(server, 'getStatus');
147
+
148
+ const sum = inputs => inputs.reduce((total, input) => total + input.satoshis, 0);
149
+ status.balance.totalAmount.should.equal(3e8 - sum(broadcast.inputs));
150
+ status.balance.lockedAmount.should.equal(sum(pending.inputs));
151
+ status.pendingTxps.map(t => t.id).should.deep.equal([pending.id]);
152
+ });
153
+
154
+ it('adds the creator\'s and the copayers\' names to pending txps', async function() {
155
+ const { server, wallet } = await helpers.createAndJoinWallet(2, 3);
156
+ await helpers.stubUtxos(server, wallet, [1]);
157
+ const txp = await createTx(server, 0.5e8);
158
+ const server2 = await helpers.getAuthServer(wallet.copayers[1].id);
159
+ await sign(server2, txp, 1);
160
+
161
+ const status = await call(server, 'getStatus');
162
+
163
+ status.pendingTxps.length.should.equal(1);
164
+ const [pending] = status.pendingTxps;
165
+ pending.creatorName.should.equal('copayer 1');
166
+ pending.actions.map(a => [a.type, a.copayerName]).should.deep.equal([['accept', 'copayer 2']]);
167
+ pending.derivationStrategy.should.equal('BIP44');
168
+ should.not.exist(pending.raw);
169
+ });
170
+
171
+ it('adds the raw tx to accepted pending txps', async function() {
172
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 2);
173
+ await helpers.stubUtxos(server, wallet, [1]);
174
+ const signed = await sign(server, await createTx(server, 0.5e8));
175
+
176
+ const status = await call(server, 'getStatus');
177
+
178
+ status.pendingTxps.length.should.equal(1);
179
+ status.pendingTxps[0].status.should.equal('accepted');
180
+ status.pendingTxps[0].raw.should.equal(signed.getRawTx());
181
+ });
182
+
183
+ it('falls back to BIP45 for txps of a wallet stored without a derivation strategy', async function() {
184
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
185
+ await helpers.stubUtxos(server, wallet, [1]);
186
+ await createTx(server, 0.5e8);
187
+ await storage.db.collection(Storage.collections.WALLETS).updateOne({ id: wallet.id }, { $unset: { derivationStrategy: '' } });
188
+
189
+ const status = await call(server, 'getStatus');
190
+
191
+ status.pendingTxps[0].derivationStrategy.should.equal('BIP45');
192
+ });
193
+
194
+ it('migrates an old BCH wallet to cash addresses', async function() {
195
+ const { server } = await helpers.createAndJoinWallet(1, 1, { coin: 'bch', earlyRet: true, nativeCashAddr: false });
196
+ (await storedWallet(server.walletId)).nativeCashAddr.should.equal(false);
197
+
198
+ const status = await call(server, 'getStatus');
199
+
200
+ status.wallet.nativeCashAddr.should.equal(true);
201
+ (await storedWallet(server.walletId)).nativeCashAddr.should.equal(true);
202
+ });
203
+
204
+ describe('wallet reads', function() {
205
+ it('reads the wallet once for a UTXO wallet with pending and broadcast txps', async function() {
206
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
207
+ await helpers.stubUtxos(server, wallet, [1, 1, 1]);
208
+ const broadcast = await sign(server, await createTx(server, 0.5e8));
209
+ helpers.stubBroadcast(broadcast.txid);
210
+ await call(server, 'broadcastTx', { txProposalId: broadcast.id });
211
+ await createTx(server, 0.5e8);
212
+ await sign(server, await createTx(server, 0.5e8)); // accepted, not yet broadcast
213
+
214
+ const reads = countWalletReads(wallet.id);
215
+ await call(server, 'getStatus');
216
+ reads().should.equal(1);
217
+ });
218
+
219
+ it('reads the wallet once for an account-based wallet', async function() {
220
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'eth' });
221
+ await call(server, 'createAddress');
222
+ await helpers.stubUtxos(server, wallet, [1]);
223
+
224
+ const reads = countWalletReads(wallet.id);
225
+ await call(server, 'getStatus');
226
+ reads().should.equal(1);
227
+ });
228
+
229
+ it('reads the wallet once when the status registers the wallet', async function() {
230
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 2);
231
+ (await storedWallet(wallet.id)).beRegistered.should.equal(false);
232
+
233
+ const reads = countWalletReads(wallet.id);
234
+ await call(server, 'getStatus');
235
+ reads().should.equal(1);
236
+ blockchainExplorer.register.calledOnce.should.equal(true);
237
+ });
238
+
239
+ it('migrates an old BCH wallet once', async function() {
240
+ const { server } = await helpers.createAndJoinWallet(1, 1, { coin: 'bch', earlyRet: true, nativeCashAddr: false });
241
+ const migrate = sinon.spy(storage, 'migrateToCashAddr');
242
+ await call(server, 'getStatus');
243
+ migrate.callCount.should.equal(1);
244
+ });
245
+ });
246
+
247
+ describe('errors', function() {
248
+ it('fails with UPGRADE_NEEDED for a SOL wallet and an old client', async function() {
249
+ const { server } = await helpers.createAndJoinWallet(1, 1, { coin: 'sol' });
250
+ server.clientVersion = 'bwc-10.10.0';
251
+ const err = await call(server, 'getStatus').catch(e => e);
252
+ err.code.should.equal('UPGRADE_NEEDED');
253
+ });
254
+
255
+ it('fails with WALLET_NOT_FOUND when the wallet is gone', async function() {
256
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
257
+ await storage.db.collection(Storage.collections.WALLETS).deleteOne({ id: wallet.id });
258
+ const err = await call(server, 'getStatus').catch(e => e);
259
+ err.code.should.equal('WALLET_NOT_FOUND');
260
+ });
261
+
262
+ it('passes on a storage error reading the wallet', async function() {
263
+ const { server } = await helpers.createAndJoinWallet(1, 1);
264
+ sinon.stub(server.storage, 'fetchWallet').callsArgWith(1, new Error('wallet read failed'));
265
+ const err = await call(server, 'getStatus').catch(e => e);
266
+ err.message.should.equal('wallet read failed');
267
+ });
268
+
269
+ it('passes on a storage error reading the pending txps', async function() {
270
+ const { server } = await helpers.createAndJoinWallet(1, 1);
271
+ sinon.stub(server.storage, 'fetchPendingTxs').callsArgWith(1, new Error('pending read failed'));
272
+ const err = await call(server, 'getStatus').catch(e => e);
273
+ err.message.should.equal('pending read failed');
274
+ });
275
+
276
+ it('passes on a storage error reading the preferences', async function() {
277
+ const { server } = await helpers.createAndJoinWallet(1, 1);
278
+ sinon.stub(server.storage, 'fetchPreferences').callsArgWith(2, new Error('preferences read failed'));
279
+ const err = await call(server, 'getStatus').catch(e => e);
280
+ err.message.should.equal('preferences read failed');
281
+ });
282
+
283
+ it('passes on a balance error, with or without includeExtendedInfo', async function() {
284
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
285
+ await helpers.stubUtxos(server, wallet, [1]);
286
+ sinon.stub(blockchainExplorer, 'getUtxos').callsArgWith(2, new Error('utxos failed'));
287
+ for (const includeExtendedInfo of [false, true]) {
288
+ const err = await call(server, 'getStatus', { includeExtendedInfo }).catch(e => e);
289
+ err.message.should.equal('utxos failed');
290
+ }
291
+ });
292
+
293
+ it('returns no balance for a wallet that needs a scan only with includeExtendedInfo', async function() {
294
+ const { server, wallet } = await helpers.createAndJoinWallet(1, 1);
295
+ await storage.db.collection(Storage.collections.WALLETS).updateOne({ id: wallet.id }, { $set: { scanStatus: 'error' } });
296
+
297
+ const err = await call(server, 'getStatus').catch(e => e);
298
+ err.code.should.equal('WALLET_NEED_SCAN');
299
+
300
+ const status = await call(server, 'getStatus', { includeExtendedInfo: true });
301
+ should.exist(status.wallet);
302
+ should.not.exist(status.balance);
303
+ });
304
+ });
305
+ });
306
+
307
+ describe('tx and note decoration', function() {
308
+ let server: WalletService;
309
+ let server2: WalletService;
310
+ let txp;
311
+
312
+ // A 1-of-2 wallet with one broadcast txp, and a note on its txid edited by the second copayer
313
+ beforeEach(async function() {
314
+ let wallet;
315
+ ({ server, wallet } = await helpers.createAndJoinWallet(1, 2));
316
+ await helpers.stubUtxos(server, wallet, [1, 1]);
317
+ txp = await sign(server, await createTx(server, 0.5e8));
318
+ helpers.stubBroadcast(txp.txid);
319
+ await call(server, 'broadcastTx', { txProposalId: txp.id });
320
+ server2 = await helpers.getAuthServer(wallet.copayers[1].id);
321
+ await call(server2, 'editTxNote', { txid: txp.txid, body: 'a note' });
322
+ });
323
+
324
+ const expectDecorated = tx => {
325
+ tx.creatorName.should.equal('copayer 1');
326
+ tx.actions.map(a => [a.type, a.copayerName]).should.deep.equal([['accept', 'copayer 1']]);
327
+ tx.note.body.should.equal('a note');
328
+ tx.note.editedByName.should.equal('copayer 2');
329
+ };
330
+
331
+ it('getTx adds the names and the note', async function() {
332
+ const tx = await call(server, 'getTx', { txProposalId: txp.id });
333
+ expectDecorated(tx);
334
+ tx.derivationStrategy.should.equal('BIP44');
335
+ });
336
+
337
+ it('getTxByHash adds the names and the note', async function() {
338
+ const tx = await call(server, 'getTxByHash', { txid: txp.txid });
339
+ expectDecorated(tx);
340
+ });
341
+
342
+ it('getTxHistory adds the proposal and the note to the matching tx', async function() {
343
+ helpers.stubHistory(0, 1000, [{
344
+ txid: txp.txid,
345
+ category: 'send',
346
+ satoshis: 0.5e8,
347
+ fee: 1000,
348
+ height: 900,
349
+ address: TO_ADDRESS,
350
+ outputs: [{ address: TO_ADDRESS, amount: 0.5e8 }],
351
+ // proposals are matched within days of the tx's time, so it has to be recent
352
+ blockTime: new Date().toISOString()
353
+ }]);
354
+
355
+ const history = await call(server, 'getTxHistory');
356
+
357
+ history.length.should.equal(1);
358
+ const [tx] = history;
359
+ tx.proposalId.should.equal(txp.id);
360
+ tx.creatorName.should.equal('copayer 1');
361
+ tx.message.should.equal('some message');
362
+ tx.actions.map(a => [a.type, a.copayerName]).should.deep.equal([['accept', 'copayer 1']]);
363
+ tx.note.body.should.equal('a note');
364
+ tx.note.editedByName.should.equal('copayer 2');
365
+ });
366
+
367
+ it('getTx reads the wallet once', async function() {
368
+ const reads = countWalletReads(server.walletId);
369
+ await call(server, 'getTx', { txProposalId: txp.id });
370
+ reads().should.equal(1);
371
+ });
372
+
373
+ it('getTxByHash reads the wallet once', async function() {
374
+ const reads = countWalletReads(server.walletId);
375
+ await call(server, 'getTxByHash', { txid: txp.txid });
376
+ reads().should.equal(1);
377
+ });
378
+
379
+ it('getTxHistory reads the wallet once', async function() {
380
+ helpers.stubHistory(0, 1000, [{
381
+ txid: txp.txid, category: 'send', satoshis: 0.5e8, fee: 1000, height: 900, address: TO_ADDRESS,
382
+ outputs: [{ address: TO_ADDRESS, amount: 0.5e8 }], blockTime: new Date().toISOString()
383
+ }]);
384
+ const reads = countWalletReads(server.walletId);
385
+ const history = await call(server, 'getTxHistory');
386
+ should.exist(history[0].proposalId);
387
+ reads().should.equal(1);
388
+ });
389
+
390
+ it('getTx and getTxByHash fail with TX_NOT_FOUND for an unknown tx', async function() {
391
+ for (const [method, opts] of [['getTx', { txProposalId: 'unknown' }], ['getTxByHash', { txid: 'unknown' }]] as Array<[string, object]>) {
392
+ const err: any = await call(server, method, opts).catch(e => e);
393
+ err.code.should.equal('TX_NOT_FOUND');
394
+ }
395
+ });
396
+
397
+ it('getTx returns a txp that has not been broadcast, without a note', async function() {
398
+ const pending = await createTx(server, 0.1e8);
399
+ const tx = await call(server, 'getTx', { txProposalId: pending.id });
400
+ tx.id.should.equal(pending.id);
401
+ tx.creatorName.should.equal('copayer 1');
402
+ should.not.exist(tx.txid);
403
+ should.not.exist(tx.note);
404
+ });
405
+
406
+ const byIdAndHash = () => [['getTx', { txProposalId: txp.id }], ['getTxByHash', { txid: txp.txid }]] as Array<[string, object]>;
407
+
408
+ it('getTx and getTxByHash pass on a storage error reading the wallet, after one read', async function() {
409
+ const fetchWallet = sinon.stub(storage, 'fetchWallet').callsArgWith(1, new Error('wallet read failed'));
410
+ for (const [method, opts] of byIdAndHash()) {
411
+ fetchWallet.resetHistory();
412
+ const err: any = await call(server, method, opts).catch(e => e);
413
+ err.message.should.equal('wallet read failed');
414
+ fetchWallet.callCount.should.equal(1, method);
415
+ }
416
+ });
417
+
418
+ it('getTx and getTxByHash pass on a storage error looking up the tx', async function() {
419
+ sinon.stub(storage, 'fetchTx').callsArgWith(2, new Error('tx read failed'));
420
+ sinon.stub(storage, 'fetchTxByHashForWallet').callsArgWith(2, new Error('tx read failed'));
421
+ for (const [method, opts] of byIdAndHash()) {
422
+ const err: any = await call(server, method, opts).catch(e => e);
423
+ err.message.should.equal('tx read failed', method);
424
+ }
425
+ });
426
+
427
+ it('getTx and getTxByHash return the tx without its note when the note read fails', async function() {
428
+ sinon.stub(storage, 'fetchTxNote').callsArgWith(2, new Error('note read failed'));
429
+ for (const [method, opts] of byIdAndHash()) {
430
+ const tx = await call(server, method, opts);
431
+ tx.id.should.equal(txp.id);
432
+ should.not.exist(tx.note);
433
+ }
434
+ });
435
+
436
+ it('getTxByHash returns a tx that has no txid as it is', async function() {
437
+ // A tx found by its hash always has a txid; the stub covers the guard for one that doesn't
438
+ const tx = await util.promisify(storage.fetchTx).call(storage, server.walletId, txp.id);
439
+ tx.txid = undefined;
440
+ sinon.stub(storage, 'fetchTxByHashForWallet').callsFake((walletId, hash, cb) => cb(null, tx));
441
+ const fetchTxNote = sinon.spy(storage, 'fetchTxNote');
442
+
443
+ const result = await call(server, 'getTxByHash', { txid: txp.txid });
444
+
445
+ result.id.should.equal(txp.id);
446
+ fetchTxNote.called.should.equal(false);
447
+ });
448
+
449
+ it('each storage fetch completes from the wallet it is given, without reading it', async function() {
450
+ // A copy of the wallet with renamed copayers shows which wallet the names came from
451
+ const given = await util.promisify(storage.fetchWallet).call(storage, server.walletId);
452
+ for (const copayer of given.copayers) copayer.name = 'given ' + copayer.name;
453
+ const fetchWallet = sinon.spy(storage, 'fetchWallet');
454
+ const walletId = server.walletId;
455
+ const pending = await createTx(server, 0.1e8);
456
+ fetchWallet.resetHistory();
457
+
458
+ const fetches: Array<[string, (cb) => void, (result) => string[]]> = [
459
+ ['fetchTx', cb => storage.fetchTx(walletId, txp.id, cb, given), tx => [tx.creatorName]],
460
+ ['fetchTxByHashForWallet', cb => storage.fetchTxByHashForWallet(walletId, txp.txid, cb, given), tx => [tx.creatorName]],
461
+ ['fetchPendingTxs', cb => storage.fetchPendingTxs(walletId, cb, given), txs => txs.map(t => t.creatorName)],
462
+ ['fetchTxs', cb => storage.fetchTxs(walletId, {}, cb, given), txs => txs.map(t => t.creatorName)],
463
+ ['fetchBroadcastedTxs', cb => storage.fetchBroadcastedTxs(walletId, {}, cb, given), txs => txs.map(t => t.creatorName)],
464
+ ['fetchTxNote', cb => storage.fetchTxNote(walletId, txp.txid, cb, given), note => [note.editedByName]],
465
+ ['fetchTxNotes', cb => storage.fetchTxNotes(walletId, {}, cb, given), notes => notes.map(n => n.editedByName)]
466
+ ];
467
+ for (const [name, fetch, names] of fetches) {
468
+ const result = await util.promisify(fetch)();
469
+ const got = names(result);
470
+ got.length.should.be.above(0, name);
471
+ got.every(n => n.startsWith('given ')).should.equal(true, name);
472
+ }
473
+ fetchWallet.callCount.should.equal(0);
474
+ should.exist(pending);
475
+ });
476
+
477
+ it('a storage error reading the wallet fails each completion', async function() {
478
+ sinon.stub(storage, 'fetchWallet').callsArgWith(1, new Error('wallet read failed'));
479
+ for (const run of [
480
+ cb => storage.fetchTx(server.walletId, txp.id, cb),
481
+ cb => storage.fetchTxNote(server.walletId, txp.txid, cb)
482
+ ]) {
483
+ const err: any = await util.promisify(run)().catch(e => e);
484
+ err.message.should.equal('wallet read failed');
485
+ }
486
+ });
487
+ });
488
+ });
@@ -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.11.0",
5
+ "version": "11.11.1",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@bitpay-labs/bitcore-logging": "^11.10.9",
45
- "@bitpay-labs/bitcore-tss": "^11.11.0",
46
- "@bitpay-labs/crypto-wallet-core": "^11.11.0",
45
+ "@bitpay-labs/bitcore-tss": "^11.11.1",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.11.1",
47
47
  "@sendgrid/mail": "6.5.4",
48
48
  "abi-decoder": "2.4.0",
49
49
  "async": "0.9.2",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/chain/btc/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/chain/btc/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAK7C,qBAAa,QAAS,YAAW,MAAM;IAIzB,OAAO,CAAC,UAAU;IAH9B,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACvC,SAAS,CAAC,yBAAyB,EAAE,MAAM,CAAC;gBAExB,UAAU,MAAa;IAK3C,mBAAmB,CAAC,IAAI,GAAE,GAAQ,GAAG,MAAM;IAO3C,wBAAwB,CAAC,IAAI,GAAE,GAAQ,GAAG,MAAM;IAOhD,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,KAAA,EAAE,IAAI,KAAA,EAAE,EAAE,KAAA;IAoCxD,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,KAAA,EAAE,IAAI,KAAA,EAAE,EAAE,KAAA;IAyF5D,kBAAkB;IAIlB,mBAAmB;IAInB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,KAAA,EAAE,IAAI,KAAA;IAmCpD,SAAS,CAAC,MAAM,KAAA;IAQhB,iBAAiB,CAAC,MAAM,KAAA;IAmBxB,8BAA8B,CAAC,GAAG,KAAA,EAAE,IAAI;;KAAoC;IA8B5E,+BAA+B,CAAC,OAAO,CAAC,EAAE,MAAM;IAShD,8BAA8B,CAAC,WAAW,CAAC,EAAE,MAAM;IAuBnD,gBAAgB,CAAC,GAAG,KAAA,EAAE,IAAI,CAAC,KAAA;IA4B3B,eAAe,CAAC,GAAG,KAAA,EAAE,IAAI,KAAA;IAoBzB,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,KAAA,EAAE,IAAI,KAAA;IAQ1C,oBAAoB,CAAC,IAAI,EAAE,UAAU;IAKrC,YAAY,CAAC,GAAG,KAAA,EAAE,IAAI;;KAAmB;IAgGzC,eAAe,CAAC,CAAC,KAAA,EAAE,QAAQ,KAAA;IAI3B,OAAO,CAAC,GAAG,KAAA;IAkDX,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,KAAA,EAAE,IAAI,KAAA,EAAE,EAAE,KAAA;IAmCjD,aAAa,CAAC,KAAK,KAAA;;;;;;;;IAgBnB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,KAAA,EAAE,MAAM,KAAA,EAAE,IAAI,KAAA,EAAE,EAAE,KAAA;IAsU3D,UAAU,CAAC,IAAI,KAAA;IAIf,kBAAkB,CAAC,MAAM,KAAA,GAAG,OAAO;IAInC,gBAAgB;IAIhB,oBAAoB;IAIpB,mBAAmB,CAAC,OAAO,EAAE,MAAM;IAMnC,WAAW;IAGX,eAAe;IAIf,2BAA2B,CAAC,QAAQ,KAAA,EAAE,QAAQ,KAAA;IAE9C,yBAAyB,CAAC,QAAQ,KAAA,EAAE,QAAQ,KAAA;IAE5C,wBAAwB,CAAC,EAAE,KAAA,EAAE,MAAM,KAAA,EAAE,UAAU,KAAA,EAAE,UAAU,KAAA,EAAE,IAAI,KAAA,EAAE,aAAa,KAAA;IA2BhF,eAAe,CAAC,MAAM,KAAA,EAAE,MAAM,KAAA,EAAE,KAAK,KAAA;IAiBrC,SAAS,CAAC,iBAAiB,CAAC,MAAM,KAAA,EAAE,IAAI,KAAA;IAUxC,MAAM,CAAC,IAAI,KAAA;;;;;;;IAcX,IAAI,CAAC,GAAG,KAAA;IAIR,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,KAAA,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI;IAIvF,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,KAAA,EAAE,KAAK,KAAA,EAAE,EAAE,KAAA;CAGrD"}
@@ -73,7 +73,8 @@ class BtcChain {
73
73
  getWalletBalance(server, wallet, opts, cb) {
74
74
  server.getUtxosForCurrentWallet({
75
75
  coin: opts.coin,
76
- addresses: opts.addresses
76
+ addresses: opts.addresses,
77
+ wallet
77
78
  }, (err, utxos) => {
78
79
  if (err)
79
80
  return cb(err);
@@ -183,7 +184,7 @@ class BtcChain {
183
184
  return new Promise((resolve, reject) => {
184
185
  const getChangeAddress = (wallet, cb) => {
185
186
  if (wallet.singleAddress) {
186
- server.storage.fetchAddresses(server.walletId, (err, addresses) => {
187
+ server.storage.fetchAddressesPage(server.walletId, { limit: 1 }, (err, addresses) => {
187
188
  if (err)
188
189
  return cb(err);
189
190
  if (!addresses?.length)