Untitled
import "dotenv/config"; import fastify, { FastifyInstance } from "fastify"; import cors from "@fastify/cors"; import fs from "fs/promises"; import { Address, createPublicClient, decodeEventLog, encodeEventTopics, getAbiItem, Hex, http, parseAbi, parseEventLogs, toHex, TransactionReceipt, TransactionReceiptNotFoundError, zeroAddress, } from "viem"; import { entryPoint06Abi, entryPoint06Address, entryPoint07Abi, entryPoint07Address, } from "viem/account-abstraction"; import { base } from "viem/chains"; // Define types for JSON-RPC interface JsonRpcRequest { jsonrpc: "2.0"; method: string; params: any; id: number | string; } interface JsonRpcResponse { jsonrpc: "2.0"; result?: any; error?: { code: number; message: string; }; id: number | string; } // Create fastify instance const server: FastifyInstance = fastify({ logger: true }); // Register CORS server.register(cors, { origin: true, }); // JSON-RPC endpoint server.post<{ Body: JsonRpcRequest; }>("/", async (request, reply) => { const { method, params, id } = request.body; // Handle different methods switch (method) { case "echo": const response: JsonRpcResponse = { jsonrpc: "2.0", result: params, id, }; return response; case "eth_getUserOperationReceipt": // @ts-ignore const { provider } = request.query; console.log(provider); const receipt = await getUserOperationReceipt(params, provider); return receipt; default: const errorResponse: JsonRpcResponse = { jsonrpc: "2.0", error: { code: -32601, message: "Method not found", }, id, }; return errorResponse; } }); async function getUserOperationReceipt( params: any, provider: string ): Promise<JsonRpcResponse> { const startTime = performance.now(); let rpcUrl: string; switch (provider) { case "tenderly": rpcUrl = process.env.TENDERLY_RPC_URL ?? ""; break; case "alchemy": rpcUrl = process.env.ALCHEMY_RPC_URL ?? ""; break; case "ankr": rpcUrl = process.env.ANKR_RPC_URL ?? ""; break; default: rpcUrl = process.env.TENDERLY_RPC_URL ?? ""; } console.log(rpcUrl); const userOperationHash = params[0]; const userOperationEventAbiItem = getAbiItem({ abi: entryPoint07Abi, name: "UserOperationEvent", }); let fromBlock: bigint | undefined = undefined; let toBlock: "latest" | undefined = undefined; const maxBlockRange = 500; const publicClient = createPublicClient({ chain: base, transport: http(rpcUrl), }); if (maxBlockRange !== undefined) { const latestBlock = await publicClient.getBlockNumber(); fromBlock = latestBlock - BigInt(maxBlockRange); if (fromBlock < 0n) { fromBlock = 0n; } fromBlock = 0n; toBlock = "latest"; } const result = await publicClient.getLogs({ address: [entryPoint06Address, entryPoint07Address], event: userOperationEventAbiItem, fromBlock, toBlock, args: { userOpHash: userOperationHash, }, }); if (result.length === 0) { return { jsonrpc: "2.0", result: null, id: 1, }; } const endTime = performance.now(); const duration = endTime - startTime; // Log performance data only for non-null results const perfData = { timestamp: new Date().toISOString(), provider, userOperationHash, duration_ms: duration, success: true, }; await logPerformance(perfData); const userOperationEvent = result[0]; // throw if any of the members of userOperationEvent are undefined if ( userOperationEvent.args.actualGasCost === undefined || userOperationEvent.args.sender === undefined || userOperationEvent.args.nonce === undefined || userOperationEvent.args.userOpHash === undefined || userOperationEvent.args.success === undefined || userOperationEvent.args.paymaster === undefined || userOperationEvent.args.actualGasUsed === undefined ) { throw new Error("userOperationEvent has undefined members"); } const txHash = userOperationEvent.transactionHash; if (txHash === null) { // transaction pending return { jsonrpc: "2.0", result: null, id: 1, }; } const getTransactionReceipt = async ( txHash: Hex ): Promise<TransactionReceipt> => { while (true) { try { const transactionReceipt = await publicClient.getTransactionReceipt({ hash: txHash, }); let effectiveGasPrice: bigint | undefined = transactionReceipt.effectiveGasPrice ?? (transactionReceipt as any).gasPrice ?? undefined; if (effectiveGasPrice === undefined) { const tx = await publicClient.getTransaction({ hash: txHash, }); effectiveGasPrice = tx.gasPrice ?? undefined; } if (effectiveGasPrice) { transactionReceipt.effectiveGasPrice = effectiveGasPrice; } return transactionReceipt; } catch (e) { if (e instanceof TransactionReceiptNotFoundError) { continue; } throw e; } } }; const receipt = await getTransactionReceipt(txHash); const logs = receipt.logs; if ( logs.some( (log) => log.blockHash === null || log.blockNumber === null || log.transactionIndex === null || log.transactionHash === null || log.logIndex === null || log.topics.length === 0 ) ) { // transaction pending return { jsonrpc: "2.0", result: null, id: 1, }; } const userOperationReceipt = parseUserOperationReceipt( userOperationHash, receipt ); return { jsonrpc: "2.0", result: userOperationReceipt, id: 1, }; } async function logPerformance(data: any) { const logFile = 'performance_logs.json'; let logs = []; try { // Check if file exists try { await fs.access(logFile); const existingData = await fs.readFile(logFile, 'utf8'); logs = JSON.parse(existingData); } catch { // File doesn't exist, create it with empty array await fs.writeFile(logFile, JSON.stringify([], null, 2)); } logs.push(data); await fs.writeFile(logFile, JSON.stringify(logs, null, 2)); } catch (error) { console.error('Failed to log performance data:', error); } } export function parseUserOperationReceipt( userOpHash: Hex, receipt: TransactionReceipt ) { const userOperationRevertReasonAbi = parseAbi([ "event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)", ]); const userOperationEventTopic = encodeEventTopics({ abi: entryPoint06Abi, eventName: "UserOperationEvent", }); const userOperationRevertReasonTopicEvent = encodeEventTopics({ abi: userOperationRevertReasonAbi, })[0]; let entryPoint: Address = zeroAddress; let revertReason = undefined; let startIndex = -1; let endIndex = -1; receipt.logs.forEach((log, index) => { if (log?.topics[0] === userOperationEventTopic[0]) { // process UserOperationEvent if (log.topics[1] === userOpHash) { // it's our userOpHash. save as end of logs array endIndex = index; entryPoint = log.address; } else if (endIndex === -1) { // it's a different hash. remember it as beginning index, but only if we didn't find our end index yet. startIndex = index; } } if (log?.topics[0] === userOperationRevertReasonTopicEvent) { // process UserOperationRevertReason if (log.topics[1] === userOpHash) { // it's our userOpHash. capture revert reason. const decodedLog = decodeEventLog({ abi: userOperationRevertReasonAbi, data: log.data, topics: log.topics, }); revertReason = decodedLog.args.revertReason; } } }); if (endIndex === -1) { throw new Error("fatal: no UserOperationEvent in logs"); } const filteredLogs = receipt.logs.slice(startIndex + 1, endIndex); const userOperationEvent = parseEventLogs({ abi: entryPoint06Abi, eventName: "UserOperationEvent", args: { userOpHash, }, logs: receipt.logs, })[0]; let paymaster: Address | undefined = userOperationEvent.args.paymaster; paymaster = paymaster === zeroAddress ? undefined : paymaster; const userOperationReceipt = { userOpHash, entryPoint, sender: userOperationEvent.args.sender, nonce: userOperationEvent.args.nonce, paymaster, actualGasUsed: userOperationEvent.args.actualGasUsed, actualGasCost: userOperationEvent.args.actualGasCost, success: userOperationEvent.args.success, reason: revertReason, logs: filteredLogs, receipt: { ...receipt, status: receipt.status === "success" ? 1 : 0, }, }; return deepHexlify(userOperationReceipt); } // biome-ignore lint/suspicious/noExplicitAny: it's a generic type export function deepHexlify(obj: any): any { if (typeof obj === "function") { return undefined } if (obj == null || typeof obj === "string" || typeof obj === "boolean") { return obj } if (typeof obj === "bigint") { return toHex(obj) } if (obj._isBigNumber != null || typeof obj !== "object") { return toHex(obj).replace(/^0x0/, "0x") } if (Array.isArray(obj)) { return obj.map((member) => deepHexlify(member)) } return Object.keys(obj).reduce( // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type (set: any, key: string) => { set[key] = deepHexlify(obj[key]) return set }, {} ) } // Start server const start = async (): Promise<void> => { try { await server.listen({ port: 3000, host: "0.0.0.0" }); } catch (err) { server.log.error(err); process.exit(1); } }; start();
Leave a Comment