/** * Bundler Manager * * Manages multiple bundler providers (Pimlico, Etherspot, custom) * and provides unified interface for bundler operations. */ import { Chain, createPublicClient, http, publicActions, walletActions } from 'viem'; import { BundlerClient, createBundlerClient, entryPoint07Address, entryPoint08Address } from 'viem/account-abstraction'; import { createPimlicoClient } from 'permissionless/clients/pimlico'; // ============================================ // Types // ============================================ export type BundlerProvider = 'pimlico' | 'etherspot' | 'custom'; import type { AA_SupportConfig } from '../lib/type'; export interface BundlerConfig { provider: BundlerProvider; apiKey?: string; customUrl?: string; chain?: Chain; aaConfig?: AA_SupportConfig; } export interface BundlerService { getClient(chain: Chain): BundlerClient; getBundlerUrl(chain: Chain): string; getProvider(): string; } // ============================================ // Pimlico Bundler // ============================================ class PimlicoBundler implements BundlerService { private apiKey: string; private clientCache: Map = new Map(); private url: string; private aaConfig?: AA_SupportConfig; constructor(apiKey: string, url: string, aaConfig?: AA_SupportConfig) { this.url = url; this.apiKey = apiKey; this.aaConfig = aaConfig; } getBundlerUrl(chain: Chain): string { if (!this.apiKey) { return this.url } return `https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${this.apiKey}`; } getClient(chain: Chain): BundlerClient { if (this.clientCache.has(chain.id)) { return this.clientCache.get(chain.id); } const bundlerUrl = this.getBundlerUrl(chain); // Get entryPoint from aaConfig or fallback to default let entryPointAddress: `0x${string}`; let entryPointVersion: '0.6' | '0.7' = '0.7'; if (this.aaConfig && this.aaConfig.entryPoints.length > 0) { // Use first entry point from config (could be enhanced to select by version) const ep = this.aaConfig.entryPoints[0]; entryPointAddress = ep.address as `0x${string}`; entryPointVersion = ep.version; } else { // Fallback to hardcoded default entryPointAddress = entryPoint07Address; entryPointVersion = '0.7'; } // Create Pimlico client for Pimlico-specific features (gas estimation) const entryPoint = { address: entryPointAddress, version: entryPointVersion }; const paymasterClient = createPimlicoClient({ transport: http(bundlerUrl), entryPoint }); // Create bundler client with Pimlico gas estimation const client = createBundlerClient({ transport: http(bundlerUrl), chain, userOperation: { estimateFeesPerGas: async () => (await paymasterClient.getUserOperationGasPrice()).fast, }, // paymaster: paymasterClient, }); this.clientCache.set(chain.id, client); return client; } getProvider(): string { return 'pimlico'; } clearCache(): void { this.clientCache.clear(); } } // // ============================================ // // Etherspot Bundler // // ============================================ // class EtherspotBundler implements BundlerService { // private clientCache: Map = new Map(); // getBundlerUrl(chain: Chain): string { // return getFreeBundlerUrl(chain.id); // } // getClient(chain: Chain): any { // if (this.clientCache.has(chain.id)) { // return this.clientCache.get(chain.id); // } // const client = createFreeBundler({ // chain, // bundlerUrl: this.getBundlerUrl(chain) // }).extend(publicActions).extend(walletActions); // this.clientCache.set(chain.id, client); // return client; // } // getProvider(): string { // return 'etherspot'; // } // clearCache(): void { // this.clientCache.clear(); // } // } // ============================================ // Custom Bundler // ============================================ class CustomBundler implements BundlerService { private bundlerUrl: string; private clientCache: Map = new Map(); constructor(bundlerUrl: string) { if (!bundlerUrl) { throw new Error('Custom bundler URL is required'); } this.bundlerUrl = bundlerUrl; } getBundlerUrl(chain: Chain): string { return this.bundlerUrl; } getClient(chain: Chain): any { if (this.clientCache.has(chain.id)) { return this.clientCache.get(chain.id); } const client = createPublicClient({ chain, transport: http(this.bundlerUrl) }).extend(publicActions).extend(walletActions); this.clientCache.set(chain.id, client); return client; } getProvider(): string { return 'custom'; } clearCache(): void { this.clientCache.clear(); } } // ============================================ // Bundler Manager // ============================================ export class BundlerManager { private bundlerService: BundlerService; constructor(config: BundlerConfig) { switch (config.provider) { case 'pimlico': this.bundlerService = new PimlicoBundler( config.apiKey ? config.apiKey : '', config.customUrl ? config.customUrl : '', config.aaConfig ); break; // case 'etherspot': // this.bundlerService = new EtherspotBundler(); // break; case 'custom': this.bundlerService = new CustomBundler(config.customUrl!); break; default: throw new Error(`Unsupported bundler provider: ${config.provider}`); } } getClient(chain: Chain): any { return this.bundlerService.getClient(chain); } getBundlerUrl(chain: Chain): string { return this.bundlerService.getBundlerUrl(chain); } getProvider(): string { return this.bundlerService.getProvider(); } clearCache(): void { if ('clearCache' in this.bundlerService) { (this.bundlerService as any).clearCache(); } } } // ============================================ // Factory Functions // ============================================ export function createBundlerManager(config: BundlerConfig): BundlerManager { return new BundlerManager(config); } export function createBundlerService(config: BundlerConfig): BundlerService { const manager = new BundlerManager(config); return { getClient: (chain?: Chain) => { const targetChain = chain ?? config.chain; if (!targetChain) { throw new Error('Chain must be provided either in config or as parameter'); } return manager.getClient(targetChain); }, getBundlerUrl: (chain?: Chain) => { const targetChain = chain ?? config.chain; if (!targetChain) { throw new Error('Chain must be provided either in config or as parameter'); } return manager.getBundlerUrl(targetChain); }, getProvider: () => manager.getProvider() }; }