near-safe-test
Version:
An SDK for controlling Ethereum Smart Accounts via ERC4337 from a Near Account.
109 lines (108 loc) • 4.17 kB
JavaScript
import { createPublicClient, http, rpcSchema, RpcError, HttpRequestError, } from "viem";
import { PLACEHOLDER_SIG } from "../util";
function bundlerUrl(chainId, apikey) {
return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${apikey}`;
}
export class Erc4337Bundler {
client;
entryPointAddress;
apiKey;
chainId;
constructor(entryPointAddress, apiKey, chainId) {
this.entryPointAddress = entryPointAddress;
this.apiKey = apiKey;
this.chainId = chainId;
this.client = createPublicClient({
transport: http(bundlerUrl(chainId, this.apiKey)),
rpcSchema: rpcSchema(),
});
}
async getPaymasterData(rawUserOp, sponsorshipPolicy) {
const userOp = { ...rawUserOp, signature: PLACEHOLDER_SIG };
if (sponsorshipPolicy) {
console.log("Requesting paymaster data...");
return handleRequest(() => this.client.request({
method: "pm_sponsorUserOperation",
params: [
userOp,
this.entryPointAddress,
{ sponsorshipPolicyId: sponsorshipPolicy },
],
}));
}
console.log("Estimating user operation gas...");
return handleRequest(() => this.client.request({
method: "eth_estimateUserOperationGas",
params: [userOp, this.entryPointAddress],
}));
}
async sendUserOperation(userOp) {
return handleRequest(() => this.client.request({
method: "eth_sendUserOperation",
params: [userOp, this.entryPointAddress],
}));
// throw new Error(`Failed to send user op with: ${error.message}`);
}
async getGasPrice() {
return handleRequest(() => this.client.request({
method: "pimlico_getUserOperationGasPrice",
params: [],
}));
}
async getUserOpReceipt(userOpHash) {
let userOpReceipt = null;
while (!userOpReceipt) {
// Wait 2 seconds before checking the status again
await new Promise((resolve) => setTimeout(resolve, 2000));
userOpReceipt = await this._getUserOpReceiptInner(userOpHash);
}
return userOpReceipt;
}
async _getUserOpReceiptInner(userOpHash) {
return handleRequest(() => this.client.request({
method: "eth_getUserOperationReceipt",
params: [userOpHash],
}));
}
// New method to query sponsorship policies
async getSponsorshipPolicies() {
const url = `https://api.pimlico.io/v2/account/sponsorship_policies?apikey=${this.apiKey}`;
const allPolocies = await handleRequest(async () => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}: ${response.statusText}`);
}
return response.json();
});
return allPolocies.data.filter((p) => p.chain_ids.allowlist.includes(this.chainId));
}
}
async function handleRequest(clientMethod) {
try {
return await clientMethod();
}
catch (error) {
const message = stripApiKey(error);
if (error instanceof HttpRequestError) {
if (error.status === 401) {
throw new Error("Unauthorized request. Please check your Pimlico API key.");
}
else {
throw new Error(`Pimlico: ${message}`);
}
}
else if (error instanceof RpcError) {
throw new Error(`Failed to send user op with: ${message}`);
}
throw new Error(`Bundler Request: ${message}`);
}
}
export function stripApiKey(error) {
const message = error instanceof Error ? error.message : String(error);
return message.replace(/(apikey=)[^\s&]+/, "$1***");
// Could also do this with slicing.
// const keyStart = message.indexOf("apikey=") + 7;
// // If no apikey in the message, return it as is.
// if (keyStart === -1) return message;
// return `${message.slice(0, keyStart)}***${message.slice(keyStart + 36)}`;
}