@owlprotocol/contracts-account-abstraction
Version:
ERC4337 Account Abstraction support for deploying relevant smart contracts and interacting with UserOps.
232 lines (208 loc) • 7.35 kB
text/typescript
import { Address, StateOverride, Client, Hex, encodeFunctionData, decodeErrorResult, decodeFunctionResult } from "viem";
import { simulateEntryPoint } from "./simulateEntryPoint.js";
import { parseFailedOpWithRevert } from "../../gasestimation/parseFailedOpWithRevert.js";
import { abi as EntryPointV07SimulationsAbi } from "../../artifacts/EntryPointSimulations.js";
import {
PackedUserOperation,
ExecutionResult,
TargetCallResult,
targetCallResultSchema,
ExecutionError,
ExecutionErrors,
ValidationErrors,
} from "../../models/index.js";
export type SimulateHandleOpResult<TypeResult extends "failed" | "execution" = "failed" | "execution"> = {
result: TypeResult;
data: TypeResult extends "failed"
? string
: {
callDataResult?: TargetCallResult;
executionResult: ExecutionResult;
};
code?: TypeResult extends "failed" ? number : undefined;
};
/**
* Similar to `simulateHandleOpV07` but reverts on error.
* Also a higher-level abstraction as:
* - targetAddress = userOp.sender
* - targetCallData = userOp.callData
* @param packedUserOperation
* @param entryPoint
* @param client
* @param entryPointSimulationsAddress
* @param stateOverride
* @returns
*/
export async function getExecutionResult(
client: Client,
parameters: {
packedUserOperation: PackedUserOperation;
entryPoint: Address;
entryPointSimulationsAddress: Address;
stateOverride?: StateOverride[number];
},
): Promise<SimulateHandleOpResult<"execution">> {
const { packedUserOperation, entryPoint, entryPointSimulationsAddress, stateOverride } = parameters;
const error = await simulateHandleOpV07(client, {
packedUserOperation,
entryPoint,
targetAddress: packedUserOperation.sender,
targetCallData: packedUserOperation.callData,
entryPointSimulationsAddress,
stateOverride,
});
if (error.result === "failed") {
throw new ExecutionError(
`UserOperation reverted during simulation with reason: ${error.data}`,
ExecutionErrors.UserOperationReverted,
);
}
return error as SimulateHandleOpResult<"execution">;
}
/**
* Simulate UserOpV7. Low level function meant to align with Solidity interface and
* decode revert data.
* @param packedUserOperation
* @param entryPoint
* @param client
* @param targetAddress currently all calls use userOp.sender but this is here to align with the interface
* @param targetCallData currently all calls use userOp.callData but this is here to align with the interface
* @param entryPointSimulationsAddress
* @param stateOverride
* @returns
*/
export async function simulateHandleOpV07(
client: Client,
parameters: {
packedUserOperation: PackedUserOperation;
entryPoint: Address;
targetAddress: Address;
targetCallData: Hex;
entryPointSimulationsAddress: Address;
stateOverride?: StateOverride[number] | undefined;
},
): Promise<SimulateHandleOpResult> {
const {
packedUserOperation,
entryPoint,
targetAddress,
targetCallData,
entryPointSimulationsAddress,
stateOverride,
} = parameters;
const entryPointSimulationsSimulateHandleOpCallData = encodeFunctionData({
abi: EntryPointV07SimulationsAbi,
functionName: "simulateHandleOp",
args: [packedUserOperation],
});
const entryPointSimulationsSimulateTargetCallData = encodeFunctionData({
abi: EntryPointV07SimulationsAbi,
functionName: "simulateCallData",
args: [packedUserOperation, targetAddress, targetCallData],
});
const cause = await simulateEntryPoint(client, {
entryPoint,
entryPointSimulationsCallData: [
entryPointSimulationsSimulateHandleOpCallData,
entryPointSimulationsSimulateTargetCallData,
],
entryPointSimulationsAddress,
stateOverride,
});
try {
const executionResult = getSimulateHandleOpResult(cause[0]);
if (executionResult.result === "failed") {
return executionResult;
}
const targetCallValidationResult = validateTargetCallDataResult(cause[1]);
if (targetCallValidationResult.result === "failed") {
return targetCallValidationResult;
}
return {
result: "execution",
data: {
callDataResult: targetCallValidationResult.data,
executionResult: (executionResult as SimulateHandleOpResult<"execution">).data.executionResult,
},
};
} catch (e) {
return {
result: "failed",
data: "Unknown error, could not parse simulate handle op result.",
code: ValidationErrors.SimulateValidation,
};
}
}
function getSimulateHandleOpResult(data: Hex): SimulateHandleOpResult {
try {
const decodedError = decodeErrorResult({
abi: EntryPointV07SimulationsAbi,
data: data,
});
if (decodedError && decodedError.errorName === "FailedOp" && decodedError.args) {
return {
result: "failed",
data: decodedError.args[1] as string,
code: ValidationErrors.SimulateValidation,
} as const;
}
if (decodedError && decodedError.errorName === "FailedOpWithRevert" && decodedError.args) {
return {
result: "failed",
data: parseFailedOpWithRevert(decodedError.args?.[2] as Hex),
code: ValidationErrors.SimulateValidation,
} as const;
}
} catch {
// no error we go the result
const decodedResult: ExecutionResult = decodeFunctionResult({
abi: EntryPointV07SimulationsAbi,
functionName: "simulateHandleOp",
data,
}) as unknown as ExecutionResult;
return {
result: "execution",
data: {
executionResult: decodedResult,
} as const,
};
}
throw new Error("Unexpected error");
}
function validateTargetCallDataResult(data: Hex):
| {
result: "success";
data: TargetCallResult;
}
| {
result: "failed";
data: string;
code: number;
} {
try {
const targetCallResult = decodeFunctionResult({
abi: EntryPointV07SimulationsAbi,
functionName: "simulateCallData",
data: data,
});
const parsedTargetCallResult = targetCallResultSchema.parse(targetCallResult);
if (parsedTargetCallResult.success) {
return {
result: "success",
data: parsedTargetCallResult,
} as const;
}
return {
result: "failed",
data: parsedTargetCallResult.returnData,
code: ExecutionErrors.UserOperationReverted,
} as const;
} catch (_e) {
// no error we go the result
return {
result: "failed",
data: "Unknown error, could not parse target call data result.",
code: ExecutionErrors.UserOperationReverted,
} as const;
}
}