@exodus/ethereum-api 8.62.3 → 8.63.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 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.63.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.4...@exodus/ethereum-api@8.63.0) (2026-01-14)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: add 'latest' evm nonce support (#7267)
13
+
14
+
15
+
16
+ ## [8.62.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.3...@exodus/ethereum-api@8.62.4) (2026-01-14)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: absolute canonical nonce (#7238)
23
+
24
+
25
+
6
26
  ## [8.62.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.2...@exodus/ethereum-api@8.62.3) (2026-01-13)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.62.3",
3
+ "version": "8.63.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",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "d1618b4c41ccde490c3e4aaa8dd239c0c5393a7b"
70
+ "gitHead": "db8bd0629b287cd2031a12ff2a3fed3fc765bbab"
71
71
  }
@@ -7,7 +7,7 @@ import { ClarityMonitor } from './tx-log/clarity-monitor.js'
7
7
  import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
8
8
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
9
9
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
10
- import { resolveNonce } from './tx-send/nonce-utils.js'
10
+ import { BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
11
11
 
12
12
  // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
13
13
  // to use for a given config.
@@ -184,7 +184,16 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
184
184
  assert(assetClientInterface, 'expected assetClientInterface')
185
185
  assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
186
186
 
187
- const getNonce = async ({ asset, fromAddress, walletAccount, triedNonce, forceFromNode }) => {
187
+ const getNonce = async ({
188
+ asset,
189
+ fromAddress,
190
+ walletAccount,
191
+ forceFromNode,
192
+ // NOTE: By default, Exodus assumes the client will by default want
193
+ // to send transactions on top of those which are currently
194
+ // pending at the public mempool.
195
+ tag = BLOCK_TAG_PENDING,
196
+ }) => {
188
197
  assert(asset, 'expected asset')
189
198
  assert(typeof fromAddress === 'string', 'expected fromAddress')
190
199
  assert(walletAccount, 'expected walletAccount')
@@ -198,13 +207,8 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
198
207
  asset,
199
208
  fromAddress,
200
209
  txLog,
201
- triedNonce,
202
210
  forceFromNode,
203
- // For assets where we'll fall back to querying the coin node, we
204
- // search for pending transactions. For base assets with history,
205
- // we'll fall back to the `TxLog` since this also has a knowledge
206
- // of which transactions are currently in pending.
207
- tag: 'pending',
211
+ tag,
208
212
  useAbsoluteNonce: useAbsoluteBalanceAndNonce,
209
213
  })
210
214
  }
@@ -51,7 +51,7 @@ export const createCustomFeesApi = ({ baseAsset }) => {
51
51
  },
52
52
  unit: 'gwei/gas',
53
53
  feeUnitPriceToNumber: (feeNumberUnit) => feeNumberUnit.toNumber(Gwei),
54
- numberToFeeUnitPrice: (value) => new NumberUnit(value, Gwei),
54
+ numberToFeeUnitPrice: (value) => NumberUnit.create(value, Gwei),
55
55
  isEnabled: ({ feeData }) => Boolean(feeData.rbfEnabled),
56
56
  }
57
57
  }
@@ -5,66 +5,8 @@ import {
5
5
  getUnconfirmedSentBalance,
6
6
  } from '@exodus/asset-lib'
7
7
  import assert from 'minimalistic-assert'
8
- import ms from 'ms'
9
-
10
- const DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH = ms('5m')
11
-
12
- // Defines whether a `Tx` is an absolute balance
13
- // node that can be sorted in terms of transaction
14
- // ordering.
15
- //
16
- export const isCanonicalAbsoluteBalanceTx = (tx) =>
17
- Boolean(
18
- tx?.data?.balanceChange &&
19
- typeof tx.data.blockNumber === 'number' &&
20
- typeof tx.data.transactionIndex === 'number'
21
- )
22
-
23
- // Attempts to find the most recent `absoluteBalance` Tx for
24
- // a given `txLog`.
25
- //
26
- // NOTE: Returns `null` if nothing is found.
27
- // NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
28
- // which are not guaranteed to exist in older transaction
29
- // histories.
30
- export const getLatestCanonicalAbsoluteBalanceTx = ({
31
- // Due to nondeterministic sorting for records in close
32
- // proximity to one-another on high TPS chains, the
33
- // order of the `reversedTxLog` cannot be trusted to be
34
- // exactly precise since two consecutive blocks may share
35
- // the same timestamp. Therefore, when finding an absolute
36
- // balance node, we use the`searchDepthMs` to take into
37
- // account extra nodes chronologically deeper than what we
38
- // presume to be the latest to make sure we resolve to the
39
- // correct node.
40
- searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH,
41
- reversedTxLog,
42
- }) => {
43
- assert(reversedTxLog, 'expected reversedTxLog')
44
-
45
- let latest = null
46
-
47
- for (const tx of reversedTxLog) {
48
- if (latest) {
49
- const diff = +latest.date - +tx.date
50
-
51
- if (diff >= searchDepthMs) break
52
- }
53
8
 
54
- if (!isCanonicalAbsoluteBalanceTx(tx)) continue
55
-
56
- if (
57
- !latest ||
58
- tx.data.blockNumber > latest.data.blockNumber ||
59
- (tx.data.blockNumber === latest.data.blockNumber &&
60
- tx.data.transactionIndex > latest.data.transactionIndex)
61
- ) {
62
- latest = tx
63
- }
64
- }
65
-
66
- return latest
67
- }
9
+ import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/index.js'
68
10
 
