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

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 (105) 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/common/constants.ts +1 -1
  8. package/src/lib/common/utils.ts +18 -1
  9. package/src/lib/model/tsskeygen.ts +16 -0
  10. package/src/lib/routes/maintenance.ts +11 -2
  11. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  12. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  13. package/src/lib/routes/services.ts +7 -1
  14. package/src/lib/routes/tss.ts +33 -30
  15. package/src/lib/routes/wallets.ts +3 -1
  16. package/src/lib/server.ts +198 -105
  17. package/src/lib/storage.ts +129 -20
  18. package/src/lib/tss.ts +111 -7
  19. package/test/chain/eth.test.ts +70 -0
  20. package/test/chain/sol.test.ts +1 -1
  21. package/test/chain/xrp.test.ts +1 -1
  22. package/test/expressapp.test.ts +243 -0
  23. package/test/integration/addressReads.test.ts +503 -0
  24. package/test/integration/bcmonitor.test.ts +44 -0
  25. package/test/integration/server.test.ts +251 -49
  26. package/test/integration/tss.test.ts +428 -0
  27. package/test/integration/walletReads.test.ts +488 -0
  28. package/ts_build/package.json +4 -4
  29. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  30. package/ts_build/src/lib/chain/btc/index.js +3 -2
  31. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  32. package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
  33. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  34. package/ts_build/src/lib/chain/eth/index.js +114 -102
  35. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  36. package/ts_build/src/lib/chain/sol/index.js +1 -1
  37. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  38. package/ts_build/src/lib/chain/xrp/index.js +1 -1
  39. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  40. package/ts_build/src/lib/common/constants.d.ts +1 -1
  41. package/ts_build/src/lib/common/constants.js +1 -1
  42. package/ts_build/src/lib/common/index.d.ts +2 -1
  43. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  44. package/ts_build/src/lib/common/utils.d.ts +1 -0
  45. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  46. package/ts_build/src/lib/common/utils.js +16 -1
  47. package/ts_build/src/lib/common/utils.js.map +1 -1
  48. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  49. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  51. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  52. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  53. package/ts_build/src/lib/routes/maintenance.js +12 -4
  54. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  55. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  56. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  57. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  58. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  59. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  60. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  61. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  62. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  63. package/ts_build/src/lib/routes/services.js +6 -1
  64. package/ts_build/src/lib/routes/services.js.map +1 -1
  65. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  66. package/ts_build/src/lib/routes/tss.js +32 -30
  67. package/ts_build/src/lib/routes/tss.js.map +1 -1
  68. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  69. package/ts_build/src/lib/routes/wallets.js +2 -1
  70. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  71. package/ts_build/src/lib/server.d.ts +24 -3
  72. package/ts_build/src/lib/server.d.ts.map +1 -1
  73. package/ts_build/src/lib/server.js +148 -97
  74. package/ts_build/src/lib/server.js.map +1 -1
  75. package/ts_build/src/lib/storage.d.ts +26 -10
  76. package/ts_build/src/lib/storage.d.ts.map +1 -1
  77. package/ts_build/src/lib/storage.js +99 -20
  78. package/ts_build/src/lib/storage.js.map +1 -1
  79. package/ts_build/src/lib/tss.d.ts +12 -0
  80. package/ts_build/src/lib/tss.d.ts.map +1 -1
  81. package/ts_build/src/lib/tss.js +64 -5
  82. package/ts_build/src/lib/tss.js.map +1 -1
  83. package/ts_build/test/chain/eth.test.js +59 -0
  84. package/ts_build/test/chain/eth.test.js.map +1 -1
  85. package/ts_build/test/chain/sol.test.js +1 -1
  86. package/ts_build/test/chain/sol.test.js.map +1 -1
  87. package/ts_build/test/chain/xrp.test.js +1 -1
  88. package/ts_build/test/chain/xrp.test.js.map +1 -1
  89. package/ts_build/test/expressapp.test.js +222 -0
  90. package/ts_build/test/expressapp.test.js.map +1 -1
  91. package/ts_build/test/integration/addressReads.test.d.ts +2 -0
  92. package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
  93. package/ts_build/test/integration/addressReads.test.js +465 -0
  94. package/ts_build/test/integration/addressReads.test.js.map +1 -0
  95. package/ts_build/test/integration/bcmonitor.test.js +40 -0
  96. package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
  97. package/ts_build/test/integration/server.test.js +228 -48
  98. package/ts_build/test/integration/server.test.js.map +1 -1
  99. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  100. package/ts_build/test/integration/tss.test.js +344 -0
  101. package/ts_build/test/integration/tss.test.js.map +1 -1
  102. package/ts_build/test/integration/walletReads.test.d.ts +2 -0
  103. package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
  104. package/ts_build/test/integration/walletReads.test.js +436 -0
  105. package/ts_build/test/integration/walletReads.test.js.map +1 -0
@@ -1,7 +1,6 @@
1
1
  import express from 'express';
2
2
  import * as Types from '../../types/expressapp';
3
3
  import { TssKeyGen, TssSign } from '../tss';
4
- import { authRequest } from './middleware/authRequest';
5
4
  import { authTssKeyGenRequest, authTssSignRequest } from './middleware/authTssRequest';
6
5
  import { createWalletLimiter } from './middleware/createWalletLimiter';
7
6
  import { verifyTssMessage } from './middleware/verifyTssMessage';
@@ -49,6 +48,36 @@ export class TssRouter {
49
48
  }
50
49
  });
51
50
 
51
+ // Must be before /keygen/:id/:round so "secret" doesn't get interpreted as a round number
52
+ router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
53
+ try {
54
+ const id = req.params.id;
55
+ const copayerId = req.headers['x-identity'];
56
+ const { chain, network } = req.query as { [key: string]: string };
57
+ const secret = await TssKeyGen.getBwsJoinSecret({ id, chain, network, copayerId });
58
+ return res.json({ secret });
59
+ } catch (err) {
60
+ return returnError(err ?? 'unknown', res, req);
61
+ }
62
+ });
63
+
64
+ router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
65
+ try {
66
+ const id = req.params.id;
67
+ const { chain, network, secrets, secret } = req.body;
68
+ const copayerId = req.headers['x-identity'];
69
+ if (secrets == null && secret != null) {
70
+ // Legacy (< v1.3) clients send a single unencrypted secret
71
+ await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
72
+ } else {
73
+ await TssKeyGen.storeBwsJoinSecrets({ id, chain, network, secrets, copayerId });
74
+ }
75
+ return res.send();
76
+ } catch (err) {
77
+ return returnError(err ?? 'unknown', res, req);
78
+ }
79
+ });
80
+
52
81
  router.get('/v1/tss/keygen/:id/:round', authTssKeyGenRequest(), async function(req, res) {
53
82
  let interval: NodeJS.Timeout;
54
83
  try {
@@ -58,10 +87,6 @@ export class TssRouter {
58
87
  const { id, round } = req.params as { [key: string]: string };
59
88
  const { maxWaitTime } = req.query as { [key: string]: string };
60
89
  const copayerId = req.headers['x-identity'];
61
- if (round === 'secret') {
62
- const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
63
- return res.json({ secret });
64
- }
65
90
 
66
91
  // Validate access and fetch session before committing to a streaming response, so that errors like
67
92
  // "session not found" or "not a participant" can still be returned with a proper
@@ -100,33 +125,10 @@ export class TssRouter {
100
125
  }
101
126
  });
102
127
 
103
- router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
104
- try {
105
- const id = req.params.id;
106
- const { secret } = req.body;
107
- const copayerId = req.headers['x-identity'];
108
- await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
109
- return res.send();
110
- } catch (err) {
111
- return returnError(err ?? 'unknown', res, req);
112
- }
113
- });
114
-
115
- router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
116
- try {
117
- const id = req.params.id;
118
- const copayerId = req.headers['x-identity'];
119
- const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
120
- return res.json({ secret });
121
- } catch (err) {
122
- return returnError(err ?? 'unknown', res, req);
123
- }
124
- });
125
-
126
128
 
