/** * EIP-7702 Session Keys Utilities * * Utilities for creating and managing session keys with ZeroDev's permission system. * This uses the NEW EIP-7702 pattern, not the legacy smart account pattern. * * Key differences from legacy pattern: * - Uses `eip7702Account` instead of `address` * - Uses `addressToEmptyAccount` for permission validator creation * - Passes `sessionKeySigner` to `deserializePermissionAccount` * - No sudo validator needed, only permission plugin */ import { Hex, Address, createPublicClient, http, parseAbi, parseUnits, parseEther } from 'viem'; import { Chain } from 'viem/chains'; import { PrivateKeyAccount, privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { entryPoint07Address } from 'viem/account-abstraction'; import { createKernelAccount, createKernelAccountClient, addressToEmptyAccount, CreateKernelAccountReturnType, // serializePermissionAccount, // deserializePermissionAccount } from '@zerodev/sdk'; import { toECDSASigner } from '@zerodev/permissions/signers'; import { toPermissionValidator, ModularSigner, serializePermissionAccount, deserializePermissionAccount } from '@zerodev/permissions'; import { toSudoPolicy, toCallPolicy, CallPolicyVersion, ParamCondition } from '@zerodev/permissions/policies'; import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants'; import { createPimlicoClient } from 'permissionless/clients/pimlico'; // ============================================ // Types // ============================================ export interface SessionKeyPermissionRule { target: Hex; abi: any[]; functionName: string; args?: any[]; } export interface CreateSessionKeyApprovalOptions { sessionKeyAddress: Address; sessionKeyPrivateKey: Hex; owner: PrivateKeyAccount; chain: Chain; entryPointVersion?: '0.6' | '0.7'; useSudoPolicy?: boolean; permissions?: SessionKeyPermissionRule[]; } export interface SessionKeyInfo { privateKey: Hex; address: Address; signer: ModularSigner; } export interface DeserializedSessionKey { account: any; client: any; } // ============================================ // Session Key Generation // ============================================ /** * Generate a new session key * * Creates a new keypair for use as a session key or use the one that was passed if available. * The private key should be stored securely and shared with the agent. * The address should be shared with the owner for approval. * * @returns Session key info including private key, address, and signer * * @example * const sessionKey = await generateSessionKey(); or * const privateKey = '0x...'; // Existing private key * const sessionKey = await generateSessionKey(privateKey); * * console.log("Share this address with owner:", sessionKey.address); * console.log("Keep this private key secure:", sessionKey.privateKey); */ export async function generateSessionKey(privateKey?: Hex): Promise { const sessionPrivateKey = privateKey || generatePrivateKey(); const sessionKeyAccount = privateKeyToAccount(sessionPrivateKey); const sessionKeySigner = await toECDSASigner({ signer: sessionKeyAccount as any, }); return { privateKey: sessionPrivateKey, address: sessionKeyAccount.address, signer: sessionKeySigner }; } // ============================================ // Owner Side: Create Approval // ============================================ /** * Create session key approval (Owner side) * * The owner calls this to create an approval for a session key address. * This uses the NEW EIP-7702 pattern with addressToEmptyAccount. * * @param options - Configuration options * @returns Serialized approval string to share with the agent * * @example * // Owner approves session key * const approval = await createSessionKeyApproval({ * sessionKeyAddress: '0x...', // Agent's session key address * owner: ownerAccount, * chain: sepolia, * permissions: [{ * target: USDC_ADDRESS, * abi: parseAbi(['function transfer(address to, uint256 amount)']), * functionName: 'transfer', * args: [null, { condition: 3, value: parseUnits('10', 6) }] * }] * }); * // Share approval with agent */ export async function createSessionKeyApproval( options: CreateSessionKeyApprovalOptions ): Promise { const { sessionKeyAddress, sessionKeyPrivateKey, owner, chain, entryPointVersion = '0.7', useSudoPolicy = false, permissions = [] } = options; const publicClient = createPublicClient({ chain, transport: http() }); const entryPoint = getEntryPoint(entryPointVersion); // Create session key signer from private key (NOT addressToEmptyAccount!) const sessionKeyAccount = privateKeyToAccount(sessionKeyPrivateKey); const sessionKeySigner = await toECDSASigner({ signer: sessionKeyAccount }); // Create permission plugin let policies: any[]; if (useSudoPolicy || permissions.length === 0) { // Sudo policy - unrestricted access policies = [toSudoPolicy({})]; } else { // Call policy - restricted access const callPermissions = permissions.map(perm => ({ target: perm.target, abi: perm.abi, functionName: perm.functionName, args: perm.args || [] })); policies = [ toCallPolicy({ policyVersion: CallPolicyVersion.V0_0_5, permissions: callPermissions }) ]; } const permissionPlugin = await toPermissionValidator(publicClient as any, { entryPoint, signer: sessionKeySigner, policies, kernelVersion: KERNEL_V3_3, }); // Create kernel account with eip7702Account AND address const kernelSessionKeyAccount = await createKernelAccount(publicClient as any, { entryPoint, eip7702Account: owner as any, plugins: { regular: permissionPlugin, }, kernelVersion: KERNEL_V3_3, address: owner.address as Hex, // Include owner's address }); // Serialize approval WITH session key private key (critical!) const approval = await serializePermissionAccount( kernelSessionKeyAccount as any, sessionKeyPrivateKey ); return approval; } // ============================================ // Agent Side: Use Session Key // ============================================ /** * Deserialize session key account (Agent side) * * The agent calls this to reconstruct the session key account from the approval. * This requires BOTH the approval AND the session key signer. * * @param approval - Approval string from owner * @param sessionKeySigner - Session key signer (created from private key) * @param chain - Chain to use * @param entryPointVersion - EntryPoint version * @returns Deserialized session key account * * @example * // Agent deserializes with private key * const sessionKey = await recreateSessionKey(storedPrivateKey); * const account = await deserializeSessionKey({ * approval, * sessionKeySigner: sessionKey.signer, * chain: sepolia * }); */ export async function deserializeSessionKey(options: { approval: string; sessionKeySigner: ModularSigner; chain: Chain; entryPointVersion?: '0.6' | '0.7'; }): Promise> { const { approval, sessionKeySigner, chain, entryPointVersion = '0.7' } = options; const publicClient = createPublicClient({ chain, transport: http() }); const entryPoint = getEntryPoint(entryPointVersion); // Deserialize WITH the session key signer // This is the 5-parameter version (not 4!) const sessionKeyAccount = await deserializePermissionAccount( publicClient, entryPoint, KERNEL_V3_3, approval, sessionKeySigner // ← Must pass the signer! ); return sessionKeyAccount; } /** * Create kernel client for session key * * Creates a client for sending transactions with the session key. * * @param account - Deserialized session key account * @param chain - Chain to use * @param bundlerUrl - Bundler RPC URL * @param paymasterUrl - Optional paymaster URL for gas sponsorship * @returns Kernel account client * * @example * const client = createSessionKeyClient({ * account: sessionKeyAccount, * chain: sepolia, * bundlerUrl: 'https://api.pimlico.io/...' * }); */ export function createSessionKeyClient(options: { account: any; chain: Chain; bundlerUrl: string; paymasterUrl?: string; entryPoint?: { address: Hex; version: '0.6' | '0.7' }; }): any { const { account, chain, bundlerUrl, paymasterUrl, entryPoint = { address: entryPoint07Address, version: '0.7' as const } } = options; // Create Pimlico client for gas estimation const paymasterClient = createPimlicoClient({ transport: http(bundlerUrl), entryPoint }); const clientOptions: any = { account, chain, bundlerTransport: http(bundlerUrl), userOperation: { estimateFeesPerGas: async () => (await paymasterClient.getUserOperationGasPrice()).fast, } }; // Add paymaster if provided if (paymasterUrl) { // Note: Paymaster client creation would go here // For now using Pimlico gas estimation } const kernelClient = createKernelAccountClient(clientOptions); return kernelClient; } // ============================================ // Convenience Functions // ============================================ /** * Create USDC transfer permission rule * * Helper to create a permission rule for USDC transfers with a maximum amount. * * @param usdcAddress - USDC contract address * @param maxAmount - Maximum USDC amount (in USDC units, e.g., "10" for 10 USDC) * @returns Permission rule * * @example * const rule = createUSDCTransferPermission( * '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', * '10' // Max 10 USDC * ); */ export function createUSDCTransferPermission( usdcAddress: Hex, maxAmount: string, destinationAddress: Hex ): SessionKeyPermissionRule { return { target: usdcAddress, abi: parseAbi(['function transfer(address to, uint256 amount) returns (bool)']) as any, functionName: 'transfer', args: [ { condition: ParamCondition.EQUAL, value: destinationAddress, } , // Specific recipient { condition: ParamCondition.LESS_THAN_OR_EQUAL, // LESS_THAN_OR_EQUAL value: parseUnits(maxAmount, 6) // USDC has 6 decimals } ] }; } /** * Create ETH transfer permission rule * * Helper to create a permission rule for ETH transfers with a maximum value. * * @param maxValue - Maximum ETH value (in ether units, e.g., "0.1" for 0.1 ETH) * @returns Permission rule * * @example * const rule = createETHTransferPermission('0.1'); // Max 0.1 ETH */ export function createETHTransferPermission(maxValue: string): SessionKeyPermissionRule { return { target: '0x0000000000000000000000000000000000000000' as Hex, // Native token abi: [], functionName: '', args: [ { condition: 3, // LESS_THAN_OR_EQUAL value: parseEther(maxValue) } ] }; }