@exodus/ethereum-api 9.0.1 → 9.0.3
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/get-balances.js +23 -13
- package/src/tx-log/clarity-truncated-history-monitor.js +32 -11
- package/src/tx-log/clarity-truncated-history-monitor.md +6 -2
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +7 -1
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
|
+
## [9.0.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.2...@exodus/ethereum-api@9.0.3) (2026-10-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ethereum-api:** prefer newer tx-log drain over stale token balance ([#8793](https://github.com/ExodusMovement/assets/issues/8793)) ([f6cc0e4](https://github.com/ExodusMovement/assets/commit/f6cc0e4031f89c71951e206c9c09fe042aa9354c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [9.0.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.1...@exodus/ethereum-api@9.0.2) (2026-10-02)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **ethereum-api:** keep balances on truncated refresh when history fails ([#8739](https://github.com/ExodusMovement/assets/issues/8739)) ([2c8bdb5](https://github.com/ExodusMovement/assets/commit/2c8bdb590ae0f91a93eae377c965dda9bda90a74))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [9.0.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.0...@exodus/ethereum-api@9.0.1) (2026-10-02)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.3",
|
|
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",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "e783e7a31c40807698ae2b9629e1c2dd5c03c228"
|
|
70
70
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -38,12 +38,17 @@ const getBaseBalanceFromAccountState = ({ asset, txLog, accountState }) => {
|
|
|
38
38
|
* Token counterpart of {@link getBaseBalance}. Tokens are sparse — there's no
|
|
39
39
|
* model default for `tokenBalances[name]`, so any value present (including an
|
|
40
40
|
* explicit ZERO from `accountInfo` after the user drained the token) is
|
|
41
|
-
* authoritative and returned as-is
|
|
42
|
-
*
|
|
41
|
+
* authoritative and returned as-is, unless the txLog holds a canonical
|
|
42
|
+
* `balanceChange` from a newer block than the `account_balance` row (or the
|
|
43
|
+
* row's block is unknown). Only when the key is missing entirely do we fall
|
|
44
|
+
* through to the truncated baseline / txLog walk.
|
|
43
45
|
*/
|
|
44
|
-
const getTokenBalanceFromAccountState = ({ asset, txLog, accountState }) => {
|
|
46
|
+
const getTokenBalanceFromAccountState = ({ asset, txLog, accountState, latestBalanceTx }) => {
|
|
45
47
|
const balance = getBalanceFromAccountStateIfAny({ asset, accountState })
|
|
46
|
-
|
|
48
|
+
const balanceBlockNumber = accountState?.tokenBalanceBlockNumbers?.[asset.name]
|
|
49
|
+
const isOlderThanTxLog =
|
|
50
|
+
latestBalanceTx && !(balanceBlockNumber >= latestBalanceTx.data.blockNumber)
|
|
51
|
+
if (balance && !isOlderThanTxLog) return balance
|
|
47
52
|
|
|
48
53
|
if (txLog.size === 0) {
|
|
49
54
|
return getTruncatedTokenBalance({ asset, accountState }) ?? asset.currency.ZERO
|
|
@@ -96,15 +101,6 @@ export const getAbsoluteBalance = ({ asset, txLog, accountState }) => {
|
|
|
96
101
|
assert(asset, 'asset is required')
|
|
97
102
|
assert(txLog, 'txLog is required')
|
|
98
103
|
|
|
99
|
-
const isBase = asset.name === asset.baseAsset.name
|
|
100
|
-
const accountStateBalance = isBase
|
|
101
|
-
? getBaseBalanceFromAccountState({ asset, txLog, accountState })
|
|
102
|
-
: getTokenBalanceFromAccountState({ asset, txLog, accountState })
|
|
103
|
-
if (accountStateBalance) {
|
|
104
|
-
// Authoritative `accountState` snapshot (or empty-txLog terminal value).
|
|
105
|
-
return accountStateBalance
|
|
106
|
-
}
|
|
107
|
-
|
|
108
104
|
// NOTE: We reverse the `txLog` to prioritize the handling
|
|
109
105
|
// of the most recent transactions first. The reason
|
|
110
106
|
// we do this is to terminate early if we find an
|
|
@@ -113,6 +109,20 @@ export const getAbsoluteBalance = ({ asset, txLog, accountState }) => {
|
|
|
113
109
|
|
|
114
110
|
const maybeLatestAbsoluteBalanceTx = getLatestCanonicalAbsoluteBalanceTx({ reversedTxLog })
|
|
115
111
|
|
|
112
|
+
const isBase = asset.name === asset.baseAsset.name
|
|
113
|
+
const accountStateBalance = isBase
|
|
114
|
+
? getBaseBalanceFromAccountState({ asset, txLog, accountState })
|
|
115
|
+
: getTokenBalanceFromAccountState({
|
|
116
|
+
asset,
|
|
117
|
+
txLog,
|
|
118
|
+
accountState,
|
|
119
|
+
latestBalanceTx: maybeLatestAbsoluteBalanceTx,
|
|
120
|
+
})
|
|
121
|
+
if (accountStateBalance) {
|
|
122
|
+
// Authoritative `accountState` snapshot (or empty-txLog terminal value).
|
|
123
|
+
return accountStateBalance
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
let balance = asset.currency.ZERO
|
|
117
127
|
let hasAbsoluteBalance = false
|
|
118
128
|
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { ClarityMonitor } from './clarity-monitor.js'
|
|
2
|
+
import { isCanonicalAbsoluteBalanceTx } from './clarity-utils/index.js'
|
|
2
3
|
import { extractBalanceFromClarityAccountInfo } from './monitor-utils/index.js'
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const omitTokensWithNewerConfirmedBalanceChange = ({
|
|
6
|
+
tokenBalances,
|
|
7
|
+
tokenBalanceBlockNumbers,
|
|
8
|
+
logItemsByAsset,
|
|
9
|
+
}) => {
|
|
10
|
+
const balances = { ...tokenBalances }
|
|
11
|
+
const blockNumbers = { ...tokenBalanceBlockNumbers }
|
|
12
|
+
for (const [assetName, txs] of Object.entries(logItemsByAsset ?? {})) {
|
|
13
|
+
const isNewer = (tx) => !(blockNumbers[assetName] >= tx.data.blockNumber)
|
|
14
|
+
if (txs.some((tx) => tx.confirmations > 0 && isCanonicalAbsoluteBalanceTx(tx) && isNewer(tx))) {
|
|
15
|
+
delete balances[assetName]
|
|
16
|
+
delete blockNumbers[assetName]
|
|
17
|
+
}
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
return
|
|
20
|
+
return { tokenBalances: tokenBalances && balances, tokenBalanceBlockNumbers: blockNumbers }
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
/**
|
|
@@ -49,11 +58,12 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
|
49
58
|
// truncated), we must ensure older checkpoints cannot
|
|
50
59
|
// be inadvertently relied upon.
|
|
51
60
|
//
|
|
52
|
-
if (refresh) {
|
|
61
|
+
if (refresh && historyResponse) {
|
|
53
62
|
const defaultAccountState = this.asset.api.createAccountState().create()
|
|
54
63
|
resetBalances = {
|
|
55
64
|
balance: defaultAccountState.balance,
|
|
56
65
|
tokenBalances: defaultAccountState.tokenBalances,
|
|
66
|
+
tokenBalanceBlockNumbers: defaultAccountState.tokenBalanceBlockNumbers,
|
|
57
67
|
}
|
|
58
68
|
currentAccountState = (currentAccountState ?? defaultAccountState).merge(resetBalances)
|
|
59
69
|
}
|
|
@@ -64,10 +74,12 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
|
64
74
|
tokensByAddress,
|
|
65
75
|
})
|
|
66
76
|
|
|
67
|
-
const prevTokenBalances =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
const { tokenBalances: prevTokenBalances, tokenBalanceBlockNumbers: prevBlockNumbers } =
|
|
78
|
+
omitTokensWithNewerConfirmedBalanceChange({
|
|
79
|
+
tokenBalances: currentAccountState?.tokenBalances,
|
|
80
|
+
tokenBalanceBlockNumbers: currentAccountState?.tokenBalanceBlockNumbers,
|
|
81
|
+
logItemsByAsset,
|
|
82
|
+
})
|
|
71
83
|
|
|
72
84
|
const { accountState } = await this.getStateUpdate({
|
|
73
85
|
derivedData,
|
|
@@ -92,6 +104,15 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
|
|
|
92
104
|
...balanceFromAccountInfo.tokenBalances,
|
|
93
105
|
...accountState.tokenBalances,
|
|
94
106
|
}
|
|
107
|
+
const tokenBalanceBlockNumbers = { ...prevBlockNumbers }
|
|
108
|
+
for (const tokenName of Object.keys(balanceFromAccountInfo.tokenBalances ?? {})) {
|
|
109
|
+
delete tokenBalanceBlockNumbers[tokenName]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
newData.tokenBalanceBlockNumbers = {
|
|
113
|
+
...tokenBalanceBlockNumbers,
|
|
114
|
+
...balanceFromAccountInfo.tokenBalanceBlockNumbers,
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
if (
|
|
@@ -131,6 +131,9 @@ Wallet `0x3F80…9164` on Ethereum. Two txs + the one real `accountInfo` row:
|
|
|
131
131
|
tokenBalances: {
|
|
132
132
|
ousd_ethereum_48fcf72d: NumberUnit('20191.469… OUSD'), // account_balance row
|
|
133
133
|
},
|
|
134
|
+
tokenBalanceBlockNumbers: {
|
|
135
|
+
ousd_ethereum_48fcf72d: 24933420, // account_balance row blockNumber
|
|
136
|
+
},
|
|
134
137
|
truncatedAccountState: {
|
|
135
138
|
balance: ZERO,
|
|
136
139
|
tokenBalances: {},
|
|
@@ -162,7 +165,7 @@ Source: `getAbsoluteBalance` in `src/get-balances.js`, ordered branches:
|
|
|
162
165
|
|
|
163
166
|
1. Non-zero `accountState` balance → return it.
|
|
164
167
|
2. `txLog.size === 0` → return `truncatedAccountState` balance or `ZERO`.
|
|
165
|
-
3. Token value exists in `accountState.tokenBalances` → return accountState balance, including explicit zero.
|
|
168
|
+
3. Token value exists in `accountState.tokenBalances` → return accountState balance, including explicit zero, unless the txLog's latest canonical `balanceChange` is in a newer block than `tokenBalanceBlockNumbers[token]` (or that block is unknown).
|
|
166
169
|
4. Walk reversed `txLog` for canonical `balanceChange` tx.
|
|
167
170
|
5. If no absolute balance exists, fall back to the txLog-derived balance path.
|
|
168
171
|
|
|
@@ -205,7 +208,8 @@ case 'clarity-truncated-history': // normalized to clarity-v2
|
|
|
205
208
|
4. Emit `source: 'truncated'` rows on the initial `cursor=0` fetch for state that existed before the returned history window.
|
|
206
209
|
5. Row shapes: `{ type: 'balance' | 'nonce' | 'token', source: 'account_balance' | 'truncated', value, blockNumber, assetId?, assetName?, decimals? }`. Sorted client-side by `blockNumber` desc; first value per type/assetId wins for `balance`/`token`. `nonce` rows are collapsed via `Math.max` regardless of source.
|
|
207
210
|
6. Unknown `assetId` → client drops the row.
|
|
208
|
-
7. `
|
|
211
|
+
7. `blockNumber` on token `account_balance` rows is the block the balance was read at. The client stores it and prefers the txLog when its latest `balanceChange` is newer. So a drained token with no new row falls back to the txLog value.
|
|
212
|
+
8. `value` arrives as a decimal string. `null`, `undefined`, and empty-string `""` mean "no value" and the row is skipped.
|
|
209
213
|
|
|
210
214
|
If the backend can't emit `accountInfo`, the monitor degrades gracefully to `clarity-v2` behavior (tx-log walk only).
|
|
211
215
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* with Ethereum-like account state: `balance`, `tokenBalances`, `nonce` (plus base units
|
|
4
4
|
* for amounts via `currency.baseUnit`).
|
|
5
5
|
* `account_balance` source rows are stored in `balance`/`tokenBalances`; truncated source rows
|
|
6
|
-
* are stored in `truncatedAccountState`.
|
|
6
|
+
* are stored in `truncatedAccountState`. Token `account_balance` rows also record their
|
|
7
|
+
* `blockNumber` in `tokenBalanceBlockNumbers` so reads can tell whether the tx-log is newer.
|
|
7
8
|
*
|
|
8
9
|
* NOTE: All `row.value` payloads arrive over the wire as decimal strings (e.g. `"0"`,
|
|
9
10
|
* `"20191469428776270391810"`). They're parsed with `currency.baseUnit(...)` for amounts
|
|
@@ -28,6 +29,7 @@ export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, token
|
|
|
28
29
|
const result = {}
|
|
29
30
|
const seenAssetIds = new Set()
|
|
30
31
|
const tokenBalances = Object.create(null)
|
|
32
|
+
const tokenBalanceBlockNumbers = Object.create(null)
|
|
31
33
|
const truncatedTokenBalances = Object.create(null)
|
|
32
34
|
let truncatedBalance
|
|
33
35
|
|
|
@@ -70,6 +72,9 @@ export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, token
|
|
|
70
72
|
const balance = token.currency.baseUnit(row.value)
|
|
71
73
|
if (row.source === ACCOUNT_BALANCE) {
|
|
72
74
|
tokenBalances[token.name] = balance
|
|
75
|
+
if (Number.isInteger(row.blockNumber)) {
|
|
76
|
+
tokenBalanceBlockNumbers[token.name] = row.blockNumber
|
|
77
|
+
}
|
|
73
78
|
} else if (row.source === TRUNCATED) {
|
|
74
79
|
truncatedTokenBalances[token.name] = balance
|
|
75
80
|
} else {
|
|
@@ -87,6 +92,7 @@ export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, token
|
|
|
87
92
|
|
|
88
93
|
if (Object.keys(tokenBalances).length > 0) {
|
|
89
94
|
result.tokenBalances = tokenBalances
|
|
95
|
+
result.tokenBalanceBlockNumbers = tokenBalanceBlockNumbers
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
const hasTruncatedTokenBalances = Object.keys(truncatedTokenBalances).length > 0
|