@exodus/ethereum-api 8.62.4 → 8.64.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.64.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.63.0...@exodus/ethereum-api@8.64.0) (2026-01-14)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: EIP delegation status on all monitors (#7193)
13
+
14
+
15
+
16
+ ## [8.63.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.4...@exodus/ethereum-api@8.63.0) (2026-01-14)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: add 'latest' evm nonce support (#7267)
23
+
24
+
25
+
6
26
  ## [8.62.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.3...@exodus/ethereum-api@8.62.4) (2026-01-14)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.62.4",
3
+ "version": "8.64.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",
@@ -29,7 +29,7 @@
29
29
  "@exodus/bip44-constants": "^195.0.0",
30
30
  "@exodus/crypto": "^1.0.0-rc.26",
31
31
  "@exodus/currency": "^6.0.1",
32
- "@exodus/ethereum-lib": "^5.20.2",
32
+ "@exodus/ethereum-lib": "^5.21.0",
33
33
  "@exodus/ethereum-meta": "^2.9.1",
34
34
  "@exodus/ethereumholesky-meta": "^2.0.5",
35
35
  "@exodus/ethereumjs": "^1.8.0",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "138043f1b7f97c9acc7b7b89b0561f26b80757d5"
70
+ "gitHead": "11f40d9da07e1e70a4e07f94defa3042c6fb21cc"
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
  }
