Untitled

 avatar
unknown
plain_text
15 days ago
10 kB
6
Indexable
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