@bitpay-labs/bitcore-wallet-service 11.11.0 → 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 (157) hide show
  1. package/Dockerfile +1 -1
  2. package/package.json +5 -5
  3. package/src/config.ts +0 -20
  4. package/src/externalservices/banxa.ts +51 -10
  5. package/src/externalservices/changelly.ts +54 -6
  6. package/src/externalservices/moonpay.ts +85 -19
  7. package/src/externalservices/oneInch.ts +13 -5
  8. package/src/externalservices/ramp.ts +42 -8
  9. package/src/externalservices/sardine.ts +51 -10
  10. package/src/externalservices/simplex.ts +58 -11
  11. package/src/externalservices/thorswap.ts +46 -11
  12. package/src/externalservices/transak.ts +53 -11
  13. package/src/lib/chain/btc/index.ts +3 -2
  14. package/src/lib/chain/eth/index.ts +134 -117
  15. package/src/lib/chain/sol/index.ts +1 -1
  16. package/src/lib/chain/xrp/index.ts +1 -1
  17. package/src/lib/routes/services.ts +0 -12
  18. package/src/lib/routes/wallets.ts +3 -1
  19. package/src/lib/server.ts +136 -91
  20. package/src/lib/storage.ts +90 -20
  21. package/src/types/externalservices.d.ts +0 -1
  22. package/test/chain/eth.test.ts +70 -0
  23. package/test/chain/sol.test.ts +1 -1
  24. package/test/chain/xrp.test.ts +1 -1
  25. package/test/expressapp.test.ts +144 -0
  26. package/test/integration/addressReads.test.ts +503 -0
  27. package/test/integration/bcmonitor.test.ts +44 -0
  28. package/test/integration/externalservices/banxa.test.ts +95 -5
  29. package/test/integration/externalservices/changelly.test.ts +118 -6
  30. package/test/integration/externalservices/coinGecko.test.ts +4 -0
  31. package/test/integration/externalservices/moonpay.test.ts +178 -9
  32. package/test/integration/externalservices/oneInch.test.ts +28 -1
  33. package/test/integration/externalservices/ramp.test.ts +126 -4
  34. package/test/integration/externalservices/sardine.test.ts +95 -5
  35. package/test/integration/externalservices/simplex.test.ts +95 -5
  36. package/test/integration/externalservices/thorswap.test.ts +81 -5
  37. package/test/integration/externalservices/transak.test.ts +98 -6
  38. package/test/integration/helpers.ts +1 -1
  39. package/test/integration/server.test.ts +254 -136
  40. package/test/integration/walletReads.test.ts +488 -0
  41. package/ts_build/package.json +3 -3
  42. package/ts_build/src/config.d.ts.map +1 -1
  43. package/ts_build/src/config.js +0 -4
  44. package/ts_build/src/config.js.map +1 -1
  45. package/ts_build/src/externalservices/banxa.d.ts.map +1 -1
  46. package/ts_build/src/externalservices/banxa.js +51 -10
  47. package/ts_build/src/externalservices/banxa.js.map +1 -1
  48. package/ts_build/src/externalservices/changelly.d.ts.map +1 -1
  49. package/ts_build/src/externalservices/changelly.js +54 -6
  50. package/ts_build/src/externalservices/changelly.js.map +1 -1
  51. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  52. package/ts_build/src/externalservices/moonpay.js +85 -19
  53. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  54. package/ts_build/src/externalservices/oneInch.d.ts.map +1 -1
  55. package/ts_build/src/externalservices/oneInch.js +13 -5
  56. package/ts_build/src/externalservices/oneInch.js.map +1 -1
  57. package/ts_build/src/externalservices/ramp.d.ts.map +1 -1
  58. package/ts_build/src/externalservices/ramp.js +42 -8
  59. package/ts_build/src/externalservices/ramp.js.map +1 -1
  60. package/ts_build/src/externalservices/sardine.d.ts.map +1 -1
  61. package/ts_build/src/externalservices/sardine.js +51 -10
  62. package/ts_build/src/externalservices/sardine.js.map +1 -1
  63. package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
  64. package/ts_build/src/externalservices/simplex.js +55 -11
  65. package/ts_build/src/externalservices/simplex.js.map +1 -1
  66. package/ts_build/src/externalservices/thorswap.d.ts.map +1 -1
  67. package/ts_build/src/externalservices/thorswap.js +45 -11
  68. package/ts_build/src/externalservices/thorswap.js.map +1 -1
  69. package/ts_build/src/externalservices/transak.d.ts.map +1 -1
  70. package/ts_build/src/externalservices/transak.js +53 -11
  71. package/ts_build/src/externalservices/transak.js.map +1 -1
  72. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  73. package/ts_build/src/lib/chain/btc/index.js +3 -2
  74. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  75. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  76. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  77. package/ts_build/src/lib/chain/eth/index.js +114 -102
  78. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  79. package/ts_build/src/lib/chain/sol/index.js +1 -1
  80. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  81. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  82. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  83. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  84. package/ts_build/src/lib/routes/services.js +0 -10
  85. package/ts_build/src/lib/routes/services.js.map +1 -1
  86. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  87. package/ts_build/src/lib/routes/wallets.js +2 -1
  88. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  89. package/ts_build/src/lib/server.d.ts +1 -3
  90. package/ts_build/src/lib/server.d.ts.map +1 -1
  91. package/ts_build/src/lib/server.js +128 -96
  92. package/ts_build/src/lib/server.js.map +1 -1
  93. package/ts_build/src/lib/storage.d.ts +17 -10
  94. package/ts_build/src/lib/storage.d.ts.map +1 -1
  95. package/ts_build/src/lib/storage.js +73 -20
  96. package/ts_build/src/lib/storage.js.map +1 -1
  97. package/ts_build/test/chain/eth.test.js +59 -0
  98. package/ts_build/test/chain/eth.test.js.map +1 -1
  99. package/ts_build/test/chain/sol.test.js +1 -1
  100. package/ts_build/test/chain/sol.test.js.map +1 -1
  101. package/ts_build/test/chain/xrp.test.js +1 -1
  102. package/ts_build/test/chain/xrp.test.js.map +1 -1
  103. package/ts_build/test/expressapp.test.js +132 -0
  104. package/ts_build/test/expressapp.test.js.map +1 -1
  105. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  106. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  107. package/ts_build/test/integration/addressReads.test.js +465 -0
  108. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  109. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  110. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  111. package/ts_build/test/integration/externalservices/banxa.test.d.ts.map +1 -1
  112. package/ts_build/test/integration/externalservices/banxa.test.js +93 -5
  113. package/ts_build/test/integration/externalservices/banxa.test.js.map +1 -1
  114. package/ts_build/test/integration/externalservices/changelly.test.d.ts.map +1 -1
  115. package/ts_build/test/integration/externalservices/changelly.test.js +111 -6
  116. package/ts_build/test/integration/externalservices/changelly.test.js.map +1 -1
  117. package/ts_build/test/integration/externalservices/coinGecko.test.js +4 -0
  118. package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
  119. package/ts_build/test/integration/externalservices/moonpay.test.d.ts.map +1 -1
  120. package/ts_build/test/integration/externalservices/moonpay.test.js +169 -9
  121. package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
  122. package/ts_build/test/integration/externalservices/oneInch.test.d.ts.map +1 -1
  123. package/ts_build/test/integration/externalservices/oneInch.test.js +26 -1
  124. package/ts_build/test/integration/externalservices/oneInch.test.js.map +1 -1
  125. package/ts_build/test/integration/externalservices/ramp.test.d.ts.map +1 -1
  126. package/ts_build/test/integration/externalservices/ramp.test.js +115 -4
  127. package/ts_build/test/integration/externalservices/ramp.test.js.map +1 -1
  128. package/ts_build/test/integration/externalservices/sardine.test.d.ts.map +1 -1
  129. package/ts_build/test/integration/externalservices/sardine.test.js +94 -5
  130. package/ts_build/test/integration/externalservices/sardine.test.js.map +1 -1
  131. package/ts_build/test/integration/externalservices/simplex.test.d.ts.map +1 -1
  132. package/ts_build/test/integration/externalservices/simplex.test.js +89 -5
  133. package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
  134. package/ts_build/test/integration/externalservices/thorswap.test.d.ts.map +1 -1
  135. package/ts_build/test/integration/externalservices/thorswap.test.js +76 -5
  136. package/ts_build/test/integration/externalservices/thorswap.test.js.map +1 -1
  137. package/ts_build/test/integration/externalservices/transak.test.d.ts.map +1 -1
  138. package/ts_build/test/integration/externalservices/transak.test.js +93 -6
  139. package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
  140. package/ts_build/test/integration/helpers.js +1 -1
  141. package/ts_build/test/integration/helpers.js.map +1 -1
  142. package/ts_build/test/integration/server.test.js +225 -124
  143. package/ts_build/test/integration/server.test.js.map +1 -1
  144. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  145. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  146. package/ts_build/test/integration/walletReads.test.js +436 -0
  147. package/ts_build/test/integration/walletReads.test.js.map +1 -0
  148. package/src/externalservices/wyre.ts +0 -131
  149. package/test/integration/externalservices/wyre.test.ts +0 -270
  150. package/ts_build/src/externalservices/wyre.d.ts +0 -7
  151. package/ts_build/src/externalservices/wyre.d.ts.map +0 -1
  152. package/ts_build/src/externalservices/wyre.js +0 -144
  153. package/ts_build/src/externalservices/wyre.js.map +0 -1
  154. package/ts_build/test/integration/externalservices/wyre.test.d.ts +0 -2
  155. package/ts_build/test/integration/externalservices/wyre.test.d.ts.map +0 -1
  156. package/ts_build/test/integration/externalservices/wyre.test.js +0 -281
  157. package/ts_build/test/integration/externalservices/wyre.test.js.map +0 -1
