/** * Pimlico ERC-4337 bundler client — wraps Pimlico's JSON-RPC v2 API. * * Provides gas estimation, paymaster sponsorship, UserOperation submission, * and receipt polling for Account Abstraction transactions. * * Docs: https://docs.pimlico.io/ */ const PIMLICO_BASE = 'https://api.pimlico.io/v2'; export interface UserOperation { sender: string; nonce: string; initCode: string; callData: string; callGasLimit?: string; verificationGasLimit?: string; preVerificationGas?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; paymasterAndData?: string; signature?: string; } export interface GasEstimate { callGasLimit: string; verificationGasLimit: string; preVerificationGas: string; maxFeePerGas: string; maxPriorityFeePerGas: string; } export interface PaymasterResult { paymasterAndData: string; callGasLimit: string; verificationGasLimit: string; preVerificationGas: string; } export interface UserOperationReceipt { userOpHash: string; sender: string; nonce: string; actualGasCost: string; actualGasUsed: string; success: boolean; receipt: { transactionHash: string; blockNumber: string; status: string; }; } export class PimlicoClient { private apiKey: string; private chainId: number; constructor(opts: { apiKey: string; chainId: number }) { this.apiKey = opts.apiKey; this.chainId = opts.chainId; } private get endpoint(): string { return `${PIMLICO_BASE}/${this.chainId}/rpc?apikey=${this.apiKey}`; } private async rpc(method: string, params: any[]): Promise { const res = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(15000), }); if (!res.ok) { const text = await res.text(); throw new Error(`Pimlico RPC error (${res.status}): ${text}`); } const data = await res.json() as any; if (data.error) { throw new Error(`Pimlico ${method} error: ${data.error.message || JSON.stringify(data.error)}`); } return data.result; } async estimateGas(userOp: UserOperation, entryPoint: string): Promise { const result = await this.rpc('eth_estimateUserOperationGas', [userOp, entryPoint]); // Pimlico also returns fee data via pimlico_getUserOperationGasPrice const fees = await this.rpc('pimlico_getUserOperationGasPrice', []); return { callGasLimit: result.callGasLimit, verificationGasLimit: result.verificationGasLimit, preVerificationGas: result.preVerificationGas, maxFeePerGas: fees.fast.maxFeePerGas, maxPriorityFeePerGas: fees.fast.maxPriorityFeePerGas, }; } async getPaymasterData( userOp: UserOperation, entryPoint: string, opts?: { sponsorshipPolicyId?: string }, ): Promise { const params: any[] = [userOp, entryPoint, { type: 'payg' }]; if (opts?.sponsorshipPolicyId) { params[2] = { sponsorshipPolicyId: opts.sponsorshipPolicyId }; } return await this.rpc('pm_sponsorUserOperation', params); } async sendUserOperation(userOp: UserOperation, entryPoint: string): Promise { return await this.rpc('eth_sendUserOperation', [userOp, entryPoint]); } async getUserOperationReceipt(userOpHash: string): Promise { return await this.rpc('eth_getUserOperationReceipt', [userOpHash]); } /** * Full prepare flow: estimate gas → get paymaster sponsorship → return complete UserOp. */ async prepareUserOperation( userOp: UserOperation, entryPoint: string, opts?: { sponsorshipPolicyId?: string }, ): Promise { // 1. Estimate gas limits + fee data const gas = await this.estimateGas(userOp, entryPoint); const prepared: UserOperation = { ...userOp, callGasLimit: gas.callGasLimit, verificationGasLimit: gas.verificationGasLimit, preVerificationGas: gas.preVerificationGas, maxFeePerGas: gas.maxFeePerGas, maxPriorityFeePerGas: gas.maxPriorityFeePerGas, }; // 2. Get paymaster sponsorship data const pm = await this.getPaymasterData(prepared, entryPoint, opts); prepared.paymasterAndData = pm.paymasterAndData; // Paymaster may override gas limits if (pm.callGasLimit) prepared.callGasLimit = pm.callGasLimit; if (pm.verificationGasLimit) prepared.verificationGasLimit = pm.verificationGasLimit; if (pm.preVerificationGas) prepared.preVerificationGas = pm.preVerificationGas; return prepared; } }