152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
/**
|
|
* 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<any> {
|
|
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<GasEstimate> {
|
|
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<PaymasterResult> {
|
|
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<string> {
|
|
return await this.rpc('eth_sendUserOperation', [userOp, entryPoint]);
|
|
}
|
|
|
|
async getUserOperationReceipt(userOpHash: string): Promise<UserOperationReceipt | null> {
|
|
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<UserOperation> {
|
|
// 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;
|
|
}
|
|
}
|