UNPKG

@zerodev/sdk

Version:

A utility library for working with ERC-4337

539 lines 22.7 kB
import { satisfies } from "semver"; import { concatHex, createNonceManager, encodeFunctionData, getTypesForEIP712Domain, hashMessage, hashTypedData, isAddressEqual, toHex, validateTypedData, zeroAddress } from "viem"; import { entryPoint06Abi, entryPoint07Abi, entryPoint07Address, toSmartAccount } from "viem/account-abstraction"; import { getChainId, getCode, signAuthorization as signAuthorizationAction } from "viem/actions"; import { getAction, verifyAuthorization } from "viem/utils"; import { getAccountNonce, getSenderAddress, isPluginInstalled } from "../../actions/public/index.js"; import { KernelVersionToAddressesMap, MAGIC_VALUE_SIG_REPLAYABLE } from "../../constants.js"; import { KERNEL_FEATURES, hasKernelFeature } from "../../utils.js"; import { validateKernelVersionWithEntryPoint } from "../../utils.js"; import { toSigner } from "../../utils/toSigner.js"; import { addressToEmptyAccount } from "../addressToEmptyAccount.js"; import { signerTo7702Validator } from "../utils/signerTo7702Validator.js"; import { isKernelPluginManager, toKernelPluginManager } from "../utils/toKernelPluginManager.js"; import { KernelInitAbi } from "./abi/KernelAccountAbi.js"; import { KernelV3InitAbi } from "./abi/kernel_v_3_0_0/KernelAccountAbi.js"; import { KernelV3FactoryAbi } from "./abi/kernel_v_3_0_0/KernelFactoryAbi.js"; import { KernelFactoryStakerAbi } from "./abi/kernel_v_3_0_0/KernelFactoryStakerAbi.js"; import { KernelV3_1AccountAbi } from "./abi/kernel_v_3_1/KernelAccountAbi.js"; import { encodeCallData as encodeCallDataEpV06 } from "./utils/account/ep0_6/encodeCallData.js"; import { encodeDeployCallData as encodeDeployCallDataV06 } from "./utils/account/ep0_6/encodeDeployCallData.js"; import { encodeCallData as encodeCallDataEpV07 } from "./utils/account/ep0_7/encodeCallData.js"; import { encodeDeployCallData as encodeDeployCallDataV07 } from "./utils/account/ep0_7/encodeDeployCallData.js"; import { accountMetadata } from "./utils/common/accountMetadata.js"; import { eip712WrapHash } from "./utils/common/eip712WrapHash.js"; import { getPluginInstallCallData } from "./utils/plugins/ep0_7/getPluginInstallCallData.js"; /** * The account creation ABI for a kernel smart account (from the KernelFactory) */ const createAccountAbi = [ { inputs: [ { internalType: "address", name: "_implementation", type: "address" }, { internalType: "bytes", name: "_data", type: "bytes" }, { internalType: "uint256", name: "_index", type: "uint256" } ], name: "createAccount", outputs: [ { internalType: "address", name: "proxy", type: "address" } ], stateMutability: "payable", type: "function" } ]; /** * Default addresses for kernel smart account */ export const KERNEL_ADDRESSES = { ACCOUNT_LOGIC_V0_6: "0xd3082872F8B06073A021b4602e022d5A070d7cfC", ACCOUNT_LOGIC_V0_7: "0x94F097E1ebEB4ecA3AAE54cabb08905B239A7D27", FACTORY_ADDRESS_V0_6: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3", FACTORY_ADDRESS_V0_7: "0x6723b44Abeec4E71eBE3232BD5B455805baDD22f", FACTORY_STAKER: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5" }; const getKernelInitData = async ({ entryPointVersion: _entryPointVersion, kernelPluginManager, initHook, kernelVersion, initConfig }) => { const { enableData, identifier, validatorAddress, initConfig: initConfig_ } = await kernelPluginManager.getValidatorInitData(); if (_entryPointVersion === "0.6") { return encodeFunctionData({ abi: KernelInitAbi, functionName: "initialize", args: [validatorAddress, enableData] }); } if (kernelVersion === "0.3.0") { return encodeFunctionData({ abi: KernelV3InitAbi, functionName: "initialize", args: [ identifier, initHook && kernelPluginManager.hook ? kernelPluginManager.hook?.getIdentifier() : zeroAddress, enableData, initHook && kernelPluginManager.hook ? await kernelPluginManager.hook?.getEnableData() : "0x" ] }); } return encodeFunctionData({ abi: KernelV3_1AccountAbi, functionName: "initialize", args: [ identifier, initHook && kernelPluginManager.hook ? kernelPluginManager.hook?.getIdentifier() : zeroAddress, enableData, initHook && kernelPluginManager.hook ? await kernelPluginManager.hook?.getEnableData() : "0x", initConfig ?? initConfig_ ?? [] ] }); }; /** * Get the account initialization code for a kernel smart account * @param index * @param factoryAddress * @param accountImplementationAddress * @param ecdsaValidatorAddress */ const getAccountInitCode = async ({ index, factoryAddress, accountImplementationAddress, entryPointVersion: _entryPointVersion, kernelPluginManager, initHook, kernelVersion, initConfig, useMetaFactory }) => { // Build the account initialization data const initialisationData = await getKernelInitData({ entryPointVersion: _entryPointVersion, kernelPluginManager, initHook, kernelVersion, initConfig }); // Build the account init code if (_entryPointVersion === "0.6") { return encodeFunctionData({ abi: createAccountAbi, functionName: "createAccount", args: [accountImplementationAddress, initialisationData, index] }); } if (!useMetaFactory) { return encodeFunctionData({ abi: KernelV3FactoryAbi, functionName: "createAccount", args: [initialisationData, toHex(index, { size: 32 })] }); } return encodeFunctionData({ abi: KernelFactoryStakerAbi, functionName: "deployWithFactory", args: [factoryAddress, initialisationData, toHex(index, { size: 32 })] }); }; const getDefaultAddresses = (entryPointVersion, kernelVersion, { accountImplementationAddress, factoryAddress, metaFactoryAddress }) => { validateKernelVersionWithEntryPoint(entryPointVersion, kernelVersion); const addresses = KernelVersionToAddressesMap[kernelVersion]; if (!addresses) { throw new Error(`No addresses found for kernel version ${kernelVersion}`); } return { accountImplementationAddress: accountImplementationAddress ?? addresses.accountImplementationAddress, factoryAddress: factoryAddress ?? addresses.factoryAddress, metaFactoryAddress: metaFactoryAddress ?? addresses.metaFactoryAddress ?? zeroAddress }; }; /** * Build a kernel smart account from a private key, that use the ECDSA signer behind the scene * @param client * @param privateKey * @param entryPoint * @param index * @param factoryAddress * @param accountImplementationAddress * @param ecdsaValidatorAddress * @param address */ export async function createKernelAccount(client, { plugins, entryPoint, index = 0n, factoryAddress: _factoryAddress, accountImplementationAddress: _accountImplementationAddress, metaFactoryAddress: _metaFactoryAddress, address, kernelVersion, initConfig, useMetaFactory: _useMetaFactory = true, eip7702Auth, eip7702Account, pluginMigrations }) { const isEip7702 = !!eip7702Account || !!eip7702Auth; if (isEip7702 && !satisfies(kernelVersion, ">=0.3.3")) { throw new Error("EIP-7702 is recommended for kernel version >=0.3.3"); } const localAccount = eip7702Account ? await toSigner({ signer: eip7702Account, address }) : undefined; let eip7702Validator; if (localAccount) { eip7702Validator = await signerTo7702Validator(client, { signer: localAccount, entryPoint, kernelVersion }); } let useMetaFactory = _useMetaFactory; const { accountImplementationAddress, factoryAddress, metaFactoryAddress } = getDefaultAddresses(entryPoint.version, kernelVersion, { accountImplementationAddress: _accountImplementationAddress, factoryAddress: _factoryAddress, metaFactoryAddress: _metaFactoryAddress }); let chainId; let cachedAccountMetadata; const getMemoizedChainId = async () => { if (chainId) return chainId; chainId = client.chain ? client.chain.id : await getAction(client, getChainId, "getChainId")({}); return chainId; }; const getMemoizedAccountMetadata = async () => { if (cachedAccountMetadata) return cachedAccountMetadata; cachedAccountMetadata = await accountMetadata(client, accountAddress, kernelVersion, await getMemoizedChainId()); return cachedAccountMetadata; }; const kernelPluginManager = isKernelPluginManager(plugins) ? plugins : await toKernelPluginManager(client, { sudo: localAccount ? eip7702Validator : plugins?.sudo, regular: plugins?.regular, hook: plugins?.hook, action: plugins?.action, pluginEnableSignature: plugins?.pluginEnableSignature, entryPoint, kernelVersion, chainId: await getMemoizedChainId() }); // initHook flag is activated only if both the hook and sudo validator are given // if the hook is given with regular plugins, then consider it as a hook for regular plugins const initHook = Boolean(isKernelPluginManager(plugins) ? plugins.hook && plugins.getIdentifier() === plugins.sudoValidator?.getIdentifier() : plugins?.hook && !plugins?.regular); // Helper to generate the init code for the smart account const generateInitCode = async () => { if (isEip7702) { return "0x"; } if (!accountImplementationAddress || !factoryAddress) throw new Error("Missing account logic address or factory address"); return getAccountInitCode({ index, factoryAddress, accountImplementationAddress, entryPointVersion: entryPoint.version, kernelPluginManager, initHook, kernelVersion, initConfig, useMetaFactory }); }; const getFactoryArgs = async () => { if (isEip7702) { return { factory: undefined, factoryData: undefined }; } return { factory: entryPoint.version === "0.6" || useMetaFactory === false ? factoryAddress : metaFactoryAddress, factoryData: await generateInitCode() }; }; // Fetch account address let accountAddress = address ?? (isEip7702 ? (localAccount?.address ?? zeroAddress) : await (async () => { const { factory, factoryData } = await getFactoryArgs(); if (!factory || !factoryData) { throw new Error("Missing factory address or factory data"); } // Get the sender address based on the init code return await getSenderAddress(client, { factory, factoryData, entryPointAddress: entryPoint.address }); })()); // If account is zeroAddress try without meta factory if (isAddressEqual(accountAddress, zeroAddress) && useMetaFactory) { useMetaFactory = false; accountAddress = await getSenderAddress(client, { factory: factoryAddress, factoryData: await generateInitCode(), entryPointAddress: entryPoint.address }); if (isAddressEqual(accountAddress, zeroAddress)) { useMetaFactory = true; } } const _entryPoint = { address: entryPoint?.address ?? entryPoint07Address, abi: ((entryPoint?.version ?? "0.7") === "0.6" ? entryPoint06Abi : entryPoint07Abi), version: entryPoint?.version ?? "0.7" }; // Cache for plugin installation status const pluginCache = { pendingPlugins: pluginMigrations || [], allInstalled: false }; const checkPluginInstallationStatus = async () => { // Skip if no plugins or all are installed if (!pluginCache.pendingPlugins.length || pluginCache.allInstalled) { pluginCache.allInstalled = true; return; } // Check all pending plugins in parallel const installationResults = await Promise.all(pluginCache.pendingPlugins.map((plugin) => isPluginInstalled(client, { address: accountAddress, plugin }))); // Filter out installed plugins pluginCache.pendingPlugins = pluginCache.pendingPlugins.filter((_, index) => !installationResults[index]); pluginCache.allInstalled = pluginCache.pendingPlugins.length === 0; }; const signAuthorization = async () => { const code = await getCode(client, { address: accountAddress }); // check if account has not activated 7702 with implementation address if (!code || code.length === 0 || !code .toLowerCase() .startsWith(`0xef0100${accountImplementationAddress.slice(2).toLowerCase()}`)) { if (eip7702Auth && !isAddressEqual(eip7702Auth.address, accountImplementationAddress)) { throw new Error("EIP-7702 authorization delegate address does not match account implementation address"); } const auth = eip7702Auth ?? (await signAuthorizationAction(client, { account: localAccount, address: accountImplementationAddress, chainId: await getMemoizedChainId() })); const verified = await verifyAuthorization({ authorization: auth, address: accountAddress }); if (!verified) { throw new Error("Authorization verification failed"); } return auth; } return undefined; }; await checkPluginInstallationStatus(); return toSmartAccount({ authorization: isEip7702 ? { account: localAccount ?? addressToEmptyAccount(accountAddress), address: accountImplementationAddress } : undefined, kernelVersion, kernelPluginManager, accountImplementationAddress, factoryAddress: (await getFactoryArgs()).factory, generateInitCode, encodeModuleInstallCallData: async () => { return await kernelPluginManager.encodeModuleInstallCallData(accountAddress); }, nonceKeyManager: createNonceManager({ source: { get: () => 0, set: () => { } } }), client, entryPoint: _entryPoint, getFactoryArgs, async getAddress() { if (accountAddress) return accountAddress; const { factory, factoryData } = await getFactoryArgs(); if (!factory || !factoryData) { throw new Error("Missing factory address or factory data"); } // Get the sender address based on the init code accountAddress = await getSenderAddress(client, { factory, factoryData, entryPointAddress: entryPoint.address }); return accountAddress; }, // Encode the deploy call data async encodeDeployCallData(_tx) { if (entryPoint.version === "0.6") { return encodeDeployCallDataV06(_tx); } return encodeDeployCallDataV07(_tx); }, async encodeCalls(calls, callType) { // Check plugin status only if we have pending plugins await checkPluginInstallationStatus(); // Add plugin installation calls if needed if (pluginCache.pendingPlugins.length > 0 && entryPoint.version === "0.7" && kernelPluginManager.activeValidatorMode === "sudo") { // convert map into for loop const pluginInstallCalls = []; for (const plugin of pluginCache.pendingPlugins) { pluginInstallCalls.push(getPluginInstallCallData(accountAddress, plugin)); } return encodeCallDataEpV07([...calls, ...pluginInstallCalls], callType, plugins?.hook ? true : undefined); } if (calls.length === 1 && (!callType || callType === "call") && calls[0].to.toLowerCase() === accountAddress.toLowerCase()) { return calls[0].data ?? "0x"; } if (entryPoint.version === "0.6") { return encodeCallDataEpV06(calls, callType); } if (plugins?.hook) { return encodeCallDataEpV07(calls, callType, true); } return encodeCallDataEpV07(calls, callType); }, eip7702Authorization: signAuthorization, async sign({ hash }) { return this.signMessage({ message: hash }); }, async signMessage({ message, useReplayableSignature }) { const messageHash = hashMessage(message); const { name, chainId: metadataChainId, version } = await getMemoizedAccountMetadata(); let signature; if (isEip7702) { signature = await kernelPluginManager.signTypedData({ message: { hash: messageHash }, primaryType: "Kernel", types: { Kernel: [{ name: "hash", type: "bytes32" }] }, domain: { name, version, chainId: useReplayableSignature ? 0 : Number(metadataChainId), verifyingContract: accountAddress } }); } else { const wrappedMessageHash = await eip712WrapHash(messageHash, { name, chainId: Number(metadataChainId), version, verifyingContract: accountAddress }, useReplayableSignature); signature = await kernelPluginManager.signMessage({ message: { raw: wrappedMessageHash } }); } if (!hasKernelFeature(KERNEL_FEATURES.ERC1271_WITH_VALIDATOR, version)) { return signature; } if (useReplayableSignature && hasKernelFeature(KERNEL_FEATURES.ERC1271_REPLAYABLE, version)) { signature = concatHex([MAGIC_VALUE_SIG_REPLAYABLE, signature]); } return concatHex([kernelPluginManager.getIdentifier(), signature]); }, async signTypedData(typedData) { const { message, primaryType, types: _types, domain } = typedData; const types = { EIP712Domain: getTypesForEIP712Domain({ domain: domain }), ..._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: domain, message: message, primaryType: primaryType, types: types }); const typedHash = hashTypedData(typedData); const { name, chainId: metadataChainId, version } = await getMemoizedAccountMetadata(); let signature; if (isEip7702) { signature = await kernelPluginManager.signTypedData({ message: { hash: typedHash }, primaryType: "Kernel", types: { Kernel: [{ name: "hash", type: "bytes32" }] }, domain: { name, version, chainId: Number(metadataChainId), verifyingContract: accountAddress } }); } else { const wrappedMessageHash = await eip712WrapHash(typedHash, { name, chainId: Number(metadataChainId), version, verifyingContract: accountAddress }); signature = await kernelPluginManager.signMessage({ message: { raw: wrappedMessageHash } }); } if (!hasKernelFeature(KERNEL_FEATURES.ERC1271_WITH_VALIDATOR, version)) { return signature; } return concatHex([kernelPluginManager.getIdentifier(), signature]); }, // Get the nonce of the smart account async getNonce(_args) { const key = await kernelPluginManager.getNonceKey(accountAddress, _args?.key); return getAccountNonce(client, { address: accountAddress, entryPointAddress: entryPoint.address, key }); }, async getStubSignature(userOperation) { if (!userOperation) { throw new Error("No user operation provided"); } return kernelPluginManager.getStubSignature(userOperation); }, // Sign a user operation async signUserOperation(parameters) { const { chainId = await getMemoizedChainId(), ...userOperation } = parameters; return kernelPluginManager.signUserOperation({ ...userOperation, sender: userOperation.sender ?? (await this.getAddress()), chainId }); } }); } //# sourceMappingURL=createKernelAccount.js.map