viem 2.55.13 → 2.55.16

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/_cjs/actions/index.js +4 -2
  3. package/_cjs/actions/index.js.map +1 -1
  4. package/_cjs/actions/public/simulateCalls.js +213 -142
  5. package/_cjs/actions/public/simulateCalls.js.map +1 -1
  6. package/_cjs/actions/public/watchBlockHeaders.js +61 -0
  7. package/_cjs/actions/public/watchBlockHeaders.js.map +1 -0
  8. package/_cjs/clients/decorators/public.js +2 -0
  9. package/_cjs/clients/decorators/public.js.map +1 -1
  10. package/_cjs/errors/version.js +1 -1
  11. package/_cjs/index.js.map +1 -1
  12. package/_cjs/tempo/actions/earn.js +2 -0
  13. package/_cjs/tempo/actions/earn.js.map +1 -1
  14. package/_cjs/tempo/actions/zone.js +18 -8
  15. package/_cjs/tempo/actions/zone.js.map +1 -1
  16. package/_cjs/utils/siwe/parseSiweMessage.js +16 -3
  17. package/_cjs/utils/siwe/parseSiweMessage.js.map +1 -1
  18. package/_cjs/utils/siwe/validateSiweMessage.js +13 -3
  19. package/_cjs/utils/siwe/validateSiweMessage.js.map +1 -1
  20. package/_esm/actions/index.js +1 -0
  21. package/_esm/actions/index.js.map +1 -1
  22. package/_esm/actions/public/simulateCalls.js +238 -150
  23. package/_esm/actions/public/simulateCalls.js.map +1 -1
  24. package/_esm/actions/public/watchBlockHeaders.js +81 -0
  25. package/_esm/actions/public/watchBlockHeaders.js.map +1 -0
  26. package/_esm/clients/decorators/public.js +2 -0
  27. package/_esm/clients/decorators/public.js.map +1 -1
  28. package/_esm/errors/version.js +1 -1
  29. package/_esm/index.js.map +1 -1
  30. package/_esm/tempo/actions/earn.js +2 -0
  31. package/_esm/tempo/actions/earn.js.map +1 -1
  32. package/_esm/tempo/actions/zone.js +20 -8
  33. package/_esm/tempo/actions/zone.js.map +1 -1
  34. package/_esm/utils/siwe/parseSiweMessage.js +18 -3
  35. package/_esm/utils/siwe/parseSiweMessage.js.map +1 -1
  36. package/_esm/utils/siwe/validateSiweMessage.js +16 -3
  37. package/_esm/utils/siwe/validateSiweMessage.js.map +1 -1
  38. package/_types/actions/index.d.ts +1 -0
  39. package/_types/actions/index.d.ts.map +1 -1
  40. package/_types/actions/public/simulateCalls.d.ts +5 -3
  41. package/_types/actions/public/simulateCalls.d.ts.map +1 -1
  42. package/_types/actions/public/watchBlockHeaders.d.ts +45 -0
  43. package/_types/actions/public/watchBlockHeaders.d.ts.map +1 -0
  44. package/_types/clients/decorators/public.d.ts +24 -0
  45. package/_types/clients/decorators/public.d.ts.map +1 -1
  46. package/_types/errors/version.d.ts +1 -1
  47. package/_types/index.d.ts +1 -0
  48. package/_types/index.d.ts.map +1 -1
  49. package/_types/tempo/actions/earn.d.ts.map +1 -1
  50. package/_types/tempo/actions/zone.d.ts +21 -5
  51. package/_types/tempo/actions/zone.d.ts.map +1 -1
  52. package/_types/utils/siwe/parseSiweMessage.d.ts.map +1 -1
  53. package/_types/utils/siwe/validateSiweMessage.d.ts.map +1 -1
  54. package/actions/index.ts +9 -0
  55. package/actions/public/simulateCalls.ts +324 -181
  56. package/actions/public/watchBlockHeaders.ts +131 -0
  57. package/clients/decorators/public.ts +33 -0
  58. package/errors/version.ts +1 -1
  59. package/index.ts +8 -0
  60. package/package.json +1 -1
  61. package/tempo/actions/earn.ts +2 -0
  62. package/tempo/actions/zone.ts +56 -5
  63. package/utils/siwe/parseSiweMessage.ts +20 -3
  64. package/utils/siwe/validateSiweMessage.ts +13 -2
