@exodus/ethereum-api 9.0.0-alpha.2 → 9.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/CHANGELOG.md +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -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 +266 -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/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
package/src/nft-utils.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
|
+
import SolidityContract from '@exodus/solidity-contract'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { EthLikeError, EVM_ERROR_REASONS } from './error-wrapper.js'
|
|
6
|
+
import { fetchGasLimit } from './gas-estimation.js'
|
|
7
|
+
|
|
8
|
+
export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) => {
|
|
9
|
+
assert(asset, 'asset is required')
|
|
10
|
+
assert(nft, 'nft is required')
|
|
11
|
+
assert(fromAddress, 'fromAddress is required')
|
|
12
|
+
assert(toAddress, 'toAddress is required')
|
|
13
|
+
const { contractAddress, tokenId, contractType } = nft
|
|
14
|
+
|
|
15
|
+
const txInputs = []
|
|
16
|
+
|
|
17
|
+
if (!contractType || contractType === 'erc1155') {
|
|
18
|
+
const txInput = `0x${SolidityContract.simpleErc1155(contractAddress)
|
|
19
|
+
.safeTransferFrom.build(fromAddress, toAddress, tokenId, 1, [])
|
|
20
|
+
.toString('hex')}`
|
|
21
|
+
txInputs.push({ txInput, contractType: 'erc1155' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!contractType || contractType === 'erc721') {
|
|
25
|
+
const txInput = `0x${SolidityContract.simpleErc721(contractAddress)
|
|
26
|
+
.safeTransferFrom.build(fromAddress, toAddress, tokenId)
|
|
27
|
+
.toString('hex')}`
|
|
28
|
+
txInputs.push({ txInput, contractType: 'erc721' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const errors = [`Cannot fetch gas for ${asset.name}`]
|
|
32
|
+
const { txInput, gasLimit } = await Promise.any(
|
|
33
|
+
txInputs.map(async ({ txInput, contractType }) => {
|
|
34
|
+
try {
|
|
35
|
+
const gasLimit = await fetchGasLimit({
|
|
36
|
+
asset,
|
|
37
|
+
fromAddress,
|
|
38
|
+
contractAddress,
|
|
39
|
+
txInput,
|
|
40
|
+
})
|
|
41
|
+
return { txInput, gasLimit }
|
|
42
|
+
} catch (e) {
|
|
43
|
+
errors.push(`ContractType ${contractType} nft input ${txInput}. ${e.message}`)
|
|
44
|
+
throw e
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
).catch((e) => {
|
|
48
|
+
throw new EthLikeError({
|
|
49
|
+
message: errors.join('\n'),
|
|
50
|
+
errorReasonInfo: EVM_ERROR_REASONS.fetchGasLimitFailed,
|
|
51
|
+
hint: safeString`getNftArguments`,
|
|
52
|
+
traceId: e.traceId,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
return {
|
|
56
|
+
contractAddress,
|
|
57
|
+
gasLimit,
|
|
58
|
+
txInput,
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/number-utils.js
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
import BN from 'bn.js'
|
|
2
|
+
|
|
1
3
|
export function fromHexToString(value) {
|
|
2
4
|
return fromHexToBigInt(value).toString()
|
|
3
5
|
}
|
|
4
6
|
|
|
5
7
|
export function fromHexToBigInt(value) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
return BigInt(addHexPrefix(value))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const addHexPrefix = (str) => {
|
|
12
|
+
if (typeof str !== 'string') {
|
|
13
|
+
return str
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return str.startsWith('0x') ? str : '0x' + str
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const removeHexPrefix = (str) => {
|
|
20
|
+
if (typeof str !== 'string' || str === '') {
|
|
21
|
+
return str
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return str.startsWith('0x') ? str.slice(2) : str
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const splitIn32BytesArray = (output) => {
|
|
28
|
+
const trimmed = removeHexPrefix(output)
|
|
29
|
+
const result = trimmed.match(/^([\da-f]{64})+$/gi)
|
|
30
|
+
if (result) {
|
|
31
|
+
return trimmed.match(/.{64}/g)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error(`${output} is not a 64 bytes hex that can be split`)
|
|
8
35
|
}
|
|
36
|
+
|
|
37
|
+
export const removeLeadingZeroes = (str) => str.replace(/^0+/, '')
|
|
38
|
+
|
|
39
|
+
// Deprecated, use BigInt
|
|
40
|
+
export const fromHexToBN = (str) => new BN(removeLeadingZeroes(removeHexPrefix(str)), 16)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createContract, createEthereumJsTx } from '@exodus/ethereum-lib'
|
|
2
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
3
|
+
import { safeString } from '@exodus/safe-string'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
|
|
6
|
+
import { EVM_ERROR_REASONS, withErrorReason } from '../error-wrapper.js'
|
|
7
|
+
import { fromHexToBigInt } from '../number-utils.js'
|
|
8
|
+
|
|
6
9
|
export const estimateL1DataFeeFactory = ({ l1GasOracleAddress, server }) => {
|
|
7
10
|
assert(l1GasOracleAddress, 'l1GasOracleAddress is required')
|
|
8
11
|
assert(server, 'server is required')
|
|
@@ -12,8 +15,13 @@ export const estimateL1DataFeeFactory = ({ l1GasOracleAddress, server }) => {
|
|
|
12
15
|
const serialized = ethjsTx.serialize()
|
|
13
16
|
const callData = gasContract.getL1Fee.build(serialized)
|
|
14
17
|
const buffer = Buffer.from(callData)
|
|
15
|
-
const data =
|
|
16
|
-
const hex = await
|
|
18
|
+
const data = bufferToHex(buffer)
|
|
19
|
+
const hex = await withErrorReason({
|
|
20
|
+
promise: server.ethCall({ to: l1GasOracleAddress, data }, 'latest'),
|
|
21
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
22
|
+
hint: safeString`estimateL1DataFee`,
|
|
23
|
+
baseAssetName: server.baseAssetName,
|
|
24
|
+
})
|
|
17
25
|
const l1DataFee = fromHexToBigInt(hex)
|
|
18
26
|
const padFee = l1DataFee / BigInt(4)
|
|
19
27
|
const maxL1DataFee = l1DataFee + padFee
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import sendValidationModel from '@exodus/send-validation-model'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
const { FIELDS, PRIORITY_LEVELS, VALIDATION_TYPES } = sendValidationModel
|
|
5
|
+
|
|
6
|
+
export const SCAM_RECIPIENT = 'SCAM_RECIPIENT'
|
|
7
|
+
export const SCAM_RECIPIENT_MESSAGE =
|
|
8
|
+
'This recipient was flagged as risky. Please verify before sending.'
|
|
9
|
+
|
|
10
|
+
const toWalletAccountId = (fromWalletAccount) =>
|
|
11
|
+
typeof fromWalletAccount === 'string' ? fromWalletAccount : fromWalletAccount?.toString?.()
|
|
12
|
+
|
|
13
|
+
export const createSendValidations = ({ assetClientInterface, checkTx }) => {
|
|
14
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
15
|
+
assert(typeof checkTx === 'function', 'checkTx must be a function')
|
|
16
|
+
|
|
17
|
+
const scamRecipientValidator = {
|
|
18
|
+
id: SCAM_RECIPIENT,
|
|
19
|
+
type: VALIDATION_TYPES.WARN,
|
|
20
|
+
field: FIELDS.ADDRESS,
|
|
21
|
+
priority: PRIORITY_LEVELS.MIDDLE,
|
|
22
|
+
validateAndGetMessage: async ({ asset, destinationAddress, sendAmount, fromWalletAccount }) => {
|
|
23
|
+
// M1: native sends only. M2 will cover tokens/approvals/contract calls
|
|
24
|
+
// via a post-createTx call site on the review screen.
|
|
25
|
+
if (!asset || asset.name !== asset.baseAsset.name) return
|
|
26
|
+
if (!destinationAddress || !sendAmount) return
|
|
27
|
+
if (sendAmount.isZero) return
|
|
28
|
+
|
|
29
|
+
const isValidAddress = await asset.address.validate(destinationAddress)
|
|
30
|
+
if (!isValidAddress) return
|
|
31
|
+
|
|
32
|
+
const walletAccountId = toWalletAccountId(fromWalletAccount)
|
|
33
|
+
if (!walletAccountId) return
|
|
34
|
+
|
|
35
|
+
const fromAddress = await assetClientInterface.getReceiveAddress({
|
|
36
|
+
assetName: asset.name,
|
|
37
|
+
walletAccount: walletAccountId,
|
|
38
|
+
useCache: true,
|
|
39
|
+
})
|
|
40
|
+
if (!fromAddress) return
|
|
41
|
+
|
|
42
|
+
// Native send: empty calldata, no signed tx hash to pass yet.
|
|
43
|
+
const result = await checkTx({
|
|
44
|
+
chain: asset.chainId,
|
|
45
|
+
fromAddress,
|
|
46
|
+
toAddress: destinationAddress,
|
|
47
|
+
value: sendAmount.toBaseString({ unit: false }),
|
|
48
|
+
input: '0x',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (result?.action !== 'WARN') return
|
|
52
|
+
|
|
53
|
+
return result.reason || SCAM_RECIPIENT_MESSAGE
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [scamRecipientValidator]
|
|
58
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FeeMonitor } from '@exodus/asset-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
|
|
4
|
+
import { calculateEthLikeFeeMonitorUpdate } from './fee-utils.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Generic eth server based fee monitor.
|
|
@@ -12,39 +12,32 @@ import { fromHexToString } from './number-utils'
|
|
|
12
12
|
*
|
|
13
13
|
* const api = {
|
|
14
14
|
* ...
|
|
15
|
-
* createFeeMonitor: serverBasedFeeMonitorFactoryFactory({ interval: '50s', server,
|
|
15
|
+
* createFeeMonitor: serverBasedFeeMonitorFactoryFactory({ interval: '50s', server, asset, aci }),
|
|
16
16
|
* ...
|
|
17
17
|
* }
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
export const serverBasedFeeMonitorFactoryFactory = ({
|
|
21
|
-
|
|
22
|
-
interval = DEFAULT_FEE_MONITOR_INTERVAL,
|
|
23
|
-
eip1559Enabled,
|
|
24
|
-
server,
|
|
25
|
-
}) => {
|
|
26
|
-
assert(assetName, 'assetName is required')
|
|
20
|
+
export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, aci }) => {
|
|
21
|
+
assert(asset, 'asset is required')
|
|
27
22
|
assert(server, 'server is required')
|
|
23
|
+
assert(aci, 'aci is required')
|
|
28
24
|
|
|
29
25
|
const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends FeeMonitor {
|
|
30
26
|
constructor({ updateFee }) {
|
|
31
27
|
assert(updateFee, 'updateFee is required')
|
|
32
28
|
super({
|
|
33
29
|
updateFee,
|
|
34
|
-
assetName,
|
|
30
|
+
assetName: asset.name,
|
|
35
31
|
interval,
|
|
36
32
|
})
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
async fetchFee() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
gasPrice: `${gasPrice} wei`,
|
|
46
|
-
baseFeePerGas,
|
|
47
|
-
}
|
|
36
|
+
return calculateEthLikeFeeMonitorUpdate({
|
|
37
|
+
assetClientInterface: aci,
|
|
38
|
+
feeAsset: asset,
|
|
39
|
+
fetchedGasPrices: await server.getGasPriceEstimation(),
|
|
40
|
+
})
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
43
|
return (...args) => new FeeMonitorClass(...args)
|
package/src/simulate-tx/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './fetch-tx-preview'
|
|
2
|
-
export * from './simulate-eth-tx'
|
|
1
|
+
export * from './fetch-tx-preview.js'
|
|
2
|
+
export * from './simulate-eth-tx.js'
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import assert from 'minimalistic-assert'
|
|
2
|
-
import { asset as ethereum } from '@exodus/ethereum-meta'
|
|
3
1
|
import {
|
|
2
|
+
APPROVE_METHOD_ID,
|
|
4
3
|
checkIsERC721InputData,
|
|
5
4
|
checkIsNFTInputData,
|
|
6
|
-
APPROVE_METHOD_ID,
|
|
7
5
|
} from '@exodus/ethereum-lib'
|
|
6
|
+
import { asset as ethereum } from '@exodus/ethereum-meta'
|
|
8
7
|
import SolidityContract from '@exodus/solidity-contract'
|
|
8
|
+
import assert from 'minimalistic-assert'
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { getERC20Params } from '../eth-like-util.js'
|
|
11
|
+
import { fromHexToBigInt } from '../number-utils.js'
|
|
12
|
+
import { fetchTxPreview } from './fetch-tx-preview.js'
|
|
12
13
|
|
|
13
14
|
const ethDecimals = ethereum.units.ETH
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// Convert a hex transaction field (e.g. "0x10a741a462780000" for 1.2 ETH in wei)
|
|
17
|
+
// to a precision-preserving decimal string. `parseInt` would silently truncate
|
|
18
|
+
// values above Number.MAX_SAFE_INTEGER (~9e15), and Ethereum wei amounts routinely
|
|
19
|
+
// exceed that (1 ETH = 1e18 wei).
|
|
20
|
+
const ethHexToDecimalString = (hexValue) => {
|
|
21
|
+
if (hexValue === undefined || hexValue === null || hexValue === '' || hexValue === '0x') {
|
|
22
|
+
return '0'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fromHexToBigInt(hexValue).toString()
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
async function getAssetSymbolFromContract(contractAddress, asset) {
|
|
18
29
|
const { symbol: assetSymbol } = await getERC20Params({
|
|
@@ -169,14 +180,16 @@ export async function retrieveSideEffects({ transaction, asset, shouldSimulate =
|
|
|
169
180
|
|
|
170
181
|
if (!transaction.to) throw new Error(`'to' field is missing in the TX object`)
|
|
171
182
|
|
|
183
|
+
const txValueDecimal = ethHexToDecimalString(transaction.value)
|
|
184
|
+
|
|
172
185
|
const blocknativeTxObject = {
|
|
173
186
|
to: transaction.to,
|
|
174
187
|
input: transaction.data,
|
|
175
188
|
from: transaction.from,
|
|
176
|
-
value:
|
|
177
|
-
gas:
|
|
178
|
-
maxFeePerGas:
|
|
179
|
-
maxPriorityFeePerGas:
|
|
189
|
+
value: txValueDecimal,
|
|
190
|
+
gas: ethHexToDecimalString(transaction.gas),
|
|
191
|
+
maxFeePerGas: ethHexToDecimalString(transaction.maxFeePerGas),
|
|
192
|
+
maxPriorityFeePerGas: ethHexToDecimalString(transaction.maxPriorityFeePerGas),
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
try {
|
|
@@ -224,7 +237,7 @@ export async function retrieveSideEffects({ transaction, asset, shouldSimulate =
|
|
|
224
237
|
if (willSend.length === 0) {
|
|
225
238
|
willSend.push({
|
|
226
239
|
symbol: 'ETH',
|
|
227
|
-
balance: `-${
|
|
240
|
+
balance: `-${txValueDecimal}`,
|
|
228
241
|
assetType: 'ether',
|
|
229
242
|
decimal: ethDecimals,
|
|
230
243
|
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import BN from 'bn.js'
|
|
2
|
+
|
|
3
|
+
export const BLOWFISH_EVM_CHAINS = {
|
|
4
|
+
avalanchec: {
|
|
5
|
+
network: 'avalanche',
|
|
6
|
+
chain: 'mainnet',
|
|
7
|
+
},
|
|
8
|
+
basemainnet: {
|
|
9
|
+
network: 'base',
|
|
10
|
+
chain: 'mainnet',
|
|
11
|
+
},
|
|
12
|
+
bsc: {
|
|
13
|
+
network: 'bnb',
|
|
14
|
+
chain: 'mainnet',
|
|
15
|
+
},
|
|
16
|
+
ethereum: {
|
|
17
|
+
network: 'ethereum',
|
|
18
|
+
chain: 'mainnet',
|
|
19
|
+
},
|
|
20
|
+
ethereumarbone: {
|
|
21
|
+
network: 'arbitrum',
|
|
22
|
+
chain: 'one',
|
|
23
|
+
},
|
|
24
|
+
matic: {
|
|
25
|
+
network: 'polygon',
|
|
26
|
+
chain: 'mainnet',
|
|
27
|
+
},
|
|
28
|
+
optimism: {
|
|
29
|
+
network: 'optimism',
|
|
30
|
+
chain: 'mainnet',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const MAX_INT256_SOLIDITY = new BN(2).pow(new BN(255)).subn(1)
|
|
@@ -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,108 @@
|
|
|
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
|
+
recipientAddress,
|
|
16
|
+
headers: headersFromParams,
|
|
17
|
+
overrideApiEndpoint,
|
|
18
|
+
}) => {
|
|
19
|
+
const headers = { ...headersFromDeps, ...headersFromParams }
|
|
20
|
+
|
|
21
|
+
const simulationResult = createEmptySimulationResult({
|
|
22
|
+
asset,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (transactions.length === 0) {
|
|
26
|
+
const message = 'No transactions provided. Cannot simulate transaction batch.'
|
|
27
|
+
simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
|
|
28
|
+
simulationResult.metadata.humanReadableError = message
|
|
29
|
+
return simulationResult
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const firstFrom = transactions[0].from.toLowerCase()
|
|
33
|
+
const hasMismatchedFrom = transactions.some((tx) => tx.from.toLowerCase() !== firstFrom)
|
|
34
|
+
if (hasMismatchedFrom) {
|
|
35
|
+
const message =
|
|
36
|
+
'All transactions in a batch must have the same "from" address. Cannot simulate transaction batch.'
|
|
37
|
+
simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
|
|
38
|
+
simulationResult.metadata.humanReadableError = message
|
|
39
|
+
return simulationResult
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasInvalidTransaction = transactions.some((tx) => !tx.to)
|
|
43
|
+
if (hasInvalidTransaction) {
|
|
44
|
+
const message =
|
|
45
|
+
'One or more transactions are missing required "to" address. Cannot simulate transaction batch.'
|
|
46
|
+
simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
|
|
47
|
+
simulationResult.metadata.humanReadableError = message
|
|
48
|
+
return simulationResult
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
simulationResult.recipientAddresses = transactions.map((tx) => decodeRecipientAddresses(tx))
|
|
52
|
+
const feeCurrency = asset.feeAsset.currency
|
|
53
|
+
|
|
54
|
+
const simulationTransactionInternalResult = await simulateTransactionsApi({
|
|
55
|
+
asset,
|
|
56
|
+
transactions,
|
|
57
|
+
apiEndpoint: overrideApiEndpoint || apiEndpoint,
|
|
58
|
+
origin,
|
|
59
|
+
blockNumber,
|
|
60
|
+
recipientAddress,
|
|
61
|
+
simulationResult,
|
|
62
|
+
headers,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (simulationTransactionInternalResult.kind === 'error') {
|
|
66
|
+
if (transactions.length > 1) {
|
|
67
|
+
const message = 'Cannot locally simulate batched transactions.'
|
|
68
|
+
simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
|
|
69
|
+
simulationResult.metadata.humanReadableError = message
|
|
70
|
+
} else {
|
|
71
|
+
const transaction = transactions[0]
|
|
72
|
+
const simulatedLocally = tryEstimatingChangesLocally({
|
|
73
|
+
asset,
|
|
74
|
+
simulationResult,
|
|
75
|
+
transaction,
|
|
76
|
+
})
|
|
77
|
+
if (simulatedLocally) {
|
|
78
|
+
simulationResult.metadata.simulatedLocally = true
|
|
79
|
+
simulationResult.balanceChanges.willPayFee.push({
|
|
80
|
+
balance: estimateFee({ feeCurrency, transaction }),
|
|
81
|
+
feeDetails: getTxFeeDetails(transaction),
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
const message = 'Balance changes cannot be estimated.'
|
|
85
|
+
simulationResult.warnings.push({ kind: 'INTERNAL_ERROR', severity: 'HIGH', message })
|
|
86
|
+
simulationResult.metadata.humanReadableError = message
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (simulationTransactionInternalResult.errorMessage) {
|
|
91
|
+
simulationResult.metadata.humanReadableError =
|
|
92
|
+
simulationTransactionInternalResult.errorMessage
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return simulationResult
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const transaction of transactions) {
|
|
99
|
+
simulationResult.balanceChanges.willPayFee.push({
|
|
100
|
+
balance: estimateFee({ feeCurrency, transaction }),
|
|
101
|
+
feeDetails: getTxFeeDetails(transaction),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
simulationResult.displayDetails = getDisplayDetails(simulationResult.balanceChanges.willApprove)
|
|
106
|
+
|
|
107
|
+
return simulationResult
|
|
108
|
+
}
|
|
@@ -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/hydra/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
|
+
}
|