UNPKG

@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
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); }); }); });