diff --git a/package.json b/package.json index 46d23c16..16b5b885 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "root", "private": true, - "version": "4.0.3", + "version": "4.0.4", "engines": { "node": ">=18.0.0" }, diff --git a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts index 64592f98..517dd7dd 100644 --- a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts +++ b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts @@ -156,8 +156,8 @@ export class EntryPointV7Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride as any, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const decodedResult = decodeFunctionResult({ @@ -259,8 +259,8 @@ export class EntryPointV7Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride as any, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const decodedResult = decodeFunctionResult({ @@ -474,8 +474,8 @@ export class EntryPointV7Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const res = decodeFunctionResult({ @@ -503,7 +503,11 @@ export class EntryPointV7Service implements IEntryPointService { try { const errorResult = await this.publicClient.request({ method: "eth_call", - params: [{ to: this.address, data }, "latest", stateOverride], + params: [ + { to: this.address, data }, + "latest", + ...(stateOverride ? [stateOverride] : []), + ] as any, }); return this.parseValidationResult(userOp, errorResult); } catch (error: any) { diff --git a/packages/executor/src/services/EntryPointService/versions/0.0.8.ts b/packages/executor/src/services/EntryPointService/versions/0.0.8.ts index 3dc8fa48..4d83f186 100644 --- a/packages/executor/src/services/EntryPointService/versions/0.0.8.ts +++ b/packages/executor/src/services/EntryPointService/versions/0.0.8.ts @@ -181,8 +181,8 @@ export class EntryPointV8Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride as any, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const decodedResult = decodeFunctionResult({ @@ -284,8 +284,8 @@ export class EntryPointV8Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride as any, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const decodedResult = decodeFunctionResult({ @@ -509,8 +509,8 @@ export class EntryPointV8Service implements IEntryPointService { gasLimit: gasLimit !== undefined ? toHex(gasLimit) : undefined, }, "latest", - stateOverride, - ], + ...(stateOverride ? [stateOverride] : []), + ] as any, }); const res = decodeFunctionResult({ @@ -539,7 +539,11 @@ export class EntryPointV8Service implements IEntryPointService { try { const errorResult = await this.publicClient.request({ method: "eth_call", - params: [{ to: this.address, data }, "latest", stateOverride], + params: [ + { to: this.address, data }, + "latest", + ...(stateOverride ? [stateOverride] : []), + ] as any, }); return this.parseValidationResult(userOp, errorResult); } catch (error: any) { diff --git a/packages/executor/test/unit/services/eth_call_state_override_params.test.ts b/packages/executor/test/unit/services/eth_call_state_override_params.test.ts new file mode 100644 index 00000000..99d6ef8c --- /dev/null +++ b/packages/executor/test/unit/services/eth_call_state_override_params.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from "vitest"; +import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; +import { EntryPointV7Service } from "../../../src/services/EntryPointService/versions/0.0.7"; +import { EntryPointV8Service } from "../../../src/services/EntryPointService/versions/0.0.8"; + +const ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; +const PIMLICO_SIMULATIONS = "0x000000000049883a1a4f4b3c1b1b0ed0c43e5e1e"; +const EP_SIMULATIONS = "0x00000000009a9d4ba6ac9a1c0c67f2dd6a2c1a3f"; + +const userOp: UserOperation = { + sender: "0x54E3d8E0F9800440581D6d82C7A85F1167090d98", + nonce: "0x01", + callData: "0x", + callGasLimit: "0x3e93f", + verificationGasLimit: "0x1aaf8", + preVerificationGas: "0x10a2b", + maxFeePerGas: "0xd59f80", + maxPriorityFeePerGas: "0x1e8480", + signature: "0x", +}; + +const networkConfig = { + pimlicoSimulationsContract: PIMLICO_SIMULATIONS, + epSimulationsContract: EP_SIMULATIONS, + binarySearchMaxRetries: 3, + gasFeeInSimulation: false, +} as any; + +const logger = { + // eslint-disable-next-line @typescript-eslint/no-empty-function + debug: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + info: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + warn: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + error: () => {}, +} as any; + +/** + * Records every eth_call params array, then fails the call so we never have to + * mock a decodable return value - the shape of `params` is the whole point. + */ +function recordingClient(): { client: any; params: unknown[][] } { + const params: unknown[][] = []; + const client = { + request: async (req: any) => { + if (req.method === "eth_call") params.push(req.params); + throw new Error("recorded"); + }, + }; + return { client, params }; +} + +type Service = EntryPointV7Service | EntryPointV8Service; + +const versions: [string, (client: any) => Service][] = [ + [ + "EntryPoint v0.0.7", + (client) => + new EntryPointV7Service(ENTRY_POINT, networkConfig, client, logger), + ], + [ + "EntryPoint v0.0.8", + (client) => + new EntryPointV8Service(ENTRY_POINT, networkConfig, client, logger), + ], +]; + +describe.each(versions)( + "%s eth_call state override param", + (_name, makeService) => { + it("omits the third param entirely when no state override is supplied", async () => { + const { client, params } = recordingClient(); + await makeService(client) + .simulateHandleOpUsingSimulatorContracts(userOp) + .catch(() => undefined); + + expect(params.length).toBeGreaterThan(0); + for (const p of params) { + // A trailing `undefined` JSON-serializes to `null`, which strict nodes + // (e.g. Monad) reject with -32602 Invalid params before any execution. + expect(p).toHaveLength(2); + expect(p[1]).toEqual("latest"); + expect(JSON.stringify(p)).not.toContain("null"); + } + }); + + it("passes the override as the third param when one is supplied", async () => { + const { client, params } = recordingClient(); + const overrides = { + "0x00000000000000000000000000000000deadbeef": { balance: "0x1" }, + } as any; + await makeService(client) + .simulateHandleOpUsingSimulatorContracts(userOp, overrides) + .catch(() => undefined); + + expect(params.length).toBeGreaterThan(0); + for (const p of params) { + expect(p).toHaveLength(3); + expect(p[2]).toEqual(overrides); + } + }); + } +);