add test button to sol push so you can mass test all RPC endpoints at once.

This commit is contained in:
KingKDot
2026-07-14 10:36:13 -05:00
parent 73700177df
commit f23fe2ff56
5 changed files with 214 additions and 2 deletions
@@ -9,6 +9,7 @@ import {
normalizeSolRpcUrl,
saveSolRpcEndpointUrls,
} from "../../sol-rpc-endpoints";
import { testSolRpcEndpoint } from "../../sol-rpc-test";
let _solana: typeof import("@solana/web3.js") | null = null;
async function getSolana() {
@@ -73,6 +74,20 @@ export async function handleSolRoutes(
try { requirePermission(user, "system:configure"); }
catch (error) { return error instanceof Response ? error : new Response("Forbidden", { status: 403 }); }
if (req.method === "POST" && url.pathname === "/api/sol/rpc-endpoints/test") {
const records = getSolRpcEndpoints();
const results = await Promise.all(records.map(async (record) => ({
...record,
...await testSolRpcEndpoint(record.url),
})));
return Response.json({
tested: results.length,
passed: results.filter((result) => result.ok).length,
failed: results.filter((result) => !result.ok).length,
results,
});
}
if (req.method === "POST" && url.pathname === "/api/sol/rpc-endpoints") {
const body = await req.json();
const current = getSolRpcEndpoints().map((item) => item.url);
+62
View File
@@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test";
import { testSolRpcEndpoint } from "./sol-rpc-test";
const validBlockhash = "11111111111111111111111111111111";
describe("testSolRpcEndpoint", () => {
test("accepts a valid latest blockhash response", async () => {
const fetchImpl = async () => Response.json({
jsonrpc: "2.0",
id: 1,
result: {
context: { slot: 123456 },
value: { blockhash: validBlockhash, lastValidBlockHeight: 123600 },
},
});
const result = await testSolRpcEndpoint("https://rpc.example.com", 100, fetchImpl);
expect(result).toMatchObject({
ok: true,
slot: 123456,
blockhash: validBlockhash,
lastValidBlockHeight: 123600,
});
});
test("rejects successful HTTP responses with invalid RPC values", async () => {
const fetchImpl = async () => Response.json({
jsonrpc: "2.0",
id: 1,
result: { context: { slot: "not-a-number" }, value: {} },
});
const result = await testSolRpcEndpoint("https://rpc.example.com", 100, fetchImpl);
expect(result.ok).toBe(false);
expect(result.error).toBe("Response contained an invalid slot");
});
test("reports HTTP and JSON-RPC errors", async () => {
const httpResult = await testSolRpcEndpoint(
"https://rpc.example.com",
100,
async () => new Response("unavailable", { status: 503 }),
);
expect(httpResult).toMatchObject({ ok: false, error: "HTTP 503" });
const rpcResult = await testSolRpcEndpoint(
"https://rpc.example.com",
100,
async () => Response.json({ error: { message: "rate limited" } }),
);
expect(rpcResult).toMatchObject({ ok: false, error: "rate limited" });
});
test("aborts endpoints that exceed the timeout", async () => {
const fetchImpl = (_input: string | URL | Request, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")));
});
const result = await testSolRpcEndpoint("https://rpc.example.com", 5, fetchImpl);
expect(result).toMatchObject({ ok: false, error: "Timed out after 5ms" });
});
});
+80
View File
@@ -0,0 +1,80 @@
const BASE58_VALUE = /^[1-9A-HJ-NP-Za-km-z]{32,64}$/;
export type SolRpcTestResult = {
ok: boolean;
latencyMs: number;
slot?: number;
blockhash?: string;
lastValidBlockHeight?: number;
error?: string;
};
type RpcFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
export async function testSolRpcEndpoint(
endpoint: string,
timeoutMs = 10_000,
fetchImpl: RpcFetch = fetch,
): Promise<SolRpcTestResult> {
const startedAt = performance.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getLatestBlockhash",
params: [{ commitment: "confirmed" }],
}),
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
}
let data: any;
try {
data = await response.json();
} catch {
throw new Error("Response was not valid JSON");
}
if (data?.error) {
const detail = typeof data.error.message === "string" ? data.error.message : "RPC returned an error";
throw new Error(detail);
}
const slot = data?.result?.context?.slot;
const blockhash = data?.result?.value?.blockhash;
const lastValidBlockHeight = data?.result?.value?.lastValidBlockHeight;
if (!Number.isSafeInteger(slot) || slot < 0) throw new Error("Response contained an invalid slot");
if (typeof blockhash !== "string" || !BASE58_VALUE.test(blockhash)) {
throw new Error("Response contained an invalid blockhash");
}
if (!Number.isSafeInteger(lastValidBlockHeight) || lastValidBlockHeight < 0) {
throw new Error("Response contained an invalid last valid block height");
}
return {
ok: true,
latencyMs: Math.round(performance.now() - startedAt),
slot,
blockhash,
lastValidBlockHeight,
};
} catch (error: any) {
const timedOut = controller.signal.aborted;
return {
ok: false,
latencyMs: Math.round(performance.now() - startedAt),
error: timedOut ? `Timed out after ${timeoutMs}ms` : (error?.message || "RPC request failed"),
};
} finally {
clearTimeout(timeout);
}
}