@exodus/ethereum-api 8.77.3 → 8.78.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,31 @@
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.78.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.78.0) (2026-08-04)
7
+
8
+
9
+ ### Features
10
+
11
+ * source eip7702 delegation whitelist from remote config ([#8356](https://github.com/ExodusMovement/assets/issues/8356)) ([1a0966b](https://github.com/ExodusMovement/assets/commit/1a0966b08befb6e9310993f323c21d003dea2754))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
17
+ * **simulation:** route recipient changes to dedicated field, guard asset-less entries ([#8370](https://github.com/ExodusMovement/assets/issues/8370)) ([c0a082b](https://github.com/ExodusMovement/assets/commit/c0a082b6c80710408337ff9285a19fb657c656be))
18
+
19
+
20
+
21
+ ## [8.77.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.4) (2026-07-14)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
27
+ * **simulation:** route recipient changes to dedicated field, guard asset-less entries ([#8370](https://github.com/ExodusMovement/assets/issues/8370)) ([c0a082b](https://github.com/ExodusMovement/assets/commit/c0a082b6c80710408337ff9285a19fb657c656be))
28
+
29
+
30
+
6
31
  ## [8.77.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.3) (2026-07-11)
7
32
 
8
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.77.3",
3
+ "version": "8.78.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",
@@ -69,5 +69,5 @@
69
69
  "type": "git",
70
70
  "url": "git+https://github.com/ExodusMovement/assets.git"
71
71
  },
72
- "gitHead": "ccdff56c057e6c4892b304fa47ecfc6c4389f629"
72
+ "gitHead": "e878130c84c596ee86050984806b0da491e14910"
73
73
  }
@@ -275,7 +275,7 @@ export const createHistoryMonitorFactory = ({
275
275
  stakingAssetNames,
276
276
  rpcBalanceAssetNames,
277
277
  wsGatewayUri,
278
- eip7702Supported,
278
+ eip7702Whitelist,
279
279
  getBlackListStatus,
280
280
  }) => {
281
281
  assert(assetName, 'expected assetName')
@@ -297,7 +297,7 @@ export const createHistoryMonitorFactory = ({
297
297
  interval: ms(monitorInterval || '5m'),
298
298
  server,
299
299
  rpcBalanceAssetNames,
300
- eip7702Supported,
300
+ eip7702Whitelist,
301
301
  getBlackListStatus,
302
302
  ...args,
303
303
  })
@@ -307,7 +307,7 @@ export const createHistoryMonitorFactory = ({
307
307
  assetClientInterface,
308
308
  interval: ms(monitorInterval || '5m'),
309
309
  server,
310
- eip7702Supported,
310
+ eip7702Whitelist,
311
311
  getBlackListStatus,
312
312
  ...args,
313
313
  })
@@ -317,7 +317,7 @@ export const createHistoryMonitorFactory = ({
317
317
  assetClientInterface,
318
318
  interval: ms(monitorInterval || '15s'),
319
319
  server,
320
- eip7702Supported,
320
+ eip7702Whitelist,
321
321
  getBlackListStatus,
322
322
  ...args,
323
323
  })
@@ -349,7 +349,7 @@ export const createHistoryMonitorFactory = ({
349
349
  }
350
350
  }
351
351
 
352
- export const createSecurityChecks = ({ eip7702Supported }) => {
352
+ export const createSecurityChecks = ({ eip7702Whitelist }) => {
353
353
  return ({ accountState }) => {
354
354
  // Always return global scam findings before lower-severity checks because
355
355
  // global checks can block the app at startup.
@@ -362,7 +362,11 @@ export const createSecurityChecks = ({ eip7702Supported }) => {
362
362
  }
363
363
 
364
364
  const delegation = accountState?.eip7702Delegation
365
- if (eip7702Supported && Boolean(delegation?.isDelegated) && !delegation?.isWhitelisted) {
365
+ if (
366
+ eip7702Whitelist.isSupported() &&
367
+ Boolean(delegation?.isDelegated) &&
368
+ !delegation?.isWhitelisted
369
+ ) {
366
370
  return {
367
371
  isSecure: false,
368
372
  type: 'LOST_PERMISSIONS',
@@ -35,6 +35,7 @@ import {
35
35
  } from './create-asset-utils.js'
36
36
  import { createTokenFactory } from './create-token-factory.js'
37
37
  import { createCustomFeesApi } from './custom-fees.js'
38
+ import { createEip7702Whitelist } from './eip7702-whitelist.js'
38
39
  import { getEIP7702Delegation, getIsNftContract } from './eth-like-util.js'
39
40
  import { createEvmServer } from './exodus-eth-server/index.js'
40
41
  import { createFeeData } from './fee-data/index.js'
@@ -270,6 +271,11 @@ export const createAssetFactory = ({
270
271
 
271
272
  const getBlackListStatus = createGetBlackListStatus({ server, address, blacklistChecks })
272
273
 
274
+ // eip7702Supported is the chain-level feature flag ([] when supported).
275
+ // The whitelist itself is populated from remote config through the
276
+ // history monitor's setServer.
277
+ const eip7702Whitelist = createEip7702Whitelist(eip7702Supported)
278
+
273
279
  const accountStateClass =
274
280
  CustomAccountState || createEthereumLikeAccountState({ asset: base, assets, extraData })
275
281
 
@@ -282,7 +288,7 @@ export const createAssetFactory = ({
282
288
  stakingAssetNames,
283
289
  rpcBalanceAssetNames,
284
290
  wsGatewayUri,
285
- eip7702Supported,
291
+ eip7702Whitelist,
286
292
  getBlackListStatus,
287
293
  })
288
294
 
@@ -318,7 +324,7 @@ export const createAssetFactory = ({
318
324
 
319
325
  const { getNonce } = getNonceFactory({ assetClientInterface, useAbsoluteBalanceAndNonce })
320
326
 
321
- const securityChecks = createSecurityChecks({ eip7702Supported })
327
+ const securityChecks = createSecurityChecks({ eip7702Whitelist })
322
328
 
323
329
  const web3 = createWeb3API({ asset })
324
330
 
@@ -412,7 +418,9 @@ export const createAssetFactory = ({
412
418
  estimateL1DataFee,
413
419
  forceGasLimitEstimation,
414
420
  eip7623Supported,
415
- eip7702Supported,
421
+ // Stable array reference kept up to date with remote config by the
422
+ // history monitor's setServer.
423
+ eip7702Supported: eip7702Whitelist.get(),
416
424
  getEIP7702Delegation: (addr) => getEIP7702Delegation({ address: addr, server }),
417
425
  getNonce,
418
426
  server,
@@ -0,0 +1,62 @@
1
+ const ADDRESS_REGEX = /^0x[\dA-Fa-f]{40}$/
2
+
3
+ const isValidWhitelistEntry = (entry) =>
4
+ typeof entry?.address === 'string' &&
5
+ ADDRESS_REGEX.test(entry.address) &&
6
+ typeof entry.name === 'string' &&
7
+ entry.name.length > 0
8
+
9
+ export const isValidEip7702Whitelist = (whitelist) =>
10
+ Array.isArray(whitelist) && whitelist.every((entry) => isValidWhitelistEntry(entry))
11
+
12
+ /**
13
+ * Mutable holder for the EIP-7702 delegation whitelist, shared between the
14
+ * history monitors, security checks and the asset itself. The whitelist data
15
+ * comes exclusively from remote config; plugins pass `eip7702Supported: []`
16
+ * as the chain-level feature flag, and the list stays empty until remote
17
+ * config delivers it. Only chains created with an array accept updates.
18
+ *
19
+ * @param {Array<{address: string, name: string}>} [defaultWhitelist] - The
20
+ * in-code value, `[]` for supported chains. A non-array means the chain
21
+ * does not support EIP-7702.
22
+ */
23
+ export const createEip7702Whitelist = (defaultWhitelist) => {
24
+ const supported = Array.isArray(defaultWhitelist)
25
+ // The array identity is stable across updates (mutated in place), so
26
+ // consumers holding a reference (e.g. asset.eip7702Supported, which gets
27
+ // spread-copied into the wallet's asset registry) observe updates too.
28
+ const current = supported
29
+ ? defaultWhitelist.map(({ address, name }) => ({ address, name }))
30
+ : undefined
31
+
32
+ return {
33
+ isSupported: () => supported,
34
+ get: () => current,
35
+ /**
36
+ * Replaces the whitelist with the remote-config value. Returns whether the
37
+ * update was applied. Invalid payloads and updates on chains without
38
+ * EIP-7702 support are ignored, keeping the last known good whitelist.
39
+ */
40
+ update: (remoteWhitelist) => {
41
+ if (!supported) return false
42
+ if (!isValidEip7702Whitelist(remoteWhitelist)) return false
43
+
44
+ current.length = 0
45
+ current.push(...remoteWhitelist.map(({ address, name }) => ({ address, name })))
46
+ return true
47
+ },
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Applies the `eip7702Whitelist` field of a per-asset remote config payload,
53
+ * as delivered to the monitors through `monitor.setServer(remoteConfig)`.
54
+ */
55
+ export const applyRemoteConfigEip7702Whitelist = ({ eip7702Whitelist, remoteConfig, logger }) => {
56
+ const remoteWhitelist = remoteConfig?.eip7702Whitelist
57
+ if (remoteWhitelist === undefined) return
58
+
59
+ if (!eip7702Whitelist.update(remoteWhitelist)) {
60
+ logger?.warn('ignoring invalid eip7702Whitelist from remote config')
61
+ }
62
+ }
@@ -1,15 +1,13 @@
1
1
  import { safeString } from '@exodus/safe-string'
2
2
  import lodash from 'lodash'
3
3
 
4
- import { fromHexToString } from '../number-utils.js'
5
4
  import { errorMessageToSafeHint } from './errors.js'
6
- import EthLikeServerBase from './eth-like-server-base.js'
5
+ import { EthLikeServer } from './eth-like-server.js'
7
6
  import { fetchJsonRetry } from './fetch-json.js'
8
- import { getFallbackGasPriceEstimation } from './utils.js'
9
7
 
10
8
  const { isEmpty } = lodash
11
9
 
12
- export default class ApiCoinNodesServer extends EthLikeServerBase {
10
+ export default class ApiCoinNodesServer extends EthLikeServer {
13
11
  constructor({ baseAssetName, uri }) {
14
12
  super()
15
13
  this.baseAssetName = baseAssetName
@@ -59,109 +57,6 @@ export default class ApiCoinNodesServer extends EthLikeServerBase {
59
57
  return result
60
58
  }
61
59
 
62
- async isContract(address) {
63
- const code = await this.getCode(address)
64
- return code.length > 2
65
- }
66
-
67
- async balanceOf(address, tokenAddress, tag = 'latest') {
68
- const request = this.balanceOfRequest(address, tokenAddress, tag)
69
- const result = await this.sendRequest(request)
70
- const balance = fromHexToString(result)
71
- return {
72
- confirmed: {
73
- [tokenAddress]: balance,
74
- },
75
- }
76
- }
77
-
78
- async getBalance(...params) {
79
- const request = this.getBalanceRequest(...params)
80
- return this.sendRequest(request)
81
- }
82
-
83
- async getBalanceProxied(...params) {
84
- return this.getBalance(...params)
85
- }
86
-
87
- async gasPrice(...params) {
88
- const request = this.gasPriceRequest(...params)
89
- return this.sendRequest(request)
90
- }
91
-
92
- async getGasPriceEstimation() {
93
- return getFallbackGasPriceEstimation({ server: this })
94
- }
95
-
96
- // for fee monitor
97
- getGasPrice = this.gasPrice
98
-
99
- async getLatestBlock() {
100
- return this.sendRequest(this.getBlockByNumberRequest('latest', false))
101
- }
102
-
103
- async getBaseFeePerGas() {
104
- const response = await this.getLatestBlock()
105
- if (response.baseFeePerGas) {
106
- return fromHexToString(response.baseFeePerGas)
107
- }
108
- }
109
-
110
- async estimateGas(...params) {
111
- const request = this.estimateGasRequest(...params)
112
- return this.sendRequest(request)
113
- }
114
-
115
- async sendRawTransaction(...params) {
116
- const request = this.sendRawTransactionRequest(...params)
117
- return this.sendRequest(request)
118
- }
119
-
120
- async getCode(...params) {
121
- const request = this.getCodeRequest(...params)
122
- return this.sendRequest(request)
123
- }
124
-
125
- async getStorageAt(...params) {
126
- const request = this.getStorageAtRequest(...params)
127
- return this.sendRequest(request)
128
- }
129
-
130
- async getTransactionCount(...params) {
131
- const request = this.getTransactionCountRequest(...params)
132
- return this.sendRequest(request)
133
- }
134
-
135
- async getTransactionByHash(...params) {
136
- const request = this.getTransactionByHashRequest(...params)
137
- return this.sendRequest(request)
138
- }
139
-
140
- async getTransactionReceipt(...params) {
141
- const request = this.getTransactionReceiptRequest(...params)
142
- return this.sendRequest(request)
143
- }
144
-
145
- async ethCall(...params) {
146
- const request = this.ethCallRequest(...params)
147
- return this.sendRequest(request)
148
- }
149
-
150
- async blockNumber(...params) {
151
- const request = this.blockNumberRequest(...params)
152
- return this.sendRequest(request)
153
- }
154
-
155
- async getBlockByNumber(...params) {
156
- const request = this.getBlockByNumberRequest(...params)
157
- return this.sendRequest(request)
158
- }
159
-
160
- async simulateRawTransaction(...params) {
161
- const request = this.simulateRawTransactionRequest(...params)
162
- return this.sendRequest(request)
163
- }
164
-
165
60
  stop() {
166
61
  // no web socket to stop!
167
62
  }
@@ -2,14 +2,12 @@ import { safeString } from '@exodus/safe-string'
2
2
  import { TraceId } from '@exodus/traceparent'
3
3
  import io from 'socket.io-client'
4
4
 
5
- import { fromHexToString } from '../number-utils.js'
6
5
  import { errorMessageToSafeHint } from './errors.js'
7
- import EthLikeServerBase from './eth-like-server-base.js'
8
- import { getFallbackGasPriceEstimation } from './utils.js'
6
+ import { EthLikeServer } from './eth-like-server.js'
9
7
 
10
8
  export const RPC_REQUEST_TIMEOUT = 'RPC_REQUEST_TIMEOUT'
11
9
 
12
- export default class ClarityServer extends EthLikeServerBase {
10
+ export default class ClarityServer extends EthLikeServer {
13
11
  constructor({ baseAssetName, uri }) {
14
12
  super()
15
13
  this.baseAssetName = baseAssetName
@@ -192,10 +190,6 @@ export default class ClarityServer extends EthLikeServerBase {
192
190
  return fee?.gasPrice
193
191
  }
194
192
 
195
- async getGasPriceEstimation() {
196
- return getFallbackGasPriceEstimation({ server: this })
197
- }
198
-
199
193
  async sendRpcRequest(rpcRequest) {
200
194
  const rpcSocket = this.connectRpc()
201
195
  return new Promise((resolve, reject) => {
@@ -226,168 +220,6 @@ export default class ClarityServer extends EthLikeServerBase {
226
220
  return this.handleJsonRPCResponse(response)
227
221
  }
228
222
 
229
- // Transport: Via getCode → sendRequest → WS first → HTTP fallback in ClarityServerV2
230
- async isContract(address) {
231
- const code = await this.getCode(address)
232
- return code.length > 2
233
- }
234
-
235
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
236
- async proxyToCoinNode(params) {
237
- const request = this.buildRequest(params)
238
- return this.sendRequest(request)
239
- }
240
-
241
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
242
- async balanceOf(address, tokenAddress, tag = 'latest') {
243
- const request = this.balanceOfRequest(address, tokenAddress, tag)
244
- const result = await this.sendRequest(request)
245
- const balance = fromHexToString(result)
246
- return {
247
- confirmed: {
248
- [tokenAddress]: balance,
249
- },
250
- }
251
- }
252
-
253
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
254
- async getBalance(...params) {
255
- const request = this.getBalanceRequest(...params) // eth_getBalance
256
- return this.sendRequest(request)
257
- }
258
-
259
- // Transport: Via getBalance → sendRequest → WS first → HTTP fallback in ClarityServerV2
260
- async getBalanceProxied(...params) {
261
- return this.getBalance(...params) // eth_getBalance
262
- }
263
-
264
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
265
- async gasPrice(...params) {
266
- const request = this.gasPriceRequest(...params)
267
- return this.sendRequest(request)
268
- }
269
-
270
- // Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
271
- async estimateGas(...params) {
272
- const request = this.estimateGasRequest(...params)
273
- return this.sendRequest(request)
274
- }
275
-
276
- // Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
277
- async sendRawTransaction(...params) {
278
- const request = this.sendRawTransactionRequest(...params)
279
- return this.sendRequest(request)
280
- }
281
-
282
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
283
- async getCode(...params) {
284
- const request = this.getCodeRequest(...params)
285
- return this.sendRequest(request)
286
- }
287
-
288
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
289
- async getStorageAt(...params) {
290
- const request = this.getStorageAtRequest(...params)
291
- return this.sendRequest(request)
292
- }
293
-
294
- // Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
295
- async getTransactionCount(...params) {
296
- const request = this.getTransactionCountRequest(...params)
297
- return this.sendRequest(request)
298
- }
299
-
300
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
301
- async getTransactionByHash(...params) {
302
- const request = this.getTransactionByHashRequest(...params)
303
- return this.sendRequest(request)
304
- }
305
-
306
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
307
- async getTransactionReceipt(...params) {
308
- const request = this.getTransactionReceiptRequest(...params)
309
- return this.sendRequest(request)
310
- }
311
-
312
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
313
- async ethCall(...params) {
314
- const request = this.ethCallRequest(...params)
315
- return this.sendRequest(request)
316
- }
317
-
318
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
319
- async blockNumber(...params) {
320
- const request = this.blockNumberRequest(...params)
321
- return this.sendRequest(request)
322
- }
323
-
324
- // Transport: Via getBlockByNumber → sendRequest → WS first → HTTP fallback in ClarityServerV2
325
- async getLatestBlock() {
326
- return this.getBlockByNumber('latest')
327
- }
328
-
329
- // Transport: Via getLatestBlock → getBlockByNumber → sendRequest → WS first → HTTP fallback in ClarityServerV2
330
- async getBaseFeePerGas() {
331
- const response = await this.getLatestBlock()
332
- if (response.baseFeePerGas) {
333
- return fromHexToString(response.baseFeePerGas)
334
- }
335
- }
336
-
337
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
338
- async getBlockByHash(...params) {
339
- const request = this.getBlockByHashRequest(...params)
340
- return this.sendRequest(request)
341
- }
342
-
343
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
344
- async getBlockTransactionCountByNumber(...params) {
345
- const request = this.getBlockTransactionCountByNumberRequest(...params)
346
- return this.sendRequest(request)
347
- }
348
-
349
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
350
- async getBlockByNumber(...params) {
351
- const request = this.getBlockByNumberRequest(...params)
352
- return this.sendRequest(request)
353
- }
354
-
355
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
356
- async simulateV1(...params) {
357
- const request = this.simulateV1Request(...params)
358
- return this.sendRequest(request)
359
- }
360
-
361
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
362
- async simulateRawTransaction(...params) {
363
- const request = this.simulateRawTransactionRequest(...params)
364
- return this.sendRequest(request)
365
- }
366
-
367
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
368
- async getCoinbase() {
369
- const request = this.coinbaseRequest()
370
- return this.sendRequest(request)
371
- }
372
-
373
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
374
- async getCompilers() {
375
- const request = this.getCompilersRequest()
376
- return this.sendRequest(request)
377
- }
378
-
379
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
380
- async getNetVersion() {
381
- const request = this.getNetVersionRequest()
382
- return this.sendRequest(request)
383
- }
384
-
385
- // Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
386
- async getLogs(...params) {
387
- const request = this.getLogsRequest(...params)
388
- return this.sendRequest(request)
389
- }
390
-
391
223
  formatTransactionsNamespace(address) {
392
224
  return `${this.baseNamespace}/addresses/${address}/transactions`
393
225
  }
@@ -0,0 +1,377 @@
1
+ import { bufferToHex } from '@exodus/ethereumjs/util'
2
+ import SolidityContract from '@exodus/solidity-contract'
3
+ import EventEmitter from 'events/events.js'
4
+ import assert from 'minimalistic-assert'
5
+
6
+ import { fromHexToString } from '../number-utils.js'
7
+ import { getFallbackGasPriceEstimation } from './utils.js'
8
+
9
+ export class EthLikeServerBase extends EventEmitter {
10
+ id = 0
11
+
12
+ buildRequest({ method, params = [] }) {
13
+ return { jsonrpc: '2.0', id: this.id++, method, params }
14
+ }
15
+
16
+ balanceOfRequest(address, tokenAddress, tag = 'latest') {
17
+ const contract = SolidityContract.simpleErc20(tokenAddress)
18
+ const callData = contract.balanceOf.build(address)
19
+ const data = {
20
+ data: bufferToHex(callData),
21
+ to: tokenAddress,
22
+ }
23
+ return this.ethCallRequest(data, tag)
24
+ }
25
+
26
+ getBalanceRequest(address, tag = 'latest') {
27
+ return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
28
+ }
29
+
30
+ gasPriceRequest() {
31
+ return this.buildRequest({ method: 'eth_gasPrice' })
32
+ }
33
+
34
+ estimateGasRequest(data, tag = 'latest') {
35
+ return this.buildRequest({ method: 'eth_estimateGas', params: [data, tag] })
36
+ }
37
+
38
+ sendRawTransactionRequest(data) {
39
+ const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
40
+ const hex = _data.startsWith('0x') ? _data : '0x' + _data
41
+ return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
42
+ }
43
+
44
+ // @deprecated
45
+ coinbaseRequest() {
46
+ return this.buildRequest({ method: 'eth_coinbase' })
47
+ }
48
+
49
+ getCodeRequest(address, tag = 'latest') {
50
+ return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
51
+ }
52
+
53
+ getStorageAtRequest(address, position, tag = 'latest') {
54
+ return this.buildRequest({ method: 'eth_getStorageAt', params: [address, position, tag] })
55
+ }
56
+
57
+ getTransactionCountRequest(address, tag = 'latest') {
58
+ return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
59
+ }
60
+
61
+ getTransactionByHashRequest(hash) {
62
+ return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
63
+ }
64
+
65
+ getTransactionReceiptRequest(txhash) {
66
+ return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
67
+ }
68
+
69
+ ethCallRequest(data, tag = 'latest') {
70
+ return this.buildRequest({ method: 'eth_call', params: [data, tag] })
71
+ }
72
+
73
+ blockNumberRequest() {
74
+ return this.buildRequest({ method: 'eth_blockNumber' })
75
+ }
76
+
77
+ getLogsRequest(object) {
78
+ return this.buildRequest({ method: 'eth_getLogs', params: [object] })
79
+ }
80
+
81
+ getBlockByNumberRequest(numberHex, isFullTxs = false) {
82
+ return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
83
+ }
84
+
85
+ getBlockByHashRequest(blockHash, isFullTxs = false) {
86
+ return this.buildRequest({ method: 'eth_getBlockByHash', params: [blockHash, isFullTxs] })
87
+ }
88
+
89
+ getBlockTransactionCountByHashRequest(blockHash) {
90
+ return this.buildRequest({ method: 'eth_getBlockTransactionCountByHash', params: [blockHash] })
91
+ }
92
+
93
+ getBlockTransactionCountByNumberRequest(quantityOrTag) {
94
+ return this.buildRequest({
95
+ method: 'eth_getBlockTransactionCountByNumber',
96
+ params: [quantityOrTag],
97
+ })
98
+ }
99
+
100
+ // @deprectated
101
+ getCompilersRequest() {
102
+ return this.buildRequest({ method: 'eth_getCompilers' })
103
+ }
104
+
105
+ // TODO: Expose on Clarity
106
+ // maxPriorityFeePerGasRequest() {
107
+ // return this.buildRequest({ method: 'eth_maxPriorityFeePerGas' })
108
+ // }
109
+
110
+ // TODO: Expose on Clarity
111
+ // feeHistoryRequest(blockCount, newestBlock, rewardPercentiles) {
112
+ // return this.buildRequest({
113
+ // method: 'eth_feeHistory',
114
+ // params: [blockCount, newestBlock, rewardPercentiles],
115
+ // })
116
+ // }
117
+
118
+ simulateV1Request(...params) {
119
+ return this.buildRequest({
120
+ method: 'eth_simulateV1',
121
+ params,
122
+ })
123
+ }
124
+
125
+ getNetVersionRequest() {
126
+ return this.buildRequest({ method: 'net_version' })
127
+ }
128
+
129
+ // @deprecated
130
+ simulateRawTransactionRequest(rawTx, applyPending = true) {
131
+ const replaced = rawTx.replace('0x', '')
132
+ return this.buildRequest({
133
+ method: 'debug_simulateRawTransaction',
134
+ params: [replaced, applyPending],
135
+ })
136
+ }
137
+
138
+ contractStaticCallFromRequest(contract, functionName, from, ...args) {
139
+ assert(contract instanceof SolidityContract, 'expected SolidityContract contract')
140
+ assert(typeof functionName === 'string', 'expected string functionName')
141
+
142
+ const requestProps = {
143
+ to: contract.address,
144
+ data: bufferToHex(contract[functionName].build(...args)),
145
+ }
146
+
147
+ if (from) requestProps.from = from
148
+
149
+ return this.ethCallRequest(requestProps)
150
+ }
151
+
152
+ sendBundleRequest({ txs }) {
153
+ assert(Array.isArray(txs), 'expected array txs')
154
+ return this.buildRequest({ method: 'eth_sendBundle', params: [{ txs }] })
155
+ }
156
+ }
157
+
158
+ const __MISSING_FUNCTION_IMPLEMENTATION__ = () => assert(false)
159
+
160
+ export class EthLikeServer extends EthLikeServerBase {
161
+ /**
162
+ * Validates a JSON-RPC batch response, returning each `result` ordered to
163
+ * match `batch` (correlated by `id`, since responses can arrive out of order).
164
+ *
165
+ * @param {Array<{ id: number|string }>} args.batch - Requests sent, in the order results are expected.
166
+ * @param {unknown} args.responses - Raw transport payload; expected `[{ id, result }]` but treated as untrusted (gateways may return `{ message }` on error).
167
+ * @param {string} args.errorMessage - Thrown-error prefix per transport (e.g. `'Bad rpc batch response'`, `'Bad Response'`).
168
+ * @returns {unknown[]} Each request's `result`, ordered to match `batch`.
169
+ * @throws {Error} When `responses` isn't an array, length mismatches `batch`, or any entry lacks a numeric `id` / defined `result`.
170
+ */
171
+ parseBatchResponse({ batch, responses, errorMessage }) {
172
+ // Gateways can respond with a JSON object (e.g. `{ message }`) instead of a batch array.
173
+ if (!Array.isArray(responses)) {
174
+ // Untrusted server text: keep only a short string preview, developer-facing (no `hint`).
175
+ const message = [responses?.message, responses?.error?.message].find(
176
+ (value) => typeof value === 'string'
177
+ )
178
+ const preview = message ? `: ${message.slice(0, 100)}` : ''
179
+ throw new Error(`${errorMessage}${preview}`)
180
+ }
181
+
182
+ const isValid = responses.every(
183
+ (response) => !isNaN(response?.id) && response?.result !== undefined
184
+ )
185
+ if (responses.length !== batch.length || !isValid) {
186
+ throw new Error(errorMessage)
187
+ }
188
+
189
+ // Safe against prototype keys: every `id` is validated numeric by the `isValid` check above.
190
+ const keyed = Object.fromEntries(
191
+ responses.map((response) => [`${response.id}`, response.result])
192
+ )
193
+ return batch.map((request) => keyed[`${request.id}`])
194
+ }
195
+
196
+ async getCode(...params) {
197
+ const request = this.getCodeRequest(...params)
198
+ return this.sendRequest(request)
199
+ }
200
+
201
+ async estimateGas(...params) {
202
+ const request = this.estimateGasRequest(...params)
203
+ return this.sendRequest(request)
204
+ }
205
+
206
+ async sendRawTransaction(...params) {
207
+ const request = this.sendRawTransactionRequest(...params)
208
+ return this.sendRequest(request)
209
+ }
210
+
211
+ async getStorageAt(...params) {
212
+ const request = this.getStorageAtRequest(...params)
213
+ return this.sendRequest(request)
214
+ }
215
+
216
+ async getTransactionCount(...params) {
217
+ const request = this.getTransactionCountRequest(...params)
218
+ return this.sendRequest(request)
219
+ }
220
+
221
+ async getTransactionByHash(...params) {
222
+ const request = this.getTransactionByHashRequest(...params)
223
+ return this.sendRequest(request)
224
+ }
225
+
226
+ async getTransactionReceipt(...params) {
227
+ const request = this.getTransactionReceiptRequest(...params)
228
+ return this.sendRequest(request)
229
+ }
230
+
231
+ async ethCall(...params) {
232
+ const request = this.ethCallRequest(...params)
233
+ return this.sendRequest(request)
234
+ }
235
+
236
+ async blockNumber(...params) {
237
+ const request = this.blockNumberRequest(...params)
238
+ return this.sendRequest(request)
239
+ }
240
+
241
+ async getBlockByNumber(...params) {
242
+ const request = this.getBlockByNumberRequest(...params)
243
+ return this.sendRequest(request)
244
+ }
245
+
246
+ async isContract(address) {
247
+ const code = await this.getCode(address)
248
+ return code.length > 2
249
+ }
250
+
251
+ // @deprecated
252
+ async simulateRawTransaction(...params) {
253
+ const request = this.simulateRawTransactionRequest(...params)
254
+ return this.sendRequest(request)
255
+ }
256
+
257
+ async getBaseFeePerGas() {
258
+ const response = await this.getLatestBlock()
259
+ if (response.baseFeePerGas) {
260
+ return fromHexToString(response.baseFeePerGas)
261
+ }
262
+ }
263
+
264
+ async getLatestBlock() {
265
+ return this.getBlockByNumber('latest')
266
+ }
267
+
268
+ async gasPrice(...params) {
269
+ const request = this.gasPriceRequest(...params)
270
+ return this.sendRequest(request)
271
+ }
272
+
273
+ // for fee monitor
274
+ async getGasPrice() {
275
+ return this.gasPrice()
276
+ }
277
+
278
+ async getBalance(...params) {
279
+ const request = this.getBalanceRequest(...params)
280
+ return this.sendRequest(request)
281
+ }
282
+
283
+ async getBalanceProxied(...params) {
284
+ return this.getBalance(...params)
285
+ }
286
+
287
+ async getBlockByHash(...params) {
288
+ const request = this.getBlockByHashRequest(...params)
289
+ return this.sendRequest(request)
290
+ }
291
+
292
+ async getBlockTransactionCountByNumber(...params) {
293
+ const request = this.getBlockTransactionCountByNumberRequest(...params)
294
+ return this.sendRequest(request)
295
+ }
296
+
297
+ async getBlockTransactionCountByHash(...params) {
298
+ const request = this.getBlockTransactionCountByHashRequest(...params)
299
+ return this.sendRequest(request)
300
+ }
301
+
302
+ async simulateV1(...params) {
303
+ const request = this.simulateV1Request(...params)
304
+ return this.sendRequest(request)
305
+ }
306
+
307
+ // @deprecated
308
+ async getCoinbase() {
309
+ const request = this.coinbaseRequest()
310
+ return this.sendRequest(request)
311
+ }
312
+
313
+ // @deprecated
314
+ async getCompilers() {
315
+ const request = this.getCompilersRequest()
316
+ return this.sendRequest(request)
317
+ }
318
+
319
+ // TODO: Expose on Clarity
320
+ // async getMaxPriorityFeePerGas() {
321
+ // const request = this.maxPriorityFeePerGasRequest()
322
+ // return this.sendRequest(request)
323
+ // }
324
+
325
+ // TODO: Expose on Clarity
326
+ // async getFeeHistory(...params) {
327
+ // const request = this.feeHistoryRequest(...params)
328
+ // return this.sendRequest(request)
329
+ // }
330
+
331
+ async getNetVersion() {
332
+ const request = this.getNetVersionRequest()
333
+ return this.sendRequest(request)
334
+ }
335
+
336
+ async getLogs(...params) {
337
+ const request = this.getLogsRequest(...params)
338
+ return this.sendRequest(request)
339
+ }
340
+
341
+ async proxyToCoinNode(params) {
342
+ const request = this.buildRequest(params)
343
+ return this.sendRequest(request)
344
+ }
345
+
346
+ async getGasPriceEstimation() {
347
+ return getFallbackGasPriceEstimation({ server: this })
348
+ }
349
+
350
+ async balanceOf(address, tokenAddress, tag = 'latest') {
351
+ const request = this.balanceOfRequest(address, tokenAddress, tag)
352
+ const result = await this.sendRequest(request)
353
+ const balance = fromHexToString(result)
354
+ return {
355
+ confirmed: {
356
+ [tokenAddress]: balance,
357
+ },
358
+ }
359
+ }
360
+
361
+ async contractStaticCallFrom(contract, functionName, ...params) {
362
+ const request = this.contractStaticCallFromRequest(contract, functionName, ...params)
363
+ return contract[functionName].parse(await this.sendRequest(request))
364
+ }
365
+
366
+ async contractStaticCall(contract, functionName, ...params) {
367
+ return this.contractStaticCallFrom(contract, functionName, undefined, ...params)
368
+ }
369
+
370
+ sendRequest() {
371
+ return __MISSING_FUNCTION_IMPLEMENTATION__()
372
+ }
373
+
374
+ sendBatchRequest() {
375
+ return __MISSING_FUNCTION_IMPLEMENTATION__()
376
+ }
377
+ }
@@ -4,7 +4,7 @@ import { bufferToHex } from '@exodus/ethereumjs/util'
4
4
  import SolidityContract from '@exodus/solidity-contract'
5
5
  import assert from 'minimalistic-assert'
6
6
 
7
- import EthLikeServerBase from '../exodus-eth-server/eth-like-server-base.js'
7
+ import { EthLikeServerBase } from '../exodus-eth-server/eth-like-server.js'
8
8
  import { BLOCK_TAG_LATEST } from '../tx-send/nonce-utils.js'
9
9
 
10
10
  const assertValidMulticall3BlockTag = (blockTag) =>
@@ -73,6 +73,11 @@ export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
73
73
  assertValidMulticall3BlockTag(tag)
74
74
  }
75
75
 
76
+ if (method === 'eth_call') {
77
+ const { from } = params[0]
78
+ assert(!from, 'cannot specify "from"')
79
+ }
80
+
76
81
  void this.#requests.push({
77
82
  originalRequest,
78
83
  request: this._serializeRequest({ originalRequest }),
@@ -103,19 +108,17 @@ export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
103
108
  )(...args)
104
109
  }
105
110
 
106
- contractStaticCallRequest(contract, functionName, ...args) {
107
- assert(contract instanceof SolidityContract, 'expected SolidityContract contract')
108
- assert(typeof functionName === 'string', 'expected string functionName')
109
-
111
+ contractStaticCallFromRequest(contract, functionName, ...args) {
110
112
  const requestParser = ({ result }) => contract[functionName].parse(result)
113
+ return this.#withRequestParser('contractStaticCallFromRequest', requestParser)(
114
+ contract,
115
+ functionName,
116
+ ...args
117
+ )
118
+ }
111
119
 
112
- return this.#withRequestParser(
113
- 'ethCallRequest',
114
- requestParser
115
- )({
116
- to: contract.address,
117
- data: bufferToHex(contract[functionName].build(...args)),
118
- })
120
+ contractStaticCallRequest(contract, functionName, ...args) {
121
+ return this.contractStaticCallFromRequest(contract, functionName, undefined, ...args)
119
122
  }
120
123
 
121
124
  length() {
@@ -39,8 +39,8 @@ const createBalanceAmount = (amount, { decimals, symbol }) => {
39
39
  return beforeAmount.sub(afterAmount)
40
40
  }
41
41
 
42
- const handleExpectedBalanceChange = (expectedBalanceChange, simulationResult, { network }) => {
43
- const { willApprove, willSend, willReceive } = simulationResult.balanceChanges
42
+ const handleExpectedBalanceChange = (expectedBalanceChange, balanceChanges, { network }) => {
43
+ const { willApprove, willSend, willReceive } = balanceChanges
44
44
  const { data, kind } = expectedBalanceChange.rawInfo
45
45
 
46
46
  if (kind === 'FARCASTER_CHANGE_RECOVERY_ADDRESS' || kind === 'ERC20_PERMIT') {
@@ -49,6 +49,10 @@ const handleExpectedBalanceChange = (expectedBalanceChange, simulationResult, {
49
49
 
50
50
  const { asset: simulationAsset } = data
51
51
 
52
+ if (!simulationAsset) {
53
+ return
54
+ }
55
+
52
56
  const isNftTransfer =
53
57
  kind === 'ERC721_TRANSFER' || (kind === 'ERC1155_TRANSFER' && !simulationAsset.decimals)
54
58
  const isNftTransferApproval =
@@ -132,17 +136,6 @@ const handleExpectedBalanceChange = (expectedBalanceChange, simulationResult, {
132
136
  }
133
137
  }
134
138
 
135
- const simulationResultHasAssets = (expectedStateChanges) => {
136
- for (const expectedChange of expectedStateChanges) {
137
- const { asset: simulationAsset } = expectedChange.rawInfo.data
138
- if (!simulationAsset) {
139
- return false
140
- }
141
- }
142
-
143
- return true
144
- }
145
-
146
139
  export const SimulationErrorMessages = {
147
140
  APICallFailed: 0,
148
141
  SimulationFailed: 1,
@@ -230,7 +223,25 @@ export const simulateTransactionsApi = async ({
230
223
  })
231
224
  }
232
225
 
233
- const noExpectedStateChangesDetected = Object.keys(expectedStateChanges).length === 0
226
+ const userAddressLower = userAddress.toLowerCase()
227
+ const recipientAddressLower =
228
+ recipientAddress && recipientAddress.toLowerCase() !== userAddressLower
229
+ ? recipientAddress.toLowerCase()
230
+ : undefined
231
+
232
+ const relevantExpectedChanges = Object.entries(expectedStateChanges).flatMap(
233
+ ([address, changes]) => {
234
+ const addressLower = address.toLowerCase()
235
+ const isUserOwned = addressLower === userAddressLower
236
+ if (!isUserOwned && addressLower !== recipientAddressLower) {
237
+ return []
238
+ }
239
+
240
+ return (changes ?? []).map((change) => ({ change, isUserOwned }))
241
+ }
242
+ )
243
+
244
+ const noExpectedStateChangesDetected = relevantExpectedChanges.length === 0
234
245
 
235
246
  if (noExpectedStateChangesDetected) {
236
247
  simulationResult.balanceChanges.willSend.push({
@@ -244,22 +255,11 @@ export const simulateTransactionsApi = async ({
244
255
  return successDefaultResult
245
256
  }
246
257
 
247
- const addressesToProcess = [userAddress.toLowerCase()]
248
- if (recipientAddress && recipientAddress.toLowerCase() !== userAddress.toLowerCase()) {
249
- addressesToProcess.push(recipientAddress.toLowerCase())
250
- }
251
-
252
- const relevantExpectedChanges = Object.entries(expectedStateChanges).flatMap(
253
- ([address, changes]) =>
254
- addressesToProcess.includes(address.toLowerCase()) ? changes ?? [] : []
255
- )
256
-
257
- if (!simulationResultHasAssets(relevantExpectedChanges)) {
258
- return successDefaultResult
259
- }
260
-
261
- relevantExpectedChanges.forEach((expectedChange) => {
262
- handleExpectedBalanceChange(expectedChange, simulationResult, { network })
258
+ relevantExpectedChanges.forEach(({ change, isUserOwned }) => {
259
+ const balanceChanges = isUserOwned
260
+ ? simulationResult.balanceChanges
261
+ : simulationResult.recipientBalanceChanges
262
+ handleExpectedBalanceChange(change, balanceChanges, { network })
263
263
  })
264
264
 
265
265
  return successDefaultResult
@@ -3,6 +3,7 @@ import { getAssetAddresses } from '@exodus/ethereum-lib'
3
3
  import lodash from 'lodash'
4
4
  import assert from 'minimalistic-assert'
5
5
 
6
+ import { applyRemoteConfigEip7702Whitelist, createEip7702Whitelist } from '../eip7702-whitelist.js'
6
7
  import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
7
8
  import {
8
9
  filterEffects,
@@ -29,6 +30,7 @@ export class ClarityMonitor extends BaseMonitor {
29
30
  config,
30
31
  rpcBalanceAssetNames,
31
32
  eip7702Supported,
33
+ eip7702Whitelist,
32
34
  getBlackListStatus,
33
35
  ...args
34
36
  }) {
@@ -36,7 +38,7 @@ export class ClarityMonitor extends BaseMonitor {
36
38
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
37
39
  this.server = server
38
40
  this.rpcBalanceAssetNames = rpcBalanceAssetNames
39
- this.eip7702Supported = eip7702Supported
41
+ this.eip7702Whitelist = eip7702Whitelist ?? createEip7702Whitelist(eip7702Supported)
40
42
  this.getBlackListStatus = getBlackListStatus
41
43
  this.getAllLogItemsByAsset = getAllLogItemsByAsset
42
44
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
@@ -48,6 +50,12 @@ export class ClarityMonitor extends BaseMonitor {
48
50
  }
49
51
 
50
52
  setServer(config) {
53
+ applyRemoteConfigEip7702Whitelist({
54
+ eip7702Whitelist: this.eip7702Whitelist,
55
+ remoteConfig: config,
56
+ logger: this.logger,
57
+ })
58
+
51
59
  const uri = config?.server || this.server.defaultUri
52
60
 
53
61
  if (uri === this.server.uri) {
@@ -294,7 +302,7 @@ export class ClarityMonitor extends BaseMonitor {
294
302
  const eip7702Delegation = await getCurrentEIP7702Delegation({
295
303
  server: this.server,
296
304
  address: derivedData.ourWalletAddress,
297
- eip7702Supported: this.eip7702Supported,
305
+ eip7702Supported: this.eip7702Whitelist.get(),
298
306
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
299
307
  logger: this.logger,
300
308
  })
@@ -32,7 +32,7 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
32
32
  const eip7702Delegation = await getCurrentEIP7702Delegation({
33
33
  server: this.server,
34
34
  address: derivedData.ourWalletAddress,
35
- eip7702Supported: this.eip7702Supported,
35
+ eip7702Supported: this.eip7702Whitelist.get(),
36
36
  currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
37
37
  logger: this.logger,
38
38
  })
@@ -3,6 +3,7 @@ import { SynchronizedTime } from '@exodus/basic-utils'
3
3
  import { Tx } from '@exodus/models'
4
4
  import lodash from 'lodash'
5
5
 
6
+ import { applyRemoteConfigEip7702Whitelist, createEip7702Whitelist } from '../eip7702-whitelist.js'
6
7
  import { getBatchedRpcBalances } from './monitor-utils/get-batched-rpc-balances.js'
7
8
  import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check.js'
8
9
  import {
@@ -18,11 +19,11 @@ const { isEmpty, unionBy, zipObject } = lodash
18
19
  // The base ethereum monitor no history class handles listening for assets with no history
19
20
 
20
21
  export class EthereumNoHistoryMonitor extends BaseMonitor {
21
- constructor({ server, config, eip7702Supported, getBlackListStatus, ...args }) {
22
+ constructor({ server, config, eip7702Supported, eip7702Whitelist, getBlackListStatus, ...args }) {
22
23
  super(args)
23
24
  this.server = server
24
25
  this.config = { ...config }
25
- this.eip7702Supported = eip7702Supported
26
+ this.eip7702Whitelist = eip7702Whitelist ?? createEip7702Whitelist(eip7702Supported)
26
27
  this.getBlackListStatus = getBlackListStatus
27
28
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
28
29
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -31,6 +32,12 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
31
32
  }
32
33
 
33
34
  setServer(config) {
35
+ applyRemoteConfigEip7702Whitelist({
36
+ eip7702Whitelist: this.eip7702Whitelist,
37
+ remoteConfig: config,
38
+ logger: this.logger,
39
+ })
40
+
34
41
  const uri = config?.server || this.server.defaultUri
35
42
 
36
43
  if (uri === this.server.uri) {
@@ -173,7 +180,7 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
173
180
  const eip7702Delegation = await getCurrentEIP7702Delegation({
174
181
  server: this.server,
175
182
  address: ourWalletAddress,
176
- eip7702Supported: this.eip7702Supported,
183
+ eip7702Supported: this.eip7702Whitelist.get(),
177
184
  currentDelegation: currentAccountState?.eip7702Delegation,
178
185
  logger: this.logger,
179
186
  })
@@ -1,158 +0,0 @@
1
- import { bufferToHex } from '@exodus/ethereumjs/util'
2
- import SolidityContract from '@exodus/solidity-contract'
3
- import EventEmitter from 'events/events.js'
4
- import assert from 'minimalistic-assert'
5
-
6
- export default class EthLikeServerBase extends EventEmitter {
7
- id = 0
8
-
9
- buildRequest({ method, params = [] }) {
10
- return { jsonrpc: '2.0', id: this.id++, method, params }
11
- }
12
-
13
- balanceOfRequest(address, tokenAddress, tag = 'latest') {
14
- const contract = SolidityContract.simpleErc20(tokenAddress)
15
- const callData = contract.balanceOf.build(address)
16
- const data = {
17
- data: bufferToHex(callData),
18
- to: tokenAddress,
19
- }
20
- return this.ethCallRequest(data, tag)
21
- }
22
-
23
- getBalanceRequest(address, tag = 'latest') {
24
- return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
25
- }
26
-
27
- gasPriceRequest() {
28
- return this.buildRequest({ method: 'eth_gasPrice' })
29
- }
30
-
31
- estimateGasRequest(data, tag = 'latest') {
32
- return this.buildRequest({ method: 'eth_estimateGas', params: [data, tag] })
33
- }
34
-
35
- sendRawTransactionRequest(data) {
36
- const _data = data instanceof Uint8Array ? Buffer.from(data).toString('hex') : data
37
- const hex = _data.startsWith('0x') ? _data : '0x' + _data
38
- return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
39
- }
40
-
41
- coinbaseRequest() {
42
- return this.buildRequest({ method: 'eth_coinbase' })
43
- }
44
-
45
- getCodeRequest(address, tag = 'latest') {
46
- return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
47
- }
48
-
49
- getStorageAtRequest(address, position, tag = 'latest') {
50
- return this.buildRequest({ method: 'eth_getStorageAt', params: [address, position, tag] })
51
- }
52
-
53
- getTransactionCountRequest(address, tag = 'latest') {
54
- return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
55
- }
56
-
57
- getTransactionByHashRequest(hash) {
58
- return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
59
- }
60
-
61
- getTransactionReceiptRequest(txhash) {
62
- return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
63
- }
64
-
65
- ethCallRequest(data, tag = 'latest') {
66
- return this.buildRequest({ method: 'eth_call', params: [data, tag] })
67
- }
68
-
69
- blockNumberRequest() {
70
- return this.buildRequest({ method: 'eth_blockNumber' })
71
- }
72
-
73
- getLogsRequest(object) {
74
- return this.buildRequest({ method: 'eth_getLogs', params: [object] })
75
- }
76
-
77
- getBlockByNumberRequest(numberHex, isFullTxs = false) {
78
- return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
79
- }
80
-
81
- getBlockByHashRequest(blockHash, isFullTxs = false) {
82
- return this.buildRequest({ method: 'eth_getBlockByHash', params: [blockHash, isFullTxs] })
83
- }
84
-
85
- getBlockTransactionCountByHashRequest(blockHash) {
86
- return this.buildRequest({ method: 'eth_getBlockTransactionCountByHash', params: [blockHash] })
87
- }
88
-
89
- getBlockTransactionCountByNumberRequest(quantityOrTag) {
90
- return this.buildRequest({
91
- method: 'eth_getBlockTransactionCountByNumber',
92
- params: [quantityOrTag],
93
- })
94
- }
95
-
96
- getCompilersRequest() {
97
- return this.buildRequest({ method: 'eth_getCompilers' })
98
- }
99
-
100
- simulateV1Request(...params) {
101
- return this.buildRequest({
102
- method: 'eth_simulateV1',
103
- params,
104
- })
105
- }
106
-
107
- getNetVersionRequest() {
108
- return this.buildRequest({ method: 'net_version' })
109
- }
110
-
111
- simulateRawTransactionRequest(rawTx, applyPending = true) {
112
- const replaced = rawTx.replace('0x', '')
113
- return this.buildRequest({
114
- method: 'debug_simulateRawTransaction',
115
- params: [replaced, applyPending],
116
- })
117
- }
118
-
119
- sendBundleRequest({ txs }) {
120
- assert(Array.isArray(txs), 'expected array txs')
121
- return this.buildRequest({ method: 'eth_sendBundle', params: [{ txs }] })
122
- }
123
-
124
- /**
125
- * Validates a JSON-RPC batch response, returning each `result` ordered to
126
- * match `batch` (correlated by `id`, since responses can arrive out of order).
127
- *
128
- * @param {Array<{ id: number|string }>} args.batch - Requests sent, in the order results are expected.
129
- * @param {unknown} args.responses - Raw transport payload; expected `[{ id, result }]` but treated as untrusted (gateways may return `{ message }` on error).
130
- * @param {string} args.errorMessage - Thrown-error prefix per transport (e.g. `'Bad rpc batch response'`, `'Bad Response'`).
131
- * @returns {unknown[]} Each request's `result`, ordered to match `batch`.
132
- * @throws {Error} When `responses` isn't an array, length mismatches `batch`, or any entry lacks a numeric `id` / defined `result`.
133
- */
134
- parseBatchResponse({ batch, responses, errorMessage }) {
135
- // Gateways can respond with a JSON object (e.g. `{ message }`) instead of a batch array.
136
- if (!Array.isArray(responses)) {
137
- // Untrusted server text: keep only a short string preview, developer-facing (no `hint`).
138
- const message = [responses?.message, responses?.error?.message].find(
139
- (value) => typeof value === 'string'
140
- )
141
- const preview = message ? `: ${message.slice(0, 100)}` : ''
142
- throw new Error(`${errorMessage}${preview}`)
143
- }
144
-
145
- const isValid = responses.every(
146
- (response) => !isNaN(response?.id) && response?.result !== undefined
147
- )
148
- if (responses.length !== batch.length || !isValid) {
149
- throw new Error(errorMessage)
150
- }
151
-
152
- // Safe against prototype keys: every `id` is validated numeric by the `isValid` check above.
153
- const keyed = Object.fromEntries(
154
- responses.map((response) => [`${response.id}`, response.result])
155
- )
156
- return batch.map((request) => keyed[`${request.id}`])
157
- }
158
- }