@exodus/ethereum-api 8.65.0 → 8.67.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,32 @@
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.67.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.66.0...@exodus/ethereum-api@8.67.0) (2026-03-09)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: add hasLostPermission api method to asset plugins (#7524)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix: queued/invalid Ethereum transactions being offered for RBF acceleration (#7463)
19
+
20
+
21
+
22
+ ## [8.66.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.65.0...@exodus/ethereum-api@8.66.0) (2026-03-04)
23
+
24
+
25
+ ### Features
26
+
27
+
28
+ * feat: add USDT blacklist status APIs for ETH and TRX and expose TRX raw account (#7444)
29
+
30
+
31
+
6
32
  ## [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
33
 
8
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.65.0",
3
+ "version": "8.67.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",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@exodus/asset": "^2.0.4",
26
- "@exodus/asset-lib": "^5.3.0",
26
+ "@exodus/asset-lib": "^5.8.0",
27
27
  "@exodus/assets": "^11.4.0",
28
28
  "@exodus/basic-utils": "^3.0.1",
29
29
  "@exodus/bip44-constants": "^195.0.0",
@@ -33,6 +33,7 @@
33
33
  "@exodus/ethereum-meta": "^2.9.1",
34
34
  "@exodus/ethereumholesky-meta": "^2.0.5",
35
35
  "@exodus/ethereumjs": "^1.8.0",
36
+ "@exodus/ethersproject-abi": "^5.4.2-exodus.2",
36
37
  "@exodus/fetch": "^1.3.0",
37
38
  "@exodus/models": "^12.13.0",
38
39
  "@exodus/safe-string": "^1.4.0",
@@ -68,5 +69,5 @@
68
69
  "type": "git",
69
70
  "url": "git+https://github.com/ExodusMovement/assets.git"
70
71
  },
71
- "gitHead": "98c4dc4e2cd52781dadc2af4fae2b80ecc276626"
72
+ "gitHead": "f27651709a0a2c70f3c0867afc543fcfa5cfd5c2"
72
73
  }
@@ -1,13 +1,17 @@
1
+ import { BlacklistCheckTypes } from '@exodus/asset-lib'
2
+ import { defaultAbiCoder } from '@exodus/ethersproject-abi'
3
+ import { safeString } from '@exodus/safe-string'
1
4
  import assert from 'minimalistic-assert'
2
5
  import ms from 'ms'
3
6
 