69
11
  /**
70
12
  * TODO: Balance model gaps for EVM staking assets
@@ -0,0 +1,92 @@
1
+ import assert from 'minimalistic-assert'
2
+ import ms from 'ms'
3
+
4
+ const DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH = ms('5m')
5
+
6
+ const ABSOLUTE_FIELD_NAME_BALANCE_CHANGE = 'balanceChange'
7
+ const ABSOLUTE_FIELD_NAME_NONCE_CHANGE = 'nonceChange'
8
+
9
+ const ABSOLUTE_BALANCE_FIELDS = new Set([
10
+ ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
11
+ ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
12
+ ])
13
+
14
+ // Defines whether a `Tx` is an absolute balance
15
+ // node that can be sorted in terms of transaction
16
+ // ordering.
17
+ //
18
+ const isCanonicalAbsoluteTx = (tx, fieldName) => {
19
+ assert(ABSOLUTE_BALANCE_FIELDS.has(fieldName), `unsupported fieldName, "${fieldName}"`)
20
+
21
+ return Boolean(
22
+ tx?.data?.[fieldName] &&
23
+ typeof tx.data.blockNumber === 'number' &&
24
+ typeof tx.data.transactionIndex === 'number'
25
+ )
26
+ }
27
+
28
+ export const isCanonicalAbsoluteBalanceTx = (tx) =>
29
+ isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_BALANCE_CHANGE)
30
+ export const isCanonicalAbsoluteNonceTx = (tx) =>
31
+ isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_NONCE_CHANGE)
32
+
33
+ // Attempts to find the most recent absolute Tx for a given
34
+ // `txLog`.
35
+ //
36
+ // NOTE: Returns `null` if nothing is found.
37
+ // NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
38
+ // which are not guaranteed to exist in older transaction
39
+ // histories.
40
+ const getLatestCanonicalAbsoluteTx = ({
41
+ // Due to nondeterministic sorting for records in close
42
+ // proximity to one-another on high TPS chains, the
43
+ // order of the `reversedTxLog` cannot be trusted to be
44
+ // exactly precise since two consecutive blocks may share
45
+ // the same timestamp. Therefore, when finding an absolute
46
+ // balance node, we use the`searchDepthMs` to take into
47
+ // account extra nodes chronologically deeper than what we
48
+ // presume to be the latest to make sure we resolve to the
49
+ // correct node.
50
+ searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH,
51
+ reversedTxLog,
52
+ fieldName,
53
+ }) => {
54
+ assert(reversedTxLog, 'expected reversedTxLog')
55
+
56
+ let latest = null
57
+
58
+ for (const tx of reversedTxLog) {
59
+ if (latest) {
60
+ const diff = +latest.date - +tx.date
61
+
62
+ if (diff >= searchDepthMs) break
63
+ }
64
+
65
+ if (!isCanonicalAbsoluteTx(tx, fieldName)) continue
66
+
67
+ if (
68
+ !latest ||
69
+ tx.data.blockNumber > latest.data.blockNumber ||
70
+ (tx.data.blockNumber === latest.data.blockNumber &&
71
+ tx.data.transactionIndex > latest.data.transactionIndex)
72
+ ) {
73
+ latest = tx
74
+ }
75
+ }
76
+
77
+ return latest
78
+ }
79
+
80
+ export const getLatestCanonicalAbsoluteBalanceTx = ({ searchDepthMs, reversedTxLog }) =>
81
+ getLatestCanonicalAbsoluteTx({
82
+ searchDepthMs,
83
+ reversedTxLog,
84
+ fieldName: ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
85
+ })
86
+
87
+ export const getLatestCanonicalAbsoluteNonceTx = ({ searchDepthMs, reversedTxLog }) =>
88
+ getLatestCanonicalAbsoluteTx({
89
+ searchDepthMs,
90
+ reversedTxLog,
91
+ fieldName: ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
92
+ })
@@ -1,2 +1,8 @@
1
1
  export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
2
2
  export { default as filterEffects } from './filter-effects.js'
3
+ export {
4
+ isCanonicalAbsoluteBalanceTx,
5
+ isCanonicalAbsoluteNonceTx,
6
+ getLatestCanonicalAbsoluteBalanceTx,
7
+ getLatestCanonicalAbsoluteNonceTx,
8
+ } from './absolute.js'
@@ -1,44 +1,75 @@
1
+ import assert from 'minimalistic-assert'
2
+
1
3
  import { getNonce } from '../eth-like-util.js'
4
+ import { getLatestCanonicalAbsoluteNonceTx } from '../tx-log/clarity-utils/absolute.js'
5
+
6
+ export const BLOCK_TAG_LATEST = 'latest'
7
+ export const BLOCK_TAG_PENDING = 'pending'
8
+
9
+ const VALID_BLOCK_TAGS = new Set([BLOCK_TAG_LATEST, BLOCK_TAG_PENDING])
10
+
11
+ // NOTE: We do not yet support tags at arbitrary block heights.
12
+ const assertValidBlockTag = (tag) => assert(VALID_BLOCK_TAGS.has(tag), `invalid tag "${tag}"`)
2
13
 
3
14
  export const resolveNonce = async ({
4
15
  asset,
5
16
  forceFromNode,
6
17
  fromAddress,
7
- providedNonce,
8
18
  txLog = [],
9
- triedNonce,
10
- tag = 'latest', // use 'pending' for unconfirmed txs
19
+ tag = BLOCK_TAG_LATEST,
11
20
  useAbsoluteNonce,
12
21
  }) => {
22
+ assertValidBlockTag(tag)
23
+
13
24
  const nonceFromNode =
14
25
  asset.baseAsset?.api?.features?.noHistory || forceFromNode
15
26
  ? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
16
27
  : 0
17
28
 
18
- const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce })
29
+ const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
30
+
31
+ return Math.max(nonceFromNode, nonceFromLog)
32
+ }
33
+
34
+ const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
35
+ assert(reversedTxLog, 'expected reversedTxLog')
36
+
37
+ const maybeLatestAbsoluteNonceTx = getLatestCanonicalAbsoluteNonceTx({
38
+ reversedTxLog,
39
+ })
19
40
 
20
- return Math.max(
21
- nonceFromNode,
22
- nonceFromLog,
23
- providedNonce ?? 0,
24
- triedNonce === undefined ? 0 : triedNonce + 1
25
- )
41
+ if (maybeLatestAbsoluteNonceTx) return maybeLatestAbsoluteNonceTx
42
+
43
+ for (const tx of reversedTxLog) {
44
+ if (tx.data.nonceChange) {
45
+ return tx
46
+ }
47
+ }
26
48
  }
27
49
 
28
- export const getNonceFromTxLog = ({ txLog, useAbsoluteNonce }) => {
50
+ const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
29
51
  let absoluteNonce = 0
52
+
30
53
  if (useAbsoluteNonce) {
31
54
  const reversedTxLog = txLog.reverse()
32
- for (const tx of reversedTxLog) {
33
- if (tx.data.nonceChange) {
34
- absoluteNonce = parseInt(tx.data.nonceChange.to, 10)
35
- break
36
- }
55
+ const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
56
+
57
+ if (maybeLatestTxWithNonceChange) {
58
+ // NOTE: This is the latest nonce currently confirmed by
59
+ // Clarity, which lags behind `CLARITY_MIN_CONFIRMS`
60
+ // depth:
61
+ //
62
+ // https://github.com/ExodusMovement/clarity/blob/4a6ea30fce33246d1ef1440e61b0a84b876900d6/deployment/eth-clarity-indexer/values.yaml#L34
63
+ absoluteNonce = parseInt(maybeLatestTxWithNonceChange.data.nonceChange.to, 10)
37
64
  }
38
65
  }
39
66
 
40
67
  const nonceFromLog = [...txLog]
41
68
  .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
69
+ // NOTE: If we're only considering the `'latest'` block `tag`,
70
+ // then we should not take into account unconfirmed
71
+ // transactions when computing the `nonce`.
72
+ .filter((tx) => tag !== BLOCK_TAG_LATEST || !tx.pending)
42
73
  .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
43
74
 
44
75
  return Math.max(absoluteNonce, nonceFromLog)
@@ -83,15 +83,22 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
83
83
  })
84
84
  } else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
85
85
  console.info('trying to send again...') // inject logger factory from platform
86
+
86
87
  // let's try to fix the nonce issue
87
- const newNonce = await baseAsset.getNonce({
88
+ let newNonce = await baseAsset.getNonce({
88
89
  asset,
89
90
  fromAddress,
90
91
  walletAccount,
91
- triedNonce: parsedTx.nonce,
92
92
  forceFromNode: true,
93
93
  })
94
94
 
95
+ // TODO: We should only do this for non-replacement transactions.
96
+ const triedNonce = parsedTx.nonce
97
+
98
+ if (typeof triedNonce === 'number') {
99
+ newNonce = Math.max(newNonce, triedNonce + 1)
100
+ }
101
+
95
102
  // NOTE: An `unsignedTx.txData.transactionBuffer` may be optional
96
103
  // in `unsignedTx`, since a `providedUnsignedTx` may be
97
104
  // truthy but composed only of `legacyParams`: