@exodus/web3-ethereum 13.6.4 → 14.0.0

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.
@@ -18,10 +18,8 @@ export class EthereumProvider extends BaseProvider {
18
18
  throw new Error('"storageKeyPrefix" must be provided.');
19
19
  }
20
20
  this.#storage = createStorageWrapper(storage, storageKeyPrefix);
21
- this.#supportedChainIds = supportedChainIds
22
- .map(this.#sanitizeChainId)
23
- .sort();
24
- this.#getDefaultChainId = async () => getDefaultChainId().then((chainId) => chainId ? this.#sanitizeChainId(chainId) : null);
21
+ this.#supportedChainIds = supportedChainIds.map(this.#sanitizeChainId).sort();
22
+ this.#getDefaultChainId = async () => getDefaultChainId().then((chainId) => (chainId ? this.#sanitizeChainId(chainId) : null));
25
23
  // Observe account changes.
26
24
  this.on('connect', () => {
27
25
  const unobserve = accountsObservable.observe((accounts) => {
@@ -126,7 +124,9 @@ export class EthereumProvider extends BaseProvider {
126
124
  // if not supported yet, add it
127
125
  const chainId = this.#sanitizeChainId(params.chainId);
128
126
  if (!this.#supportedChainIds.includes(chainId)) {
129
- await this._callRpcMethod('wallet_addEthereumChain', [params]);
127
+ await this._callRpcMethod('wallet_addEthereumChain', [
128
+ params,
129
+ ]);
130
130
  this.#supportedChainIds.push(chainId);
131
131
  }
132
132
  return this.#switchChain(chainId);
@@ -163,8 +163,7 @@ export class EthereumProvider extends BaseProvider {
163
163
  data: args,
164
164
  });
165
165
  }
166
- if (!Array.isArray(params) &&
167
- (typeof params !== 'object' || params === null)) {
166
+ if (!Array.isArray(params) && (typeof params !== 'object' || params === null)) {
168
167
  throw ethErrors.rpc.invalidRequest({
169
168
  message: "'args.params' must be an object or array if provided.",
170
169
  data: args,
@@ -261,6 +260,7 @@ export class EthereumProvider extends BaseProvider {
261
260
  const params = paramsOrCallback;
262
261
  return new Promise((resolve, reject) => this.request({ method, params })
263
262
  .then((result) => resolve({ id: '1', jsonrpc: '2.0', result }))
263
+ // eslint-disable-next-line prefer-promise-reject-errors -- JSON-RPC response format
264
264
  .catch((err) => reject({ id: '1', jsonrpc: '2.0', error: err })));
265
265
  }
266
266
  const payload = methodOrPayload;
@@ -274,11 +274,13 @@ export class EthereumProvider extends BaseProvider {
274
274
  this.request({ method, params })
275
275
  .then((result) => {
276
276
  if (hasCallback) {
277
+ // eslint-disable-next-line promise/no-callback-in-promise -- legacy callback-based API
277
278
  callback(null, { id, jsonrpc, result });
278
279
  }
279
280
  })
280
281
  .catch((err) => {
281
282
  if (hasCallback) {
283
+ // eslint-disable-next-line promise/no-callback-in-promise -- legacy callback-based API
282
284
  callback(err, { id, jsonrpc, error: err });
283
285
  }
284
286
  });
@@ -4,7 +4,7 @@ export function createStorageWrapper(storage, storageKeyPrefix) {
4
4
  try {
5
5
  return await storage.getItem(chainIdKey);
6
6
  }
7
- catch (err) {
7
+ catch {
8
8
  return null;
9
9
  }
10
10
  }
@@ -12,7 +12,7 @@ export function createStorageWrapper(storage, storageKeyPrefix) {
12
12
  try {
13
13
  await storage.setItem(chainIdKey, chainId);
14
14
  }
15
- catch (err) {
15
+ catch {
16
16
  // Ignore error.
17
17
  }
18
18
  }
@@ -20,7 +20,7 @@ export function createStorageWrapper(storage, storageKeyPrefix) {
20
20
  try {
21
21
  await storage.removeItem(chainIdKey);
22
22
  }
23
- catch (err) {
23
+ catch {
24
24
  // Ignore error.
25
25
  }
26
26
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @exodus/hydra/no-param-reassign-prop-only */
1
2
  import { createProviderProxy } from '@exodus/web3-provider/proxy';
2
3
  const ETHEREUM_INIT_METHODS_SET = new Set([
3
4
  'accountsChangedSet',
@@ -69,8 +70,7 @@ export const createEthereumProviderProxy = ({ isWalletNameFlag, impersonateOther
69
70
  if (shouldInitProvider) {
70
71
  await initProvider();
71
72
  }
72
- if (property === 'enable' ||
73
- args[0]?.method === 'eth_requestAccounts') {
73
+ if (property === 'enable' || args[0]?.method === 'eth_requestAccounts') {
74
74
  const managementProvider = await getManagementProvider();
75
75
  const chainId = (await getActiveChainId()) || (await getDefaultChainId());
76
76
  await managementProvider.askUserToChooseWallet(`evm:${chainId}`);
@@ -50,9 +50,7 @@ const createEVMDepsFactory = ({ analytics, blockchainMetadata, }) => {
50
50
  gasPrice,
51
51
  };
52
52
  if (eip1559Enabled) {
53
- feeData.maxPriorityFeePerGas = `0x${tipGasPrice
54
- .toBaseNumber()
55
- .toString(16)}`;
53
+ feeData.maxPriorityFeePerGas = `0x${tipGasPrice.toBaseNumber().toString(16)}`;
56
54
  feeData.maxFeePerGas = gasPrice;
57
55
  }
58
56
  return feeData;
@@ -79,16 +77,13 @@ const createEVMDepsFactory = ({ analytics, blockchainMetadata, }) => {
79
77
  // then compute the priority / tip fee.
80
78
  if (baseFeePerGas && _maxFeePerGas.gt(baseFeePerGas)) {
81
79
  const maxPriorityFeePerGas = _maxFeePerGas.sub(baseFeePerGas);
82
- feeData.maxPriorityFeePerGas = `0x${maxPriorityFeePerGas
83
- .toBaseNumber()
84
- .toString(16)}`;
80
+ feeData.maxPriorityFeePerGas = `0x${maxPriorityFeePerGas.toBaseNumber().toString(16)}`;
85
81
  }
86
82
  }
87
83
  return feeData;
88
84
  };
89
- const trackWalletWatchAssetUse = _onEthWalletWatchAssetRequest
90
- ? _onEthWalletWatchAssetRequest
91
- : async (assetDetails) => {
85
+ const trackWalletWatchAssetUse = _onEthWalletWatchAssetRequest ??
86
+ (async (assetDetails) => {
92
87
  // TODO: do we still need this? If yes, this ...assetDetails expansion is no bueno
93
88
  // why is this not always returning true in browser?
94
89
  analytics.track({
@@ -99,7 +94,7 @@ const createEVMDepsFactory = ({ analytics, blockchainMetadata, }) => {
99
94
  },
100
95
  });
101
96
  return true;
102
- };
97
+ });
103
98
  return {
104
99
  addEthereumChain,
105
100
  chainId: _chainId || 'undefined',
@@ -1,6 +1,6 @@
1
1
  import { SignTypedDataVersion } from '@exodus/ethereumjs/eth-sig-util';
2
2
  import { AssetNotSupportedError, InvalidInputError, UnauthorizedError, UnsupportedMethodError, UserRejectedRequestError, } from '@exodus/web3-errors';
3
- import { bufferToHex0xString, hex0xStringToBuffer, } from '@exodus/web3-ethereum-utils';
3
+ import { bufferToHex0xString, hex0xStringToBuffer } from '@exodus/web3-ethereum-utils';
4
4
  import { connect } from '@exodus/web3-rpc-handlers';
5
5
  import { hexToBN } from '@exodus/web3-utils';
6
6
  import { parseAndValidateTypedData } from './parseAndValidateTypedData.js';
@@ -63,6 +63,23 @@ const isValidGasLimitValue = (value) => {
63
63
  const proposedGasLimit = hexToBN(value);
64
64
  return proposedGasLimit.lte(maximumBlockGasLimit);
65
65
  };
66
+ /*
67
+ A `personal_sign` message, as the bytes that will actually be signed.
68
+
69
+ Only a 0x-prefixed, even-length, fully hex string counts as hex. Every other shape is
70
+ rejected rather than reinterpreted, because the approval screen is shown the raw request
71
+ string: if the signer applies a decoding rule the screen does not, the user reads one
72
+ message and authenticates another. Bare hex is refused for exactly that reason -- the
73
+ screen has no 0x to strip, so it renders the digits literally while the old signer decoded
74
+ them.
75
+ */
76
+ const PERSONAL_SIGN_MESSAGE = /^0x([0-9a-fA-F]{2})*$/;
77
+ const decodePersonalSignMessage = (message) => {
78
+ if (typeof message !== 'string' || !PERSONAL_SIGN_MESSAGE.test(message)) {
79
+ throw new InvalidInputError();
80
+ }
81
+ return hex0xStringToBuffer(message);
82
+ };
66
83
  export function exposeHandlers(rpc, deps) {
67
84
  const { chainId, isTrusted, isAutoApproved, ensureTrusted, ensureUntrusted, approveConnection, approveMessage, approveTransactions, forwardRequest, getSupportedChainIds = async () => [], getAddress, getEstimatedGas, getCustomFeeData, getFeeData, getNonce, sendRawTransaction, simulateEthereumTransactions, scanDomains, onTransactionsSigned = () => { }, onEthWalletWatchAssetRequest = () => { }, isUnlocked = () => true, ensureUnlocked = async () => true, getIsConnected, getAsset, addEthereumChain, getOrigin, getPathname, getActiveWalletAccountData, transactionSigner, messageSigner, } = deps;
68
85
  const network = `evm:${chainId}`;
@@ -86,7 +103,7 @@ export function exposeHandlers(rpc, deps) {
86
103
  gasPrice,
87
104
  };
88
105
  }
89
- async function prepareTransaction(transaction) {
106
+ async function prepareTransaction(transaction, from) {
90
107
  const transactionNeedsGasPrice = !transaction.gasPrice || !transaction.maxFeePerGas;
91
108
  const preparedTransaction = { ...transaction };
92
109
  preparedTransaction.nonce = await getNonce();
@@ -109,10 +126,14 @@ export function exposeHandlers(rpc, deps) {
109
126
  if (!transaction.data) {
110
127
  preparedTransaction.data = '0x';
111
128
  }
112
- // NOTE: Keep this at the end as other parameters can affect gas estimation.
129
+ if (transaction.from?.toLowerCase() !== from.toLowerCase()) {
130
+ preparedTransaction.from = from;
131
+ }
113
132
  if (!transaction.gas || !isValidGasLimitValue(transaction.gas)) {
114
133
  preparedTransaction.gas = await getEstimatedGas(preparedTransaction);
115
134
  }
135
+ // NOTE: DO NOT introduce any parameter changes after gas
136
+ // estimation!
116
137
  return preparedTransaction;
117
138
  }
118
139
  async function applyCustomFeeRate(transaction) {
@@ -121,9 +142,7 @@ export function exposeHandlers(rpc, deps) {
121
142
  const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = feeData;
122
143
  return { ...transaction, gasPrice, maxFeePerGas, maxPriorityFeePerGas };
123
144
  }
124
- else {
125
- return transaction;
126
- }
145
+ return transaction;
127
146
  }
128
147
  function validateTransaction(transaction) {
129
148
  const gasKeys = [
@@ -152,7 +171,8 @@ export function exposeHandlers(rpc, deps) {
152
171
  await assertTrusted();
153
172
  await ensureUnlocked();
154
173
  const asset = await getAsset(network);
155
- const preparedTransaction = await prepareTransaction(transaction);
174
+ const address = await getAddress();
175
+ const preparedTransaction = await prepareTransaction(transaction, address);
156
176
  validateTransaction(preparedTransaction);
157
177
  const origin = getOrigin();
158
178
  const pathname = getPathname ? getPathname() : '';
@@ -183,6 +203,20 @@ export function exposeHandlers(rpc, deps) {
183
203
  async function approveAndSignMessage(message) {
184
204
  await assertTrusted();
185
205
  await ensureUnlocked();
206
+ // The approval screen is handed this exact string, so the bytes we sign have to be
207
+ // derivable from it under ONE rule that the screen shares. `personal_sign` takes a
208
+ // 0x-prefixed hex string; the signer used to hex-decode whatever it was given, which
209
+ // disagreed with the screen for any message that was not 0x-prefixed hex:
210
+ //
211
+ // `<hex of a SIWE login>` screen showed raw hex, we signed the login
212
+ // `<hex of a SIWE login> Login only.` screen showed the suffix, we dropped it
213
+ // `Sign in to example.com` screen showed the text, we signed nothing
214
+ //
215
+ // Each one let a dapp put readable text in front of the user and take a signature over
216
+ // something else. Requiring the prefix means a string the screen renders as text can
217
+ // never be reinterpreted as bytes, and decoding BEFORE approval means a message we
218
+ // would refuse is never approved in the first place.
219
+ const rawMessage = decodePersonalSignMessage(message);
186
220
  await approve(approveMessage, network, message);
187
221
  const walletAccount = await getActiveWalletAccountData();
188
222
  const asset = await getAsset(network);
@@ -190,7 +224,7 @@ export function exposeHandlers(rpc, deps) {
190
224
  walletAccount,
191
225
  baseAssetName: asset.baseAssetName,
192
226
  message: {
193
- rawMessage: hex0xStringToBuffer(message),
227
+ rawMessage,
194
228
  },
195
229
  });
196
230
  return bufferToHex0xString(signedMessageBuffer);
@@ -283,7 +317,10 @@ export function exposeHandlers(rpc, deps) {
283
317
  }
284
318
  return permissions;
285
319
  });
286
- exposeEvmFunction('wallet_requestPermissions', async (requestedPermissions = { [ETH_ACCOUNTS_PERMISSION]: {} }) => {
320
+ const defaultRequestedPermissions = {
321
+ [ETH_ACCOUNTS_PERMISSION]: {},
322
+ };
323
+ exposeEvmFunction('wallet_requestPermissions', async (requestedPermissions = defaultRequestedPermissions) => {
287
324
  const parentCapability = Object.keys(requestedPermissions)[0];
288
325
  if (parentCapability !== ETH_ACCOUNTS_PERMISSION) {
289
326
  // `eth_accounts` is currently the only permission.
@@ -1,6 +1,6 @@
1
- export { default as createEVMDepsFactory } from './createEVMDeps.js';
2
1
  import type { Dependencies } from '../types.js';
3
2
  import type { RPC, Transport } from '@exodus/web3-types';
3
+ export { default as createEVMDepsFactory } from './createEVMDeps.js';
4
4
  export declare const registerRPCHandlers: ({ deps, RPC, transport, }: {
5
5
  deps: Dependencies;
6
6
  RPC: RPC;
@@ -1,5 +1,5 @@
1
1
  import { RPC_REQUEST_TIMEOUT } from '@exodus/web3-constants';
2
- import { validate as validateDeps, validateCommonDeps, } from '@exodus/web3-rpc-handlers';
2
+ import { validate as validateDeps, validateCommonDeps } from '@exodus/web3-rpc-handlers';
3
3
  import { exposeHandlers } from './exposeHandlers.js';
4
4
  import { validateRequestWithEVMPrefix } from './validateRequestWithEVMPrefix.js';
5
5
  export { default as createEVMDepsFactory } from './createEVMDeps.js';
@@ -18,11 +18,7 @@ const validateEVMDeps = (evm) => {
18
18
  'transactionSigner',
19
19
  'messageSigner',
20
20
  ];
21
- const optionalKeys = [
22
- 'getSupportedChainIds',
23
- 'onEthWalletWatchAssetRequest',
24
- 'addEthereumChain',
25
- ];
21
+ const optionalKeys = ['getSupportedChainIds', 'onEthWalletWatchAssetRequest', 'addEthereumChain'];
26
22
  validateDeps(evm, requiredKeys, optionalKeys);
27
23
  };
28
24
  function validateEVMsDeps(evms) {
@@ -2,7 +2,7 @@ import { InvalidInputError } from '@exodus/web3-errors';
2
2
  import { parseAndValidateTypedData as _parseAndValidateTypedData } from './validator.js';
3
3
  export function convertChainIdToHex(chainId) {
4
4
  if (typeof chainId !== 'string') {
5
- throw new Error(`'chainId' should be a string, got: ${typeof chainId}`);
5
+ throw new TypeError(`'chainId' should be a string, got: ${typeof chainId}`);
6
6
  }
7
7
  // `chainId` can be a stringified decimal
8
8
  if (!chainId.startsWith('0x')) {
@@ -17,7 +17,7 @@ export function parseAndValidateTypedData(typedData, { chainId: activeChainId })
17
17
  try {
18
18
  parsedTypedData = _parseAndValidateTypedData(typedData);
19
19
  }
20
- catch (e) {
20
+ catch {
21
21
  throw new InvalidInputError();
22
22
  }
23
23
  // Security: checking if the 'message' property includes fields that don't belong to the primary type.
@@ -26,15 +26,9 @@ export function parseAndValidateTypedData(typedData, { chainId: activeChainId })
26
26
  }
27
27
  const allowedFieldsSet = new Set(parsedTypedData.types[parsedTypedData.primaryType].map(({ name }) => name));
28
28
  // Handle cases where the message includes fields that doesn't belong to the primary type.
29
- const isFieldExempted = (field) => {
30
- // Handle OpenSea case. See: https://github.com/status-im/status-mobile/issues/13769
31
- if (field === 'totalOriginalConsiderationItems' &&
32
- parsedTypedData.primaryType === 'OrderComponents' &&
33
- parsedTypedData.message['consideration']) {
34
- return true;
35
- }
36
- return false;
37
- };
29
+ const isFieldExempted = (field) => field === 'totalOriginalConsiderationItems' &&
30
+ parsedTypedData.primaryType === 'OrderComponents' &&
31
+ Boolean(parsedTypedData.message['consideration']);
38
32
  for (const field in parsedTypedData.message) {
39
33
  if (!allowedFieldsSet.has(field) && !isFieldExempted(field)) {
40
34
  throw new InvalidInputError(`field '${field}' is not defined in the primary type definition`);