package/src/lib/server.ts CHANGED
@@ -27,7 +27,6 @@ import { SardineService } from '../externalservices/sardine';
27
27
  import { SimplexService } from '../externalservices/simplex';
28
28
  import { ThorswapService } from '../externalservices/thorswap';
29
29
  import { TransakService } from '../externalservices/transak';
30
- import { WyreService } from '../externalservices/wyre';
31
30
  import { serverMessages } from '../serverMessages';
32
31
  import { BCHAddressTranslator } from './bchaddresstranslator';
33
32
  import { BlockChainExplorer } from './blockchainexplorer';
@@ -160,7 +159,6 @@ export class WalletService implements IWalletService {
160
159
  simplex: SimplexService;
161
160
  thorswap: ThorswapService;
162
161
  transak: TransakService;
163
- wyre: WyreService;
164
162
  coinGecko: CoinGeckoService;
165
163
  };
166
164
 
@@ -190,7 +188,6 @@ export class WalletService implements IWalletService {
190
188
  simplex: new SimplexService(),
191
189
  thorswap: new ThorswapService(),
192
190
  transak: new TransakService(),
193
- wyre: new WyreService(),
194
191
  coinGecko: new CoinGeckoService(this.storage),
195
192
  };
196
193
  }
@@ -876,26 +873,29 @@ export class WalletService implements IWalletService {
876
873
  pendingTxps?: ITxProposal[];
877
874
  preferences?: boolean;
878
875
  } = {};
