@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
package/src/lib/server.ts CHANGED
@@ -873,26 +873,29 @@ export class WalletService implements IWalletService {
873
873
  pendingTxps?: ITxProposal[];
874
874
  preferences?: boolean;
875
875
  } = {};
876
- async.parallel(
877
- [
878
- next => {
879
- this.getWallet({}, (err, wallet: Partial<Wallet>) => {
880
- if (err) return next(err);
881
- if (this._upgradeNeeded(UPGRADES.SOL_bwc_$lt_10_10_12, wallet)) {
882
- return next(Errors.UPGRADE_NEEDED);
883
- }
876
+ this.getWallet({}, (err, fetchedWallet: Wallet) => {
877
+ if (err) return cb(err);
884
878
 
879
+ // Read the wallet once, and give each step its own copy, the same as reading it again: the steps change their
880
+ // wallet in place (this one strips copayer data, the balance code sets tokenAddress on it), and the balance step
881
+ // can store its copy whole.
882
+ const copyOfWallet = () => Wallet.fromObj(fetchedWallet.toObject());
883
+ const omit = (obj, keys: string[]): any => Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key)));
884
+ async.parallel(
885
+ [
886
+ next => {
887
+ let wallet: Partial<Wallet> = copyOfWallet();
885
888
  const walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager'];
886
889
  const copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
887
890
 
888
891
  wallet.copayers = (wallet.copayers || []).map(copayer => {
889
892
  if (copayer.id == this.copayerId) return copayer;
890
- return _.omit(copayer, 'customData');
893
+ return omit(copayer, ['customData']);
891
894
  });
892
895
  if (!opts.includeExtendedInfo) {
893
- wallet = _.omit(wallet, walletExtendedKeys);
896
+ wallet = omit(wallet, walletExtendedKeys);
894
897
  wallet.copayers = (wallet.copayers || []).map(copayer => {
895
- return _.omit(copayer, copayerExtendedKeys);
898
+ return omit(copayer, copayerExtendedKeys);
896
899
  }) as Copayer[];
897
900
  }
898
901
  status.wallet = wallet;
@@ -903,46 +906,45 @@ export class WalletService implements IWalletService {
903
906
  status.serverMessage = deprecatedServerMessage(wallet, this.appName, this.appVersion);
904
907
  }
905
908
  next();
906
- });
907
- },
908
- next => {
909
- opts.wallet = status.wallet;
910
- this.getBalance(opts, (err, balance) => {
911
- // ignore WALLET_NEED_SCAN err is includeExtendedInfo is given
912
- // (to allow `importWallet` to import a wallet, while scan has
913
- // failed)
914
- if (opts.includeExtendedInfo) {
915
- if (err && err.code != 'WALLET_NEED_SCAN') {
909
+ },
910
+ next => {
911
+ this.getBalance({ ...opts, wallet: copyOfWallet() }, (err, balance) => {
912
+ // ignore WALLET_NEED_SCAN err is includeExtendedInfo is given
913
+ // (to allow `importWallet` to import a wallet, while scan has
914
+ // failed)
915
+ if (opts.includeExtendedInfo) {
916
+ if (err && err.code != 'WALLET_NEED_SCAN') {
917
+ return next(err);
918
+ }
919
+ } else if (err) {
916
920
  return next(err);
917
921
  }
918
- } else if (err) {
919
- return next(err);
920
- }
921
922
 
922
- status.balance = balance;
923
- next();
924
- });
925
- },
926
- next => {
927
- this.getPendingTxs(opts, (err, pendingTxps) => {
928
- if (err) return next(err);
929
- status.pendingTxps = pendingTxps;
930
- next();
931
- });
932
- },
933
- next => {
934
- this.getPreferences({}, (err, preferences) => {
935
- if (err) return next(err);
936
- status.preferences = preferences;
937
- next();
938
- });
923
+ status.balance = balance;
924
+ next();
925
+ });
926
+ },
927
+ next => {
928
+ this.getPendingTxs({ ...opts, wallet: copyOfWallet() }, (err, pendingTxps) => {
929
+ if (err) return next(err);
930
+ status.pendingTxps = pendingTxps;
931
+ next();
932
+ });
933
+ },
934
+ next => {
935
+ this.getPreferences({}, (err, preferences) => {
936
+ if (err) return next(err);
937
+ status.preferences = preferences;
938
+ next();
939
+ });
940
+ }
941
+ ],
942
+ err => {
943
+ if (err) return cb(err);
944
+ return cb(null, status);
939
945
  }
940
- ],
941
- err => {
942
- if (err) return cb(err);
943
- return cb(null, status);
944
- }
945
- );
946
+ );
947
+ });
946
948
  }
