UNPKG

@tevm/actions

Version:

A typesafe library for writing forge scripts in typescript

177 lines (167 loc) 5.99 kB
import { createAddress } from '@tevm/address' import { DecodeFunctionDataError, InvalidRequestError, RevertError } from '@tevm/errors' import { decodeErrorResult, decodeFunctionResult, encodeFunctionData, isHex } from '@tevm/utils' import { callHandler } from '../Call/callHandler.js' import { maybeThrowOnFail } from '../internal/maybeThrowOnFail.js' import { validateContractParams } from './validateContractParams.js' /** * Creates a tree-shakable instance of `contractHandler` for handling contract interactions with the Ethereumjs EVM. * This function uses `callHandler` under the hood to execute contract calls. * * Note: This is the internal logic used by higher-level APIs such as `tevmContract`. * * @param {import('@tevm/node').TevmNode} client - The TEVM base client instance. * @param {object} [options] - Optional parameters. * @param {boolean} [options.throwOnFail=true] - Whether to throw an error on failure. * @returns {import("../Contract/ContractHandlerType.js").ContractHandler} The contract handler function. * @throws {import('./TevmCallError.js').TevmCallError} If `throwOnFail` is true, returns `TevmCallError` as value. * * @example * ```typescript * import { createTevmNode } from 'tevm/node' * import { contractHandler } from 'tevm/actions' * * const client = createTevmNode() * * const contract = contractHandler(client) * * const res = await contract({ * to: `0x${'69'.repeat(20)}`, * abi: [{...}], // ABI array * functionName: 'myFunction', * args: [1, 2, 3], * }) * ``` */ export const contractHandler = (client, { throwOnFail: throwOnFailDefault = true } = {}) => async (params) => { const errors = validateContractParams(/** @type any*/ (params)) if (errors.length > 0) { client.logger.debug(errors, 'contractHandler: Invalid params') return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { errors, executionGasUsed: 0n, rawData: '0x', }) } const vm = await client.getVm() const contract = params.to && (await vm.evm.stateManager.getCode(createAddress(params.to))) const precompile = params.to && vm.evm.getPrecompile(createAddress(params.to)) if (!params.deployedBytecode && !params.code && contract && contract?.length === 0 && !precompile) { client.logger.debug( { contract, precompile, to: params.to }, 'contractHandler: No contract bytecode nor precompile was found at specified `to` address. Unable to execute contract call.', ) return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { rawData: '0x', executionGasUsed: 0n, errors: [ { _tag: 'InvalidRequestError', name: 'InvalidRequestError', message: `Contract at address ${params.to} does not exist`, }, ], }) } let functionData try { functionData = encodeFunctionData( /** @type {any} */ ({ abi: params.abi, functionName: params.functionName, args: params.args, }), ) } catch (e) { client.logger.debug(e, 'contractHandler: Unable to encode the abi, functionName, and args into hex data') const cause = /** @type {Error}*/ (e) const err = new InvalidRequestError(cause.message, { cause }) return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { rawData: '0x', executionGasUsed: 0n, errors: [err], }) } client.logger.debug( functionData, 'contractHandler: Encoded data, functionName, and args into hex data to execute call', ) const result = await callHandler(client, { throwOnFail: throwOnFailDefault, })({ ...params, throwOnFail: false, data: functionData, }) if (result.errors && result.errors.length > 0) { result.errors = result.errors.map((err) => { if (isHex(err.message) && err instanceof RevertError) { client.logger.debug(err, 'contractHandler: Contract revert error. Decoding the error') /** * @type {undefined |ReturnType<typeof decodeErrorResult>} */ let decodedError = undefined try { decodedError = decodeErrorResult( /** @type {any} */ ({ abi: params.abi, data: err.message, functionName: params.functionName, }), ) const message = `Revert: ${decodedError?.errorName ?? `There was a revert with no revert message ${err.message}`}` client.logger.debug(err, message) return new RevertError(message, { cause: err }) } catch (e) { client.logger.debug(e, 'There was an error decoding error result') return err } } return err }) client.logger.debug(result.errors, 'contractHandler: Execution errors') return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, result) } let decodedResult try { decodedResult = decodeFunctionResult( /** @type {any} */ ({ abi: params.abi, data: result.rawData, functionName: params.functionName, }), ) } catch (e) { if (result.rawData === '0x') { return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { errors: [ new Error( '0x data returned from EVM with no error message. This indicates a the contract was missing or bug in Tevm if no other errors were thrown', ), ], rawData: '0x', executionGasUsed: 0n, }) } client.logger.debug(e, 'contractHandler: Error decoding returned call data with provided abi and functionName') const cause = /** @type {Error}*/ (e) const err = new DecodeFunctionDataError(cause.message, { cause }) return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { debugContext: { abi: params.abi, rawData: result.rawData, functionName: params.functionName, }, rawData: '0x', executionGasUsed: 0n, errors: [err], }) } client.logger.debug(decodedResult, 'contractHandler: decoded data into a final result') return maybeThrowOnFail(params.throwOnFail ?? throwOnFailDefault, { .../** @type any */ (result), data: decodedResult, }) }