UNPKG

viem

Version:

TypeScript Interface for Ethereum

415 lines (382 loc) • 12.7 kB
import type { AbiStateMutability, Address, Narrow } from 'abitype' import * as AbiConstructor from 'ox/AbiConstructor' import * as AbiFunction from 'ox/AbiFunction' import { parseAccount } from '../../accounts/utils/parseAccount.js' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { ethAddress, zeroAddress } from '../../constants/address.js' import { deploylessCallViaBytecodeBytecode } from '../../constants/contracts.js' import { BaseError } from '../../errors/base.js' import type { ErrorType } from '../../errors/utils.js' import type { Account } from '../../types/account.js' import type { Block } from '../../types/block.js' import type { Call, Calls } from '../../types/calls.js' import type { Chain } from '../../types/chain.js' import type { Log } from '../../types/log.js' import type { Hex } from '../../types/misc.js' import type { MulticallResults } from '../../types/multicall.js' import type { StateOverride } from '../../types/stateOverride.js' import type { Mutable } from '../../types/utils.js' import { type EncodeFunctionDataErrorType, encodeFunctionData, } from '../../utils/abi/encodeFunctionData.js' import { hexToBigInt } from '../../utils/index.js' import { type CreateAccessListErrorType, createAccessList, } from './createAccessList.js' import { type SimulateBlocksErrorType, type SimulateBlocksParameters, simulateBlocks, } from './simulateBlocks.js' const getBalanceCode = '0x6080604052348015600e575f80fd5b5061016d8061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063f8b2cb4f1461002d575b5f80fd5b610047600480360381019061004291906100db565b61005d565b604051610054919061011e565b60405180910390f35b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100aa82610081565b9050919050565b6100ba816100a0565b81146100c4575f80fd5b50565b5f813590506100d5816100b1565b92915050565b5f602082840312156100f0576100ef61007d565b5b5f6100fd848285016100c7565b91505092915050565b5f819050919050565b61011881610106565b82525050565b5f6020820190506101315f83018461010f565b9291505056fea26469706673582212203b9fe929fe995c7cf9887f0bdba8a36dd78e8b73f149b17d2d9ad7cd09d2dc6264736f6c634300081a0033' export type SimulateCallsParameters< calls extends readonly unknown[] = readonly unknown[], account extends Account | Address | undefined = Account | Address | undefined, > = Omit<SimulateBlocksParameters, 'blocks' | 'returnFullTransactions'> & { /** Account attached to the calls (msg.sender). */ account?: account | undefined /** Calls to simulate. */ calls: Calls<Narrow<calls>> /** State overrides. */ stateOverrides?: StateOverride | undefined /** Whether to trace asset changes. */ traceAssetChanges?: boolean | undefined } export type SimulateCallsReturnType< calls extends readonly unknown[] = readonly unknown[], > = { /** Asset changes. */ assetChanges: readonly { token: { address: Address decimals?: number | undefined symbol?: string | undefined } value: { pre: bigint; post: bigint; diff: bigint } }[] /** Block results. */ block: Block /** Call results. */ results: MulticallResults< Narrow<calls>, true, { extraProperties: { data: Hex gasUsed: bigint logs?: Log[] | undefined } error: Error mutability: AbiStateMutability } > } export type SimulateCallsErrorType = | AbiFunction.encodeData.ErrorType | AbiFunction.from.ErrorType | CreateAccessListErrorType | EncodeFunctionDataErrorType | SimulateBlocksErrorType | ErrorType /** * 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< const calls extends readonly unknown[], chain extends Chain | undefined, account extends Account | Address | undefined = undefined, >( client: Client<Transport, chain>, parameters: SimulateCallsParameters<calls, account>, ): Promise<SimulateCallsReturnType<calls>> { 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 // Fetch ERC20/721 addresses that were "touched" from the calls. const assetAddresses = traceAssetChanges ? await Promise.all( parameters.calls.map(async (call: any) => { if (!call.data && !call.abi) return const { accessList } = await createAccessList(client, { account: account!.address, ...call, data: call.abi ? encodeFunctionData(call) : call.data, }) return accessList.map(({ address, storageKeys }) => storageKeys.length > 0 ? address : null, ) }), ).then((x) => x.flat().filter(Boolean)) : [] const resultsStateOverrides = stateOverrides?.map((override) => { if (override.address === account?.address) return { ...override, nonce: 0, } return override }) const blocks = await simulateBlocks(client, { blockNumber, blockTag: blockTag as undefined, blocks: [ ...(traceAssetChanges ? [ // ETH pre balances { calls: [{ data: getBalanceData }], stateOverrides, }, // Asset pre balances { calls: assetAddresses.map((address, i) => ({ abi: [ AbiFunction.from( 'function balanceOf(address) returns (uint256)', ), ], functionName: 'balanceOf', args: [account!.address], to: address, from: zeroAddress, nonce: i, })), stateOverrides: [ { address: zeroAddress, nonce: 0, }, ], }, ] : []), { calls: [...calls, {}].map((call, index) => ({ ...(call as Call), from: account?.address, nonce: index, })) as any, stateOverrides: resultsStateOverrides, }, ...(traceAssetChanges ? [ // ETH post balances { calls: [{ data: getBalanceData }], }, // Asset post balances { calls: assetAddresses.map((address, i) => ({ abi: [ AbiFunction.from( 'function balanceOf(address) returns (uint256)', ), ], functionName: 'balanceOf', args: [account!.address], to: address, from: zeroAddress, nonce: i, })), stateOverrides: [ { address: zeroAddress, nonce: 0, }, ], }, // Decimals { calls: assetAddresses.map((address, i) => ({ to: address, abi: [ AbiFunction.from('function decimals() returns (uint256)'), ], functionName: 'decimals', from: zeroAddress, nonce: i, })), stateOverrides: [ { address: zeroAddress, nonce: 0, }, ], }, // Token URI { calls: assetAddresses.map((address, i) => ({ to: address, abi: [ AbiFunction.from( 'function tokenURI(uint256) returns (string)', ), ], functionName: 'tokenURI', args: [0n], from: zeroAddress, nonce: i, })), stateOverrides: [ { address: zeroAddress, nonce: 0, }, ], }, // Symbols { calls: assetAddresses.map((address, i) => ({ to: address, abi: [AbiFunction.from('function symbol() returns (string)')], functionName: 'symbol', from: zeroAddress, nonce: i, })), stateOverrides: [ { address: zeroAddress, nonce: 0, }, ], }, ] : []), ], traceTransfers, validation, }) const block_results = traceAssetChanges ? blocks[2] : blocks[0] const [ block_ethPre, block_assetsPre, , block_ethPost, block_assetsPost, block_decimals, block_tokenURI, block_symbols, ] = traceAssetChanges ? blocks : [] // 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 ethPre = block_ethPre?.calls ?? [] const assetsPre = block_assetsPre?.calls ?? [] const balancesPre = [...ethPre, ...assetsPre].map((call) => call.status === 'success' ? hexToBigInt(call.data) : null, ) // Extract post-execution ETH and asset balances. const ethPost = block_ethPost?.calls ?? [] const assetsPost = block_assetsPost?.calls ?? [] const balancesPost = [...ethPost, ...assetsPost].map((call) => call.status === 'success' ? hexToBigInt(call.data) : null, ) // Extract asset symbols & decimals. const decimals = (block_decimals?.calls ?? []).map((x) => x.status === 'success' ? x.result : null, ) as (number | null)[] const symbols = (block_symbols?.calls ?? []).map((x) => x.status === 'success' ? x.result : null, ) as (string | null)[] const tokenURI = (block_tokenURI?.calls ?? []).map((x) => x.status === 'success' ? x.result : null, ) as (string | null)[] const changes: Mutable<SimulateCallsReturnType<calls>['assetChanges']> = [] for (const [i, balancePost] of balancesPost.entries()) { const balancePre = balancesPre[i] 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]! as Address, decimals: tokenURI_ || decimals_ ? Number(decimals_ ?? 1) : undefined, symbol: symbol_ ?? undefined, } })() if (changes.some((change) => change.token.address === token.address)) continue changes.push({ token, value: { pre: balancePre, post: balancePost, diff: balancePost - balancePre, }, }) } return { assetChanges: changes, block, results, } as unknown as SimulateCallsReturnType<calls> }