879
- async.parallel(
880
- [
881
- next => {
882
- this.getWallet({}, (err, wallet: Partial<Wallet>) => {
883
- if (err) return next(err);
884
- if (this._upgradeNeeded(UPGRADES.SOL_bwc_$lt_10_10_12, wallet)) {
885
- return next(Errors.UPGRADE_NEEDED);
886
- }
876
+ this.getWallet({}, (err, fetchedWallet: Wallet) => {
877
+ if (err) return cb(err);
887
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();
888
888
  const walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager'];
889
889
  const copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
890
890
 
891
891
  wallet.copayers = (wallet.copayers || []).map(copayer => {
892
892
  if (copayer.id == this.copayerId) return copayer;
893
- return _.omit(copayer, 'customData');
893
+ return omit(copayer, ['customData']);
894
894
  });
895
895
  if (!opts.includeExtendedInfo) {
896
- wallet = _.omit(wallet, walletExtendedKeys);
896
+ wallet = omit(wallet, walletExtendedKeys);
897
897
  wallet.copayers = (wallet.copayers || []).map(copayer => {
898
- return _.omit(copayer, copayerExtendedKeys);
898
+ return omit(copayer, copayerExtendedKeys);
899
899
  }) as Copayer[];
900
900
  }
901
901
  status.wallet = wallet;
@@ -906,46 +906,45 @@ export class WalletService implements IWalletService {
906
906
  status.serverMessage = deprecatedServerMessage(wallet, this.appName, this.appVersion);
907
907
  }
908
908
  next();
909
- });
910
- },
911
- next => {
912
- opts.wallet = status.wallet;
913
- this.getBalance(opts, (err, balance) => {
914
- // ignore WALLET_NEED_SCAN err is includeExtendedInfo is given
915
- // (to allow `importWallet` to import a wallet, while scan has
916
- // failed)
917
- if (opts.includeExtendedInfo) {
918
- 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) {
919
920
  return next(err);
920
921
  }
921
- } else if (err) {
922
- return next(err);
923
- }
924
922
 
925
- status.balance = balance;
926
- next();
927
- });
928
- },
929
- next => {
930
- this.getPendingTxs(opts, (err, pendingTxps) => {
931
- if (err) return next(err);
932
- status.pendingTxps = pendingTxps;
933
- next();
934
- });
935
- },
936
- next => {
937
- this.getPreferences({}, (err, preferences) => {
938
- if (err) return next(err);
939
- status.preferences = preferences;
940
- next();
941
- });
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);
942
945
  }
943
- ],
944
- err => {
945
- if (err) return cb(err);
946
- return cb(null, status);
947
- }
948
- );
946
+ );
947
+ });
949
948
  }
