diff --git a/packages/core/src/internal/execution/future-processor/helpers/future-resolvers.ts b/packages/core/src/internal/execution/future-processor/helpers/future-resolvers.ts index 54cc84b9d..4b034b757 100644 --- a/packages/core/src/internal/execution/future-processor/helpers/future-resolvers.ts +++ b/packages/core/src/internal/execution/future-processor/helpers/future-resolvers.ts @@ -131,8 +131,14 @@ export function resolveFutureData( return data; } - // this type coercion is safe because we know the type of data is EncodeFunctionCallFuture - return findResultForFutureById(deploymentState, data.id) as string; + const result = findResultForFutureById(deploymentState, data.id); + + assertIgnitionInvariant( + typeof result === "string", + "Expected future data to be a string" + ); + + return result; } /** diff --git a/packages/core/src/internal/execution/types/execution-state.ts b/packages/core/src/internal/execution/types/execution-state.ts index a1cb827d0..519f02b47 100644 --- a/packages/core/src/internal/execution/types/execution-state.ts +++ b/packages/core/src/internal/execution/types/execution-state.ts @@ -149,7 +149,7 @@ export interface EncodeFunctionCallExecutionState artifactId: string; functionName: string; args: SolidityParameterType[]; - result?: string; + result: string; } /** diff --git a/packages/core/src/internal/module-builder.ts b/packages/core/src/internal/module-builder.ts index 626903b48..40d3d7f60 100644 --- a/packages/core/src/internal/module-builder.ts +++ b/packages/core/src/internal/module-builder.ts @@ -73,6 +73,7 @@ import { assertIgnitionInvariant } from "./utils/assertions"; import { toCallFutureId, toContractFutureId, + toEncodeFunctionCallFutureId, toReadEventArgumentFutureId, toSendDataFutureId, } from "./utils/future-id-builders"; @@ -637,7 +638,7 @@ class IgnitionModuleBuilderImplementation< ); } - const futureId = toCallFutureId( + const futureId = toEncodeFunctionCallFutureId( this._module.id, options.id, contractFuture.module.id, diff --git a/packages/core/src/internal/reconciliation/helpers/reconcile-data.ts b/packages/core/src/internal/reconciliation/helpers/reconcile-data.ts index 5072495d7..60f8e5ae9 100644 --- a/packages/core/src/internal/reconciliation/helpers/reconcile-data.ts +++ b/packages/core/src/internal/reconciliation/helpers/reconcile-data.ts @@ -1,5 +1,6 @@ import { SendDataFuture } from "../../../types/module"; import { SendDataExecutionState } from "../../execution/types/execution-state"; +import { assertIgnitionInvariant } from "../../utils/assertions"; import { findResultForFutureById } from "../../views/find-result-for-future-by-id"; import { ReconciliationContext, @@ -17,10 +18,15 @@ export function reconcileData( return compare(future, "Data", exState.data, future.data ?? "0x"); } - return compare( - future, - "Data", - exState.data, - findResultForFutureById(context.deploymentState, future.data.id) as string + const newData = findResultForFutureById( + context.deploymentState, + future.data.id ); + + assertIgnitionInvariant( + typeof newData === "string", + "Expected data to be a string" + ); + + return compare(future, "Data", exState.data, newData); } diff --git a/packages/core/src/internal/utils/future-id-builders.ts b/packages/core/src/internal/utils/future-id-builders.ts index 981336b88..75fafa888 100644 --- a/packages/core/src/internal/utils/future-id-builders.ts +++ b/packages/core/src/internal/utils/future-id-builders.ts @@ -53,7 +53,7 @@ export function toContractFutureId( } /** - * Construct the future id for a call, static call, or encoded function call, namespaced by the moduleId. + * Construct the future id for a call or static call, namespaced by the moduleId. * * @param moduleId - the id of the module the future is part of * @param userProvidedId - the overriding id provided by the user (it will still @@ -87,6 +87,40 @@ export function toCallFutureId( return `${moduleId}${MODULE_SEPERATOR}${submoduleContractId}${SUBKEY_SEPERATOR}${functionName}`; } +/** + * Construct the future id for an encoded function call, namespaced by the moduleId. + * + * @param moduleId - the id of the module the future is part of + * @param userProvidedId - the overriding id provided by the user (it will still + * be namespaced) + * @param contractName - the contract or library name that forms part of the + * fallback + * @param functionName - the function name that forms part of the fallback + * @returns the future id + */ +export function toEncodeFunctionCallFutureId( + moduleId: string, + userProvidedId: string | undefined, + contractModuleId: string, + contractId: string, + functionName: string +) { + if (userProvidedId !== undefined) { + return `${moduleId}${MODULE_SEPERATOR}${userProvidedId}`; + } + + if (moduleId === contractModuleId) { + return `${moduleId}${MODULE_SEPERATOR}encodeFunctionCall(${contractId}${SUBKEY_SEPERATOR}${functionName})`; + } + + // We replace the MODULE_SEPARATOR for SUBMODULE_SEPARATOR + const submoduleContractId = `${contractModuleId}${SUBMODULE_SEPARATOR}${contractId.substring( + contractModuleId.length + MODULE_SEPERATOR.length + )}`; + + return `${moduleId}${MODULE_SEPERATOR}encodeFunctionCall(${submoduleContractId}${SUBKEY_SEPERATOR}${functionName})`; +} + /** * Construct the future id for a read event argument future, namespaced by * the moduleId. diff --git a/packages/core/src/internal/validation/futures/validateNamedEncodeFunctionCall.ts b/packages/core/src/internal/validation/futures/validateNamedEncodeFunctionCall.ts index c04eddd00..25b75a030 100644 --- a/packages/core/src/internal/validation/futures/validateNamedEncodeFunctionCall.ts +++ b/packages/core/src/internal/validation/futures/validateNamedEncodeFunctionCall.ts @@ -8,13 +8,17 @@ import { DeploymentParameters } from "../../../types/deploy"; import { EncodeFunctionCallFuture } from "../../../types/module"; import { ERRORS } from "../../errors-list"; import { validateArtifactFunction } from "../../execution/abi"; -import { retrieveNestedRuntimeValues } from "../utils"; +import { + filterToAccountRuntimeValues, + retrieveNestedRuntimeValues, + validateAccountRuntimeValue, +} from "../utils"; export async function validateNamedEncodeFunctionCall( future: EncodeFunctionCallFuture, artifactLoader: ArtifactResolver, deploymentParameters: DeploymentParameters, - _accounts: string[] + accounts: string[] ): Promise { const errors: IgnitionError[] = []; @@ -47,6 +51,13 @@ export async function validateNamedEncodeFunctionCall( const runtimeValues = retrieveNestedRuntimeValues(future.args); const moduleParams = runtimeValues.filter(isModuleParameterRuntimeValue); + const accountParams = [...filterToAccountRuntimeValues(runtimeValues)]; + + errors.push( + ...accountParams.flatMap((arv) => + validateAccountRuntimeValue(arv, accounts) + ) + ); const missingParams = moduleParams.filter( (param) => diff --git a/packages/core/src/types/module-builder.ts b/packages/core/src/types/module-builder.ts index a4bbd27e4..4a8221af2 100644 --- a/packages/core/src/types/module-builder.ts +++ b/packages/core/src/types/module-builder.ts @@ -387,9 +387,12 @@ export interface IgnitionModuleBuilder { ): StaticCallFuture; /** - * Encode a function call. + * ABI encode a function call, including both the function's name and + * the parameters it is being called with. This is useful when + * sending a raw transaction to invoke a smart contract or + * when invoking a smart contract proxied through an intermediary function. * - * @param contractFuture - The contract ABI to encode with + * @param contractFuture - The contract that the ABI for encoding will be taken from * @param functionName - The name of the function * @param args - The arguments to pass to the function * @param options - The options for the call @@ -398,7 +401,7 @@ export interface IgnitionModuleBuilder { * ``` * const myContract = m.contract("MyContract"); * const data = m.encodeFunctionCall(myContract, "updateCounter", [100]); - * m.send("callFunctionOnContract", myContract, 0n, data); + * m.send("callUpdateCounter", myContract, 0n, data); * ``` */ encodeFunctionCall< diff --git a/packages/core/test/call.ts b/packages/core/test/call.ts index 8991329d2..c57bf154e 100644 --- a/packages/core/test/call.ts +++ b/packages/core/test/call.ts @@ -887,7 +887,7 @@ m.call(..., { id: "MyUniqueId"})` ); }); - it("should validate a module parameter with a default value that is an AccountRuntimeValue for a negative index", async () => { + it("should not validate a module parameter with a default value that is an AccountRuntimeValue for a negative index", async () => { const fakerArtifact: Artifact = { ...fakeArtifact, abi: [ diff --git a/packages/core/test/encodeFunctionCall.ts b/packages/core/test/encodeFunctionCall.ts index 680ecb4aa..98fe0004b 100644 --- a/packages/core/test/encodeFunctionCall.ts +++ b/packages/core/test/encodeFunctionCall.ts @@ -9,7 +9,7 @@ import { NamedEncodeFunctionCallFutureImplementation, } from "../src/internal/module"; import { getFuturesFromModule } from "../src/internal/utils/get-futures-from-module"; -import { validateNamedContractCall } from "../src/internal/validation/futures/validateNamedContractCall"; +import { validateNamedEncodeFunctionCall } from "../src/internal/validation/futures/validateNamedEncodeFunctionCall"; import { FutureType } from "../src/types/module"; import { @@ -74,7 +74,7 @@ describe("encodeFunctionCall", () => { ); const callFuture = [...moduleWithDependentContracts.futures].find( - ({ id }) => id === "Module1#Example.test" + ({ id }) => id === "Module1#encodeFunctionCall(Module1#Example.test)" ); if (!(callFuture instanceof NamedEncodeFunctionCallFutureImplementation)) { @@ -107,7 +107,7 @@ describe("encodeFunctionCall", () => { ); const callFuture = [...moduleWithDependentContracts.futures].find( - ({ id }) => id === "Module1#Example.test" + ({ id }) => id === "Module1#encodeFunctionCall(Module1#Example.test)" ); if (!(callFuture instanceof NamedEncodeFunctionCallFutureImplementation)) { @@ -318,7 +318,7 @@ describe("encodeFunctionCall", () => { return { sameContract1 }; }), - `The autogenerated future id ("Module1#SameContract.test") is already used. Please provide a unique id, as shown below: + `The autogenerated future id ("Module1#encodeFunctionCall(Module1#SameContract.test)") is already used. Please provide a unique id, as shown below: m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); @@ -383,7 +383,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: {} as any }), {}, @@ -411,7 +411,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver(), {}, @@ -453,7 +453,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver(), {}, @@ -514,7 +514,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver(), {}, @@ -539,7 +539,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: fakeArtifact }), {}, @@ -583,7 +583,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); await assert.isFulfilled( - validateNamedContractCall( + validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: fakeArtifact }), {}, @@ -609,7 +609,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: fakeArtifact }), {}, @@ -655,7 +655,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); await assert.isFulfilled( - validateNamedContractCall( + validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: fakeArtifact }), {}, @@ -698,7 +698,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); await assert.isFulfilled( - validateNamedContractCall( + validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Another: fakeArtifact }), {}, @@ -707,7 +707,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); }); - it("should validate a module parameter with a default value that is an AccountRuntimeValue for a negative index", async () => { + it("should not validate a module parameter with a default value that is an AccountRuntimeValue for a negative index", async () => { const fakerArtifact: Artifact = { ...fakeArtifact, abi: [ @@ -741,7 +741,7 @@ m.encodeFunctionCall(..., { id: "MyUniqueId"})` ); assertValidationError( - await validateNamedContractCall( + await validateNamedEncodeFunctionCall( future as any, setupMockArtifactResolver({ Test: fakerArtifact }), {}, diff --git a/packages/core/test/reconciliation/futures/reconcileNamedEncodeFunctionCall.ts b/packages/core/test/reconciliation/futures/reconcileNamedEncodeFunctionCall.ts index f7b35eee9..82d05cfbb 100644 --- a/packages/core/test/reconciliation/futures/reconcileNamedEncodeFunctionCall.ts +++ b/packages/core/test/reconciliation/futures/reconcileNamedEncodeFunctionCall.ts @@ -48,6 +48,7 @@ describe("Reconciliation - named encode function call", () => { artifactId: "./artifact.json", functionName: "function", args: [], + result: "", }; it("should reconcile unchanged", async () => { @@ -79,7 +80,7 @@ describe("Reconciliation - named encode function call", () => { }, { ...exampleEncodeFunctionCallState, - id: "Submodule#Contract1.function1", + id: "Submodule#encodeFunctionCall(Submodule#Contract1.function1)", futureType: FutureType.ENCODE_FUNCTION_CALL, status: ExecutionStatus.SUCCESS, functionName: "function1", @@ -89,7 +90,7 @@ describe("Reconciliation - named encode function call", () => { ); }); - it("should find changes to contract unreconciliable", async () => { + it("should find changes to future dependencies unreconciliable", async () => { const moduleDefinition = buildModule("Module", (m) => { const contract1 = m.contract("Contract1"); @@ -194,7 +195,7 @@ describe("Reconciliation - named encode function call", () => { }, { ...exampleEncodeFunctionCallState, - id: "Module#Contract1.function1", + id: "Module#encodeFunctionCall(Module#Contract1.function1)", futureType: FutureType.ENCODE_FUNCTION_CALL, status: ExecutionStatus.STARTED, functionName: "function1", @@ -205,90 +206,7 @@ describe("Reconciliation - named encode function call", () => { assert.deepStrictEqual(reconiliationResult.reconciliationFailures, [ { - futureId: "Module#Contract1.function1", - failure: "Argument at index 0 has been changed", - }, - ]); - }); - - it("should reconcile an address arg with entirely different casing", async () => { - const moduleDefinition = buildModule("Module", (m) => { - const contract1 = m.contract("Contract1"); - - m.encodeFunctionCall( - contract1, - "function1", - ["0x15d34aaf54267db7d7c367839aaf71a00a2c6a65"], - {} - ); - - return { contract1 }; - }); - - await assertSuccessReconciliation( - moduleDefinition, - createDeploymentState( - { - ...exampleDeploymentState, - id: "Module#Contract1", - status: ExecutionStatus.SUCCESS, - result: { - type: ExecutionResultType.SUCCESS, - address: differentAddress, - }, - }, - { - ...exampleEncodeFunctionCallState, - id: "Module#Contract1.function1", - futureType: FutureType.ENCODE_FUNCTION_CALL, - status: ExecutionStatus.STARTED, - functionName: "function1", - args: ["0x15D34AAF54267DB7D7C367839AAF71A00A2C6A65"], - } - ) - ); - }); - - it("should fail to reconcile an address arg with partially different casing", async () => { - const moduleDefinition = buildModule("Module", (m) => { - const contract1 = m.contract("Contract1"); - - m.encodeFunctionCall( - contract1, - "function1", - ["0x15d34aaf54267db7d7c367839aaf71a00a2c6a65"], - {} - ); - - return { contract1 }; - }); - - const reconiliationResult = await reconcile( - moduleDefinition, - createDeploymentState( - { - ...exampleDeploymentState, - id: "Module#Contract1", - status: ExecutionStatus.SUCCESS, - result: { - type: ExecutionResultType.SUCCESS, - address: differentAddress, - }, - }, - { - ...exampleEncodeFunctionCallState, - id: "Module#Contract1.function1", - futureType: FutureType.ENCODE_FUNCTION_CALL, - status: ExecutionStatus.STARTED, - functionName: "function1", - args: ["0x15d34aaf54267db7D7c367839aaf71a00a2c6a65"], - } - ) - ); - - assert.deepStrictEqual(reconiliationResult.reconciliationFailures, [ - { - futureId: "Module#Contract1.function1", + futureId: "Module#encodeFunctionCall(Module#Contract1.function1)", failure: "Argument at index 0 has been changed", }, ]); diff --git a/packages/core/test/send.ts b/packages/core/test/send.ts index fa79bff98..2233e567a 100644 --- a/packages/core/test/send.ts +++ b/packages/core/test/send.ts @@ -5,6 +5,7 @@ import { buildModule } from "../src/build-module"; import { AccountRuntimeValueImplementation, ModuleParameterRuntimeValueImplementation, + NamedEncodeFunctionCallFutureImplementation, SendDataFutureImplementation, } from "../src/internal/module"; import { getFuturesFromModule } from "../src/internal/utils/get-futures-from-module"; @@ -248,7 +249,7 @@ describe("send", () => { assert.isDefined(moduleWithDependentContracts); const exampleFuture = [...moduleWithDependentContracts.futures].find( - ({ id }) => id === "Module1#Example.test" + ({ id }) => id === "Module1#encodeFunctionCall(Module1#Example.test)" ); const sendFuture = [...moduleWithDependentContracts.futures].find( @@ -259,6 +260,12 @@ describe("send", () => { assert.fail("Not a send data future"); } + if ( + !(exampleFuture instanceof NamedEncodeFunctionCallFutureImplementation) + ) { + assert.fail("Not an encode function call future"); + } + assert.equal(sendFuture.dependencies.size, 2); assert(sendFuture.dependencies.has(exampleFuture!)); });