UNPKG

@owlprotocol/contracts-account-abstraction

Version:

ERC4337 Account Abstraction support for deploying relevant smart contracts and interacting with UserOps.

322 lines (294 loc) 12.5 kB
import { describe, test, beforeEach, expect, beforeAll } from "vitest"; import { LocalAccount, Address, Hex, createPublicClient, createWalletClient, http, encodeAbiParameters, encodeFunctionData, parseEther, hexToBytes, concatHex, zeroAddress, Hash, nonceManager, } from "viem"; import { localhost } from "viem/chains"; import { getLocalAccount } from "@owlprotocol/viem-utils"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { entryPoint07Address, UserOperation, getUserOperationHash } from "viem/account-abstraction"; import { port } from "./test/constants.js"; import { VerifyingPaymaster } from "./artifacts/VerifyingPaymaster.js"; import { IEntryPoint } from "./artifacts/IEntryPoint.js"; import { getSimpleAccountAddress } from "./SimpleAccount.js"; import { ERC1967Proxy } from "./artifacts/ERC1967Proxy.js"; import { SimpleAccountFactory } from "./artifacts/SimpleAccountFactory.js"; import { dummySignature, encodeUserOp } from "./models/UserOperation.js"; import { erc4337Contracts, setupVerifyingPaymaster } from "./setupERC4337Contracts.js"; import { toPackedUserOperation } from "./models/PackedUserOperation.js"; import { getVerifyingPaymasterHash } from "./VerifyingPaymaster.js"; import { estimateUserOperationGas, EstimateUserOperationGasParameters07, } from "./actions/bundler/estimateUserOperationGas.js"; describe("VerifyingPaymaster.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 simpleAccountFactory: Address; let verifyingPaymaster: Address; beforeAll(async () => { // entryPoint = contracts.entrypoint.address; simpleAccountFactory = erc4337Contracts.simpleAccountFactory; verifyingPaymaster = ( await setupVerifyingPaymaster(walletClient, { verifyingSignerAddress: walletClient.account.address, }) ).address; }); test("getVerifyingPaymasterHash", async () => { const validUntil = Date.now() + 3600; const validAfter = 0; const paymasterDataUnsigned = encodeAbiParameters( [ { name: "validUntil", type: "uint48" }, { name: "validAfter", type: "uint48" }, ], [validUntil, validAfter], ); const userOperation: UserOperation<"0.7"> = { sender: zeroAddress, nonce: 0n, factory: zeroAddress, factoryData: "0x", callData: "0x", preVerificationGas: 0n, verificationGasLimit: 0n, callGasLimit: 0n, maxFeePerGas: 0n, maxPriorityFeePerGas: 0n, signature: dummySignature, paymaster: verifyingPaymaster, paymasterVerificationGasLimit: 0n, paymasterPostOpGasLimit: 0n, paymasterData: paymasterDataUnsigned, }; const userOperationPacked = toPackedUserOperation(encodeUserOp(userOperation)); const hashExpected = (await publicClient.readContract({ address: verifyingPaymaster, abi: VerifyingPaymaster.abi, functionName: "getHash", args: [userOperationPacked as any, validUntil, validAfter], })) as Hash; const hash = getVerifyingPaymasterHash({ chainId: publicClient.chain.id, userOperation: userOperationPacked, }); expect(hash).toBe(hashExpected); }); /** Tests involving interacting with an existing paymaster */ describe("Exec existing Simple Account", () => { let account: LocalAccount; let simpleAccount: { address: Address; factoryData: Hex; factoryAddress: Address; }; beforeEach(async () => { //Get SimpleAccount address const privateKey = generatePrivateKey(); account = privateKeyToAccount(privateKey); //Get SimpleAccount address 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, }; //Deploy SimpleAccount 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 }); //Pre-fund wallet const fundSimpleAccountHash = await walletClient.sendTransaction({ to: simpleAccount.address, value: 1n, }); await publicClient.waitForTransactionReceipt({ hash: fundSimpleAccountHash }); }); /** * Create a UserOp from start to end (using Paymaster) * - Encode callData * - Sign UserOp with account owner * - Sign UserOp with paymaster * - Prefund paymaster * - Submit to EntryPoint * Also see similar test in SimpleAccount.test.ts without Paymaster. **/ test("submitUserOp - manual", async () => { //Create UserOp //Encode smart account tx, send to random address 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], }); // Paymaster data const validUntil = Date.now() + 3600; const validAfter = 0; const paymasterDataUnsigned = encodeAbiParameters( [ { name: "validUntil", type: "uint48" }, { name: "validAfter", type: "uint48" }, ], [validUntil, validAfter], ); const paymasterDataDummySignature = concatHex([paymasterDataUnsigned, dummySignature]); // Estimate UserOp gas const gasPrice = await publicClient.estimateFeesPerGas(); const userOpData: EstimateUserOperationGasParameters07 = { sender: simpleAccount.address, nonce: 0n, callData, paymaster: verifyingPaymaster, //Empty, will be replaced with signature paymasterData: paymasterDataDummySignature, maxFeePerGas: gasPrice.maxFeePerGas!, }; const { preVerificationGas, verificationGasLimit, callGasLimit, paymasterVerificationGasLimit, paymasterPostOpGasLimit, } = await estimateUserOperationGas( { ...publicClient, entryPointSimulationsAddress: erc4337Contracts.pimlicoEntrypointSimulations }, userOpData, ); // Construct final UserOp const userOp: UserOperation<"0.7"> = { sender: simpleAccount.address, nonce: 0n, signature: "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", callData, callGasLimit, verificationGasLimit, preVerificationGas, maxFeePerGas: gasPrice.maxFeePerGas!, maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas!, paymaster: verifyingPaymaster, //Empty, will be replaced with signature paymasterData: paymasterDataDummySignature, paymasterVerificationGasLimit, paymasterPostOpGasLimit, }; const userOpPaymasterPacked = toPackedUserOperation(encodeUserOp(userOp)); const userOpPaymasterHash = await publicClient.readContract({ address: verifyingPaymaster, abi: VerifyingPaymaster.abi, functionName: "getHash", args: [userOpPaymasterPacked as any, validUntil, validAfter], }); const paymasterSignature = await walletClient.account.signMessage({ message: { raw: userOpPaymasterHash, }, }); const paymasterSignatureBytes = hexToBytes(paymasterSignature); expect(paymasterSignatureBytes.length).toBeGreaterThanOrEqual(64); expect(paymasterSignatureBytes.length).toBeLessThanOrEqual(65); const paymasterDataSigned = concatHex([paymasterDataUnsigned, paymasterSignature]); userOp.paymasterData = paymasterDataSigned; const userOpHash = getUserOperationHash({ userOperation: userOp, entryPointAddress: entryPoint07Address, entryPointVersion: "0.7", chainId: localhost.id, }); const signature = await account.signMessage({ message: { raw: userOpHash }, }); userOp.signature = signature; const userOpPacked = toPackedUserOperation(encodeUserOp(userOp)); //types seem to be inferred as [never[], Address] const handleOpsArgs = [[userOpPacked] as any[], walletClient.account.address] as const; //Pre-fund paymaster const paymasterDeposit = await publicClient.simulateContract({ account: walletClient.account, address: verifyingPaymaster, abi: VerifyingPaymaster.abi, functionName: "deposit", value: parseEther("10"), args: [], }); const paymasterDepositHash = await walletClient.writeContract(paymasterDeposit.request); await publicClient.waitForTransactionReceipt({ hash: paymasterDepositHash }); //Simulate handleOps const { request } = await publicClient.simulateContract({ account: walletClient.account, address: entryPoint07Address, abi: IEntryPoint.abi, functionName: "handleOps", args: handleOpsArgs, }); //Submit UserOp const handleOpsHash = await walletClient.writeContract(request as any); await publicClient.waitForTransactionReceipt({ hash: handleOpsHash }); //Get balanceOf "to" const balance = await publicClient.getBalance({ address: to }); expect(balance).toBe(value); }); }); });