/** * EVM Smart Wallet Implementation * * Provides Account Abstraction (EIP-4337) and EIP-7702 capabilities to EVMChainWallet. * This class wraps the AA service and provides a clean API for smart wallet features. */ import { Chain, Hex, parseEther, parseUnits, createPublicClient, http, PublicClient } from "viem"; import { privateKeyToAccount, PrivateKeyAccount } from "viem/accounts"; import { ModularSigner } from "@zerodev/permissions"; import { Balance, TransactionResult } from "../types"; import { SmartWalletOptions, SmartWalletTransactionResult, Call, SessionKeyInfo, SessionKeyPermissionRule, SessionKeyApprovalOptions, SessionKeyUsageOptions, ModuleInstallOptions, ModuleUninstallOptions, MultiSigConfig, RecoveryConfig, PaymasterConfig, SmartAccountInfo, SmartWalletError, SessionKeyError, ModuleError, TransactionError, ModuleType } from "./smartWallet.types"; // Import AA service from local aa-service import { AccountAbstractionService } from "./aa-service/services/account-abstraction"; import { createKernelAuthorization } from "./aa-service/lib/kernel-account"; import type { KernelAccountInstance, Call as AACall } from "./aa-service/lib/kernel-account"; import type { SessionKeyPermission } from "./aa-service/lib/kernel-modules"; /** * EVMSmartWallet - Smart wallet capabilities for EVM chains * * Provides: * - EIP-7702 account delegation * - Batch transactions (pay gas once for multiple operations) * - Session keys with granular permissions * - Module management (validators, hooks, executors) * - Gas sponsorship (paymasters) * - Multi-signature support * - Account recovery */ export class EVMSmartWallet { aaService: AccountAbstractionService | null = null; private kernelAccount: KernelAccountInstance | null = null; private ownerAccount: PrivateKeyAccount; private chain: Chain; private options: SmartWalletOptions; private paymasterConfig: PaymasterConfig | null = null; constructor( privateKey: string, chain: Chain, options: SmartWalletOptions = {} ) { this.ownerAccount = privateKeyToAccount(privateKey as Hex); this.chain = chain; this.options = { entryPointVersion: '0.7', autoInitialize: true, ...options }; // Set paymaster if provided if (options.paymasterUrl) { this.paymasterConfig = { paymasterUrl: options.paymasterUrl }; } } // ============================================ // Core Methods // ============================================ /** * Initialize the smart wallet * Creates the Kernel account and sets up delegation if needed */ async initialize(): Promise { try { // Initialize AA service singleton with AA config this.aaService = AccountAbstractionService.getInstance({ bundlerProvider: 'pimlico', customBundlerUrl: this.options.bundlerUrl!, aaConfig: this.options.aaConfig }); // Create Kernel account this.kernelAccount = await this.aaService.createAccount({ chain: this.chain, owner: this.ownerAccount, entryPointVersion: this.options.entryPointVersion, aaConfig: this.options.aaConfig }); console.log(`Smart wallet initialized: ${this.kernelAccount.address}`); return this } catch (error) { throw new SmartWalletError( `Failed to initialize smart wallet: ${error instanceof Error ? error.message : 'Unknown error'}`, 'INIT_ERROR' ); } } /** * Get the smart account address */ getAddress(): Hex { if (!this.kernelAccount) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } return this.kernelAccount.address as Hex; } /** * Get smart account information */ async getAccountInfo(): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } const balance = await this.getBalance(); const isDelegated = await this.aaService.isAccountDelegated( this.ownerAccount, this.chain ); return { address: this.kernelAccount.address as Hex, ownerAddress: this.ownerAccount.address as Hex, chain: this.chain, entryPointVersion: this.options.entryPointVersion!, isDelegated, balance: BigInt(balance.balance.toString()) }; } /** * Get smart account balance */ async getBalance(): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } const balance = await this.aaService.getBalance(this.kernelAccount); return { balance: balance as any, decimal: 18, formatted: Number(balance) / 1e18 }; } /** * Check if account is delegated */ async isAccountDelegated(): Promise { if (!this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } return await this.aaService.isAccountDelegated(this.ownerAccount, this.chain); } // ============================================ // Transaction Methods // ============================================ /** * Send a single transaction via UserOperation * * @param to - Recipient address * @param value - ETH value in wei * @param data - Optional calldata * @returns Transaction result with UserOp hash * * @example * await smartWallet.sendTransaction( * '0xRecipient', * parseEther('0.1'), * '0x' * ); */ async sendTransaction( to: Hex, value: bigint = 0n, data: Hex = '0x', options?: { sessionAccount: Omit } ): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { // Use session account if available and requested if (options?.sessionAccount) { // Session keys use kernel account client, not bundler client const { createSessionKeyClient } = await import('./aa-service/lib/session-keys'); const kernelClient = createSessionKeyClient({ account: options.sessionAccount.account, chain: this.chain, bundlerUrl: this.options.bundlerUrl!, }); // Use kernel client to send transaction const userOpHash = await kernelClient.sendUserOperation({ callData: await options.sessionAccount.account.encodeCalls([{ to, value, data }]) }); // Wait for receipt const receipt = await kernelClient.waitForUserOperationReceipt({ hash: userOpHash }); return { success: true, userOpHash, transactionHash: receipt.receipt.transactionHash }; } else { // Use regular kernel account with AA service const userOpHash = await this.aaService.sendTransaction({ account: this.kernelAccount, to, value, data }); // Wait for receipt const receipt = await this.aaService.waitForReceipt({ userOpHash, chain: this.chain }); return { success: true, userOpHash, transactionHash: receipt.transactionHash }; } } catch (error) { console.log('error: ', error); const errorMsg = error instanceof Error ? error.message : 'Unknown error'; return { success: false, userOpHash: '0x' as Hex, error: errorMsg }; } } /** * Send multiple transactions in a single UserOperation * This is one of the main advantages of smart accounts - pay gas ONLY ONCE! * * @param calls - Array of calls to execute * @returns Transaction result * * @example * await smartWallet.sendBatchTransaction([ * { to: recipient1, value: parseEther('0.1'), data: '0x' }, * { to: recipient2, value: parseEther('0.2'), data: '0x' }, * { to: usdcAddress, value: 0n, data: transferCalldata } * ]); */ async sendBatchTransaction(calls: Call[], options?: { sessionAccount: KernelAccountInstance }): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } if (calls.length === 0) { throw new TransactionError('Batch transaction must have at least one call'); } try { // Convert to proper Call format const normalizedCalls = calls.map(call => ({ to: call.to, value: call.value ?? 0n, data: call.data ?? '0x' as Hex })); // Use session account if available if (options?.sessionAccount) { // Session keys use kernel account client const { createSessionKeyClient } = await import('./aa-service/lib/session-keys'); const kernelClient = createSessionKeyClient({ account: options.sessionAccount.account, chain: this.chain, bundlerUrl: this.options.bundlerUrl!, entryPoint: options.sessionAccount.entryPoint }); // Use kernel client to send batch transaction const userOpHash = await kernelClient.sendUserOperation({ callData: await options.sessionAccount.account.encodeCalls(normalizedCalls) }); // Wait for receipt const receipt = await kernelClient.waitForUserOperationReceipt({ hash: userOpHash }); return { success: true, userOpHash, transactionHash: receipt.receipt.transactionHash }; } else { // Use regular kernel account with AA service const aaCalls: AACall[] = normalizedCalls.map(call => ({ to: call.to, value: call.value, data: call.data })); const userOpHash = await this.aaService.sendBatchTransaction({ account: this.kernelAccount, calls: aaCalls }); // Wait for receipt const receipt = await this.aaService.waitForReceipt({ userOpHash, chain: this.chain }); return { success: true, userOpHash, transactionHash: receipt.transactionHash }; } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; return { success: false, userOpHash: '0x' as Hex, error: errorMsg }; } } /** * Prepare a call for batching * Helper method to create Call objects */ prepareCall(to: Hex, value: bigint = 0n, data: Hex = '0x'): Call { return { to, value, data }; } // ============================================ // Session Key Methods // ============================================ /** * Generate a new session key * The private key should be stored securely by the agent * * @returns Session key info with private key, address, and signer * * @example * const sessionKey = await smartWallet.generateSessionKey(); * console.log('Address:', sessionKey.address); * // Store sessionKey.privateKey securely */ async generateSessionKey(): Promise { if (!this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { return await this.aaService.generateSessionKey(); } catch (error) { throw new SessionKeyError( `Failed to generate session key: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Create session key approval (Owner side) * Owner approves a session key with specific permissions * * @param options - Approval options with session key address and permissions * @returns Serialized approval string to share with agent * * @example * const approval = await smartWallet.approveSessionKey({ * sessionKeyAddress: '0x...', * permissions: [ * smartWallet.createUSDCPermission(USDC_ADDRESS, '100') * ] * }); */ async approveSessionKey(options: SessionKeyApprovalOptions): Promise { if (!this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { return await this.aaService.createSessionKeyApproval({ sessionKeyAddress: options.sessionKeyAddress, sessionKeyPrivateKey: options.sessionKeyPrivateKey, owner: this.ownerAccount, chain: this.chain, entryPointVersion: this.options.entryPointVersion, useSudoPolicy: options.useSudoPolicy, permissions: options.permissions }); } catch (error) { throw new SessionKeyError( `Failed to approve session key: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Create USDC transfer permission * * @param usdcAddress - USDC contract address * @param maxAmount - Maximum USDC amount (in USDC units, e.g., "10" for 10 USDC) * @returns Permission rule */ createUSDCPermission(usdcAddress: Hex, maxAmount: string, destinationAddress: Hex): SessionKeyPermissionRule { if (!this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } return this.aaService.createUSDCTransferPermission(usdcAddress, maxAmount, destinationAddress); } /** * Create ETH transfer permission * * @param maxValue - Maximum ETH value (in ether units, e.g., "0.1" for 0.1 ETH) * @returns Permission rule */ createETHPermission(maxValue: string): SessionKeyPermissionRule { if (!this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } return this.aaService.createETHTransferPermission(maxValue); } // ============================================ // Module Management Methods // ============================================ /** * Install a module on the smart account * * @param options - Module installation options * @returns Transaction result * * @example * await smartWallet.installModule({ * moduleType: 'validator', * moduleAddress: '0x...', * initData: '0x...' * }); */ async installModule(options: ModuleInstallOptions): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { const userOpHash = await this.aaService.installModule({ account: this.kernelAccount, moduleType: options.moduleType, moduleAddress: options.moduleAddress, initData: options.initData }); const receipt = await this.aaService.waitForReceipt({ userOpHash, chain: this.chain }); return { success: true, userOpHash, transactionHash: receipt.transactionHash }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; throw new ModuleError(`Failed to install module: ${errorMsg}`); } } /** * Uninstall a module from the smart account * * @param options - Module uninstallation options * @returns Transaction result */ async uninstallModule(options: ModuleUninstallOptions): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { const userOpHash = await this.aaService.uninstallModule({ account: this.kernelAccount, moduleType: options.moduleType, moduleAddress: options.moduleAddress, deInitData: options.deInitData }); const receipt = await this.aaService.waitForReceipt({ userOpHash, chain: this.chain }); return { success: true, userOpHash, transactionHash: receipt.transactionHash }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; throw new ModuleError(`Failed to uninstall module: ${errorMsg}`); } } /** * Check if a module is installed * * @param moduleType - Type of module * @param moduleAddress - Module contract address * @returns Whether the module is installed */ async isModuleInstalled(moduleType: ModuleType, moduleAddress: Hex): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { return await this.aaService.isModuleInstalled({ account: this.kernelAccount, moduleType, moduleAddress }); } catch (error) { return false; } } /** * Prepare module installation call (for batching) * * @param options - Module installation options * @returns Call object for batch transaction */ prepareInstallModule(options: ModuleInstallOptions): Call { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } const aaCall = this.aaService.prepareInstallModule({ account: this.kernelAccount, moduleType: options.moduleType, moduleAddress: options.moduleAddress, initData: options.initData }); return { to: aaCall.to, value: aaCall.value, data: aaCall.data }; } /** * Prepare module uninstallation call (for batching) * * @param options - Module uninstallation options * @returns Call object for batch transaction */ prepareUninstallModule(options: ModuleUninstallOptions): Call { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } const aaCall = this.aaService.prepareUninstallModule({ account: this.kernelAccount, moduleType: options.moduleType, moduleAddress: options.moduleAddress, deInitData: options.deInitData }); return { to: aaCall.to, value: aaCall.value, data: aaCall.data }; } // ============================================ // Advanced Features // ============================================ /** * Enable multi-signature validation * * @param config - Multi-sig configuration with owners and threshold * @returns Transaction result * * @example * await smartWallet.enableMultiSig({ * owners: [owner1, owner2, owner3], * threshold: 2 // 2 of 3 required * }); */ async enableMultiSig(config: MultiSigConfig): Promise { if (!this.kernelAccount || !this.aaService) { throw new SmartWalletError('Smart wallet not initialized', 'NOT_INITIALIZED'); } try { const userOpHash = await this.aaService.installMultiSigValidator({ account: this.kernelAccount, owners: config.owners, threshold: config.threshold }); const receipt = await this.aaService.waitForReceipt({ userOpHash, chain: this.chain }); return { success: true, userOpHash, transactionHash: receipt.transactionHash }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; throw new SmartWalletError(`Failed to enable multi-sig: ${errorMsg}`, 'MULTISIG_ERROR'); } } /** * Set paymaster for gas sponsorship * * @param paymasterUrl - Paymaster service URL * * @example * smartWallet.setPaymaster('https://api.pimlico.io/v2/sepolia/paymaster'); */ setPaymaster(paymasterUrl: string, context?: any): void { this.paymasterConfig = { paymasterUrl, context }; } /** * Clear paymaster (user pays gas) */ clearPaymaster(): void { this.paymasterConfig = null; } /** * Check if paymaster is configured */ hasPaymaster(): boolean { return this.paymasterConfig !== null; } /** * Get bundler information */ getBundlerInfo(): { provider: string; url: string } { if (!this.aaService) { return { provider: 'custom', url: this.options.bundlerUrl || '' }; } return this.aaService.getBundlerInfo(this.chain); } }