@owlprotocol/contracts-account-abstraction
Version:
ERC4337 Account Abstraction support for deploying relevant smart contracts and interacting with UserOps.
207 lines (206 loc) • 7.79 kB
JavaScript
import { describe, test, beforeEach, expect } from "vitest";
import {
createPublicClient,
createWalletClient,
http,
encodeFunctionData,
parseEther,
nonceManager
} from "viem";
import { localhost } from "viem/chains";
import { getLocalAccount } from "@owlprotocol/viem-utils";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { getUserOperationHash } from "viem/account-abstraction";
import { port } from "./test/constants.js";
import { SimpleAccountFactory } from "./artifacts/SimpleAccountFactory.js";
import { getSimpleAccountAddress } from "./SimpleAccount.js";
import { SIMPLE_ACCOUNT_IMPLEMENTATION_ADDRESS } from "./constants.js";
import { ERC1967Proxy } from "./artifacts/ERC1967Proxy.js";
import { SimpleAccount } from "./artifacts/SimpleAccount.js";
import { encodeUserOp } from "./models/UserOperation.js";
import { IEntryPoint } from "./artifacts/IEntryPoint.js";
import { erc4337Contracts } from "./setupERC4337Contracts.js";
import { toPackedUserOperation } from "./models/PackedUserOperation.js";
import {
estimateUserOperationGas
} from "./actions/bundler/estimateUserOperationGas.js";
describe("SimpleAccount.test.ts", function() {
const chain = {
...localhost,
rpcUrls: {
default: {
http: [`http://127.0.0.1:${port}`]
}
}
};
const transport = http(chain.rpcUrls.default.http[0]);
const publicClient = createPublicClient({
chain,
transport
});
const walletClient = createWalletClient({
account: getLocalAccount(0, { nonceManager }),
chain,
transport
});
let account;
const entryPoint = erc4337Contracts.entrypoint;
const simpleAccountFactory = erc4337Contracts.simpleAccountFactory;
beforeEach(async () => {
account = privateKeyToAccount(generatePrivateKey());
});
describe("Deploy Simple Account", () => {
test("getSimpleAccountAddress", async () => {
const simpleAccountImplementation = await publicClient.readContract({
address: simpleAccountFactory,
abi: SimpleAccountFactory.abi,
functionName: "accountImplementation"
});
expect(simpleAccountImplementation).toBe(SIMPLE_ACCOUNT_IMPLEMENTATION_ADDRESS);
const simpleAccountAddressFromFactory = await publicClient.readContract({
address: simpleAccountFactory,
abi: SimpleAccountFactory.abi,
functionName: "getAddress",
args: [account.address, 0n]
});
const simpleAccountAddressOffchain = getSimpleAccountAddress(
{
owner: account.address,
salt: 0n
},
{
factoryAddress: simpleAccountFactory,
proxyBytecode: ERC1967Proxy.bytecode
}
);
expect(simpleAccountAddressOffchain).toBe(simpleAccountAddressFromFactory);
const accountExistingBytecode = await publicClient.getCode({ address: simpleAccountAddressOffchain });
expect(
accountExistingBytecode,
"Generated smart account counterfactual address should have 0x code"
).toBeUndefined();
const { request: createAccountRequest } = await publicClient.simulateContract({
account: walletClient.account,
address: simpleAccountFactory,
abi: SimpleAccountFactory.abi,
functionName: "createAccount",
args: [account.address, 0n]
});
const createAccountHash = await walletClient.writeContract(createAccountRequest);
await publicClient.waitForTransactionReceipt({ hash: createAccountHash });
const accountBytecode = await publicClient.getCode({ address: simpleAccountAddressOffchain });
expect(accountBytecode).toBeDefined();
const accountOwner = await publicClient.readContract({
address: simpleAccountAddressOffchain,
abi: SimpleAccount.abi,
functionName: "owner"
});
expect(accountOwner).toBe(account.address);
});
});
describe("Exec existing Simple Account", () => {
let simpleAccount;
beforeEach(async () => {
const simpleAccountAddress = getSimpleAccountAddress(
{
owner: account.address,
salt: 0n
},
{
factoryAddress: simpleAccountFactory,
proxyBytecode: ERC1967Proxy.bytecode
}
);
const simpleAccountFactoryData = encodeFunctionData({
abi: SimpleAccountFactory.abi,
functionName: "createAccount",
args: [account.address, 0n]
});
simpleAccount = {
address: simpleAccountAddress,
factoryData: simpleAccountFactoryData,
factoryAddress: simpleAccountFactory
};
const { request: createAccountRequest } = await publicClient.simulateContract({
account: walletClient.account,
address: simpleAccountFactory,
abi: SimpleAccountFactory.abi,
functionName: "createAccount",
args: [account.address, 0n]
});
const createAccountHash = await walletClient.writeContract(createAccountRequest);
await publicClient.waitForTransactionReceipt({ hash: createAccountHash });
const fundSimpleAccountHash = await walletClient.sendTransaction({
to: simpleAccount.address,
value: parseEther("1")
});
await publicClient.waitForTransactionReceipt({ hash: fundSimpleAccountHash });
});
test("submitUserOp - manual", async () => {
const to = privateKeyToAccount(generatePrivateKey()).address;
const value = 1n;
const data = "0x";
const callData = encodeFunctionData({
abi: [
{
inputs: [
{ name: "dest", type: "address" },
{ name: "value", type: "uint256" },
{ name: "func", type: "bytes" }
],
name: "execute",
outputs: [],
stateMutability: "nonpayable",
type: "function"
}
],
args: [to, value, data]
});
const gasPrice = await publicClient.estimateFeesPerGas();
const userOpData = {
sender: simpleAccount.address,
nonce: 0n,
callData,
maxFeePerGas: gasPrice.maxFeePerGas
};
const { preVerificationGas, verificationGasLimit, callGasLimit } = await estimateUserOperationGas(
{ ...publicClient, entryPointSimulationsAddress: erc4337Contracts.pimlicoEntrypointSimulations },
userOpData
);
const userOp = {
sender: simpleAccount.address,
nonce: 0n,
signature: "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
callData,
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas: gasPrice.maxFeePerGas,
maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas
};
const userOpHash = getUserOperationHash({
userOperation: userOp,
entryPointAddress: entryPoint,
entryPointVersion: "0.7",
chainId: localhost.id
});
const signature = await account.signMessage({
message: { raw: userOpHash }
});
userOp.signature = signature;
const userOpPacked = toPackedUserOperation(encodeUserOp(userOp));
const handleOpsArgs = [[userOpPacked], walletClient.account.address];
const { request } = await publicClient.simulateContract({
account: walletClient.account,
address: entryPoint,
abi: IEntryPoint.abi,
functionName: "handleOps",
args: handleOpsArgs
});
const handleOpsHash = await walletClient.writeContract(request);
await publicClient.waitForTransactionReceipt({ hash: handleOpsHash });
const balance = await publicClient.getBalance({ address: to });
expect(balance).toBe(value);
});
});
});