@exodus/ethereum-api 8.76.6 → 8.77.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 (38) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +2 -3
  3. package/src/create-asset-plugin-factory.js +1 -0
  4. package/src/create-asset-utils.js +39 -35
  5. package/src/create-asset.js +32 -14
  6. package/src/create-token-factory.js +3 -0
  7. package/src/exodus-eth-server/api-coin-nodes.js +11 -84
  8. package/src/exodus-eth-server/clarity-v2.js +30 -51
  9. package/src/exodus-eth-server/clarity.js +2 -115
  10. package/src/exodus-eth-server/errors.js +5 -1
  11. package/src/exodus-eth-server/eth-like-server-base.js +123 -0
  12. package/src/exodus-eth-server/fetch-json.js +48 -0
  13. package/src/gas-estimation.js +19 -4
  14. package/src/get-balances.js +14 -0
  15. package/src/get-historical-balance.js +58 -0
  16. package/src/index.js +2 -0
  17. package/src/multicall3/index.js +169 -0
  18. package/src/simulation/common.js +34 -0
  19. package/src/simulation/create-simulate-message.js +49 -0
  20. package/src/simulation/create-simulate-transactions.js +106 -0
  21. package/src/simulation/estimate-fee.js +14 -0
  22. package/src/simulation/estimate-simple-transfer.js +15 -0
  23. package/src/simulation/get-message-type.js +18 -0
  24. package/src/simulation/simulate-message-api.js +68 -0
  25. package/src/simulation/simulate-transactions-api.js +265 -0
  26. package/src/simulation/simulate-transactions.js +16 -0
  27. package/src/simulation/transactions.js +52 -0
  28. package/src/simulation/try-estimating-changes-locally.js +26 -0
  29. package/src/staking/ethereum/staking-utils.js +3 -1
  30. package/src/staking/matic/matic-staking-utils.js +3 -1
  31. package/src/tx-log/clarity-truncated-history-monitor.js +34 -0
  32. package/src/tx-log/clarity-utils/absolute.js +5 -1
  33. package/src/tx-log/ethereum-no-history-monitor.js +2 -23
  34. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +28 -0
  35. package/src/tx-send/broadcast-error-handler.js +7 -2
  36. package/src/tx-send/tx-send.js +1 -0
  37. package/src/web3/createSimulateMessage.js +2 -1
  38. package/src/web3/createSimulateTransactions.js +3 -9
