@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 +30 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +33 -0
- package/src/create-asset.js +6 -6
- package/src/exodus-eth-server/clarity-v2.js +7 -4
- package/src/exodus-eth-server/clarity.js +8 -1
- package/src/get-fee.js +4 -3
- package/src/tx-log/clarity-monitor-v2.js +14 -1
- package/src/tx-log/clarity-monitor.js +24 -2
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +5 -0
- package/src/tx-log/ethereum-no-history-monitor.js +14 -2
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +30 -20
- package/src/tx-log/monitor-utils/index.js +1 -0
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.
|
|
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": "
|
|
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')
|
package/src/create-asset.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
...(
|
|
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({
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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 = {
|
|
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
|
|
8
|
-
* On error, conservatively returns
|
|
9
|
-
*
|
|
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
|
|
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))
|
|
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)
|
|
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
|
|
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
|
-
|
|
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'
|