@exodus/ethereum-api 8.76.5 → 8.76.7
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/CHANGELOG.md +30 -0
- package/package.json +3 -4
- package/src/address-has-history.js +6 -2
- package/src/create-asset-plugin-factory.js +1 -0
- package/src/create-asset-utils.js +39 -35
- package/src/create-asset.js +26 -16
- package/src/eth-like-util.js +53 -0
- package/src/exodus-eth-server/api-coin-nodes.js +11 -84
- package/src/exodus-eth-server/clarity-v2.js +30 -51
- package/src/exodus-eth-server/clarity.js +2 -115
- package/src/exodus-eth-server/errors.js +5 -1
- package/src/exodus-eth-server/eth-like-server-base.js +123 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/gas-estimation.js +19 -4
- package/src/get-balances.js +14 -0
- package/src/index.js +1 -0
- package/src/multicall3/index.js +169 -0
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +106 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +265 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/ethereum/staking-utils.js +3 -1
- package/src/staking/matic/matic-staking-utils.js +3 -1
- package/src/tx-log/clarity-truncated-history-monitor.js +34 -0
- package/src/tx-log/ethereum-no-history-monitor.js +2 -23
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +28 -0
- package/src/tx-send/broadcast-error-handler.js +7 -2
- package/src/tx-send/tx-send.js +1 -0
- package/src/web3/createSimulateMessage.js +2 -1
- package/src/web3/createSimulateTransactions.js +3 -9
|
@@ -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
|
|
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
|
|
215
|
+
const simulationResponse = await simulateEthereumTransactions({
|
|
216
|
+
asset: asset.baseAsset,
|
|
215
217
|
...simulationTxData,
|
|
216
218
|
transactions: [approveTxRequest, delegateTxRequest],
|
|
217
219
|
})
|
|
@@ -75,6 +75,40 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
|
75
75
|
let clarityCursor
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
+
// When refreshing, we need to clear out old asset
|
|
79
|
+
// balances in `accountState` since these can surface
|
|
80
|
+
// values populated from alternative monitors i.e.
|
|
81
|
+
// `no-history`, which can be written at arbitrary
|
|
82
|
+
// points in time.
|
|
83
|
+
//
|
|
84
|
+
// Since the `truncated-history-monitor` does not
|
|
85
|
+
// guarantee an `accountState` write for an asset,
|
|
86
|
+
// (these are only provided when the history is
|
|
87
|
+
// truncated), we must ensure older checkpoints cannot
|
|
88
|
+
// be inadvertently relied upon.
|
|
89
|
+
//
|
|
90
|
+
if (refresh) {
|
|
91
|
+
const defaultAccountState = this.asset.api.createAccountState().create()
|
|
92
|
+
|
|
93
|
+
const currentAccountState = derivedData.currentAccountState ?? defaultAccountState
|
|
94
|
+
|
|
95
|
+
const resetTokenBalances = {
|
|
96
|
+
balance: defaultAccountState.balance,
|
|
97
|
+
tokenBalances: defaultAccountState.tokenBalances,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Optimistically update the `derivedData` for downstream.
|
|
101
|
+
derivedData.currentAccountState = currentAccountState.merge(resetTokenBalances)
|
|
102
|
+
|
|
103
|
+
this.aci.updateAccountStateBatch({
|
|
104
|
+
assetName,
|
|
105
|
+
walletAccount,
|
|
106
|
+
accountState: currentAccountState,
|
|
107
|
+
newData: resetTokenBalances,
|
|
108
|
+
batch,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
79
113
|
|
|
80
114
|
;({ allTxs } = await normalizeTransactionsResponse({
|