From 8e1764d16935ff32bdcf9741605ecf77670fc782 Mon Sep 17 00:00:00 2001 From: adnpark Date: Wed, 6 Mar 2024 19:08:56 +0900 Subject: [PATCH] feat: add a draft code for kernel v1 support --- .../accounts/kernel/createKernelAccount.ts | 9 +- .../accounts/kernel/createKernelV1Account.ts | 205 ++++++++++++++++++ packages/core/accounts/utils/index.ts | 9 + 3 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 packages/core/accounts/kernel/createKernelV1Account.ts diff --git a/packages/core/accounts/kernel/createKernelAccount.ts b/packages/core/accounts/kernel/createKernelAccount.ts index 1bedc779..c0c8b0ff 100644 --- a/packages/core/accounts/kernel/createKernelAccount.ts +++ b/packages/core/accounts/kernel/createKernelAccount.ts @@ -36,6 +36,7 @@ import type { } from "../../types/kernel.js" import { getKernelVersion } from "../../utils.js" import { wrapSignatureWith6492 } from "../utils/6492.js" +import { parseFactoryAddressAndCallDataFromAccountInitCode } from "../utils/index.js" import { isKernelPluginManager, toKernelPluginManager @@ -207,14 +208,6 @@ const getAccountAddress = async < }) } -const parseFactoryAddressAndCallDataFromAccountInitCode = ( - initCode: Hex -): [Address, Hex] => { - const factoryAddress = `0x${initCode.substring(2, 42)}` as Address - const factoryCalldata = `0x${initCode.substring(42)}` as Hex - return [factoryAddress, factoryCalldata] -} - /** * Build a kernel smart account from a private key, that use the ECDSA signer behind the scene * @param client diff --git a/packages/core/accounts/kernel/createKernelV1Account.ts b/packages/core/accounts/kernel/createKernelV1Account.ts new file mode 100644 index 00000000..715c79a3 --- /dev/null +++ b/packages/core/accounts/kernel/createKernelV1Account.ts @@ -0,0 +1,205 @@ +import { + getAccountNonce, + getSenderAddress, + getUserOperationHash +} from "permissionless" +import { + SignTransactionNotSupportedBySmartAccount, + SmartAccountSigner +} from "permissionless/accounts" +import { + Address, + Chain, + Client, + Hash, + Hex, + Transport, + TypedDataDefinition, + concatHex, + encodeFunctionData, + getTypesForEIP712Domain, + hashMessage, + hashTypedData, + validateTypedData +} from "viem" +import { toAccount } from "viem/accounts" +import { getBytecode, getChainId } from "viem/actions" +import { wrapSignatureWith6492 } from "../utils/6492.js" +import { parseFactoryAddressAndCallDataFromAccountInitCode } from "../utils/index.js" +import { KernelSmartAccount } from "./createKernelAccount" + +const createAccountAbi = [ + { + inputs: [ + { internalType: "address", name: "_owner", type: "address" }, + { internalType: "uint256", name: "_index", type: "uint256" } + ], + name: "createAccount", + outputs: [ + { + internalType: "contract EIP1967Proxy", + name: "proxy", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } +] + +const KERNEL_V1_ADDRESSES: { + FACTORY_ADDRESS: Address + ENTRYPOINT_V0_6: Address +} = { + FACTORY_ADDRESS: "0x4E4946298614FC299B50c947289F4aD0572CB9ce", + ENTRYPOINT_V0_6: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" +} + +export async function createKernelV1Account< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + entrypoint = KERNEL_V1_ADDRESSES.ENTRYPOINT_V0_6, + index = 0n, + factoryAddress = KERNEL_V1_ADDRESSES.FACTORY_ADDRESS, + deployedAccountAddress + }: { + signer: SmartAccountSigner + entrypoint?: Address + index?: bigint + factoryAddress?: Address + deployedAccountAddress?: Address + } +): Promise> { + if (entrypoint !== KERNEL_V1_ADDRESSES.ENTRYPOINT_V0_6) { + throw new Error("Only EntryPoint 0.6 is supported") + } + + // Fetch chain id + const chainId = await getChainId(client) + + const getAccountInitCode = async (): Promise => { + return concatHex([ + KERNEL_V1_ADDRESSES.FACTORY_ADDRESS, + encodeFunctionData({ + abi: createAccountAbi, + functionName: "createAccount", + args: [signer.address, index] + }) + ]) as Hex + } + + const initCode = await getAccountInitCode() + const accountAddress = await getSenderAddress(client, { + initCode, + entryPoint: entrypoint + }) + + if (!accountAddress) throw new Error("Account address not found") + + const account = toAccount({ + address: accountAddress, + async signMessage({ message }) { + const hash = hashMessage(message) + const [isDeployed, signature] = await Promise.all([ + isAccountDeployed(), + signer.signMessage({ message: hash }) + ]) + return create6492Signature(isDeployed, signature) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData(typedData) { + const types = { + EIP712Domain: getTypesForEIP712Domain({ + domain: typedData.domain + }), + ...typedData.types + } + + // Need to do a runtime validation check on addresses, byte ranges, integer ranges, etc + // as we can't statically check this with TypeScript. + validateTypedData({ + domain: typedData.domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: types + } as TypedDataDefinition) + + const typedHash = hashTypedData(typedData) + const [isDeployed, signature] = await Promise.all([ + isAccountDeployed(), + signer.signMessage({ message: typedHash }) + ]) + return create6492Signature(isDeployed, signature) + } + }) + + const isAccountDeployed = async (): Promise => { + const contractCode = await getBytecode(client, { + address: accountAddress + }) + + return (contractCode?.length ?? 0) > 2 + } + + const create6492Signature = async ( + isDeployed: boolean, + signature: Hash + ): Promise => { + if (isDeployed) { + return signature + } + + const [factoryAddress, factoryCalldata] = + parseFactoryAddressAndCallDataFromAccountInitCode( + await getAccountInitCode() + ) + + return wrapSignatureWith6492({ + factoryAddress, + factoryCalldata, + signature + }) + } + + return { + ...account, + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entrypoint + }) + }, + async signUserOperation(userOperation) { + const hash = getUserOperationHash({ + userOperation: { + ...userOperation, + signature: "0x" + }, + entryPoint: entrypoint, + chainId: chainId + }) + const signature = await account.signMessage({ message: hash }) + return signature + }, + async getInitCode() { + if (await isAccountDeployed()) { + return "0x" + } else { + return getAccountInitCode() + } + }, + async encodeCallData(_tx) {}, + async getDummySignature() { + return "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" + }, + async encodeDeployCallData() {} + } +} diff --git a/packages/core/accounts/utils/index.ts b/packages/core/accounts/utils/index.ts index 33ab4e61..a96846d6 100644 --- a/packages/core/accounts/utils/index.ts +++ b/packages/core/accounts/utils/index.ts @@ -1,5 +1,14 @@ import { toKernelPluginManager } from "./toKernelPluginManager.js" export { toKernelPluginManager } +import { Address, Hex } from "viem" import { verifyEIP6492Signature } from "./6492.js" export { verifyEIP6492Signature } + +export const parseFactoryAddressAndCallDataFromAccountInitCode = ( + initCode: Hex +): [Address, Hex] => { + const factoryAddress = `0x${initCode.substring(2, 42)}` as Address + const factoryCalldata = `0x${initCode.substring(42)}` as Hex + return [factoryAddress, factoryCalldata] +}