@exodus/ethereum-api 8.64.7 → 8.65.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,18 @@
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.65.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.5...@exodus/ethereum-api@8.65.0) (2026-03-03)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: encode per-network support flag, fix stale delegation state, eip7702 whitelist (#7477)
13
+
14
+ * feat: gas price bump for evm (#7500)
15
+
16
+
17
+
6
18
  ## [8.64.7](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.5...@exodus/ethereum-api@8.64.7) (2026-02-24)
7
19
 
8
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.64.7",
3
+ "version": "8.65.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",
@@ -68,5 +68,5 @@
68
68
  "type": "git",
69
69
  "url": "git+https://github.com/ExodusMovement/assets.git"
70
70
  },
71
- "gitHead": "7d25477c075c7cef416ffe2c7e6e4c99e2928bdc"
71
+ "gitHead": "98c4dc4e2cd52781dadc2af4fae2b80ecc276626"
72
72
  }
@@ -141,6 +141,7 @@ export const createHistoryMonitorFactory = ({
141
141
  stakingAssetNames,
142
142
  rpcBalanceAssetNames,
143
143
  wsGatewayUri,
144
+ eip7702Supported,
144
145
  }) => {
145
146
  assert(assetName, 'expected assetName')
146
147
  assert(assetClientInterface, 'expected assetClientInterface')
@@ -158,6 +159,7 @@ export const createHistoryMonitorFactory = ({
158
159
  interval: ms(monitorInterval || '5m'),
159
160
  server,
160
161
  rpcBalanceAssetNames,
162
+ eip7702Supported,
161
163
  ...args,
162
164
  })
163
165
  break
@@ -168,6 +170,7 @@ export const createHistoryMonitorFactory = ({
168
170
  server,
169
171
  rpcBalanceAssetNames,
170
172
  wsGatewayClient: createWsGateway({ uri: wsGatewayUri }),
173
+ eip7702Supported,
171
174
  ...args,
172
175
  })
173
176
  break
@@ -176,6 +179,7 @@ export const createHistoryMonitorFactory = ({
176
179
  assetClientInterface,
177
180
  interval: ms(monitorInterval || '15s'),
178
181
  server,
182
+ eip7702Supported,
179
183
  ...args,
180
184
  })
181
185
  break
@@ -73,6 +73,7 @@ export const createAssetFactory = ({
73
73
  delisted = false,
74
74
  privacyRpcUrl: defaultPrivacyRpcUrl,
75
75
  wsGatewayUri: defaultWsGatewayUri,
76
+ eip7702Supported,
76
77
  }) => {
77
78
  assert(assetsList, 'assetsList is required')
78
79
  assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
@@ -233,6 +234,7 @@ export const createAssetFactory = ({
233
234
  stakingAssetNames,
234
235
  rpcBalanceAssetNames,
235
236
  wsGatewayUri,
237
+ eip7702Supported,
236
238
  })
237
239
 
238
240
  const defaultAddressPath = 'm/0/0'
@@ -326,6 +328,7 @@ export const createAssetFactory = ({
326
328
  broadcastPrivateBundle,
327
329
  broadcastPrivateTx,
328
330
  forceGasLimitEstimation,
331
+ eip7702Supported,
329
332
  getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
330
333
  getNonce,
331
334
  privacyServer,
package/src/fee-utils.js CHANGED
@@ -1,34 +1,40 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- export const applyMultiplierToPrice = ({ feeAsset, gasPriceMultiplier, price }) => {
4
- assert(typeof price === 'string', 'price should be a string')
5
- return feeAsset.currency
6
- .parse(price)
7
- .mul(gasPriceMultiplier || 1)
8
- .toBaseString({ unit: true })
9
- }
10
-
11
3
  /**
12
4
  * Returns augmented `feeConfig` values manipulated using the `feeData`,
13
5
  * for example, like multiplying the `gasPrice`. This helps us to apply
14
6
  * modifications to values before presenting them to the consumer.
15
- *
16
- * TODO: We shouldn't actually do this, lol. The closer we are to
17
- * the node, the better!
18
7
  */
19
- export const rewriteFeeConfig = ({ feeAsset, feeConfig, gasPriceMultiplier }) => {
8
+ export const refineFeeConfig = ({ feeAsset, feeConfig, gasPriceBump, gasPriceMultiplier }) => {
9
+ const fromString = (str) => {
10
+ assert(typeof str === 'string', 'expected string')
11
+ return feeAsset.currency.parse(str)
12
+ }
13
+
20
14
  try {
21
- const { gasPrice, baseFeePerGas, ...extras } = feeConfig
15
+ const { gasPrice, baseFeePerGas, tipGasPrice, ...extras } = feeConfig
16
+
17
+ gasPriceMultiplier ||= 1
18
+ gasPriceBump ||= feeAsset.currency.ZERO
19
+
20
+ if (!baseFeePerGas) {
21
+ const nextGasPrice = fromString(gasPrice).mul(gasPriceMultiplier).add(gasPriceBump)
22
+ return {
23
+ ...extras,
24
+ gasPrice: nextGasPrice.toBaseString({ unit: true }),
25
+ }
26
+ }
27
+
28
+ const nextBaseFeePerGas = fromString(baseFeePerGas).mul(gasPriceMultiplier)
29
+ const nextTipGasPrice = tipGasPrice
30
+ ? fromString(tipGasPrice).mul(gasPriceMultiplier).add(gasPriceBump)
31
+ : gasPriceBump
32
+
22
33
  return {
23
34
  ...extras,
24
- gasPrice: applyMultiplierToPrice({
25
- feeAsset,
26
- gasPriceMultiplier,
27
- price: gasPrice,
28
- }) /* required */,
29
- baseFeePerGas: baseFeePerGas
30
- ? applyMultiplierToPrice({ feeAsset, gasPriceMultiplier, price: baseFeePerGas })
31
- : undefined,
35
+ baseFeePerGas: nextBaseFeePerGas.toBaseString({ unit: true }),
36
+ tipGasPrice: nextTipGasPrice.toBaseString({ unit: true }),
37
+ gasPrice: nextBaseFeePerGas.add(nextTipGasPrice).toBaseString({ unit: true }),
32
38
  }
33
39
  } catch (e) {
34
40
  console.error(
@@ -148,9 +154,10 @@ export const calculateEthLikeFeeMonitorUpdate = async ({
148
154
 
149
155
  // Depending on whether `eip1559Enabled` on the client, we
150
156
  // can choose to return different properties.
151
- const { eip1559Enabled, gasPriceMultiplier } = await assetClientInterface.getFeeConfig({
152
- assetName: feeAsset.name,
153
- })
157
+ const { eip1559Enabled, gasPriceBump, gasPriceMultiplier } =
158
+ await assetClientInterface.getFeeConfig({
159
+ assetName: feeAsset.name,
160
+ })
154
161
 
155
162
  // NOTE: The `gasPrice` is a required value for all EVM networks.
156
163
  const gasPrice = ensureBigInt(fetchedGasPrices.gasPrice)
@@ -159,12 +166,7 @@ export const calculateEthLikeFeeMonitorUpdate = async ({
159
166
  ? calculateEthLikeFeeMonitorUpdateEip1559({ gasPrice, feeAsset, fetchedGasPrices })
160
167
  : calculateEthLikeFeeMonitorUpdateNonEip1559({ gasPrice }))
161
168
 
162
- // HACK: We use `rewriteFeeConfig` to apply some additional padding
163
- // to our `gasPrice` and `baseFeePerGas` values. Once we're
164
- // feeling confident, we should be safe to skip doing this for
165
- // `eip1559Enabled` chains, since we should be able to guarantee
166
- // next block inclusion with accuracy.
167
- return rewriteFeeConfig({ feeAsset, feeConfig, gasPriceMultiplier })
169
+ return refineFeeConfig({ feeAsset, feeConfig, gasPriceBump, gasPriceMultiplier })
168
170
  }
169
171
 
170
172
  /**
@@ -28,7 +28,14 @@ export class ClarityMonitorV2 extends BaseMonitor {
28
28
  #walletAccountByAddress = new Map()
29
29
  #walletAccountInfo = new Map()
30
30
  #rpcBalanceAssetNames = []
31
- constructor({ server, wsGatewayClient, rpcBalanceAssetNames, config, ...args } = {}) {
31
+ constructor({
32
+ server,
33
+ wsGatewayClient,
34
+ rpcBalanceAssetNames,
35
+ eip7702Supported,
36
+ config,
37
+ ...args
38
+ } = {}) {
32
39
  super(args)
33
40
  assert(wsGatewayClient instanceof WsGateway, 'expected WsGateway wsGatewayClient')
34
41
 
@@ -36,6 +43,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
36
43
  this.server = server
37
44
  this.#wsClient = wsGatewayClient
38
45
  this.#rpcBalanceAssetNames = rpcBalanceAssetNames
46
+ this.eip7702Supported = eip7702Supported
39
47
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
40
48
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
41
49
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -227,6 +235,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
227
235
  const eip7702Delegation = await getCurrentEIP7702Delegation({
228
236
  server: this.server,
229
237
  address: derivedData.ourWalletAddress,
238
+ eip7702Supported: this.eip7702Supported,
230
239
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
231
240
  logger: this.logger,
232
241
  })
@@ -23,11 +23,12 @@ import {
23
23
  const { isEmpty } = lodash
24
24
 
25
25
  export class ClarityMonitor extends BaseMonitor {
26
- constructor({ server, config, rpcBalanceAssetNames, ...args }) {
26
+ constructor({ server, config, rpcBalanceAssetNames, eip7702Supported, ...args }) {
27
27
  super(args)
28
28
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
29
29
  this.server = server
30
30
  this.rpcBalanceAssetNames = rpcBalanceAssetNames
31
+ this.eip7702Supported = eip7702Supported
31
32
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
32
33
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
33
34
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -191,6 +192,7 @@ export class ClarityMonitor extends BaseMonitor {
191
192
  const eip7702Delegation = await getCurrentEIP7702Delegation({
192
193
  server: this.server,
193
194
  address: derivedData.ourWalletAddress,
195
+ eip7702Supported: this.eip7702Supported,
194
196
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
195
197
  logger: this.logger,
196
198
  })
@@ -17,10 +17,11 @@ const { isEmpty, unionBy, zipObject } = lodash
17
17
  // The base ethereum monitor no history class handles listening for assets with no history
18
18
 
19
19
  export class EthereumNoHistoryMonitor extends BaseMonitor {
20
- constructor({ server, config, ...args }) {
20
+ constructor({ server, config, eip7702Supported, ...args }) {
21
21
  super(args)
22
22
  this.server = server
23
23
  this.config = { ...config }
24
+ this.eip7702Supported = eip7702Supported
24
25
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
25
26
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
26
27
  getTxLog: (...args) => this.aci.getTxLog(...args),
@@ -190,6 +191,7 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
190
191
  const eip7702Delegation = await getCurrentEIP7702Delegation({
191
192
  server: this.server,
192
193
  address: ourWalletAddress,
194
+ eip7702Supported: this.eip7702Supported,
193
195
  currentDelegation: currentAccountState?.eip7702Delegation,
194
196
  logger: this.logger,
195
197
  })
@@ -1,38 +1,68 @@
1
1
  import { getEIP7702Delegation } from '../../eth-like-util.js'
2
2
 
3
+ const NOT_DELEGATED = { isDelegated: false, delegatedAddress: null, isWhitelisted: null }
4
+
3
5
  /**
4
6
  * Checks if the address has an EIP-7702 delegation and returns the delegation state.
5
7
  * Returns the new state if changed, or the current state if unchanged.
6
- * On error, returns the current state to preserve existing data.
8
+ * On error, conservatively returns { isDelegated: false } to avoid showing stale delegation
9
+ * state that may no longer be accurate.
7
10
  *
8
11
  * @param {Object} params
9
12
  * @param {Object} params.server - The server instance to use for getCode
10
13
  * @param {string} params.address - The wallet address to check
14
+ * @param {Array<{address: string, name: string}>} [params.eip7702Supported] - Whitelist of trusted delegation targets.
15
+ * If not an array, the check is skipped entirely (chain does not support EIP-7702).
16
+ * An empty array means the check runs but every delegation will be isWhitelisted: false.
11
17
  * @param {Object} [params.currentDelegation] - The current delegation state from accountState
12
18
  * @param {Object} [params.logger] - Optional logger for warnings
13
19
  * @returns {Promise<Object|undefined>} The delegation state to use
14
20
  */
15
- export async function getCurrentEIP7702Delegation({ server, address, currentDelegation, logger }) {
21
+ export async function getCurrentEIP7702Delegation({
22
+ server,
23
+ address,
24
+ eip7702Supported,
25
+ currentDelegation,
26
+ logger,
27
+ }) {
28
+ // Non-array (undefined, false, etc.) → chain doesn't support EIP-7702, skip entirely
29
+ if (!Array.isArray(eip7702Supported)) return NOT_DELEGATED
30
+
16
31
  try {
17
32
  const result = await getEIP7702Delegation({ address, server })
18
33
 
19
- // Return new state if changed
34
+ if (!result.isDelegated) return NOT_DELEGATED
35
+
36
+ // [] → check runs but nothing is trusted; populated array → whitelist check
37
+ const isWhitelisted = eip7702Supported.some(
38
+ ({ address }) => address.toLowerCase() === result.delegatedAddress.toLowerCase()
39
+ )
40
+
41
+ const newDelegation = {
42
+ isDelegated: true,
43
+ delegatedAddress: result.delegatedAddress,
44
+ isWhitelisted,
45
+ }
46
+
47
+ // Return new state only if something changed
20
48
  if (
21
- currentDelegation?.isDelegated !== result.isDelegated ||
22
- currentDelegation?.delegatedAddress !== result.delegatedAddress
49
+ currentDelegation?.isDelegated !== newDelegation.isDelegated ||
50
+ currentDelegation?.delegatedAddress !== newDelegation.delegatedAddress ||
51
+ currentDelegation?.isWhitelisted !== newDelegation.isWhitelisted
23
52
  ) {
24
- return {
25
- isDelegated: result.isDelegated,
26
- delegatedAddress: result.delegatedAddress,
27
- }
53
+ return newDelegation
28
54
  }
29
55
  } catch (error) {
30
56
  if (logger) {
31
57
  logger.warn('Failed to check EIP-7702 delegation:', error)
32
58
  }
59
+
60
+ // On error, conservatively clear delegation state — only hard RPC confirmation
61
+ // should result in isDelegated: true being shown to the user.
62
+ return NOT_DELEGATED
33
63
  }
34
64
 
35
- // Return current state if unchanged or on error
65
+ // Return current state if unchanged
36
66
  return currentDelegation
37
67
  }
38
68