@exodus/ethereum-api 10.2.2 → 10.3.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 +18 -0
- package/package.json +2 -2
- package/src/exodus-eth-server/clarity-v2.js +28 -2
- package/src/tx-create.js +5 -4
- package/src/tx-log/clarity-monitor.js +50 -13
- package/src/tx-log/clarity-truncated-history-monitor.js +3 -3
- package/src/tx-log/clarity-truncated-history-monitor.md +2 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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
|
+
## [10.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.2.3...@exodus/ethereum-api@10.3.0) (2026-10-30)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **ethereum-api:** self-heal tx log with clarity tip mode ([#9123](https://github.com/ExodusMovement/assets/issues/9123)) ([67cbc64](https://github.com/ExodusMovement/assets/commit/67cbc641606e18f1c4cdf66ee0d76b3e7fdd70fd))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [10.2.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.2.2...@exodus/ethereum-api@10.2.3) (2026-10-23)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **ethereum-api:** allow accelerating a tx parked below the base fee ([#9049](https://github.com/ExodusMovement/assets/issues/9049)) ([2dac03f](https://github.com/ExodusMovement/assets/commit/2dac03f68d1de7cc56a191e82e4e217ad8a5bf07))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [10.2.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.2.1...@exodus/ethereum-api@10.2.2) (2026-10-22)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.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",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "99c7d9d4aae75358a0269479e056e5785fbad5ed"
|
|
69
69
|
}
|
|
@@ -63,10 +63,14 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
63
63
|
this.updateBaseApiPath(uri) // pass in the uri from remote config to override assets-gateway
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
getTransactionsFromBlockNumber = async ({ address, blockNumber, withInput, truncated }) => {
|
|
66
|
+
getTransactionsFromBlockNumber = async ({ address, blockNumber, withInput, truncated, tip }) => {
|
|
67
67
|
const url = new URL(`${this.baseApiPath}/addresses/${encodeURIComponent(address)}/transactions`)
|
|
68
68
|
url.searchParams.set('cursor', blockNumber || '0')
|
|
69
69
|
|
|
70
|
+
if (tip) {
|
|
71
|
+
url.searchParams.set('mode', 'tip')
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
if (withInput) {
|
|
71
75
|
url.searchParams.set('withInput', 'true')
|
|
72
76
|
}
|
|
@@ -122,8 +126,12 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
122
126
|
* into the backend's truncation mode + `accountInfo` pre-cursor snapshots (used by
|
|
123
127
|
* `ClarityTruncatedHistoryMonitor`). For `clarity`/`clarity-v2`/`clarity-v3` flows,
|
|
124
128
|
* leave the default so Clarity returns full history.
|
|
129
|
+
*
|
|
130
|
+
* Pass `tip: true` for incremental syncs: Clarity pages like the default mode until the
|
|
131
|
+
* chain tip, then returns the newest txs with a cursor anchored at the oldest of them and
|
|
132
|
+
* `hasMore: false`, so each call re-reads that window.
|
|
125
133
|
*/
|
|
126
|
-
async getAllTransactions({ address, cursor, withInput = true, truncated = false }) {
|
|
134
|
+
async getAllTransactions({ address, cursor, withInput = true, truncated = false, tip = false }) {
|
|
127
135
|
let { blockNumber } = decodeCursor(cursor)
|
|
128
136
|
|
|
129
137
|
let transactions = []
|
|
@@ -139,6 +147,7 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
139
147
|
blockNumber: blockNumber.toString(),
|
|
140
148
|
withInput,
|
|
141
149
|
truncated,
|
|
150
|
+
tip,
|
|
142
151
|
})
|
|
143
152
|
|
|
144
153
|
const txs = result.transactions || []
|
|
@@ -149,6 +158,23 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
149
158
|
|
|
150
159
|
const nextBlockNumber = BigInt(result.cursor)
|
|
151
160
|
|
|
161
|
+
if (tip) {
|
|
162
|
+
transactions.push(...txs)
|
|
163
|
+
|
|
164
|
+
if (!result.hasMore) {
|
|
165
|
+
blockNumber = nextBlockNumber
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (nextBlockNumber <= blockNumber) {
|
|
170
|
+
console.warn(`Tip mode cursor did not advance past ${blockNumber} with hasMore, stopping`)
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
blockNumber = nextBlockNumber
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
152
178
|
if (txs.length === 0) {
|
|
153
179
|
// fetch until no more new transactions
|
|
154
180
|
blockNumber = nextBlockNumber
|
package/src/tx-create.js
CHANGED
|
@@ -291,10 +291,11 @@ async function verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce
|
|
|
291
291
|
// [0][1][2][3]{4}{5}{6}...
|
|
292
292
|
// ^--- next empty slot (pendingNonce)
|
|
293
293
|
//
|
|
294
|
-
// Nonce to replace must be
|
|
295
|
-
//
|
|
294
|
+
// Nonce to replace must be <= pendingNonce. At pendingNonce the node holds the tx
|
|
295
|
+
// without counting it, e.g. its fee cap is below the base fee, and it is the next
|
|
296
|
+
// executable slot. If nonce > pendingNonce, the slots below it are gapped — not safe to replace.
|
|
296
297
|
const pendingNonce = parseInt(pendingNonceHex, 16)
|
|
297
|
-
if (pendingNonce
|
|
298
|
+
if (pendingNonce < nonce) {
|
|
298
299
|
// A predecessor tx was dropped by the node, leaving a gap below this tx.
|
|
299
300
|
// The tx itself is still in the node's queued pool but can't execute until
|
|
300
301
|
// the missing nonce slot is filled.
|
|
@@ -304,7 +305,7 @@ async function verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce
|
|
|
304
305
|
// first, then you'll be able to accelerate this one."
|
|
305
306
|
throw new Error(
|
|
306
307
|
`ERR_BUMP_TX_NONCE_GAP: Cannot bump transaction ${bumpTxId}: nonce gap detected — ` +
|
|
307
|
-
`node's pending nonce (${pendingNonce})
|
|
308
|
+
`node's pending nonce (${pendingNonce}) < tx nonce (${nonce}), ` +
|
|
308
309
|
`broadcasting would produce ErrFutureReplacePending`
|
|
309
310
|
)
|
|
310
311
|
}
|
|
@@ -42,6 +42,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
42
42
|
this.eip7702Whitelist = eip7702Whitelist ?? createEip7702Whitelist(eip7702Supported)
|
|
43
43
|
this.getBlackListStatus = getBlackListStatus
|
|
44
44
|
this.getAllLogItemsByAsset = getAllLogItemsByAsset
|
|
45
|
+
this.emittedUnknownTokenAddresses = new Set()
|
|
45
46
|
this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
|
|
46
47
|
this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
|
|
47
48
|
getTxLog: (...args) => this.aci.getTxLog(...args),
|
|
@@ -240,7 +241,6 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
240
241
|
|
|
241
242
|
const batch = this.aci.createOperationsBatch()
|
|
242
243
|
let allTxs = []
|
|
243
|
-
let hasNewTxs = false
|
|
244
244
|
let historyResponse
|
|
245
245
|
let historyError
|
|
246
246
|
let processedLogItemsByAsset
|
|
@@ -255,8 +255,6 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
255
255
|
walletAccount,
|
|
256
256
|
}))
|
|
257
257
|
|
|
258
|
-
hasNewTxs = allTxs.length > 0
|
|
259
|
-
|
|
260
258
|
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
261
259
|
getLogItemsFromServerTx,
|
|
262
260
|
ourWalletAddress: derivedData.ourWalletAddress,
|
|
@@ -266,6 +264,10 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
266
264
|
assets,
|
|
267
265
|
})
|
|
268
266
|
|
|
267
|
+
if (this.isTipSync({ derivedData, refresh })) {
|
|
268
|
+
await this.warnReaddedTxs({ walletAccount, logItemsByAsset })
|
|
269
|
+
}
|
|
270
|
+
|
|
269
271
|
const { txsToRemove } = await this.checkPendingTransactions({
|
|
270
272
|
txlist: allTxs,
|
|
271
273
|
walletAccount,
|
|
@@ -333,13 +335,44 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
333
335
|
: new Error(String(historyError), { cause: historyError })
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
339
|
+
transactions: allTxs,
|
|
340
|
+
tokensByAddress,
|
|
341
|
+
})
|
|
342
|
+
const addressesToEmit = refresh
|
|
343
|
+
? unknownTokenAddresses
|
|
344
|
+
: unknownTokenAddresses.filter((address) => !this.emittedUnknownTokenAddresses.has(address))
|
|
345
|
+
for (const address of unknownTokenAddresses) this.emittedUnknownTokenAddresses.add(address)
|
|
346
|
+
if (addressesToEmit.length > 0) {
|
|
347
|
+
this.emit('unknown-tokens', addressesToEmit)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async warnReaddedTxs({ walletAccount, logItemsByAsset }) {
|
|
352
|
+
const isConfirmed = (tx) => tx.confirmations > 0 && Number.isInteger(tx.data?.blockNumber)
|
|
353
|
+
|
|
354
|
+
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
355
|
+
const confirmedTxs = txs.filter(isConfirmed)
|
|
356
|
+
if (confirmedTxs.length === 0) continue
|
|
357
|
+
|
|
358
|
+
const txLog = await this.aci.getTxLog({ assetName, walletAccount })
|
|
359
|
+
const missingTxs = confirmedTxs.filter((tx) => !txLog.has(tx.txId))
|
|
360
|
+
if (missingTxs.length === 0) continue
|
|
361
|
+
|
|
362
|
+
let newestLoggedBlock = -1
|
|
363
|
+
for (const tx of txLog) {
|
|
364
|
+
if (isConfirmed(tx)) newestLoggedBlock = Math.max(newestLoggedBlock, tx.data.blockNumber)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const readdedTxIds = missingTxs
|
|
368
|
+
.filter((tx) => tx.data.blockNumber <= newestLoggedBlock)
|
|
369
|
+
.map((tx) => tx.txId)
|
|
370
|
+
if (readdedTxIds.length > 0) {
|
|
371
|
+
this.logger.warn('clarity tip window re-added txs missing from tx log', {
|
|
372
|
+
assetName,
|
|
373
|
+
walletAccount,
|
|
374
|
+
txIds: readdedTxIds,
|
|
375
|
+
})
|
|
343
376
|
}
|
|
344
377
|
}
|
|
345
378
|
}
|
|
@@ -482,11 +515,15 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
482
515
|
this.server.dispose()
|
|
483
516
|
}
|
|
484
517
|
|
|
518
|
+
isTipSync({ derivedData, refresh }) {
|
|
519
|
+
return !refresh && Boolean(derivedData.currentAccountState?.clarityCursor)
|
|
520
|
+
}
|
|
521
|
+
|
|
485
522
|
async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
|
|
486
523
|
const address = derivedData.ourWalletAddress
|
|
487
|
-
const
|
|
488
|
-
const cursor =
|
|
489
|
-
return this.server.getAllTransactions({ walletAccount, address, cursor })
|
|
524
|
+
const tip = this.isTipSync({ derivedData, refresh })
|
|
525
|
+
const cursor = tip ? derivedData.currentAccountState.clarityCursor : null
|
|
526
|
+
return this.server.getAllTransactions({ walletAccount, address, cursor, tip })
|
|
490
527
|
}
|
|
491
528
|
|
|
492
529
|
listenToServerEvents() {
|
|
@@ -137,15 +137,15 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
|
137
137
|
|
|
138
138
|
async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
|
|
139
139
|
const address = derivedData.ourWalletAddress
|
|
140
|
-
const
|
|
141
|
-
const cursor =
|
|
140
|
+
const tip = this.isTipSync({ derivedData, refresh })
|
|
141
|
+
const cursor = tip ? derivedData.currentAccountState.clarityCursor : null
|
|
142
142
|
return this.server.getAllTransactions({
|
|
143
143
|
walletAccount,
|
|
144
144
|
address,
|
|
145
145
|
cursor,
|
|
146
|
-
refresh,
|
|
147
146
|
withInput: true,
|
|
148
147
|
truncated: true,
|
|
148
|
+
tip,
|
|
149
149
|
})
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Extends `ClarityMonitor`. Used when Clarity only serves a **recent window** of txs and attaches pre-cursor snapshots in `accountInfo`.
|
|
4
4
|
|
|
5
5
|
- Sends `truncated=true` on `getAllTransactions`.
|
|
6
|
+
- Incremental ticks use Clarity `mode=tip`, like `clarity-v2`. The tip page carries `accountInfo`, so `account_balance` rows still update rebasing tokens. First sync and refresh use default mode.
|
|
6
7
|
- Folds `accountInfo` into `accountState`, including truncated-history baselines.
|
|
7
8
|
- Forces `rpcBalanceAssetNames = []` (no per-asset RPC `eth_getBalance` batch).
|
|
8
9
|
|
|
@@ -217,6 +218,7 @@ If the backend can't emit `accountInfo`, the monitor degrades gracefully to `cla
|
|
|
217
218
|
|
|
218
219
|
## Tests
|
|
219
220
|
|
|
221
|
+
- `src/tx-log/__tests__/clarity-tip-mode.test.js` — `clarity-v2` tip mode (dropped tx re-added, stable cursor, default mode on refresh); this monitor's tip ticks send `truncated=true` and `account_balance` rows track a rebase; refresh uses default mode and still collects `accountInfo`.
|
|
220
222
|
- `src/tx-log/__tests__/clarity-truncated-history-monitor.test.js` — `truncated: true`, no RPC balance batch, accountInfo → `balance`, token `tokenBalances`, and `truncatedAccountState`.
|
|
221
223
|
- `src/__tests__/create-asset-utils.test.js` — factory → `ClarityTruncatedHistoryMonitor`.
|
|
222
224
|
- `src/__tests__/get-balances.test.js` — accountState token short-circuit and truncated baseline cases.
|
|
@@ -55,9 +55,8 @@ export default function getLogItemsFromServerTx({
|
|
|
55
55
|
const toAddress = tryFindExternalRecipient(ethereumTransfers, ourWalletAddress)
|
|
56
56
|
const ourWalletWasSender = serverTx.from === ourWalletAddress
|
|
57
57
|
const METHOD_ID_LENGTH = 10
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
}
|
|
58
|
+
const serverMethodId = serverTx.methodId ?? serverTx.input?.slice(0, METHOD_ID_LENGTH)
|
|
59
|
+
const methodId = serverMethodId && { methodId: serverMethodId }
|
|
61
60
|
const data = (ourWalletWasSender ? serverTx.input : methodId?.methodId) || '0x'
|
|
62
61
|
const walletUpdates = getWalletUpdates(ourWalletAddress, serverTx.walletChanges || [])
|
|
63
62
|
const { baseBalanceUpdate, nonceUpdate, tokenBalancesUpdate } = walletUpdates
|