@exodus/ethereum-api 8.77.3 → 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 CHANGED
@@ -3,6 +3,16 @@
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
+
6
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)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.77.3",
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": "ccdff56c057e6c4892b304fa47ecfc6c4389f629"
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 }] })
@@ -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