@@ -1,5 +1,6 @@
1
1
  import type { AbiStateMutability, Address, Narrow } from 'abitype'
2
2
  import * as AbiConstructor from 'ox/AbiConstructor'
3
+ import * as AbiEvent from 'ox/AbiEvent'
3
4
  import * as AbiFunction from 'ox/AbiFunction'
4
5
 
5
6
  import { parseAccount } from '../../accounts/utils/parseAccount.js'
@@ -8,9 +9,11 @@ import type { Transport } from '../../clients/transports/createTransport.js'
8
9
  import { ethAddress, zeroAddress } from '../../constants/address.js'
9
10
  import { deploylessCallViaBytecodeBytecode } from '../../constants/contracts.js'
10
11
  import { BaseError } from '../../errors/base.js'
12
+ import { CallExecutionError } from '../../errors/contract.js'
13
+ import { ExecutionRevertedError } from '../../errors/node.js'
11
14
  import type { ErrorType } from '../../errors/utils.js'
12
15
  import type { Account } from '../../types/account.js'
13
- import type { Block } from '../../types/block.js'
16
+ import type { Block, BlockTag } from '../../types/block.js'
14
17
  import type { Call, Calls } from '../../types/calls.js'
15
18
  import type { Chain } from '../../types/chain.js'
16
19
  import type { Log } from '../../types/log.js'
@@ -18,15 +21,14 @@ import type { Hex } from '../../types/misc.js'
18
21
  import type { MulticallResults } from '../../types/multicall.js'
19
22
  import type { StateOverride } from '../../types/stateOverride.js'
20
23
  import type { Mutable } from '../../types/utils.js'
