@exodus/ethereum-plugin 2.35.0 → 2.35.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,41 @@
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
+ ## [2.35.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.35.1...@exodus/ethereum-plugin@2.35.2) (2026-09-28)
7
+
8
+ **Note:** Version bump only for package @exodus/ethereum-plugin
9
+
10
+
11
+
12
+
13
+
14
+ ## [2.35.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.33.0...@exodus/ethereum-plugin@2.35.1) (2026-09-09)
15
+
16
+
17
+ ### Features
18
+
19
+ * source eip7702 delegation whitelist from remote config ([#8356](https://github.com/ExodusMovement/assets/issues/8356)) ([1a0966b](https://github.com/ExodusMovement/assets/commit/1a0966b08befb6e9310993f323c21d003dea2754))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * disable @exodus/hydra/no-param-reassign-prop-only warnings ([#8531](https://github.com/ExodusMovement/assets/issues/8531)) ([ddae556](https://github.com/ExodusMovement/assets/commit/ddae556361285919fe82cd10a81cfeae79ccc6b5))
25
+ * **ethereum-api:** preserve live pending txs across refresh to prevent nonce reuse ([#8430](https://github.com/ExodusMovement/assets/issues/8430)) ([a24ad82](https://github.com/ExodusMovement/assets/commit/a24ad82d303ce50606143bc5d1d917fbbad45ef1))
26
+ * **integration-tests:** solana safe report, foundry setup, anvil 1.8 fork-test races ([#8574](https://github.com/ExodusMovement/assets/issues/8574)) ([a8268b8](https://github.com/ExodusMovement/assets/commit/a8268b875ec85dd7818e3b8e5795aad02d1bcaaa))
27
+ * **lint:** add braces for multi-line if statements ([#8515](https://github.com/ExodusMovement/assets/issues/8515)) ([3891645](https://github.com/ExodusMovement/assets/commit/38916457ad0651163bd37843fc12d16656e214df))
28
+ * **lint:** fix easy auto-fixable oxlint warnings ([#8491](https://github.com/ExodusMovement/assets/issues/8491)) ([23407c5](https://github.com/ExodusMovement/assets/commit/23407c56aabd9f6968c5477306910f6702d59c39))
29
+ * **lint:** fix no-await-expression-member warnings and upgrade to error ([#8503](https://github.com/ExodusMovement/assets/issues/8503)) ([a7fb2c3](https://github.com/ExodusMovement/assets/commit/a7fb2c3bcb4e6b9e97e7f8bb3b8434b78036fe68))
30
+ * **lint:** fix no-throw-literal warnings and upgrade to error ([#8498](https://github.com/ExodusMovement/assets/issues/8498)) ([8fc68eb](https://github.com/ExodusMovement/assets/commit/8fc68eb65682e0b5a7da9415410ee1dedf309696))
31
+ * lower ETH min staking amount ([#8319](https://github.com/ExodusMovement/assets/issues/8319)) ([461e4c4](https://github.com/ExodusMovement/assets/commit/461e4c4f46e58e3750363edb9d0b6cddbfae5478))
32
+ * missing liquidRewards prop for polygon staking ([#8382](https://github.com/ExodusMovement/assets/issues/8382)) ([7d7d8ba](https://github.com/ExodusMovement/assets/commit/7d7d8ba62570fc3bf8ea9c58d514e0fc4dc70697))
33
+
34
+
35
+ ### Performance Improvements
36
+
37
+ * **ethereum:** reduce background staking request volume ([#8617](https://github.com/ExodusMovement/assets/issues/8617)) ([a9746d9](https://github.com/ExodusMovement/assets/commit/a9746d98ee0a2a04f5fd0131d3f1228cbc0aacf2))
38
+
39
+
40
+
6
41
  ## [2.35.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.33.0...@exodus/ethereum-plugin@2.35.0) (2026-08-04)
7
42
 
8
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-plugin",
3
- "version": "2.35.0",
3
+ "version": "2.35.2",
4
4
  "description": "Ethereum plugin for Exodus SDK powered wallets",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -16,15 +16,13 @@
16
16
  "provenance": false
17
17
  },
18
18
  "scripts": {
19
- "test": "run -T exodus-test --jest",
20
- "lint": "run -T eslintc .",
21
- "lint:fix": "yarn lint --fix"
19
+ "test": "exodus-test --jest"
22
20
  },
23
21
  "dependencies": {
24
22
  "@exodus/asset-lib": "^5.9.2",
25
23
  "@exodus/basic-utils": "^3.0.1",
26
24
  "@exodus/currency": "^6.0.1",
27
- "@exodus/ethereum-api": "^8.77.2",
25
+ "@exodus/ethereum-api": "^9.0.0",
28
26
  "@exodus/ethereum-lib": "^5.25.0",
29
27
  "@exodus/ethereum-meta": "^2.9.0",
30
28
  "@exodus/ethereumjs": "^1.0.0",
@@ -38,10 +36,10 @@
38
36
  "@exodus/assets": "^11.4.0",
39
37
  "@exodus/assets-testing": "^1.0.0",
40
38
  "@exodus/crypto": "^1.0.0-rc.13",
41
- "@exodus/evm-fork-testing": "^0.7.1",
39
+ "@exodus/evm-fork-testing": "^0.7.2",
42
40
  "@exodus/models": "^13.0.0",
43
41
  "@exodus/solidity-contract": "^1.3.0",
44
- "@exodus/web3-ethereum-utils": "^4.7.6",
42
+ "@exodus/web3-ethereum-utils": "^5.0.1",
45
43
  "delay": "^4.0.1",
46
44
  "ms": "^2.1.1"
47
45
  },
@@ -53,5 +51,5 @@
53
51
  "type": "git",
54
52
  "url": "git+https://github.com/ExodusMovement/assets.git"
55
53
  },
56
- "gitHead": "e878130c84c596ee86050984806b0da491e14910"
54
+ "gitHead": "92d6cdde313e5244fb05a99b1be964a96f989c3b"
57
55
  }
@@ -1,4 +1,6 @@
1
1
  import {
2
+ createCachedStakingRead,
3
+ EthLikeRpcRequestAccumulator,
2
4
  EVM_ERROR_REASONS,
3
5
  fromHexToBN,
4
6
  fromHexToString,
@@ -16,7 +18,15 @@ import { mainnetContracts } from './contracts/index.js'
16
18
  const RETRY_DELAYS = ['10s']
17
19
 
18
20
  class StakingServer {
19
- constructor({ asset, server, contracts = mainnetContracts }) {
21
+ constructor({ asset, server, contracts = mainnetContracts, createRpcRequestAccumulator }) {
22
+ // Only the reviewed mainnet getters are caller-independent. Custom contracts
23
+ // retain isolated JSON-RPC execution rather than inheriting that assumption.
24
+ this.createRpcRequestAccumulator =
25
+ Object.entries(mainnetContracts).every(
26
+ ([key, address]) => contracts[key]?.toLowerCase() === address
27
+ ) && createRpcRequestAccumulator
28
+ ? createRpcRequestAccumulator
29
+ : () => new EthLikeRpcRequestAccumulator()
20
30
  this.asset = asset
21
31
  // harcoded exchange rate from the validator share contract
22
32
  // in order to calculate claim unstake amount off-chain
@@ -54,6 +64,51 @@ class StakingServer {
54
64
  })
55
65
  }
56
66
 
67
+ // Reviewed ValidatorShare getters use explicit user arguments, public storage
68
+ // getters and caller-independent exchange-rate/reward calculations. Unlike
69
+ // transaction simulation, none requires the original eth_call msg.sender.
70
+ // aggregate is all-or-nothing: a reverted/malformed read must reject the state.
71
+ getStakingState = async (address) => {
72
+ const read = () => {
73
+ const requests = this.createRpcRequestAccumulator()
74
+ for (const [contract, method, ...args] of [
75
+ [this.validatorShareContract, 'getTotalStake', address],
76
+ [this.validatorShareContract, 'minAmount'],
77
+ [this.validatorShareContract, 'getLiquidRewards', address],
78
+ [this.validatorShareContract, 'unbondNonces', address],
79
+ [this.stakingManagerContract, 'withdrawalDelay'],
80
+ [this.stakingManagerContract, 'epoch'],
81
+ [this.validatorShareContract, 'withdrawExchangeRate'],
82
+ ]) {
83
+ requests.contractStaticCallRequest(contract, method, ...args)
84
+ }
85
+ return requests.flush(this.server)
86
+ }
87
+ const [[stake], [minimum], [liquid], [nonce], [delay], [epoch], [rate]] = await withErrorReason(
88
+ {
89
+ promise: retry(read, { delayTimesMs: RETRY_DELAYS })(),
90
+ errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
91
+ hint: safeString`stakingPolygonReadFunctionContract`,
92
+ baseAssetName: this.server.baseAssetName,
93
+ }
94
+ )
95
+ const unbondNonce = Number(nonce)
96
+ if (!Number.isSafeInteger(unbondNonce)) throw new Error('Invalid Polygon unbond nonce')
97
+ return {
98
+ delegatedBalance: this.asset.currency.baseUnit(stake),
99
+ minRewardsToWithdraw: this.asset.currency.baseUnit(minimum),
100
+ liquidRewards: this.asset.currency.baseUnit(liquid),
101
+ unbondNonce,
102
+ withdrawalDelay: new BN(delay),
103
+ currentEpoch: new BN(epoch),
104
+ withdrawExchangeRate: new BN(rate),
105
+ }
106
+ }
107
+
108
+ getCachedTotalRewards = createCachedStakingRead({
109
+ read: (address) => this.getTotalRewards(address, { throwOnError: true }),
110
+ })
111
+
57
112
  getWithdrawalDelay = async () => {
58
113
  const withdrawalDelay = await this.#callReadFunctionContract(
59
114
  this.stakingManagerContract,
@@ -127,7 +182,7 @@ class StakingServer {
127
182
  return this.asset.currency.baseUnit(liquidRewards)
128
183
  }
129
184
 
130
- getTotalRewards = async (address) => {
185
+ getTotalRewards = async (address, { throwOnError = false } = {}) => {
131
186
  // use external everstake API to get total rewards
132
187
  const EVERSTAKE_API_URL = 'https://polygon-clarity.a.exodus.io/everstake-rewards'
133
188
  const url = new URL(EVERSTAKE_API_URL)
@@ -142,11 +197,22 @@ class StakingServer {
142
197
 
143
198
  try {
144
199
  const response = await fetch(url.toString(), options)
200
+ if (!response.ok) throw new Error('Failed to fetch Polygon total rewards')
145
201
  const res = await response.json()
146
- if (res === 'address' || !res?.rewards) return this.asset.currency.ZERO // address not found
147
- return this.asset.currency.baseUnit(res.rewards)
202
+ if (res === 'address') return this.asset.currency.ZERO // address not found
203
+ const rewards = res?.rewards
204
+ if (
205
+ !(
206
+ (typeof rewards === 'string' && /^\d+$/.test(rewards)) ||
207
+ (Number.isSafeInteger(rewards) && rewards >= 0)
208
+ )
209
+ ) {
210
+ throw new Error('Malformed Polygon total rewards')
211
+ }
212
+ return this.asset.currency.baseUnit(rewards)
148
213
  } catch (error) {
149
214
  console.warn('Error fetching Polygon total rewards:', error)
215
+ if (throwOnError) throw error
150
216
  return this.asset.currency.ZERO
151
217
  }
152
218
  }
@@ -218,5 +284,16 @@ class StakingServer {
218
284
  }
219
285
  }
220
286
 
221
- export const stakingServerFactory = ({ assetName, currency, contracts, server }) =>
222
- new StakingServer({ asset: { name: assetName, currency }, contracts, server })
287
+ export const stakingServerFactory = ({
288
+ assetName,
289
+ currency,
290
+ contracts,
291
+ server,
292
+ createRpcRequestAccumulator,
293
+ }) =>
294
+ new StakingServer({
295
+ asset: { name: assetName, currency },
296
+ contracts,
297
+ server,
298
+ createRpcRequestAccumulator,
299
+ })
@@ -1,6 +1,8 @@
1
+ import { EVERSTAKE_VALIDATOR_CONTRACT_ADDR, STAKING_MANAGER_ADDR } from '@exodus/ethereum-api'
2
+
1
3
  export const mainnetContracts = {
2
- VALIDATOR_SHARES_CONTRACT_ADDR: '0xf30cf4ed712d3734161fdaab5b1dbb49fd2d0e5c',
3
- STAKING_MANAGER_ADDR: '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908',
4
+ VALIDATOR_SHARES_CONTRACT_ADDR: EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
5
+ STAKING_MANAGER_ADDR,
4
6
  TOKEN_CONTRACT: '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0',
5
7
  }
6
8
 
@@ -17,6 +17,12 @@ export const polygonStakingConfig = {
17
17
  claimRewards: true,
18
18
  },
19
19
  contracts: mainnetContracts,
20
+ // The token contract is deliberately excluded: plain POL transfers are not
21
+ // staking activity.
22
+ activityContracts: [
23
+ mainnetContracts.VALIDATOR_SHARES_CONTRACT_ADDR,
24
+ mainnetContracts.STAKING_MANAGER_ADDR,
25
+ ],
20
26
  methodIds,
21
27
  }
22
28
 
@@ -1,5 +1,4 @@
1
1
  import { memoize } from '@exodus/basic-utils'
2
- // eslint-disable-next-line @exodus/import/no-deprecated
3
2
  import NumberUnit, { isNumberUnit } from '@exodus/currency'
4
3
  import {
5
4
  estimateGasLimit,
@@ -42,7 +41,6 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
42
41
  const assetName = 'ethereum'
43
42
 
44
43
  function amountToCurrency({ asset, amount }) {
45
- // eslint-disable-next-line @exodus/import/no-deprecated
46
44
  return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
47
45
  }
48
46
 
@@ -589,22 +587,6 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
589
587
  }
590
588
  }
591
589
 
592
- async function fetchUnstakedClaimInfo({ stakingServer, delegator }) {
593
- const [unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate] = await Promise.all([
594
- stakingServer.getCurrentUnbondNonce(delegator),
595
- stakingServer.getWithdrawalDelay(),
596
- stakingServer.getCurrentCheckpoint(),
597
- stakingServer.getWithdrawExchangeRate(),
598
- ])
599
-
600
- return {
601
- unbondNonce,
602
- withdrawalDelay,
603
- currentEpoch,
604
- withdrawExchangeRate,
605
- }
606
- }
607
-
608
590
  function calculateUnclaimedTokens({
609
591
  currency,
610
592
  exchangeRatePrecision,
@@ -665,40 +647,34 @@ async function getUnstakedUnclaimedInfo({
665
647
  }
666
648
  }
667
649
 
668
- async function fetchRewardsInfo({ stakingServer, delegator, currency }) {
669
- const [minRewardsToWithdraw, lastRewards, rewardsBalance] = await Promise.all([
670
- stakingServer.getMinRewardsToWithdraw(),
671
- stakingServer.getLiquidRewards(delegator),
672
- stakingServer.getTotalRewards(delegator),
673
- ])
674
- const withdrawable = lastRewards.sub(minRewardsToWithdraw).gte(currency.ZERO)
675
- ? lastRewards
676
- : currency.ZERO
677
-
678
- return {
679
- rewardsBalance: rewardsBalance.add(lastRewards), // all time accrued rewards
680
- liquidRewards: lastRewards, // current pending rewards (on-chain)
681
- minRewardsToWithdraw,
682
- withdrawable, // unclaimed rewards
683
- }
684
- }
685
-
686
650
  export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
687
- return async function ({ address }) {
651
+ return async function ({ address, useCache = false, background = false }) {
688
652
  const { polygon: asset } = await assetClientInterface.getAssetsForNetwork({
689
653
  baseAssetName: 'ethereum', // Polygon token lives in ETH network
690
654
  })
691
655
  const { currency } = asset
692
656
  const delegator = address.toLowerCase()
693
657
  const [
694
- delegatedBalance,
695
- { rewardsBalance, liquidRewards, minRewardsToWithdraw, withdrawable },
696
- { unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
658
+ {
659
+ delegatedBalance,
660
+ liquidRewards,
661
+ minRewardsToWithdraw,
662
+ unbondNonce,
663
+ withdrawalDelay,
664
+ currentEpoch,
665
+ withdrawExchangeRate,
666
+ },
667
+ totalRewards,
697
668
  ] = await Promise.all([
698
- stakingServer.getTotalStake(delegator),
699
- fetchRewardsInfo({ stakingServer, delegator, currency }),
700
- fetchUnstakedClaimInfo({ stakingServer, delegator }),
669
+ stakingServer.getStakingState(delegator),
670
+ // Preserve rewards metadata on provider failure without blocking discovery
671
+ // of real on-chain stake during restore.
672
+ stakingServer
673
+ .getCachedTotalRewards(delegator, { useCache })
674
+ .catch(() => (background ? null : currency.ZERO)),
701
675
  ])
676
+ const rewardsBalance = totalRewards?.add(liquidRewards)
677
+ const withdrawable = liquidRewards.gte(minRewardsToWithdraw) ? liquidRewards : currency.ZERO
702
678
  const minDelegateAmount = currency.defaultUnit(1)
703
679
  const isDelegating = !delegatedBalance.isZero
704
680
 
@@ -713,7 +689,7 @@ export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
713
689
  })
714
690
 
715
691
  return {
716
- rewardsBalance,
692
+ ...(rewardsBalance !== undefined && { rewardsBalance }),
717
693
  liquidRewards,
718
694
  withdrawable,
719
695
  unbondNonce,
@@ -177,7 +177,7 @@ const calculateRewardsFromStakeTxFactory =
177
177
 
178
178
  if (stakeTxContainsReward) {
179
179
  const txAmount = stakedAmount.sub(tx.coinAmount.abs()).abs()
180
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only -- TODO: Fix this the next time the file is edited.
180
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only -- TODO: Fix this the next time the file is edited.
181
181
  return txAmount.toDefaultString()
182
182
  }
183
183
  }
package/src/staking.js CHANGED
@@ -1,11 +1,14 @@
1
- import { ethereumStakingDeps } from '@exodus/ethereum-api'
1
+ import { EthereumStaking, ethereumStakingDeps } from '@exodus/ethereum-api'
2
2
  import { ethStakeAccountState } from '@exodus/ethereum-lib'
3
3
  import { asset } from '@exodus/ethereum-meta'
4
4
 
5
5
  import { polygonStakingConfig, polygonStakingDeps } from './staking/polygon/index.js'
6
6
 
7
7
  export const stakingConfiguration = {
8
- ethereum: { accountStateExtraData: ethStakeAccountState(asset) },
8
+ ethereum: {
9
+ accountStateExtraData: ethStakeAccountState(asset),
10
+ activityContracts: Object.values(EthereumStaking.addresses.ethereum),
11
+ },
9
12
  polygon: polygonStakingConfig,
10
13
  }
11
14