950
949
 
951
950
  /**
@@ -1538,9 +1537,10 @@ export class WalletService implements IWalletService {
1538
1537
  _canCreateAddress(ignoreMaxGap, cb) {
1539
1538
  if (ignoreMaxGap) return cb(null, true);
1540
1539
 
1541
- 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[]) => {
1542
1542
  if (err) return cb(err);
1543
- const latestAddresses = addresses.filter(x => !x.isChange).slice(-Defaults.MAX_MAIN_ADDRESS_GAP) as IAddress[];
1543
+ const latestAddresses = addresses.reverse();
1544
1544
  if (
1545
1545
  latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP ||
1546
1546
  latestAddresses.some(a => a.hasActivity)
@@ -1641,7 +1641,7 @@ export class WalletService implements IWalletService {
1641
1641
  };
1642
1642
 
1643
1643
  const getFirstAddress = (wallet, cb) => {
1644
- this.storage.fetchAddresses(this.walletId, (err, addresses) => {
1644
+ this.storage.fetchAddressesPage(this.walletId, { limit: 1 }, (err, addresses) => {
1645
1645
  if (err) return cb(err);
1646
1646
  if (addresses?.length) {
1647
1647
  const x = addresses[0];
@@ -1694,17 +1694,9 @@ export class WalletService implements IWalletService {
1694
1694
 
1695
1695
  getAddresses(opts: GetAddressesOpts, cb) {
1696
1696
  opts = opts || {};
1697
- 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) => {
1698
1699
  if (err) return cb(err);
1699
- if (opts.noChange) {
1700
- addresses = addresses.filter(a => !a.isChange);
1701
- }
1702
- if (opts.addresses) {
1703
- addresses = addresses.filter(a => opts.addresses.includes(a.address));
1704
- }
1705
- if (opts.reverse) addresses.reverse();
1706
- if (opts.skip > 0) addresses = addresses.slice(opts.skip);
1707
- if (opts.limit > 0) addresses = addresses.slice(0, opts.limit);
1708
1700
 
1709
1701
  this.getWallet({}, (err, wallet) => {
1710
1702
  if (err) return cb(err);
@@ -1786,7 +1778,7 @@ export class WalletService implements IWalletService {
1786
1778
  async.series(
1787
1779
  [
1788
1780
  next => {
1789
- this.getWallet({}, (err, w) => {
1781
+ const useWallet = (err, w) => {
1790
1782
  if (err) return next(err);
1791
1783
 
1792
1784
  wallet = w;
@@ -1796,7 +1788,10 @@ export class WalletService implements IWalletService {
1796
1788
  bc = this._getBlockchainExplorer(wallet.chain, wallet.network);
1797
1789
  if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
1798
1790
  return next();
1799
- });
1791
+ };
1792
+ // the balance code passes the wallet it already has
1793
+ if (opts.wallet) return useWallet(null, opts.wallet);
1794
+ this.getWallet({}, useWallet);
1800
1795
  },
1801
1796
  next => {
1802
1797
  if (Array.isArray(opts.addresses)) {
@@ -1892,7 +1887,7 @@ export class WalletService implements IWalletService {
1892
1887
  next => {
1893
1888
  utxoIndex = _.keyBy(allUtxos, utxoKey);
1894
1889
 
1895
- this.getPendingTxs({ numberFormat: opts.numberFormat }, (err, txps) => {
1890
+ this.getPendingTxs({ numberFormat: opts.numberFormat, wallet }, (err, txps) => {
1896
1891
  if (err) return next(err);
1897
1892
 
1898
1893
  const lockedInputs = txps.flatMap(t => t.inputs).map(utxoKey);
@@ -1936,7 +1931,8 @@ export class WalletService implements IWalletService {
1936
1931
  });
1937
1932
  logger.debug(`Got ${allUtxos.length} usable UTXOs`);
1938
1933
  return next();
1939
- }
1934
+ },
1935
+ wallet
1940
1936
  );
1941
1937
  },
1942
1938
  next => {
@@ -1949,6 +1945,7 @@ export class WalletService implements IWalletService {
1949
1945
  }
1950
1946
  utxo.path = addressToPath[utxo.address].path;
1951
1947
  utxo.publicKeys = addressToPath[utxo.address].publicKeys;
1948
+ utxo.isChange = addressToPath[utxo.address].isChange;
1952
1949
  }
1953
1950
  return next();
1954
1951
  }
@@ -2402,6 +2399,37 @@ export class WalletService implements IWalletService {
2402
2399
  }
2403
2400
 
2404
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
+ }
2405
2433
  async.series(
2406
2434
  [
2407
2435
  next => {
@@ -2409,7 +2437,7 @@ export class WalletService implements IWalletService {
2409
2437
  boolToNum(!!opts.feeLevel) + boolToNum(Utils.isNumber(opts.feePerKb)) + boolToNum(Utils.isNumber(opts.fee));
2410
2438
  if (feeArgs > 1) return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
2411
2439
 
2412
- if (feeArgs == 0) {
2440
+ if (feeArgs == 0 && !hasGasFeeOverride) {
2413
2441
  opts.feeLevel = 'normal';
2414
2442
  }
2415
2443
 
@@ -2421,6 +2449,11 @@ export class WalletService implements IWalletService {
2421
2449
  );
2422
2450
  }
2423
2451
 
2452
+ // An explicit gas fee determines the price, not the named fee level.
2453
+ if (hasGasFeeOverride) {
2454
+ delete opts.feeLevel;
2455
+ }
2456
+
2424
2457
  const error = ChainService.checkUtxos(wallet.chain, opts);
2425
2458
  if (error) {
2426
2459
  return next(new ClientError('fee can only be set when inputs are specified'));
@@ -2782,6 +2815,9 @@ export class WalletService implements IWalletService {
2782
2815
  * @param {Boolean} opts.enableRBF - Optional. enable BTC Replace By Fee
2783
2816
  * @param {Boolean} opts.replaceTxByFee - Optional. Ignore locked utxos check ( used for replacing a transaction designated as RBF)
2784
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.
2785
2821
  * @param {number} opts.gasLimitBuffer - Optional. Percentage of buffer to add to the gasLimit
2786
2822
  * @param {number} opts.priorityFeePercentile - Optional. Percentile of targeted priority fee rate
2787
2823
  * @param {Boolean} opts.multiTx - Optional. Proposal will create multiple transactions
@@ -3172,19 +3208,23 @@ export class WalletService implements IWalletService {
3172
3208
  * @returns {Object} txProposal
3173
3209
  */