24
+ import { type PadErrorType, pad } from '../../utils/data/pad.js'
25
+ import { hexToBigInt } from '../../utils/encoding/fromHex.js'
26
+ import { type CallErrorType, call } from './call.js'
27
+ import { type GetBlockErrorType, getBlock } from './getBlock.js'
21
28
  import {
22
- type EncodeFunctionDataErrorType,
23
- encodeFunctionData,
24
- } from '../../utils/abi/encodeFunctionData.js'
25
- import { hexToBigInt } from '../../utils/index.js'
26
- import {
27
- type CreateAccessListErrorType,
28
- createAccessList,
29
- } from './createAccessList.js'
29
+ type GetBlockNumberErrorType,
30
+ getBlockNumber,
31
+ } from './getBlockNumber.js'
30
32
  import {
31
33
  type SimulateBlocksErrorType,
32
34
  type SimulateBlocksParameters,
@@ -36,6 +38,33 @@ import {
36
38
  const getBalanceCode =
37
39
  '0x6080604052348015600e575f80fd5b5061016d8061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063f8b2cb4f1461002d575b5f80fd5b610047600480360381019061004291906100db565b61005d565b604051610054919061011e565b60405180910390f35b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100aa82610081565b9050919050565b6100ba816100a0565b81146100c4575f80fd5b50565b5f813590506100d5816100b1565b92915050565b5f602082840312156100f0576100ef61007d565b5b5f6100fd848285016100c7565b91505092915050565b5f819050919050565b61011881610106565b82525050565b5f6020820190506101315f83018461010f565b9291505056fea26469706673582212203b9fe929fe995c7cf9887f0bdba8a36dd78e8b73f149b17d2d9ad7cd09d2dc6264736f6c634300081a0033'
38
40
 
41
+ // Runtime bytecode from `contracts/src/deployless/StaticCall.sol`, compiled with
42
+ // Solidity 0.8.35.
43
+ const staticCallCode =
44
+ '0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063fd00430c1461002d575b5f5ffd5b6100476004803603810190610042919061012b565b610049565b005b80825f375f5f825f865afa610060573d5f5f3e3d5ffd5b3d5f5f3e3d5ff35b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61009982610070565b9050919050565b6100a98161008f565b81146100b3575f5ffd5b50565b5f813590506100c4816100a0565b92915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126100eb576100ea6100ca565b5b8235905067ffffffffffffffff811115610108576101076100ce565b5b602083019150836001820283011115610124576101236100d2565b5b9250929050565b5f5f5f6040848603121561014257610141610068565b5b5f61014f868287016100b6565b935050602084013567ffffffffffffffff8111156101705761016f61006c565b5b61017c868287016100d6565b9250925050925092509256fea2646970667358221220635ed99185cacf3f2acba6921f23687c969cec2bbaf5f9ad599f507e6e105e6964736f6c63430008230033'
45
+
46
+ const staticCallAddressBase = 0x00000000000000000000000000000000deadbeefn
47
+
48
+ // ERC20 & ERC721 share this selector – both are `Transfer(address,address,uint256)`.
49
+ const transferEventSelector = AbiEvent.getSelector(
50
+ AbiEvent.from(
51
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
52
+ ),
53
+ )
54
+
55
+ const balanceOfFunction = AbiFunction.from(
56
+ 'function balanceOf(address) returns (uint256)',
57
+ )
58
+ const decimalsFunction = AbiFunction.from(
59
+ 'function decimals() returns (uint256)',
60
+ )
61
+ const tokenUriFunction = AbiFunction.from(
62
+ 'function tokenURI(uint256) returns (string)',
63
+ )
64
+ const symbolFunction = AbiFunction.from('function symbol() returns (string)')
65
+ const staticCallFunction = AbiFunction.from(
66
+ 'function query(address target, bytes data)',
67
+ )
39
68
  export type SimulateCallsParameters<
40
69
  calls extends readonly unknown[] = readonly unknown[],
41
70
  account extends Account | Address | undefined = Account | Address | undefined,
@@ -83,8 +112,10 @@ export type SimulateCallsReturnType<
83
112
  export type SimulateCallsErrorType =
84
113
  | AbiFunction.encodeData.ErrorType
85
114
  | AbiFunction.from.ErrorType
86
- | CreateAccessListErrorType
87
- | EncodeFunctionDataErrorType
115
+ | CallErrorType
116
+ | GetBlockErrorType
117
+ | GetBlockNumberErrorType
118
+ | PadErrorType
88
119
  | SimulateBlocksErrorType
89
120
  | ErrorType
90
121
 
@@ -162,204 +193,224 @@ export async function simulateCalls<
162
193
  })
163
194
  : undefined
164
195
 
165
- // Fetch ERC20/721 addresses that were "touched" from the calls.
166
- const assetAddresses = traceAssetChanges
167
- ? await Promise.all(
168
- parameters.calls.map(async (call: any) => {
169
- if (!call.data && !call.abi) return
170
- const { accessList } = await createAccessList(client, {
171
- account: account!.address,
172
- ...call,
173
- data: call.abi ? encodeFunctionData(call) : call.data,
174
- })
175
- return accessList.map(({ address, storageKeys }) =>
176
- storageKeys.length > 0 ? address : null,
177
- )
178
- }),
179
- ).then((x) => x.flat().filter(Boolean))
196
+ // Resolve moving canonical tags once so both simulations share a block. Preserve
197
+ // `pending` because its prospective number cannot be queried directly.
198
+ // Do not infer the base from results because clients disagree on simulated numbering.
199
+ const blockTag_ = blockTag ?? client.experimental_blockTag ?? 'latest'
200
+ let baseBlockNumber = blockNumber
201
+ if (
202
+ traceAssetChanges &&
203
+ typeof baseBlockNumber !== 'bigint' &&
204
+ blockTag_ !== 'earliest' &&
205
+ blockTag_ !== 'pending'
206
+ ) {
207
+ if (blockTag_ === 'latest')
208
+ baseBlockNumber = await getBlockNumber(client, { cacheTime: 0 })
209
+ else {
210
+ const block = await getBlock(client, { blockTag: blockTag_ })
211
+ if (typeof block.number !== 'bigint')
212
+ throw new BaseError(
213
+ `Block tag \`${blockTag_}\` did not resolve to a number.`,
214
+ )
215
+ baseBlockNumber = block.number
216
+ }
217
+ }
218
+ const block_ =
219
+ typeof baseBlockNumber === 'bigint'
220
+ ? { blockNumber: baseBlockNumber }
221
+ : { blockTag: blockTag_ }
222
+
223
+ // Discover ERC20/721 addresses the calls move assets in. Simulating the batch as a
224
+ // whole is what makes this correct: the calls run sequentially, with the caller's
225
+ // state overrides, on the same base block the results are measured against.
226
+ const discovery = traceAssetChanges
227
+ ? await simulateBlocks(client, {
228
+ ...block_,
229
+ blocks: [
230
+ {
231
+ calls: calls.map((call) => ({
232
+ ...(call as Call),
233
+ from: account!.address,
234
+ })) as any,
235
+ stateOverrides,
236
+ },
237
+ ],
238
+ traceTransfers,
239
+ validation,
240
+ })
241
+ : undefined
242
+
243
+ const assetAddresses = discovery
244
+ ? [
245
+ ...new Set([
246
+ ...tokensFromLogs(
247
+ discovery[0]!.calls.flatMap((call) => call.logs ?? []),
248
+ account!.address,
249
+ ),
250
+ // Included even for calls without data: contracts that mint on receiving
251
+ // native value (WETH) emit `Deposit`, not a `Transfer` the logs would catch.
252
+ // Candidates without code fall out at `isBalance`.
253
+ ...parameters.calls.map((call: any) => call.to?.toLowerCase()),
254
+ ]),
255
+ ].filter(
256
+ (address): address is Address =>
257
+ Boolean(address) && address !== ethAddress && address !== zeroAddress,
258
+ )
180
259
  : []
