@exodus/ethereum-api 8.62.1 → 8.62.2

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,16 @@
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.62.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.1...@exodus/ethereum-api@8.62.2) (2026-01-07)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: when using absolute balances, ensure we process the most recent record (#7127)
13
+
14
+
15
+
6
16
  ## [8.62.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.62.0...@exodus/ethereum-api@8.62.1) (2026-01-01)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.62.1",
3
+ "version": "8.62.2",
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",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "18e9c9aa3976c189bbba24d8cad7c413ddec336d"
70
+ "gitHead": "be4898d14d842c79dfcc327501c390dfaf56157b"
71
71
  }
@@ -5,6 +5,66 @@ import {
5
5
  getUnconfirmedSentBalance,
6
6
  } from '@exodus/asset-lib'
7
7
  import assert from 'minimalistic-assert'
8
+ import ms from 'ms'
9
+
10
+ const DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH = ms('5m')
11
+
12
+ // Defines whether a `Tx` is an absolute balance
13
+ // node that can be sorted in terms of transaction
14
+ // ordering.
15
+ //
16
+ export const isCanonicalAbsoluteBalanceTx = (tx) =>
17
+ Boolean(
18
+ tx?.data?.balanceChange &&
19
+ typeof tx.data.blockNumber === 'number' &&
20
+ typeof tx.data.transactionIndex === 'number'
21
+ )
22
+
23
+ // Attempts to find the most recent `absoluteBalance` Tx for
24
+ // a given `txLog`.
25
+ //
26
+ // NOTE: Returns `null` if nothing is found.
27
+ // NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
28
+ // which are not guaranteed to exist in older transaction
29
+ // histories.
30
+ export const getLatestCanonicalAbsoluteBalanceTx = ({
31
+ // Due to nondeterministic sorting for records in close
32
+ // proximity to one-another on high TPS chains, the
33
+ // order of the `reversedTxLog` cannot be trusted to be
34
+ // exactly precise since two consecutive blocks may share
35
+ // the same timestamp. Therefore, when finding an absolute
36
+ // balance node, we use the`searchDepthMs` to take into
37
+ // account extra nodes chronologically deeper than what we
38
+ // presume to be the latest to make sure we resolve to the
39
+ // correct node.
40
+ searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_BALANCE_SEARCH_DEPTH,
41
+ reversedTxLog,
42
+ }) => {
43
+ assert(reversedTxLog, 'expected reversedTxLog')
44
+
45
+ let latest = null
46
+
47
+ for (const tx of reversedTxLog) {
48
+ if (latest) {
49
+ const diff = +latest.date - +tx.date
50
+
51
+ if (diff >= searchDepthMs) break
52
+ }
53
+
54
+ if (!isCanonicalAbsoluteBalanceTx(tx)) continue
55
+
56
+ if (
57
+ !latest ||
58
+ tx.data.blockNumber > latest.data.blockNumber ||
59
+ (tx.data.blockNumber === latest.data.blockNumber &&
60
+ tx.data.transactionIndex > latest.data.transactionIndex)
61
+ ) {
62
+ latest = tx
63
+ }
64
+ }
65
+
66
+ return latest
67
+ }
8
68
 
9
69
  /**
10
70
  * TODO: Balance model gaps for EVM staking assets
@@ -32,7 +92,6 @@ import assert from 'minimalistic-assert'
32
92
  *
33
93
  * See: docs/balances-model.md for the intended balance model spec.
34
94
  */
35
-
36
95
  export const getAbsoluteBalance = ({ asset, txLog }) => {
37
96
  assert(asset, 'asset is required')
38
97
  assert(txLog, 'txLog is required')
@@ -41,13 +100,26 @@ export const getAbsoluteBalance = ({ asset, txLog }) => {
41
100
  return asset.currency.ZERO
42
101
  }
43
102
 
103
+ // NOTE: We reverse the `txLog` to prioritize the handling
104
+ // of the most recent transactions first. The reason
105
+ // we do this is to terminate early if we find an
106
+ // absolute balance node.
107
+ const reversedTxLog = txLog.reverse()
108
+
109
+ const maybeLatestAbsoluteBalanceTx = getLatestCanonicalAbsoluteBalanceTx({ reversedTxLog })
110
+
44
111
  let balance = asset.currency.ZERO
45
112
  let hasAbsoluteBalance = false
46
113
 
47
- const reversedTxLog = txLog.reverse()
48
-
49
114
  for (const tx of reversedTxLog) {
50
115
  if (tx.data.balanceChange) {
116
+ // If we are aware of the existence of the most
117
+ // recent absolute balance, then we can ignore
118
+ // any competing instances.
119
+ if (maybeLatestAbsoluteBalanceTx && tx.txId !== maybeLatestAbsoluteBalanceTx.txId) {
120
+ continue
121
+ }
122
+
51
123
  hasAbsoluteBalance = true
52
124
  balance = balance.add(asset.currency.baseUnit(tx.data.balanceChange.to))
53
125
 
@@ -510,7 +510,6 @@ export function createEthereumStakingService({
510
510
  walletAccount,
511
511
  address: to,
512
512
  amount: amount || asset.currency.ZERO,
513
- shouldLog: true,
514
513
  txInput,
515
514
  gasPrice,
516
515
  gasLimit,
@@ -430,7 +430,6 @@ export function createPolygonStakingService({
430
430
  walletAccount,
431
431
  address: to,
432
432
  amount: asset.currency.ZERO,
433
- shouldLog: true,
434
433
  txInput,
435
434
  gasPrice,
436
435
  gasLimit,
@@ -24,6 +24,8 @@ export default function getLogItemsFromServerTx({
24
24
  const txId = serverTx.hash
25
25
  const nonce = parseInt(serverTx.nonce, 10)
26
26
  const gasLimit = parseInt(serverTx.gas, 10)
27
+ const blockNumber = serverTx.blockNumber
28
+ const transactionIndex = parseInt(serverTx.transactionIndex, 10)
27
29
  const error = serverTx.error || (serverTx.status === '0' ? 'Failed' : null)
28
30
  const feeAmount = getFeeAmount(asset, serverTx)
29
31
  const internalTransfers = filterEffects(serverTx.effects, 'internal') || []
@@ -91,6 +93,8 @@ export default function getLogItemsFromServerTx({
91
93
  nonceChange: nonceUpdate,
92
94
  ...methodId,
93
95
  ...(sent?.length > 0 ? { sent } : undefined),
96
+ blockNumber,
97
+ transactionIndex,
94
98
  },
95
99
  ...(ourWalletWasSender
96
100
  ? { from: [], to: toAddress, feeAmount, feeCoinName: asset.feeAsset.name }
@@ -149,7 +153,15 @@ export default function getLogItemsFromServerTx({
149
153
  ...logItemCommonProperties,
150
154
  coinAmount,
151
155
  coinName: tokenName,
152
- data: { data, nonce, gasLimit, balanceChange, ...methodId },
156
+ data: {
157
+ data,
158
+ nonce,
159
+ gasLimit,
160
+ balanceChange,
161
+ ...methodId,
162
+ blockNumber,
163
+ transactionIndex,
164
+ },
153
165
  ...(isConsideredSent
154
166
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: token.feeAsset.name }
155
167
  : { from: tokenFromAddresses }),