-
Notifications
You must be signed in to change notification settings - Fork 320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: transaction simulation endpoint #294
Comments
Related brainstorming thread in case it's useful later: https://warpcast.com/ncitron.eth/0x4238de4f |
Nice work! Many variables to consider. Perhaps more complex cases involve greater decentralisation contingent or security detail depending, or applied analytics. Mutual respect and disclosure of raw info or tasteful privacy. Idea-flow possibilities: origination, consent, scope model promoting user awareness best practice and or training module, mental status, biographics, attempt versus equal benefit, time mechanisms, early observatory clicks, too often repetitive feature, response to request, response to robot request or vice versa false attempt, close, total failure, failure to respond after responding respectfully to request, respond with reputation risks, clear definables or bona fide glossary, login attempts, aggregates, opt out, opt less in, blindness radar, actual response to actual request, timidability versus publishability, royalty if any, etc. |
Integrating Tevm into helios will create an absurdly powerful version of this. tevm_call params: /**
* Properties shared across call-like params.
* This type is used as the base for various call-like parameter types:
* - [CallParams](https://tevm.sh/reference/tevm/actions/type-aliases/callparams-1/)
* - [ContractParams](https://tevm.sh/reference/tevm/actions/type-aliases/contractparams-1/)
* - [DeployParams](https://tevm.sh/reference/tevm/actions/type-aliases/deployparams-1/)
* - [ScriptParams](https://tevm.sh/reference/tevm/actions/type-aliases/scriptparams-1/)
*
* @extends BaseParams
* @example
* ```typescript
* import { BaseCallParams } from 'tevm'
*
* const params: BaseCallParams = {
* createTrace: true,
* createAccessList: true,
* createTransaction: 'on-success',
* blockTag: 'latest',
* skipBalance: true,
* gas: 1000000n,
* gasPrice: 1n,
* maxFeePerGas: 1n,
* maxPriorityFeePerGas: 1n,
* gasRefund: 0n,
* from: '0x123...',
* origin: '0x123...',
* caller: '0x123...',
* value: 0n,
* depth: 0,
* to: '0x123...',
* }
* ```
*/
export type BaseCallParams<TThrowOnFail extends boolean = boolean> = BaseParams<TThrowOnFail> & {
/**
* Whether to return a complete trace with the call.
* Defaults to `false`.
* @example
* ```ts
* import { createMemoryClient } from 'tevm'
*
* const client = createMemoryClient()
*
* const { trace } = await client.call({ address: '0x1234', data: '0x1234', createTrace: true })
*
* trace.structLogs.forEach(console.log)
* ```
*/
readonly createTrace?: boolean
/**
* Whether to return an access list mapping of addresses to storage keys.
* Defaults to `false`.
* @example
* ```ts
* import { createMemoryClient } from 'tevm'
*
* const client = createMemoryClient()
*
* const { accessList } = await client.tevmCall({ to: '0x1234...', data: '0x1234', createAccessList: true })
* console.log(accessList) // { "0x...": Set(["0x..."]) }
* ```
*/
readonly createAccessList?: boolean
/**
* Whether or not to update the state or run the call in a dry-run. Defaults to `never`.
* - `on-success`: Only update the state if the call is successful.
* - `always`: Always include the transaction even if it reverts.
* - `never`: Never include the transaction.
* - `true`: Alias for `on-success`.
* - `false`: Alias for `never`.
*
* @example
* ```typescript
* const { txHash } = await client.call({ address: '0x1234', data: '0x1234', createTransaction: 'on-success' })
* await client.mine()
* const receipt = await client.getTransactionReceipt({ hash: txHash })
* ```
*/
readonly createTransaction?: 'on-success' | 'always' | 'never' | boolean
/**
* The block number or block tag to execute the call at. Defaults to `latest`.
* - `bigint`: The block number to execute the call at.
* - `Hex`: The block hash to execute the call at.
* - `BlockTag`: The named block tag to execute the call at.
*
* Notable block tags:
* - 'latest': The canonical head.
* - 'pending': A block that is optimistically built with transactions in the txpool that have not yet been mined.
* - 'forked': If forking, the 'forked' block will be the block the chain was forked at.
*/
readonly blockTag?: BlockParam
/**
* Whether to skip the balance check. Defaults to `false`, except for scripts where it is set to `true`.
*/
readonly skipBalance?: boolean
/**
* The gas limit for the call.
* Defaults to the block gas limit as specified by the common configuration or the fork URL.
*/
readonly gas?: bigint
/**
* The gas price for the call.
* Note: This option is currently ignored when creating transactions because only EIP-1559 transactions are supported. This will be fixed in a future release.
*/
readonly gasPrice?: bigint
/**
* The maximum fee per gas for EIP-1559 transactions.
*/
readonly maxFeePerGas?: bigint
/**
* The maximum priority fee per gas for EIP-1559 transactions.
*/
readonly maxPriorityFeePerGas?: bigint
/**
* The refund counter. Defaults to `0`.
*/
readonly gasRefund?: bigint
/**
* The from address for the call. Defaults to the zero address for reads and the first account for writes.
* It is also possible to set the `origin` and `caller` addresses separately using those options. Otherwise, both are set to the `from` address.
*/
readonly from?: Address
/**
* The address where the call originated from. Defaults to the zero address.
* If the `from` address is set, it defaults to the `from` address; otherwise, it defaults to the zero address.
*/
readonly origin?: Address
/**
* The address that ran this code (`msg.sender`). Defaults to the zero address.
* If the `from` address is set, it defaults to the `from` address; otherwise, it defaults to the zero address.
*/
readonly caller?: Address
/**
* The value in ether that is being sent to the `to` address. Defaults to `0`.
*/
readonly value?: bigint
/**
* The depth of the EVM call. Useful for simulating an internal call. Defaults to `0`.
*/
readonly depth?: number
/**
* Addresses to selfdestruct. Defaults to an empty set.
*/
readonly selfdestruct?: Set<Address>
/**
* The address of the account executing this code (`address(this)`). Defaults to the zero address.
* This is not set for create transactions but is required for most transactions.
*/
readonly to?: Address
/**
* Versioned hashes for each blob in a blob transaction for EIP-4844 transactions.
*/
readonly blobVersionedHashes?: Hex[]
/**
* The state override set is an optional address-to-state mapping where each entry specifies some state to be ephemerally overridden prior to executing the call. Each address maps to an object containing:
* This option cannot be used when `createTransaction` is set to `true`.
*
* @example
* ```ts
* const stateOverride = {
* "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3": {
* balance: "0xde0b6b3a7640000"
* },
* "0xebe8efa441b9302a0d7eaecc277c09d20d684540": {
* code: "0x...",
* state: {
* "0x...": "0x..."
* }
* }
* }
* const res = await client.call({ address: '0x1234', data: '0x1234', stateOverrideSet: stateOverride })
* ```
*/
readonly stateOverrideSet?: StateOverrideSet
/**
* The fields of this optional object customize the block as part of which the call is simulated.
* The object contains fields such as block number, hash, parent hash, nonce, etc.
* This option cannot be used when `createTransaction` is set to `true`.
* Setting the block number to a past block will not run in the context of that block's state. To do that, fork that block number first.
*
* @example
* ```ts
* const blockOverride = {
* number: "0x1b4",
* hash: "0x...",
* parentHash: "0x...",
* nonce: "0x0000000000000042",
* }
* const res = await client.call({ address: '0x1234', data: '0x1234', blockOverrideSet: blockOverride })
* ```
*/
readonly blockOverrideSet?: BlockOverrideSet
} import type { BaseCallParams } from '../BaseCall/BaseCallParams.js'
import type { Hex } from '../common/index.js'
/**
* TEVM parameters to execute a call on the VM.
* `Call` is the lowest level method to interact with the VM, and other methods such as `contract` and `script` use `call` under the hood.
*
* @example
* ```typescript
* import { createClient } from 'viem'
* import { createTevmTransport, tevmCall } from 'tevm'
* import { optimism } from 'tevm/common'
*
* const client = createClient({
* transport: createTevmTransport({}),
* chain: optimism,
* })
*
* const callParams = {
* data: '0x...',
* bytecode: '0x...',
* gasLimit: 420n,
* }
*
* await tevmCall(client, callParams)
* ```
*
* @see [BaseCallParams](https://tevm.sh/reference/tevm/actions/type-aliases/basecallparams-1/)
* @see [tevmCall](https://tevm.sh/reference/tevm/memory-client/functions/tevmCall/)
*/
export type CallParams<TThrowOnFail extends boolean = boolean> = BaseCallParams<TThrowOnFail> & {
/**
* An optional CREATE2 salt.
*
* @example
* ```typescript
* import { createClient } from 'viem'
* import { createTevmTransport, tevmCall } from 'tevm'
* import { optimism } from 'tevm/common'
*
* const client = createClient({
* transport: createTevmTransport({}),
* chain: optimism,
* })
*
* const callParams = {
* data: '0x...',
* bytecode: '0x...',
* gasLimit: 420n,
* salt: '0x1234...',
* }
*
* await tevmCall(client, callParams)
* ```
*
* @see [CREATE2](https://eips.ethereum.org/EIPS/eip-1014)
*/
readonly salt?: Hex
/**
* The input data for the call.
*/
readonly data?: Hex
/**
* The encoded code to deploy with for a deployless call. Code is encoded with constructor arguments, unlike `deployedBytecode`.
*
* @example
* ```typescript
* import { createClient } from 'viem'
* import { createTevmTransport, tevmCall, encodeDeployData } from 'tevm'
* import { optimism } from 'tevm/common'
*
* const client = createClient({
* transport: createTevmTransport({}),
* chain: optimism,
* })
*
* const callParams = {
* createTransaction: true,
* data: encodeDeployData({
* bytecode: '0x...',
* data: '0x...',
* abi: [{...}],
* args: [1, 2, 3],
* })
* }
*
* await tevmCall(client, callParams)
* ```
* Code is also automatically created if using TEVM contracts via the `script` method.
*
* @example
* ```typescript
* import { createClient } from 'viem'
* import { createTevmTransport, tevmContract } from 'tevm'
* import { optimism } from 'tevm/common'
* import { SimpleContract } from 'tevm/contracts'
*
* const client = createClient({
* transport: createTevmTransport({}),
* chain: optimism,
* })
*
* const script = SimpleContract.script({ constructorArgs: [420n] })
*
* await tevmContract(client, script.read.get()) // 420n
* ```
*/
readonly code?: Hex
/**
* The code to put into the state before executing the call. If you wish to call the constructor, use `code` instead.
*
* @example
* ```typescript
* import { createClient } from 'viem'
* import { createTevmTransport, tevmCall } from 'tevm'
* import { optimism } from 'tevm/common'
*
* const client = createClient({
* transport: createTevmTransport({}),
* chain: optimism,
* })
*
* const callParams = {
* data: '0x...',
* deployedBytecode: '0x...',
* }
*
* await tevmCall(client, callParams)
* ```
*/
readonly deployedBytecode?: Hex
} And it returns the following export type CallResult<ErrorType = TevmCallError> = {
/**
* The call trace if tracing is enabled on call.
*
* @example
* ```typescript
* const trace = result.trace
* trace.structLogs.forEach(console.log)
* ```
*/
trace?: DebugTraceCallResult
/**
* The access list if enabled on call.
* Mapping of addresses to storage slots.
*
* @example
* ```typescript
* const accessList = result.accessList
* console.log(accessList) // { "0x...": Set(["0x..."]) }
* ```
*/
accessList?: Record<Address, Set<Hex>>
/**
* Preimages mapping of the touched accounts from the transaction (see `reportPreimages` option).
*/
preimages?: Record<Hex, Hex>
/**
* The returned transaction hash if the call was included in the chain.
* Will not be defined if the call was not included in the chain.
* Whether a call is included in the chain depends on the `createTransaction` option and the result of the call.
*
* @example
* ```typescript
* const txHash = result.txHash
* if (txHash) {
* console.log(`Transaction included in the chain with hash: ${txHash}`)
* }
* ```
*/
txHash?: Hex
/**
* Amount of gas left after execution.
*/
gas?: bigint
/**
* Amount of gas the code used to run within the EVM.
* This only includes gas spent on the EVM execution itself and doesn't account for gas spent on other factors such as data storage.
*/
executionGasUsed: bigint
/**
* Array of logs that the contract emitted.
*
* @example
* ```typescript
* const logs = result.logs
* logs?.forEach(log => console.log(log))
* ```
*/
logs?: Log[]
/**
* The gas refund counter as a uint256.
*/
gasRefund?: bigint
/**
* Amount of blob gas consumed by the transaction.
*/
blobGasUsed?: bigint
/**
* Address of created account during the transaction, if any.
*/
createdAddress?: Address
/**
* A set of accounts to selfdestruct.
*/
selfdestruct?: Set<Address>
/**
* Map of addresses which were created (used in EIP 6780).
* Note the addresses are not actually created until the transaction is mined.
*/
createdAddresses?: Set<Address>
/**
* Encoded return value from the contract as a hex string.
*
* @example
* ```typescript
* const rawData = result.rawData
* console.log(`Raw data returned: ${rawData}`)
* ```
*/
rawData: Hex
/**
* Description of the exception, if any occurred.
*/
errors?: ErrorType[]
/**
* Priority fee set by the transaction.
*/
priorityFee?: bigint
/**
* The base fee of the transaction.
*/
baseFee?: bigint
/**
* L1 fee that should be paid for the transaction.
* Only included when an OP-Stack common is provided.
*
* @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
*/
l1Fee?: bigint
/**
* Amount of L1 gas used to publish the transaction.
* Only included when an OP-Stack common is provided.
*
* @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
*/
l1GasUsed?: bigint
/**
* Current blob base fee known by the L2 chain.
*
* @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
*/
l1BlobFee?: bigint
/**
* Latest known L1 base fee known by the L2 chain.
* Only included when an OP-Stack common is provided.
*
* @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
*/
l1BaseFee?: bigint
/**
* The amount of gas used in this transaction, which is paid for.
* This contains the gas units that have been used on execution, plus the upfront cost,
* which consists of calldata cost, intrinsic cost, and optionally the access list costs.
* This is analogous to what `eth_estimateGas` would return. Does not include L1 fees.
*/
totalGasSpent?: bigint
/**
* The amount of ether used by this transaction. Does not include L1 fees.
*/
amountSpent?: bigint
/**
* The value that accrues to the miner by this transaction.
*/
minerValue?: bigint
} Furthermore you can actually write javascript to arbitrarily plug into the EVM events. You can even modify the evm in the middle of the call if you want import { DefensiveNullCheckError } from '@tevm/errors'
import { bytesToHex, invariant, numberToHex } from '@tevm/utils'
/**
* @internal
* Prepares a trace to be listened to. If laizlyRun is true, it will return an object with the trace and not run the evm internally
* @param {import('@tevm/vm').Vm} vm
* @param {import('@tevm/node').TevmNode['logger']} logger
* @param {import('@tevm/evm').EvmRunCallOpts} params
* @param {boolean} [lazilyRun]
* @returns {Promise<import('@tevm/evm').EvmResult & {trace: import('../debug/DebugResult.js').DebugTraceCallResult}>}
* @throws {never}
*/
export const runCallWithTrace = async (vm, logger, params, lazilyRun = false) => {
/**
* As the evm runs we will be updating this trace object
* and then returning it
*/
const trace = {
gas: 0n,
/**
* @type {import('@tevm/utils').Hex}
*/
returnValue: '0x0',
failed: false,
/**
* @type {Array<import('../debug/DebugResult.js').DebugTraceCallResult['structLogs'][number]>}
*/
structLogs: [],
}
/**
* On every step push a struct log
*/
vm.evm.events?.on('step', async (step, next) => {
logger.debug(step, 'runCallWithTrace: new evm step')
trace.structLogs.push({
pc: step.pc,
op: step.opcode.name,
gasCost: BigInt(step.opcode.fee) + (step.opcode.dynamicFee ?? 0n),
gas: step.gasLeft,
depth: step.depth,
stack: step.stack.map((code) => numberToHex(code)),
})
next?.()
})
/**
* After any internal call push error if any
*/
vm.evm.events?.on('afterMessage', (data, next) => {
logger.debug(data.execResult, 'runCallWithTrace: new message result')
if (data.execResult.exceptionError !== undefined && trace.structLogs.length > 0) {
// Mark last opcode trace as error if exception occurs
const nextLog = trace.structLogs[trace.structLogs.length - 1]
invariant(nextLog, new DefensiveNullCheckError('No structLogs to mark as error'))
// TODO fix this type
Object.assign(nextLog, {
error: data.execResult.exceptionError,
})
}
next?.()
})
if (lazilyRun) {
// TODO internally used function is not typesafe here
return /** @type any*/ ({ trace })
}
const runCallResult = await vm.evm.runCall(params)
logger.debug(runCallResult, 'runCallWithTrace: evm run call complete')
trace.gas = runCallResult.execResult.executionGasUsed
trace.failed = runCallResult.execResult.exceptionError !== undefined
trace.returnValue = bytesToHex(runCallResult.execResult.returnValue)
return {
...runCallResult,
trace,
}
} |
Supporting transaction simulation would be an awesome usecase for Helios. We need to think about what exactly this endpoint would look like.
The simplest case would be to have the same interface as
eth_call
but returning a receipt for the simulated transaction. Two additional features we could add is showing a state diff from the transaction, or even to support allowing arbitrary rpc calls on the post transaction state.The text was updated successfully, but these errors were encountered: