@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.
|
|
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": "
|
|
70
|
+
"gitHead": "be4898d14d842c79dfcc327501c390dfaf56157b"
|
|
71
71
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -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
|
|
|
@@ -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: {
|
|
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 }),
|