diff --git a/packages/core/src/internal/execution/abi.ts b/packages/core/src/internal/execution/abi.ts index 3e80188eb..3b1207fce 100644 --- a/packages/core/src/internal/execution/abi.ts +++ b/packages/core/src/internal/execution/abi.ts @@ -611,7 +611,7 @@ function ethersResultIntoEvmTuple( * @param iface The interface to search in. * @param functionName The function name to search for. MUST be validated first. */ -function getFunctionFragment( +export function getFunctionFragment( iface: Interface, functionName: string ): FunctionFragment { @@ -639,7 +639,10 @@ function getFunctionFragment( * @param iface The interface to search in. * @param eventName The event name to search for. MUST be validated first. */ -function getEventFragment(iface: Interface, eventName: string): EventFragment { +export function getEventFragment( + iface: Interface, + eventName: string +): EventFragment { const { ethers } = require("ethers") as typeof import("ethers"); // TODO: Add support for event overloading diff --git a/packages/core/src/internal/validation/futures/validateNamedContractDeployment.ts b/packages/core/src/internal/validation/futures/validateNamedContractDeployment.ts index f10b72b86..b855dc3fd 100644 --- a/packages/core/src/internal/validation/futures/validateNamedContractDeployment.ts +++ b/packages/core/src/internal/validation/futures/validateNamedContractDeployment.ts @@ -14,6 +14,7 @@ import { filterToAccountRuntimeValues, retrieveNestedRuntimeValues, validateAccountRuntimeValue, + validateArgs, } from "../utils"; export async function validateNamedContractDeployment( @@ -75,6 +76,10 @@ export async function validateNamedContractDeployment( name: missingParams[0].name, }) ); + } else { + errors.push( + ...validateArgs(artifact, future.constructorArgs, deploymentParameters) + ); } if (isModuleParameterRuntimeValue(future.value)) { diff --git a/packages/core/src/internal/validation/utils.ts b/packages/core/src/internal/validation/utils.ts index 527b228d3..9c167e9a9 100644 --- a/packages/core/src/internal/validation/utils.ts +++ b/packages/core/src/internal/validation/utils.ts @@ -1,15 +1,23 @@ import { IgnitionError } from "../../errors"; import { isAccountRuntimeValue, + isContractFuture, isFuture, + isNamedStaticCallFuture, + isReadEventArgumentFuture, isRuntimeValue, } from "../../type-guards"; +import { Artifact } from "../../types/artifact"; +import { DeploymentParameters } from "../../types/deploy"; import { AccountRuntimeValue, ArgumentType, + ReadEventArgumentFuture, RuntimeValue, + StaticCallFuture, } from "../../types/module"; import { ERRORS } from "../errors-list"; +import { getEventFragment, getFunctionFragment } from "../execution/abi"; export function validateAccountRuntimeValue( arv: AccountRuntimeValue, @@ -72,3 +80,171 @@ function checkForValues( return null; } + +export function validateArgs( + artifact: Artifact, + args: ArgumentType[], + deploymentParameters: DeploymentParameters, + functionName?: string +): IgnitionError[] { + // loop through, probably recursively, and resolve to primitive type strings + const argTypes = args.map((arg) => + resolveTypeForArg(artifact, arg, deploymentParameters) + ); + + // get an array of primitive type strings representing the valid type for each arg, derived from the ABI and likely with help from ethers + const validTypes = resolveValidABITypesForArgs(artifact, functionName); + + // compare each argument against the valid type + const errors = []; + for (let i = 0; i < argTypes.length; i++) { + if (argTypes[i] !== validTypes[i]) { + errors.push(new IgnitionError("invalid arg type")); + } + } + + return errors; +} + +function resolveTypeForArg( + artifact: Artifact, + arg: ArgumentType, + deploymentParameters: DeploymentParameters +): string { + if (isRuntimeValue(arg)) { + if (isAccountRuntimeValue(arg)) { + return "string"; + } + + return ( + typeof deploymentParameters[arg.moduleId]?.[arg.name] ?? arg.defaultValue + ); + } + + if (isFuture(arg)) { + if (isContractFuture(arg)) { + return "string"; + } + + if (isNamedStaticCallFuture(arg)) { + return resolveTypeForStaticCall(artifact, arg); + } + + if (isReadEventArgumentFuture(arg)) { + return resolveTypeForEvent(artifact, arg); + } + } + + // todo: handle arrays + if (Array.isArray(arg)) { + return "array"; + } + + // todo: handle objects + if (typeof arg === "object") { + return "object"; + } + + return typeof arg; +} + +function resolveTypeForStaticCall( + artifact: Artifact, + future: StaticCallFuture +): string { + const { ethers } = require("ethers") as typeof import("ethers"); + + const iface = new ethers.Interface(artifact.abi); + const functionFragment = getFunctionFragment(iface, future.functionName); + + if (typeof future.nameOrIndex === "string") { + for (const output of functionFragment.outputs) { + if (output.name === future.nameOrIndex) { + return resolveABITypeToPrimitive(output.baseType); + } + } + + throw new Error(`output name not found: ${future.nameOrIndex}`); + } else { + return resolveABITypeToPrimitive( + functionFragment.outputs[future.nameOrIndex].baseType + ); + } +} + +function resolveTypeForEvent( + artifact: Artifact, + future: ReadEventArgumentFuture +): string { + const { ethers } = require("ethers") as typeof import("ethers"); + + const iface = new ethers.Interface(artifact.abi); + const eventFragment = getEventFragment(iface, future.eventName); + + if (typeof future.nameOrIndex === "string") { + for (const input of eventFragment.inputs) { + if (input.name === future.nameOrIndex) { + return resolveABITypeToPrimitive(input.baseType); + } + } + + throw new Error(`input name not found: ${future.nameOrIndex}`); + } else { + return resolveABITypeToPrimitive( + eventFragment.inputs[future.nameOrIndex].baseType + ); + } +} + +function resolveValidABITypesForArgs( + artifact: Artifact, + functionName?: string +): string[] { + const { ethers } = require("ethers") as typeof import("ethers"); + + const iface = new ethers.Interface(artifact.abi); + const fragment = + functionName === undefined + ? iface.deploy + : getFunctionFragment(iface, functionName); + + return fragment.inputs.map((input) => + resolveABITypeToPrimitive(input.baseType) + ); +} + +function resolveABITypeToPrimitive(abiType: string): string { + if (abiType.startsWith("array")) { + return "array"; + } + + if (abiType.startsWith("tuple")) { + return "object"; + } + + if (abiType.startsWith("uint") || abiType.startsWith("int")) { + return "bigint"; + } + + if (abiType.startsWith("fixed") || abiType.startsWith("ufixed")) { + return "number"; + } + + if (abiType.startsWith("bool")) { + return "boolean"; + } + + if (abiType.startsWith("bytes")) { + return "string"; + } + + if (abiType.startsWith("address")) { + return "string"; + } + + if (abiType.startsWith("string")) { + return "string"; + } + + throw new Error(`unrecognized ABI type: ${abiType}`); +}