@exodus/ethereum-api 8.62.0 → 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,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.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
+
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)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * Fix: expose matic 'liquid' rewards (#7151)
23
+
24
+
25
+
6
26
  ## [8.62.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.61.3...@exodus/ethereum-api@8.62.0) (2025-12-22)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.62.0",
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": "5702f44af435bd6781879ba3e9b2f28fba655c6c"
70
+ "gitHead": "be4898d14d842c79dfcc327501c390dfaf56157b"
71
71
  }
@@ -5,7 +5,93 @@ 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
8
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
+ }
68
+
69
+ /**
70
+ * TODO: Balance model gaps for EVM staking assets
71
+ *
72
+ * Missing fields that should be added to getBalancesFactory return value:
73
+ *
74
+ * 1. `rewards` / `liquidRewards` - Currently, accountState.staking[asset.name] contains
75
+ * rewardsBalance and liquidRewards (from getPolygonStakingInfo / getEthereumStakingInfo),
76
+ * but getBalancesFactory doesn't read or expose them. Need a getRewards() helper similar
77
+ * to getStaked() and getStaking().
78
+ *
79
+ * 2. `rewardsBalance` (total lifetime rewards) - Available in accountState but not surfaced.
80
+ *
81
+ * 3. `withdrawable` - For Polygon, this represents claimable rewards, distinct from
82
+ * unclaimedUndelegatedBalance (unstaked principal). Currently conflated.
83
+ *
84
+ * 4. `walletReserve` - Documented in balances-model.md but not implemented.
85
+ *
86
+ * 5. `staking` field reads pendingBalance + pendingDepositedBalance, which Polygon staking
87
+ * doesn't return (only Ethereum native staking uses these). Needs asset-specific handling.
88
+ *
89
+ * The data flow already exists: monitor hooks call getStakingInfo() → accountState.staking[asset.name].
90
+ * Implementation would add helper functions (getLiquidRewards, getRewardsBalance, etc.) and
91
+ * include these in the returned balance object, standardized across Ethereum/Polygon/etc.
92
+ *
93
+ * See: docs/balances-model.md for the intended balance model spec.
94
+ */
9
95
  export const getAbsoluteBalance = ({ asset, txLog }) => {
10
96
  assert(asset, 'asset is required')
11
97
  assert(txLog, 'txLog is required')
@@ -14,13 +100,26 @@ export const getAbsoluteBalance = ({ asset, txLog }) => {
14
100
  return asset.currency.ZERO
15
101
  }
16
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
+
17
111
  let balance = asset.currency.ZERO
18
112
  let hasAbsoluteBalance = false
19
113
 
20
- const reversedTxLog = txLog.reverse()
21
-
22
114
  for (const tx of reversedTxLog) {
23
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
+
24
123
  hasAbsoluteBalance = true
25
124
  balance = balance.add(asset.currency.baseUnit(tx.data.balanceChange.to))
26
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,
@@ -554,6 +553,7 @@ async function fetchRewardsInfo({ stakingApi, delegator, currency }) {
554
553
 
555
554
  return {
556
555
  rewardsBalance: rewardsBalance.add(lastRewards), // all time accrued rewards
556
+ liquidRewards: lastRewards, // current pending rewards (on-chain)
557
557
  minRewardsToWithdraw,
558
558
  withdrawable, // unclaimed rewards
559
559
  }
@@ -564,7 +564,7 @@ export async function getPolygonStakingInfo({ address, asset: { currency, baseAs
564
564
  const delegator = address.toLowerCase()
565
565
  const [
566
566
  delegatedBalance,
567
- { rewardsBalance, minRewardsToWithdraw, withdrawable },
567
+ { rewardsBalance, liquidRewards, minRewardsToWithdraw, withdrawable },
568
568
  { unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
569
569
  ] = await Promise.all([
570
570
  stakingApi.getTotalStake(delegator),
@@ -586,6 +586,7 @@ export async function getPolygonStakingInfo({ address, asset: { currency, baseAs
586
586
 
587
587
  return {
588
588
  rewardsBalance,
589
+ liquidRewards,
589
590
  withdrawable,
590
591
  unbondNonce,
591
592
  isDelegating,
@@ -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 }),