@@ -0,0 +1,49 @@
1
+ import { tryParsingSIWE } from '@exodus/web3-utils'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ import { getMessageType, MessageTypeEnum } from './get-message-type.js'
5
+ import { simulateMessageApi } from './simulate-message-api.js'
6
+
7
+ export const createSimulateMessage =
8
+ ({
9
+ apiEndpoint = 'https://simulation.a.exodus.io/simulateMessage',
10
+ headers = {
11
+ 'X-Api-Version': '2023-06-05',
12
+ },
13
+ } = {}) =>
14
+ async ({ message, url, asset, address }) => {
15
+ assert(url instanceof URL, "'url' should be an instance of the URL object.")
16
+
17
+ const simulationResult = {
18
+ baseAssetName: asset.baseAssetName,
19
+ action: 'NONE',
20
+ }
21
+
22
+ const messageType = getMessageType(message)
23
+ if (messageType === MessageTypeEnum.Unknown) {
24
+ return simulationResult
25
+ }
26
+
27
+ if (messageType === MessageTypeEnum.RawMessage) {
28
+ tryParsingSIWE({
29
+ address,
30
+ message,
31
+ url,
32
+ simulationResult,
33
+ })
34
+ }
35
+
36
+ await simulateMessageApi({
37
+ address,
38
+ message: {
39
+ message,
40
+ messageType,
41
+ },
42
+ url,
43
+ apiEndpoint,
44
+ headers,
45
+ simulationResult,
46
+ })
47
+
48
+ return simulationResult
49
+ }
@@ -0,0 +1,106 @@
1
+ import { createEmptySimulationResult } from '@exodus/web3-utils'
2
+
3
+ import { estimateFee } from './estimate-fee.js'
4
+ import { simulateTransactionsApi } from './simulate-transactions-api.js'
5
+ import { decodeRecipientAddresses, getDisplayDetails, getTxFeeDetails } from './transactions.js'
6
+ import { tryEstimatingChangesLocally } from './try-estimating-changes-locally.js'
7
+
8
+ export const createSimulateTransactions =
9
+ ({ apiEndpoint, headers: headersFromDeps }) =>
10
+ async ({
11
+ transactions,
12
+ asset,
13
+ origin,
14
+ blockNumber,
15
+ headers: headersFromParams,
16
+ overrideApiEndpoint,
17
+ }) => {
18
+ const headers = { ...headersFromDeps, ...headersFromParams }
19
+
20
+ const simulationResult = createEmptySimulationResult({
21
+ asset,
22
+ })
23
+
24
+ if (transactions.length === 0) {
25
+ const message = 'No transactions provided. Cannot simulate transaction batch.'
26
+ simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
27
+ simulationResult.metadata.humanReadableError = message
28
+ return simulationResult
29
+ }
30
+
31
+ const firstFrom = transactions[0].from.toLowerCase()
32
+ const hasMismatchedFrom = transactions.some((tx) => tx.from.toLowerCase() !== firstFrom)
33
+ if (hasMismatchedFrom) {
34
+ const message =
35
+ 'All transactions in a batch must have the same "from" address. Cannot simulate transaction batch.'
36
+ simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
37
+ simulationResult.metadata.humanReadableError = message
38
+ return simulationResult
39
+ }
40
+
41
+ const hasInvalidTransaction = transactions.some((tx) => !tx.to)
42
+ if (hasInvalidTransaction) {
43
+ const message =
44
+ 'One or more transactions are missing required "to" address. Cannot simulate transaction batch.'
45
+ simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
46
+ simulationResult.metadata.humanReadableError = message
47
+ return simulationResult
48
+ }
49
+
50
+ simulationResult.recipientAddresses = transactions.map((tx) => decodeRecipientAddresses(tx))
51
+ const feeCurrency = asset.feeAsset.currency
52
+
53
+ const simulationTransactionInternalResult = await simulateTransactionsApi({
54
+ asset,
55
+ transactions,
56
+ apiEndpoint: overrideApiEndpoint || apiEndpoint,
57
+ origin,
58
+ blockNumber,
59
+ simulationResult,
60
+ headers,
61
+ })
62
+
63
+ if (simulationTransactionInternalResult.kind === 'error') {
64
+ if (transactions.length > 1) {
65
+ const message = 'Cannot locally simulate batched transactions.'
66
+ simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
67
+ simulationResult.metadata.humanReadableError = message
68
+ } else {
69
+ const transaction = transactions[0]
70
+ const simulatedLocally = tryEstimatingChangesLocally({
71
+ asset,
72
+ simulationResult,
73
+ transaction,
74
+ })
75
+ if (simulatedLocally) {
76
+ simulationResult.metadata.simulatedLocally = true
77
+ simulationResult.balanceChanges.willPayFee.push({
78
+ balance: estimateFee({ feeCurrency, transaction }),
79
+ feeDetails: getTxFeeDetails(transaction),
80
+ })
81
+ } else {
82
+ const message = 'Balance changes cannot be estimated.'
83
+ simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
84
+ simulationResult.metadata.humanReadableError = message
85
+ }
86
+ }
87
+
88
+ if (simulationTransactionInternalResult.errorMessage) {
89
+ simulationResult.metadata.humanReadableError =
90
+ simulationTransactionInternalResult.errorMessage
91
+ }
92
+
93
+ return simulationResult
94
+ }
95
+
96
+ for (const transaction of transactions) {
97
+ simulationResult.balanceChanges.willPayFee.push({
98
+ balance: estimateFee({ feeCurrency, transaction }),
99
+ feeDetails: getTxFeeDetails(transaction),
100
+ })
101
+ }
102
+
103
+ simulationResult.displayDetails = getDisplayDetails(simulationResult.balanceChanges.willApprove)
104
+
105
+ return simulationResult
106
+ }
@@ -0,0 +1,14 @@
1
+ import { hexToBN } from '@exodus/web3-utils'
2
+
3
+ export const estimateFee = ({ feeCurrency, transaction }) => {
4
+ const gas = hexToBN(transaction.gas)
5
+
6
+ if (transaction.maxFeePerGas) {
7
+ const maxFeePerGas = hexToBN(transaction.maxFeePerGas)
8
+ const maximumFee = gas.mul(maxFeePerGas)
9
+ return feeCurrency.baseUnit(maximumFee.toString())
10
+ }
11
+
12
+ const gasPrice = hexToBN(transaction.gasPrice)
13
+ return feeCurrency.baseUnit(gas.mul(gasPrice).toString())
14
+ }
@@ -0,0 +1,15 @@
1
+ import { hexToBN } from '@exodus/web3-utils'
2
+
3
+ import { isSimpleTransfer } from './transactions.js'
4
+
5
+ export class NotSimpleTransferError extends Error {}
6
+
7
+ export const estimateSimpleTransfer = ({ baseAsset, transaction }) => {
8
+ if (!isSimpleTransfer(transaction)) {
9
+ throw new NotSimpleTransferError('The transaction input should be empty ("0x").')
10
+ }
11
+
12
+ const txValue = hexToBN(transaction.value || '0x0').toString()
13
+
14
+ return baseAsset.currency.baseUnit(txValue)
15
+ }
@@ -0,0 +1,18 @@
1
+ export const MessageTypeEnum = {
2
+ RawMessage: 0,
3
+ TypedData: 1,
4
+ Unknown: 2,
5
+ }
6
+
7
+ export const getMessageType = (message) => {
8
+ if (typeof message !== 'string') {
9
+ return MessageTypeEnum.Unknown
10
+ }
11
+
12
+ try {
13
+ JSON.parse(message)
14
+ return MessageTypeEnum.TypedData
15
+ } catch {
16
+ return MessageTypeEnum.RawMessage
17
+ }
18
+ }
@@ -0,0 +1,68 @@
1
+ import { makeSimulationAPICall } from '@exodus/web3-utils'
2
+
3
+ import { BLOWFISH_EVM_CHAINS } from './common.js'
4
+ import { MessageTypeEnum } from './get-message-type.js'
5
+
6
+ const convertToAPIPayload = ({ address, message, url }) => {
7
+ const restParameters = {
8
+ metadata: { origin: url.href },
9
+ userAccount: address,
10
+ }
11
+
12
+ if (message.messageType === MessageTypeEnum.TypedData) {
13
+ return {
14
+ message: {
15
+ kind: 'SIGN_TYPED_DATA',
16
+ data: JSON.parse(message.message),
17
+ },
18
+ ...restParameters,
19
+ }
20
+ }
21
+
22
+ return {
23
+ message: {
24
+ kind: 'SIGN_MESSAGE',
25
+ rawMessage: message.message,
26
+ },
27
+ ...restParameters,
28
+ }
29
+ }
30
+
31
+ export const handleActionResponse = ({ simulationResult, response }) => {
32
+ if (['BLOCK', 'WARN'].includes(response.action)) {
33
+ // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
34
+ simulationResult.action = response.action
35
+ }
36
+ }
37
+
38
+ export const simulateMessageApi = async ({
39
+ address,
40
+ apiEndpoint,
41
+ message,
42
+ url,
43
+ headers,
44
+ simulationResult,
45
+ }) => {
46
+ const chainInfo = BLOWFISH_EVM_CHAINS[simulationResult.baseAssetName]
47
+ if (!chainInfo) {
48
+ return
49
+ }
50
+
51
+ const { network, chain } = chainInfo
52
+
53
+ const payload = convertToAPIPayload({ address, message, url })
54
+
55
+ const response = await makeSimulationAPICall({
56
+ url: apiEndpoint,
57
+ chain,
58
+ network,
59
+ payload,
60
+ headers,
61
+ })
62
+
63
+ if (!response) {
64
+ return
65
+ }
66
+
67
+ handleActionResponse({ simulationResult, response })
68
+ }
@@ -0,0 +1,265 @@
1
+ import { pick } from '@exodus/basic-utils'
2
+ import { createCurrency, makeSimulationAPICall } from '@exodus/web3-utils'
3
+
4
+ import { BLOWFISH_EVM_CHAINS, MAX_INT256_SOLIDITY } from './common.js'
5
+
6
+ const convertTransactionToApiPayload = (transactions, origin, blockNumber) => {
7
+ const payload = {
8
+ txObjects: transactions.map((transaction) => ({
9
+ from: transaction.from,
10
+ to: transaction.to,
11
+ data: transaction.data,
12
+ value: transaction.value,
13
+ gas: transaction.gas,
14
+ gas_price: transaction.gasPrice,
15
+ })),
16
+ metadata: {
17
+ origin,
18
+ },
19
+ userAccount: transactions[0].from,
20
+ }
21
+
22
+ if (blockNumber !== undefined) {
23
+ payload.blockNumber = blockNumber
24
+ }
25
+
26
+ return payload
27
+ }
28
+
29
+ const createBalanceAmount = (amount, { decimals, symbol }) => {
30
+ const beforeAmount = createCurrency({
31
+ amount: amount.before,
32
+ symbol,
33
+ denominator: decimals,
34
+ })
35
+ const afterAmount = createCurrency({
36
+ amount: amount.after,
37
+ symbol,
38
+ denominator: decimals,
39
+ })
40
+
41
+ return beforeAmount.sub(afterAmount)
42
+ }
43
+
44
+ const handleExpectedBalanceChange = (expectedBalanceChange, simulationResult, { network }) => {
45
+ const { willApprove, willSend, willReceive } = simulationResult.balanceChanges
46
+ const { data, kind } = expectedBalanceChange.rawInfo
47
+
48
+ if (kind === 'FARCASTER_CHANGE_RECOVERY_ADDRESS' || kind === 'ERC20_PERMIT') {
49
+ return
50
+ }
51
+
52
+ const { asset: simulationAsset } = data
53
+
54
+ const isNftTransfer =
55
+ kind === 'ERC721_TRANSFER' || (kind === 'ERC1155_TRANSFER' && !simulationAsset.decimals)
56
+ const isNftTransferApproval =
57
+ kind === 'ERC721_APPROVAL' ||
58
+ (kind === 'ERC721_APPROVAL_FOR_ALL' && !simulationAsset.decimals) ||
59
+ (kind === 'ERC1155_APPROVAL_FOR_ALL' && !simulationAsset.decimals)
60
+ const decimals =
61
+ isNftTransfer || isNftTransferApproval || !Number.isFinite(simulationAsset.decimals)
62
+ ? 0
63
+ : simulationAsset.decimals
64
+ const simulationAssetInfo = pick(simulationAsset, [
65
+ 'address',
66
+ 'imageUrl',
67
+ 'name',
68
+ 'symbol',
69
+ 'verified',
70
+ ])
71
+
72
+ let symbol = simulationAsset.symbol || simulationAsset.name || 'NFT'
73
+ if (network === 'polygon' && simulationAsset.symbol === 'MATIC') {
74
+ symbol = 'POL'
75
+ }
76
+
77
+ if (isNftTransfer || isNftTransferApproval) {
78
+ simulationAssetInfo.imageUrl = data?.metadata?.rawImageUrl
79
+ }
80
+
81
+ if (
82
+ kind === 'ERC20_APPROVAL' ||
83
+ kind === 'ERC721_APPROVAL_FOR_ALL' ||
84
+ kind === 'ERC1155_APPROVAL_FOR_ALL' ||
85
+ kind === 'ERC721_APPROVAL'
86
+ ) {
87
+ const unitsToUse = symbol || simulationAsset.name || 'Units.'
88
+
89
+ const approvingBalance = createCurrency({
90
+ amount: data.amount.after,
91
+ symbol: unitsToUse,
92
+ denominator: decimals,
93
+ })
94
+
95
+ willApprove.push({
96
+ asset: simulationAssetInfo,
97
+ spender: data.spender.address,
98
+ unitName: unitsToUse,
99
+ balance: approvingBalance,
100
+ isMaxApproval: approvingBalance._number.gte(MAX_INT256_SOLIDITY),
101
+ })
102
+ return
103
+ }
104
+
105
+ const { amount } = data
106
+
107
+ const balance = createBalanceAmount(amount, { decimals, symbol })
108
+
109
+ if (balance.isZero) {
110
+ return
111
+ }
112
+
113
+ const balanceChange = {
114
+ asset: simulationAssetInfo,
115
+ balance,
116
+ }
117
+
118
+ if (isNftTransfer || isNftTransferApproval) {
119
+ const tokenId = data.tokenId
120
+ const compositeId = `${simulationAsset.address}/${tokenId}`
121
+ balanceChange.nft = {
122
+ id: `${network}:${compositeId}`,
123
+ compositeId,
124
+ title: simulationAsset.name,
125
+ }
126
+ }
127
+
128
+ if (balance.isNegative) {
129
+ balanceChange.balance = balanceChange.balance.negate()
130
+
131
+ willReceive.push(balanceChange)
132
+ } else {
133
+ willSend.push(balanceChange)
134
+ }
135
+ }
136
+
137
+ const simulationResultHasAssets = (expectedStateChanges) => {
138
+ for (const expectedChange of expectedStateChanges) {
139
+ const { asset: simulationAsset } = expectedChange.rawInfo.data
140
+ if (!simulationAsset) {
141
+ return false
142
+ }
143
+ }
144
+
145
+ return true
146
+ }
147
+
148
+ export const SimulationErrorMessages = {
149
+ APICallFailed: 0,
150
+ SimulationFailed: 1,
151
+ SimulationNotSupported: 2,
152
+ }
153
+
154
+ export const simulateTransactionsApi = async ({
155
+ transactions,
156
+ apiEndpoint,
157
+ origin,
158
+ blockNumber,
159
+ simulationResult,
160
+ headers,
161
+ asset,
162
+ }) => {
163
+ const successDefaultResult = {
164
+ kind: 'success',
165
+ value: undefined,
166
+ }
167
+ if (!BLOWFISH_EVM_CHAINS[asset.name]) {
168
+ return {
169
+ kind: 'error',
170
+ error: SimulationErrorMessages.SimulationNotSupported,
171
+ errorMessage: 'Simulation is not supported for this asset.',
172
+ }
173
+ }
174
+
175
+ const userAddress = transactions[0].from
176
+
177
+ const payload = convertTransactionToApiPayload(transactions, origin, blockNumber)
178
+
179
+ if (!Object.hasOwnProperty.call(BLOWFISH_EVM_CHAINS, asset.name)) {
180
+ return successDefaultResult
181
+ }
182
+
183
+ const { network, chain } = BLOWFISH_EVM_CHAINS[asset.name]
184
+
185
+ const response = await makeSimulationAPICall({
186
+ url: apiEndpoint,
187
+ chain,
188
+ network,
189
+ payload,
190
+ headers,
191
+ })
192
+ if (!response) {
193
+ return {
194
+ kind: 'error',
195
+ error: SimulationErrorMessages.APICallFailed,
196
+ errorMessage: 'Simulation API call failed.',
197
+ }
198
+ }
199
+
200
+ // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
201
+ simulationResult.metadata.simulatedLocally = false
202
+
203
+ const { expectedStateChanges = {}, error } = response.simulationResults.aggregated
204
+
205
+ if (error) {
206
+ return {
207
+ kind: 'error',
208
+ error: SimulationErrorMessages.SimulationFailed,
209
+ errorMessage: error?.humanReadableError,
210
+ }
211
+ }
212
+
213
+ const { action } = response
214
+
215
+ if (action === 'BLOCK') {
216
+ simulationResult.warnings.push({
217
+ kind: 'MALICIOUS_ACTION',
218
+ severity: 'CRITICAL',
219
+ message: 'This dApp could be malicious. Do not proceed unless you are certain this is safe.',
220
+ })
221
+ } else if (action === 'WARN') {
222
+ simulationResult.warnings.push({
223
+ kind: 'SUSPICIOUS_ACTION',
224
+ severity: 'MEDIUM',
225
+ message: 'This transaction may be suspicious. Proceed with caution.',
226
+ })
227
+ }
228
+
229
+ const noExpectedStateChangesDetected = Object.keys(expectedStateChanges).length === 0
230
+
231
+ if (noExpectedStateChangesDetected) {
232
+ simulationResult.balanceChanges.willSend.push({
233
+ balance: createCurrency({
234
+ amount: 0,
235
+ symbol: asset.displayTicker,
236
+ denominator: asset.currency.defaultUnit.power,
237
+ }),
238
+ })
239
+
240
+ return successDefaultResult
241
+ }
242
+
243
+ const userExpectedChanges = Object.keys(expectedStateChanges).reduce((prev, address) => {
244
+ if (userAddress.toLowerCase() !== address.toLowerCase()) {
245
+ return prev
246
+ }
247
+
248
+ const result =
249
+ (Object.prototype.hasOwnProperty.call(expectedStateChanges, address) &&
250
+ expectedStateChanges[address]) ||
251
+ []
252
+
253
+ return [...prev, ...result]
254
+ }, [])
255
+
256
+ if (!simulationResultHasAssets(userExpectedChanges)) {
257
+ return successDefaultResult
258
+ }
259
+
260
+ userExpectedChanges.forEach((expectedChange) => {
261
+ handleExpectedBalanceChange(expectedChange, simulationResult, { network })
262
+ })
263
+
264
+ return successDefaultResult
265
+ }
@@ -0,0 +1,16 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ import { createSimulateTransactions } from './create-simulate-transactions.js'
4
+
5
+ const simulateEVMTransactions = createSimulateTransactions({
6
+ apiEndpoint: 'https://simulation.a.exodus.io/simulate',
7
+ headers: {
8
+ 'X-Api-Version': '2023-06-05',
9
+ },
10
+ })
11
+
12
+ export function simulateTransactions({ asset, transactions, ...restParameters }) {
13
+ assert(asset, '"asset" should be passed.')
14
+
15
+ return simulateEVMTransactions({ asset, transactions, ...restParameters })
16
+ }
@@ -0,0 +1,52 @@
1
+ import SolidityContract from '@exodus/solidity-contract'
2
+
3
+ export function getDisplayDetails(willApprove) {
4
+ let title = 'Approve Transaction?'
5
+ const isApproveTransaction = willApprove.length
6
+
7
+ if (isApproveTransaction) {
8
+ const tokenName = willApprove[0].unitName
9
+ title = tokenName ? `Approve ${tokenName} Transactions?` : title
10
+ }
11
+
12
+ return { title }
13
+ }
14
+
15
+ export const isSimpleTransfer = (transaction) => {
16
+ return ['0x', null, undefined].includes(transaction.data)
17
+ }
18
+
19
+ export const getTxFeeDetails = (transaction) => {
20
+ const maxFeePerGas = transaction.maxFeePerGas
21
+ const gasPrice = transaction.gasPrice
22
+ const gasLimit = transaction.gas
23
+
24
+ return {
25
+ gasLimit,
26
+ maxFeePerGas,
27
+ gasPrice,
28
+ }
29
+ }
30
+
31
+ export function decodeRecipientAddresses(transaction) {
32
+ try {
33
+ if (!transaction.to) {
34
+ return []
35
+ }
36
+
37
+ if (isSimpleTransfer(transaction)) {
38
+ return [transaction.to]
39
+ }
40
+
41
+ const token = SolidityContract.erc20(transaction.to)
42
+ const { method, values } = token.decodeInput(transaction.data)
43
+
44
+ if (method === 'transfer') {
45
+ return [values[0]]
46
+ }
47
+
48
+ return []
49
+ } catch {
50
+ return []
51
+ }
52
+ }
@@ -0,0 +1,26 @@
1
+ import { estimateSimpleTransfer, NotSimpleTransferError } from './estimate-simple-transfer.js'
2
+
3
+ export const tryEstimatingChangesLocally = ({ asset, simulationResult, transaction }) => {
4
+ if (typeof transaction.from !== 'string' || typeof transaction.to !== 'string') {
5
+ return false
6
+ }
7
+
8
+ try {
9
+ const estimatedValue = estimateSimpleTransfer({ baseAsset: asset, transaction })
10
+ const actualTransferValue =
11
+ transaction.from.toLowerCase() === transaction.to.toLowerCase()
12
+ ? asset.currency.ZERO
13
+ : estimatedValue
14
+ simulationResult.balanceChanges.willSend.push({
15
+ balance: actualTransferValue,
16
+ })
17
+
18
+ return true
19
+ } catch (err) {
20
+ if (!(err instanceof NotSimpleTransferError)) {
21
+ // TODO: accept a logger dep and log the unexpected local simulation error.
22
+ }
23
+
24
+ return false
25
+ }
26
+ }
@@ -3,6 +3,7 @@ import { parseUnsignedTx } from '@exodus/ethereum-lib'
3
3
  import { bufferToHex } from '@exodus/ethereumjs/util'
