/** * Kernel Module Management * * Handles installation and management of ERC-7579 modules for Kernel v3 accounts: * - Validators: Session keys, multi-sig, passkeys * - Executors: Additional execution logic * - Hooks: Pre/post execution hooks * - Fallbacks: Handle unknown function calls * * ERC-7579 Module Types: * 1 = Validator * 2 = Executor * 3 = Fallback * 4 = Hook */ import { Hex, encodePacked, parseAbi } from 'viem'; import { KernelAccountInstance } from './kernel-account'; import { BundlerManager } from '../services/bundler'; // ============================================ // Module Types (ERC-7579 Standard) // ============================================ export type ModuleType = 'validator' | 'executor' | 'fallback' | 'hook'; export const MODULE_TYPE_IDS: Record = { validator: 1, executor: 2, fallback: 3, hook: 4 }; // ============================================ // Module Installation Configuration // ============================================ export interface InstallModuleConfig { account: KernelAccountInstance; bundlerManager: BundlerManager; moduleType: ModuleType; moduleAddress: Hex; initData?: Hex; } export interface UninstallModuleConfig { account: KernelAccountInstance; bundlerManager: BundlerManager; moduleType: ModuleType; moduleAddress: Hex; deInitData?: Hex; } export interface ModuleStatus { isInstalled: boolean; moduleAddress: Hex; moduleType: ModuleType; } // ============================================ // Session Key Configuration // ============================================ /** * Session key permissions for automated operations * * Example: Allow automated monthly transfers to a savings account */ export interface SessionKeyPermission { /** Target contract address (token, vault, etc.) */ target: Hex; /** Maximum value per transaction (for ETH transfers) */ maxValuePerUse?: bigint; /** Function selector to allow (e.g., transfer, approve) */ functionSelector?: Hex; /** Maximum gas per transaction */ maxGasPerUse?: bigint; /** Session expiry timestamp */ validUntil?: number; /** Session start timestamp */ validAfter?: number; /** Maximum number of uses */ maxUses?: number; } export interface CreateSessionKeyConfig { account: KernelAccountInstance; bundlerManager: BundlerManager; sessionKeyAddress: Hex; permissions: SessionKeyPermission[]; } // ============================================ // Module Call Preparation (For Batching) // ============================================ import { Call } from './kernel-account'; /** * Prepare installModule call (for batching) * * Returns a Call object that can be batched with other operations. * Use this when you want to install multiple modules in one transaction. * * @example * // Batch install multiple modules * const calls = [ * prepareInstallModule({ account, moduleType: 'validator', moduleAddress: '0x...' }), * prepareInstallModule({ account, moduleType: 'hook', moduleAddress: '0x...' }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ export function prepareInstallModule(config: Omit): Call { const { account, moduleType, moduleAddress, initData = '0x' } = config; // Get the module type ID according to ERC-7579 const moduleTypeId = MODULE_TYPE_IDS[moduleType]; // Encode the installModule call // function installModule(uint256 moduleTypeId, address module, bytes calldata initData) const calldata = encodePacked( ['bytes4', 'uint256', 'address', 'bytes'], [ '0x8f6e1e8e', // installModule selector BigInt(moduleTypeId), moduleAddress, initData ] ); return { to: account.address as Hex, data: calldata }; } /** * Install a module on a Kernel account (execute immediately) * * This uses the ERC-7579 standard installModule function. * For batching multiple modules, use prepareInstallModule instead. * * @example * // Install a single module * const userOpHash = await installModule({ * account, * bundlerManager, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS, * initData: encodedPermissions * }); */ export async function installModule(config: InstallModuleConfig): Promise { const { account, bundlerManager, moduleType, moduleAddress, initData = '0x' } = config; const bundlerClient = bundlerManager.getClient(account.chain); console.log(`Installing ${moduleType} module at ${moduleAddress}...`); // Use prepareInstallModule to get the call const call = prepareInstallModule({ account, moduleType, moduleAddress, initData }); // Execute immediately const userOpHash = await bundlerClient.sendUserOperation({ account: account.account, calls: [call] }); console.log(`✅ Module installation submitted: ${userOpHash}`); return userOpHash as Hex; } /** * Prepare uninstallModule call (for batching) * * Returns a Call object that can be batched with other operations. * * @example * // Batch uninstall multiple modules * const calls = [ * prepareUninstallModule({ account, moduleType: 'validator', moduleAddress: '0x...' }), * prepareUninstallModule({ account, moduleType: 'hook', moduleAddress: '0x...' }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ export function prepareUninstallModule(config: Omit): Call { const { account, moduleType, moduleAddress, deInitData = '0x' } = config; const moduleTypeId = MODULE_TYPE_IDS[moduleType]; // Encode the uninstallModule call // function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) const calldata = encodePacked( ['bytes4', 'uint256', 'address', 'bytes'], [ '0xbf8a8dc4', // uninstallModule selector BigInt(moduleTypeId), moduleAddress, deInitData ] ); return { to: account.address as Hex, data: calldata }; } /** * Uninstall a module from a Kernel account (execute immediately) * * For batching multiple uninstalls, use prepareUninstallModule instead. * * @example * // Uninstall a single module * const userOpHash = await uninstallModule({ * account, * bundlerManager, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS * }); */ export async function uninstallModule(config: UninstallModuleConfig): Promise { const { account, bundlerManager, moduleType, moduleAddress, deInitData = '0x' } = config; const bundlerClient = bundlerManager.getClient(account.chain); console.log(`Uninstalling ${moduleType} module at ${moduleAddress}...`); // Use prepareUninstallModule to get the call const call = prepareUninstallModule({ account, moduleType, moduleAddress, deInitData }); // Execute immediately const userOpHash = await bundlerClient.sendUserOperation({ account: account.account, calls: [call] }); console.log(`✅ Module uninstallation submitted: ${userOpHash}`); return userOpHash as Hex; } /** * Check if a module is installed on an account * * @example * const isInstalled = await isModuleInstalled({ * account, * bundlerManager, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS * }); */ export async function isModuleInstalled( config: Omit ): Promise { const { account, bundlerManager, moduleType, moduleAddress } = config; const publicClient = bundlerManager.getClient(account.chain); const moduleTypeId = MODULE_TYPE_IDS[moduleType]; try { // Call isModuleInstalled(uint256 moduleTypeId, address module) const result = await publicClient.readContract({ address: account.address as Hex, abi: parseAbi([ 'function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) view returns (bool)' ]), functionName: 'isModuleInstalled', args: [BigInt(moduleTypeId), moduleAddress, '0x'] }); return result as boolean; } catch (error) { console.error('Error checking module installation:', error); return false; } } // ============================================ // Session Key Helpers // ============================================ /** * Prepare session key installation call (for batching) * * Returns a Call object that can be batched with other operations. * * @example * // Batch install session key with other modules * const calls = [ * prepareInstallSessionKey({ account, sessionKeyAddress, permissions }), * prepareInstallModule({ account, moduleType: 'hook', moduleAddress: '0x...' }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ export function prepareInstallSessionKey(config: Omit): Call { const { account, sessionKeyAddress, permissions } = config; // Encode session key permissions const initData = encodeSessionKeyPermissions(sessionKeyAddress, permissions); // Get session key validator address for your chain const SESSION_KEY_VALIDATOR = getSessionKeyValidatorAddress(account.chain.id); // Return the install module call return prepareInstallModule({ account, moduleType: 'validator', moduleAddress: SESSION_KEY_VALIDATOR, initData }); } /** * Create and install a session key validator (execute immediately) * * Session keys allow delegated access with specific permissions. * Perfect for automated operations like recurring payments. * * For batching with other operations, use prepareInstallSessionKey instead. * * @example * // Install session key for monthly savings automation * const sessionKey = privateKeyToAccount('0x...'); * * await createSessionKey({ * account, * bundlerManager, * sessionKeyAddress: sessionKey.address, * permissions: [{ * target: USDC_ADDRESS, * functionSelector: '0xa9059cbb', // transfer(address,uint256) * maxValuePerUse: parseUnits('100', 6), // Max 100 USDC per tx * validUntil: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days * maxUses: 1 // Once per month * }] * }); */ export async function createSessionKey( config: CreateSessionKeyConfig ): Promise { const { account, bundlerManager, sessionKeyAddress, permissions } = config; // Encode session key permissions const initData = encodeSessionKeyPermissions(sessionKeyAddress, permissions); // Get session key validator address for your chain const SESSION_KEY_VALIDATOR = getSessionKeyValidatorAddress(account.chain.id); // Install the session key validator with permissions return installModule({ account, bundlerManager, moduleType: 'validator', moduleAddress: SESSION_KEY_VALIDATOR, initData }); } /** * Prepare session key revocation call (for batching) * * Returns a Call object that can be batched with other operations. * * @example * // Batch revoke multiple session keys * const calls = [ * prepareRevokeSessionKey({ account, sessionKeyAddress: key1 }), * prepareRevokeSessionKey({ account, sessionKeyAddress: key2 }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ export function prepareRevokeSessionKey(config: { account: KernelAccountInstance; sessionKeyAddress: Hex; }): Call { const { account, sessionKeyAddress } = config; const SESSION_KEY_VALIDATOR = getSessionKeyValidatorAddress(account.chain.id); const deInitData = encodePacked(['address'], [sessionKeyAddress]); return prepareUninstallModule({ account, moduleType: 'validator', moduleAddress: SESSION_KEY_VALIDATOR, deInitData }); } /** * Revoke a session key (execute immediately) * * For batching with other operations, use prepareRevokeSessionKey instead. */ export async function revokeSessionKey( account: KernelAccountInstance, bundlerManager: BundlerManager, sessionKeyAddress: Hex ): Promise { const SESSION_KEY_VALIDATOR = getSessionKeyValidatorAddress(account.chain.id); const deInitData = encodePacked(['address'], [sessionKeyAddress]); return uninstallModule({ account, bundlerManager, moduleType: 'validator', moduleAddress: SESSION_KEY_VALIDATOR, deInitData }); } // ============================================ // Helper Functions // ============================================ function encodeSessionKeyPermissions( sessionKeyAddress: Hex, permissions: SessionKeyPermission[] ): Hex { // This encoding depends on your session key validator implementation // Example encoding for a simple permission structure: const encodedPermissions = permissions.map(p => { return encodePacked( ['address', 'uint256', 'bytes4', 'uint256', 'uint48', 'uint48', 'uint48'], [ p.target, p.maxValuePerUse ?? 0n, p.functionSelector ?? '0x00000000', p.maxGasPerUse ?? 0n, BigInt(p.validAfter ?? 0) as any, BigInt(p.validUntil ?? 0) as any, BigInt(p.maxUses ?? 0) as any ] ); }); // Combine session key address with permissions return encodePacked( ['address', 'uint256', 'bytes[]'], [ sessionKeyAddress, BigInt(permissions.length), encodedPermissions ] ); } function getSessionKeyValidatorAddress(chainId: number): Hex { // Known session key validator addresses per chain // These would be pre-deployed or use a universal address const addresses: Record = { 1: '0x0000000000000000000000000000000000000000', // Mainnet 11155111: '0x0000000000000000000000000000000000000000', // Sepolia // Add more chains as needed }; const address = addresses[chainId]; if (!address) { throw new Error(`Session key validator not available for chain ${chainId}`); } return address; } // ============================================ // Multi-Signature Module Helpers // ============================================ /** * Prepare multi-sig validator installation call (for batching) * * Returns a Call object that can be batched with other operations. * * @example * // Batch install multi-sig with session key * const calls = [ * prepareInstallMultiSigValidator({ account, owners, threshold: 2 }), * prepareInstallSessionKey({ account, sessionKeyAddress, permissions }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ export function prepareInstallMultiSigValidator(config: { account: KernelAccountInstance; owners: Hex[]; threshold: number; }): Call { const { account, owners, threshold } = config; const initData = encodePacked( ['address[]', 'uint256'], [owners, BigInt(threshold)] ); const MULTISIG_VALIDATOR = getMultiSigValidatorAddress(account.chain.id); return prepareInstallModule({ account, moduleType: 'validator', moduleAddress: MULTISIG_VALIDATOR, initData }); } /** * Install a multi-signature validator (execute immediately) * * For batching with other operations, use prepareInstallMultiSigValidator instead. * * @example * await installMultiSigValidator({ * account, * bundlerManager, * owners: [owner1, owner2, owner3], * threshold: 2 // 2 of 3 signatures required * }); */ export async function installMultiSigValidator(config: { account: KernelAccountInstance; bundlerManager: BundlerManager; owners: Hex[]; threshold: number; }): Promise { const { account, bundlerManager, owners, threshold } = config; const initData = encodePacked( ['address[]', 'uint256'], [owners, BigInt(threshold)] ); const MULTISIG_VALIDATOR = getMultiSigValidatorAddress(account.chain.id); return installModule({ account, bundlerManager, moduleType: 'validator', moduleAddress: MULTISIG_VALIDATOR, initData }); } function getMultiSigValidatorAddress(chainId: number): Hex { // Known multi-sig validator addresses per chain const addresses: Record = { 1: '0x0000000000000000000000000000000000000000', 11155111: '0x0000000000000000000000000000000000000000', }; const address = addresses[chainId]; if (!address) { throw new Error(`Multi-sig validator not available for chain ${chainId}`); } return address; }