relay-plugin-eth/src/index.ts

240 lines
5.8 KiB
TypeScript

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<void> {
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<Uint8Array> {
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<Uint8Array> {
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<object> {
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<object> {
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<object> {
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;