@exodus/web3-ethereum 13.6.3 → 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.
- package/lib/provider/provider.js +9 -7
- package/lib/provider/storage.js +3 -3
- package/lib/provider-proxy/proxy.js +2 -2
- package/lib/rpc-handlers/createEVMDeps.js +5 -10
- package/lib/rpc-handlers/exposeHandlers.js +46 -9
- package/lib/rpc-handlers/index.d.ts +1 -1
- package/lib/rpc-handlers/index.js +2 -6
- package/lib/rpc-handlers/parseAndValidateTypedData.js +9 -12
- package/lib/rpc-handlers/validator.js +2137 -2103
- package/lib/types.d.ts +1 -2
- package/package.json +12 -12
package/lib/provider/provider.js
CHANGED
|
@@ -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
|
-
|
|
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', [
|
|
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
|
});
|
package/lib/provider/storage.js
CHANGED
|
@@ -4,7 +4,7 @@ export function createStorageWrapper(storage, storageKeyPrefix) {
|
|
|
4
4
|
try {
|
|
5
5
|
return await storage.getItem(chainIdKey);
|
|
6
6
|
}
|
|
7
|
-
catch
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,23 +26,20 @@ 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
|
-
|
|
31
|
-
|
|
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`);
|
|
41
35
|
}
|
|
42
36
|
}
|
|
43
37
|
// Security: checking if the requested chain matches the active chain ID.
|
|
44
|
-
if (parsedTypedData?.domain?.chainId) {
|
|
38
|
+
if (parsedTypedData?.domain?.chainId != null) {
|
|
45
39
|
const suppliedChainIdHex = convertChainIdToHex(`${parsedTypedData.domain.chainId}`);
|
|
40
|
+
if (parseInt(suppliedChainIdHex, 16) === 0) {
|
|
41
|
+
throw new InvalidInputError(`eip-155 replay protection required`);
|
|
42
|
+
}
|
|
46
43
|
if (activeChainId.toLowerCase() !== suppliedChainIdHex) {
|
|
47
44
|
throw new InvalidInputError(`chainId supplied in typed data does not match active chainId`);
|
|
48
45
|
}
|