From 73ba48e302ba1aa0e2ac274d1adb7c6f153b29bb Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Wed, 24 Jul 2024 10:49:07 +0100 Subject: [PATCH] Add installModules and uninstallModules Functions --- .../actions/erc7579/installModules.test.ts | 244 ++++++++++++++++++ .../actions/erc7579/installModules.ts | 115 +++++++++ .../actions/erc7579/uninstallModules.test.ts | 168 ++++++++++++ .../actions/erc7579/uninstallModules.ts | 120 +++++++++ 4 files changed, 647 insertions(+) create mode 100644 packages/permissionless/actions/erc7579/installModules.test.ts create mode 100644 packages/permissionless/actions/erc7579/installModules.ts create mode 100644 packages/permissionless/actions/erc7579/uninstallModules.test.ts create mode 100644 packages/permissionless/actions/erc7579/uninstallModules.ts diff --git a/packages/permissionless/actions/erc7579/installModules.test.ts b/packages/permissionless/actions/erc7579/installModules.test.ts new file mode 100644 index 00000000..3bc398ca --- /dev/null +++ b/packages/permissionless/actions/erc7579/installModules.test.ts @@ -0,0 +1,244 @@ +import { + http, + type Chain, + type Transport, + encodeAbiParameters, + encodePacked, + isHash, + zeroAddress +} from "viem" +import { generatePrivateKey } from "viem/accounts" +import { describe, expect } from "vitest" +import { testWithRpc } from "../../../permissionless-test/src/testWithRpc" +import { + getCoreSmartAccounts, + getPimlicoPaymasterClient +} from "../../../permissionless-test/src/utils" +import type { SmartAccount } from "../../accounts" +import { createBundlerClient } from "../../clients/createBundlerClient" +import type { SmartAccountClient } from "../../clients/createSmartAccountClient" +import type { ENTRYPOINT_ADDRESS_V07_TYPE } from "../../types/entrypoint" +import { ENTRYPOINT_ADDRESS_V07 } from "../../utils" +import { erc7579Actions } from "../erc7579" +import { installModules } from "./installModules" + +describe.each(getCoreSmartAccounts())( + "installmodules $name", + ({ getErc7579SmartAccountClient, name }) => { + testWithRpc.skipIf(!getErc7579SmartAccountClient)( + "installModules", + async ({ rpc }) => { + const { anvilRpc, altoRpc, paymasterRpc } = rpc + + if (!getErc7579SmartAccountClient) { + throw new Error("getErc7579SmartAccountClient not defined") + } + + const privateKey = generatePrivateKey() + + const smartClientWithoutExtend: SmartAccountClient< + ENTRYPOINT_ADDRESS_V07_TYPE, + Transport, + Chain, + SmartAccount + > = await getErc7579SmartAccountClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + privateKey: privateKey, + altoRpc: altoRpc, + anvilRpc: anvilRpc, + paymasterClient: getPimlicoPaymasterClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + paymasterRpc + }) + }) + + const smartClient = smartClientWithoutExtend.extend( + erc7579Actions({ + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + ) + + const moduleData = encodePacked( + ["address"], + [smartClient.account.address] + ) + + const opHash = await installModules(smartClient as any, { + account: smartClient.account as any, + modules: [ + { + type: "executor", + address: + "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: + name === "Kernel 7579" + ? encodePacked( + ["address", "bytes"], + [ + zeroAddress, + encodeAbiParameters( + [ + { type: "bytes" }, + { type: "bytes" } + ], + [moduleData, "0x"] + ) + ] + ) + : moduleData + } + ] + }) + + const bundlerClientV07 = createBundlerClient({ + transport: http(altoRpc), + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + expect(isHash(opHash)).toBe(true) + + const userOperationReceipt = + await bundlerClientV07.waitForUserOperationReceipt({ + hash: opHash, + timeout: 100000 + }) + expect(userOperationReceipt).not.toBeNull() + expect(userOperationReceipt?.userOpHash).toBe(opHash) + expect( + userOperationReceipt?.receipt.transactionHash + ).toBeTruthy() + + const receipt = await bundlerClientV07.getUserOperationReceipt({ + hash: opHash + }) + + expect(receipt?.receipt.transactionHash).toBe( + userOperationReceipt?.receipt.transactionHash + ) + + const isModuleInstalled = await smartClient.isModuleInstalled({ + type: "executor", + address: "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: "0x" + }) + + expect(isModuleInstalled).toBe(true) + } + ) + testWithRpc.skipIf(!getErc7579SmartAccountClient)( + "installModule", + async ({ rpc }) => { + const { anvilRpc, altoRpc, paymasterRpc } = rpc + + if (!getErc7579SmartAccountClient) { + throw new Error("getErc7579SmartAccountClient not defined") + } + + const privateKey = generatePrivateKey() + + const smartClientWithoutExtend: SmartAccountClient< + ENTRYPOINT_ADDRESS_V07_TYPE, + Transport, + Chain, + SmartAccount + > = await getErc7579SmartAccountClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + privateKey: privateKey, + altoRpc: altoRpc, + anvilRpc: anvilRpc, + paymasterClient: getPimlicoPaymasterClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + paymasterRpc + }) + }) + + const smartClient = smartClientWithoutExtend.extend( + erc7579Actions({ + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + ) + + await smartClient.sendTransactions({ + transactions: [ + { + to: smartClient.account.address, + value: 0n, + data: "0x" + }, + { + to: smartClient.account.address, + value: 0n, + data: "0x" + } + ] + }) + + const moduleData = encodePacked( + ["address"], + [smartClient.account.address] + ) + + const opHash = await installModules(smartClient as any, { + account: smartClient.account as any, + modules: [ + { + type: "executor", + address: + "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: + name === "Kernel 7579" + ? encodePacked( + ["address", "bytes"], + [ + zeroAddress, + encodeAbiParameters( + [ + { type: "bytes" }, + { type: "bytes" } + ], + [moduleData, "0x"] + ) + ] + ) + : moduleData + } + ] + }) + + const bundlerClientV07 = createBundlerClient({ + transport: http(altoRpc), + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + expect(isHash(opHash)).toBe(true) + + const userOperationReceipt = + await bundlerClientV07.waitForUserOperationReceipt({ + hash: opHash, + timeout: 100000 + }) + expect(userOperationReceipt).not.toBeNull() + expect(userOperationReceipt?.userOpHash).toBe(opHash) + expect( + userOperationReceipt?.receipt.transactionHash + ).toBeTruthy() + + const receipt = await bundlerClientV07.getUserOperationReceipt({ + hash: opHash + }) + + expect(receipt?.receipt.transactionHash).toBe( + userOperationReceipt?.receipt.transactionHash + ) + + const isModuleInstalled = await smartClient.isModuleInstalled({ + type: "executor", + address: "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: "0x" + }) + + expect(isModuleInstalled).toBe(true) + } + ) + } +) diff --git a/packages/permissionless/actions/erc7579/installModules.ts b/packages/permissionless/actions/erc7579/installModules.ts new file mode 100644 index 00000000..50d3ce46 --- /dev/null +++ b/packages/permissionless/actions/erc7579/installModules.ts @@ -0,0 +1,115 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { getAction } from "viem/utils" +import type { SmartAccount } from "../../accounts/types" +import type { GetAccountParameter, Prettify } from "../../types/" +import type { EntryPoint } from "../../types/entrypoint" +import { parseAccount } from "../../utils/" +import { AccountOrClientNotFoundError } from "../../utils/signUserOperationHashWithECDSA" +import type { Middleware } from "../smartAccount/prepareUserOperationRequest" +import { sendUserOperation } from "../smartAccount/sendUserOperation" +import { type ModuleType, parseModuleTypeId } from "./supportsModule" + +export type InstallModulesParameters< + TEntryPoint extends EntryPoint, + TSmartAccount extends SmartAccount | undefined +> = GetAccountParameter & + Middleware & { + modules: { + type: ModuleType + address: Address + context: Hex + }[] + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint + } + +export async function installModules< + TEntryPoint extends EntryPoint, + TSmartAccount extends SmartAccount | undefined, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + parameters: Prettify> +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + middleware, + modules + } = parameters + + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const installModulesCallData = await account.encodeCallData( + await Promise.all( + modules.map(({ type, address, context }) => ({ + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "installModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "initData" + } + ], + outputs: [] + } + ], + functionName: "installModule", + args: [ + parseModuleTypeId(type), + getAddress(address), + context + ] + }) + })) + ) + ) + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + userOperation: { + sender: account.address, + maxFeePerGas: maxFeePerGas || BigInt(0), + maxPriorityFeePerGas: maxPriorityFeePerGas || BigInt(0), + callData: installModulesCallData, + nonce: nonce ? BigInt(nonce) : undefined + }, + account: account, + middleware + }) +} diff --git a/packages/permissionless/actions/erc7579/uninstallModules.test.ts b/packages/permissionless/actions/erc7579/uninstallModules.test.ts new file mode 100644 index 00000000..4c79dd96 --- /dev/null +++ b/packages/permissionless/actions/erc7579/uninstallModules.test.ts @@ -0,0 +1,168 @@ +import { + http, + type Chain, + type Transport, + encodeAbiParameters, + encodePacked, + isHash, + zeroAddress +} from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { describe, expect } from "vitest" +import { testWithRpc } from "../../../permissionless-test/src/testWithRpc" +import { + getCoreSmartAccounts, + getPimlicoPaymasterClient, + getPublicClient +} from "../../../permissionless-test/src/utils" +import type { SmartAccount } from "../../accounts" +import { createBundlerClient } from "../../clients/createBundlerClient" +import type { SmartAccountClient } from "../../clients/createSmartAccountClient" +import type { ENTRYPOINT_ADDRESS_V07_TYPE } from "../../types" +import { ENTRYPOINT_ADDRESS_V07 } from "../../utils" +import { erc7579Actions } from "../erc7579" +import { uninstallModules } from "./uninstallModules" + +describe.each(getCoreSmartAccounts())( + "uninstallModules $name", + ({ getErc7579SmartAccountClient, name }) => { + testWithRpc.skipIf(!getErc7579SmartAccountClient)( + "uninstallModules", + async ({ rpc }) => { + const { anvilRpc, altoRpc, paymasterRpc } = rpc + + if (!getErc7579SmartAccountClient) { + throw new Error("getErc7579SmartAccountClient not defined") + } + + const privateKey = generatePrivateKey() + + const eoaAccount = privateKeyToAccount(privateKey) + + const smartClientWithoutExtend: SmartAccountClient< + ENTRYPOINT_ADDRESS_V07_TYPE, + Transport, + Chain, + SmartAccount + > = await getErc7579SmartAccountClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + privateKey: privateKey, + altoRpc: altoRpc, + anvilRpc: anvilRpc, + paymasterClient: getPimlicoPaymasterClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + paymasterRpc + }) + }) + + const smartClient = smartClientWithoutExtend.extend( + erc7579Actions({ + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + ) + + const moduleData = encodePacked( + ["address"], + [smartClient.account.address] + ) + + const bundlerClientV07 = createBundlerClient({ + transport: http(altoRpc), + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + const publicClient = getPublicClient(anvilRpc) + + const opHash = await smartClient.installModule({ + type: "executor", + address: "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: + name === "Kernel 7579" + ? encodePacked( + ["address", "bytes"], + [ + zeroAddress, + encodeAbiParameters( + [ + { type: "bytes" }, + { type: "bytes" } + ], + [moduleData, "0x"] + ) + ] + ) + : moduleData + }) + + const userOperationReceipt = + await bundlerClientV07.waitForUserOperationReceipt({ + hash: opHash, + timeout: 100000 + }) + + await publicClient.waitForTransactionReceipt({ + hash: userOperationReceipt.receipt.transactionHash + }) + + const uninstallModulesUserOpHash = await uninstallModules( + smartClient as any, + { + account: smartClient.account as any, + modules: [ + { + type: "executor", + address: + "0xc98B026383885F41d9a995f85FC480E9bb8bB891", + context: + name === "Kernel 7579" + ? "0x" + : encodeAbiParameters( + [ + { + name: "prev", + type: "address" + }, + { + name: "moduleInitData", + type: "bytes" + } + ], + [ + "0x0000000000000000000000000000000000000001", + "0x" + ] + ) + } + ] + } + ) + + expect(isHash(uninstallModulesUserOpHash)).toBe(true) + + const userOperationReceiptUninstallModules = + await bundlerClientV07.waitForUserOperationReceipt({ + hash: uninstallModulesUserOpHash, + timeout: 100000 + }) + expect(userOperationReceiptUninstallModules).not.toBeNull() + expect(userOperationReceiptUninstallModules?.userOpHash).toBe( + uninstallModulesUserOpHash + ) + expect( + userOperationReceiptUninstallModules?.receipt + .transactionHash + ).toBeTruthy() + + const receiptUninstallModules = + await bundlerClientV07.getUserOperationReceipt({ + hash: uninstallModulesUserOpHash + }) + + expect(receiptUninstallModules?.receipt.transactionHash).toBe( + userOperationReceiptUninstallModules?.receipt + .transactionHash + ) + } + ) + } +) diff --git a/packages/permissionless/actions/erc7579/uninstallModules.ts b/packages/permissionless/actions/erc7579/uninstallModules.ts new file mode 100644 index 00000000..1d0713f7 --- /dev/null +++ b/packages/permissionless/actions/erc7579/uninstallModules.ts @@ -0,0 +1,120 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { getAction } from "viem/utils" +import type { SmartAccount } from "../../accounts/types" +import type { GetAccountParameter, Prettify } from "../../types/" +import type { EntryPoint } from "../../types/entrypoint" +import { parseAccount } from "../../utils/" +import { AccountOrClientNotFoundError } from "../../utils/signUserOperationHashWithECDSA" +import type { Middleware } from "../smartAccount/prepareUserOperationRequest" +import { sendUserOperation } from "../smartAccount/sendUserOperation" +import { type ModuleType, parseModuleTypeId } from "./supportsModule" + +export type UninstallModulesParameters< + TEntryPoint extends EntryPoint, + TSmartAccount extends SmartAccount | undefined = + | SmartAccount + | undefined +> = GetAccountParameter & { + modules: [ + { + type: ModuleType + address: Address + context: Hex + } + ] + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} & Middleware + +export async function uninstallModules< + TEntryPoint extends EntryPoint, + TSmartAccount extends SmartAccount | undefined = + | SmartAccount + | undefined, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + parameters: Prettify> +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + middleware, + modules + } = parameters + + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const uninstallModulesCallData = await account.encodeCallData( + await Promise.all( + modules.map(({ type, address, context }) => ({ + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "uninstallModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "deInitData" + } + ], + outputs: [] + } + ], + functionName: "uninstallModule", + args: [ + parseModuleTypeId(type), + getAddress(address), + context + ] + }) + })) + ) + ) + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + userOperation: { + sender: account.address, + maxFeePerGas: maxFeePerGas || BigInt(0), + maxPriorityFeePerGas: maxPriorityFeePerGas || BigInt(0), + callData: uninstallModulesCallData, + nonce: nonce ? BigInt(nonce) : undefined + }, + account: account, + middleware + }) +}