@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 +20 -0
- package/package.json +3 -3
- package/src/create-asset-utils.js +12 -8
- package/src/create-asset.js +1 -1
- package/src/eth-like-util.js +1 -2
- package/src/tx-log/clarity-monitor-v2.js +13 -1
- package/src/tx-log/clarity-monitor.js +16 -1
- package/src/tx-log/ethereum-no-history-monitor.js +15 -1
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +39 -0
- package/src/tx-log/monitor-utils/index.js +1 -0
- package/src/tx-send/nonce-utils.js +23 -11
- package/src/tx-send/tx-send.js +9 -2
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.
|
|
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.
|
|
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": "
|
|
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 ({
|
|
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
|
-
|
|
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
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -303,7 +303,7 @@ export const createAssetFactory = ({
|
|
|
303
303
|
broadcastPrivateBundle,
|
|
304
304
|
broadcastPrivateTx,
|
|
305
305
|
forceGasLimitEstimation,
|
|
306
|
-
getEIP7702Delegation: (addr) => getEIP7702Delegation({
|
|
306
|
+
getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
|
|
307
307
|
getNonce,
|
|
308
308
|
privacyServer,
|
|
309
309
|
server,
|
package/src/eth-like-util.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -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
|
-
|
|
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`:
|