947
949
 
948
950
  /**
@@ -1535,9 +1537,10 @@ export class WalletService implements IWalletService {
1535
1537
  _canCreateAddress(ignoreMaxGap, cb) {
1536
1538
  if (ignoreMaxGap) return cb(null, true);
1537
1539
 
1538
- this.storage.fetchAddresses(this.walletId, (err, addresses: IAddress[]) => {
1540
+ const newest = { noChange: true, reverse: true, limit: Defaults.MAX_MAIN_ADDRESS_GAP };
1541
+ this.storage.fetchAddressesPage(this.walletId, newest, (err, addresses: IAddress[]) => {
1539
1542
  if (err) return cb(err);
1540
- const latestAddresses = addresses.filter(x => !x.isChange).slice(-Defaults.MAX_MAIN_ADDRESS_GAP) as IAddress[];
1543
+ const latestAddresses = addresses.reverse();
1541
1544
  if (
1542
1545
  latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP ||
1543
1546
  latestAddresses.some(a => a.hasActivity)
@@ -1638,7 +1641,7 @@ export class WalletService implements IWalletService {
1638
1641
  };
1639
1642
 
1640
1643
  const getFirstAddress = (wallet, cb) => {
1641
- this.storage.fetchAddresses(this.walletId, (err, addresses) => {
1644
+ this.storage.fetchAddressesPage(this.walletId, { limit: 1 }, (err, addresses) => {
1642
1645
  if (err) return cb(err);
1643
1646
  if (addresses?.length) {
1644
1647
  const x = addresses[0];
@@ -1691,17 +1694,9 @@ export class WalletService implements IWalletService {
1691
1694
 
1692
1695
  getAddresses(opts: GetAddressesOpts, cb) {
1693
1696
  opts = opts || {};
1694
- this.storage.fetchAddresses(this.walletId, (err, addresses) => {
1697
+ const { noChange, addresses: only, reverse, skip, limit } = opts;
1698
+ this.storage.fetchAddressesPage(this.walletId, { noChange, addresses: only, reverse, skip, limit }, (err, addresses) => {
1695
1699
  if (err) return cb(err);
1696
- if (opts.noChange) {
1697
- addresses = addresses.filter(a => !a.isChange);
1698
- }
1699
- if (opts.addresses) {
1700
- addresses = addresses.filter(a => opts.addresses.includes(a.address));
1701
- }
1702
- if (opts.reverse) addresses.reverse();
1703
- if (opts.skip > 0) addresses = addresses.slice(opts.skip);
1704
- if (opts.limit > 0) addresses = addresses.slice(0, opts.limit);
1705
1700
 
1706
1701
  this.getWallet({}, (err, wallet) => {
1707
1702
  if (err) return cb(err);
@@ -1783,7 +1778,7 @@ export class WalletService implements IWalletService {
1783
1778
  async.series(
1784
1779
  [
1785
1780
  next => {
1786
- this.getWallet({}, (err, w) => {
1781
+ const useWallet = (err, w) => {
1787
1782
  if (err) return next(err);
1788
1783
 
1789
1784
  wallet = w;
@@ -1793,7 +1788,10 @@ export class WalletService implements IWalletService {
1793
1788
  bc = this._getBlockchainExplorer(wallet.chain, wallet.network);
1794
1789
  if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
1795
1790
  return next();
1796
- });
1791
+ };
1792
+ // the balance code passes the wallet it already has
1793
+ if (opts.wallet) return useWallet(null, opts.wallet);
1794
+ this.getWallet({}, useWallet);
1797
1795
  },
1798
1796
  next => {
1799
1797
  if (Array.isArray(opts.addresses)) {
@@ -1889,7 +1887,7 @@ export class WalletService implements IWalletService {
1889
1887
  next => {
1890
1888
  utxoIndex = _.keyBy(allUtxos, utxoKey);
1891
1889
 
1892
- this.getPendingTxs({ numberFormat: opts.numberFormat }, (err, txps) => {
1890
+ this.getPendingTxs({ numberFormat: opts.numberFormat, wallet }, (err, txps) => {
1893
1891
  if (err) return next(err);
1894
1892
 
1895
1893
  const lockedInputs = txps.flatMap(t => t.inputs).map(utxoKey);
@@ -1933,7 +1931,8 @@ export class WalletService implements IWalletService {
1933
1931
  });
1934
1932
  logger.debug(`Got ${allUtxos.length} usable UTXOs`);
1935
1933
  return next();
1936
- }
1934
+ },
1935
+ wallet
1937
1936
  );
1938
1937
  },
1939
1938
  next => {
@@ -1946,6 +1945,7 @@ export class WalletService implements IWalletService {
1946
1945
  }
1947
1946
  utxo.path = addressToPath[utxo.address].path;
1948
1947
  utxo.publicKeys = addressToPath[utxo.address].publicKeys;
1948
+ utxo.isChange = addressToPath[utxo.address].isChange;
1949
1949
  }
1950
1950
  return next();
1951
1951
  }
@@ -2399,6 +2399,37 @@ export class WalletService implements IWalletService {
2399
2399
  }
2400
2400
 
2401
2401
  _validateAndSanitizeTxOpts(wallet, opts, cb) {
2402
+ const gasFeeFields = ['gasPrice', 'maxGasFee', 'priorityGasFee'];
2403
+ const hasGasFeeOverride = gasFeeFields.some(key => opts[key] != null);
2404
+ if (hasGasFeeOverride) {
2405
+ if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) {
2406
+ return cb(new ClientError('Gas fee overrides are only supported for EVM chains'));
2407
+ }
2408
+ for (const key of gasFeeFields) {
2409
+ if (opts[key] != null && (!Number.isSafeInteger(opts[key]) || opts[key] <= 0)) {
2410
+ return cb(new ClientError(`${key} must be a positive safe integer in wei`));
2411
+ }
2412
+ }
2413
+ if (opts.gasPrice != null && Number(opts.txType || 0) !== 0) {
2414
+ return cb(new ClientError('gasPrice requires a type 0 transaction'));
2415
+ }
2416
+ if ((opts.maxGasFee != null || opts.priorityGasFee != null) && Number(opts.txType) !== 2) {
2417
+ return cb(new ClientError('maxGasFee and priorityGasFee require a type 2 transaction'));
2418
+ }
2419
+ if ((opts.maxGasFee != null) !== (opts.priorityGasFee != null)) {
2420
+ return cb(new ClientError('maxGasFee and priorityGasFee must be supplied together'));
2421
+ }
2422
+ if (opts.priorityGasFee > opts.maxGasFee) {
2423
+ return cb(new ClientError('priorityGasFee must not exceed maxGasFee'));
2424
+ }
2425
+ if (opts.feePerKb != null || opts.fee != null) {
2426
+ return cb(new ClientError('Gas fee overrides cannot be combined with feePerKb or fee'));
2427
+ }
2428
+ // sendMax calculates its amount before getFee, using a separate fee estimate.
2429
+ if (opts.sendMax) {
2430
+ return cb(new ClientError('Gas fee overrides are not supported with sendMax'));
2431
+ }
2432
+ }
2402
2433
  async.series(
2403
2434
  [
2404
2435
  next => {
@@ -2406,7 +2437,7 @@ export class WalletService implements IWalletService {
2406
2437
  boolToNum(!!opts.feeLevel) + boolToNum(Utils.isNumber(opts.feePerKb)) + boolToNum(Utils.isNumber(opts.fee));
2407
2438
  if (feeArgs > 1) return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
2408
2439
 
2409
- if (feeArgs == 0) {
2440
+ if (feeArgs == 0 && !hasGasFeeOverride) {
2410
2441
  opts.feeLevel = 'normal';
2411
2442
  }
2412
2443
 
@@ -2418,6 +2449,11 @@ export class WalletService implements IWalletService {
2418
2449
  );
2419
2450
  }
2420
2451
 
2452
+ // An explicit gas fee determines the price, not the named fee level.
2453
+ if (hasGasFeeOverride) {
2454
+ delete opts.feeLevel;
2455
+ }
2456
+
2421
2457
  const error = ChainService.checkUtxos(wallet.chain, opts);
2422
2458
  if (error) {
2423
2459
  return next(new ClientError('fee can only be set when inputs are specified'));
@@ -2779,6 +2815,9 @@ export class WalletService implements IWalletService {
2779
2815
  * @param {Boolean} opts.enableRBF - Optional. enable BTC Replace By Fee
2780
2816
  * @param {Boolean} opts.replaceTxByFee - Optional. Ignore locked utxos check ( used for replacing a transaction designated as RBF)
2781
2817
  * @param {number} opts.txType - Optional. Type of EVM transaction
2818
+ * @param {number} opts.gasPrice - Optional. Type 0 gas price in wei. Overrides feeLevel; incompatible with fee/feePerKb/sendMax.
2819
+ * @param {number} opts.maxGasFee - Optional. Type 2 maximum fee per gas in wei; requires priorityGasFee.
2820
+ * @param {number} opts.priorityGasFee - Optional. Positive type 2 priority fee per gas in wei; requires maxGasFee and must not exceed it. Gas fee overrides cannot be used with fee/feePerKb/sendMax.
2782
2821
  * @param {number} opts.gasLimitBuffer - Optional. Percentage of buffer to add to the gasLimit
2783
2822
  * @param {number} opts.priorityFeePercentile - Optional. Percentile of targeted priority fee rate
2784
2823
  * @param {Boolean} opts.multiTx - Optional. Proposal will create multiple transactions
@@ -3169,19 +3208,23 @@ export class WalletService implements IWalletService {
3169
3208
  * @returns {Object} txProposal
3170
3209
  */
3171
3210
  getTx(opts, cb: (err: any, txp?: TxProposal) => void) {
3172
- this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
3211
+ // the tx and its note are both completed from the wallet, so read it once
3212
+ this.storage.fetchWallet(this.walletId, (err, wallet) => {
3173
3213
  if (err) return cb(err);
3174
- if (!txp) return cb(Errors.TX_NOT_FOUND);
3214
+ this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
3215
+ if (err) return cb(err);
3216
+ if (!txp) return cb(Errors.TX_NOT_FOUND);
3175
3217
 
3176
- if (!txp.txid) return cb(null, txp);
3218
+ if (!txp.txid) return cb(null, txp);
3177
3219
 
3178
- this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3179
- if (err) {
3180
- this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3181
- }
3182
- txp.note = note;
3183
- return cb(null, txp);
3184
- });
3220
+ this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3221
+ if (err) {
3222
+ this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3223
+ }
3224
+ txp.note = note;
3225
+ return cb(null, txp);
3226
+ }, wallet);
3227
+ }, wallet);
3185
3228
  });
3186
3229
  }
3187
3230
 
@@ -3199,19 +3242,22 @@ export class WalletService implements IWalletService {
3199
3242
  // foreign wallet's proposal data to any authenticated copayer. The
3200
3243
  // internal global consumers (BlockchainMonitor, getWalletFromIdentifier)
3201
3244
  // are unaffected; they keep calling storage.fetchTxByHash directly.
3202
- this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3245
+ this.storage.fetchWallet(this.walletId, (err, wallet) => {
3203
3246
  if (err) return cb(err);
3204
- if (!txp) return cb(Errors.TX_NOT_FOUND);
3247
+ this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3248
+ if (err) return cb(err);
3249
+ if (!txp) return cb(Errors.TX_NOT_FOUND);
3205
3250
 
3206
- if (!txp.txid) return cb(null, txp);
3251
+ if (!txp.txid) return cb(null, txp);
3207
3252
 
3208
- this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3209
- if (err) {
3210
- this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3211
- }
3212
- txp.note = note;
3213
- return cb(null, txp);
3214
- });
3253
+ this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3254
+ if (err) {
3255
+ this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3256
+ }
3257
+ txp.note = note;
3258
+ return cb(null, txp);
3259
+ }, wallet);
3260
+ }, wallet);
3215
3261
  });
