@exodus/ethereum-api 8.64.4 → 8.64.6
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 -2
- package/src/exodus-eth-server/api-coin-nodes.js +5 -0
- package/src/exodus-eth-server/api.js +5 -0
- package/src/exodus-eth-server/clarity.js +5 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/fee-utils.js +0 -9
- package/src/server-based-fee-monitor.js +2 -10
- package/src/tx-log/clarity-monitor-v2.js +13 -2
- package/src/tx-log/clarity-monitor.js +13 -3
- package/src/tx-log/clarity-utils/index.js +1 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +59 -0
- package/src/tx-send/nonce-utils.js +4 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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.64.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.5...@exodus/ethereum-api@8.64.6) (2026-02-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: avoid race conditions resulting in a `tipGasPrice` of `0` where possible (#7458)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.64.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.4...@exodus/ethereum-api@8.64.5) (2026-02-11)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: future transaction replaces pending evm transactions (#7406)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.64.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.3...@exodus/ethereum-api@8.64.4) (2026-02-09)
|
|
7
27
|
|
|
8
28
|
**Note:** Version bump only for package @exodus/ethereum-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.64.
|
|
3
|
+
"version": "8.64.6",
|
|
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",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "05a14c42a3963199e2b0b11d0a643884ebae8e46"
|
|
71
71
|
}
|
|
@@ -6,6 +6,7 @@ import lodash from 'lodash'
|
|
|
6
6
|
|
|
7
7
|
import { fromHexToString } from '../number-utils.js'
|
|
8
8
|
import { errorMessageToSafeHint } from './errors.js'
|
|
9
|
+
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
9
10
|
|
|
10
11
|
const { isEmpty } = lodash
|
|
11
12
|
|
|
@@ -166,6 +167,10 @@ export default class ApiCoinNodesServer extends EventEmitter {
|
|
|
166
167
|
return this.sendRequest(request)
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
async getGasPriceEstimation() {
|
|
171
|
+
return getFallbackGasPriceEstimation({ server: this })
|
|
172
|
+
}
|
|
173
|
+
|
|
169
174
|
// for fee monitor
|
|
170
175
|
getGasPrice = this.gasPrice
|
|
171
176
|
|
|
@@ -6,6 +6,7 @@ import SolidityContract from '@exodus/solidity-contract'
|
|
|
6
6
|
import ms from 'ms'
|
|
7
7
|
|
|
8
8
|
import { fromHexToString } from '../number-utils.js'
|
|
9
|
+
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
9
10
|
import createWebSocket from './ws.js'
|
|
10
11
|
|
|
11
12
|
const RETRY_DELAYS = ['10s']
|
|
@@ -120,6 +121,10 @@ export function create(defaultURL, ensAssetName) {
|
|
|
120
121
|
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|
|
121
122
|
},
|
|
122
123
|
|
|
124
|
+
async getGasPriceEstimation() {
|
|
125
|
+
return getFallbackGasPriceEstimation({ server: this })
|
|
126
|
+
},
|
|
127
|
+
|
|
123
128
|
// for fee monitor
|
|
124
129
|
async getGasPrice() {
|
|
125
130
|
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|
|
@@ -6,6 +6,7 @@ import io from 'socket.io-client'
|
|
|
6
6
|
|
|
7
7
|
import { fromHexToString } from '../number-utils.js'
|
|
8
8
|
import { errorMessageToSafeHint } from './errors.js'
|
|
9
|
+
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
9
10
|
|
|
10
11
|
export const RPC_REQUEST_TIMEOUT = 'RPC_REQUEST_TIMEOUT'
|
|
11
12
|
|
|
@@ -174,6 +175,10 @@ export default class ClarityServer extends EventEmitter {
|
|
|
174
175
|
return fee?.gasPrice
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
async getGasPriceEstimation() {
|
|
179
|
+
return getFallbackGasPriceEstimation({ server: this })
|
|
180
|
+
}
|
|
181
|
+
|
|
177
182
|
async sendRpcRequest(rpcRequest) {
|
|
178
183
|
const rpcSocket = this.connectRpc()
|
|
179
184
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
const desc = (a, b) => (a > b ? -1 : b > a ? 1 : 0)
|
|
4
|
+
|
|
5
|
+
export const getFallbackGasPriceEstimation = async ({ server }) => {
|
|
6
|
+
const [latestBlock, gasPrice] = await Promise.all([server.getLatestBlock(), server.getGasPrice()])
|
|
7
|
+
|
|
8
|
+
assert(latestBlock, 'expected latestBlock')
|
|
9
|
+
assert(gasPrice, 'expected gasPrice')
|
|
10
|
+
|
|
11
|
+
const baseFeePerGas = latestBlock.baseFeePerGas
|
|
12
|
+
if (!baseFeePerGas) return { gasPrice }
|
|
13
|
+
|
|
14
|
+
const [max, min] = [BigInt(gasPrice), BigInt(baseFeePerGas)].sort(desc)
|
|
15
|
+
|
|
16
|
+
const toHex = (b) => `0x${b.toString(16)}`
|
|
17
|
+
|
|
18
|
+
// TODO: Use `eth_feeHistory` or `eth_maxPriorityFeePerGas`
|
|
19
|
+
// instead (requires allowlist at the RPC).
|
|
20
|
+
// HACK: Infer the RPC's implicit `tipGasPrice`:
|
|
21
|
+
// https://github.com/ethereum/go-ethereum/blob/d3dd48e59db28ea04bd92e4337cdd488ccb8fbec/internal/ethapi/api.go#L69C1-L79C2
|
|
22
|
+
const maxPriorityFeePerGas50Percentile = max - min
|
|
23
|
+
|
|
24
|
+
const rewardPercentiles = {
|
|
25
|
+
25: toHex(maxPriorityFeePerGas50Percentile / BigInt(2)),
|
|
26
|
+
50: toHex(maxPriorityFeePerGas50Percentile),
|
|
27
|
+
75: toHex((maxPriorityFeePerGas50Percentile * BigInt(3)) / BigInt(2)),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { gasPrice, baseFeePerGas, rewardPercentiles }
|
|
31
|
+
}
|
package/src/fee-utils.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
export const shouldFetchEthLikeFallbackGasPrices = async ({ eip1559Enabled, server }) => {
|
|
4
|
-
const [gasPrice, baseFeePerGas] = await Promise.all([
|
|
5
|
-
server.getGasPrice(),
|
|
6
|
-
eip1559Enabled ? server.getBaseFeePerGas() : undefined,
|
|
7
|
-
])
|
|
8
|
-
|
|
9
|
-
return { gasPrice, baseFeePerGas }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
3
|
export const applyMultiplierToPrice = ({ feeAsset, gasPriceMultiplier, price }) => {
|
|
13
4
|
assert(typeof price === 'string', 'price should be a string')
|
|
14
5
|
return feeAsset.currency
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { FeeMonitor } from '@exodus/asset-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
calculateEthLikeFeeMonitorUpdate,
|
|
6
|
-
shouldFetchEthLikeFallbackGasPrices,
|
|
7
|
-
} from './fee-utils.js'
|
|
4
|
+
import { calculateEthLikeFeeMonitorUpdate } from './fee-utils.js'
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Generic eth server based fee monitor.
|
|
@@ -25,11 +22,6 @@ export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, a
|
|
|
25
22
|
assert(server, 'server is required')
|
|
26
23
|
assert(aci, 'aci is required')
|
|
27
24
|
|
|
28
|
-
const shouldFetchGasPrices = async () => {
|
|
29
|
-
const { eip1559Enabled } = await aci.getFeeConfig({ assetName: asset.name })
|
|
30
|
-
return shouldFetchEthLikeFallbackGasPrices({ eip1559Enabled, server })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
25
|
const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends FeeMonitor {
|
|
34
26
|
constructor({ updateFee }) {
|
|
35
27
|
assert(updateFee, 'updateFee is required')
|
|
@@ -44,7 +36,7 @@ export const serverBasedFeeMonitorFactoryFactory = ({ asset, interval, server, a
|
|
|
44
36
|
return calculateEthLikeFeeMonitorUpdate({
|
|
45
37
|
assetClientInterface: aci,
|
|
46
38
|
feeAsset: asset,
|
|
47
|
-
fetchedGasPrices: await
|
|
39
|
+
fetchedGasPrices: await server.getGasPriceEstimation(),
|
|
48
40
|
})
|
|
49
41
|
}
|
|
50
42
|
}
|
|
@@ -6,7 +6,11 @@ import assert from 'minimalistic-assert'
|
|
|
6
6
|
import WsGateway from '../exodus-eth-server/ws-gateway.js'
|
|
7
7
|
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
8
8
|
import { fromHexToString } from '../number-utils.js'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
filterEffects,
|
|
11
|
+
getLogItemsFromServerTx,
|
|
12
|
+
normalizeTransactionsResponse,
|
|
13
|
+
} from './clarity-utils/index.js'
|
|
10
14
|
import {
|
|
11
15
|
checkPendingTransactions,
|
|
12
16
|
excludeUnchangedTokenBalances,
|
|
@@ -160,7 +164,14 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
160
164
|
const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
|
|
161
165
|
|
|
162
166
|
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
163
|
-
|
|
167
|
+
|
|
168
|
+
const { allTxs } = await normalizeTransactionsResponse({
|
|
169
|
+
asset: this.asset,
|
|
170
|
+
fromAddress: derivedData.ourWalletAddress,
|
|
171
|
+
response,
|
|
172
|
+
walletAccount,
|
|
173
|
+
})
|
|
174
|
+
|
|
164
175
|
const cursor = response.cursor
|
|
165
176
|
|
|
166
177
|
await this.processAndFillTransactionsToState({
|
|
@@ -5,7 +5,11 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
|
|
6
6
|
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
7
7
|
import { fromHexToString } from '../number-utils.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
filterEffects,
|
|
10
|
+
getLogItemsFromServerTx,
|
|
11
|
+
normalizeTransactionsResponse,
|
|
12
|
+
} from './clarity-utils/index.js'
|
|
9
13
|
import {
|
|
10
14
|
checkPendingTransactions,
|
|
11
15
|
excludeUnchangedTokenBalances,
|
|
@@ -148,10 +152,16 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
148
152
|
return map
|
|
149
153
|
}, new Map())
|
|
150
154
|
const assetName = this.asset.name
|
|
151
|
-
|
|
152
155
|
const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
|
|
153
156
|
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
154
|
-
|
|
157
|
+
|
|
158
|
+
const { allTxs } = await normalizeTransactionsResponse({
|
|
159
|
+
asset: this.asset,
|
|
160
|
+
fromAddress: derivedData.ourWalletAddress,
|
|
161
|
+
response,
|
|
162
|
+
walletAccount,
|
|
163
|
+
})
|
|
164
|
+
|
|
155
165
|
const hasNewTxs = allTxs.length > 0
|
|
156
166
|
|
|
157
167
|
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
// Converts the `pending` and `confirmed` transactions returned
|
|
4
|
+
// by Clarity into a single contiguous array of transactions.
|
|
5
|
+
//
|
|
6
|
+
// Since it is possible for Clarity to return very old pending
|
|
7
|
+
// transactions that our own RPC has forgotten (see `--txpool.lifetime`),
|
|
8
|
+
// we ensure that all transactions we expose to the consumer have
|
|
9
|
+
// `nonce`s do not exceed the maximum pending nonce that's currently
|
|
10
|
+
// maintained at the RPC.
|
|
11
|
+
//
|
|
12
|
+
// This avoids the "future transaction replaces pending" error.
|
|
13
|
+
export const normalizeTransactionsResponse = async ({
|
|
14
|
+
asset,
|
|
15
|
+
fromAddress,
|
|
16
|
+
response,
|
|
17
|
+
walletAccount,
|
|
18
|
+
}) => {
|
|
19
|
+
assert(asset, 'expected asset')
|
|
20
|
+
assert(fromAddress, 'expected fromAddress')
|
|
21
|
+
assert(response, 'expected response')
|
|
22
|
+
assert(walletAccount, 'expected walletAccount')
|
|
23
|
+
|
|
24
|
+
const { baseAsset } = asset
|
|
25
|
+
|
|
26
|
+
// NOTE: We query the current pending `nonce` from the RPC
|
|
27
|
+
// to sanity check the contents of the `txLog`.
|
|
28
|
+
const pendingNonce = await baseAsset
|
|
29
|
+
.getNonce({
|
|
30
|
+
asset: baseAsset,
|
|
31
|
+
fromAddress,
|
|
32
|
+
walletAccount,
|
|
33
|
+
forceFromNode: true,
|
|
34
|
+
})
|
|
35
|
+
.catch(() => null)
|
|
36
|
+
|
|
37
|
+
const allTxs = [...response.transactions.pending, ...response.transactions.confirmed].filter(
|
|
38
|
+
(tx) => {
|
|
39
|
+
// If the transaction isn't one we've sent, then ignore.
|
|
40
|
+
if (tx.from?.toLowerCase() !== fromAddress.toLowerCase()) return true
|
|
41
|
+
|
|
42
|
+
const nonce = parseInt(tx.nonce, 10)
|
|
43
|
+
if (!Number.isFinite(nonce) || !Number.isInteger(nonce)) return false
|
|
44
|
+
|
|
45
|
+
// HACK: If we were unable to determine the `pendingNonce`, then
|
|
46
|
+
// allow send transactions through by default without
|
|
47
|
+
// further validation.
|
|
48
|
+
if (pendingNonce === null) return true
|
|
49
|
+
|
|
50
|
+
// NOTE: If the `tx` response contains pending transactions at
|
|
51
|
+
// a nonce offset that's greater than what's considered
|
|
52
|
+
// pending at the RPC, these transactions are no longer
|
|
53
|
+
// viable and should be replaced.
|
|
54
|
+
return nonce < pendingNonce
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return { allTxs }
|
|
59
|
+
}
|
|
@@ -21,14 +21,11 @@ export const resolveNonce = async ({
|
|
|
21
21
|
}) => {
|
|
22
22
|
assertValidBlockTag(tag)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
asset.baseAsset
|
|
26
|
-
|
|
27
|
-
: 0
|
|
28
|
-
|
|
29
|
-
const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
|
|
24
|
+
if (asset.baseAsset?.api?.features?.noHistory || forceFromNode) {
|
|
25
|
+
return getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
|
|
26
|
+
}
|
|
30
27
|
|
|
31
|
-
return
|
|
28
|
+
return getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
|