3174
3210
  getTx(opts, cb: (err: any, txp?: TxProposal) => void) {
3175
- 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) => {
3176
3213
  if (err) return cb(err);
3177
- 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);
3178
3217
 
3179
- if (!txp.txid) return cb(null, txp);
3218
+ if (!txp.txid) return cb(null, txp);
3180
3219
 
3181
- this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3182
- if (err) {
3183
- this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3184
- }
3185
- txp.note = note;
3186
- return cb(null, txp);
3187
- });
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);
3188
3228
  });
3189
3229
  }
3190
3230
 
@@ -3202,19 +3242,22 @@ export class WalletService implements IWalletService {
3202
3242
  // foreign wallet's proposal data to any authenticated copayer. The
3203
3243
  // internal global consumers (BlockchainMonitor, getWalletFromIdentifier)
3204
3244
  // are unaffected; they keep calling storage.fetchTxByHash directly.
3205
- this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3245
+ this.storage.fetchWallet(this.walletId, (err, wallet) => {
3206
3246
  if (err) return cb(err);
3207
- 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);
3208
3250
 
3209
- if (!txp.txid) return cb(null, txp);
3251
+ if (!txp.txid) return cb(null, txp);
3210
3252
 
3211
- this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3212
- if (err) {
3213
- this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3214
- }
3215
- txp.note = note;
3216
- return cb(null, txp);
3217
- });
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);
3218
3261
  });
3219
3262
  }
3220
3263
 
@@ -3844,7 +3887,7 @@ export class WalletService implements IWalletService {
3844
3887
  return cb(err, txps);
3845
3888
  }
3846
3889
  );
3847
- });
3890
+ }, opts.wallet);
3848
3891
  }
3849
3892
  }
3850
3893
 
@@ -4870,7 +4913,8 @@ export class WalletService implements IWalletService {
4870
4913
  minTs,
4871
4914
  maxTs
4872
4915
  },
4873
- done
4916
+ done,
4917
+ wallet
4874
4918
  );
4875
4919
  },
4876
4920
  done => {
@@ -4879,7 +4923,8 @@ export class WalletService implements IWalletService {
4879
4923
  {
4880
4924
  minTs
4881
4925
  },
4882
- done
4926
+ done,
4927
+ wallet
4883
4928
  );
4884
4929
  }
4885
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
 
@@ -20,7 +20,6 @@ export interface BuyCryptoConfig {
20
20
  sardine?: ExchangeConfig;
21
21
  simplex?: ExchangeConfig;
22
22
  transak?: ExchangeConfig;
23
- wyre?: ExchangeConfig;
24
23
  }
25
24
 
26
25
  export interface SellCryptoConfig {