3216
3262
  }
3217
3263
 
@@ -3841,7 +3887,7 @@ export class WalletService implements IWalletService {
3841
3887
  return cb(err, txps);
3842
3888
  }
3843
3889
  );
3844
- });
3890
+ }, opts.wallet);
3845
3891
  }
3846
3892
  }
3847
3893
 
@@ -4867,7 +4913,8 @@ export class WalletService implements IWalletService {
4867
4913
  minTs,
4868
4914
  maxTs
4869
4915
  },
4870
- done
4916
+ done,
4917
+ wallet
4871
4918
  );
4872
4919
  },
4873
4920
  done => {
@@ -4876,7 +4923,8 @@ export class WalletService implements IWalletService {
4876
4923
  {
4877
4924
  minTs
4878
4925
  },
4879
- done
4926
+ done,
4927
+ wallet
4880
4928
  );
4881
4929
  }
4882
4930
  ],
@@ -304,8 +304,9 @@ export class Storage {
304
304
  }
305
305
 
306
306
  // TODO: should be done client-side
307
- _completeTxData(walletId: string, txs: TxProposal | TxProposal[], cb: (err?: any, txs?: TxProposal | TxProposal[]) => void) {
308
- this.fetchWallet(walletId, (err, wallet) => {
307
+ // Callers that already have the wallet pass it, to save reading it again
308
+ _completeTxData(walletId: string, txs: TxProposal | TxProposal[], cb: (err?: any, txs?: TxProposal | TxProposal[]) => void, wallet?: Wallet) {
309
+ const complete = (err, wallet: Wallet) => {
309
310
  if (err) return cb(err);
310
311
  for (const tx of [].concat(txs) as TxProposal[]) {
311
312
  tx.derivationStrategy = wallet.derivationStrategy || 'BIP45';
@@ -318,11 +319,13 @@ export class Storage {
318
319
  tx.raw = tx.getRawTx();
319
320
  }
320
321
  return cb(null, txs);
321
- });
322
+ };
323
+ if (wallet) return complete(null, wallet);
324
+ this.fetchWallet(walletId, complete);
322
325
  }
323
326
 
324
327
  // TODO: remove walletId from signature
325
- fetchTx(walletId: string, txProposalId: string, cb: (err?: any, tx?: TxProposal) => void) {
328
+ fetchTx(walletId: string, txProposalId: string, cb: (err?: any, tx?: TxProposal) => void, wallet?: Wallet) {
326
329
  if (!this.db) return cb();
327
330
 
328
331
  this.db.collection(collections.TXS).findOne(
@@ -333,7 +336,7 @@ export class Storage {
333
336
  (err, result) => {
334
337
  if (err) return cb(err);
335
338
  if (!result) return cb();
336
- return this._completeTxData(walletId, TxProposal.fromObj(result), cb);
339
+ return this._completeTxData(walletId, TxProposal.fromObj(result), cb, wallet);
337
340
  }
338
341
  );
339
342
  }
@@ -356,7 +359,7 @@ export class Storage {
356
359
 
357
360
  // Wallet-scoped counterpart to fetchTxByHash, used by the authenticated
358
361
  // by-hash API read.
359
- fetchTxByHashForWallet(walletId: string, hash, cb: (err?: any, tx?: TxProposal) => void) {
362
+ fetchTxByHashForWallet(walletId: string, hash, cb: (err?: any, tx?: TxProposal) => void, wallet?: Wallet) {
360
363
  if (!this.db) return cb();
361
364
 
362
365
  this.db.collection(collections.TXS).findOne(
@@ -368,7 +371,7 @@ export class Storage {
368
371
  if (err) return cb(err);
369
372
  if (!result) return cb();
370
373
 
371
- return this._completeTxData(walletId, TxProposal.fromObj(result), cb);
374
+ return this._completeTxData(walletId, TxProposal.fromObj(result), cb, wallet);
372
375
  }
373
376
  );
374
377
  }
@@ -444,7 +447,7 @@ export class Storage {
444
447
  });
445
448
  }
446
449
 
447
- fetchPendingTxs(walletId, cb) {
450
+ fetchPendingTxs(walletId, cb, wallet?: Wallet) {
448
451
  this.db
449
452
  .collection(collections.TXS)
450
453
  .find({
@@ -460,7 +463,7 @@ export class Storage {
460
463
  const txs = _.map(result, tx => {
461
464
  return TxProposal.fromObj(tx);
462
465
  });
463
- return this._completeTxData(walletId, txs, cb);
466
+ return this._completeTxData(walletId, txs, cb, wallet);
464
467
  });
465
468
  }
466
469
 
@@ -472,7 +475,7 @@ export class Storage {
472
475
  * @param opts.maxTs
473
476
  * @param opts.limit
474
477
  */
475
- fetchTxs(walletId, opts, cb) {
478
+ fetchTxs(walletId, opts, cb, wallet?: Wallet) {
476
479
  opts = opts || {};
477
480
 
478
481
  const tsFilter: { $gte?: number; $lte?: number } = {};
@@ -499,7 +502,7 @@ export class Storage {
499
502
  const txs = _.map(result, tx => {
500
503
  return TxProposal.fromObj(tx);
501
504
  });
502
- return this._completeTxData(walletId, txs, cb);
505
+ return this._completeTxData(walletId, txs, cb, wallet);
503
506
  });
504
507
  }
505
508
 
@@ -511,7 +514,7 @@ export class Storage {
511
514
  * @param opts.maxTs
512
515
  * @param opts.limit
513
516
  */
514
- fetchBroadcastedTxs(walletId, opts, cb) {
517
+ fetchBroadcastedTxs(walletId, opts, cb, wallet?: Wallet) {
515
518
  opts = opts || {};
516
519
 
517
520
  const tsFilter: { $gte?: number; $lte?: number } = {};
@@ -543,7 +546,7 @@ export class Storage {
543
546
  const txs = _.map(result, tx => {
544
547
  return TxProposal.fromObj(tx);
545
548
  });
546
- return this._completeTxData(walletId, txs, cb);
549
+ return this._completeTxData(walletId, txs, cb, wallet);
547
550
  });
548
551
  }
549
552
 
@@ -678,6 +681,70 @@ export class Storage {
678
681
  });
679
682
  }
680
683
 
684
+ /**
685
+ * A page of the wallet's addresses, in fetchAddresses order (createdOn, then path), reading only the addresses the
686
+ * page needs. noChange and addresses filter the list, then reverse, skip and limit apply in that order.
687
+ *
688
+ * @param walletId
689
+ * @param opts.noChange - leave out change addresses
690
+ * @param opts.addresses - only these addresses
691
+ * @param opts.reverse - newest first
692
+ * @param opts.skip
693
+ * @param opts.limit
694
+ */
695
+ fetchAddressesPage(
696
+ walletId: string,
697
+ opts: { noChange?: boolean; addresses?: string[]; reverse?: boolean; skip?: number; limit?: number },
698
+ cb: (err?: any, addresses?: Address[]) => void
699
+ ) {
700
+ const query: { walletId: string; isChange?: { $in: Array<false | null> }; address?: { $in: string[] } } = { walletId };
701
+ // false, null or missing, the same addresses as !isChange
702
+ if (opts.noChange) query.isChange = { $in: [false, null] };
703
+ if (opts.addresses) query.address = { $in: opts.addresses };
704
+ // skip and limit apply only when positive, and as whole numbers, as slice does
705
+ const skip = opts.skip > 0 ? Math.floor(opts.skip) : 0;
706
+ const limit = opts.limit > 0 ? Math.floor(opts.limit) : null;
707
+ const collection = this.db.collection(collections.ADDRESSES);
708
+
709
+ const page = docs => {
710
+ let addresses = Utils.sortAsc(docs.map(Address.fromObj), 'createdOn', 'path');
711
+ if (opts.reverse) addresses.reverse();
712
+ if (skip) addresses = addresses.slice(skip);
713
+ if (limit !== null) addresses = addresses.slice(0, limit);
714
+ return addresses;
715
+ };
716
+ const readAll = () => collection.find(query).sort({ createdOn: 1 }).toArray((err, docs) => {
717
+ if (err) return cb(err);
718
+ return cb(null, page(docs));
719
+ });
720
+
721
+ // filtering by address, or reading without a limit or with one too large for a query limit, reads every matching
722
+ // address
723
+ if (opts.addresses || !(limit > 0) || !Number.isSafeInteger(skip + limit + 1)) return readAll();
724
+
725
+ // fetchAddresses sorts an address with a missing createdOn last and one with a null createdOn first, but Mongo sorts
726
+ // both first, so a wallet that has either is read whole
727
+ collection.findOne({ walletId, createdOn: null }, { projection: { _id: 1 } }, (err, unordered) => {
728
+ if (err) return cb(err);
729
+ if (unordered) return readAll();
730
+
731
+ // The page is within the first skip + limit addresses by createdOn plus any others sharing the last one's
732
+ // createdOn, whatever order addresses created in the same second are in. One more address shows whether there are any.
733
+ const count = skip + limit;
734
+ collection.find(query).sort({ createdOn: opts.reverse ? -1 : 1 }).limit(count + 1).toArray((err, docs) => {
735
+ if (err) return cb(err);
736
+ if (docs.length <= count || docs[count].createdOn !== docs[count - 1].createdOn) {
737
+ return cb(null, page(docs.slice(0, count)));
738
+ }
739
+ collection.find({ ...query, createdOn: docs[count - 1].createdOn }).toArray((err, ties) => {
740
+ if (err) return cb(err);
741
+ const read = new Set(docs.slice(0, count).map(doc => String(doc._id)));
742
+ return cb(null, page([...docs.slice(0, count), ...ties.filter(doc => !read.has(String(doc._id)))]));
743
+ });
744
+ });
745
+ });
746
+ }
747
+
681
748
  migrateToCashAddr(walletId, cb) {
682
749
  const cursor = this.db.collection(collections.ADDRESSES).find({
683
750
  walletId
@@ -1300,7 +1367,7 @@ export class Storage {
1300
1367
  });
1301
1368
  }
1302
1369
 
1303
- fetchTxNote(walletId, txid, cb: (err?: any, note?: TxNote) => void) {
1370
+ fetchTxNote(walletId, txid, cb: (err?: any, note?: TxNote) => void, wallet?: Wallet) {
1304
1371
  this.db.collection(collections.TX_NOTES).findOne(
1305
1372
  {
1306
1373
  walletId,
@@ -1309,20 +1376,23 @@ export class Storage {
1309
1376
  (err, result) => {
1310
1377
  if (err) return cb(err);
1311
1378
  if (!result) return cb();
1312
- return this._completeTxNotesData(walletId, TxNote.fromObj(result), cb);
1379
+ return this._completeTxNotesData(walletId, TxNote.fromObj(result), cb, wallet);
1313
1380
  }
1314
1381
  );
1315
1382
  }
1316
1383
 
1317
1384
  // TODO: should be done client-side
1318
- _completeTxNotesData(walletId, notes: TxNote | TxNote[], cb: (err?: any, notes?: TxNote | TxNote[]) => void) {
1319
- this.fetchWallet(walletId, (err, wallet) => {
1385
+ // Callers that already have the wallet pass it, to save reading it again
1386
+ _completeTxNotesData(walletId, notes: TxNote | TxNote[], cb: (err?: any, notes?: TxNote | TxNote[]) => void, wallet?: Wallet) {
1387
+ const complete = (err, wallet: Wallet) => {
1320
1388
  if (err) return cb(err);
1321
1389
  for (const note of [].concat(notes) as TxNote[]) {
1322
1390
  note.editedByName = wallet.getCopayer(note.editedBy).name;
1323
1391
  }
1324
1392
  return cb(null, notes);
1325
- });
1393
+ };
1394
+ if (wallet) return complete(null, wallet);
1395
+ this.fetchWallet(walletId, complete);
1326
1396
  }
1327
1397
 
1328
1398
  /**
@@ -1331,7 +1401,7 @@ export class Storage {
1331
1401
  * @param walletId
1332
1402
  * @param opts.minTs
1333
1403
  */
1334
- fetchTxNotes(walletId, opts, cb) {
1404
+ fetchTxNotes(walletId, opts, cb, wallet?: Wallet) {
1335
1405
  const filter: { walletId: string; editedOn?: { $gte: number } } = {
1336
1406
  walletId
1337
1407
  };
@@ -1349,7 +1419,7 @@ export class Storage {
1349
1419
  return TxNote.fromObj(note);
1350
1420
  })
1351
1421
  );
1352
- return this._completeTxNotesData(walletId, notes, cb);
1422
+ return this._completeTxNotesData(walletId, notes, cb, wallet);
1353
1423
  });
1354
1424
  }
1355
1425