rspace-online/modules/rflows/lib/pimlico.ts

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;
}
}