7
+ import { EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
4
8
  import { createEvmServer, createWsGateway, ValidMonitorTypes } from './exodus-eth-server/index.js'
5
9
  import { createEthereumHooks } from './hooks/index.js'
6
10
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
7
11
  import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
8
12
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
9
13
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
10
- import { BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
14
+ import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
11
15
 
12
16
  // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
13
17
  // to use for a given config.
@@ -245,3 +249,64 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
245
249
 
246
250
  return { getNonce }
247
251
  }
252
+
253
+ // Creates a contract-based blacklist check function.
254
+ // async ({ address }) => boolean
255
+ export const createContractBlackListCheck = ({ contractAddress, selector, server }) => {
256
+ assert(contractAddress, 'expected contractAddress')
257
+ assert(selector, 'expected selector')
258
+ assert(server, 'expected server')
259
+
260
+ return async ({ address }) => {
261
+ const tag = BLOCK_TAG_LATEST
262
+ const encodedAddress = address.slice(2).toLowerCase().padStart(64, '0')
263
+ const data = selector + encodedAddress
264
+ const result = await withErrorReason({
265
+ promise: server.ethCall({ to: contractAddress, data }, tag),
266
+ errorReasonInfo: EVM_ERROR_REASONS.ethCallErc20Failed,
267
+ hint: safeString`getBlackListStatus:ethCall`,
268
+ baseAssetName: server.baseAssetName,
269
+ })
270
+
271
+ if (typeof result !== 'string' || result.length === 0) {
272
+ throw new Error('getBlackListStatus got invalid ethCall response')
273
+ }
274
+
275
+ let isBlacklisted
276
+ try {
277
+ ;[isBlacklisted] = defaultAbiCoder.decode(['bool'], result)
278
+ } catch (err) {
279
+ throw new Error(`getBlackListStatus got invalid ABI bool result: ${err.message}`)
280
+ }
281
+
282
+ return isBlacklisted
283
+ }
284
+ }
285
+
286
+ // Composes blacklist check factories into a single getBlackListStatus.
287
+ // Returns undefined if no checks are provided.
288
+ // Returns { isBlacklisted: true } if ANY check returns true.
289
+ // NOTE: blacklistChecks defaults to [] as a convenience, but it is the asset's responsibility
290
+ // to be properly configured with the appropriate checks. An empty array means no blacklist
291
+ // checks are run, which implies EIP-7702 is effectively enabled on that network with no whitelist.
292
+ export const createGetBlackListStatus = ({ address: addressApi, blacklistChecks = [], server }) => {
293
+ if (blacklistChecks.length === 0) return
294
+
295
+ assert(addressApi, 'expected address')
296
+
297
+ // Extend here to support additional blacklist sources (e.g. an internal database query).
298
+ const checks = blacklistChecks.map((factory) => {
299
+ if (factory.type === BlacklistCheckTypes.CONTRACT_CALL) {
300
+ assert(server, 'expected server for CONTRACT_CALL blacklist check')
301
+ return factory({ server })
302
+ }
303
+
304
+ throw new Error(`Unknown blacklist check type: ${factory.type}`)
305
+ })
306
+
307
+ return async ({ address }) => {
308
+ assert(addressApi.validate(address), 'getBlackListStatus requires a valid Ethereum address')
309
+ const results = await Promise.all(checks.map((check) => check({ address })))
310
+ return { isBlacklisted: results.some(Boolean) }
311
+ }
312
+ }
@@ -22,6 +22,7 @@ import assert from 'minimalistic-assert'
22
22
 
23
23
  import { addressHasHistoryFactory } from './address-has-history.js'
24
24
  import {
25
+ createGetBlackListStatus,
25
26
  createHistoryMonitorFactory,
26
27
  createTransactionPrivacyFactory,
27
28
  getNonceFactory,
@@ -68,6 +69,7 @@ export const createAssetFactory = ({
68
69
  useEip1191ChainIdChecksum = false,
69
70
  forceGasLimitEstimation = false,
70
71
  rpcBalanceAssetNames = [],
72
+ blacklistChecks = [],
71
73
  supportsCustomFees: defaultSupportsCustomFees = false,
72
74
  useAbsoluteBalanceAndNonce = false,
73
75
  delisted = false,
@@ -264,10 +266,17 @@ export const createAssetFactory = ({
264
266
 
265
267
  const { getNonce } = getNonceFactory({ assetClientInterface, useAbsoluteBalanceAndNonce })
266
268
 
269
+ const getBlackListStatus = createGetBlackListStatus({ server, address, blacklistChecks })
270
+
267
271
  const api = {
268
272
  addressHasHistory,
269
273
  broadcastTx: (...args) => server.sendRawTransaction(...args),
270
274
  createAccountState: () => accountStateClass,
275
+ hasLostPermission: ({ accountState }) => {
276
+ if (!eip7702Supported) return false
277
+ const delegation = accountState?.eip7702Delegation
278
+ return Boolean(delegation?.isDelegated) && !delegation?.isWhitelisted
279
+ },
271
280
  createFeeMonitor,
272
281
  createHistoryMonitor,
273
282
  createToken,
@@ -279,6 +288,7 @@ export const createAssetFactory = ({
279
288
  getActivityTxs,
280
289
  getBalances,
281
290
  getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
291
+ ...(getBlackListStatus && { getBlackListStatus }),
282
292
  getConfirmationsNumber: () => confirmationsNumber,
283
293
  getDefaultAddressPath: () => defaultAddressPath,
284
294
  getFeeAsync: createTx, // createTx alias, remove me when possible
package/src/index.js CHANGED
@@ -100,6 +100,8 @@ export { txSendFactory } from './tx-send/index.js'
100
100
 
101
101
  export { createAssetFactory } from './create-asset.js'
102
102
 
103
+ export { createContractBlackListCheck } from './create-asset-utils.js'
104
+
103
105
  export {
104
106
  createAssetPluginFactory,
105
107
  fromAddEthereumChainParameterToFactoryParams,
package/src/tx-create.js CHANGED
@@ -159,6 +159,63 @@ const resolveTxFactoryGasPrices = async ({
159
159
  }
160
160
  }
161
161
 
162
+ // Pre-broadcast safety check for RBF replacements.
163
+ //
164
+ // Clarity can lag behind the node: it may still report a tx as pending after
165
+ // the node has evicted it, or show a queued (gapped) tx as acceleratable.
166
+ // Broadcasting an RBF for a gapped tx — one whose predecessor slot is empty in
167
+ // the node's mempool — produces ErrFutureReplacePending.
168
+ //
169
+ // This check is best-effort: if either RPC call fails (network error, unsupported
170
+ // endpoint, etc.) we fall through and let the broadcast surface the error.
171
+ async function verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce }) {
172
+ let rpcTx, pendingNonceHex
173
+
174
+ try {
175
+ ;[rpcTx, pendingNonceHex] = await Promise.all([
176
+ baseAsset.server.getTransactionByHash(bumpTxId),
177
+ baseAsset.server.getTransactionCount(fromAddress, 'pending'),
178
+ ])
179
+ } catch (err) {
180
+ console.warn(
181
+ `verifyBumpTxCanReplace: pre-broadcast RPC check failed for ${bumpTxId}, falling through`,
182
+ err.message
183
+ )
184
+ return
185
+ }
186
+
187
+ if (!rpcTx) {
188
+ // The tx was dropped from the node's mempool — Clarity hasn't caught up yet.
189
+ // TODO: eagerly mark this tx as dropped in the tx log so the UI updates
190
+ // immediately instead of waiting for Clarity's next polling cycle.
191
+ throw new Error(
192
+ `ERR_BUMP_TX_DROPPED: Cannot bump transaction ${bumpTxId}: transaction was dropped from the network`
193
+ )
194
+ }
195
+
196
+ // pendingNonce = 4
197
+ // [0][1][2][3]{4}{5}{6}...
198
+ // ^--- next empty slot (pendingNonce)
199
+ //
200
+ // Nonce to replace must be < pendingNonce (i.e. slot is already accounted for).
201
+ // If nonce >= pendingNonce, the slot is empty or gapped — not safe to replace.
202
+ const pendingNonce = parseInt(pendingNonceHex, 16)
203
+ if (pendingNonce <= nonce) {
204
+ // A predecessor tx was dropped by the node, leaving a gap below this tx.
205
+ // The tx itself is still in the node's queued pool but can't execute until
206
+ // the missing nonce slot is filled.
207
+ // TODO: identify the dropped predecessor (scan nonces from pendingNonce to
208
+ // nonce - 1), mark it as dropped in the tx log, and surface a clear message
209
+ // to the user: "a previous transaction was dropped — send a new transaction
210
+ // first, then you'll be able to accelerate this one."
211
+ throw new Error(
212
+ `ERR_BUMP_TX_NONCE_GAP: Cannot bump transaction ${bumpTxId}: nonce gap detected — ` +
213
+ `node's pending nonce (${pendingNonce}) ≤ tx nonce (${nonce}), ` +
214
+ `broadcasting would produce ErrFutureReplacePending`
215
+ )
216
+ }
217
+ }
218
+
162
219
  const createBumpUnsignedTx = async ({
163
220
  fromAddress,
164
221
  chainId,
@@ -223,6 +280,8 @@ const createBumpUnsignedTx = async ({
223
280
 
224
281
  const nonce = maybeProvidedNonce ?? replacedTxNonce
225
282
 
283
+ await verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce })
284
+
226
285
  const resolvedTxAttributes = await resolveTxAttributesByTxType({
227
286
  asset,
228
287
  assetClientInterface,