127
129
  /** Signature Methods */
128
130
 
129
- router.post('/v1/tss/sign/:id', authRequest(), verifyTssMessage, async function(req, res) {
131
+ router.post('/v1/tss/sign/:id', authTssSignRequest({ joinable: true }), verifyTssMessage, async function(req, res) {
130
132
  try {
131
133
  const id = req.params.id;
132
134
  // version was not given by client until 1.1, so fallback to 1.0
@@ -178,7 +180,8 @@ export class TssRouter {
178
180
  try {
179
181
  const id = req.params.id;
180
182
  const { signature } = req.body;
181
- await TssSign.storeSignature({ id, signature });
183
+ const copayerId = req.headers['x-identity'];
184
+ await TssSign.storeSignature({ id, signature, copayerId });
182
185
  return res.send();
183
186
  } catch (err) {
184
187
  return returnError(err ?? 'unknown', res, req);
@@ -228,8 +228,10 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
228
228
  return res.json([]);
229
229
  }
230
230
 
231
+ // a copayer that isn't an object with a string id can't exist, so it isn't looked up
232
+ const wellFormed = copayers.filter(c => typeof c?.copayerId === 'string');
231
233
  const storage = WalletService.getStorage();
232
- const existing = await Promise.all<{ copayerId: string; verified: boolean }>(copayers.map(async c => {
234
+ const existing = await Promise.all<{ copayerId: string; verified: boolean }>(wellFormed.map(async c => {
233
235
  const copayer = await storage.fetchCopayerLookup(c.copayerId).catch(() => null);
234
236
  if (!copayer) {
235
237
  return { copayerId: c.copayerId, verified: false };
package/src/lib/server.ts CHANGED
@@ -604,6 +604,7 @@ export class WalletService implements IWalletService {
604
604
  * @param {number} [opts.tssVersion] TSS version to use. Supplying this with n > 1 and a multisig chain (e.g. btc) will tell the wallet to use
605
605
  * threshold insead of on-chain multisig. Otherwise, n > 1 and a non-multisig chain will default to TSS_KEYGEN_SCHEME_VERSION
606
606
  * @param {string} [opts.tssKeyId] TSS key session id. This is the id of the TSS key generation session that will be used to create the wallet.
607
+ * @param {string} [opts.tssKeygenCopayerId] The ID of the copayer creating the wallet. Required for TSS version >= 1.3.
607
608
  */
608
609
  async createWallet(opts, cb) {
609
610
  let pubKey;
@@ -674,6 +675,17 @@ export class WalletService implements IWalletService {
674
675
  return cb(new ClientError('Invalid TSS version'));
675
676
  }
676
677
 
678
+ if (opts.tssVersion >= 1.3) {
679
+ if (!opts.tssKeygenCopayerId) {
680
+ return cb(new ClientError('Copayer ID is required for TSS version >= 1.3'));
681
+ }
682
+ // This is only a sanity check to prevent problems down the line. If this is somehow
683
+ // spoofed, there are more strict authorization checks later in the wallet creation flow.
684
+ if (keySession.participants.indexOf(opts.tssKeygenCopayerId) !== 0) {
685
+ return cb(new ClientError('Only the TSS session leader can create a wallet'));
686
+ }
687
+ }
688
+
677
689
  // TSS wallets behave like a single-sig
678
690
  opts.m = 1;
679
691
  opts.n = 1;
@@ -873,26 +885,29 @@ export class WalletService implements IWalletService {
873
885
  pendingTxps?: ITxProposal[];
874
886
  preferences?: boolean;
875
887
  } = {};
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
- }
888
+ this.getWallet({}, (err, fetchedWallet: Wallet) => {
889
+ if (err) return cb(err);
884
890
 
891
+ // Read the wallet once, and give each step its own copy, the same as reading it again: the steps change their
892
+ // wallet in place (this one strips copayer data, the balance code sets tokenAddress on it), and the balance step
893
+ // can store its copy whole.
894
+ const copyOfWallet = () => Wallet.fromObj(fetchedWallet.toObject());
895
+ const omit = (obj, keys: string[]): any => Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key)));
896
+ async.parallel(
897
+ [
898
+ next => {
899
+ let wallet: Partial<Wallet> = copyOfWallet();
885
900
  const walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager'];
886
901
  const copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
887
902
 
888
903
  wallet.copayers = (wallet.copayers || []).map(copayer => {
889
904
  if (copayer.id == this.copayerId) return copayer;
890
- return _.omit(copayer, 'customData');
905
+ return omit(copayer, ['customData']);
891
906
  });
892
907
  if (!opts.includeExtendedInfo) {
893
- wallet = _.omit(wallet, walletExtendedKeys);
908
+ wallet = omit(wallet, walletExtendedKeys);
894
909
  wallet.copayers = (wallet.copayers || []).map(copayer => {
895
- return _.omit(copayer, copayerExtendedKeys);
910
+ return omit(copayer, copayerExtendedKeys);
896
911
  }) as Copayer[];
897
912
  }
898
913
  status.wallet = wallet;
@@ -903,46 +918,45 @@ export class WalletService implements IWalletService {
903
918
  status.serverMessage = deprecatedServerMessage(wallet, this.appName, this.appVersion);
904
919
  }
905
920
  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') {
921
+ },
922
+ next => {
923
+ this.getBalance({ ...opts, wallet: copyOfWallet() }, (err, balance) => {
924
+ // ignore WALLET_NEED_SCAN err is includeExtendedInfo is given
925
+ // (to allow `importWallet` to import a wallet, while scan has
926
+ // failed)
927
+ if (opts.includeExtendedInfo) {
928
+ if (err && err.code != 'WALLET_NEED_SCAN') {
929
+ return next(err);
930
+ }
931
+ } else if (err) {
916
932
  return next(err);
917
933
  }
918
- } else if (err) {
919
- return next(err);
920
- }
921
934
 
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
- });
935
+ status.balance = balance;
936
+ next();
937
+ });
938
+ },
939
+ next => {
940
+ this.getPendingTxs({ ...opts, wallet: copyOfWallet() }, (err, pendingTxps) => {
941
+ if (err) return next(err);
942
+ status.pendingTxps = pendingTxps;
943
+ next();
944
+ });
945
+ },
946
+ next => {
947
+ this.getPreferences({}, (err, preferences) => {
948
+ if (err) return next(err);
949
+ status.preferences = preferences;
950
+ next();
951
+ });
952
+ }
953
+ ],
954
+ err => {
955
+ if (err) return cb(err);
956
+ return cb(null, status);
939
957
  }
940
- ],
941
- err => {
942
- if (err) return cb(err);
943
- return cb(null, status);
944
- }
945
- );
958
+ );
959
+ });
946
960
  }
947
961
 
948
962
  /**
@@ -1036,7 +1050,11 @@ export class WalletService implements IWalletService {
1036
1050
  this._notify(type, data, {}, cb);
1037
1051
  }
1038
1052
 
1039
- _addCopayerToWallet(wallet: Wallet, opts, cb) {
1053
+ _addCopayerToWallet(
1054
+ wallet: Wallet,
1055
+ opts: JoinWalletOpts,
1056
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1057
+ ) {
1040
1058
  const copayer = Copayer.create({
1041
1059
  coin: wallet.coin,
1042
1060
  chain: wallet.chain, // chain === coin for stored clients
@@ -1192,20 +1210,11 @@ export class WalletService implements IWalletService {
1192
1210
 
1193
1211
  /**
1194
1212
  * Joins a wallet in creation.
1195
- * @param {Object} opts
1196
- * @param {string} opts.walletId The wallet id.
1197
- * @param {string} opts.coin The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc
1198
- * @param {string} opts.chain The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc
1199
- * @param {string} opts.name The copayer name.
1200
- * @param {string} opts.xPubKey Extended Public Key for this copayer
1201
- * @param {string} opts.hardwareSourcePublicKey Public key from a hardware device for this copayer
1202
- * @param {string} opts.clientDerivedPublicKey Public key from the client for this wallet
1203
- * @param {string} opts.requestPubKey Public Key used to check requests from this copayer.
1204
- * @param {string} opts.copayerSignature S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
1205
- * @param {string} [opts.customData] Custom data for this copayer.
1206
- * @param {boolean} [opts.dryRun] Simulate the action but do not change server state.
1207
1213
  */
1208
- joinWallet(opts, cb) {
1214
+ joinWallet(
1215
+ opts: JoinWalletOpts,
1216
+ cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
1217
+ ) {
1209
1218
  // xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
1210
1219
  // fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
1211
1220
  // end-to-end identity protocol rather than a local identity fallback.
@@ -1224,6 +1233,9 @@ export class WalletService implements IWalletService {
1224
1233
  opts.chain = opts.coin; // chain === coin for stored clients
1225
1234
  }
1226
1235
  if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
1236
+ if (opts.tssKeygenChain != null && !Utils.checkValueInCollection(opts.tssKeygenChain, Constants.CHAINS)) {
1237
+ return cb(new ClientError('Invalid TSS keygen chain'));
1238
+ }
1227
1239
 
1228
1240
  // xPubKey is parsed and validated for every join; ancillary fields never suppress it.
1229
1241
  // A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
@@ -1284,8 +1296,16 @@ export class WalletService implements IWalletService {
1284
1296
 
1285
1297
  if (wallet.tssKeyId) {
1286
1298
  const keySession = await storage.fetchTssKeyGenSession({ id: wallet.tssKeyId });
1287
- const copayerId = Copayer.xPubToCopayerId(opts.chain, opts.xPubKey);
1288
- if (!keySession.participants.includes(copayerId)) {
1299
+ // A sibling wallet on another network (e.g. livenet -> testnet) has an xPubKey serialized for that network,
1300
+ // but the keygen session's copayer IDs were derived from the xPubKey serialized for the session's network.
1301
+ const idChain = opts.tssKeygenChain || opts.chain;
1302
+ const copayerIds = new Set([
1303
+ Copayer.xPubToCopayerId(idChain, opts.xPubKey), // this is probably a duplicate of one of the next two entries, but just in case.
1304
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'livenet')),
1305
+ Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'testnet'))
1306
+ ]);
1307
+
1308
+ if (!keySession?.participants.some(id => copayerIds.has(id))) {
1289
1309
  return cb(Errors.TSS_NON_PARTICIPANT);
1290
1310
  }
1291
1311
  return this._addCopayerToWallet(wallet, opts, cb);
@@ -1535,9 +1555,10 @@ export class WalletService implements IWalletService {
1535
1555
  _canCreateAddress(ignoreMaxGap, cb) {
1536
1556
  if (ignoreMaxGap) return cb(null, true);
1537
1557
 
1538
- this.storage.fetchAddresses(this.walletId, (err, addresses: IAddress[]) => {
1558
+ const newest = { noChange: true, reverse: true, limit: Defaults.MAX_MAIN_ADDRESS_GAP };
1559
+ this.storage.fetchAddressesPage(this.walletId, newest, (err, addresses: IAddress[]) => {
1539
1560
  if (err) return cb(err);
1540
- const latestAddresses = addresses.filter(x => !x.isChange).slice(-Defaults.MAX_MAIN_ADDRESS_GAP) as IAddress[];
1561
+ const latestAddresses = addresses.reverse();
1541
1562
  if (
1542
1563
  latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP ||
1543
1564
  latestAddresses.some(a => a.hasActivity)
@@ -1638,7 +1659,7 @@ export class WalletService implements IWalletService {
1638
1659
  };
1639
1660
 
1640
1661
  const getFirstAddress = (wallet, cb) => {
1641
- this.storage.fetchAddresses(this.walletId, (err, addresses) => {
1662
+ this.storage.fetchAddressesPage(this.walletId, { limit: 1 }, (err, addresses) => {
1642
1663
  if (err) return cb(err);
1643
1664
  if (addresses?.length) {
1644
1665
  const x = addresses[0];
@@ -1691,17 +1712,9 @@ export class WalletService implements IWalletService {
1691
1712
 
1692
1713
  getAddresses(opts: GetAddressesOpts, cb) {
1693
1714
  opts = opts || {};
1694
- this.storage.fetchAddresses(this.walletId, (err, addresses) => {
1715
+ const { noChange, addresses: only, reverse, skip, limit } = opts;
1716
+ this.storage.fetchAddressesPage(this.walletId, { noChange, addresses: only, reverse, skip, limit }, (err, addresses) => {
1695
1717
  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
1718
 
1706
1719
  this.getWallet({}, (err, wallet) => {
1707
1720
  if (err) return cb(err);
@@ -1783,7 +1796,7 @@ export class WalletService implements IWalletService {
1783
1796
  async.series(
1784
1797
  [
1785
1798
  next => {
1786
- this.getWallet({}, (err, w) => {
1799
+ const useWallet = (err, w) => {
1787
1800
  if (err) return next(err);
1788
1801
 
1789
1802
  wallet = w;
@@ -1793,7 +1806,10 @@ export class WalletService implements IWalletService {
1793
1806
  bc = this._getBlockchainExplorer(wallet.chain, wallet.network);
1794
1807
  if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
1795
1808
  return next();
1796
- });
1809
+ };
1810
+ // the balance code passes the wallet it already has
1811
+ if (opts.wallet) return useWallet(null, opts.wallet);
1812
+ this.getWallet({}, useWallet);
1797
1813
  },
1798
1814
  next => {
1799
1815
  if (Array.isArray(opts.addresses)) {
@@ -1889,7 +1905,7 @@ export class WalletService implements IWalletService {
1889
1905
  next => {
1890
1906
  utxoIndex = _.keyBy(allUtxos, utxoKey);
1891
1907
 
1892
- this.getPendingTxs({ numberFormat: opts.numberFormat }, (err, txps) => {
1908
+ this.getPendingTxs({ numberFormat: opts.numberFormat, wallet }, (err, txps) => {
1893
1909
  if (err) return next(err);
1894
1910
 
1895
1911
  const lockedInputs = txps.flatMap(t => t.inputs).map(utxoKey);
@@ -1933,7 +1949,8 @@ export class WalletService implements IWalletService {
1933
1949
  });
1934
1950
  logger.debug(`Got ${allUtxos.length} usable UTXOs`);
1935
1951
  return next();
1936
- }
1952
+ },
1953
+ wallet
1937
1954
  );
1938
1955
  },
1939
1956
  next => {
@@ -1946,6 +1963,7 @@ export class WalletService implements IWalletService {
1946
1963
  }
1947
1964
  utxo.path = addressToPath[utxo.address].path;
1948
1965
  utxo.publicKeys = addressToPath[utxo.address].publicKeys;
1966
+ utxo.isChange = addressToPath[utxo.address].isChange;
1949
1967
  }
1950
1968
  return next();
1951
1969
  }
@@ -2399,6 +2417,37 @@ export class WalletService implements IWalletService {
2399
2417
  }
2400
2418
 
2401
2419
  _validateAndSanitizeTxOpts(wallet, opts, cb) {
2420
+ const gasFeeFields = ['gasPrice', 'maxGasFee', 'priorityGasFee'];
2421
+ const hasGasFeeOverride = gasFeeFields.some(key => opts[key] != null);
2422
+ if (hasGasFeeOverride) {
2423
+ if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) {
2424
+ return cb(new ClientError('Gas fee overrides are only supported for EVM chains'));
2425
+ }
2426
+ for (const key of gasFeeFields) {
2427
+ if (opts[key] != null && (!Number.isSafeInteger(opts[key]) || opts[key] <= 0)) {
2428
+ return cb(new ClientError(`${key} must be a positive safe integer in wei`));
2429
+ }
2430
+ }
2431
+ if (opts.gasPrice != null && Number(opts.txType || 0) !== 0) {
2432
+ return cb(new ClientError('gasPrice requires a type 0 transaction'));
2433
+ }
2434
+ if ((opts.maxGasFee != null || opts.priorityGasFee != null) && Number(opts.txType) !== 2) {
2435
+ return cb(new ClientError('maxGasFee and priorityGasFee require a type 2 transaction'));
2436
+ }
2437
+ if ((opts.maxGasFee != null) !== (opts.priorityGasFee != null)) {
2438
+ return cb(new ClientError('maxGasFee and priorityGasFee must be supplied together'));
2439
+ }
2440
+ if (opts.priorityGasFee > opts.maxGasFee) {
2441
+ return cb(new ClientError('priorityGasFee must not exceed maxGasFee'));
2442
+ }
2443
+ if (opts.feePerKb != null || opts.fee != null) {
2444
+ return cb(new ClientError('Gas fee overrides cannot be combined with feePerKb or fee'));
2445
+ }
2446
+ // sendMax calculates its amount before getFee, using a separate fee estimate.
2447
+ if (opts.sendMax) {
2448
+ return cb(new ClientError('Gas fee overrides are not supported with sendMax'));
2449
+ }
2450
+ }
2402
2451
  async.series(
2403
2452
  [
2404
2453
  next => {
@@ -2406,7 +2455,7 @@ export class WalletService implements IWalletService {
2406
2455
  boolToNum(!!opts.feeLevel) + boolToNum(Utils.isNumber(opts.feePerKb)) + boolToNum(Utils.isNumber(opts.fee));
2407
2456
  if (feeArgs > 1) return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
2408
2457
 
2409
- if (feeArgs == 0) {
2458
+ if (feeArgs == 0 && !hasGasFeeOverride) {
2410
2459
  opts.feeLevel = 'normal';
2411
2460
  }
2412
2461
 
@@ -2418,6 +2467,11 @@ export class WalletService implements IWalletService {
2418
2467
  );
2419
2468
  }
2420
2469
 
2470
+ // An explicit gas fee determines the price, not the named fee level.
2471
+ if (hasGasFeeOverride) {
2472
+ delete opts.feeLevel;
2473
+ }
2474
+
2421
2475
  const error = ChainService.checkUtxos(wallet.chain, opts);
2422
2476
  if (error) {
2423
2477
  return next(new ClientError('fee can only be set when inputs are specified'));
@@ -2779,6 +2833,9 @@ export class WalletService implements IWalletService {
2779
2833
  * @param {Boolean} opts.enableRBF - Optional. enable BTC Replace By Fee
2780
2834
  * @param {Boolean} opts.replaceTxByFee - Optional. Ignore locked utxos check ( used for replacing a transaction designated as RBF)
2781
2835
  * @param {number} opts.txType - Optional. Type of EVM transaction
2836
+ * @param {number} opts.gasPrice - Optional. Type 0 gas price in wei. Overrides feeLevel; incompatible with fee/feePerKb/sendMax.
2837
+ * @param {number} opts.maxGasFee - Optional. Type 2 maximum fee per gas in wei; requires priorityGasFee.
2838
+ * @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
2839
  * @param {number} opts.gasLimitBuffer - Optional. Percentage of buffer to add to the gasLimit
2783
2840
  * @param {number} opts.priorityFeePercentile - Optional. Percentile of targeted priority fee rate
2784
2841
  * @param {Boolean} opts.multiTx - Optional. Proposal will create multiple transactions
@@ -3169,19 +3226,23 @@ export class WalletService implements IWalletService {
3169
3226
  * @returns {Object} txProposal
3170
3227
  */
3171
3228
  getTx(opts, cb: (err: any, txp?: TxProposal) => void) {
3172
- this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
3229
+ // the tx and its note are both completed from the wallet, so read it once
3230
+ this.storage.fetchWallet(this.walletId, (err, wallet) => {
3173
3231
  if (err) return cb(err);
3174
- if (!txp) return cb(Errors.TX_NOT_FOUND);
3232
+ this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
3233
+ if (err) return cb(err);
3234
+ if (!txp) return cb(Errors.TX_NOT_FOUND);
3175
3235
 
3176
- if (!txp.txid) return cb(null, txp);
3236
+ if (!txp.txid) return cb(null, txp);
3177
3237
 
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
- });
3238
+ this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3239
+ if (err) {
3240
+ this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3241
+ }
3242
+ txp.note = note;
3243
+ return cb(null, txp);
3244
+ }, wallet);
3245
+ }, wallet);
3185
3246
  });
3186
3247
  }
3187
3248
 
@@ -3199,19 +3260,22 @@ export class WalletService implements IWalletService {
3199
3260
  // foreign wallet's proposal data to any authenticated copayer. The
3200
3261
  // internal global consumers (BlockchainMonitor, getWalletFromIdentifier)
3201
3262
  // are unaffected; they keep calling storage.fetchTxByHash directly.
3202
- this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3263
+ this.storage.fetchWallet(this.walletId, (err, wallet) => {
3203
3264
  if (err) return cb(err);
3204
- if (!txp) return cb(Errors.TX_NOT_FOUND);
3265
+ this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
3266
+ if (err) return cb(err);
3267
+ if (!txp) return cb(Errors.TX_NOT_FOUND);
3205
3268
 
3206
- if (!txp.txid) return cb(null, txp);
3269
+ if (!txp.txid) return cb(null, txp);
3207
3270
 
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
- });
3271
+ this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
3272
+ if (err) {
3273
+ this.logw(`Error fetching tx note for ${txp.txid}:`, err);
3274
+ }
3275
+ txp.note = note;
3276
+ return cb(null, txp);
3277
+ }, wallet);
3278
+ }, wallet);
3215
3279
  });
3216
3280
  }
3217
3281
 
@@ -3841,7 +3905,7 @@ export class WalletService implements IWalletService {
3841
3905
  return cb(err, txps);
3842
3906
  }
3843
3907
  );
3844
- });
3908
+ }, opts.wallet);
3845
3909
  }
3846
3910
  }
3847
3911
 
@@ -4131,7 +4195,7 @@ export class WalletService implements IWalletService {
4131
4195
  return cb(err);
4132
4196
  }
4133
4197
  wallet.beRegistered = true;
4134
- return this.storage.storeWallet(wallet, err => cb(err, true));
4198
+ return this.storage.storeWalletRegistration(wallet, err => cb(err, true));
4135
4199
  });
4136
4200
  }
4137
4201
 
@@ -4867,7 +4931,8 @@ export class WalletService implements IWalletService {
4867
4931
  minTs,
4868
4932
  maxTs
4869
4933
  },
4870
- done
4934
+ done,
4935
+ wallet
4871
4936
  );
4872
4937
  },
4873
4938
  done => {
@@ -4876,7 +4941,8 @@ export class WalletService implements IWalletService {
4876
4941
  {
4877
4942
  minTs
4878
4943
  },
4879
- done
4944
+ done,
4945
+ wallet
4880
4946
  );
4881
4947
  }
4882
4948
  ],
@@ -5790,3 +5856,30 @@ export const UPGRADES = {
5790
5856
  } as const;
5791
5857
 
5792
5858
  type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
5859
+
5860
+ export interface JoinWalletOpts {
5861
+ /** The wallet id. */
5862
+ walletId: string;
5863
+ /** The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5864
+ coin?: string;
5865
+ /** The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc */
5866
+ chain?: string;
5867
+ /** The copayer name. */
5868
+ name: string;
5869
+ /** Extended Public Key for this copayer */
5870
+ xPubKey: string;
5871
+ /** Public key from a hardware device for this copayer */
5872
+ hardwareSourcePublicKey?: string;
5873
+ /** Public key from the client for this wallet */
5874
+ clientDerivedPublicKey?: string;
5875
+ /** Public Key used to check requests from this copayer. */
5876
+ requestPubKey: string;
5877
+ /** Signature proving knowledge of the wallet secret for this copayer. */
5878
+ copayerSignature: string;
5879
+ /** Custom data for this copayer. */
5880
+ customData?: string;
5881
+ /** Simulate the action but do not change server state. */
5882
+ dryRun?: boolean;
5883
+ /** The chain used for TSS key generation. */
5884
+ tssKeygenChain?: string;
5885
+ };