@exodus/ethereum-api 8.76.6 → 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 +20 -0
- package/package.json +2 -3
- package/src/create-asset-plugin-factory.js +1 -0
- package/src/create-asset-utils.js +39 -35
- package/src/create-asset.js +21 -14
- 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
|
@@ -22,7 +22,11 @@ const DYNAMIC_RPC_ERRORS = {
|
|
|
22
22
|
TRANSACTION_GAS_PRICE_BELOW_MINIMUM: safeString`transaction gas price below minimum`,
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const SERVO_ERRORS = {
|
|
26
|
+
EMPTY_BUNDLE: safeString`numTxs=0: invalid num txs`,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const KNOWN_RPC_ERRORS = { ...TxPoolErrors, ...DYNAMIC_RPC_ERRORS, ...SERVO_ERRORS }
|
|
26
30
|
|
|
27
31
|
export const tryMappingToRpcErrors = (errorMessage) => {
|
|
28
32
|
if (typeof errorMessage !== 'string') return
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
2
|
+
import SolidityContract from '@exodus/solidity-contract'
|
|
3
|
+
import EventEmitter from 'events/events.js'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
5
|
+
|
|
6
|
+
export default class EthLikeServerBase extends EventEmitter {
|
|
7
|
+
id = 0
|
|
8
|
+
|
|
9
|
+
buildRequest({ method, params = [] }) {
|
|
10
|
+
return { jsonrpc: '2.0', id: this.id++, method, params }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
balanceOfRequest(address, tokenAddress, tag = 'latest') {
|
|
14
|
+
const contract = SolidityContract.simpleErc20(tokenAddress)
|
|
15
|
+
const callData = contract.balanceOf.build(address)
|
|
16
|
+
const data = {
|
|
17
|
+
data: bufferToHex(callData),
|
|
18
|
+
to: tokenAddress,
|
|
19
|
+
}
|
|
20
|
+
return this.ethCallRequest(data, tag)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getBalanceRequest(address, tag = 'latest') {
|
|
24
|
+
return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
gasPriceRequest() {
|
|
28
|
+
return this.buildRequest({ method: 'eth_gasPrice' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
estimateGasRequest(data, tag = 'latest') {
|
|
32
|
+
return this.buildRequest({ method: 'eth_estimateGas', params: [data, tag] })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
sendRawTransactionRequest(data) {
|
|
36
|
+
const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
|
|
37
|
+
const hex = _data.startsWith('0x') ? _data : '0x' + _data
|
|
38
|
+
return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
coinbaseRequest() {
|
|
42
|
+
return this.buildRequest({ method: 'eth_coinbase' })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getCodeRequest(address, tag = 'latest') {
|
|
46
|
+
return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getStorageAtRequest(address, position, tag = 'latest') {
|
|
50
|
+
return this.buildRequest({ method: 'eth_getStorageAt', params: [address, position, tag] })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getTransactionCountRequest(address, tag = 'latest') {
|
|
54
|
+
return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getTransactionByHashRequest(hash) {
|
|
58
|
+
return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getTransactionReceiptRequest(txhash) {
|
|
62
|
+
return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ethCallRequest(data, tag = 'latest') {
|
|
66
|
+
return this.buildRequest({ method: 'eth_call', params: [data, tag] })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
blockNumberRequest() {
|
|
70
|
+
return this.buildRequest({ method: 'eth_blockNumber' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getLogsRequest(object) {
|
|
74
|
+
return this.buildRequest({ method: 'eth_getLogs', params: [object] })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getBlockByNumberRequest(numberHex, isFullTxs = false) {
|
|
78
|
+
return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getBlockByHashRequest(blockHash, isFullTxs = false) {
|
|
82
|
+
return this.buildRequest({ method: 'eth_getBlockByHash', params: [blockHash, isFullTxs] })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getBlockTransactionCountByHashRequest(blockHash) {
|
|
86
|
+
return this.buildRequest({ method: 'eth_getBlockTransactionCountByHash', params: [blockHash] })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getBlockTransactionCountByNumberRequest(quantityOrTag) {
|
|
90
|
+
return this.buildRequest({
|
|
91
|
+
method: 'eth_getBlockTransactionCountByNumber',
|
|
92
|
+
params: [quantityOrTag],
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getCompilersRequest() {
|
|
97
|
+
return this.buildRequest({ method: 'eth_getCompilers' })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
simulateV1Request(...params) {
|
|
101
|
+
return this.buildRequest({
|
|
102
|
+
method: 'eth_simulateV1',
|
|
103
|
+
params,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getNetVersionRequest() {
|
|
108
|
+
return this.buildRequest({ method: 'net_version' })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
simulateRawTransactionRequest(rawTx, applyPending = true) {
|
|
112
|
+
const replaced = rawTx.replace('0x', '')
|
|
113
|
+
return this.buildRequest({
|
|
114
|
+
method: 'debug_simulateRawTransaction',
|
|
115
|
+
params: [replaced, applyPending],
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sendBundleRequest({ txs }) {
|
|
120
|
+
assert(Array.isArray(txs), 'expected array txs')
|
|
121
|
+
return this.buildRequest({ method: 'eth_sendBundle', params: [{ txs }] })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { retry } from '@exodus/simple-retry'
|
|
2
|
+
import { TraceId } from '@exodus/traceparent'
|
|
3
|
+
|
|
4
|
+
const getTextFromResponse = async (response) => {
|
|
5
|
+
try {
|
|
6
|
+
const responseBody = await response.text()
|
|
7
|
+
return responseBody.slice(0, 100)
|
|
8
|
+
} catch {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const fetchJson = async (url, fetchOptions) => {
|
|
14
|
+
const response = await fetch(url, fetchOptions)
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const traceId = TraceId.fromResponse(response)
|
|
18
|
+
const error = new Error(
|
|
19
|
+
`${url} returned ${response.status}: ${
|
|
20
|
+
response.statusText || 'Unknown Status Text'
|
|
21
|
+
}. Body: ${await getTextFromResponse(response)}`
|
|
22
|
+
)
|
|
23
|
+
if (traceId) {
|
|
24
|
+
error.traceId = traceId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw error
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const json = await response.json()
|
|
31
|
+
|
|
32
|
+
// Only capture trace ID if there's an RPC error in the response
|
|
33
|
+
// (consumers extract it via __traceId when throwing the error)
|
|
34
|
+
if (json.error) {
|
|
35
|
+
const traceId = TraceId.fromResponse(response)
|
|
36
|
+
if (traceId) {
|
|
37
|
+
json.__traceId = traceId
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return json
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const fetchJsonRetry = (url, fetchOptions) => {
|
|
45
|
+
const waitTimes = ['3s']
|
|
46
|
+
const fetchWithRetry = retry(fetchJson, { delayTimesMs: waitTimes })
|
|
47
|
+
return fetchWithRetry(url, fetchOptions)
|
|
48
|
+
}
|
package/src/gas-estimation.js
CHANGED
|
@@ -98,12 +98,27 @@ export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fro
|
|
|
98
98
|
return DEFAULT_GAS_LIMIT_MULTIPLIER
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// https://eips.ethereum.org/EIPS/eip-7623
|
|
102
|
+
export const getEip7623CalldataCost = (calldata) => {
|
|
103
|
+
calldata = toBuffer(calldata)
|
|
104
|
+
|
|
105
|
+
const TOTAL_COST_FLOOR_PER_TOKEN = 10
|
|
106
|
+
|
|
107
|
+
const nonZeroBytesInCalldata = calldata.filter(Boolean).length
|
|
108
|
+
const zeroBytesInCalldata = calldata.length - nonZeroBytesInCalldata
|
|
109
|
+
const tokensInCalldata = zeroBytesInCalldata + nonZeroBytesInCalldata * 4
|
|
110
|
+
|
|
111
|
+
return TOTAL_COST_FLOOR_PER_TOKEN * tokensInCalldata
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
export const defaultGasLimit = ({ asset, txInput }) => {
|
|
102
115
|
const isToken = isEthereumLikeToken(asset)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
|
|
117
|
+
const calldataCost = asset.baseAsset.eip7623Supported
|
|
118
|
+
? getEip7623CalldataCost(txInput)
|
|
119
|
+
: GAS_PER_NON_ZERO_BYTE * toBuffer(txInput).length
|
|
120
|
+
|
|
121
|
+
return (isToken ? DEFAULT_TOKEN_GAS_LIMIT : DEFAULT_GAS_LIMIT) + calldataCost
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
// TODO: `gasLimit` needs to be a responsibility of `resolveTxAttributesByTxType`.
|
package/src/get-balances.js
CHANGED
|
@@ -293,6 +293,20 @@ export const getBalancesFactory = ({ monitorType, useAbsoluteBalance, rpcBalance
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// A pending send already reflected in the account-state reading is double-counted
|
|
297
|
+
// via `unconfirmedSent`, making spendable transiently negative (bounded by
|
|
298
|
+
// `unconfirmedSent`). A negative beyond that bound is a real accounting bug.
|
|
299
|
+
if (spendable.isNegative && spendable.abs().gt(unconfirmedSent)) {
|
|
300
|
+
console.warn('unexpected negative spendable beyond pending sends', {
|
|
301
|
+
assetName: asset.name,
|
|
302
|
+
spendable: spendable.toBaseString(),
|
|
303
|
+
unconfirmedSent: unconfirmedSent.toBaseString(),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Clamp before deriving `total` so the artifact doesn't drag it below staked balances.
|
|
308
|
+
spendable = spendable.clampLowerZero()
|
|
309
|
+
|
|
296
310
|
const total = spendable.add(staked).add(staking).add(unstaking).add(unstaked)
|
|
297
311
|
const stakeable = spendable
|
|
298
312
|
|
package/src/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export {
|
|
|
61
61
|
} from './staking/index.js'
|
|
62
62
|
|
|
63
63
|
export { fetchTxPreview, maybeRemoveDuplicates, retrieveSideEffects } from './simulate-tx/index.js'
|
|
64
|
+
export { simulateTransactions } from './simulation/simulate-transactions.js'
|
|
64
65
|
|
|
65
66
|
export {
|
|
66
67
|
isZeroAllowanceAsset,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { memoize } from '@exodus/basic-utils'
|
|
2
|
+
import { ABI } from '@exodus/ethereum-lib'
|
|
3
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
4
|
+
import SolidityContract from '@exodus/solidity-contract'
|
|
5
|
+
import assert from 'minimalistic-assert'
|
|
6
|
+
|
|
7
|
+
import EthLikeServerBase from '../exodus-eth-server/eth-like-server-base.js'
|
|
8
|
+
import { BLOCK_TAG_LATEST } from '../tx-send/nonce-utils.js'
|
|
9
|
+
|
|
10
|
+
const assertValidMulticall3BlockTag = (blockTag) =>
|
|
11
|
+
assert(blockTag === BLOCK_TAG_LATEST, 'blockTag must be "latest" for multicall3')
|
|
12
|
+
|
|
13
|
+
const SUPPORTED_METHODS = new Set(['eth_getBalance', 'eth_blockNumber', 'eth_call'])
|
|
14
|
+
|
|
15
|
+
const assertSupportedMethod = (method) => {
|
|
16
|
+
assert(
|
|
17
|
+
SUPPORTED_METHODS.has(method),
|
|
18
|
+
`method "${method}" is not supported for static call batching`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getMulticall3Contract = memoize(() => new SolidityContract(ABI.multicall3))
|
|
23
|
+
|
|
24
|
+
export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
|
|
25
|
+
#requestContext = null
|
|
26
|
+
#requests = []
|
|
27
|
+
|
|
28
|
+
#withRequestContext(requestContext, ...args) {
|
|
29
|
+
assert(this.#requestContext === null)
|
|
30
|
+
this.#requestContext = requestContext
|
|
31
|
+
|
|
32
|
+
const result = super[requestContext](...args)
|
|
33
|
+
|
|
34
|
+
this.#requestContext = null
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_serializeRequest({ context: _, originalRequest }) {
|
|
39
|
+
return super.buildRequest(originalRequest)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async _execute({ requests, server }) {
|
|
43
|
+
return server.sendBatchRequest(requests.map(({ request }) => request))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
buildRequest(originalRequest) {
|
|
47
|
+
const { method, params } = originalRequest
|
|
48
|
+
assertSupportedMethod(method)
|
|
49
|
+
|
|
50
|
+
if (method === 'eth_getBalance' || method === 'eth_call') {
|
|
51
|
+
const tag = params[1]
|
|
52
|
+
assertValidMulticall3BlockTag(tag)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const context = this.#requestContext ?? method
|
|
56
|
+
|
|
57
|
+
void this.#requests.push({
|
|
58
|
+
context,
|
|
59
|
+
originalRequest,
|
|
60
|
+
request: this._serializeRequest({ context, originalRequest }),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return this
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
balanceOfRequest(...args) {
|
|
67
|
+
return this.#withRequestContext('balanceOfRequest', ...args)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
length() {
|
|
71
|
+
return this.#requests.length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reset() {
|
|
75
|
+
this.#requests.length = 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async flush(server) {
|
|
79
|
+
assert(server, 'expected server')
|
|
80
|
+
|
|
81
|
+
const requests = [...this.#requests]
|
|
82
|
+
this.reset()
|
|
83
|
+
|
|
84
|
+
if (requests.length === 0) return []
|
|
85
|
+
|
|
86
|
+
const results = await this._execute({ requests, server })
|
|
87
|
+
|
|
88
|
+
return results.map((result, i) => {
|
|
89
|
+
const request = requests[i]
|
|
90
|
+
assert(request)
|
|
91
|
+
|
|
92
|
+
const { context, originalRequest } = request
|
|
93
|
+
|
|
94
|
+
if (context === 'eth_getBalance') return `0x${BigInt(result).toString(16)}`
|
|
95
|
+
if (context === 'eth_blockNumber') return `0x${BigInt(result).toString(16)}`
|
|
96
|
+
if (context === 'balanceOfRequest') {
|
|
97
|
+
const {
|
|
98
|
+
params: [{ to: tokenAddress }],
|
|
99
|
+
} = originalRequest
|
|
100
|
+
return {
|
|
101
|
+
confirmed: {
|
|
102
|
+
[tokenAddress]: BigInt(result).toString(),
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class EthLikeMulticall3RpcRequestAccumulator extends EthLikeRpcRequestAccumulator {
|
|
113
|
+
#multicall3Address
|
|
114
|
+
#multicall3Contract
|
|
115
|
+
|
|
116
|
+
constructor({ multicall3Address }) {
|
|
117
|
+
super()
|
|
118
|
+
|
|
119
|
+
assert(typeof multicall3Address === 'string')
|
|
120
|
+
this.#multicall3Address = multicall3Address
|
|
121
|
+
this.#multicall3Contract = getMulticall3Contract()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_serializeRequest({ context: _, originalRequest: { method, params } }) {
|
|
125
|
+
switch (method) {
|
|
126
|
+
case 'eth_getBalance':
|
|
127
|
+
return {
|
|
128
|
+
address: this.#multicall3Address,
|
|
129
|
+
calldata: bufferToHex(this.#multicall3Contract.getEthBalance.build(params[0])),
|
|
130
|
+
}
|
|
131
|
+
case 'eth_blockNumber':
|
|
132
|
+
return {
|
|
133
|
+
address: this.#multicall3Address,
|
|
134
|
+
calldata: bufferToHex(this.#multicall3Contract.getBlockNumber.build()),
|
|
135
|
+
}
|
|
136
|
+
case 'eth_call': {
|
|
137
|
+
const [data] = params
|
|
138
|
+
const { to, data: calldata } = data
|
|
139
|
+
return { address: to, calldata }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
assert(false)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async _execute({ requests, server }) {
|
|
148
|
+
const data = await server.ethCall({
|
|
149
|
+
data: bufferToHex(
|
|
150
|
+
this.#multicall3Contract.aggregate.build(
|
|
151
|
+
requests.map(({ request: { address, calldata } }) => [address, calldata])
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
to: this.#multicall3Address,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const multicall3Result = this.#multicall3Contract.decodeOutput({ data, method: 'aggregate' })
|
|
158
|
+
|
|
159
|
+
assert(
|
|
160
|
+
Array.isArray(multicall3Result) && multicall3Result.length === 2,
|
|
161
|
+
'invalid multicall3Result'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const results = multicall3Result[1].map((buf) => bufferToHex(buf))
|
|
165
|
+
assert(results.length === requests.length)
|
|
166
|
+
|
|
167
|
+
return results
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -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,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
|
+
}
|