/** * Account Abstraction Service (Singleton) * * Centralized service for managing Kernel accounts, bundlers, and transactions. * Use this singleton across your application for consistent AA operations. * * Usage: * const aaService = AccountAbstractionService.getInstance({ * bundlerProvider: 'pimlico', * apiKey: env.pimlicoApiKey * }); * * const account = await aaService.createAccount(sepolia, owner); * const txHash = await aaService.sendTransaction({ account, to, value }); */ import { Chain, Hex, createPublicClient, http, PublicClient } from 'viem'; import { PrivateKeyAccount } from 'viem/accounts'; import { SignAuthorizationReturnType } from 'viem'; import { BundlerManager, createBundlerManager, BundlerConfig } from './bundler'; import { KernelAccountInstance, EntryPointVersion, Call, createKernel7702Account, createKernelAuthorization, sendKernelTransaction, sendBatchTransaction, waitForKernelReceipt } from '../lib/kernel-account'; import { ModuleType, installModule, uninstallModule, isModuleInstalled, createSessionKey, revokeSessionKey, SessionKeyPermission, installMultiSigValidator, prepareInstallModule, prepareUninstallModule, prepareInstallSessionKey, prepareRevokeSessionKey, prepareInstallMultiSigValidator } from '../lib/kernel-modules'; import { generateSessionKey, createSessionKeyApproval, deserializeSessionKey, createSessionKeyClient, createUSDCTransferPermission, createETHTransferPermission, SessionKeyInfo, SessionKeyPermissionRule } from '../lib/session-keys'; import { ModularSigner } from '@zerodev/permissions'; // ============================================ // Types // ============================================ import { AA_SupportConfig } from '../lib/type'; export interface AAServiceConfig { bundlerProvider: 'pimlico' | 'etherspot' | 'custom'; apiKey?: string; customBundlerUrl?: string; aaConfig?: AA_SupportConfig; } export interface CreateAccountOptions { chain: Chain; owner: PrivateKeyAccount; entryPointVersion?: EntryPointVersion; aaConfig?: AA_SupportConfig; } export interface SendTransactionOptions { account: KernelAccountInstance; to: Hex; value?: bigint; data?: Hex; authorization?: SignAuthorizationReturnType; } export interface SendBatchTransactionOptions { account: KernelAccountInstance; calls: Call[]; authorization?: SignAuthorizationReturnType; } export interface WaitForReceiptOptions { userOpHash: Hex; chain: Chain; } // ============================================ // Singleton Service // ============================================ export class AccountAbstractionService { private static instance: AccountAbstractionService | null = null; private bundlerManager: BundlerManager; private aaConfig: AA_SupportConfig | undefined; private publicClientCache: Map = new Map(); /** * Private constructor (singleton pattern) */ private constructor(config: AAServiceConfig) { const bundlerConfig: BundlerConfig = { provider: config.bundlerProvider, apiKey: config.apiKey, customUrl: config.customBundlerUrl, aaConfig: config.aaConfig }; this.bundlerManager = createBundlerManager(bundlerConfig); this.aaConfig = config.aaConfig; } /** * Get singleton instance */ static getInstance(config?: AAServiceConfig): AccountAbstractionService { if (!AccountAbstractionService.instance) { if (!config) { throw new Error( 'AccountAbstractionService must be initialized with config on first call' ); } AccountAbstractionService.instance = new AccountAbstractionService(config); } return AccountAbstractionService.instance; } /** * Reset singleton instance (useful for testing or reconfiguration) */ static reset(): void { AccountAbstractionService.instance = null; } // ============================================ // Account Management // ============================================ /** * Create a Kernel 7702 account */ async createAccount(options: CreateAccountOptions): Promise { const { chain, owner, entryPointVersion = '0.7', aaConfig } = options; // Use aaConfig from options or fallback to service-level config const config = aaConfig || this.aaConfig; // Create new account const account = await createKernel7702Account({ chain, owner, entryPointVersion, aaConfig: config }); return account; } /** * Check if account is already delegated to Kernel */ async isAccountDelegated(owner: PrivateKeyAccount, chain: Chain): Promise { const authorization = await this.createAuthorization({ owner, chain }); return authorization === undefined; } // ============================================ // Authorization Management // ============================================ /** * Create EIP-7702 authorization (if needed) */ async createAuthorization(options: { owner: PrivateKeyAccount; chain: Chain; }): Promise { const { owner, chain } = options; // Create authorization const authorization = await createKernelAuthorization({ owner, chain, aaConfig: this.aaConfig }); return authorization; } // ============================================ // Transaction Management // ============================================ /** * Prepare a call (for batching) * * Returns a Call object that can be included in a batch transaction. * Use this to prepare any transfer or contract interaction for batching. * * @example * // Prepare multiple calls * const calls = [ * aaService.prepareCall({ to: recipient1, value: parseEther('0.1') }), * aaService.prepareCall({ to: recipient2, value: parseEther('0.2') }), * aaService.prepareCall({ to: contractAddress, data: encodedData }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareCall(options: { to: Hex; value?: bigint; data?: Hex; }): Call { return { to: options.to, value: options.value ?? 0n, data: options.data ?? '0x' }; } /** * Prepare a contract call (for batching) * * Helper for preparing contract interactions with type-safe encoding. * * @example * import { parseAbi, encodeFunctionData } from 'viem'; * * const call = aaService.prepareContractCall({ * to: USDC_ADDRESS, * data: encodeFunctionData({ * abi: parseAbi(['function transfer(address to, uint256 amount)']), * functionName: 'transfer', * args: [recipient, parseUnits('100', 6)] * }) * }); */ prepareContractCall(options: { to: Hex; data: Hex; value?: bigint; }): Call { return { to: options.to, data: options.data, value: options.value ?? 0n }; } /** * Send a transaction using Kernel account (execute immediately) * * For batching multiple operations, use prepareCall() + sendBatchTransaction() instead. * * @example * // Single transaction * await aaService.sendTransaction({ * account, * to: recipient, * value: parseEther('0.1') * }); * * // For batching, use prepareCall instead: * const calls = [ * aaService.prepareCall({ to: recipient1, value: parseEther('0.1') }), * aaService.prepareCall({ to: recipient2, value: parseEther('0.2') }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ async sendTransaction(options: SendTransactionOptions): Promise { const { account, to, value = 0n, data = '0x', authorization } = options; // If no authorization provided, try to create one let auth = authorization; if (!auth) { auth = await this.createAuthorization({ owner: account.owner, chain: account.chain }); } // Send transaction const userOpHash = await sendKernelTransaction({ kernelAccount: account, bundlerManager: this.bundlerManager, authorization: auth, to, value, data }); return userOpHash; } /** * Wait for transaction receipt */ async waitForReceipt(options: WaitForReceiptOptions): Promise { const { userOpHash, chain } = options; return await waitForKernelReceipt({ userOpHash, chain, bundlerManager: this.bundlerManager }); } /** * Send transaction and wait for receipt (convenience method) */ async sendTransactionAndWait(options: SendTransactionOptions): Promise<{ userOpHash: Hex; receipt: any; }> { const userOpHash = await this.sendTransaction(options); const receipt = await this.waitForReceipt({ userOpHash, chain: options.account.chain }); return { userOpHash, receipt }; } /** * Send batch transaction (RECOMMENDED for smart accounts) * * Sends multiple calls in a single UserOperation, paying gas ONLY ONCE. * This is one of the main advantages of smart accounts over EOAs. * * Use prepare*() methods to create Call objects for batching: * - prepareCall() - for transfers and contract interactions * - prepareInstallModule() - for module installations * - prepareInstallSessionKey() - for session keys * - prepareInstallMultiSigValidator() - for multi-sig * - etc. * * @example * // Send ETH to 3 recipients in one transaction * const calls = [ * aaService.prepareCall({ to: '0xRecipient1', value: parseEther('0.01') }), * aaService.prepareCall({ to: '0xRecipient2', value: parseEther('0.02') }), * aaService.prepareCall({ to: '0xRecipient3', value: parseEther('0.03') }) * ]; * await aaService.sendBatchTransaction({ account, calls }); * * @example * // Mix transfers, contract calls, and module installations * const calls = [ * aaService.prepareCall({ to: recipient, value: parseEther('0.1') }), * aaService.prepareContractCall({ to: USDC, data: transferData }), * aaService.prepareInstallSessionKey({ account, sessionKeyAddress, permissions }) * ]; * await aaService.sendBatchTransaction({ account, calls }); * * @example * // Install multiple modules at once * const calls = [ * aaService.prepareInstallSessionKey({ account, ... }), * aaService.prepareInstallMultiSigValidator({ account, ... }), * aaService.prepareInstallModule({ account, moduleType: 'hook', ... }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ async sendBatchTransaction(options: SendBatchTransactionOptions): Promise { const { account, calls, authorization } = options; if (calls.length === 0) { throw new Error('Batch transaction must have at least one call'); } // If no authorization provided, try to create one let auth = authorization; if (!auth) { auth = await this.createAuthorization({ owner: account.owner, chain: account.chain }); } // Send batch transaction const userOpHash = await sendBatchTransaction({ kernelAccount: account, bundlerManager: this.bundlerManager, authorization: auth, calls }); return userOpHash; } /** * Send batch transaction and wait for receipt (convenience method) * * Same as sendBatchTransaction but waits for confirmation. */ async sendBatchTransactionAndWait(options: SendBatchTransactionOptions): Promise<{ userOpHash: Hex; receipt: any; }> { const userOpHash = await this.sendBatchTransaction(options); const receipt = await this.waitForReceipt({ userOpHash, chain: options.account.chain }); return { userOpHash, receipt }; } // ============================================ // Utility Methods // ============================================ /** * Get account balance */ async getBalance(account: KernelAccountInstance): Promise { const publicClient = this.getPublicClient(account.chain); return await publicClient.getBalance({ address: account.address as Hex }); } /** * Get or create public client for standard RPC calls * (bundler clients don't support standard eth_* methods) */ private getPublicClient(chain: Chain): PublicClient { if (!this.publicClientCache.has(chain.id)) { const client = createPublicClient({ chain, transport: http() }); this.publicClientCache.set(chain.id, client); } return this.publicClientCache.get(chain.id)!; } /** * Get bundler information */ getBundlerInfo(chain: Chain): { provider: string; url: string; } { return { provider: this.bundlerManager.getProvider(), url: this.bundlerManager.getBundlerUrl(chain) }; } /** * Get bundler client directly (advanced usage) */ getBundlerClient(chain: Chain) { return this.bundlerManager.getClient(chain); } /** * Get bundler manager (advanced usage) */ getBundlerManager(chain: Chain): BundlerManager { return this.bundlerManager; } // ============================================ // Module Management (ERC-7579) // ============================================ /** * Prepare module installation call (for batching) * * Returns a Call object that can be included in a batch transaction. * This allows you to install multiple modules in a single UserOperation. * * @example * // Batch install multiple modules * const calls = [ * aaService.prepareInstallModule({ * account, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR * }), * aaService.prepareInstallModule({ * account, * moduleType: 'hook', * moduleAddress: SPENDING_LIMIT_HOOK * }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareInstallModule(options: { account: KernelAccountInstance; moduleType: ModuleType; moduleAddress: Hex; initData?: Hex; }): Call { return prepareInstallModule(options); } /** * Install a module on an account (execute immediately) * * For batching multiple modules, use prepareInstallModule instead. * * @example * // Install a single module * await aaService.installModule({ * account, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS, * initData: encodedPermissions * }); */ async installModule(options: { account: KernelAccountInstance; moduleType: ModuleType; moduleAddress: Hex; initData?: Hex; }): Promise { return installModule({ ...options, bundlerManager: this.bundlerManager }); } /** * Prepare module uninstallation call (for batching) * * Returns a Call object that can be included in a batch transaction. * * @example * // Batch uninstall multiple modules * const calls = [ * aaService.prepareUninstallModule({ account, moduleType: 'validator', moduleAddress: '0x...' }), * aaService.prepareUninstallModule({ account, moduleType: 'hook', moduleAddress: '0x...' }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareUninstallModule(options: { account: KernelAccountInstance; moduleType: ModuleType; moduleAddress: Hex; deInitData?: Hex; }): Call { return prepareUninstallModule(options); } /** * Uninstall a module from an account (execute immediately) * * For batching, use prepareUninstallModule instead. * * @example * await aaService.uninstallModule({ * account, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS * }); */ async uninstallModule(options: { account: KernelAccountInstance; moduleType: ModuleType; moduleAddress: Hex; deInitData?: Hex; }): Promise { return uninstallModule({ ...options, bundlerManager: this.bundlerManager }); } /** * Check if a module is installed * * @example * const isInstalled = await aaService.isModuleInstalled({ * account, * moduleType: 'validator', * moduleAddress: SESSION_KEY_VALIDATOR_ADDRESS * }); */ async isModuleInstalled(options: { account: KernelAccountInstance; moduleType: ModuleType; moduleAddress: Hex; }): Promise { return isModuleInstalled({ ...options, bundlerManager: this.bundlerManager }); } /** * Prepare session key installation call (for batching) * * Returns a Call object that can be batched with other operations. * * @example * // Batch install session key with multi-sig * const calls = [ * aaService.prepareInstallSessionKey({ account, sessionKeyAddress, permissions }), * aaService.prepareInstallMultiSigValidator({ account, owners, threshold: 2 }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareInstallSessionKey(options: { account: KernelAccountInstance; sessionKeyAddress: Hex; permissions: SessionKeyPermission[]; }): Call { return prepareInstallSessionKey(options); } /** * Create and install a session key for automated operations (execute immediately) * * Perfect for use cases like: * - Recurring payments * - Automated savings * - Subscription management * - DCA (Dollar Cost Averaging) * * For batching with other operations, use prepareInstallSessionKey instead. * * @example * // Create session key for monthly savings * const sessionKey = privateKeyToAccount('0x...'); * * await aaService.createSessionKey({ * account, * sessionKeyAddress: sessionKey.address, * permissions: [{ * target: USDC_ADDRESS, * functionSelector: '0xa9059cbb', // transfer(address,uint256) * maxValuePerUse: parseUnits('100', 6), // Max 100 USDC * validUntil: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days * maxUses: 1 // Once per month * }] * }); */ async createSessionKey(options: { account: KernelAccountInstance; sessionKeyAddress: Hex; permissions: SessionKeyPermission[]; }): Promise { return createSessionKey({ ...options, bundlerManager: this.bundlerManager }); } /** * 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 = [ * aaService.prepareRevokeSessionKey({ account, sessionKeyAddress: key1 }), * aaService.prepareRevokeSessionKey({ account, sessionKeyAddress: key2 }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareRevokeSessionKey(options: { account: KernelAccountInstance; sessionKeyAddress: Hex; }): Call { return prepareRevokeSessionKey(options); } /** * Revoke a session key (execute immediately) * * For batching, use prepareRevokeSessionKey instead. * * @example * await aaService.revokeSessionKey({ * account, * sessionKeyAddress: sessionKey.address * }); */ async revokeSessionKey(options: { account: KernelAccountInstance; sessionKeyAddress: Hex; }): Promise { return revokeSessionKey( options.account, this.bundlerManager, options.sessionKeyAddress ); } /** * 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 = [ * aaService.prepareInstallMultiSigValidator({ account, owners, threshold: 2 }), * aaService.prepareInstallSessionKey({ account, sessionKeyAddress, permissions }) * ]; * await aaService.sendBatchTransaction({ account, calls }); */ prepareInstallMultiSigValidator(options: { account: KernelAccountInstance; owners: Hex[]; threshold: number; }): Call { return prepareInstallMultiSigValidator(options); } /** * Install a multi-signature validator (execute immediately) * * For batching, use prepareInstallMultiSigValidator instead. * * @example * await aaService.installMultiSigValidator({ * account, * owners: [owner1, owner2, owner3], * threshold: 2 // 2 of 3 required * }); */ async installMultiSigValidator(options: { account: KernelAccountInstance; owners: Hex[]; threshold: number; }): Promise { return installMultiSigValidator({ ...options, bundlerManager: this.bundlerManager }); } // ============================================ // EIP-7702 Session Keys (NEW Pattern) // ============================================ /** * Generate a new session key * * Creates a new keypair for use as a session key with EIP-7702 accounts. * The private key should be stored securely and shared with the agent. * The address should be shared with the owner for approval. * * This uses the NEW EIP-7702 pattern, not the legacy module-based pattern. * * @returns Session key info including private key, address, and signer * * @example * // Agent generates session key * const sessionKey = await aaService.generateSessionKey(); * console.log("Share this address with owner:", sessionKey.address); * console.log("Keep this private key secure:", sessionKey.privateKey); * // Store sessionKey.privateKey securely for later use */ async generateSessionKey(): Promise { return await generateSessionKey(); } /** * 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 with USDC transfer restrictions * const approval = await aaService.createSessionKeyApproval({ * sessionKeyAddress: '0x...', // Agent's session key address * owner: ownerAccount, * chain: sepolia, * permissions: [ * aaService.createUSDCTransferPermission(USDC_ADDRESS, '10') // Max 10 USDC * ] * }); * // Share approval with agent * * @example * // Owner approves session key with sudo (unrestricted) access * const approval = await aaService.createSessionKeyApproval({ * sessionKeyAddress: agentSessionKey.address, * owner: ownerAccount, * chain: sepolia, * useSudoPolicy: true * }); */ async createSessionKeyApproval(options: { sessionKeyAddress: Hex; sessionKeyPrivateKey: Hex; owner: PrivateKeyAccount; chain: Chain; entryPointVersion?: '0.6' | '0.7'; useSudoPolicy?: boolean; permissions?: SessionKeyPermissionRule[]; }): Promise { return await createSessionKeyApproval(options); } /** * 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 options - Deserialization options * @returns Deserialized session key account * * @example * // Agent deserializes with private key * const sessionKey = await aaService.recreateSessionKey(storedPrivateKey); * const account = await aaService.deserializeSessionKey({ * approval, * sessionKeySigner: sessionKey.signer, * chain: sepolia * }); */ async deserializeSessionKey(options: { approval: string; sessionKeySigner: ModularSigner; chain: Chain; entryPointVersion?: '0.6' | '0.7'; }) { return await deserializeSessionKey(options); } /** * Create kernel client for session key * * Creates a client for sending transactions with the session key. * * @param options - Client options * @returns Kernel account client * * @example * // Agent creates client for session key * const sessionKey = await aaService.recreateSessionKey(storedPrivateKey); * const account = await aaService.deserializeSessionKey({ * approval, * sessionKeySigner: sessionKey.signer, * chain: sepolia * }); * const client = aaService.createSessionKeyClient({ * account, * chain: sepolia, * bundlerUrl: 'https://api.pimlico.io/...' * }); */ createSessionKeyClient(options: { account: any; chain: Chain; bundlerUrl: string; paymasterUrl?: string; }): any { return createSessionKeyClient(options); } /** * 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 = aaService.createUSDCTransferPermission( * '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', // Sepolia USDC * '10' // Max 10 USDC * ); */ createUSDCTransferPermission( usdcAddress: Hex, maxAmount: string, destinationAddress: Hex ): SessionKeyPermissionRule { return createUSDCTransferPermission(usdcAddress, maxAmount, destinationAddress); } /** * 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 = aaService.createETHTransferPermission('0.1'); // Max 0.1 ETH */ createETHTransferPermission(maxValue: string): SessionKeyPermissionRule { return createETHTransferPermission(maxValue); } // ============================================ // Private Helpers // ============================================ private getAccountCacheKey( chain: Chain, owner: PrivateKeyAccount, entryPointVersion: EntryPointVersion ): string { return `${chain.id}-${owner.address}-${entryPointVersion}`; } private getAuthorizationCacheKey( chain: Chain, owner: PrivateKeyAccount ): string { return `${chain.id}-${owner.address}`; } } // ============================================ // Convenience Exports // ============================================ export default AccountAbstractionService; // Re-export session key types for convenience export type { SessionKeyInfo, SessionKeyPermissionRule } from '../lib/session-keys';