import type { Plugin, PluginAPI } from "@lumeweb/interface-relay"; import fetch, { Request, RequestInit } from "node-fetch"; import NodeCache from "node-cache"; import stringify from "json-stringify-deterministic"; import { Client, Prover } from "./client/index.js"; import { MemoryStore } from "./client/memory-store.js"; const EXECUTION_RPC_URL = "https://g.w.lavanet.xyz:443/gateway/eth/rpc-http/f195d68175eb091ec1f71d00f8952b85"; const CONSENSUS_RPC_URL = "https://www.lightclientdata.org"; const RPC_CACHE = new NodeCache({ stdTTL: 60 * 60 * 12 }); async function doFetch(url: string, request: RequestInit) { sanitizeRequestArgs(url, request); let req = new Request(url, { ...request, headers: { "Content-Type": "application/json", }, }); const resp = await fetch(req); return (await resp.json()) as any; } function sanitizeRequestArgs(url: string, request: RequestInit) { if (!request || typeof request !== "object") { throw Error("invalid request"); } [ "agent", "hostname", "referrer", "referrerPolicy", "compress", "port", "protocol", "hostname", "insecureHTTPParser", "highWaterMark", "size", ].forEach((element) => { if (element in request) { delete request[element]; } }); } interface ExecutionRequest { method: string; params: any[]; } interface ConsensusCommitteeHashesRequest { start: number; count: number; } interface ConsensusCommitteePeriodRequest { period: number | "latest"; } interface ConsensusBlockRequest { block: number; } let client: Client; const plugin: Plugin = { name: "eth", async plugin(api: PluginAPI): Promise { const prover = new Prover(CONSENSUS_RPC_URL); const store = new MemoryStore(); client = new Client(prover, store, CONSENSUS_RPC_URL); await client.sync(); api.registerMethod("consensus_committee_hashes", { cacheable: false, async handler( request: ConsensusCommitteeHashesRequest ): Promise { if (!(request?.start && typeof request.start == "number")) { throw new Error('start required and must be a number"'); } if (!(request?.count && typeof request.count == "number")) { throw new Error('count required and must be a number"'); } if (!client.isSynced) { await client.sync(); } let hashes; try { hashes = store.getCommitteeHashes(request.start, request.count); } catch { await client.sync(); } if (!hashes) { try { hashes = store.getCommitteeHashes(request.start, request.count); } catch (e) { return e; } } return hashes; }, }); api.registerMethod("consensus_committee_period", { cacheable: false, async handler( request: ConsensusCommitteePeriodRequest ): Promise { if ( !( request?.period && (typeof request.period == "number" || request.period === "latest") ) ) { throw new Error('period required and must be a number or "latest"'); } if (!client.isSynced) { await client.sync(); } let committee; try { committee = store.getCommittee( request.period === "latest" ? client.latestPeriod : request.period ); } catch { await client.sync(); } if (!committee) { try { committee = store.getCommittee( request.period === "latest" ? client.latestPeriod : request.period ); } catch (e) { return e; } } return committee; }, }); api.registerMethod("execution_request", { cacheable: false, async handler(request: ExecutionRequest): Promise { const tempRequest = { method: request.method, ...request.params, }; const hash = api.util.crypto .createHash(stringify(tempRequest)) .toString("hex"); if (RPC_CACHE.has(hash)) { RPC_CACHE.ttl(hash); return RPC_CACHE.get(hash); } try { let resp = await doFetch(EXECUTION_RPC_URL, { method: "POST", body: JSON.stringify(request), }); if (resp && resp.result) { RPC_CACHE.set(hash, resp); } return resp; } catch (e) { return e; } }, }); api.registerMethod("consensus_optimistic_update", { cacheable: false, async handler(): Promise { return await doFetch( `${CONSENSUS_RPC_URL}/eth/v1/beacon/light_client/optimistic_update`, { method: "GET", } ); }, }); api.registerMethod("consensus_block", { cacheable: false, async handler(request: ConsensusBlockRequest): Promise { try { BigInt(request?.block); } catch { throw new Error("block is required and must be a number"); } const block = request?.block; if (BigInt(block) > BigInt(client.latestPeriod)) { await client.sync(); } if (!client.blockHashCache.has(request.block)) { throw new Error("block not found"); } if (client.blockCache.has(request.block)) { client.blockCache.ttl(request.block); return client.blockCache.get(request.block); } await client.getExecutionFromBlockRoot( request.block as any, client.blockHashCache.get(request.block) ); return client.blockCache.get(request.block); }, }); }, }; export default plugin;