/** * Kernel Account Factory * * Modular factory for creating Kernel smart accounts with EIP-7702. * Supports multiple bundlers, chains, and EntryPoint versions. */ import { Chain, Hex, parseEther, PublicClient, SignAuthorizationReturnType, createPublicClient, http } from 'viem'; import { PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; import { createKernelAccount } from '@zerodev/sdk'; import { KERNEL_V3_3, KernelVersionToAddressesMap } from '@zerodev/sdk/constants'; import { createBundlerClient, entryPoint07Address, entryPoint08Address } from 'viem/account-abstraction'; import { BundlerManager, createBundlerService } from '../services/bundler'; import { BatchTransactionConfig, EntryPointVersion, KernelAccountConfig, KernelAccountInstance, KernelVersion, SingleTransactionConfig, Call } from './type'; // Re-export types so other files can import from this module export type { Call, KernelAccountInstance, EntryPointVersion, KernelVersion, KernelAccountConfig, BatchTransactionConfig, SingleTransactionConfig } from './type'; // ============================================ // EntryPoint Mapping // ============================================ const ENTRYPOINT_MAP: Record = { '0.6': entryPoint07Address, // Fallback to 0.7 for 0.6 requests '0.7': entryPoint07Address }; // ============================================ // Kernel Account Factory // ============================================ export async function createKernel7702Account( config: KernelAccountConfig ): Promise { const { chain, owner, entryPointVersion = '0.7', kernelVersion = '0.3.3', aaConfig } = config; // Create public client for standard RPC calls // (Bundler clients don't support standard eth_* methods) const publicClient = createPublicClient({ chain, transport: http() }); // Get EntryPoint address from aaConfig or fallback to default let entryPointAddress: Hex; if (aaConfig && aaConfig.entryPoints.length > 0) { // Find matching entry point from config const entryPoint = aaConfig.entryPoints.find(ep => ep.version === entryPointVersion); if (entryPoint) { entryPointAddress = entryPoint.address as Hex; } else { // Fallback to first entry point if version not found console.warn(`EntryPoint version ${entryPointVersion} not found in aaConfig, using first available`); entryPointAddress = aaConfig.entryPoints[0].address as Hex; } } else { // Fallback to hardcoded map if no config entryPointAddress = ENTRYPOINT_MAP[entryPointVersion]; if (!entryPointAddress) { throw new Error(`Unsupported EntryPoint version: ${entryPointVersion}`); } } // Create Kernel account with EIP-7702 // Use public client for account creation (needs eth_call, eth_getCode, etc.) const kernelAccount = await createKernelAccount( publicClient as any, { entryPoint: { address: entryPointAddress, version: entryPointVersion }, kernelVersion: KERNEL_V3_3, eip7702Account: owner as any } ); return { account: kernelAccount, address: kernelAccount.address, owner, chain, entryPoint: { address: entryPointAddress, version: entryPointVersion } }; } // ============================================ // EIP-7702 Authorization Helper // ============================================ export async function createKernelAuthorization( config: { owner: PrivateKeyAccount; chain: Chain; kernelVersion?: KernelVersion; aaConfig?: import('./type').AA_SupportConfig; } ): Promise { const { owner, chain, kernelVersion = '0.3.3', aaConfig } = config; // Create public client for eth_getCode (bundler clients don't support it) const publicClient = createPublicClient({ chain, transport: http(chain.rpcUrls.default.http[0]) }); // Get Kernel implementation address from aaConfig or fallback to default let delegateAddress: Hex; if (aaConfig && aaConfig.kernelImplementations.length > 0) { // Try to find matching kernel version (currently only 0.3.3 / version 3 is supported) const versionNumber = 3; // kernel version 0.3.3 maps to version 3 const kernelImpl = aaConfig.kernelImplementations.find(impl => impl.version === versionNumber); if (kernelImpl) { delegateAddress = kernelImpl.address as Hex; } else { // Fallback to first implementation if version not found console.warn(`Kernel version ${kernelVersion} (v${versionNumber}) not found in aaConfig, using first available`); delegateAddress = aaConfig.kernelImplementations[0].address as Hex; } } else { // Fallback to hardcoded map if no config delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3].accountImplementationAddress; } // Check if already delegated const code = await publicClient.getCode({ address: owner.address }); const expectedCode = `0xef0100${delegateAddress.toLowerCase().substring(2)}`; if (code === expectedCode) { console.log('Already delegated to Kernel, no authorization needed'); return undefined; } // Create authorization console.log('Creating EIP-7702 authorization for Kernel...'); // Get nonce for authorization const nonce = await publicClient.getTransactionCount({ address: owner.address, blockTag: 'pending' }); // Sign authorization using the account's signAuthorization method const authorization = await owner.signAuthorization({ contractAddress: delegateAddress, chainId: chain.id, nonce: nonce }); return authorization; } // ============================================ // Transaction Helpers // ============================================ /** * Send batch transaction (RECOMMENDED for smart accounts) * * Sends multiple calls in a single UserOperation, paying gas only once. * This is one of the main benefits of smart accounts over EOAs. * * @example * // Send ETH to 3 recipients in one transaction * await sendBatchTransaction({ * kernelAccount: account, * bundlerManager, * calls: [ * { to: '0xRecipient1', value: parseEther('0.01') }, * { to: '0xRecipient2', value: parseEther('0.02') }, * { to: '0xRecipient3', value: parseEther('0.03') } * ] * }); */ export async function sendBatchTransaction(config: BatchTransactionConfig): Promise { const { kernelAccount, bundlerManager, authorization, calls } = config; if (calls.length === 0) { throw new Error('Batch transaction must have at least one call'); } const bundlerClient = bundlerManager.getClient(kernelAccount.chain); // Normalize calls to ensure value and data are set const normalizedCalls = calls.map(call => ({ to: call.to, value: call.value ?? 0n, data: call.data ?? '0x' as Hex })); const userOpHash = await bundlerClient.sendUserOperation({ account: kernelAccount.account, authorization, calls: normalizedCalls }); return userOpHash as Hex; } /** * Send single transaction (convenience wrapper around sendBatchTransaction) * * For single operations, you can use this instead of sendBatchTransaction. * Under the hood, it creates a batch with one call. */ export async function sendKernelTransaction(config: SingleTransactionConfig): Promise { const { kernelAccount, bundlerManager, authorization, to, value = 0n, data = '0x' } = config; // Use batch transaction with single call return sendBatchTransaction({ kernelAccount, bundlerManager, authorization, calls: [{ to, value, data }] }); } // ============================================ // Wait for Receipt Helper // ============================================ export async function waitForKernelReceipt(config: { userOpHash: Hex; chain: Chain; bundlerManager: BundlerManager; }): Promise { const { userOpHash, chain, bundlerManager } = config; const bundlerClient = bundlerManager.getClient(chain); const receipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash }); return receipt; }