@exodus/ethereum-api 8.71.3 → 8.73.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 +4 -4
- package/src/create-asset-utils.js +22 -4
- package/src/create-asset.js +5 -1
- package/src/exodus-eth-server/clarity-v2.js +37 -10
- package/src/exodus-eth-server/index.js +11 -1
- package/src/get-balances.js +13 -3
- package/src/tx-log/clarity-monitor-v2.js +54 -26
- package/src/tx-log/clarity-monitor.js +34 -15
- package/src/tx-log/clarity-truncated-history-monitor.js +207 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +208 -0
- package/src/tx-log/index.js +1 -0
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +62 -0
- package/src/tx-log/monitor-utils/index.js +1 -0
- package/src/tx-send/nonce-utils.js +8 -1
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.73.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.72.0...@exodus/ethereum-api@8.73.0) (2026-05-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: Faster Security Hydration Across Monitor (#7824)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.72.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.71.3...@exodus/ethereum-api@8.72.0) (2026-04-30)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: start adding truncated history for clarity evm (#7604)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.71.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.71.2...@exodus/ethereum-api@8.71.3) (2026-04-24)
|
|
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.73.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",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@exodus/asset": "^2.0.4",
|
|
26
|
-
"@exodus/asset-lib": "^5.
|
|
26
|
+
"@exodus/asset-lib": "^5.9.0",
|
|
27
27
|
"@exodus/assets": "^11.4.0",
|
|
28
28
|
"@exodus/basic-utils": "^3.0.1",
|
|
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.24.0",
|
|
33
33
|
"@exodus/ethereum-meta": "^2.9.1",
|
|
34
34
|
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
35
35
|
"@exodus/ethereumjs": "^1.11.0",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"type": "git",
|
|
69
69
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "d17ee07f3f8bf4cc67518f8a8395522578e8dae1"
|
|
72
72
|
}
|
|
@@ -9,6 +9,7 @@ import { createEvmServer, createWsGateway, ValidMonitorTypes } from './exodus-et
|
|
|
9
9
|
import { createEthereumHooks } from './hooks/index.js'
|
|
10
10
|
import { ClarityMonitor } from './tx-log/clarity-monitor.js'
|
|
11
11
|
import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
|
|
12
|
+
import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
|
|
12
13
|
import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
|
|
13
14
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
|
|
14
15
|
import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
|
|
@@ -169,6 +170,16 @@ export const createHistoryMonitorFactory = ({
|
|
|
169
170
|
...args,
|
|
170
171
|
})
|
|
171
172
|
break
|
|
173
|
+
case 'clarity-truncated-history':
|
|
174
|
+
monitor = new ClarityTruncatedHistoryMonitor({
|
|
175
|
+
assetClientInterface,
|
|
176
|
+
interval: ms(monitorInterval || '5m'),
|
|
177
|
+
server,
|
|
178
|
+
eip7702Supported,
|
|
179
|
+
getBlackListStatus,
|
|
180
|
+
...args,
|
|
181
|
+
})
|
|
182
|
+
break
|
|
172
183
|
case 'clarity-v3':
|
|
173
184
|
monitor = new ClarityMonitorV2({
|
|
174
185
|
assetClientInterface,
|
|
@@ -265,15 +276,22 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
|
|
|
265
276
|
assert(typeof fromAddress === 'string', 'expected fromAddress')
|
|
266
277
|
assert(walletAccount, 'expected walletAccount')
|
|
267
278
|
|
|
268
|
-
const txLog = await
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
const [txLog, accountState] = await Promise.all([
|
|
280
|
+
assetClientInterface.getTxLog({
|
|
281
|
+
assetName: asset.baseAsset.name,
|
|
282
|
+
walletAccount,
|
|
283
|
+
}),
|
|
284
|
+
assetClientInterface.getAccountState({
|
|
285
|
+
assetName: asset.baseAsset.name,
|
|
286
|
+
walletAccount,
|
|
287
|
+
}),
|
|
288
|
+
])
|
|
272
289
|
|
|
273
290
|
return resolveNonce({
|
|
274
291
|
asset,
|
|
275
292
|
fromAddress,
|
|
276
293
|
txLog,
|
|
294
|
+
accountState,
|
|
277
295
|
forceFromNode,
|
|
278
296
|
tag,
|
|
279
297
|
useAbsoluteNonce: useAbsoluteBalanceAndNonce,
|
package/src/create-asset.js
CHANGED
|
@@ -137,7 +137,11 @@ export const createAssetFactory = ({
|
|
|
137
137
|
useAbsoluteBalanceAndNonce = overrideUseAbsoluteBalanceAndNonce
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const server = createEvmServer({
|
|
140
|
+
const server = createEvmServer({
|
|
141
|
+
assetName: asset.name,
|
|
142
|
+
serverUrl,
|
|
143
|
+
monitorType,
|
|
144
|
+
})
|
|
141
145
|
|
|
142
146
|
const address = {
|
|
143
147
|
validate: validateFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
@@ -108,10 +108,18 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
108
108
|
this.updateBaseApiPath(uri) // pass in the uri from remote config to override assets-gateway
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
getTransactionsFromBlockNumber = async ({ address, blockNumber, withInput, truncated }) => {
|
|
112
112
|
const url = new URL(`${this.baseApiPath}/addresses/${encodeURIComponent(address)}/transactions`)
|
|
113
|
-
url.searchParams.set('cursor', blockNumber)
|
|
114
|
-
|
|
113
|
+
url.searchParams.set('cursor', blockNumber || '0')
|
|
114
|
+
|
|
115
|
+
if (withInput) {
|
|
116
|
+
url.searchParams.set('withInput', 'true')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (truncated) {
|
|
120
|
+
url.searchParams.set('truncated', 'true')
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
return fetchJsonRetry(url)
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -154,19 +162,37 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Paginates GET .../addresses/:addr/transactions. Pass `truncated: true` to opt
|
|
167
|
+
* into the backend's truncation mode + `accountInfo` pre-cursor snapshots (used by
|
|
168
|
+
* `ClarityTruncatedHistoryMonitor`). For `clarity`/`clarity-v2`/`clarity-v3` flows,
|
|
169
|
+
* leave the default so Clarity returns full history.
|
|
170
|
+
*/
|
|
171
|
+
async getAllTransactions({ address, cursor, withInput = true, truncated = false }) {
|
|
158
172
|
let { blockNumber } = decodeCursor(cursor)
|
|
159
173
|
|
|
160
174
|
let transactions = []
|
|
161
175
|
|
|
176
|
+
// accountInfo rows carry balance/nonce snapshots for blocks before the
|
|
177
|
+
// initial cursor — used to derive correct state with truncated history.
|
|
178
|
+
// Only collected (and returned) when the caller opted into `truncated`.
|
|
179
|
+
const accountInfo = truncated ? [] : null
|
|
180
|
+
|
|
162
181
|
while (true) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
182
|
+
const result = await this.getTransactionsFromBlockNumber({
|
|
183
|
+
address,
|
|
184
|
+
blockNumber: blockNumber.toString(),
|
|
185
|
+
withInput,
|
|
186
|
+
truncated,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const txs = result.transactions || []
|
|
190
|
+
|
|
191
|
+
if (accountInfo && Array.isArray(result.accountInfo)) {
|
|
192
|
+
accountInfo.push(...result.accountInfo)
|
|
193
|
+
}
|
|
168
194
|
|
|
169
|
-
const nextBlockNumber = BigInt(
|
|
195
|
+
const nextBlockNumber = BigInt(result.cursor)
|
|
170
196
|
|
|
171
197
|
if (txs.length === 0) {
|
|
172
198
|
// fetch until no more new transactions
|
|
@@ -200,6 +226,7 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
200
226
|
return {
|
|
201
227
|
transactions: { confirmed, pending },
|
|
202
228
|
cursor: newCursor,
|
|
229
|
+
...(accountInfo && { accountInfo }),
|
|
203
230
|
}
|
|
204
231
|
}
|
|
205
232
|
|
|
@@ -14,18 +14,28 @@ import ClarityServer from './clarity.js'
|
|
|
14
14
|
import ClarityServerV2 from './clarity-v2.js'
|
|
15
15
|
import WsGateway from './ws-gateway.js'
|
|
16
16
|
|
|
17
|
-
export const ValidMonitorTypes = [
|
|
17
|
+
export const ValidMonitorTypes = [
|
|
18
|
+
'no-history',
|
|
19
|
+
'clarity',
|
|
20
|
+
'clarity-v2',
|
|
21
|
+
'clarity-truncated-history',
|
|
22
|
+
'clarity-v3',
|
|
23
|
+
'magnifier',
|
|
24
|
+
]
|
|
18
25
|
|
|
19
26
|
export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
20
27
|
assert(assetName, 'assetName is required')
|
|
21
28
|
assert(serverUrl, 'serverUrl is required')
|
|
22
29
|
assert(monitorType, 'monitorType is required')
|
|
30
|
+
|
|
31
|
+
// Truncated-history uses the same Clarity v2 HTTP/WS server; only the monitor class differs.
|
|
23
32
|
switch (monitorType) {
|
|
24
33
|
case 'no-history':
|
|
25
34
|
return new ApiCoinNodesServer({ baseAssetName: assetName, uri: serverUrl })
|
|
26
35
|
case 'clarity':
|
|
27
36
|
return new ClarityServer({ baseAssetName: assetName, uri: serverUrl })
|
|
28
37
|
case 'clarity-v2':
|
|
38
|
+
case 'clarity-truncated-history':
|
|
29
39
|
case 'clarity-v3':
|
|
30
40
|
return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
|
|
31
41
|
case 'magnifier':
|
package/src/get-balances.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getBalanceFromAccountState,
|
|
3
|
+
getBalanceFromAccountStateIfAny,
|
|
3
4
|
getBalanceFromTxLog,
|
|
4
5
|
getUnconfirmedReceivedBalance,
|
|
5
6
|
getUnconfirmedSentBalance,
|
|
@@ -34,12 +35,20 @@ import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/inde
|
|
|
34
35
|
*
|
|
35
36
|
* See: docs/balances-model.md for the intended balance model spec.
|
|
36
37
|
*/
|
|
37
|
-
export const getAbsoluteBalance = ({ asset, txLog }) => {
|
|
38
|
+
export const getAbsoluteBalance = ({ asset, txLog, accountState }) => {
|
|
38
39
|
assert(asset, 'asset is required')
|
|
39
40
|
assert(txLog, 'txLog is required')
|
|
40
41
|
|
|
42
|
+
const accountStateBalance = getBalanceFromAccountStateIfAny({ asset, accountState })
|
|
43
|
+
|
|
41
44
|
if (txLog.size === 0) {
|
|
42
|
-
return asset.currency.ZERO
|
|
45
|
+
return accountStateBalance || asset.currency.ZERO
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Prefer an explicit `accountState` snapshot (e.g. populated from Clarity `accountInfo`
|
|
49
|
+
// in truncated-history mode) over walking a potentially truncated `txLog`.
|
|
50
|
+
if (accountStateBalance) {
|
|
51
|
+
return accountStateBalance
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// NOTE: We reverse the `txLog` to prioritize the handling
|
|
@@ -191,7 +200,8 @@ export const getBalancesFactory = ({ monitorType, useAbsoluteBalance, rpcBalance
|
|
|
191
200
|
// pessimistic reading that's safe for consumers.
|
|
192
201
|
spendable = getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
|
|
193
202
|
} else {
|
|
194
|
-
const absoluteBalance =
|
|
203
|
+
const absoluteBalance =
|
|
204
|
+
useAbsoluteBalance && getAbsoluteBalance({ asset, txLog, accountState })
|
|
195
205
|
|
|
196
206
|
if (absoluteBalance) {
|
|
197
207
|
// NOTE: The returned `absoluteBalance` returns only confirmed
|
|
@@ -163,6 +163,23 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
163
163
|
return { txsToRemove }
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
async persistSecurityState({ walletAccount, accountState, isBlacklisted, eip7702Delegation }) {
|
|
167
|
+
const securityStatePatch = {
|
|
168
|
+
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
169
|
+
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isEmpty(securityStatePatch)) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await this.updateAccountState({
|
|
177
|
+
walletAccount,
|
|
178
|
+
accountState,
|
|
179
|
+
newData: securityStatePatch,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
async tick({ walletAccount, refresh }) {
|
|
167
184
|
await this.subscribeWalletAddresses(walletAccount)
|
|
168
185
|
|
|
@@ -173,17 +190,26 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
|
|
176
|
-
const {
|
|
193
|
+
const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
|
|
177
194
|
derivedData,
|
|
178
|
-
tokens,
|
|
179
195
|
walletAccount,
|
|
180
196
|
})
|
|
197
|
+
|
|
198
|
+
await this.persistSecurityState({
|
|
199
|
+
walletAccount,
|
|
200
|
+
accountState: derivedData.currentAccountState,
|
|
201
|
+
isBlacklisted,
|
|
202
|
+
eip7702Delegation,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const accountState = await this.getNewAccountState({
|
|
206
|
+
tokens,
|
|
207
|
+
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
208
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
209
|
+
})
|
|
210
|
+
|
|
181
211
|
const batch = this.aci.createOperationsBatch()
|
|
182
|
-
const newData = {
|
|
183
|
-
...accountState,
|
|
184
|
-
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
185
|
-
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
186
|
-
}
|
|
212
|
+
const newData = { ...accountState }
|
|
187
213
|
let allTxs = []
|
|
188
214
|
let hasNewTxs = false
|
|
189
215
|
let historyError
|
|
@@ -285,6 +311,24 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
285
311
|
}) {
|
|
286
312
|
const hasNewTxs = allTxs.length > 0
|
|
287
313
|
|
|
314
|
+
const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
|
|
315
|
+
derivedData,
|
|
316
|
+
walletAccount,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
await this.persistSecurityState({
|
|
320
|
+
walletAccount,
|
|
321
|
+
accountState: derivedData.currentAccountState,
|
|
322
|
+
isBlacklisted,
|
|
323
|
+
eip7702Delegation,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const accountState = await this.getNewAccountState({
|
|
327
|
+
tokens,
|
|
328
|
+
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
329
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
330
|
+
})
|
|
331
|
+
|
|
288
332
|
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
289
333
|
getLogItemsFromServerTx,
|
|
290
334
|
ourWalletAddress: derivedData.ourWalletAddress,
|
|
@@ -303,12 +347,6 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
303
347
|
...derivedData,
|
|
304
348
|
})
|
|
305
349
|
|
|
306
|
-
const { accountState, eip7702Delegation, isBlacklisted } = await this.getStateUpdate({
|
|
307
|
-
derivedData,
|
|
308
|
-
tokens,
|
|
309
|
-
walletAccount,
|
|
310
|
-
})
|
|
311
|
-
|
|
312
350
|
const batch = this.aci.createOperationsBatch()
|
|
313
351
|
|
|
314
352
|
this.aci.removeTxLogBatch({
|
|
@@ -329,11 +367,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
329
367
|
}
|
|
330
368
|
|
|
331
369
|
// All updates must go through newData (accountState param is only used for mem merging)
|
|
332
|
-
const newData = {
|
|
333
|
-
...accountState,
|
|
334
|
-
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
335
|
-
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
336
|
-
}
|
|
370
|
+
const newData = { ...accountState }
|
|
337
371
|
|
|
338
372
|
if (cursor) {
|
|
339
373
|
newData.clarityCursor = cursor
|
|
@@ -360,14 +394,8 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
360
394
|
}
|
|
361
395
|
}
|
|
362
396
|
|
|
363
|
-
async
|
|
397
|
+
async getSecurityAccountState({ derivedData, walletAccount }) {
|
|
364
398
|
const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
|
|
365
|
-
const accountState = await this.getNewAccountState({
|
|
366
|
-
tokens,
|
|
367
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
368
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
369
|
-
})
|
|
370
|
-
|
|
371
399
|
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
372
400
|
server: this.server,
|
|
373
401
|
address: derivedData.ourWalletAddress,
|
|
@@ -384,7 +412,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
384
412
|
})
|
|
385
413
|
: undefined
|
|
386
414
|
|
|
387
|
-
return {
|
|
415
|
+
return { eip7702Delegation, isBlacklisted }
|
|
388
416
|
}
|
|
389
417
|
|
|
390
418
|
async addSingleTx({ tx, address, cursor }) {
|
|
@@ -151,6 +151,23 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
151
151
|
return { txsToRemove }
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
async persistSecurityState({ walletAccount, accountState, isBlacklisted, eip7702Delegation }) {
|
|
155
|
+
const securityStatePatch = {
|
|
156
|
+
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
157
|
+
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (isEmpty(securityStatePatch)) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await this.updateAccountState({
|
|
165
|
+
walletAccount,
|
|
166
|
+
accountState,
|
|
167
|
+
newData: securityStatePatch,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
async tick({ walletAccount, refresh }) {
|
|
155
172
|
await this.subscribeWalletAddresses()
|
|
156
173
|
const tickCount = this.tickCount[walletAccount]
|
|
@@ -165,18 +182,26 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
165
182
|
}, new Map())
|
|
166
183
|
const assetName = this.asset.name
|
|
167
184
|
const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
|
|
168
|
-
const {
|
|
185
|
+
const { eip7702Delegation, isBlacklisted } = await this.getSecurityAccountState({
|
|
169
186
|
derivedData,
|
|
170
|
-
tokens,
|
|
171
187
|
shouldCheckBlacklist,
|
|
172
188
|
})
|
|
173
189
|
|
|
190
|
+
await this.persistSecurityState({
|
|
191
|
+
walletAccount,
|
|
192
|
+
accountState: derivedData.currentAccountState,
|
|
193
|
+
isBlacklisted,
|
|
194
|
+
eip7702Delegation,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const accountState = await this.getNewAccountState({
|
|
198
|
+
tokens,
|
|
199
|
+
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
200
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
201
|
+
})
|
|
202
|
+
|
|
174
203
|
const batch = this.aci.createOperationsBatch()
|
|
175
|
-
const newData = {
|
|
176
|
-
...accountState,
|
|
177
|
-
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
178
|
-
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
179
|
-
}
|
|
204
|
+
const newData = { ...accountState }
|
|
180
205
|
let allTxs = []
|
|
181
206
|
let hasNewTxs = false
|
|
182
207
|
let historyError
|
|
@@ -265,13 +290,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
265
290
|
}
|
|
266
291
|
}
|
|
267
292
|
|
|
268
|
-
async
|
|
269
|
-
const accountState = await this.getNewAccountState({
|
|
270
|
-
tokens,
|
|
271
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
272
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
273
|
-
})
|
|
274
|
-
|
|
293
|
+
async getSecurityAccountState({ derivedData, shouldCheckBlacklist }) {
|
|
275
294
|
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
276
295
|
server: this.server,
|
|
277
296
|
address: derivedData.ourWalletAddress,
|
|
@@ -289,7 +308,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
289
308
|
})
|
|
290
309
|
: undefined
|
|
291
310
|
|
|
292
|
-
return {
|
|
311
|
+
return { eip7702Delegation, isBlacklisted }
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
2
|
+
|
|
3
|
+
import { ClarityMonitor } from './clarity-monitor.js'
|
|
4
|
+
import { getLogItemsFromServerTx, normalizeTransactionsResponse } from './clarity-utils/index.js'
|
|
5
|
+
import {
|
|
6
|
+
extractBalanceFromClarityAccountInfo,
|
|
7
|
+
getCurrentBlackListStatus,
|
|
8
|
+
getCurrentEIP7702Delegation,
|
|
9
|
+
} from './monitor-utils/index.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clarity v2 + HTTP `getAllTransactions` with `truncated: true` so the backend can attach
|
|
13
|
+
* `accountInfo` (pre-cursor balance/nonce/token snapshots). Merges that data in `tick` before
|
|
14
|
+
* optional RPC follow-up; does not use `rpcBalanceAssetNames` (forced empty).
|
|
15
|
+
*/
|
|
16
|
+
export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
17
|
+
constructor({ rpcBalanceAssetNames, ...args } = {}) {
|
|
18
|
+
super({ ...args, rpcBalanceAssetNames: [] })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getNewAccountState(_args) {
|
|
22
|
+
return Object.create(null)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getStateUpdate({ derivedData, tokens, shouldCheckBlacklist, currentTokenBalances }) {
|
|
26
|
+
const accountState = await this.getNewAccountState({
|
|
27
|
+
tokens,
|
|
28
|
+
currentTokenBalances: currentTokenBalances || derivedData.currentAccountState?.tokenBalances,
|
|
29
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
33
|
+
server: this.server,
|
|
34
|
+
address: derivedData.ourWalletAddress,
|
|
35
|
+
eip7702Supported: this.eip7702Supported,
|
|
36
|
+
currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
|
|
37
|
+
logger: this.logger,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const isBlacklisted = shouldCheckBlacklist
|
|
41
|
+
? await getCurrentBlackListStatus({
|
|
42
|
+
getBlackListStatus: this.getBlackListStatus,
|
|
43
|
+
address: derivedData.ourWalletAddress,
|
|
44
|
+
currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
|
|
45
|
+
logger: this.logger,
|
|
46
|
+
})
|
|
47
|
+
: undefined
|
|
48
|
+
|
|
49
|
+
return { accountState, eip7702Delegation, isBlacklisted }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply tx history and `accountInfo` first, then merge `getStateUpdate` (empty RPC snapshot)
|
|
54
|
+
* and persist — opposite order from {@link ClarityMonitor#tick} on master.
|
|
55
|
+
*/
|
|
56
|
+
async tick({ walletAccount, refresh }) {
|
|
57
|
+
await this.subscribeWalletAddresses()
|
|
58
|
+
const tickCount = this.tickCount[walletAccount]
|
|
59
|
+
const shouldCheckBlacklist = tickCount === 0
|
|
60
|
+
|
|
61
|
+
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
62
|
+
const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
|
|
63
|
+
const tokensByAddress = tokens.reduce((map, token) => {
|
|
64
|
+
const addresses = getAssetAddresses(token)
|
|
65
|
+
for (const address of addresses) map.set(address.toLowerCase(), token)
|
|
66
|
+
return map
|
|
67
|
+
}, new Map())
|
|
68
|
+
const assetName = this.asset.name
|
|
69
|
+
const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
|
|
70
|
+
const batch = this.aci.createOperationsBatch()
|
|
71
|
+
let allTxs = []
|
|
72
|
+
let hasNewTxs = false
|
|
73
|
+
let historyError
|
|
74
|
+
let balanceFromAccountInfo = Object.create(null)
|
|
75
|
+
let clarityCursor
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
79
|
+
|
|
80
|
+
;({ allTxs } = await normalizeTransactionsResponse({
|
|
81
|
+
asset: this.asset,
|
|
82
|
+
fromAddress: derivedData.ourWalletAddress,
|
|
83
|
+
response,
|
|
84
|
+
walletAccount,
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
hasNewTxs = allTxs.length > 0
|
|
88
|
+
|
|
89
|
+
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
90
|
+
getLogItemsFromServerTx,
|
|
91
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
92
|
+
allTransactionsFromServer: allTxs,
|
|
93
|
+
asset: this.asset,
|
|
94
|
+
tokensByAddress,
|
|
95
|
+
assets,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const { txsToRemove } = await this.checkPendingTransactions({
|
|
99
|
+
txlist: allTxs,
|
|
100
|
+
walletAccount,
|
|
101
|
+
refresh,
|
|
102
|
+
logItemsByAsset,
|
|
103
|
+
asset: this.asset,
|
|
104
|
+
...derivedData,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
this.aci.removeTxLogBatch({
|
|
108
|
+
assetName,
|
|
109
|
+
walletAccount,
|
|
110
|
+
txs: txsToRemove,
|
|
111
|
+
batch,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
for (const [an, txs] of Object.entries(logItemsByAsset)) {
|
|
115
|
+
this.aci.updateTxLogAndNotifyBatch({
|
|
116
|
+
assetName: an,
|
|
117
|
+
walletAccount,
|
|
118
|
+
txs,
|
|
119
|
+
refresh,
|
|
120
|
+
batch,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
balanceFromAccountInfo = extractBalanceFromClarityAccountInfo({
|
|
125
|
+
accountInfo: response.accountInfo,
|
|
126
|
+
asset: this.asset,
|
|
127
|
+
tokensByAddress,
|
|
128
|
+
})
|
|
129
|
+
clarityCursor = response.cursor
|
|
130
|
+
} catch (error) {
|
|
131
|
+
historyError = error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const currentTokenBalances = {
|
|
135
|
+
...derivedData.currentAccountState?.tokenBalances,
|
|
136
|
+
...balanceFromAccountInfo.tokenBalances,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { accountState, eip7702Delegation, isBlacklisted } = await this.getStateUpdate({
|
|
140
|
+
derivedData,
|
|
141
|
+
tokens,
|
|
142
|
+
shouldCheckBlacklist,
|
|
143
|
+
currentTokenBalances,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Pre-cursor snapshot from Clarity `accountInfo`, then RPC `accountState` (empty here) overlays.
|
|
147
|
+
const prevTokenBalances = derivedData.currentAccountState?.tokenBalances
|
|
148
|
+
const newData = {
|
|
149
|
+
...balanceFromAccountInfo,
|
|
150
|
+
...accountState,
|
|
151
|
+
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
152
|
+
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
153
|
+
...(clarityCursor !== undefined && { clarityCursor }),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (prevTokenBalances || balanceFromAccountInfo.tokenBalances || accountState.tokenBalances) {
|
|
157
|
+
newData.tokenBalances = {
|
|
158
|
+
...prevTokenBalances,
|
|
159
|
+
...balanceFromAccountInfo.tokenBalances,
|
|
160
|
+
...accountState.tokenBalances,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
this.aci.updateAccountStateBatch({
|
|
166
|
+
assetName,
|
|
167
|
+
walletAccount,
|
|
168
|
+
accountState,
|
|
169
|
+
newData,
|
|
170
|
+
batch,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await this.aci.executeOperationsBatch(batch)
|
|
174
|
+
} catch (batchError) {
|
|
175
|
+
if (!historyError) throw batchError
|
|
176
|
+
this.logger.warn('error persisting account state after history failure', batchError)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (historyError) {
|
|
180
|
+
throw historyError
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (refresh || hasNewTxs) {
|
|
184
|
+
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
185
|
+
transactions: allTxs,
|
|
186
|
+
tokensByAddress,
|
|
187
|
+
})
|
|
188
|
+
if (unknownTokenAddresses.length > 0) {
|
|
189
|
+
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getHistoryFromServer({ walletAccount, derivedData, refresh }) {
|
|
195
|
+
const address = derivedData.ourWalletAddress
|
|
196
|
+
const currentCursor = derivedData.currentAccountState?.clarityCursor
|
|
197
|
+
const cursor = currentCursor && !refresh ? currentCursor : null
|
|
198
|
+
return this.server.getAllTransactions({
|
|
199
|
+
walletAccount,
|
|
200
|
+
address,
|
|
201
|
+
cursor,
|
|
202
|
+
refresh,
|
|
203
|
+
withInput: true,
|
|
204
|
+
truncated: true,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# `clarity-truncated-history` monitor
|
|
2
|
+
|
|
3
|
+
Extends `ClarityMonitor`. Used when Clarity only serves a **recent window** of txs and attaches pre-cursor snapshots in `accountInfo`.
|
|
4
|
+
|
|
5
|
+
- Sends `truncated=true` on `getAllTransactions`.
|
|
6
|
+
- Folds `accountInfo` into `accountState`.
|
|
7
|
+
- Forces `rpcBalanceAssetNames = []` (no per-asset RPC `eth_getBalance` batch).
|
|
8
|
+
|
|
9
|
+
Plugins opting in: `bsc-plugin`, `basemainnet-plugin`, `ethereum-plugin`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Flow
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
A[Clarity HTTP<br/>GET /addresses/:addr/transactions<br/>truncated=true & cursor] --> B[response]
|
|
18
|
+
B --> B1[transactions.confirmed + .pending]
|
|
19
|
+
B --> B2[accountInfo rows<br/>rebase/special tokens only]
|
|
20
|
+
B --> B3[cursor]
|
|
21
|
+
|
|
22
|
+
B1 --> C[normalizeTransactionsResponse<br/>filter spam + stale pending]
|
|
23
|
+
C --> D[getAllLogItemsByAsset<br/>bucket per assetName]
|
|
24
|
+
D --> E[aci.updateTxLogAndNotifyBatch<br/>per asset]
|
|
25
|
+
|
|
26
|
+
B2 --> F[extractBalanceFromClarityAccountInfo<br/>balance? nonce? tokenBalances?]
|
|
27
|
+
|
|
28
|
+
E --> G[accountState]
|
|
29
|
+
F --> G
|
|
30
|
+
B3 --> G[accountState<br/>balance, nonce, tokenBalances,<br/>clarityCursor]
|
|
31
|
+
|
|
32
|
+
G --> H1[getBalances - asset.api.getBalances]
|
|
33
|
+
G --> H2[resolveNonce - tx send path]
|
|
34
|
+
|
|
35
|
+
H1 --> I1{txLog empty?}
|
|
36
|
+
I1 -- yes --> I2[accountState.balance]
|
|
37
|
+
I1 -- no --> I3{accountState<br/>balance<br/>non-zero?}
|
|
38
|
+
I3 -- yes --> I2
|
|
39
|
+
I3 -- no --> I4[walk txLog for latest<br/>balanceChange]
|
|
40
|
+
|
|
41
|
+
H2 --> J1[max - nonceFromTxLog,<br/>accountState.nonce]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Left-to-right: `accountInfo` only writes `accountState` for what Clarity can't derive from `walletChanges` (rebase tokens). Regular ERC-20s and ETH flow through the tx log. `getBalances` and `resolveNonce` then read `accountState` + `txLog` together.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Example response (trimmed)
|
|
49
|
+
|
|
50
|
+
Wallet `0x3F80…9164` on Ethereum. Two txs + the one real `accountInfo` row:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"transactions": [
|
|
55
|
+
{
|
|
56
|
+
"blockNumber": 24190233,
|
|
57
|
+
"hash": "0x7544…35bb",
|
|
58
|
+
"from": "0xccef06b2…8136c",
|
|
59
|
+
"to": "0xfa7093cd…fa4b9",
|
|
60
|
+
"effects": [
|
|
61
|
+
{
|
|
62
|
+
"address": "0xa0b86991…0eb48",
|
|
63
|
+
"effect": "erc20",
|
|
64
|
+
"from": "0xfa7093cd…fa4b9",
|
|
65
|
+
"to": "0x3f80…9164",
|
|
66
|
+
"value": "9668069"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"walletChanges": [
|
|
70
|
+
{
|
|
71
|
+
"wallet": "0x3f80…9164",
|
|
72
|
+
"type": "token",
|
|
73
|
+
"from": "0",
|
|
74
|
+
"to": "9668069",
|
|
75
|
+
"contract": "0xa0b86991…0eb48"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"blockNumber": 24204497,
|
|
81
|
+
"hash": "0xb5fa…5b23",
|
|
82
|
+
"from": "0x3f80…9164",
|
|
83
|
+
"to": "0x2a8e…5e86",
|
|
84
|
+
"nonce": "0",
|
|
85
|
+
"effects": [
|
|
86
|
+
{
|
|
87
|
+
"address": "0x2a8e…5e86",
|
|
88
|
+
"effect": "erc20",
|
|
89
|
+
"from": "0x3f80…9164",
|
|
90
|
+
"to": "0xcbad…4853",
|
|
91
|
+
"value": "10000000000000000000"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"walletChanges": [
|
|
95
|
+
{
|
|
96
|
+
"wallet": "0x3f80…9164",
|
|
97
|
+
"type": "balance",
|
|
98
|
+
"from": "254470008324222",
|
|
99
|
+
"to": "101067364839680"
|
|
100
|
+
},
|
|
101
|
+
{ "wallet": "0x3f80…9164", "type": "nonce", "from": "0", "to": "1" }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"cursor": 24211168,
|
|
106
|
+
"accountInfo": [
|
|
107
|
+
{
|
|
108
|
+
"type": "token",
|
|
109
|
+
"balanceType": "account_balance",
|
|
110
|
+
"value": "20191469428776270391810",
|
|
111
|
+
"blockNumber": 24933420,
|
|
112
|
+
"assetId": "0x2a8e…5e86",
|
|
113
|
+
"assetName": "ousd_ethereum_48fcf72d",
|
|
114
|
+
"decimals": 18
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **Tx A** — incoming USDC (9.668069). Balance derivable from `walletChanges`.
|
|
121
|
+
- **Tx B** — first outgoing OUSD send, nonce `0 → 1`, ETH balance delta.
|
|
122
|
+
- **`accountInfo`** — OUSD rebase balance ≈ **20,191.469 OUSD**, authoritative.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## `accountState` after this tick
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
{
|
|
130
|
+
clarityCursor: <Buffer LE 24211168>,
|
|
131
|
+
tokenBalances: {
|
|
132
|
+
ousd_ethereum_48fcf72d: NumberUnit('20191.469… OUSD'), // from accountInfo
|
|
133
|
+
},
|
|
134
|
+
balance: ZERO, // unchanged by monitor; derived live by getBalances
|
|
135
|
+
nonce: 0, // unchanged by monitor; derived live by resolveNonce
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`accountInfo` **only** wrote `tokenBalances.ousd_...` here — no `type: 'balance'` or `type: 'nonce'` rows were sent. Everything else is derived at read time from the tx log.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Read paths
|
|
144
|
+
|
|
145
|
+
### `getBalances`
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
asset branch taken result
|
|
149
|
+
ethereum (ETH) walk txLog → latest 43_326_434_520_430 wei
|
|
150
|
+
balanceChange.to
|
|
151
|
+
usdc walk txLog → latest 95_948_474_200 base
|
|
152
|
+
walletChange.to
|
|
153
|
+
ousd_ethereum_48fcf72d accountState short-circuit 20_191_469_428_… base
|
|
154
|
+
(non-zero tokenBalances) (from accountInfo)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Source: `getAbsoluteBalance` in `src/get-balances.js`, ordered branches:
|
|
158
|
+
|
|
159
|
+
1. `txLog.size === 0` → `getBalanceFromAccountState`
|
|
160
|
+
2. `accountState` non-zero → return it (`getBalanceFromAccountStateIfAny` skips default `ZERO` via `NumberUnit.isZero` getter)
|
|
161
|
+
3. Walk reversed `txLog` for canonical `balanceChange` tx
|
|
162
|
+
|
|
163
|
+
### `resolveNonce` (`src/tx-send/nonce-utils.js`)
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
nonce = Math.max(
|
|
167
|
+
nonceFromTxLog, // latest canonical `type: 'nonce'` walletChange
|
|
168
|
+
accountState.nonce || 0 // populated from accountInfo row if sent
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`accountInfo` `nonce` row patches the gap when truncation hides the latest `nonceChange`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Monitor + server wiring
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
// create-asset-utils.js
|
|
180
|
+
case 'clarity-truncated-history':
|
|
181
|
+
monitor = new ClarityTruncatedHistoryMonitor({ server, ...args }) // server is ClarityServerV2
|
|
182
|
+
|
|
183
|
+
// exodus-eth-server/index.js — no new server class
|
|
184
|
+
case 'clarity-v2':
|
|
185
|
+
case 'clarity-truncated-history': // normalized to clarity-v2
|
|
186
|
+
return new ClarityServerV2(...)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Backend contract
|
|
192
|
+
|
|
193
|
+
1. Accept `?truncated=true` on `GET /addresses/:addr/transactions`.
|
|
194
|
+
2. Attach `accountInfo: []` (or omit) when nothing to snapshot.
|
|
195
|
+
3. Emit `accountInfo` **only** for assets that can't be derived from `walletChanges` (rebase / `balanceType: 'account_balance'`).
|
|
196
|
+
4. Row shapes: `{ type: 'balance' | 'nonce' | 'token', value, blockNumber, assetId?, assetName?, decimals? }`. Sorted client-side by `blockNumber` desc; first value per type/assetId wins.
|
|
197
|
+
5. Unknown `assetId` → client drops the row.
|
|
198
|
+
|
|
199
|
+
If the backend can't emit `accountInfo`, the monitor degrades gracefully to `clarity-v2` behavior (tx-log walk only).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Tests
|
|
204
|
+
|
|
205
|
+
- `src/tx-log/__tests__/clarity-truncated-history-monitor.test.js` — `truncated: true`, no RPC balance batch, accountInfo → `tokenBalances`.
|
|
206
|
+
- `src/__tests__/create-asset-utils.test.js` — factory → `ClarityTruncatedHistoryMonitor`.
|
|
207
|
+
- `src/__tests__/get-balances.test.js` — accountState short-circuit (truncated history cases).
|
|
208
|
+
- `src/__tests__/balances-model.test.js` — default-ZERO accountState still falls through (regression for `NumberUnit.isZero` getter).
|
package/src/tx-log/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EthereumMonitor } from './ethereum-monitor.js'
|
|
2
2
|
export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
|
|
3
3
|
export { ClarityMonitor } from './clarity-monitor.js'
|
|
4
|
+
export { ClarityTruncatedHistoryMonitor } from './clarity-truncated-history-monitor.js'
|
|
4
5
|
export { getOptimisticTxLogEffects } from './get-optimistic-txlog-effects.js'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps Clarity `accountInfo` rows (GET .../transactions?truncated=true) into fields aligned
|
|
3
|
+
* with Ethereum-like account state: `balance`, `tokenBalances`, `nonce` (plus base units
|
|
4
|
+
* for amounts via `currency.baseUnit`).
|
|
5
|
+
* Rows are applied newest-first by `blockNumber` so duplicate types resolve like
|
|
6
|
+
* walletChanges-derived snapshots.
|
|
7
|
+
*/
|
|
8
|
+
export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, tokensByAddress }) => {
|
|
9
|
+
if (!accountInfo || accountInfo.length === 0) return {}
|
|
10
|
+
|
|
11
|
+
const sorted = [...accountInfo].sort((a, b) => (b.blockNumber || 0) - (a.blockNumber || 0))
|
|
12
|
+
|
|
13
|
+
const result = {}
|
|
14
|
+
let haveBalance = false
|
|
15
|
+
let haveNonce = false
|
|
16
|
+
const seenAssetIds = new Set()
|
|
17
|
+
const tokenBalances = Object.create(null)
|
|
18
|
+
|
|
19
|
+
for (const row of sorted) {
|
|
20
|
+
switch (row.type) {
|
|
21
|
+
case 'balance':
|
|
22
|
+
if (!haveBalance && row.value != null) {
|
|
23
|
+
result.balance = asset.currency.baseUnit(row.value)
|
|
24
|
+
haveBalance = true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
case 'nonce': {
|
|
30
|
+
if (haveNonce) break
|
|
31
|
+
const nonce = Number(row.value)
|
|
32
|
+
|
|
33
|
+
if (Number.isFinite(nonce) && nonce >= 0) {
|
|
34
|
+
result.nonce = nonce
|
|
35
|
+
haveNonce = true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'token': {
|
|
42
|
+
const assetId = row.assetId?.toLowerCase()
|
|
43
|
+
|
|
44
|
+
if (!assetId || seenAssetIds.has(assetId) || row.value == null) break
|
|
45
|
+
const token = tokensByAddress.get(assetId)
|
|
46
|
+
if (!token) break
|
|
47
|
+
tokenBalances[token.name] = token.currency.baseUnit(row.value)
|
|
48
|
+
seenAssetIds.add(assetId)
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Object.keys(tokenBalances).length > 0) {
|
|
58
|
+
result.tokenBalances = tokenBalances
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
@@ -8,4 +8,5 @@ export { default as getDeriveTransactionsToCheck } from './get-derive-transactio
|
|
|
8
8
|
export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
|
|
9
9
|
export { default as getCurrentBlackListStatus } from './get-current-blacklist-status.js'
|
|
10
10
|
export { default as verifyRpcPendingTxStatusBatch } from './verify-pending-tx-status-rpc.js'
|
|
11
|
+
export { extractBalanceFromClarityAccountInfo } from './extract-balance-from-clarity-account-info.js'
|
|
11
12
|
export * from './exclude-unchanged-token-balances.js'
|
|
@@ -16,6 +16,7 @@ export const resolveNonce = async ({
|
|
|
16
16
|
forceFromNode,
|
|
17
17
|
fromAddress,
|
|
18
18
|
txLog = [],
|
|
19
|
+
accountState,
|
|
19
20
|
tag = BLOCK_TAG_LATEST,
|
|
20
21
|
useAbsoluteNonce,
|
|
21
22
|
}) => {
|
|
@@ -25,7 +26,13 @@ export const resolveNonce = async ({
|
|
|
25
26
|
return getNonce({ asset: asset.baseAsset, address: fromAddress, tag })
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const nonceFromTxLog = getNonceFromTxLog({ txLog, useAbsoluteNonce, tag })
|
|
30
|
+
|
|
31
|
+
// Fallback to accountState nonce when txLog has no nonce info (truncated history).
|
|
32
|
+
// accountState.nonce is populated from Clarity accountInfo during monitor ticks.
|
|
33
|
+
const nonceFromAccountState = accountState?.nonce || 0
|
|
34
|
+
|
|
35
|
+
return Math.max(nonceFromTxLog, nonceFromAccountState)
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const getLatestTxWithNonceChange = ({ reversedTxLog }) => {
|