@exodus/web3-ethereum 1.0.0 → 1.2.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.
Files changed (33) hide show
  1. package/lib/index.d.ts +3 -2
  2. package/lib/index.js +3 -2
  3. package/lib/rpc-handlers/exposeHandlers.d.ts +4 -0
  4. package/lib/rpc-handlers/exposeHandlers.js +215 -0
  5. package/lib/rpc-handlers/index.d.ts +12 -0
  6. package/lib/rpc-handlers/index.js +48 -0
  7. package/lib/rpc-handlers/parseAndValidateTypedData.d.ts +5 -0
  8. package/lib/rpc-handlers/parseAndValidateTypedData.js +30 -0
  9. package/lib/rpc-handlers/validator.d.ts +1 -0
  10. package/lib/rpc-handlers/validator.js +3 -0
  11. package/lib/simulation/constant.d.ts +1 -5
  12. package/lib/simulation/constant.js +1 -32
  13. package/lib/simulation/index.d.ts +1 -16
  14. package/lib/simulation/index.js +1 -70
  15. package/lib/simulation/simulation.d.ts +16 -0
  16. package/lib/simulation/simulation.js +67 -0
  17. package/lib/simulation/types.d.ts +0 -4
  18. package/lib/simulation/utils.d.ts +0 -6
  19. package/lib/simulation/utils.js +0 -7
  20. package/lib/types.d.ts +28 -11
  21. package/lib/wallet-standard/icon.d.ts +2 -0
  22. package/lib/wallet-standard/icon.js +2 -0
  23. package/lib/wallet-standard/wallet.d.ts +17 -0
  24. package/lib/wallet-standard/wallet.js +35 -0
  25. package/package.json +7 -4
  26. package/lib/provider/wallet-standard/account.d.ts +0 -11
  27. package/lib/provider/wallet-standard/account.js +0 -36
  28. package/lib/provider/wallet-standard/chains.d.ts +0 -9
  29. package/lib/provider/wallet-standard/chains.js +0 -11
  30. package/lib/provider/wallet-standard/wallet.d.ts +0 -37
  31. package/lib/provider/wallet-standard/wallet.js +0 -123
  32. /package/lib/{provider/wallet-standard → wallet-standard}/index.d.ts +0 -0
  33. /package/lib/{provider/wallet-standard → wallet-standard}/index.js +0 -0
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export * from './provider/index.js';
2
- export * from './simulation/index.js';
1
+ export { EthereumProvider } from './provider/index.js';
2
+ export { simulateTransaction } from './simulation/index.js';
3
+ export { ExodusEthereumWallet } from './wallet-standard/index.js';
package/lib/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export * from './provider/index.js';
2
- export * from './simulation/index.js';
1
+ export { EthereumProvider } from './provider/index.js';
2
+ export { simulateTransaction } from './simulation/index.js';
3
+ export { ExodusEthereumWallet } from './wallet-standard/index.js';
@@ -0,0 +1,4 @@
1
+ import type RPC from '@exodus/json-rpc';
2
+ import type { AppDeps, WalletDeps } from '@exodus/web3-types';
3
+ import type { EVMDeps } from '../types.js';
4
+ export declare function exposeHandlers(rpc: typeof RPC, deps: AppDeps & EVMDeps & WalletDeps): void;
@@ -0,0 +1,215 @@
1
+ import { getTransactionId, signMessage, signTransaction, signTypedData, } from '@exodus/web3-ethereum-utils';
2
+ import { AssetNotSupportedError, InvalidInputError, UnauthorizedError, UserRejectedRequestError, } from '@exodus/web3-errors';
3
+ import { SignTypedDataVersion } from '@metamask/eth-sig-util';
4
+ import { parseAndValidateTypedData } from './parseAndValidateTypedData.js';
5
+ // See: https://eips.ethereum.org/EIPS/eip-2255.
6
+ const ETH_ACCOUNTS_PERMISSION = 'eth_accounts';
7
+ const PROXIED_METHODS = [
8
+ 'eth_blockNumber',
9
+ 'eth_call',
10
+ 'eth_coinbase',
11
+ 'eth_createAccessList',
12
+ 'eth_estimateGas',
13
+ 'eth_feeHistory',
14
+ 'eth_gasPrice',
15
+ 'eth_getBalance',
16
+ 'eth_getBlockByHash',
17
+ 'eth_getBlockByNumber',
18
+ 'eth_getBlockTransactionCountByHash',
19
+ 'eth_getBlockTransactionCountByNumber',
20
+ 'eth_getCode',
21
+ 'eth_getCompilers',
22
+ 'eth_getLogs',
23
+ 'eth_getProof',
24
+ 'eth_getStorageAt',
25
+ 'eth_getTransactionByBlockHashAndIndex',
26
+ 'eth_getTransactionByBlockNumberAndIndex',
27
+ 'eth_getTransactionByHash',
28
+ 'eth_getTransactionCount',
29
+ 'eth_getTransactionReceipt',
30
+ 'eth_getUncleByBlockHashAndIndex',
31
+ 'eth_getUncleByBlockNumberAndIndex',
32
+ 'eth_getUncleCountByBlockHash',
33
+ 'eth_getWork',
34
+ 'eth_hashrate',
35
+ 'eth_pendingTransactions',
36
+ 'eth_sendRawTransaction',
37
+ 'eth_submitWork',
38
+ 'net_version',
39
+ // TODO: Should we handle the response like MetaMask, e.g. "MetaMask/v10.14.7"?
40
+ 'web3_clientVersion',
41
+ ];
42
+ function typedDataToMessage(typedData,
43
+ // TODO: Remove eslint-disable comment when implementing.
44
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
45
+ version) {
46
+ // TODO: Return human-readable message from typed data.
47
+ return typedData.toString();
48
+ }
49
+ export function exposeHandlers(rpc, deps) {
50
+ const { chainId, isTrusted, isAutoApproved, ensureTrusted, approveConnection, approveMessage, approveTransactions, forwardRequest, getAddress, getPrivateKey, getEstimatedGas, getFeeData, getNonce, sendRawTransaction, onTransactionsSigned = () => { }, onEthWalletWatchAssetRequest = () => { }, isUnlocked = () => true, ensureUnlocked = () => true, } = deps;
51
+ const network = `evm:${chainId}`;
52
+ async function assertTrusted() {
53
+ const trusted = await isTrusted();
54
+ if (!trusted) {
55
+ throw new UnauthorizedError();
56
+ }
57
+ }
58
+ async function prepareTransaction(transaction) {
59
+ const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await getFeeData();
60
+ const preparedTransaction = { ...transaction };
61
+ preparedTransaction.nonce = await getNonce(transaction.from);
62
+ if (!transaction.value) {
63
+ preparedTransaction.value = '0x0';
64
+ }
65
+ if (!transaction.gasPrice) {
66
+ preparedTransaction.gasPrice = gasPrice;
67
+ }
68
+ if (!transaction.maxFeePerGas && maxFeePerGas) {
69
+ preparedTransaction.maxFeePerGas = maxFeePerGas;
70
+ }
71
+ if (!transaction.maxPriorityFeePerGas && maxPriorityFeePerGas) {
72
+ preparedTransaction.maxPriorityFeePerGas = maxPriorityFeePerGas;
73
+ }
74
+ if (!transaction.data) {
75
+ preparedTransaction.data = '0x';
76
+ }
77
+ // NOTE: Keep this at the end as other parameters can affect gas estimation.
78
+ if (!transaction.gas) {
79
+ preparedTransaction.gas = await getEstimatedGas(preparedTransaction);
80
+ }
81
+ return preparedTransaction;
82
+ }
83
+ function validateTransaction(transaction) {
84
+ const gasKeys = [
85
+ 'gas',
86
+ 'gasPrice',
87
+ 'maxFeePerGas',
88
+ 'maxPriorityFeePerGas',
89
+ ];
90
+ for (const key of gasKeys) {
91
+ if (transaction[key] === '0x') {
92
+ throw new InvalidInputError();
93
+ }
94
+ }
95
+ }
96
+ async function approve(approvalFn, ...args) {
97
+ const autoApproved = await isAutoApproved();
98
+ if (autoApproved) {
99
+ return;
100
+ }
101
+ const approved = await approvalFn(...args);
102
+ if (!approved) {
103
+ throw new UserRejectedRequestError();
104
+ }
105
+ }
106
+ async function approveAndSignTransaction(transaction) {
107
+ await assertTrusted();
108
+ const preparedTransaction = await prepareTransaction(transaction);
109
+ validateTransaction(preparedTransaction);
110
+ await approve(approveTransactions, network, [preparedTransaction]);
111
+ const privateKey = await getPrivateKey();
112
+ const signedTransaction = signTransaction({ ...preparedTransaction, chainId }, privateKey);
113
+ const isEIP1559Enabled = !!preparedTransaction.maxFeePerGas;
114
+ const transactionId = getTransactionId(signedTransaction, isEIP1559Enabled);
115
+ onTransactionsSigned(network, [transactionId]);
116
+ return `0x${signedTransaction.toString('hex')}`;
117
+ }
118
+ async function approveAndSignMessage(message) {
119
+ await assertTrusted();
120
+ await approve(approveMessage, network, message);
121
+ const privateKey = await getPrivateKey();
122
+ return signMessage(message, privateKey);
123
+ }
124
+ async function approveAndSignTypedData(typedData) {
125
+ await assertTrusted();
126
+ const parsedTypedData = parseAndValidateTypedData(typedData, { chainId });
127
+ const message = typedDataToMessage(typedData, SignTypedDataVersion.V4);
128
+ await approve(approveMessage, network, message);
129
+ const privateKey = await getPrivateKey();
130
+ return signTypedData(parsedTypedData, privateKey, SignTypedDataVersion.V4);
131
+ }
132
+ function exposeEvmFunction(name,
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ fn) {
135
+ return rpc.exposeFunction(`${chainId}_${name}`, fn);
136
+ }
137
+ PROXIED_METHODS.forEach((method) => {
138
+ exposeEvmFunction(method, async (...params) => {
139
+ // TODO: Do we need to check `assertTrusted` here?
140
+ // See: https://eips.ethereum.org/EIPS/eip-1193#connectivity.
141
+ // TODO: Check support for named params (i.e. object).
142
+ const args = { method, params };
143
+ return forwardRequest(args);
144
+ });
145
+ });
146
+ exposeEvmFunction('eth_accounts', async () => {
147
+ const unlocked = await isUnlocked();
148
+ if (!unlocked) {
149
+ return [];
150
+ }
151
+ const trusted = await isTrusted();
152
+ if (!trusted) {
153
+ return [];
154
+ }
155
+ const address = await getAddress();
156
+ return [address];
157
+ });
158
+ exposeEvmFunction('eth_signTransaction', approveAndSignTransaction);
159
+ exposeEvmFunction('eth_sendTransaction', async (transaction) => {
160
+ const rawTransaction = await approveAndSignTransaction(transaction);
161
+ return sendRawTransaction(rawTransaction);
162
+ });
163
+ exposeEvmFunction('personal_sign', approveAndSignMessage);
164
+ exposeEvmFunction('eth_sign', async (account, message) =>
165
+ // Should we really be using `personalSign` here?
166
+ // See: https://docs.metamask.io/guide/signing-data.html#a-brief-history.
167
+ approveAndSignMessage(message));
168
+ exposeEvmFunction('eth_signTypedData_v1', async (typedData) => {
169
+ await assertTrusted();
170
+ const message = typedDataToMessage(typedData, SignTypedDataVersion.V1);
171
+ await approve(approveMessage, network, message);
172
+ const privateKey = await getPrivateKey();
173
+ return signTypedData(typedData, privateKey, SignTypedDataVersion.V1);
174
+ });
175
+ exposeEvmFunction('eth_signTypedData_v3', async (account, typedData) => approveAndSignTypedData(typedData));
176
+ exposeEvmFunction('eth_signTypedData_v4', async (account, typedData) => approveAndSignTypedData(typedData));
177
+ exposeEvmFunction('eth_signTypedData', async (account, typedData) => approveAndSignTypedData(typedData));
178
+ exposeEvmFunction('wallet_getPermissions', async () => {
179
+ const permissions = [];
180
+ const trusted = await isTrusted();
181
+ if (trusted) {
182
+ permissions.push({ parentCapability: ETH_ACCOUNTS_PERMISSION });
183
+ }
184
+ return permissions;
185
+ });
186
+ exposeEvmFunction('wallet_requestPermissions', async (requestedPermissions) => {
187
+ const parentCapability = Object.keys(requestedPermissions)[0];
188
+ if (parentCapability !== ETH_ACCOUNTS_PERMISSION) {
189
+ // `eth_accounts` is currently the only permission.
190
+ // See: https://docs.metamask.io/guide/rpc-api.html#restricted-methods.
191
+ throw new InvalidInputError();
192
+ }
193
+ const unlocked = await ensureUnlocked();
194
+ if (!unlocked) {
195
+ throw new UserRejectedRequestError();
196
+ }
197
+ const trusted = await isTrusted();
198
+ if (!trusted) {
199
+ const approved = await approveConnection(network);
200
+ if (!approved) {
201
+ throw new UserRejectedRequestError();
202
+ }
203
+ await ensureTrusted();
204
+ }
205
+ return [{ parentCapability: ETH_ACCOUNTS_PERMISSION }];
206
+ });
207
+ exposeEvmFunction('wallet_watchAsset', async (assetDetails) => {
208
+ await assertTrusted();
209
+ const assetAdded = await onEthWalletWatchAssetRequest(assetDetails);
210
+ if (!assetAdded) {
211
+ throw new AssetNotSupportedError(assetDetails.type);
212
+ }
213
+ return true;
214
+ });
215
+ }
@@ -0,0 +1,12 @@
1
+ import type RPC from '@exodus/json-rpc';
2
+ import type { Dependencies } from '../types.js';
3
+ import type { Transport } from '@exodus/web3-types';
4
+ interface RpcConstructor {
5
+ new (...options: unknown[]): typeof RPC;
6
+ }
7
+ export declare const registerRPCHandlers: ({ deps, RPC, transport, }: {
8
+ deps: Dependencies;
9
+ RPC: RpcConstructor;
10
+ transport: Transport;
11
+ }) => any;
12
+ export {};
@@ -0,0 +1,48 @@
1
+ import { validate as validateDeps, validateCommonDeps, } from '@exodus/web3-rpc-handlers';
2
+ import { RPC_REQUEST_TIMEOUT } from '@exodus/web3-constants';
3
+ import { exposeHandlers } from './exposeHandlers.js';
4
+ import { validateRequest } from './validator.js';
5
+ const validateEVMDeps = (evm) => {
6
+ const requiredKeys = [
7
+ 'chainId',
8
+ 'forwardRequest',
9
+ 'getAddress',
10
+ 'getPrivateKey',
11
+ 'getEstimatedGas',
12
+ 'getFeeData',
13
+ 'getNonce',
14
+ 'sendRawTransaction',
15
+ ];
16
+ const optionalKeys = ['onEthWalletWatchAssetRequest'];
17
+ validateDeps(evm, requiredKeys, optionalKeys);
18
+ };
19
+ function validateEVMsDeps(evms) {
20
+ const chainIds = evms.map(({ chainId }) => chainId);
21
+ const uniqueChainIds = [...new Set(chainIds)];
22
+ if (chainIds.length !== uniqueChainIds.length) {
23
+ throw new Error('EVMs must have unique chain IDs');
24
+ }
25
+ evms.forEach(validateEVMDeps);
26
+ }
27
+ export const registerRPCHandlers = ({ deps, RPC, transport, }) => {
28
+ validateCommonDeps(deps, ['evms']);
29
+ validateEVMsDeps(deps.evms);
30
+ if (!RPC) {
31
+ throw new Error('"RPC" constructor parameter is required');
32
+ }
33
+ if (!transport) {
34
+ throw new Error('"transport" parameter is required');
35
+ }
36
+ const rpc = new RPC({
37
+ transport,
38
+ parse: validateRequest,
39
+ getIsDevelopmentMode: () => app.isDevelopmentEnvironment?.() ?? false,
40
+ requestTimeout: RPC_REQUEST_TIMEOUT,
41
+ });
42
+ const { app, evms = [], wallet } = deps;
43
+ for (const evm of evms) {
44
+ const evmDeps = { ...app, ...evm, ...wallet };
45
+ exposeHandlers(rpc, evmDeps);
46
+ }
47
+ return rpc;
48
+ };
@@ -0,0 +1,5 @@
1
+ import type { MessageTypes, TypedDataV1, TypedMessage } from '@metamask/eth-sig-util';
2
+ export declare function convertChainIdToHex(chainId: string): string;
3
+ export declare function parseAndValidateTypedData(typedData: string, { chainId: activeChainId }: {
4
+ chainId: string;
5
+ }): TypedDataV1 | TypedMessage<MessageTypes> | never;
@@ -0,0 +1,30 @@
1
+ import { validateAndParseEthereumTypedData } from '@exodus/web3-validation';
2
+ import { InvalidInputError } from '@exodus/web3-errors';
3
+ export function convertChainIdToHex(chainId) {
4
+ if (typeof chainId !== 'string') {
5
+ throw new Error(`'chainId' should be a string, got: ${typeof chainId}`);
6
+ }
7
+ // `chainId` can be a stringified decimal
8
+ if (!chainId.startsWith('0x')) {
9
+ return '0x' + parseInt(chainId, 10).toString(16).toLowerCase();
10
+ }
11
+ return chainId.toLowerCase();
12
+ }
13
+ // Returns the parsed typed data object if it is valid
14
+ // Otherwise throws the InvalidInputError
15
+ export function parseAndValidateTypedData(typedData, { chainId: activeChainId }) {
16
+ let parsedTypedData;
17
+ try {
18
+ parsedTypedData = validateAndParseEthereumTypedData(typedData);
19
+ }
20
+ catch (e) {
21
+ throw new InvalidInputError();
22
+ }
23
+ if (parsedTypedData.domain?.chainId) {
24
+ const suppliedChainIdHex = convertChainIdToHex(`${parsedTypedData.domain.chainId}`);
25
+ if (activeChainId.toLowerCase() !== suppliedChainIdHex) {
26
+ throw new InvalidInputError();
27
+ }
28
+ }
29
+ return parsedTypedData;
30
+ }
@@ -0,0 +1 @@
1
+ export declare const validateRequest: () => never;
@@ -0,0 +1,3 @@
1
+ export const validateRequest = () => {
2
+ throw new Error('Should be replaced with the generated code');
3
+ };
@@ -1,5 +1 @@
1
- import type { EVMConstants } from './types.js';
2
- export declare const CHAIN_IDS: {
3
- [key: string]: number;
4
- };
5
- export declare const ENABLED_EVMS: EVMConstants[];
1
+ export declare const SIMULATION_SUPPORTED_ASSET_NAMES: string[];
@@ -1,32 +1 @@
1
- export const CHAIN_IDS = {
2
- avalanchec: 43114,
3
- bsc: 56,
4
- ethereum: 1,
5
- ethereumclassic: 61,
6
- ethereumgoerli: 5,
7
- fantommainnet: 250,
8
- harmonymainnet: 1666600000,
9
- matic: 137,
10
- };
11
- export const ENABLED_EVMS = [
12
- {
13
- assetName: 'ethereum',
14
- shouldSimulate: true,
15
- },
16
- {
17
- assetName: 'matic',
18
- shouldSimulate: false,
19
- },
20
- {
21
- assetName: 'bsc',
22
- shouldSimulate: false,
23
- },
24
- {
25
- assetName: 'avalanchec',
26
- shouldSimulate: false,
27
- },
28
- {
29
- assetName: 'fantommainnet',
30
- shouldSimulate: false,
31
- },
32
- ];
1
+ export const SIMULATION_SUPPORTED_ASSET_NAMES = ['ethereum']; // just ethereum for now
@@ -1,16 +1 @@
1
- import type { EthTransaction } from '@exodus/web3-ethereum-utils';
2
- import type { Asset, EthBalanceChanges } from './types.js';
3
- export declare const simulateTransaction: (assets: Record<string, Asset>, transaction: EthTransaction, network: string) => Promise<{
4
- assetName: string;
5
- advancedDetails: never[];
6
- balanceChanges: EthBalanceChanges;
7
- displayInfo: {
8
- title: string;
9
- };
10
- txFee: unknown;
11
- txFeeDetails: {
12
- gasLimit: number;
13
- maxFeePerGas: number | undefined;
14
- gasPrice: number;
15
- };
16
- }>;
1
+ export { simulateTransaction } from './simulation.js';
@@ -1,70 +1 @@
1
- import { retrieveSideEffects } from '@exodus/ethereum-api';
2
- import { createCurrency, formatAccounts, getEthereumDisplayInfo, supportedNetworkEvms, hexToBN, isEthTransferTx, } from './utils.js';
3
- const computeBalanceChanges = async (asset, shouldSimulate, transaction) => {
4
- const assetName = asset.name;
5
- let balanceChanges = {
6
- willReceive: [],
7
- willSend: [],
8
- willApprove: [],
9
- };
10
- if (isEthTransferTx(transaction)) {
11
- const txValue = hexToBN(transaction.value).toString();
12
- balanceChanges.willSend.push({
13
- balance: createCurrency({
14
- amount: txValue,
15
- denominator: asset.currency.defaultUnit.power,
16
- symbol: asset.displayTicker,
17
- }),
18
- });
19
- }
20
- else {
21
- try {
22
- const sideEffect = await retrieveSideEffects({
23
- assetName,
24
- shouldSimulate,
25
- transaction,
26
- });
27
- if (sideEffect) {
28
- balanceChanges = formatAccounts(sideEffect);
29
- }
30
- }
31
- catch (error) {
32
- console.error('Ethereum transaction simulation failed.', error);
33
- }
34
- }
35
- return balanceChanges;
36
- };
37
- const getTxFeeDetails = (asset, transaction) => {
38
- const maxFeePerGasBn = transaction.maxFeePerGas
39
- ? hexToBN(transaction.maxFeePerGas)
40
- : undefined;
41
- const gasPriceBn = hexToBN(transaction.gasPrice);
42
- const gasBn = hexToBN(transaction.gas);
43
- return {
44
- txFee: asset.currency.baseUnit(gasBn.mul(maxFeePerGasBn || gasPriceBn).toString()),
45
- txFeeDetails: {
46
- gasLimit: gasBn.toNumber(),
47
- maxFeePerGas: maxFeePerGasBn?.toNumber(),
48
- gasPrice: gasPriceBn.toNumber(),
49
- },
50
- };
51
- };
52
- export const simulateTransaction = async (assets, transaction, network) => {
53
- const evm = supportedNetworkEvms.get(network);
54
- if (!evm) {
55
- throw new Error(`unsupported network, ${network}`);
56
- }
57
- const { assetName, shouldSimulate } = evm;
58
- const asset = assets[assetName];
59
- const balanceChanges = await computeBalanceChanges(asset, shouldSimulate, transaction);
60
- const { txFee, txFeeDetails } = getTxFeeDetails(asset, transaction);
61
- const displayInfo = getEthereumDisplayInfo(balanceChanges);
62
- return {
63
- assetName,
64
- advancedDetails: [],
65
- balanceChanges,
66
- displayInfo,
67
- txFee,
68
- txFeeDetails,
69
- };
70
- };
1
+ export { simulateTransaction } from './simulation.js';
@@ -0,0 +1,16 @@
1
+ import type { EthTransaction } from '@exodus/web3-ethereum-utils';
2
+ import type { Asset, EthBalanceChanges } from './types.js';
3
+ export declare const simulateTransaction: (asset: Asset, transaction: EthTransaction) => Promise<{
4
+ assetName: string;
5
+ advancedDetails: never[];
6
+ balanceChanges: EthBalanceChanges;
7
+ displayInfo: {
8
+ title: string;
9
+ };
10
+ txFee: unknown;
11
+ txFeeDetails: {
12
+ gasLimit: number;
13
+ maxFeePerGas: number | undefined;
14
+ gasPrice: number;
15
+ };
16
+ }>;
@@ -0,0 +1,67 @@
1
+ import { retrieveSideEffects } from '@exodus/ethereum-api';
2
+ import { SIMULATION_SUPPORTED_ASSET_NAMES } from './constant.js';
3
+ import { createCurrency, formatAccounts, getEthereumDisplayInfo, hexToBN, isEthTransferTx, } from './utils.js';
4
+ const computeBalanceChanges = async (asset, shouldSimulate, transaction) => {
5
+ const assetName = asset.name;
6
+ let balanceChanges = {
7
+ willReceive: [],
8
+ willSend: [],
9
+ willApprove: [],
10
+ };
11
+ if (isEthTransferTx(transaction)) {
12
+ const txValue = hexToBN(transaction.value).toString();
13
+ balanceChanges.willSend.push({
14
+ balance: createCurrency({
15
+ amount: txValue,
16
+ denominator: asset.currency.defaultUnit.power,
17
+ symbol: asset.displayTicker,
18
+ }),
19
+ });
20
+ }
21
+ else {
22
+ try {
23
+ const sideEffect = await retrieveSideEffects({
24
+ assetName,
25
+ shouldSimulate,
26
+ transaction,
27
+ });
28
+ if (sideEffect) {
29
+ balanceChanges = formatAccounts(sideEffect);
30
+ }
31
+ }
32
+ catch (error) {
33
+ console.error('Ethereum transaction simulation failed.', error);
34
+ }
35
+ }
36
+ return balanceChanges;
37
+ };
38
+ const getTxFeeDetails = (asset, transaction) => {
39
+ const maxFeePerGasBn = transaction.maxFeePerGas
40
+ ? hexToBN(transaction.maxFeePerGas)
41
+ : undefined;
42
+ const gasPriceBn = hexToBN(transaction.gasPrice);
43
+ const gasBn = hexToBN(transaction.gas);
44
+ return {
45
+ txFee: asset.currency.baseUnit(gasBn.mul(maxFeePerGasBn || gasPriceBn).toString()),
46
+ txFeeDetails: {
47
+ gasLimit: gasBn.toNumber(),
48
+ maxFeePerGas: maxFeePerGasBn?.toNumber(),
49
+ gasPrice: gasPriceBn.toNumber(),
50
+ },
51
+ };
52
+ };
53
+ export const simulateTransaction = async (asset, transaction) => {
54
+ const assetName = asset.name;
55
+ const shouldSimulate = SIMULATION_SUPPORTED_ASSET_NAMES.includes(assetName);
56
+ const balanceChanges = await computeBalanceChanges(asset, shouldSimulate, transaction);
57
+ const { txFee, txFeeDetails } = getTxFeeDetails(asset, transaction);
58
+ const displayInfo = getEthereumDisplayInfo(balanceChanges);
59
+ return {
60
+ assetName,
61
+ advancedDetails: [],
62
+ balanceChanges,
63
+ displayInfo,
64
+ txFee,
65
+ txFeeDetails,
66
+ };
67
+ };
@@ -1,7 +1,3 @@
1
- export interface EVMConstants {
2
- assetName: string;
3
- shouldSimulate: boolean;
4
- }
5
1
  export interface CurrencyParams {
6
2
  amount: string;
7
3
  symbol: string;
@@ -2,12 +2,6 @@ import BN from 'bn.js';
2
2
  import type { CurrencyParams, EthBalanceChange, EthBalanceChanges } from './types.js';
3
3
  import type { EthTransaction } from '@exodus/web3-ethereum-utils';
4
4
  export declare const hexToBN: (hexValue?: string) => BN;
5
- export declare const supportedNetworkEvms: Map<string, {
6
- chainId: string;
7
- network: string;
8
- assetName: string;
9
- shouldSimulate: boolean;
10
- }>;
11
5
  export declare const isEthTransferTx: (transaction: EthTransaction) => boolean;
12
6
  export declare function getEthereumDisplayInfo(balanceChanges: EthBalanceChanges): {
13
7
  title: string;
@@ -1,13 +1,6 @@
1
1
  import { UnitType } from '@exodus/currency';
2
2
  import BN from 'bn.js';
3
- import { CHAIN_IDS, ENABLED_EVMS } from './constant.js';
4
3
  export const hexToBN = (hexValue) => new BN((hexValue || '').slice(2), 'hex');
5
- const getEvmNetwork = (chainId) => `evm:${chainId}`;
6
- export const supportedNetworkEvms = new Map(ENABLED_EVMS.map((constant) => {
7
- const chainId = '0x' + CHAIN_IDS[constant.assetName].toString(16);
8
- const network = getEvmNetwork(chainId);
9
- return [network, { ...constant, chainId, network }];
10
- }));
11
4
  export const isEthTransferTx = (transaction) => {
12
5
  return (['0x', null, undefined].includes(transaction.data) &&
13
6
  [undefined, '0x5208'].includes(transaction.gas));
package/lib/types.d.ts CHANGED
@@ -1,13 +1,30 @@
1
- import type { EthereumProvider } from './provider/provider.js';
2
- export interface WalletStandardAccountParams {
3
- address: string;
4
- chains?: ChainId[];
5
- publicKey?: string;
6
- label?: string;
7
- icon?: string;
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { EthTransaction, EthTransactionHash, EthWalletWatchAsset } from '@exodus/web3-ethereum-utils';
3
+ import type { CommonDeps } from '@exodus/web3-rpc-handlers';
4
+ /**
5
+ * EVM-compatible chains.
6
+ */
7
+ export interface EthRequestArguments {
8
+ method: string;
9
+ params?: unknown[] | Record<string, unknown>;
8
10
  }
9
- export interface ExodusProvider {
10
- ethereum: EthereumProvider;
11
+ interface FeeData {
12
+ gasPrice: string;
13
+ maxFeePerGas?: string;
14
+ maxPriorityFeePerGas?: string;
11
15
  }
12
- export declare type ChainId = string;
13
- export declare type UnknownFunction = (...args: unknown[]) => unknown;
16
+ export declare type EVMDeps = {
17
+ chainId: string;
18
+ getAddress: () => Promise<string>;
19
+ getPrivateKey: () => Promise<Buffer>;
20
+ sendRawTransaction: (rawTransaction: string) => Promise<EthTransactionHash>;
21
+ getEstimatedGas: (transaction: EthTransaction) => Promise<string>;
22
+ getFeeData: () => Promise<FeeData>;
23
+ getNonce: (account: string) => Promise<string>;
24
+ forwardRequest: (args: EthRequestArguments) => Promise<unknown>;
25
+ onEthWalletWatchAssetRequest?: (assetDetails: EthWalletWatchAsset) => Promise<boolean>;
26
+ };
27
+ export declare type Dependencies = {
28
+ evms: EVMDeps[];
29
+ } & CommonDeps;
30
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { WalletIcon } from '@wallet-standard/base';
2
+ export declare const icon: WalletIcon;
@@ -0,0 +1,2 @@
1
+ import { EXODUS_ICON } from '@exodus/web3-constants';
2
+ export const icon = EXODUS_ICON;
@@ -0,0 +1,17 @@
1
+ import type { Wallet } from '@wallet-standard/base';
2
+ import type { EthereumProvider } from '../provider/provider.js';
3
+ export declare type EthereumProviderFeature = {
4
+ 'ethereum:provider': {
5
+ provider: EthereumProvider;
6
+ };
7
+ };
8
+ export declare class ExodusEthereumWallet implements Wallet {
9
+ #private;
10
+ get version(): "1.0.0";
11
+ get name(): "Exodus";
12
+ get icon(): `data:image/svg+xml;base64,${string}` | `data:image/webp;base64,${string}` | `data:image/png;base64,${string}` | `data:image/gif;base64,${string}`;
13
+ get chains(): never[];
14
+ get features(): EthereumProviderFeature;
15
+ get accounts(): never[];
16
+ constructor(ethereumProvider: EthereumProvider);
17
+ }
@@ -0,0 +1,35 @@
1
+ import { icon } from './icon.js';
2
+ export class ExodusEthereumWallet {
3
+ #version = '1.0.0';
4
+ #name = 'Exodus';
5
+ #icon = icon;
6
+ #provider = null;
7
+ get version() {
8
+ return this.#version;
9
+ }
10
+ get name() {
11
+ return this.#name;
12
+ }
13
+ get icon() {
14
+ return this.#icon;
15
+ }
16
+ get chains() {
17
+ return [];
18
+ }
19
+ get features() {
20
+ return {
21
+ 'ethereum:provider': {
22
+ provider: this.#provider,
23
+ },
24
+ };
25
+ }
26
+ get accounts() {
27
+ return [];
28
+ }
29
+ constructor(ethereumProvider) {
30
+ if (!ethereumProvider) {
31
+ throw new Error('Ethereum Provider is required!');
32
+ }
33
+ this.#provider = ethereumProvider;
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/web3-ethereum",
3
3
  "description": "Apps connectivity library for EVM-compatible chains.",
4
- "version": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "module": "lib/index.js",
@@ -12,7 +12,7 @@
12
12
  "./provider": "./lib/provider/index.js",
13
13
  "./rpc": "./lib/rpc-handlers/index.js",
14
14
  "./simulation": "./lib/simulation/index.js",
15
- "./utils": "./lib/rpc-handlers/index.js"
15
+ "./wallet-standard": "./lib/wallet-standard/index.js"
16
16
  },
17
17
  "react-native": "./lib/index.js",
18
18
  "files": [
@@ -39,11 +39,14 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@exodus/assets": "^8.0.72",
42
+ "@exodus/json-rpc": "^1.11.0",
42
43
  "@exodus/schemasafe": "^1.0.0-rc.7",
43
- "@exodus/web3-types": "^1.6.0"
44
+ "@exodus/web3-types": "^1.6.0",
45
+ "@exodus/web3-validation": "^1.26.0",
46
+ "@wallet-standard/base": "^1.0.1"
44
47
  },
45
48
  "peerDependencies": {
46
49
  "@exodus/assets": "^8.0.72"
47
50
  },
48
- "gitHead": "c220026347ce27cfc25bacc083d61b1aae5c876b"
51
+ "gitHead": "39137384d78bfa3e008b92e47e106a7be1df071e"
49
52
  }
@@ -1,11 +0,0 @@
1
- import type { WalletStandardAccountParams } from '../../types.js';
2
- export declare class ExodusEthereumWalletAccount {
3
- #private;
4
- get address(): string;
5
- get publicKey(): string | undefined;
6
- get chains(): string[] | undefined;
7
- get features(): string[];
8
- get label(): string | undefined;
9
- get icon(): string | undefined;
10
- constructor({ address, publicKey, label, icon, chains, }: WalletStandardAccountParams);
11
- }
@@ -1,36 +0,0 @@
1
- // This file is copied with modification from @wallet-standard/util
2
- const features = ['ethereum:request'];
3
- export class ExodusEthereumWalletAccount {
4
- #address;
5
- #publicKey;
6
- #chains;
7
- #features;
8
- #label;
9
- #icon;
10
- get address() {
11
- return this.#address;
12
- }
13
- get publicKey() {
14
- return this.#publicKey?.slice();
15
- }
16
- get chains() {
17
- return this.#chains?.slice();
18
- }
19
- get features() {
20
- return this.#features?.slice();
21
- }
22
- get label() {
23
- return this.#label;
24
- }
25
- get icon() {
26
- return this.#icon;
27
- }
28
- constructor({ address, publicKey, label, icon, chains, }) {
29
- this.#address = address;
30
- this.#publicKey = publicKey;
31
- this.#chains = chains;
32
- this.#features = features;
33
- this.#label = label;
34
- this.#icon = icon;
35
- }
36
- }
@@ -1,9 +0,0 @@
1
- import type { ChainId } from '../../types.js';
2
- /** Ethereum Mainnet, e.g. https://ethereum.publicnode.com */
3
- export declare const ETHEREUM_MAINNET = "ethereum:1";
4
- /** Array of Ethereum chains supported by Exodus */
5
- export declare const ETHEREUM_CHAINS: string[];
6
- /**
7
- * Converts a hexadecimal chain ID to a [human-readable format](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md)
8
- */
9
- export declare function convertChainIdToCanonicalBlockchainIdentifier(chainId: ChainId): string;
@@ -1,11 +0,0 @@
1
- /** Ethereum Mainnet, e.g. https://ethereum.publicnode.com */
2
- export const ETHEREUM_MAINNET = 'ethereum:1';
3
- /** Array of Ethereum chains supported by Exodus */
4
- export const ETHEREUM_CHAINS = [ETHEREUM_MAINNET];
5
- /**
6
- * Converts a hexadecimal chain ID to a [human-readable format](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md)
7
- */
8
- export function convertChainIdToCanonicalBlockchainIdentifier(chainId) {
9
- chainId = chainId.replace('0x', '');
10
- return `ethereum:${chainId}`;
11
- }
@@ -1,37 +0,0 @@
1
- import { ExodusEthereumWalletAccount } from './account.js';
2
- import type { ChainId, ExodusProvider, UnknownFunction } from '../../types.js';
3
- import type { EthereumProvider } from '../provider.js';
4
- export declare class ExodusEthereumWallet {
5
- #private;
6
- get version(): string;
7
- get name(): string;
8
- get icon(): string;
9
- get chains(): string[];
10
- get features(): {
11
- 'standard:connect': {
12
- version: string;
13
- connect: ({ silent }?: {
14
- silent: boolean;
15
- }) => Promise<{
16
- accounts: ExodusEthereumWalletAccount[];
17
- }>;
18
- };
19
- 'standard:disconnect': {
20
- version: string;
21
- disconnect: () => Promise<void>;
22
- };
23
- 'standard:events': {
24
- version: string;
25
- on: (event: string, listener: UnknownFunction) => () => void;
26
- };
27
- 'ethereum:provider': {
28
- version: string;
29
- provider: EthereumProvider | null;
30
- };
31
- 'exodus:': {
32
- exodus: ExodusProvider | null;
33
- };
34
- };
35
- get accounts(): ExodusEthereumWalletAccount[];
36
- constructor(exodusProvider: ExodusProvider, supportedChainIds: ChainId[]);
37
- }
@@ -1,123 +0,0 @@
1
- import { EXODUS_ICON } from '@exodus/web3-constants';
2
- import { ExodusEthereumWalletAccount } from './account.js';
3
- import { convertChainIdToCanonicalBlockchainIdentifier } from './chains.js';
4
- export class ExodusEthereumWallet {
5
- #exodus = null;
6
- #provider = null;
7
- #listeners = {};
8
- #version = '1.0.0';
9
- #name = 'Exodus';
10
- #icon = EXODUS_ICON;
11
- #account = null;
12
- #chains;
13
- get version() {
14
- return this.#version;
15
- }
16
- get name() {
17
- return this.#name;
18
- }
19
- get icon() {
20
- return this.#icon;
21
- }
22
- get chains() {
23
- return this.#chains?.slice() ?? [];
24
- }
25
- get features() {
26
- return {
27
- 'standard:connect': {
28
- version: '1.0.0',
29
- connect: this.#connect,
30
- },
31
- 'standard:disconnect': {
32
- version: '1.0.0',
33
- disconnect: this.#disconnect,
34
- },
35
- 'standard:events': {
36
- version: '1.0.0',
37
- on: this.#on,
38
- },
39
- 'ethereum:provider': {
40
- version: '1.0.0',
41
- provider: this.#provider,
42
- },
43
- 'exodus:': {
44
- exodus: this.#exodus,
45
- },
46
- };
47
- }
48
- get accounts() {
49
- return this.#account ? [this.#account] : [];
50
- }
51
- constructor(exodusProvider, supportedChainIds) {
52
- if (!exodusProvider) {
53
- throw new Error('Exodus Provider is required!');
54
- }
55
- if (!exodusProvider.ethereum) {
56
- throw new Error('Exodus Provider must include Ethereum Provider!');
57
- }
58
- if (!supportedChainIds?.length) {
59
- throw new Error('At least one supported chain must be provided!');
60
- }
61
- this.#exodus = exodusProvider;
62
- this.#provider = exodusProvider.ethereum;
63
- this.#chains = supportedChainIds.map(convertChainIdToCanonicalBlockchainIdentifier);
64
- this.#provider.on('accountsChanged', this.#reconnected, this);
65
- this.#connected();
66
- }
67
- #on = (event, listener) => {
68
- this.#listeners[event]?.push(listener) ||
69
- (this.#listeners[event] = [listener]);
70
- return () => this.#off(event, listener);
71
- };
72
- #emit = (event, ...args) => {
73
- // eslint-disable-next-line no-unused-expressions,prefer-spread
74
- this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
75
- };
76
- #off = (event, listener) => {
77
- this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener);
78
- };
79
- #connected = async () => {
80
- const address = await this.#getAddress();
81
- if (address) {
82
- const account = this.#account;
83
- if (!account || account.address !== address) {
84
- this.#account = new ExodusEthereumWalletAccount({ address });
85
- this.#emit('change', { accounts: this.accounts });
86
- }
87
- }
88
- };
89
- #disconnected = () => {
90
- if (this.#account) {
91
- this.#account = null;
92
- this.#emit('change', { accounts: this.accounts });
93
- }
94
- };
95
- #reconnected = async () => {
96
- const address = await this.#getAddress();
97
- if (address) {
98
- this.#connected();
99
- }
100
- else {
101
- this.#disconnected();
102
- }
103
- };
104
- #connect = async ({ silent } = { silent: false }) => {
105
- if (!silent) {
106
- await this.#requestAddress();
107
- }
108
- await this.#connected();
109
- return { accounts: this.accounts };
110
- };
111
- #disconnect = async () => {
112
- this.#disconnected();
113
- };
114
- #requestAddress = () => {
115
- return this.#provider.request({ method: 'eth_requestAccounts' });
116
- };
117
- #getAddress = async () => {
118
- const [address] = (await this.#provider.request({
119
- method: 'eth_accounts',
120
- }));
121
- return address;
122
- };
123
- }