4
4
  import assert from 'minimalistic-assert'
5
5
 
6
+ import { simulateTransactions as simulateEthereumTransactions } from '../../simulation/simulate-transactions.js'
6
7
  import { EthereumStaking, UNSTAKE_DEFAULTS } from './api.js'
7
8
  import { simulateEverstakeUnstake } from './everstake.js'
8
9
 
@@ -211,7 +212,8 @@ export async function simulateUndelegateTransactions({
211
212
 
212
213
  if (transactions.length === 0) return
213
214
 
214
- const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
215
+ const simulationResponse = await simulateEthereumTransactions({
216
+ asset: asset.baseAsset,
215
217
  baseAssetName: asset.baseAsset.name,
216
218
  origin: 'exodus-staking',
217
219
  senderAddress,
@@ -4,6 +4,7 @@ import assetsList, { asset as ethereum } from '@exodus/ethereum-meta'
4
4
  import { Tx } from '@exodus/models'
5
5
  import assert from 'minimalistic-assert'
6
6
 
7
+ import { simulateTransactions as simulateEthereumTransactions } from '../../simulation/simulate-transactions.js'
7
8
  import { decodePolygonStakingTxInputAmount } from '../../tx-log-staking-processor/utils.js'
8
9
  import { MaticStakingApi } from './api.js'
9
10
 
@@ -211,7 +212,8 @@ export async function maticDelegateSimulateTransactions({
211
212
  senderAddress,
212
213
  }
213
214
 
214
- const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
215
+ const simulationResponse = await simulateEthereumTransactions({
216
+ asset: asset.baseAsset,
215
217
  ...simulationTxData,
216
218
  transactions: [approveTxRequest, delegateTxRequest],
217
219
  })