@exodus/ethereum-api 8.64.4 → 8.64.5

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 CHANGED
@@ -3,6 +3,16 @@
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.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.4...@exodus/ethereum-api@8.64.5) (2026-02-11)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: future transaction replaces pending evm transactions (#7406)
13
+
14
+
15
+
6
16
  ## [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
17
 
8
18
  **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.4",
3
+ "version": "8.64.5",
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": "ebcc3fe022dfc1e21e3ccdc00faf0769e8fb3092"
70
+ "gitHead": "12e4eb3740319bb74eaedcc04b53c0dc056ad1a1"
71
71
  }
@@ -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 { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
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
- const allTxs = [...response.transactions.pending, ...response.transactions.confirmed]
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 { filterEffects, getLogItemsFromServerTx } from './clarity-utils/index.js'
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
- const allTxs = [...response.transactions.pending, ...response.transactions.confirmed]
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({
@@ -6,3 +6,4 @@ export {
6
6
  getLatestCanonicalAbsoluteBalanceTx,
7
7
  getLatestCanonicalAbsoluteNonceTx,
8
8
  } from './absolute.js'
9
+ export { normalizeTransactionsResponse } from './normalize-transactions-response.js'
@@ -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
- const nonceFromNode =
25
- asset.baseAsset?.api?.features?.noHistory || forceFromNode
26
- ? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
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 Math.max(nonceFromNode, nonceFromLog)
28
+ return getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
32
29
  }
33
30
 
34
31
  const getLatestTxWithNonceChange = ({ reversedTxLog }) => {