@exodus/ethereum-api 8.35.2-alpha.0 → 8.37.0-patch.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 +21 -26
- package/package.json +5 -7
- package/src/create-asset-utils.js +55 -0
- package/src/create-asset.js +23 -36
- package/src/exodus-eth-server/clarity-v2.js +50 -19
- package/src/exodus-eth-server/index.js +1 -2
- package/src/gas-estimation.js +96 -44
- package/src/get-balances.js +58 -7
- package/src/get-fee-async.js +9 -61
- package/src/get-fee.js +1 -1
- package/src/index.js +1 -1
- package/src/staking/ethereum/service.js +4 -2
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +16 -5
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-send/get-fee-info.js +1 -0
- package/src/tx-send/nonce-utils.js +21 -3
- package/src/tx-send/tx-send.js +8 -2
- package/src/exodus-eth-server/ws-gateway.js +0 -241
- package/src/tx-log/clarity-monitor-v2.js +0 -425
package/CHANGELOG.md
CHANGED
|
@@ -3,36 +3,31 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [8.
|
|
6
|
+
## [8.37.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.36.0...@exodus/ethereum-api@8.37.0) (2025-06-12)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
### Features
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
* ethereum api cleaning up ([#5832](https://github.com/ExodusMovement/assets/issues/5832)) ([29d167e](https://github.com/ExodusMovement/assets/commit/29d167e573993be4f49ea87523f5a2156ce7cef1))
|
|
32
|
-
* improve evm gas estimation when using arbitrary addresses ([#5842](https://github.com/ExodusMovement/assets/issues/5842)) ([9518cc1](https://github.com/ExodusMovement/assets/commit/9518cc1f6f5d861edc7a9e9fd563ee556901d5e5))
|
|
33
|
-
* reconnection issues ([5cbdbfc](https://github.com/ExodusMovement/assets/commit/5cbdbfc8a1109c8c4e95e9b93c4b6269037e72b0))
|
|
34
|
-
* remove extra async. Use address ([10a1380](https://github.com/ExodusMovement/assets/commit/10a13803d4fd913cf98dccc130eb22021a72245d))
|
|
35
|
-
* update code and tests to support send method instead of emit ([379406a](https://github.com/ExodusMovement/assets/commit/379406a020f8c56048b2373e8ccb2a284ea61a5d))
|
|
11
|
+
|
|
12
|
+
* feat: remote config feeData.gasLimits (#5830)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.36.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.1...@exodus/ethereum-api@8.36.0) (2025-06-11)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: ethereum api use balances and nonces data (#5727)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
* fix: ethereum api cleaning up (#5832)
|
|
29
|
+
|
|
30
|
+
* fix: improve evm gas estimation when using arbitrary addresses (#5842)
|
|
36
31
|
|
|
37
32
|
|
|
38
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.37.0-patch.0",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,18 +28,17 @@
|
|
|
28
28
|
"@exodus/bip44-constants": "^195.0.0",
|
|
29
29
|
"@exodus/crypto": "^1.0.0-rc.13",
|
|
30
30
|
"@exodus/currency": "^6.0.1",
|
|
31
|
-
"@exodus/ethereum-lib": "^5.
|
|
31
|
+
"@exodus/ethereum-lib": "^5.12.0",
|
|
32
32
|
"@exodus/ethereum-meta": "^2.5.0",
|
|
33
33
|
"@exodus/ethereumholesky-meta": "^2.0.2",
|
|
34
34
|
"@exodus/ethereumjs": "^1.0.0",
|
|
35
35
|
"@exodus/fetch": "^1.3.0",
|
|
36
|
-
"@exodus/models": "^12.0
|
|
36
|
+
"@exodus/models": "^12.13.0",
|
|
37
37
|
"@exodus/simple-retry": "^0.0.6",
|
|
38
38
|
"@exodus/solidity-contract": "^1.1.3",
|
|
39
39
|
"@exodus/web3-ethereum-utils": "^4.2.1",
|
|
40
40
|
"bn.js": "^5.2.1",
|
|
41
41
|
"delay": "^4.0.1",
|
|
42
|
-
"eventemitter3": "^4.0.7",
|
|
43
42
|
"events": "^1.1.1",
|
|
44
43
|
"idna-uts46-hx": "^2.3.1",
|
|
45
44
|
"lodash": "^4.17.15",
|
|
@@ -50,7 +49,7 @@
|
|
|
50
49
|
"ws": "^6.1.0"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@exodus/assets-testing": "
|
|
52
|
+
"@exodus/assets-testing": "workspace:^",
|
|
54
53
|
"@exodus/bsc-meta": "^2.1.2",
|
|
55
54
|
"@exodus/ethereumarbone-meta": "^2.0.3",
|
|
56
55
|
"@exodus/ethereumgoerli-meta": "^2.0.1",
|
|
@@ -64,6 +63,5 @@
|
|
|
64
63
|
"repository": {
|
|
65
64
|
"type": "git",
|
|
66
65
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
67
|
-
}
|
|
68
|
-
"gitHead": "d88b71e86cdc0c3289f8ddaac4e0e1b642da04f2"
|
|
66
|
+
}
|
|
69
67
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
|
|
3
|
+
import { ValidMonitorTypes } from './exodus-eth-server/index.js'
|
|
4
|
+
|
|
5
|
+
// Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
|
|
6
|
+
// to use for a given config.
|
|
7
|
+
export const resolveMonitorSettings = (
|
|
8
|
+
{
|
|
9
|
+
asset,
|
|
10
|
+
configWithOverrides,
|
|
11
|
+
defaultMonitorInterval,
|
|
12
|
+
defaultMonitorType,
|
|
13
|
+
defaultServerUrl,
|
|
14
|
+
} = Object.create(null)
|
|
15
|
+
) => {
|
|
16
|
+
assert(asset, 'expected asset')
|
|
17
|
+
assert(
|
|
18
|
+
ValidMonitorTypes.includes(defaultMonitorType),
|
|
19
|
+
`defaultMonitorType ${defaultMonitorType} for ${asset.name} is invalid.`
|
|
20
|
+
)
|
|
21
|
+
assert(defaultServerUrl, `expected default serverUrl for ${asset.name}`)
|
|
22
|
+
|
|
23
|
+
const overrideMonitorType = configWithOverrides?.monitorType
|
|
24
|
+
const overrideServerUrl = configWithOverrides?.serverUrl
|
|
25
|
+
const overrideMonitorInterval = configWithOverrides?.monitorInterval
|
|
26
|
+
|
|
27
|
+
const defaultResolution = {
|
|
28
|
+
// NOTE: Regardless of the `monitorType`, the `monitorInterval`
|
|
29
|
+
// will be respected.
|
|
30
|
+
monitorInterval: overrideMonitorInterval ?? defaultMonitorInterval,
|
|
31
|
+
monitorType: defaultMonitorType,
|
|
32
|
+
serverUrl: defaultServerUrl,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!overrideMonitorType && overrideServerUrl) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`Received an \`overrideServerUrl\`, but not the \`monitorType\` for ${asset.name}. Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`
|
|
38
|
+
)
|
|
39
|
+
return defaultResolution
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If we don't attempt to override the `monitorType`, we resort
|
|
43
|
+
// to the default configuration.
|
|
44
|
+
if (!overrideMonitorType) return defaultResolution
|
|
45
|
+
|
|
46
|
+
if (!ValidMonitorTypes.includes(overrideMonitorType)) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`"${overrideMonitorType}" is not a valid \`MonitorType\`. Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`
|
|
49
|
+
)
|
|
50
|
+
return defaultResolution
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Permit the `monitorType` and `serverUrl` to be overrided.
|
|
54
|
+
return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
|
|
55
|
+
}
|
package/src/create-asset.js
CHANGED
|
@@ -22,10 +22,10 @@ import assert from 'minimalistic-assert'
|
|
|
22
22
|
import ms from 'ms'
|
|
23
23
|
|
|
24
24
|
import { addressHasHistoryFactory } from './address-has-history.js'
|
|
25
|
+
import { resolveMonitorSettings } from './create-asset-utils.js'
|
|
25
26
|
import { createTokenFactory } from './create-token-factory.js'
|
|
26
27
|
import { createCustomFeesApi } from './custom-fees.js'
|
|
27
|
-
import { createEvmServer
|
|
28
|
-
import { createWsGateway } from './exodus-eth-server/ws-gateway.js'
|
|
28
|
+
import { createEvmServer } from './exodus-eth-server/index.js'
|
|
29
29
|
import { createFeeData } from './fee-data-factory.js'
|
|
30
30
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
31
31
|
import { getBalancesFactory } from './get-balances.js'
|
|
@@ -36,7 +36,6 @@ import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/ind
|
|
|
36
36
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
37
37
|
import { createStakingApi } from './staking-api.js'
|
|
38
38
|
import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
39
|
-
import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
|
|
40
39
|
import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
|
|
41
40
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
|
|
42
41
|
import { txSendFactory } from './tx-send/index.js'
|
|
@@ -59,18 +58,19 @@ export const createAssetFactory = ({
|
|
|
59
58
|
isMaxFeeAsset = false,
|
|
60
59
|
isTestnet = false,
|
|
61
60
|
l1GasOracleAddress, // l1 extra fee for base and optostakingConfiguration = {},
|
|
62
|
-
monitorInterval,
|
|
63
|
-
monitorType = 'magnifier',
|
|
61
|
+
monitorInterval: defaultMonitorInterval,
|
|
62
|
+
monitorType: defaultMonitorType = 'magnifier',
|
|
64
63
|
nfts: defaultNfts = false,
|
|
65
|
-
serverUrl,
|
|
64
|
+
serverUrl: defaultServerUrl,
|
|
66
65
|
stakingConfiguration = {},
|
|
67
66
|
useEip1191ChainIdChecksum = false,
|
|
68
67
|
forceGasLimitEstimation = false,
|
|
69
68
|
supportsCustomFees: defaultSupportsCustomFees = false,
|
|
69
|
+
useAbsoluteBalanceAndNonce = false,
|
|
70
70
|
}) => {
|
|
71
71
|
assert(assetsList, 'assetsList is required')
|
|
72
72
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
73
|
-
assert(
|
|
73
|
+
assert(defaultServerUrl, 'serverUrl is required')
|
|
74
74
|
assert(confirmationsNumber, 'confirmationsNumber is required')
|
|
75
75
|
|
|
76
76
|
const base = assetsList.find((asset) => asset.name === asset.baseAssetName)
|
|
@@ -99,38 +99,32 @@ export const createAssetFactory = ({
|
|
|
99
99
|
) => {
|
|
100
100
|
const assets = connectAssetsList(assetsList)
|
|
101
101
|
|
|
102
|
+
const configWithOverrides = { ...defaultConfig, ...config }
|
|
103
|
+
|
|
102
104
|
const {
|
|
103
105
|
allowMetaMaskCompat,
|
|
104
106
|
supportsStaking,
|
|
105
107
|
nfts,
|
|
106
108
|
customTokens,
|
|
107
109
|
supportsCustomFees,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
monitorInterval: overrideMonitorInterval,
|
|
111
|
-
} = {
|
|
112
|
-
...defaultConfig,
|
|
113
|
-
...config,
|
|
114
|
-
}
|
|
110
|
+
useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
|
|
111
|
+
} = configWithOverrides
|
|
115
112
|
|
|
116
113
|
const asset = assets[base.name]
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
115
|
+
const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
|
|
116
|
+
asset,
|
|
117
|
+
configWithOverrides,
|
|
118
|
+
defaultMonitorInterval,
|
|
119
|
+
defaultMonitorType,
|
|
120
|
+
defaultServerUrl,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (overrideUseAbsoluteBalanceAndNonce !== undefined) {
|
|
124
|
+
useAbsoluteBalanceAndNonce = overrideUseAbsoluteBalanceAndNonce
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
monitorType = overrideMonitorType || monitorType
|
|
132
127
|
const server = createEvmServer({ assetName: asset.name, serverUrl, monitorType })
|
|
133
|
-
const wsGatewayClient = createWsGateway()
|
|
134
128
|
|
|
135
129
|
const gasLimit = 21e3 // 21 KGas, enough only for sending ether to normal address
|
|
136
130
|
|
|
@@ -152,6 +146,7 @@ export const createAssetFactory = ({
|
|
|
152
146
|
const getBalances = getBalancesFactory({
|
|
153
147
|
monitorType,
|
|
154
148
|
config,
|
|
149
|
+
useAbsoluteBalance: useAbsoluteBalanceAndNonce,
|
|
155
150
|
})
|
|
156
151
|
|
|
157
152
|
const { createToken, getTokens } = createTokenFactory(
|
|
@@ -217,15 +212,6 @@ export const createAssetFactory = ({
|
|
|
217
212
|
...args,
|
|
218
213
|
})
|
|
219
214
|
break
|
|
220
|
-
case 'clarity-v3':
|
|
221
|
-
monitor = new ClarityMonitorV2({
|
|
222
|
-
assetClientInterface,
|
|
223
|
-
interval: ms(monitorInterval || '5m'),
|
|
224
|
-
server,
|
|
225
|
-
wsGatewayClient,
|
|
226
|
-
...args,
|
|
227
|
-
})
|
|
228
|
-
break
|
|
229
215
|
case 'no-history':
|
|
230
216
|
monitor = new EthereumNoHistoryMonitor({
|
|
231
217
|
assetClientInterface,
|
|
@@ -266,6 +252,7 @@ export const createAssetFactory = ({
|
|
|
266
252
|
const sendTx = txSendFactory({
|
|
267
253
|
assetClientInterface,
|
|
268
254
|
createUnsignedTx,
|
|
255
|
+
useAbsoluteBalanceAndNonce,
|
|
269
256
|
})
|
|
270
257
|
|
|
271
258
|
const estimateL1DataFee = l1GasOracleAddress
|
|
@@ -2,6 +2,33 @@ import { retry } from '@exodus/simple-retry'
|
|
|
2
2
|
|
|
3
3
|
import ClarityServer from './clarity.js'
|
|
4
4
|
|
|
5
|
+
export const encodeCursor = (blockNumberBigInt, isLegacy = false) => {
|
|
6
|
+
if (typeof blockNumberBigInt !== 'bigint') throw new Error('expected bigint')
|
|
7
|
+
|
|
8
|
+
if (isLegacy) {
|
|
9
|
+
const cursor = Buffer.alloc(26)
|
|
10
|
+
cursor.writeBigInt64LE(blockNumberBigInt, 10)
|
|
11
|
+
return cursor
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cursor = Buffer.alloc(8)
|
|
15
|
+
cursor.writeBigUInt64LE(blockNumberBigInt, 0)
|
|
16
|
+
return cursor
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const decodeCursor = (cursor) => {
|
|
20
|
+
if (Buffer.isBuffer(cursor)) {
|
|
21
|
+
// New format: buffer containing only the block number
|
|
22
|
+
if (cursor.length === 8) return { blockNumber: cursor.readBigUInt64LE(0) }
|
|
23
|
+
|
|
24
|
+
// Old format: buffer with length 26
|
|
25
|
+
if (cursor.length === 26) return { blockNumber: cursor.readBigInt64LE(10) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Doesn't match any known format, return default
|
|
29
|
+
return { blockNumber: BigInt(0) }
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
const getTextFromResponse = async (response) => {
|
|
6
33
|
try {
|
|
7
34
|
const responseBody = await response.text()
|
|
@@ -44,18 +71,25 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
44
71
|
this.updateBaseApiPath()
|
|
45
72
|
}
|
|
46
73
|
|
|
74
|
+
getTransactionsAtBlockNumber = async ({ address, blockNumber, withInput = true }) => {
|
|
75
|
+
const url = new URL(`${this.baseApiPath}/addresses/${encodeURIComponent(address)}/transactions`)
|
|
76
|
+
url.searchParams.set('cursor', blockNumber)
|
|
77
|
+
url.searchParams.set('withInput', Boolean(withInput).toString())
|
|
78
|
+
return fetchJsonRetry(url)
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
async getAllTransactions({ address, cursor }) {
|
|
48
|
-
let { blockNumber } =
|
|
82
|
+
let { blockNumber } = decodeCursor(cursor)
|
|
49
83
|
blockNumber = blockNumber.toString()
|
|
50
84
|
|
|
51
85
|
let transactions = []
|
|
52
86
|
|
|
53
87
|
while (true) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
const { transactions: txs, cursor: nextBlockNumber } =
|
|
89
|
+
await this.getTransactionsAtBlockNumber({
|
|
90
|
+
address,
|
|
91
|
+
blockNumber,
|
|
92
|
+
})
|
|
59
93
|
|
|
60
94
|
if (txs.length === 0) {
|
|
61
95
|
// fetch until no more new transactions
|
|
@@ -91,19 +125,6 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
#decodeCursor(cursor) {
|
|
95
|
-
if (Buffer.isBuffer(cursor)) {
|
|
96
|
-
// New format: buffer containing only the block number
|
|
97
|
-
if (cursor.length === 8) return { blockNumber: cursor.readBigUInt64LE(0) }
|
|
98
|
-
|
|
99
|
-
// Old format: buffer with length 26
|
|
100
|
-
if (cursor.length === 26) return { blockNumber: cursor.readBigInt64LE(10) }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Doesn't match any known format, return default
|
|
104
|
-
return { blockNumber: BigInt(0) }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
128
|
async sendHttpRequest({ path, method, body }) {
|
|
108
129
|
const url = new URL(`${this.baseApiPath}${path}`)
|
|
109
130
|
const fetchOptions = {
|
|
@@ -130,4 +151,14 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
130
151
|
body: request,
|
|
131
152
|
})
|
|
132
153
|
}
|
|
154
|
+
|
|
155
|
+
async getTransactionCount(...params) {
|
|
156
|
+
// nonce is called during tx send, use it in rest api
|
|
157
|
+
const request = this.getTransactionCountRequest(...params)
|
|
158
|
+
return this.sendHttpRequest({
|
|
159
|
+
path: '/rpc',
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: request,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
133
164
|
}
|
|
@@ -13,7 +13,7 @@ import ApiCoinNodesServer from './api-coin-nodes.js'
|
|
|
13
13
|
import ClarityServer from './clarity.js'
|
|
14
14
|
import ClarityServerV2 from './clarity-v2.js'
|
|
15
15
|
|
|
16
|
-
export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', '
|
|
16
|
+
export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'magnifier']
|
|
17
17
|
|
|
18
18
|
export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
19
19
|
assert(assetName, 'assetName is required')
|
|
@@ -25,7 +25,6 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
|
25
25
|
case 'clarity':
|
|
26
26
|
return new ClarityServer({ baseAssetName: assetName, uri: serverUrl })
|
|
27
27
|
case 'clarity-v2':
|
|
28
|
-
case 'clarity-v3':
|
|
29
28
|
return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
|
|
30
29
|
case 'magnifier':
|
|
31
30
|
return create(serverUrl, assetName)
|
package/src/gas-estimation.js
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
import { currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
2
2
|
import { bufferToHex, toBuffer } from '@exodus/ethereumjs/util'
|
|
3
3
|
|
|
4
|
-
import { estimateGas, isContractAddressCached } from './eth-like-util.js'
|
|
4
|
+
import { estimateGas, isContractAddressCached, isForwarderContractCached } from './eth-like-util.js'
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const DEFAULT_GAS_LIMIT_MULTIPLIER = 1.29
|
|
7
7
|
|
|
8
8
|
// 16 gas per non-zero byte (4 gas per zero byte) of "transaction input data"
|
|
9
9
|
const GAS_PER_NON_ZERO_BYTE = 16
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
|
|
12
12
|
|
|
13
|
+
// HACK: If a recipient address is not defined, we usually fall back to
|
|
14
|
+
// default address so gas estimation can still complete successfully
|
|
15
|
+
// without knowledge of which accounts are involved.
|
|
16
|
+
//
|
|
17
|
+
// However, we must be careful to select addresses which are unlikely
|
|
18
|
+
// to have existing obligations, such as popular dead addresses or the
|
|
19
|
+
// reserved addresses of precompiles, since these can influence gas
|
|
20
|
+
// estimation.
|
|
21
|
+
//
|
|
22
|
+
// Here, we use an address which is mostly all `1`s to make sure we can
|
|
23
|
+
// exaggerate the worst-case calldata cost (which is priced per high bit)
|
|
24
|
+
// whilst being unlikely to have any token balances.
|
|
25
|
+
//
|
|
26
|
+
// Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
|
|
27
|
+
// since this address is a whale.
|
|
28
|
+
export const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
|
|
29
|
+
|
|
13
30
|
// HACK: RPCs generally provide imprecise estimates
|
|
14
31
|
// for `gasUsed` (often these are insufficient).
|
|
15
|
-
export const scaleGasLimitEstimate = ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
export const scaleGasLimitEstimate = ({
|
|
33
|
+
estimatedGasLimit,
|
|
34
|
+
gasLimitMultiplier = DEFAULT_GAS_LIMIT_MULTIPLIER,
|
|
35
|
+
}) => {
|
|
36
|
+
const gasLimit = parseInt(estimatedGasLimit)
|
|
37
|
+
if (!Number.isInteger(gasLimit)) {
|
|
38
|
+
throw new TypeError('Invalid scaleGasLimitEstimate ' + estimatedGasLimit)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const multiplier = parseFloat(gasLimitMultiplier)
|
|
42
|
+
if (Number.isNaN(multiplier)) {
|
|
43
|
+
throw new TypeError('Invalid gasLimitMultiplier ' + gasLimitMultiplier)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Math.floor(gasLimit * multiplier)
|
|
47
|
+
}
|
|
26
48
|
|
|
27
49
|
// Starting with geth v1.9.14, if gasPrice is set for eth_estimateGas call, the call allowance will
|
|
28
50
|
// be calculated with account's balance divided by gasPrice. If user's balance is too low,
|
|
@@ -34,14 +56,8 @@ export async function estimateGasLimit(
|
|
|
34
56
|
toAddress,
|
|
35
57
|
amount,
|
|
36
58
|
data,
|
|
37
|
-
gasPrice = '0x'
|
|
38
|
-
extraPercentage
|
|
59
|
+
gasPrice = '0x'
|
|
39
60
|
) {
|
|
40
|
-
// move this to remote config
|
|
41
|
-
if (hardcodedGasLimits.has(asset.name)) {
|
|
42
|
-
return hardcodedGasLimits.get(asset.name)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
61
|
const opts = {
|
|
46
62
|
from: fromAddress,
|
|
47
63
|
to: toAddress,
|
|
@@ -51,62 +67,98 @@ export async function estimateGasLimit(
|
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
const estimatedGas = await estimateGas({ asset, ...opts })
|
|
70
|
+
return Number(BigInt(estimatedGas))
|
|
71
|
+
}
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fromAddress }) {
|
|
74
|
+
const gasLimitMultiplierWhenUnknownAddress =
|
|
75
|
+
feeData?.gasLimits?.[asset.name]?.gasLimitMultiplierWhenUnknownAddress
|
|
76
|
+
|
|
77
|
+
if (gasLimitMultiplierWhenUnknownAddress && (!fromAddress || !toAddress)) {
|
|
78
|
+
return gasLimitMultiplierWhenUnknownAddress
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const gasLimitMultiplier = feeData?.gasLimits?.[asset.name]?.gasLimitMultiplier
|
|
82
|
+
|
|
83
|
+
if (gasLimitMultiplier) {
|
|
84
|
+
return gasLimitMultiplier
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (asset.baseAsset.estimateL1DataFee) {
|
|
88
|
+
return 2
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isToken = asset.name !== asset.baseAsset.name
|
|
92
|
+
|
|
93
|
+
// calling forwarder contracts with a bumped gas limit causes 'Out Of Gas' error on chain
|
|
94
|
+
const hasForwarderContract =
|
|
95
|
+
!isToken && toAddress ? await isForwarderContractCached({ asset, address: toAddress }) : false
|
|
96
|
+
|
|
97
|
+
if (hasForwarderContract) {
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return DEFAULT_GAS_LIMIT_MULTIPLIER
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
export function resolveDefaultTxInput({ asset, toAddress, amount }) {
|
|
62
105
|
return isEthereumLikeToken(asset)
|
|
63
|
-
? bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount
|
|
106
|
+
? bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount?.toBaseString()))
|
|
64
107
|
: '0x'
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
export async function fetchGasLimit({
|
|
68
111
|
asset,
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
feeData,
|
|
113
|
+
fromAddress: providedFromAddress,
|
|
114
|
+
toAddress: providedToAddress,
|
|
71
115
|
txInput: providedTxInput,
|
|
72
|
-
amount,
|
|
116
|
+
amount: providedAmount,
|
|
73
117
|
bip70,
|
|
74
118
|
throwOnError = true,
|
|
75
|
-
extraPercentage,
|
|
76
119
|
}) {
|
|
77
120
|
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice)
|
|
78
121
|
return asset.name === 'ethereum' ? 65_000 : 130_000 // from on chain stats https://dune.xyz/queries/189123
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
const fixedGasLimit = feeData?.gasLimits?.[asset.name]?.fixedGasLimit
|
|
124
|
+
if (fixedGasLimit) {
|
|
125
|
+
return fixedGasLimit
|
|
126
|
+
}
|
|
81
127
|
|
|
128
|
+
const amount = providedAmount ?? asset.currency.ZERO
|
|
129
|
+
const fromAddress = providedFromAddress ?? ARBITRARY_ADDRESS
|
|
130
|
+
const toAddress = providedToAddress ?? ARBITRARY_ADDRESS
|
|
82
131
|
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
83
132
|
|
|
84
133
|
const defaultGasLimit = () => asset.gasLimit + GAS_PER_NON_ZERO_BYTE * toBuffer(txInput).length
|
|
85
134
|
|
|
86
135
|
const isContract = await isContractAddressCached({ asset, address: toAddress })
|
|
87
136
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
toAddress = asset.contract.address
|
|
91
|
-
} else if (!isContract && !asset.forceGasLimitEstimation) {
|
|
137
|
+
const isToken = isEthereumLikeToken(asset)
|
|
138
|
+
if (!isToken && !isContract && !asset.forceGasLimitEstimation) {
|
|
92
139
|
return defaultGasLimit()
|
|
93
140
|
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
const gasLimitMultiplier = await resolveGasLimitMultiplier({
|
|
143
|
+
asset,
|
|
144
|
+
feeData,
|
|
145
|
+
fromAddress: providedFromAddress,
|
|
146
|
+
toAddress: providedToAddress,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
150
|
+
const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
|
|
99
151
|
|
|
100
152
|
try {
|
|
101
|
-
|
|
153
|
+
const estimatedGasLimit = await estimateGasLimit(
|
|
102
154
|
asset,
|
|
103
155
|
fromAddress,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
txInput
|
|
107
|
-
gasPrice,
|
|
108
|
-
extraPercentage
|
|
156
|
+
txToAddress,
|
|
157
|
+
txAmount,
|
|
158
|
+
txInput
|
|
109
159
|
)
|
|
160
|
+
|
|
161
|
+
return scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
110
162
|
} catch (err) {
|
|
111
163
|
if (throwOnError) throw err
|
|
112
164
|
|