@exodus/ethereum-api 8.77.2 → 8.77.4
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 +19 -0
- package/package.json +2 -2
- package/src/exodus-eth-server/api-coin-nodes.js +9 -0
- package/src/exodus-eth-server/clarity.js +11 -0
- package/src/exodus-eth-server/eth-like-server-base.js +14 -0
- package/src/multicall3/index.js +14 -11
- package/src/simulation/simulate-transactions-api.js +30 -30
- package/src/staking/ethereum/staking-utils.js +4 -1
- package/src/tx-log/clarity-monitor.js +6 -27
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +29 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.77.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.4) (2026-07-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
|
|
12
|
+
* **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))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.77.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.3) (2026-07-11)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [8.77.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.2) (2026-07-10)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @exodus/ethereum-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.77.
|
|
3
|
+
"version": "8.77.4",
|
|
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": "480ce44451fc531981074b3062ccc7df72735aa9"
|
|
73
73
|
}
|
|
@@ -162,6 +162,15 @@ export default class ApiCoinNodesServer extends EthLikeServerBase {
|
|
|
162
162
|
return this.sendRequest(request)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
async contractStaticCallFrom(contract, functionName, ...params) {
|
|
166
|
+
const request = this.contractStaticCallFromRequest(contract, functionName, ...params)
|
|
167
|
+
return contract[functionName].parse(await this.sendRequest(request))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async contractStaticCall(contract, functionName, ...params) {
|
|
171
|
+
return this.contractStaticCallFrom(contract, functionName, undefined, ...params)
|
|
172
|
+
}
|
|
173
|
+
|
|
165
174
|
stop() {
|
|
166
175
|
// no web socket to stop!
|
|
167
176
|
}
|
|
@@ -364,6 +364,17 @@ export default class ClarityServer extends EthLikeServerBase {
|
|
|
364
364
|
return this.sendRequest(request)
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
368
|
+
async contractStaticCallFrom(contract, functionName, ...params) {
|
|
369
|
+
const request = this.contractStaticCallFromRequest(contract, functionName, ...params)
|
|
370
|
+
return contract[functionName].parse(await this.sendRequest(request))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
374
|
+
async contractStaticCall(contract, functionName, ...params) {
|
|
375
|
+
return this.contractStaticCallFrom(contract, functionName, undefined, ...params)
|
|
376
|
+
}
|
|
377
|
+
|
|
367
378
|
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
368
379
|
async getCoinbase() {
|
|
369
380
|
const request = this.coinbaseRequest()
|
|
@@ -116,6 +116,20 @@ export default class EthLikeServerBase extends EventEmitter {
|
|
|
116
116
|
})
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
contractStaticCallFromRequest(contract, functionName, from, ...args) {
|
|
120
|
+
assert(contract instanceof SolidityContract, 'expected SolidityContract contract')
|
|
121
|
+
assert(typeof functionName === 'string', 'expected string functionName')
|
|
122
|
+
|
|
123
|
+
const requestProps = {
|
|
124
|
+
to: contract.address,
|
|
125
|
+
data: bufferToHex(contract[functionName].build(...args)),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (from) requestProps.from = from
|
|
129
|
+
|
|
130
|
+
return this.ethCallRequest(requestProps)
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
sendBundleRequest({ txs }) {
|
|
120
134
|
assert(Array.isArray(txs), 'expected array txs')
|
|
121
135
|
return this.buildRequest({ method: 'eth_sendBundle', params: [{ txs }] })
|
package/src/multicall3/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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,
|
|
43
|
-
const { willApprove, willSend, willReceive } =
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
@@ -23,7 +23,10 @@ export const isEthereumStakingTx = ({ coinName }) =>
|
|
|
23
23
|
export const isEthereumStakingPoolContract = (address) =>
|
|
24
24
|
typeof address === 'string' && STAKING_MANAGER_CONTRACTS.has(address.toLowerCase())
|
|
25
25
|
export const isEthereumDelegate = (tx) =>
|
|
26
|
-
isEthereumStakingTx(tx) &&
|
|
26
|
+
isEthereumStakingTx(tx) &&
|
|
27
|
+
((STAKING_MANAGER_CONTRACTS.has(tx.to) && tx.data?.methodId === DELEGATE) ||
|
|
28
|
+
tx.data?.delegate === true ||
|
|
29
|
+
typeof tx.data?.delegate === 'string')
|
|
27
30
|
export const isEthereumUndelegatePending = (tx) =>
|
|
28
31
|
isEthereumStakingTx(tx) && tx.data?.methodId === UNSTAKE_PENDING
|
|
29
32
|
export const isEthereumUndelegate = (tx) =>
|
|
@@ -4,12 +4,12 @@ import lodash from 'lodash'
|
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
|
|
6
6
|
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
7
|
-
import { fromHexToString } from '../number-utils.js'
|
|
8
7
|
import {
|
|
9
8
|
filterEffects,
|
|
10
9
|
getLogItemsFromServerTx,
|
|
11
10
|
normalizeTransactionsResponse,
|
|
12
11
|
} from './clarity-utils/index.js'
|
|
12
|
+
import { getBatchedRpcBalances } from './monitor-utils/get-batched-rpc-balances.js'
|
|
13
13
|
import {
|
|
14
14
|
checkPendingTransactions,
|
|
15
15
|
excludeUnchangedTokenBalances,
|
|
@@ -364,33 +364,12 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
async getBalances({ tokens, ourWalletAddress }) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (const token of tokens) {
|
|
374
|
-
if (this.rpcBalanceAssetNames.includes(token.name) && token.contract.address) {
|
|
375
|
-
const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
|
|
376
|
-
batch[token.name] = request
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const pairs = Object.entries(batch)
|
|
381
|
-
if (pairs.length === 0) {
|
|
382
|
-
return {}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const requests = pairs.map((pair) => pair[1])
|
|
386
|
-
const responses = await this.server.sendBatchRequest(requests)
|
|
387
|
-
const entries = pairs.map((pair, idx) => {
|
|
388
|
-
const balanceHex = responses[idx]
|
|
389
|
-
const name = pair[0]
|
|
390
|
-
const balance = fromHexToString(balanceHex)
|
|
391
|
-
return [name, balance]
|
|
367
|
+
return getBatchedRpcBalances({
|
|
368
|
+
baseAsset: this.asset,
|
|
369
|
+
ourWalletAddress,
|
|
370
|
+
tokens,
|
|
371
|
+
rpcBalanceAssetNames: this.rpcBalanceAssetNames,
|
|
392
372
|
})
|
|
393
|
-
return Object.fromEntries(entries)
|
|
394
373
|
}
|
|
395
374
|
|
|
396
375
|
getUnknownTokenAddresses({ transactions, tokensByAddress }) {
|
|
@@ -2,27 +2,46 @@ import assert from 'minimalistic-assert'
|
|
|
2
2
|
|
|
3
3
|
import { fromHexToString } from '../../number-utils.js'
|
|
4
4
|
|
|
5
|
-
export const getBatchedRpcBalances = async ({
|
|
5
|
+
export const getBatchedRpcBalances = async ({
|
|
6
|
+
baseAsset,
|
|
7
|
+
tokens,
|
|
8
|
+
ourWalletAddress,
|
|
9
|
+
rpcBalanceAssetNames,
|
|
10
|
+
}) => {
|
|
6
11
|
assert(baseAsset, 'expected baseAsset')
|
|
7
12
|
assert(Array.isArray(tokens), 'expected array tokens')
|
|
8
13
|
assert(ourWalletAddress, 'expected ourWalletAddress')
|
|
9
14
|
|
|
10
|
-
const
|
|
15
|
+
const batch = (assetName) =>
|
|
16
|
+
!Array.isArray(rpcBalanceAssetNames) || rpcBalanceAssetNames.includes(assetName)
|
|
17
|
+
|
|
18
|
+
const isBaseAssetBatched = batch(baseAsset.name)
|
|
19
|
+
|
|
20
|
+
const rpcRequestAccumulator = tokens
|
|
21
|
+
.filter(({ name, contract }) => batch(name) && contract?.address)
|
|
11
22
|
.reduce(
|
|
12
23
|
(acc, { contract: { address: tokenAddress } }) =>
|
|
13
24
|
acc.balanceOfRequest(ourWalletAddress, tokenAddress),
|
|
14
|
-
|
|
25
|
+
isBaseAssetBatched
|
|
26
|
+
? baseAsset.createRpcRequestAccumulator().getBalanceRequest(ourWalletAddress)
|
|
27
|
+
: baseAsset.createRpcRequestAccumulator()
|
|
15
28
|
)
|
|
16
|
-
|
|
29
|
+
|
|
30
|
+
if (rpcRequestAccumulator.length() === 0) return Object.create(null)
|
|
31
|
+
|
|
32
|
+
const results = await rpcRequestAccumulator.flush(baseAsset.server)
|
|
17
33
|
|
|
18
34
|
return {
|
|
19
|
-
[baseAsset.name]: fromHexToString(
|
|
35
|
+
...(isBaseAssetBatched && { [baseAsset.name]: fromHexToString(results[0]) }),
|
|
20
36
|
...Object.fromEntries(
|
|
21
|
-
tokens
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
tokens
|
|
38
|
+
.filter(({ name, contract }) => batch(name) && contract?.address)
|
|
39
|
+
.map(({ name, contract: { address: tokenAddress } }, i) => {
|
|
40
|
+
const tokenBalance = results[i + (isBaseAssetBatched ? 1 : 0)]?.confirmed?.[tokenAddress]
|
|
41
|
+
|
|
42
|
+
assert(tokenBalance != null, `missing token balance for ${name} (${tokenAddress})`)
|
|
43
|
+
return [name, tokenBalance]
|
|
44
|
+
})
|
|
26
45
|
),
|
|
27
46
|
}
|
|
28
47
|
}
|