UNPKG

viem

Version:

TypeScript Interface for Ethereum

349 lines 16.9 kB
import * as AbiConstructor from 'ox/AbiConstructor'; import * as AbiEvent from 'ox/AbiEvent'; import * as AbiFunction from 'ox/AbiFunction'; import { parseAccount } from '../../accounts/utils/parseAccount.js'; import { ethAddress, zeroAddress } from '../../constants/address.js'; import { deploylessCallViaBytecodeBytecode } from '../../constants/contracts.js'; import { BaseError } from '../../errors/base.js'; import { CallExecutionError } from '../../errors/contract.js'; import { ExecutionRevertedError } from '../../errors/node.js'; import { pad } from '../../utils/data/pad.js'; import { hexToBigInt } from '../../utils/encoding/fromHex.js'; import { call } from './call.js'; import { getBlock } from './getBlock.js'; import { getBlockNumber, } from './getBlockNumber.js'; import { simulateBlocks, } from './simulateBlocks.js'; const getBalanceCode = '0x6080604052348015600e575f80fd5b5061016d8061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063f8b2cb4f1461002d575b5f80fd5b610047600480360381019061004291906100db565b61005d565b604051610054919061011e565b60405180910390f35b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100aa82610081565b9050919050565b6100ba816100a0565b81146100c4575f80fd5b50565b5f813590506100d5816100b1565b92915050565b5f602082840312156100f0576100ef61007d565b5b5f6100fd848285016100c7565b91505092915050565b5f819050919050565b61011881610106565b82525050565b5f6020820190506101315f83018461010f565b9291505056fea26469706673582212203b9fe929fe995c7cf9887f0bdba8a36dd78e8b73f149b17d2d9ad7cd09d2dc6264736f6c634300081a0033'; // Runtime bytecode from `contracts/src/deployless/StaticCall.sol`, compiled with // Solidity 0.8.35. const staticCallCode = '0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063fd00430c1461002d575b5f5ffd5b6100476004803603810190610042919061012b565b610049565b005b80825f375f5f825f865afa610060573d5f5f3e3d5ffd5b3d5f5f3e3d5ff35b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61009982610070565b9050919050565b6100a98161008f565b81146100b3575f5ffd5b50565b5f813590506100c4816100a0565b92915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126100eb576100ea6100ca565b5b8235905067ffffffffffffffff811115610108576101076100ce565b5b602083019150836001820283011115610124576101236100d2565b5b9250929050565b5f5f5f6040848603121561014257610141610068565b5b5f61014f868287016100b6565b935050602084013567ffffffffffffffff8111156101705761016f61006c565b5b61017c868287016100d6565b9250925050925092509256fea2646970667358221220635ed99185cacf3f2acba6921f23687c969cec2bbaf5f9ad599f507e6e105e6964736f6c63430008230033'; // Bound each probe so a failing candidate cannot exhaust the simulation gas budget. const assetProbeGas = 1000000n; const staticCallAddressBase = 0x00000000000000000000000000000000deadbeefn; // ERC20 & ERC721 share this selector – both are `Transfer(address,address,uint256)`. const transferEventSelector = AbiEvent.getSelector(AbiEvent.from('event Transfer(address indexed from, address indexed to, uint256 value)')); const balanceOfFunction = AbiFunction.from('function balanceOf(address) returns (uint256)'); const decimalsFunction = AbiFunction.from('function decimals() returns (uint256)'); const tokenUriFunction = AbiFunction.from('function tokenURI(uint256) returns (string)'); const symbolFunction = AbiFunction.from('function symbol() returns (string)'); const staticCallFunction = AbiFunction.from('function query(address target, bytes data)'); /** * Simulates execution of a batch of calls. * * @param client - Client to use * @param parameters - {@link SimulateCallsParameters} * @returns Results. {@link SimulateCallsReturnType} * * @example * ```ts * import { createPublicClient, http, parseEther } from 'viem' * import { mainnet } from 'viem/chains' * import { simulateCalls } from 'viem/actions' * * const client = createPublicClient({ * chain: mainnet, * transport: http(), * }) * * const result = await simulateCalls(client, { * account: '0x5a0b54d5dc17e482fe8b0bdca5320161b95fb929', * calls: [{ * { * data: '0xdeadbeef', * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', * }, * { * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', * value: parseEther('1'), * }, * ] * }) * ``` */ export async function simulateCalls(client, parameters) { const { blockNumber, blockTag, calls, stateOverrides, traceAssetChanges, traceTransfers, validation, } = parameters; const account = parameters.account ? parseAccount(parameters.account) : undefined; if (traceAssetChanges && !account) throw new BaseError('`account` is required when `traceAssetChanges` is true'); // Derive bytecode to extract ETH balance via a contract call. const getBalanceData = account ? AbiConstructor.encode(AbiConstructor.from('constructor(bytes, bytes)'), { bytecode: deploylessCallViaBytecodeBytecode, args: [ getBalanceCode, AbiFunction.encodeData(AbiFunction.from('function getBalance(address)'), [account.address]), ], }) : undefined; // Resolve moving canonical tags once so both simulations share a block. Preserve // `pending` because its prospective number cannot be queried directly. // Do not infer the base from results because clients disagree on simulated numbering. const blockTag_ = blockTag ?? client.experimental_blockTag ?? 'latest'; let baseBlockNumber = blockNumber; if (traceAssetChanges && typeof baseBlockNumber !== 'bigint' && blockTag_ !== 'earliest' && blockTag_ !== 'pending') { if (blockTag_ === 'latest') baseBlockNumber = await getBlockNumber(client, { cacheTime: 0 }); else { const block = await getBlock(client, { blockTag: blockTag_ }); if (typeof block.number !== 'bigint') throw new BaseError(`Block tag \`${blockTag_}\` did not resolve to a number.`); baseBlockNumber = block.number; } } const block_ = typeof baseBlockNumber === 'bigint' ? { blockNumber: baseBlockNumber } : { blockTag: blockTag_ }; // Discover ERC20/721 addresses the calls move assets in. Simulating the batch as a // whole is what makes this correct: the calls run sequentially, with the caller's // state overrides, on the same base block the results are measured against. const discovery = traceAssetChanges ? await simulateBlocks(client, { ...block_, blocks: [ { calls: calls.map((call) => ({ ...call, from: account.address, })), stateOverrides, }, ], traceTransfers, validation, }) : undefined; const assetAddresses = discovery ? [ ...new Set([ ...tokensFromLogs(discovery[0].calls.flatMap((call) => call.logs ?? []), account.address), // Included even for calls without data: contracts that mint on receiving // native value (WETH) emit `Deposit`, not a `Transfer` the logs would catch. // Candidates without code fall out at `isBalance`. ...parameters.calls.map((call) => call.to?.toLowerCase()), ]), ].filter((address) => Boolean(address) && address !== ethAddress && address !== zeroAddress) : []; const staticCallAddress = getStaticCallAddress([ ...(account ? [account.address] : []), ...assetAddresses, ...(stateOverrides?.map(({ address }) => address) ?? []), ]); const staticCallStateOverrides = [ { address: staticCallAddress, code: staticCallCode }, ]; const [balanceCallsPre, blocks] = await Promise.all([ traceAssetChanges ? Promise.all([ readBalance(client, { account: account.address, ...block_, data: getBalanceData, stateOverride: stateOverrides, }), ...assetAddresses.map((address) => readBalance(client, { account: account.address, address, ...block_, data: AbiFunction.encodeData(balanceOfFunction, [ account.address, ]), staticCallAddress, stateOverride: stateOverrides, })), ]) : [], simulateBlocks(client, { ...block_, blocks: [ { calls: [...calls, { to: zeroAddress }].map((call) => ({ ...call, from: account?.address, })), stateOverrides, }, ...(traceAssetChanges ? [ // ETH post balances { calls: [{ data: getBalanceData }], }, // Asset post balances { calls: assetAddresses.map((address) => ({ to: staticCallAddress, gas: assetProbeGas, data: encodeStaticCall(address, AbiFunction.encodeData(balanceOfFunction, [ account.address, ])), })), stateOverrides: staticCallStateOverrides, }, // Decimals { calls: assetAddresses.map((address) => ({ to: staticCallAddress, gas: assetProbeGas, data: encodeStaticCall(address, AbiFunction.encodeData(decimalsFunction)), })), stateOverrides: staticCallStateOverrides, }, // Token URI { calls: assetAddresses.map((address) => ({ to: staticCallAddress, gas: assetProbeGas, data: encodeStaticCall(address, AbiFunction.encodeData(tokenUriFunction, [0n])), })), stateOverrides: staticCallStateOverrides, }, // Symbols { calls: assetAddresses.map((address) => ({ to: staticCallAddress, gas: assetProbeGas, data: encodeStaticCall(address, AbiFunction.encodeData(symbolFunction)), })), stateOverrides: staticCallStateOverrides, }, ] : []), ], traceTransfers, validation, }), ]); const block_results = blocks[0]; const [block_ethPost, block_assetsPost, block_decimals, block_tokenURI, block_symbols,] = traceAssetChanges ? blocks.slice(1) : []; // Extract call results from the simulation. const { calls: block_calls, ...block } = block_results; const results = block_calls.slice(0, -1); // Extract pre-execution ETH and asset balances. const balancesPre = balanceCallsPre.map((call) => isBalance(call) ? hexToBigInt(call.data) : null); // Extract post-execution ETH and asset balances. const ethPost = block_ethPost?.calls ?? []; const assetsPost = block_assetsPost?.calls ?? []; const balanceCallsPost = [...ethPost, ...assetsPost]; const balancesPost = balanceCallsPost.map((call) => isBalance(call) ? hexToBigInt(call.data) : null); // Extract asset symbols & decimals. const decimals = (block_decimals?.calls ?? []).map((call) => decodeAssetResult(call, decimalsFunction)); const symbols = (block_symbols?.calls ?? []).map((call) => decodeAssetResult(call, symbolFunction)); const tokenURI = (block_tokenURI?.calls ?? []).map((call) => decodeAssetResult(call, tokenUriFunction)); const changes = []; for (const [i, balancePost] of balancesPost.entries()) { const balancePre_ = balancesPre[i]; const preCall = balanceCallsPre[i]; const balancePre = typeof balancePre_ === 'bigint' ? balancePre_ : i > 0 && preCall?.status === 'success' && preCall.data === '0x' ? 0n : null; if (typeof balancePost !== 'bigint') continue; if (typeof balancePre !== 'bigint') continue; const decimals_ = decimals[i - 1]; const symbol_ = symbols[i - 1]; const tokenURI_ = tokenURI[i - 1]; const token = (() => { if (i === 0) return { address: ethAddress, decimals: 18, symbol: 'ETH', }; return { address: assetAddresses[i - 1], decimals: tokenURI_ || decimals_ ? Number(decimals_ ?? 1) : undefined, symbol: symbol_ ?? undefined, }; })(); changes.push({ token, value: { pre: balancePre, post: balancePost, diff: balancePost - balancePre, }, }); } return { assetChanges: changes, block, results, }; } function encodeStaticCall(address, data) { return AbiFunction.encodeData(staticCallFunction, [address, data]); } // Extract token addresses that the account transferred, from a simulated block's logs. // Only topics 0-2 are read: ERC721's fourth topic is the token id, which is irrelevant // as `balanceOf(address)` on a 721 returns the owner's NFT count. function tokensFromLogs(logs, account) { const account_ = pad(account.toLowerCase(), { size: 32 }); return logs .filter((log) => { if (log.topics[0]?.toLowerCase() !== transferEventSelector) return false; // `traceTransfers` emits synthetic native transfer logs under `ethAddress`, which // is measured separately. if (log.address.toLowerCase() === ethAddress) return false; return (log.topics[1]?.toLowerCase() === account_ || log.topics[2]?.toLowerCase() === account_); }) .map((log) => log.address.toLowerCase()); } // Whether a `balanceOf` probe returned a balance. A candidate without code (an EOA that // merely received value) succeeds with empty data instead of reverting, so `status` // alone is not enough. function isBalance(call) { return call.status === 'success' && /^0x[\da-f]{64}$/i.test(call.data); } function decodeAssetResult(call, abiFunction) { if (call.status === 'failure' || call.data === '0x') return null; try { return AbiFunction.decodeResult(abiFunction, call.data); } catch { return null; } } async function readBalance(client, parameters) { const { account, address, blockNumber, blockTag, data, staticCallAddress, stateOverride, } = parameters; try { const result = await call({ ...client, ccipRead: false }, { account: address ? zeroAddress : account, data: address ? encodeStaticCall(address, data) : data, stateOverride: address && staticCallAddress ? [ ...(stateOverride ?? []), { address: staticCallAddress, code: staticCallCode }, ] : stateOverride, ...(address ? { gas: assetProbeGas, to: staticCallAddress } : {}), ...(typeof blockNumber === 'bigint' ? { blockNumber } : { blockTag }), }); return { data: result.data ?? '0x', status: 'success' }; } catch (error) { if (!(error instanceof CallExecutionError) || !(error.cause instanceof ExecutionRevertedError)) throw error; return { data: '0x', status: 'failure' }; } } function getStaticCallAddress(addresses) { const occupied = new Set(addresses.map((address) => address.toLowerCase())); let value = staticCallAddressBase; while (occupied.has(`0x${value.toString(16).padStart(40, '0')}`)) value++; return `0x${value.toString(16).padStart(40, '0')}`; } //# sourceMappingURL=simulateCalls.js.map