@exodus/ethereum-api 9.0.3 → 10.1.0

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,28 @@
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
+ ## [10.1.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.0.0...@exodus/ethereum-api@10.1.0) (2026-10-05)
7
+
8
+
9
+ ### Features
10
+
11
+ * **ethereum:** add evm staking api core with Everstake ETH and polygon adapters ([#8775](https://github.com/ExodusMovement/assets/issues/8775)) ([2d90106](https://github.com/ExodusMovement/assets/commit/2d901069989be82eb56aa253d5171e4f6af97356))
12
+
13
+
14
+
15
+ ## [10.0.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@9.0.3...@exodus/ethereum-api@10.0.0) (2026-10-05)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * **ethereum-api:** remove magnifier evm monitor (#8828)
21
+
22
+ ### Code Refactoring
23
+
24
+ * **ethereum-api:** remove magnifier evm monitor ([#8828](https://github.com/ExodusMovement/assets/issues/8828)) ([d179ae1](https://github.com/ExodusMovement/assets/commit/d179ae145410f2a8033718a1889a7ea6676001e1))
25
+
26
+
27
+
6
28
  ## [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
29
 
8
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "9.0.3",
3
+ "version": "10.1.0",
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": "e783e7a31c40807698ae2b9629e1c2dd5c03c228"
69
+ "gitHead": "f5c81fc2962e5d2836794149870fb9aaf094ddc9"
70
70
  }
@@ -15,7 +15,6 @@ import {
15
15
  } from './multicall3/index.js'
16
16
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
17
17
  import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
18
- import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
19
18
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
20
19
  import { getOptimisticTxLogEffects } from './tx-log/get-optimistic-txlog-effects.js'
21
20
  import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
@@ -177,7 +176,7 @@ export const createTransactionPrivacyResult = ({
177
176
  return { nonce, txId }
178
177
  }
179
178
 
180
- const sendPrivateBundle = async ({ unsignedTxBundle }) => {
179
+ const sendPrivateBundle = async ({ unsignedTxBundle, onBeforeSign }) => {
181
180
  assert(
182
181
  Array.isArray(unsignedTxBundle) && unsignedTxBundle.length > 0,
183
182
  'expected non-empty unsignedTxBundle'
@@ -189,11 +188,11 @@ export const createTransactionPrivacyResult = ({
189
188
  await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
190
189
  }
191
190
 
192
- const signTxResults = await Promise.all(
193
- unsignedTxBundle.map(({ asset, unsignedTx, walletAccount }) =>
194
- signTx({ assetClientInterface, asset, unsignedTx, walletAccount })
195
- )
196
- )
191
+ const signTxResults = []
192
+ for (const [index, { asset, unsignedTx, walletAccount }] of unsignedTxBundle.entries()) {
193
+ onBeforeSign?.({ index, total: unsignedTxBundle.length })
194
+ signTxResults.push(await signTx({ assetClientInterface, asset, unsignedTx, walletAccount }))
195
+ }
197
196
 
198
197
  const maybeBundleResult = await broadcastPrivateBundle({
199
198
  txs: signTxResults.map(({ rawTx }) => rawTx.toString('hex')),
@@ -355,15 +354,6 @@ export const createHistoryMonitorFactory = ({
355
354
  ...args,
356
355
  })
357
356
  break
358
- case 'magnifier':
359
- monitor = new EthereumMonitor({
360
- assetClientInterface,
361
- interval: ms(monitorInterval || '15s'),
362
- server,
363
- rpcBalanceAssetNames,
364
- ...args,
365
- })
366
- break
367
357
  default:
368
358
  throw new Error(`Monitor type ${monitorType} of evm asset ${assetName} is unknown`)
369
359
  }
@@ -49,6 +49,11 @@ import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/ind
49
49
  import { createSendValidations } from './send-validations.js'
50
50
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
51
51
  import { stakingApiFactory } from './staking/api/index.js'
52
+ import {
53
+ applyStakingApiOverrides,
54
+ composeStakingApi,
55
+ isStakingApiEnabled,
56
+ } from './staking/evm/compose-staking-api.js'
52
57
  import { createTxFactory } from './tx-create.js'
53
58
  import { txSendFactory } from './tx-send/index.js'
54
59
  import { createWeb3API } from './web3/index.js'
@@ -135,8 +140,14 @@ export const createAssetFactory = ({
135
140
  supportsTransactionPrivacy,
136
141
  transactionAssessment,
137
142
  multicall3Address,
143
+ stakingApiEnabled: stakingApiOverrides,
138
144
  } = configWithOverrides
139
145
 
146
+ const assetStakingDependencies = applyStakingApiOverrides({
147
+ stakingDependencies,
148
+ overrides: stakingApiOverrides,
149
+ })
150
+
140
151
  const asset = assets[base.name]
141
152
 
142
153
  const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
@@ -206,7 +217,7 @@ export const createAssetFactory = ({
206
217
  assetClientInterface,
207
218
  server,
208
219
  stakingConfiguration,
209
- stakingDependencies,
220
+ stakingDependencies: assetStakingDependencies,
210
221
  createRpcRequestAccumulator,
211
222
  },
212
223
  assets
@@ -236,6 +247,23 @@ export const createAssetFactory = ({
236
247
  supportsTransactionPrivacy,
237
248
  })
238
249
 
250
+ const stakingApiEnabled =
251
+ supportsStaking && isStakingApiEnabled(assetStakingDependencies[asset.name])
252
+
253
+ const createAssetStakingApi = () =>
254
+ composeStakingApi({
255
+ assetClientInterface,
256
+ stakingDependencies: assetStakingDependencies[asset.name],
257
+ legacyStakingApi: stakingApiFactory({
258
+ assetName: asset.name,
259
+ currency: asset.currency,
260
+ assetClientInterface,
261
+ server,
262
+ stakingConfiguration: stakingConfiguration[asset.name],
263
+ stakingDependencies: assetStakingDependencies[asset.name],
264
+ }),
265
+ })
266
+
239
267
  const features = {
240
268
  accountState: true,
241
269
  customTokens,
@@ -254,6 +282,7 @@ export const createAssetFactory = ({
254
282
  supportsCustomFees,
255
283
  supportsTransactionPrivacy: Boolean(supportsTransactionPrivacy),
256
284
  ...(supportsStaking && { staking: {} }),
285
+ ...(stakingApiEnabled && { stakingApi: true }),
257
286
  ...(delisted && { delisted }),
258
287
  }
259
288
 
@@ -393,15 +422,8 @@ export const createAssetFactory = ({
393
422
  signer ? signMessageWithSigner({ message, signer }) : signMessage({ privateKey, message }),
394
423
  ...(checkTx && { checkTx }),
395
424
  ...(supportsStaking &&
396
- stakingDependencies[asset.name] && {
397
- staking: stakingApiFactory({
398
- assetName: asset.name,
399
- currency: asset.currency,
400
- assetClientInterface,
401
- server,
402
- stakingConfiguration: stakingConfiguration[asset.name],
403
- stakingDependencies: stakingDependencies[asset.name],
404
- }),
425
+ assetStakingDependencies[asset.name] && {
426
+ staking: createAssetStakingApi(),
405
427
  }),
406
428
  validateAssetId: address.validate,
407
429
  web3,
@@ -3,6 +3,7 @@ import { createContract } from '@exodus/ethereum-lib'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
5
  import { stakingApiFactory } from './staking/api/index.js'
6
+ import { composeStakingApi, isStakingApiEnabled } from './staking/evm/compose-staking-api.js'
6
7
  import {
7
8
  isPolygonClaimUndelegate,
8
9
  isPolygonDelegate,
@@ -48,7 +49,13 @@ const getCreateBaseToken =
48
49
  }) =>
49
50
  ({ name, contract, features, ...tokenDef }) => {
50
51
  assert(getBalances, 'getBalances is required')
51
- const tokenSpecificFeatures = name === 'polygon' ? { staking: {} } : {}
52
+ const tokenSpecificFeatures =
53
+ name === 'polygon'
54
+ ? {
55
+ staking: {},
56
+ ...(isStakingApiEnabled(stakingDependencies[name]) && { stakingApi: true }),
57
+ }
58
+ : {}
52
59
 
53
60
  const tokenFeatures = { ...defaultTokenFeatures, ...features, ...tokenSpecificFeatures }
54
61
  const baseApi = {
@@ -72,14 +79,18 @@ const getCreateBaseToken =
72
79
  }
73
80
 
74
81
  if (name === 'polygon' && stakingDependencies[name]) {
75
- baseApi.staking = stakingApiFactory({
76
- assetName: tokenAsset.name,
77
- currency: tokenAsset.currency,
82
+ baseApi.staking = composeStakingApi({
78
83
  assetClientInterface,
79
- server,
80
- stakingConfiguration: stakingConfiguration[name],
81
84
  stakingDependencies: stakingDependencies[name],
82
- createRpcRequestAccumulator,
85
+ legacyStakingApi: stakingApiFactory({
86
+ assetName: tokenAsset.name,
87
+ currency: tokenAsset.currency,
88
+ assetClientInterface,
89
+ server,
90
+ stakingConfiguration: stakingConfiguration[name],
91
+ stakingDependencies: stakingDependencies[name],
92
+ createRpcRequestAccumulator,
93
+ }),
83
94
  })
84
95
  }
85
96
 
@@ -281,6 +281,13 @@ export function getEvmErrorReason(errorMessage) {
281
281
  return null
282
282
  }
283
283
 
284
+ export function getFetchGasLimitErrorReason(errorMessage) {
285
+ const reason = getEvmErrorReason(errorMessage)?.reason
286
+ return reason && reason !== EVM_ERROR_REASONS.executionReverted.reason
287
+ ? { reason, type: EVM_ERROR_TYPES.CONTRACT_CALL }
288
+ : EVM_ERROR_REASONS.fetchGasLimitFailed
289
+ }
290
+
284
291
  const MAX_HINT_LENGTH = 100
285
292
  // TODO: move this to be a generic error for all assets
286
293
  export class EthLikeError extends Error {
@@ -374,7 +381,8 @@ export const withErrorReason = async ({ promise, errorReasonInfo, hint, baseAsse
374
381
 
375
382
  throw new EthLikeError({
376
383
  message: err.message,
377
- errorReasonInfo,
384
+ errorReasonInfo:
385
+ typeof errorReasonInfo === 'function' ? errorReasonInfo(err.message) : errorReasonInfo,
378
386
  hint,
379
387
  traceId: err.traceId,
380
388
  baseAssetName,
@@ -4,7 +4,12 @@ import { safeString } from '@exodus/safe-string'
4
4
  import SolidityContract from '@exodus/solidity-contract'
5
5
  import assert from 'minimalistic-assert'
6
6
 
7
- import { EthLikeError, EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
7
+ import {
8
+ EthLikeError,
9
+ EVM_ERROR_REASONS,
10
+ getFetchGasLimitErrorReason,
11
+ withErrorReason,
12
+ } from './error-wrapper.js'
8
13
  import { getServer } from './exodus-eth-server/index.js'
9
14
  import { fromHexToString } from './number-utils.js'
10
15
 
@@ -60,7 +65,7 @@ export async function getNonce({ asset, address, tag = 'latest' }) {
60
65
  export async function estimateGas({ asset, ...args }) {
61
66
  return withErrorReason({
62
67
  promise: getServer(asset).estimateGas(args),
63
- errorReasonInfo: EVM_ERROR_REASONS.fetchGasLimitFailed,
68
+ errorReasonInfo: getFetchGasLimitErrorReason,
64
69
  hint: safeString`estimateGas`,
65
70
  baseAssetName: asset.baseAsset.name,
66
71
  })
@@ -1,6 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { create } from './api.js'
4
3
  import ApiCoinNodesServer from './api-coin-nodes.js'
5
4
  import ClarityServer from './clarity.js'
6
5
  import ClarityServerV2 from './clarity-v2.js'
@@ -12,7 +11,6 @@ export const ValidMonitorTypes = [
12
11
  'clarity-v2',
13
12
  'clarity-truncated-history',
14
13
  'clarity-v3',
15
- 'magnifier',
16
14
  ]
17
15
 
18
16
  export function createEvmServer({ assetName, serverUrl, monitorType }) {
@@ -30,8 +28,6 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
30
28
  case 'clarity-truncated-history':
31
29
  case 'clarity-v3':
32
30
  return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
33
- case 'magnifier':
34
- return create(serverUrl, assetName)
35
31
  default:
36
32
  throw new Error(`No server API found for ${assetName} and type ${monitorType}`)
37
33
  }
@@ -11,7 +11,7 @@ export const createGetBalanceForAddress = ({ asset, server }) => {
11
11
  }
12
12
 
13
13
  if (balance && typeof balance === 'object' && typeof balance.confirmed?.value === 'string') {
14
- // Magnifier/Clarity reponse
14
+ // Clarity response
15
15
  return asset.currency.baseUnit(balance.confirmed.value)
16
16
  }
17
17
 
package/src/index.js CHANGED
@@ -31,7 +31,6 @@ export {
31
31
  export { createEvmServer, getServer } from './exodus-eth-server/index.js'
32
32
 
33
33
  export {
34
- EthereumMonitor,
35
34
  EthereumNoHistoryMonitor,
36
35
  ClarityMonitor,
37
36
  getOptimisticTxLogEffects,
@@ -44,6 +43,20 @@ export { createEncodeMultisigContract } from './multisig-address.js'
44
43
  export { FantomStaking, stakingProviderClientFactory } from './staking/index.js'
45
44
  export { EthLikeRpcRequestAccumulator } from './multicall3/index.js'
46
45
  export { createCachedStakingRead } from './staking/cached-read.js'
46
+ export { createStakingTxs, sendStakingTxs, STAKING_TX_MODE } from './staking/evm/staking-txs.js'
47
+ export { createEvmStakingApi } from './staking/evm/create-evm-staking-api.js'
48
+ export {
49
+ createRejectionError as createStakingRejectionError,
50
+ createSubmitError as createStakingSubmitError,
51
+ fail as failStakingCheck,
52
+ pass as passStakingCheck,
53
+ sumOperationFees as sumStakingOperationFees,
54
+ warnIfBelow as warnIfStakingBalanceBelow,
55
+ } from './staking/evm/staking-errors.js'
56
+ export {
57
+ createEverstakeStakingAdapter,
58
+ ETH_STAKING_GAS_LIMITS,
59
+ } from './staking/ethereum/staking-adapter.js'
47
60
  export { ethereumStakingDeps } from './staking/ethereum/deps.js'
48
61
  export { getEthereumStakingInfo, createEthereumStakingService } from './staking/ethereum/service.js'
49
62
  export { EthereumStaking } from './staking/ethereum/api.js'
@@ -66,6 +79,7 @@ export {
66
79
  isPolygonReward,
67
80
  isPolygonClaimUndelegate,
68
81
  isPendingPolygonUndelegateTxInEthereumTxLog,
82
+ isPolygonUndelegateTxInEthereumTxLog,
69
83
  } from './staking/matic/matic-staking-utils.js'
70
84
 
71
85
  export { fetchTxPreview } from './simulate-tx/fetch-tx-preview.js'
package/src/nft-utils.js CHANGED
@@ -2,7 +2,7 @@ import { safeString } from '@exodus/safe-string'
2
2
  import SolidityContract from '@exodus/solidity-contract'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
- import { EthLikeError, EVM_ERROR_REASONS } from './error-wrapper.js'
5
+ import { EthLikeError, EVM_ERROR_REASONS, getFetchGasLimitErrorReason } from './error-wrapper.js'
6
6
  import { fetchGasLimit } from './gas-estimation.js'
7
7
 
8
8
  export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) => {
@@ -45,9 +45,13 @@ export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) =>
45
45
  }
46
46
  })
47
47
  ).catch((e) => {
48
+ const [first, ...rest] = e.errors.map((err) => getFetchGasLimitErrorReason(err.message))
48
49
  throw new EthLikeError({
49
50
  message: errors.join('\n'),
50
- errorReasonInfo: EVM_ERROR_REASONS.fetchGasLimitFailed,
51
+ errorReasonInfo:
52
+ first && rest.every(({ reason }) => reason === first.reason)
53
+ ? first
54
+ : EVM_ERROR_REASONS.fetchGasLimitFailed,
51
55
  hint: safeString`getNftArguments`,
52
56
  traceId: e.traceId,
53
57
  })
@@ -6,7 +6,7 @@ import { calculateEthLikeFeeMonitorUpdate } from './fee-utils.js'
6
6
  /**
7
7
  * Generic eth server based fee monitor.
8
8
  *
9
- * Compatible with all servers, clarity, magnifier and api-coin (they all implement the gasPrice method).
9
+ * Compatible with all servers, clarity and api-coin (they all implement the gasPrice method).
10
10
  *
11
11
  * Usage in the index files:
12
12
  *
@@ -36,8 +36,9 @@ export const stakingApiFactory = ({
36
36
 
37
37
  return {
38
38
  ...stakingService,
39
+ // @deprecated legacy staking api, remove once evm staking unification is done
39
40
  isStaking: async ({ isDelegating }) => isDelegating,
40
41
  isUnstaking: async ({ isUndelegateInProgress }) => isUndelegateInProgress,
41
- isUnstaked: async ({ canClaimUndelegateBalance }) => canClaimUndelegateBalance,
42
+ isUnstaked: async ({ canClaimUndelegatedBalance }) => canClaimUndelegatedBalance,
42
43
  }
43
44
  }
@@ -7,9 +7,8 @@ import assert from 'minimalistic-assert'
7
7
  import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
8
8
  import { EthLikeRpcRequestAccumulator } from '../../multicall3/index.js'
9
9
  import { createCachedStakingRead } from '../cached-read.js'
10
+ import { ETH_STAKING_MIN_AMOUNT } from './min-amount.js'
10
11
 
11
- // TODO: Shouldn't this be a function of `ethereumStakingState.minDelegateAmount`?
12
- const MIN_AMOUNT = '0.1'
13
12
  const RETRY_DELAYS = ['10s']
14
13
 
15
14
  const EVERSTAKE_API_URL = 'https://eth-clarity.a.exodus.io/everstake-rewards'
@@ -46,7 +45,7 @@ export class EthereumStaking {
46
45
  return txInputHex.toLowerCase().startsWith(EthereumStaking.METHODS_IDS.DELEGATE)
47
46
  }
48
47
 
49
- constructor(assetName, currency, minAmount = MIN_AMOUNT, server) {
48
+ constructor(assetName, currency, minAmount = ETH_STAKING_MIN_AMOUNT, server) {
50
49
  this.asset = { name: assetName, currency }
51
50
  const accountingAddress =
52
51
  EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
@@ -1,5 +1,6 @@
1
1
  import { EthereumStaking } from './api.js'
2
2
  import { createEthereumStakingService, getEthereumStakingInfo } from './service.js'
3
+ import { createEverstakeStakingAdapter } from './staking-adapter.js'
3
4
 
4
5
  /** Staking server factory for ethereum (used by stakingApiFactory). */
5
6
  export const stakingServerFactory = ({ assetName, currency, server, minAmount }) =>
@@ -24,6 +25,7 @@ export const stakingServiceFactory = ({ assetClientInterface, server, stakingSer
24
25
  useQueueCache,
25
26
  background,
26
27
  }),
28
+ // @deprecated legacy staking api, remove once evm staking unification is done
27
29
  stake: service.delegate,
28
30
  unstake: service.undelegate,
29
31
  claimUnstaked: service.claimUndelegatedBalance,
@@ -34,4 +36,5 @@ export const stakingServiceFactory = ({ assetClientInterface, server, stakingSer
34
36
  export const ethereumStakingDeps = {
35
37
  stakingServerFactory,
36
38
  stakingServiceFactory,
39
+ createStakingAdapter: ({ service }) => createEverstakeStakingAdapter({ service }),
37
40
  }
@@ -0,0 +1 @@
1
+ export const ETH_STAKING_MIN_AMOUNT = '0.1'