@@ -303,7 +303,7 @@ export const createAssetFactory = ({
303
303
  broadcastPrivateBundle,
304
304
  broadcastPrivateTx,
305
305
  forceGasLimitEstimation,
306
- getEIP7702Delegation: (addr) => getEIP7702Delegation({ asset: base, address: addr }),
306
+ getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
307
307
  getNonce,
308
308
  privacyServer,
309
309
  server,
@@ -180,8 +180,7 @@ export const getERC20Params = async ({ asset, address, paramNames = DEFAULT_PARA
180
180
  return response
181
181
  }
182
182
 
183
- export async function getEIP7702Delegation({ asset, address }) {
184
- const server = getServer(asset)
183
+ export async function getEIP7702Delegation({ address, server }) {
185
184
  const code = await server.getCode(address)
186
185
 
187
186
  // No code at all
@@ -10,6 +10,7 @@ import {
10
10
  checkPendingTransactions,
11
11
  excludeUnchangedTokenBalances,
12
12
  getAllLogItemsByAsset,
13
+ getCurrentEIP7702Delegation,
13
14
  getDeriveDataNeededForTick,
14
15
  getDeriveTransactionsToCheck,
15
16
  } from './monitor-utils/index.js'
@@ -199,6 +200,13 @@ export class ClarityMonitorV2 extends BaseMonitor {
199
200
  ourWalletAddress: derivedData.ourWalletAddress,
200
201
  })
201
202
 
203
+ const eip7702Delegation = await getCurrentEIP7702Delegation({
204
+ server: this.server,
205
+ address: derivedData.ourWalletAddress,
206
+ currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
207
+ logger: this.logger,
208
+ })
209
+
202
210
  const batch = this.aci.createOperationsBatch()
203
211
 
204
212
  this.aci.removeTxLogBatch({
@@ -218,7 +226,11 @@ export class ClarityMonitorV2 extends BaseMonitor {
218
226
  })
219
227
  }
220
228
 
221
- const newData = { ...accountState }
229
+ // All updates must go through newData (accountState param is only used for mem merging)
230
+ const newData = {
231
+ ...accountState,
232
+ ...(eip7702Delegation && { eip7702Delegation }),
233
+ }
222
234
 
223
235
  if (cursor) {
224
236
  newData.clarityCursor = cursor
@@ -10,6 +10,7 @@ import {
10
10
  checkPendingTransactions,
11
11
  excludeUnchangedTokenBalances,
12
12
  getAllLogItemsByAsset,
13
+ getCurrentEIP7702Delegation,
13
14
  getDeriveDataNeededForTick,
14
15
  getDeriveTransactionsToCheck,
15
16
  } from './monitor-utils/index.js'
@@ -159,6 +160,13 @@ export class ClarityMonitor extends BaseMonitor {
159
160
  ourWalletAddress: derivedData.ourWalletAddress,
160
161
  })
161
162
 
163
+ const eip7702Delegation = await getCurrentEIP7702Delegation({
164
+ server: this.server,
165
+ address: derivedData.ourWalletAddress,
166
+ currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
167
+ logger: this.logger,
168
+ })
169
+
162
170
  const batch = this.aci.createOperationsBatch()
163
171
 
164
172
  this.aci.removeTxLogBatch({
@@ -178,11 +186,18 @@ export class ClarityMonitor extends BaseMonitor {
178
186
  })
179
187
  }
180
188
 
189
+ // All updates must go through newData (accountState param is only used for mem merging)
190
+ const newData = {
191
+ ...accountState,
192
+ clarityCursor: response.cursor,
193
+ ...(eip7702Delegation && { eip7702Delegation }),
194
+ }
195
+
181
196
  this.aci.updateAccountStateBatch({
182
197
  assetName,
183
198
  walletAccount,
184
199
  accountState,
185
- newData: { clarityCursor: response.cursor, ...accountState },
200
+ newData,
186
201
  batch,
187
202
  })
188
203
 
@@ -7,6 +7,7 @@ import { fromHexToString } from '../number-utils.js'
7
7
  import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check.js'
8
8
  import {
9
9
  excludeUnchangedTokenBalances,
10
+ getCurrentEIP7702Delegation,
10
11
  getDeriveDataNeededForTick,
11
12
  getDeriveTransactionsToCheck,
12
13
  } from './monitor-utils/index.js'
@@ -186,7 +187,20 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
186
187
  ourWalletAddress,
187
188
  })
188
189
 
189
- await this.updateAccountState({ newData: { ...accountState }, walletAccount })
190
+ const eip7702Delegation = await getCurrentEIP7702Delegation({
191
+ server: this.server,
192
+ address: ourWalletAddress,
193
+ currentDelegation: currentAccountState?.eip7702Delegation,
194
+ logger: this.logger,
195
+ })
196
+
197
+ // All updates must go through newData (accountState param is only used for mem merging)
198
+ const newData = {
199
+ ...accountState,
200
+ ...(eip7702Delegation && { eip7702Delegation }),
201
+ }
202
+
203
+ await this.updateAccountState({ accountState, newData, walletAccount })
190
204
 
191
205
  await this.removeFromTxLog(txsToRemove)
192
206
  await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
@@ -0,0 +1,39 @@
1
+ import { getEIP7702Delegation } from '../../eth-like-util.js'
2
+
3
+ /**
4
+ * Checks if the address has an EIP-7702 delegation and returns the delegation state.
5
+ * Returns the new state if changed, or the current state if unchanged.
6
+ * On error, returns the current state to preserve existing data.
7
+ *
8
+ * @param {Object} params
9
+ * @param {Object} params.server - The server instance to use for getCode
10
+ * @param {string} params.address - The wallet address to check
11
+ * @param {Object} [params.currentDelegation] - The current delegation state from accountState
12
+ * @param {Object} [params.logger] - Optional logger for warnings
13
+ * @returns {Promise<Object|undefined>} The delegation state to use
14
+ */
15
+ export async function getCurrentEIP7702Delegation({ server, address, currentDelegation, logger }) {
16
+ try {
17
+ const result = await getEIP7702Delegation({ address, server })
18
+
19
+ // Return new state if changed
20
+ if (
21
+ currentDelegation?.isDelegated !== result.isDelegated ||
22
+ currentDelegation?.delegatedAddress !== result.delegatedAddress
23
+ ) {
24
+ return {
25
+ isDelegated: result.isDelegated,
26
+ delegatedAddress: result.delegatedAddress,
27
+ }
28
+ }
29
+ } catch (error) {
30
+ if (logger) {
31
+ logger.warn('Failed to check EIP-7702 delegation:', error)
32
+ }
33
+ }
34
+
35
+ // Return current state if unchanged or on error
36
+ return currentDelegation
37
+ }
38
+
39
+ export default getCurrentEIP7702Delegation
@@ -5,4 +5,5 @@ export { default as getLogItemsFromServerTx } from './get-log-items-from-server-
5
5
  export { default as getHistoryFromServer } from './get-history-from-server.js'
6
6
  export { default as checkPendingTransactions } from './check-pending-transactions.js'
7
7
  export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
8
+ export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
8
9
  export * from './exclude-unchanged-token-balances.js'
@@ -3,29 +3,32 @@ import assert from 'minimalistic-assert'
3
3
  import { getNonce } from '../eth-like-util.js'
4
4
  import { getLatestCanonicalAbsoluteNonceTx } from '../tx-log/clarity-utils/absolute.js'
5
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}"`)
13
+
6
14
  export const resolveNonce = async ({
7
15
  asset,
8
16
  forceFromNode,
9
17
  fromAddress,
10
- providedNonce,
11
18
  txLog = [],
12
- triedNonce,
13
- tag = 'latest', // use 'pending' for unconfirmed txs
19
+ tag = BLOCK_TAG_LATEST,
14
20
  useAbsoluteNonce,
15
21
  }) => {
22
+ assertValidBlockTag(tag)
23
+
16
24
  const nonceFromNode =
17
25
  asset.baseAsset?.api?.features?.noHistory || forceFromNode
18
26
  ? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
19
27
  : 0
20
28
 
21
- const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce })
29
+ const nonceFromLog = getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
22
30
 
23
- return Math.max(
24
- nonceFromNode,
25
- nonceFromLog,
26
- providedNonce ?? 0,
27
- triedNonce === undefined ? 0 : triedNonce + 1
28
- )
31
+ return Math.max(nonceFromNode, nonceFromLog)
29
32
  }
30
33
 
31
34
  const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
@@ -44,7 +47,7 @@ const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
44
47
  }
45
48
  }
46
49
 
47
- export const getNonceFromTxLog = ({ txLog, useAbsoluteNonce }) => {
50
+ const getNonceFromTxLog = ({ txLog, useAbsoluteNonce, tag }) => {
48
51
  let absoluteNonce = 0
49
52
 
50
53
  if (useAbsoluteNonce) {
@@ -52,12 +55,21 @@ export const getNonceFromTxLog = ({ txLog, useAbsoluteNonce }) => {
52
55
  const maybeLatestTxWithNonceChange = getLatestTxWithNonceChange({ reversedTxLog })
53
56
 
54
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
55
63
  absoluteNonce = parseInt(maybeLatestTxWithNonceChange.data.nonceChange.to, 10)
56
64
  }
57
65
  }
58
66
 
59
67
  const nonceFromLog = [...txLog]
60
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)
61
73
  .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
62
74
 
63
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`: