Files
wax/packages/plugins/test/e2e/utils/userOpUtils.ts
2024-04-21 03:34:11 +04:00

215 lines
5.3 KiB
TypeScript

import {
AbiCoder,
BigNumberish,
BytesLike,
concat,
hexlify,
isHexString,
keccak256,
} from "ethers";
import { PackedUserOperationStruct } from "../../../typechain-types/lib/account-abstraction/contracts/core/EntryPoint";
/**
* @notice these utils have been largely copied from ERC4337Utils.ts in eth-infinitism's
* bundler repo, which form part of @account-abstraction/utils. This is because
* @account-abstraction/utils v0.7.0 has not been published on npm yet. These utility
* function can be swapped out once v0.7.0 has been published.
*
* The only changes were to update ethers functionality from v5 to v6
*/
export type PackedUserOperation = PackedUserOperationStruct;
export type UserOperation = {
sender: string;
nonce: BigNumberish;
factory?: string;
factoryData?: BytesLike;
callData: BytesLike;
callGasLimit: BigNumberish;
verificationGasLimit: BigNumberish;
preVerificationGas: BigNumberish;
maxFeePerGas: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
paymaster?: string;
paymasterVerificationGasLimit?: BigNumberish;
paymasterPostOpGasLimit?: BigNumberish;
paymasterData?: BytesLike;
signature: BytesLike;
};
export type FactoryParams = {
factory: string;
factoryData?: BytesLike;
};
/**
* calculate the userOpHash of a given userOperation.
* The userOpHash is a hash of all UserOperation fields, except the "signature" field.
* The entryPoint uses this value in the emitted UserOperationEvent.
* A wallet may use this value as the hash to sign (the SampleWallet uses this method)
* @param op
* @param entryPoint
* @param chainId
*/
export function getUserOpHash(
op: UserOperation,
entryPoint: string,
chainId: number
): string {
const userOpHash = keccak256(encodeUserOp(op, true));
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
const enc = defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[userOpHash, entryPoint, chainId]
);
return keccak256(enc);
}
/**
* abi-encode the userOperation
* @param op a PackedUserOp
* @param forSignature "true" if the hash is needed to calculate the getUserOpHash()
* "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain.
*/
export function encodeUserOp(
op1: PackedUserOperation | UserOperation,
forSignature = true
): string {
// if "op" is unpacked UserOperation, then pack it first, before we ABI-encode it.
let op: PackedUserOperation;
if ("callGasLimit" in op1) {
op = packUserOp(op1);
} else {
op = op1;
}
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
if (forSignature) {
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"bytes32",
"uint256",
"bytes32",
"bytes32",
],
[
op.sender,
op.nonce,
keccak256(op.initCode),
keccak256(op.callData),
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
keccak256(op.paymasterAndData),
]
);
} else {
// for the purpose of calculating gas cost encode also signature (and no keccak of bytes)
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes",
"bytes",
"bytes32",
"uint256",
"bytes32",
"bytes",
"bytes",
],
[
op.sender,
op.nonce,
op.initCode,
op.callData,
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
op.paymasterAndData,
op.signature,
]
);
}
}
export function packUserOp(op: UserOperation): PackedUserOperation {
let paymasterAndData: BytesLike;
if (op.paymaster == null) {
paymasterAndData = "0x";
} else {
if (
op.paymasterVerificationGasLimit == null ||
op.paymasterPostOpGasLimit == null
) {
throw new Error("paymaster with no gas limits");
}
paymasterAndData = packPaymasterData(
op.paymaster,
op.paymasterVerificationGasLimit,
op.paymasterPostOpGasLimit,
op.paymasterData
);
}
return {
sender: op.sender,
nonce: "0x" + BigInt(op.nonce).toString(16),
initCode:
op.factory == null ? "0x" : concat([op.factory, op.factoryData ?? ""]),
callData: op.callData,
accountGasLimits: packUint(op.verificationGasLimit, op.callGasLimit),
preVerificationGas: "0x" + BigInt(op.preVerificationGas).toString(16),
gasFees: packUint(op.maxPriorityFeePerGas, op.maxFeePerGas),
paymasterAndData,
signature: op.signature,
};
}
export function packPaymasterData(
paymaster: string,
paymasterVerificationGasLimit: BigNumberish,
postOpGasLimit: BigNumberish,
paymasterData?: BytesLike
): BytesLike {
return concat([
paymaster,
packUint(paymasterVerificationGasLimit, postOpGasLimit),
paymasterData ?? "0x",
]);
}
export function packUint(high128: BigNumberish, low128: BigNumberish): string {
high128 = BigInt(high128);
low128 = BigInt(low128);
const packed = "0x" + ((high128 << 128n) + low128).toString(16);
return hexZeroPad(packed, 32);
}
/**
* @notice Copied from ethers v5.7 as the ethers v6 equivalent behaves
* in a way the bundler doesn't expect and an error is thrown
*/
export function hexZeroPad(value: BytesLike, length: number): string {
if (typeof value !== "string") {
value = hexlify(value);
} else if (!isHexString(value)) {
console.log("invalid hex string", "value", value);
}
if (value.length > 2 * length + 2) {
console.log("value out of range", "value", arguments[1]);
}
while (value.length < 2 * length + 2) {
value = "0x0" + value.substring(2);
}
return value;
}