260
+ const staticCallAddress = getStaticCallAddress([
261
+ ...(account ? [account.address] : []),
262
+ ...assetAddresses,
263
+ ...(stateOverrides?.map(({ address }) => address) ?? []),
264
+ ])
265
+ const staticCallStateOverrides: StateOverride = [
266
+ { address: staticCallAddress, code: staticCallCode },
267
+ ]
181
268
 
182
- const blocks = await simulateBlocks(client, {
183
- blockNumber,
184
- blockTag: blockTag as undefined,
185
- blocks: [
186
- ...(traceAssetChanges
187
- ? [
188
- // ETH pre balances
189
- {
190
- calls: [{ data: getBalanceData }],
191
- stateOverrides,
192
- },
193
-
194
- // Asset pre balances
195
- {
196
- calls: assetAddresses.map((address, i) => ({
197
- abi: [
198
- AbiFunction.from(
199
- 'function balanceOf(address) returns (uint256)',
269
+ const [balanceCallsPre, blocks] = await Promise.all([
270
+ traceAssetChanges
271
+ ? Promise.all([
272
+ readBalance(client, {
273
+ account: account!.address,
274
+ ...block_,
275
+ data: getBalanceData!,
276
+ stateOverride: stateOverrides,
277
+ }),
278
+ ...assetAddresses.map((address) =>
279
+ readBalance(client, {
280
+ account: account!.address,
281
+ address,
282
+ ...block_,
283
+ data: AbiFunction.encodeData(balanceOfFunction, [
284
+ account!.address,
285
+ ]),
286
+ staticCallAddress,
287
+ stateOverride: stateOverrides,
288
+ }),
289
+ ),
290
+ ])
291
+ : [],
292
+ simulateBlocks(client, {
293
+ ...block_,
294
+ blocks: [
295
+ {
296
+ calls: [...calls, { to: zeroAddress }].map((call) => ({
297
+ ...(call as Call),
298
+ from: account?.address,
299
+ })) as any,
300
+ stateOverrides,
301
+ },
302
+
303
+ ...(traceAssetChanges
304
+ ? [
305
+ // ETH post balances
306
+ {
307
+ calls: [{ data: getBalanceData }],
308
+ },
309
+
310
+ // Asset post balances
311
+ {
312
+ calls: assetAddresses.map((address) => ({
313
+ to: staticCallAddress,
314
+ data: encodeStaticCall(
315
+ address,
316
+ AbiFunction.encodeData(balanceOfFunction, [
317
+ account!.address,
318
+ ]),
200
319
  ),
201
- ],
202
- functionName: 'balanceOf',
203
- args: [account!.address],
204
- to: address,
205
- from: zeroAddress,
206
- nonce: i,
207
- })),
208
- stateOverrides: [
209
- {
210
- address: zeroAddress,
211
- nonce: 0,
212
- },
213
- ],
214
- },
215
- ]
216
- : []),
217
-
218
- {
219
- calls: [...calls, { to: zeroAddress }].map((call) => ({
220
- ...(call as Call),
221
- from: account?.address,
222
- })) as any,
223
- stateOverrides,
224
- },
320
+ })),
321
+ stateOverrides: staticCallStateOverrides,
322
+ },
225
323
 
226
- ...(traceAssetChanges
227
- ? [
228
- // ETH post balances
229
- {
230
- calls: [{ data: getBalanceData }],
231
- },
232
-
233
- // Asset post balances
234
- {
235
- calls: assetAddresses.map((address, i) => ({
236
- abi: [
237
- AbiFunction.from(
238
- 'function balanceOf(address) returns (uint256)',
324
+ // Decimals
325
+ {
326
+ calls: assetAddresses.map((address) => ({
327
+ to: staticCallAddress,
328
+ data: encodeStaticCall(
329
+ address,
330
+ AbiFunction.encodeData(decimalsFunction),
239
331
  ),
240
- ],
241
- functionName: 'balanceOf',
242
- args: [account!.address],
243
- to: address,
244
- from: zeroAddress,
245
- nonce: i,
246
- })),
247
- stateOverrides: [
248
- {
249
- address: zeroAddress,
250
- nonce: 0,
251
- },
252
- ],
253
- },
254
-
255
- // Decimals
256
- {
257
- calls: assetAddresses.map((address, i) => ({
258
- to: address,
259
- abi: [
260
- AbiFunction.from('function decimals() returns (uint256)'),
261
- ],
262
- functionName: 'decimals',
263
- from: zeroAddress,
264
- nonce: i,
265
- })),
266
- stateOverrides: [
267
- {
268
- address: zeroAddress,
269
- nonce: 0,
270
- },
271
- ],
272
- },
273
-
274
- // Token URI
275
- {
276
- calls: assetAddresses.map((address, i) => ({
277
- to: address,
278
- abi: [
279
- AbiFunction.from(
280
- 'function tokenURI(uint256) returns (string)',
332
+ })),
333
+ stateOverrides: staticCallStateOverrides,
334
+ },
335
+
336
+ // Token URI
337
+ {
338
+ calls: assetAddresses.map((address) => ({
339
+ to: staticCallAddress,
340
+ data: encodeStaticCall(
341
+ address,
342
+ AbiFunction.encodeData(tokenUriFunction, [0n]),
281
343
  ),
282
- ],
283
- functionName: 'tokenURI',
284
- args: [0n],
285
- from: zeroAddress,
286
- nonce: i,
287
- })),
288
- stateOverrides: [
289
- {
290
- address: zeroAddress,
291
- nonce: 0,
292
- },
293
- ],
294
- },
295
-
296
- // Symbols
297
- {
298
- calls: assetAddresses.map((address, i) => ({
299
- to: address,
300
- abi: [AbiFunction.from('function symbol() returns (string)')],
301
- functionName: 'symbol',
302
- from: zeroAddress,
303
- nonce: i,
304
- })),
305
- stateOverrides: [
306
- {
307
- address: zeroAddress,
308
- nonce: 0,
309
- },
310
- ],
311
- },
312
- ]
313
- : []),
314
- ],
315
- traceTransfers,
316
- validation,
317
- })
344
+ })),
345
+ stateOverrides: staticCallStateOverrides,
346
+ },
347
+
348
+ // Symbols
349
+ {
350
+ calls: assetAddresses.map((address) => ({
351
+ to: staticCallAddress,
352
+ data: encodeStaticCall(
353
+ address,
354
+ AbiFunction.encodeData(symbolFunction),
355
+ ),
356
+ })),
357
+ stateOverrides: staticCallStateOverrides,
358
+ },
359
+ ]
360
+ : []),
361
+ ],
362
+ traceTransfers,
363
+ validation,
364
+ }),
365
+ ])
318
366
 
319
- const block_results = traceAssetChanges ? blocks[2] : blocks[0]
367
+ const block_results = blocks[0]!
320
368
  const [
321
- block_ethPre,
322
- block_assetsPre,
323
- ,
324
369
  block_ethPost,
325
370
  block_assetsPost,
326
371
  block_decimals,
327
372
  block_tokenURI,
328
373
  block_symbols,
329
- ] = traceAssetChanges ? blocks : []
374
+ ] = traceAssetChanges ? blocks.slice(1) : []
330
375
 
331
376
  // Extract call results from the simulation.
332
377
  const { calls: block_calls, ...block } = block_results
333
- const results = block_calls.slice(0, -1) ?? []
378
+ const results = block_calls.slice(0, -1)
334
379
 
335
380
  // Extract pre-execution ETH and asset balances.
336
- const ethPre = block_ethPre?.calls ?? []
337
- const assetsPre = block_assetsPre?.calls ?? []
338
- const balancesPre = [...ethPre, ...assetsPre].map((call) =>
339
- call.status === 'success' ? hexToBigInt(call.data) : null,
381
+ const balancesPre = balanceCallsPre.map((call) =>
382
+ isBalance(call) ? hexToBigInt(call.data) : null,
340
383
  )
341
384
 
342
385
  // Extract post-execution ETH and asset balances.
343
386
  const ethPost = block_ethPost?.calls ?? []
344
387
  const assetsPost = block_assetsPost?.calls ?? []
345
- const balancesPost = [...ethPost, ...assetsPost].map((call) =>
346
- call.status === 'success' ? hexToBigInt(call.data) : null,
388
+ const balanceCallsPost = [...ethPost, ...assetsPost]
389
+ const balancesPost = balanceCallsPost.map((call) =>
390
+ isBalance(call) ? hexToBigInt(call.data) : null,
347
391
  )
348
392
 
349
393
  // Extract asset symbols & decimals.
350
- const decimals = (block_decimals?.calls ?? []).map((x) =>
351
- x.status === 'success' ? x.result : null,
352
- ) as (number | null)[]
353
- const symbols = (block_symbols?.calls ?? []).map((x) =>
354
- x.status === 'success' ? x.result : null,
394
+ const decimals = (block_decimals?.calls ?? []).map((call) =>
395
+ decodeAssetResult(call, decimalsFunction),
396
+ ) as (bigint | null)[]
397
+ const symbols = (block_symbols?.calls ?? []).map((call) =>
398
+ decodeAssetResult(call, symbolFunction),
355
399
  ) as (string | null)[]
356
- const tokenURI = (block_tokenURI?.calls ?? []).map((x) =>
357
- x.status === 'success' ? x.result : null,
400
+ const tokenURI = (block_tokenURI?.calls ?? []).map((call) =>
401
+ decodeAssetResult(call, tokenUriFunction),
358
402
  ) as (string | null)[]
359
403
 
360
404
  const changes: Mutable<SimulateCallsReturnType<calls>['assetChanges']> = []
361
405
  for (const [i, balancePost] of balancesPost.entries()) {
362
- const balancePre = balancesPre[i]
406
+ const balancePre_ = balancesPre[i]
407
+ const preCall = balanceCallsPre[i]
408
+ const balancePre =
409
+ typeof balancePre_ === 'bigint'
410
+ ? balancePre_
411
+ : i > 0 && preCall?.status === 'success' && preCall.data === '0x'
412
+ ? 0n
413
+ : null
363
414
 
364
415
  if (typeof balancePost !== 'bigint') continue
365
416
  if (typeof balancePre !== 'bigint') continue
@@ -383,9 +434,6 @@ export async function simulateCalls<
383
434
  }
384
435
  })()
385
436
 
386
- if (changes.some((change) => change.token.address === token.address))
387
- continue
388
-
389
437
  changes.push({
390
438
  token,
391
439
  value: {
@@ -402,3 +450,98 @@ export async function simulateCalls<
402
450
  results,
403
451
  } as unknown as SimulateCallsReturnType<calls>
404
452
  }
453
+
454
+ function encodeStaticCall(address: Address, data: Hex) {
455
+ return AbiFunction.encodeData(staticCallFunction, [address, data])
456
+ }
457
+
458
+ // Extract token addresses that the account transferred, from a simulated block's logs.
459
+ // Only topics 0-2 are read: ERC721's fourth topic is the token id, which is irrelevant
460
+ // as `balanceOf(address)` on a 721 returns the owner's NFT count.
461
+ function tokensFromLogs(logs: readonly Log[], account: Address): Address[] {
462
+ const account_ = pad(account.toLowerCase() as Hex, { size: 32 })
463
+ return logs
464
+ .filter((log) => {
465
+ if (log.topics[0]?.toLowerCase() !== transferEventSelector) return false
466
+ // `traceTransfers` emits synthetic native transfer logs under `ethAddress`, which
467
+ // is measured separately.
468
+ if (log.address.toLowerCase() === ethAddress) return false
469
+ return (
470
+ log.topics[1]?.toLowerCase() === account_ ||
471
+ log.topics[2]?.toLowerCase() === account_
472
+ )
473
+ })
474
+ .map((log) => log.address.toLowerCase() as Address)
475
+ }
476
+
477
+ // Whether a `balanceOf` probe returned a balance. A candidate without code (an EOA that
478
+ // merely received value) succeeds with empty data instead of reverting, so `status`
479
+ // alone is not enough.
480
+ function isBalance(call: { data: Hex; status: 'success' | 'failure' }) {
481
+ return call.status === 'success' && /^0x[\da-f]{64}$/i.test(call.data)
482
+ }
483
+
484
+ function decodeAssetResult(
485
+ call: { data: Hex; status: 'success' | 'failure' },
486
+ abiFunction: AbiFunction.AbiFunction,
487
+ ) {
488
+ if (call.status === 'failure' || call.data === '0x') return null
489
+ try {
490
+ return AbiFunction.decodeResult(abiFunction, call.data)
491
+ } catch {
492
+ return null
493
+ }
494
+ }
495
+
496
+ async function readBalance<chain extends Chain | undefined>(
497
+ client: Client<Transport, chain>,
498
+ parameters: {
499
+ account: Address
500
+ address?: Address | undefined
501
+ blockNumber?: bigint | undefined
502
+ blockTag?: BlockTag | undefined
503
+ data: Hex
504
+ staticCallAddress?: Address | undefined
505
+ stateOverride: StateOverride | undefined
506
+ },
507
+ ) {
508
+ const {
509
+ account,
510
+ address,
511
+ blockNumber,
512
+ blockTag,
513
+ data,
514
+ staticCallAddress,
515
+ stateOverride,
516
+ } = parameters
517
+ try {
518
+ const result = await call({ ...client, ccipRead: false }, {
519
+ account: address ? zeroAddress : account,
520
+ data: address ? encodeStaticCall(address, data) : data,
521
+ stateOverride:
522
+ address && staticCallAddress
523
+ ? [
524
+ ...(stateOverride ?? []),
525
+ { address: staticCallAddress, code: staticCallCode },
526
+ ]
527
+ : stateOverride,
528
+ ...(address ? { to: staticCallAddress } : {}),
529
+ ...(typeof blockNumber === 'bigint' ? { blockNumber } : { blockTag }),
530
+ } as never)
531
+ return { data: result.data ?? '0x', status: 'success' as const }
532
+ } catch (error) {
533
+ if (
534
+ !(error instanceof CallExecutionError) ||
535
+ !(error.cause instanceof ExecutionRevertedError)
536
+ )
537
+ throw error
538
+ return { data: '0x' as const, status: 'failure' as const }
539
+ }
540
+ }
541
+
542
+ function getStaticCallAddress(addresses: readonly Address[]): Address {
543
+ const occupied = new Set(addresses.map((address) => address.toLowerCase()))
544
+ let value = staticCallAddressBase
545
+ while (occupied.has(`0x${value.toString(16).padStart(40, '0')}`)) value++
546
+ return `0x${value.toString(16).padStart(40, '0')}`
547
+ }