@exodus/ethereum-api 8.67.0 → 8.69.1

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,36 @@
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.69.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.69.0...@exodus/ethereum-api@8.69.1) (2026-03-12)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix(ethereum-api): ignore Clarity pending spam transactions in normalization (#7503)
13
+
14
+
15
+
16
+ ## [8.69.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.67.0...@exodus/ethereum-api@8.69.0) (2026-03-10)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: pass `baseAsset` to `getExtraFeeForBump` instead of `baseAsset.name` (#7543)
23
+
24
+
25
+
26
+ ## [8.68.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.67.0...@exodus/ethereum-api@8.68.0) (2026-03-09)
27
+
28
+
29
+ ### Features
30
+
31
+
32
+ * feat: add assets-gateway enable for clarity-v2, enable bsc (#7480)
33
+
34
+
35
+
6
36
  ## [8.67.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.66.0...@exodus/ethereum-api@8.67.0) (2026-03-09)
7
37
 
8
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.67.0",
3
+ "version": "8.69.1",
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",
@@ -69,5 +69,5 @@
69
69
  "type": "git",
70
70
  "url": "git+https://github.com/ExodusMovement/assets.git"
71
71
  },
72
- "gitHead": "f27651709a0a2c70f3c0867afc543fcfa5cfd5c2"
72
+ "gitHead": "51cc10fa866ae472882aa09353e2425bc7d7031f"
73
73
  }
@@ -146,6 +146,7 @@ export const createHistoryMonitorFactory = ({
146
146
  rpcBalanceAssetNames,
147
147
  wsGatewayUri,
148
148
  eip7702Supported,
149
+ getBlackListStatus,
149
150
  }) => {
150
151
  assert(assetName, 'expected assetName')
151
152
  assert(assetClientInterface, 'expected assetClientInterface')
@@ -164,6 +165,7 @@ export const createHistoryMonitorFactory = ({
164
165
  server,
165
166
  rpcBalanceAssetNames,
166
167
  eip7702Supported,
168
+ getBlackListStatus,
167
169
  ...args,
168
170
  })
169
171
  break
@@ -175,6 +177,7 @@ export const createHistoryMonitorFactory = ({
175
177
  rpcBalanceAssetNames,
176
178
  wsGatewayClient: createWsGateway({ uri: wsGatewayUri }),
177
179
  eip7702Supported,
180
+ getBlackListStatus,
178
181
  ...args,
179
182
  })
180
183
  break
@@ -184,6 +187,7 @@ export const createHistoryMonitorFactory = ({
184
187
  interval: ms(monitorInterval || '15s'),
185
188
  server,
186
189
  eip7702Supported,
190
+ getBlackListStatus,
187
191
  ...args,
188
192
  })
189
193
  break
@@ -214,6 +218,35 @@ export const createHistoryMonitorFactory = ({
214
218
  }
215
219
  }
216
220
 
221
+ export const createSecurityChecks = ({ eip7702Supported }) => {
222
+ return ({ accountState }) => {
223
+ // Always return global scam findings before lower-severity checks because
224
+ // global checks can block the app at startup.
225
+ if (accountState?.isBlacklisted) {
226
+ return {
227
+ isSecure: false,
228
+ type: 'GLOBAL_SCAM',
229
+ reason: 'Account is globally blacklisted.',
230
+ }
231
+ }
232
+
233
+ const delegation = accountState?.eip7702Delegation
234
+ if (eip7702Supported && Boolean(delegation?.isDelegated) && !delegation?.isWhitelisted) {
235
+ return {
236
+ isSecure: false,
237
+ type: 'LOST_PERMISSIONS',
238
+ reason: 'Account is delegated to a non-whitelisted EIP-7702 address.',
239
+ }
240
+ }
241
+
242
+ return {
243
+ isSecure: true,
244
+ type: null,
245
+ reason: null,
246
+ }
247
+ }
248
+ }
249
+
217
250
  export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNonce }) => {
218
251
  assert(assetClientInterface, 'expected assetClientInterface')
219
252
  assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
@@ -24,6 +24,7 @@ import { addressHasHistoryFactory } from './address-has-history.js'
24
24
  import {
25
25
  createGetBlackListStatus,
26
26
  createHistoryMonitorFactory,
27
+ createSecurityChecks,
27
28
  createTransactionPrivacyFactory,
28
29
  getNonceFactory,
29
30
  resolveMonitorSettings,
@@ -224,6 +225,8 @@ export const createAssetFactory = ({
224
225
  }
225
226
  : undefined
226
227
 
228
+ const getBlackListStatus = createGetBlackListStatus({ server, address, blacklistChecks })
229
+
227
230
  const accountStateClass =
228
231
  CustomAccountState || createEthereumLikeAccountState({ asset: base, assets, extraData })
229
232
 
@@ -237,6 +240,7 @@ export const createAssetFactory = ({
237
240
  rpcBalanceAssetNames,
238
241
  wsGatewayUri,
239
242
  eip7702Supported,
243
+ getBlackListStatus,
240
244
  })
241
245
 
242
246
  const defaultAddressPath = 'm/0/0'
@@ -266,17 +270,13 @@ export const createAssetFactory = ({
266
270
 
267
271
  const { getNonce } = getNonceFactory({ assetClientInterface, useAbsoluteBalanceAndNonce })
268
272
 
269
- const getBlackListStatus = createGetBlackListStatus({ server, address, blacklistChecks })
273
+ const securityChecks = createSecurityChecks({ eip7702Supported })
270
274
 
271
275
  const api = {
272
276
  addressHasHistory,
273
277
  broadcastTx: (...args) => server.sendRawTransaction(...args),
274
278
  createAccountState: () => accountStateClass,
275
- hasLostPermission: ({ accountState }) => {
276
- if (!eip7702Supported) return false
277
- const delegation = accountState?.eip7702Delegation
278
- return Boolean(delegation?.isDelegated) && !delegation?.isWhitelisted
279
- },
279
+ securityChecks,
280
280
  createFeeMonitor,
281
281
  createHistoryMonitor,
282
282
  createToken,
@@ -4,6 +4,8 @@ import assert from 'minimalistic-assert'
4
4
 
5
5
  import ClarityServer, { RPC_REQUEST_TIMEOUT } from './clarity.js'
6
6
 
7
+ const ASSETS_GATEWAY_URL = 'https://assets-gateway-clarity-api.a.exodus.io/assets'
8
+
7
9
  export const encodeCursor = (blockNumberBigInt, isLegacy = false) => {
8
10
  if (typeof blockNumberBigInt !== 'bigint') throw new Error('expected bigint')
9
11
 
@@ -93,16 +95,17 @@ const fetchHttpRequest = ({ baseApiPath, path, method, body }) => {
93
95
  export default class ClarityServerV2 extends ClarityServer {
94
96
  constructor({ baseAssetName, uri }) {
95
97
  super({ baseAssetName, uri })
96
- this.updateBaseApiPath()
98
+ this.updateBaseApiPath() // default to assets-gateway
97
99
  }
98
100
 
99
- updateBaseApiPath() {
100
- this.baseApiPath = this.uri + `/api/v2/${this.baseAssetName}`
101
+ updateBaseApiPath(uri) {
102
+ const base = uri || ASSETS_GATEWAY_URL
103
+ this.baseApiPath = new URL(`${base}/api/v2/${this.baseAssetName}`).toString()
101
104
  }
102
105
 
103
106
  setURI(uri) {
104
107
  super.setURI(uri)
105
- this.updateBaseApiPath()
108
+ this.updateBaseApiPath(uri) // pass in the uri from remote config to override assets-gateway
106
109
  }
107
110
 
108
111
  getTransactionsAtBlockNumber = async ({ address, blockNumber, withInput = true }) => {
@@ -16,6 +16,7 @@ export default class ClarityServer extends EventEmitter {
16
16
  super()
17
17
  this.baseAssetName = baseAssetName
18
18
  this.uri = uri
19
+ this.wsUri = uri
19
20
  this.defaultUri = uri
20
21
  this.baseNamespace = `/v1/${this.baseAssetName}`
21
22
  this.sockets = Object.create(null)
@@ -23,6 +24,12 @@ export default class ClarityServer extends EventEmitter {
23
24
  }
24
25
 
25
26
  setURI(uri) {
27
+ if (!uri.includes('assets-gateway')) {
28
+ // assets-gateway endpoint doesn't support legacy ws
29
+ // guard this against remote config override
30
+ this.wsUri = uri
31
+ }
32
+
26
33
  this.dispose()
27
34
  this.uri = uri
28
35
  }
@@ -59,7 +66,7 @@ export default class ClarityServer extends EventEmitter {
59
66
  }
60
67
 
61
68
  createSocket(namespace) {
62
- return io(`${this.uri}${namespace}`, {
69
+ return io(`${this.wsUri}${namespace}`, {
63
70
  transports: ['websocket', 'polling'],
64
71
  extraHeaders: { 'User-Agent': 'exodus' },
65
72
  reconnection: true,
package/src/get-fee.js CHANGED
@@ -109,13 +109,14 @@ export const getFeeFactory =
109
109
  return { ...maybeReturnTipGasPrice, fee, gasPrice, extraFeeData }
110
110
  }
111
111
 
112
- // TODO: sanity check this usage
113
112
  // Used in Mobile
114
- export const getExtraFeeForBump = ({ tx, feeData, balance, unconfirmedBalance }) => {
113
+ export const getExtraFeeForBump = ({ baseAsset, tx, feeData, balance, unconfirmedBalance }) => {
114
+ assert(baseAsset, 'expected baseAsset')
115
+
115
116
  if (!balance || !unconfirmedBalance) return null
116
117
  const { gasPrice: currentGasPrice, eip1559Enabled, baseFeePerGas: currentBaseFee } = feeData
117
118
  const { bumpedGasPrice } = calculateBumpedGasPrice({
118
- baseAsset: 'ethereum',
119
+ baseAsset,
119
120
  tx,
120
121
  currentGasPrice,
121
122
  currentBaseFee,
@@ -15,6 +15,7 @@ import {
15
15
  checkPendingTransactions,
16
16
  excludeUnchangedTokenBalances,
17
17
  getAllLogItemsByAsset,
18
+ getCurrentBlackListStatus,
18
19
  getCurrentEIP7702Delegation,
19
20
  getDeriveDataNeededForTick,
20
21
  getDeriveTransactionsToCheck,
@@ -33,6 +34,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
33
34
  wsGatewayClient,
34
35
  rpcBalanceAssetNames,
35
36
  eip7702Supported,
37
+ getBlackListStatus,
36
38
  config,
37
39
  ...args
38
40
  } = {}) {
@@ -44,6 +46,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
44
46
  this.#wsClient = wsGatewayClient
45
47
  this.#rpcBalanceAssetNames = rpcBalanceAssetNames
46
48
  this.eip7702Supported = eip7702Supported
49
+ this.getBlackListStatus = getBlackListStatus
47
50
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
48
51
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
49
52
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -206,6 +209,7 @@ export class ClarityMonitorV2 extends BaseMonitor {
206
209
  refresh,
207
210
  cursor,
208
211
  }) {
212
+ const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
209
213
  const hasNewTxs = allTxs.length > 0
210
214
 
211
215
  const logItemsByAsset = this.getAllLogItemsByAsset({
@@ -239,6 +243,14 @@ export class ClarityMonitorV2 extends BaseMonitor {
239
243
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
240
244
  logger: this.logger,
241
245
  })
246
+ const isBlacklisted = shouldCheckBlacklist
247
+ ? await getCurrentBlackListStatus({
248
+ getBlackListStatus: this.getBlackListStatus,
249
+ address: derivedData.ourWalletAddress,
250
+ currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
251
+ logger: this.logger,
252
+ })
253
+ : undefined
242
254
 
243
255
  const batch = this.aci.createOperationsBatch()
244
256
 
@@ -262,7 +274,8 @@ export class ClarityMonitorV2 extends BaseMonitor {
262
274
  // All updates must go through newData (accountState param is only used for mem merging)
263
275
  const newData = {
264
276
  ...accountState,
265
- ...(eip7702Delegation && { eip7702Delegation }),
277
+ ...(isBlacklisted !== undefined && { isBlacklisted }),
278
+ ...(eip7702Delegation !== undefined && { eip7702Delegation }),
266
279
  }
267
280
 
268
281
  if (cursor) {
@@ -14,6 +14,7 @@ import {
14
14
  checkPendingTransactions,
15
15
  excludeUnchangedTokenBalances,
16
16
  getAllLogItemsByAsset,
17
+ getCurrentBlackListStatus,
17
18
  getCurrentEIP7702Delegation,
18
19
  getDeriveDataNeededForTick,
19
20
  getDeriveTransactionsToCheck,
@@ -23,12 +24,20 @@ import {
23
24
  const { isEmpty } = lodash
24
25
 
25
26
  export class ClarityMonitor extends BaseMonitor {
26
- constructor({ server, config, rpcBalanceAssetNames, eip7702Supported, ...args }) {
27
+ constructor({
28
+ server,
29
+ config,
30
+ rpcBalanceAssetNames,
31
+ eip7702Supported,
32
+ getBlackListStatus,
33
+ ...args
34
+ }) {
27
35
  super(args)
28
36
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
29
37
  this.server = server
30
38
  this.rpcBalanceAssetNames = rpcBalanceAssetNames
31
39
  this.eip7702Supported = eip7702Supported
40
+ this.getBlackListStatus = getBlackListStatus
32
41
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
33
42
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
34
43
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -144,6 +153,10 @@ export class ClarityMonitor extends BaseMonitor {
144
153
 
145
154
  async tick({ walletAccount, refresh }) {
146
155
  await this.subscribeWalletAddresses()
156
+ // TODO: Investigate routing the onTransaction path through tickWithExtra first,
157
+ // so tickCount is initialized and this fallback can be removed in a dedicated follow-up.
158
+ const tickCount = this.tickCount[walletAccount] ?? 0
159
+ const shouldCheckBlacklist = tickCount === 0
147
160
 
148
161
  const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
149
162
  const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
@@ -196,6 +209,14 @@ export class ClarityMonitor extends BaseMonitor {
196
209
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
197
210
  logger: this.logger,
198
211
  })
212
+ const isBlacklisted = shouldCheckBlacklist
213
+ ? await getCurrentBlackListStatus({
214
+ getBlackListStatus: this.getBlackListStatus,
215
+ address: derivedData.ourWalletAddress,
216
+ currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
217
+ logger: this.logger,
218
+ })
219
+ : undefined
199
220
 
200
221
  const batch = this.aci.createOperationsBatch()
201
222
 
@@ -220,7 +241,8 @@ export class ClarityMonitor extends BaseMonitor {
220
241
  const newData = {
221
242
  ...accountState,
222
243
  clarityCursor: response.cursor,
223
- ...(eip7702Delegation && { eip7702Delegation }),
244
+ ...(isBlacklisted !== undefined && { isBlacklisted }),
245
+ ...(eip7702Delegation !== undefined && { eip7702Delegation }),
224
246
  }
225
247
 
226
248
  this.aci.updateAccountStateBatch({
@@ -0,0 +1,13 @@
1
+ const isPendingClarityTx = (tx) => tx?.blockNumber == null
2
+
3
+ const isSpamNote = (note) => {
4
+ if (!note || typeof note !== 'object') return false
5
+ if (typeof note.type !== 'string') return false
6
+ return note.type.toLowerCase() === 'spam'
7
+ }
8
+
9
+ export const isSpamPendingTx = (tx) => {
10
+ if (!isPendingClarityTx(tx)) return false
11
+ if (!Array.isArray(tx?.transactionNotes)) return false
12
+ return tx.transactionNotes.some((note) => isSpamNote(note))
13
+ }
@@ -1,5 +1,7 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
+ import { isSpamPendingTx } from './is-spam-pending-tx.js'
4
+
3
5
  // Converts the `pending` and `confirmed` transactions returned
4
6
  // by Clarity into a single contiguous array of transactions.
5
7
  //
@@ -36,6 +38,9 @@ export const normalizeTransactionsResponse = async ({
36
38
 
37
39
  const allTxs = [...response.transactions.pending, ...response.transactions.confirmed].filter(
38
40
  (tx) => {
41
+ // Pending spam transactions should not enter txlog state at all.
42
+ if (isSpamPendingTx(tx)) return false
43
+
39
44
  // If the transaction isn't one we've sent, then ignore.
40
45
  if (tx.from?.toLowerCase() !== fromAddress.toLowerCase()) return true
41
46
 
@@ -7,6 +7,7 @@ import { fromHexToString } from '../number-utils.js'
7
7
  import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check.js'
8
8
  import {
9
9
  excludeUnchangedTokenBalances,
10
+ getCurrentBlackListStatus,
10
11
  getCurrentEIP7702Delegation,
11
12
  getDeriveDataNeededForTick,
12
13
  getDeriveTransactionsToCheck,
@@ -17,11 +18,12 @@ const { isEmpty, unionBy, zipObject } = lodash
17
18
  // The base ethereum monitor no history class handles listening for assets with no history
18
19
 
19
20
  export class EthereumNoHistoryMonitor extends BaseMonitor {
20
- constructor({ server, config, eip7702Supported, ...args }) {
21
+ constructor({ server, config, eip7702Supported, getBlackListStatus, ...args }) {
21
22
  super(args)
22
23
  this.server = server
23
24
  this.config = { ...config }
24
25
  this.eip7702Supported = eip7702Supported
26
+ this.getBlackListStatus = getBlackListStatus
25
27
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
26
28
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
27
29
  getTxLog: (...args) => this.aci.getTxLog(...args),
@@ -156,6 +158,7 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
156
158
  }
157
159
 
158
160
  async tick({ refresh, walletAccount }) {
161
+ const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
159
162
  const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
160
163
  const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
161
164
 
@@ -195,11 +198,20 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
195
198
  currentDelegation: currentAccountState?.eip7702Delegation,
196
199
  logger: this.logger,
197
200
  })
201
+ const isBlacklisted = shouldCheckBlacklist
202
+ ? await getCurrentBlackListStatus({
203
+ getBlackListStatus: this.getBlackListStatus,
204
+ address: ourWalletAddress,
205
+ currentIsBlacklisted: currentAccountState?.isBlacklisted,
206
+ logger: this.logger,
207
+ })
208
+ : undefined
198
209
 
199
210
  // All updates must go through newData (accountState param is only used for mem merging)
200
211
  const newData = {
201
212
  ...accountState,
202
- ...(eip7702Delegation && { eip7702Delegation }),
213
+ ...(isBlacklisted !== undefined && { isBlacklisted }),
214
+ ...(eip7702Delegation !== undefined && { eip7702Delegation }),
203
215
  }
204
216
 
205
217
  await this.updateAccountState({ accountState, newData, walletAccount })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Checks the current blacklist status for an address and returns the state to use.
3
+ * Returns the new state if changed, undefined if unchanged.
4
+ * On error, resets to null (unknown) to avoid showing a stale blacklist warning —
5
+ * preferring a false negative over a false positive.
6
+ *
7
+ * @param {Object} params
8
+ * @param {Function} [params.getBlackListStatus] - Asset blacklist status checker
9
+ * @param {string} params.address - Wallet address to check
10
+ * @param {boolean|null} [params.currentIsBlacklisted] - Current blacklist flag from accountState
11
+ * @param {Object} [params.logger] - Optional logger for warnings
12
+ * @returns {Promise<boolean|null|undefined>} The new blacklist flag, or undefined if unchanged
13
+ */
14
+ export async function getCurrentBlackListStatus({
15
+ getBlackListStatus,
16
+ address,
17
+ currentIsBlacklisted,
18
+ logger,
19
+ }) {
20
+ if (typeof getBlackListStatus !== 'function') return
21
+
22
+ try {
23
+ const { isBlacklisted } = await getBlackListStatus({ address })
24
+
25
+ if (currentIsBlacklisted !== isBlacklisted) {
26
+ return isBlacklisted
27
+ }
28
+ } catch (error) {
29
+ if (logger) {
30
+ logger.warn('Failed to check blacklist status:', error)
31
+ }
32
+
33
+ // Reset to null (unknown) on error — only a confirmed check should show the blacklist warning.
34
+ return null
35
+ }
36
+ }
37
+
38
+ export default getCurrentBlackListStatus
@@ -1,12 +1,26 @@
1
1
  import { getEIP7702Delegation } from '../../eth-like-util.js'
2
2
 
3
- const NOT_DELEGATED = { isDelegated: false, delegatedAddress: null, isWhitelisted: null }
3
+ const NOT_DELEGATED = {
4
+ isDelegated: false,
5
+ delegatedAddress: null,
6
+ delegatedName: null,
7
+ isWhitelisted: null,
8
+ }
9
+
10
+ function hasDelegationChanged(currentDelegation, newDelegation) {
11
+ return (
12
+ currentDelegation?.isDelegated !== newDelegation.isDelegated ||
13
+ currentDelegation?.delegatedAddress !== newDelegation.delegatedAddress ||
14
+ currentDelegation?.delegatedName !== newDelegation.delegatedName ||
15
+ currentDelegation?.isWhitelisted !== newDelegation.isWhitelisted
16
+ )
17
+ }
4
18
 
5
19
  /**
6
20
  * Checks if the address has an EIP-7702 delegation and returns the delegation state.
7
- * Returns the new state if changed, or the current state if unchanged.
8
- * On error, conservatively returns { isDelegated: false } to avoid showing stale delegation
9
- * state that may no longer be accurate.
21
+ * Returns the new state if changed, or undefined if unchanged (monitors treat undefined as "no write").
22
+ * On error, conservatively returns NOT_DELEGATED to avoid showing stale isDelegated: true,
23
+ * but only if the current state actually needs clearing.
10
24
  *
11
25
  * @param {Object} params
12
26
  * @param {Object} params.server - The server instance to use for getCode
@@ -16,7 +30,7 @@ const NOT_DELEGATED = { isDelegated: false, delegatedAddress: null, isWhiteliste
16
30
  * An empty array means the check runs but every delegation will be isWhitelisted: false.
17
31
  * @param {Object} [params.currentDelegation] - The current delegation state from accountState
18
32
  * @param {Object} [params.logger] - Optional logger for warnings
19
- * @returns {Promise<Object|undefined>} The delegation state to use
33
+ * @returns {Promise<Object|undefined>} The new delegation state, or undefined if unchanged
20
34
  */
21
35
  export async function getCurrentEIP7702Delegation({
22
36
  server,
@@ -26,32 +40,31 @@ export async function getCurrentEIP7702Delegation({
26
40
  logger,
27
41
  }) {
28
42
  // Non-array (undefined, false, etc.) → chain doesn't support EIP-7702, skip entirely
29
- if (!Array.isArray(eip7702Supported)) return NOT_DELEGATED
43
+ if (!Array.isArray(eip7702Supported)) {
44
+ return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
45
+ }
30
46
 
31
47
  try {
32
48
  const result = await getEIP7702Delegation({ address, server })
33
49
 
34
- if (!result.isDelegated) return NOT_DELEGATED
50
+ if (!result.isDelegated) {
51
+ return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
52
+ }
35
53
 
36
54
  // [] → check runs but nothing is trusted; populated array → whitelist check
37
- const isWhitelisted = eip7702Supported.some(
55
+ const matchedDelegation = eip7702Supported.find(
38
56
  ({ address }) => address.toLowerCase() === result.delegatedAddress.toLowerCase()
39
57
  )
58
+ const isWhitelisted = Boolean(matchedDelegation)
40
59
 
41
60
  const newDelegation = {
42
61
  isDelegated: true,
43
62
  delegatedAddress: result.delegatedAddress,
63
+ delegatedName: matchedDelegation?.name ?? null,
44
64
  isWhitelisted,
45
65
  }
46
66
 
47
- // Return new state only if something changed
48
- if (
49
- currentDelegation?.isDelegated !== newDelegation.isDelegated ||
50
- currentDelegation?.delegatedAddress !== newDelegation.delegatedAddress ||
51
- currentDelegation?.isWhitelisted !== newDelegation.isWhitelisted
52
- ) {
53
- return newDelegation
54
- }
67
+ return hasDelegationChanged(currentDelegation, newDelegation) ? newDelegation : undefined
55
68
  } catch (error) {
56
69
  if (logger) {
57
70
  logger.warn('Failed to check EIP-7702 delegation:', error)
@@ -59,11 +72,8 @@ export async function getCurrentEIP7702Delegation({
59
72
 
60
73
  // On error, conservatively clear delegation state — only hard RPC confirmation
61
74
  // should result in isDelegated: true being shown to the user.
62
- return NOT_DELEGATED
75
+ return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
63
76
  }
64
-
65
- // Return current state if unchanged
66
- return currentDelegation
67
77
  }
68
78
 
69
79
  export default getCurrentEIP7702Delegation
@@ -6,5 +6,6 @@ export { default as getHistoryFromServer } from './get-history-from-server.js'
6
6
  export { default as checkPendingTransactions } from './check-pending-transactions.js'
7
7
  export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
8
8
  export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
9
+ export { default as getCurrentBlackListStatus } from './get-current-blacklist-status.js'
9
10
  export { default as verifyRpcPendingTxStatusBatch } from './verify-pending-tx-status-rpc.js'
10
11
  export * from './exclude-unchanged-token-balances.js'