add test button to sol push so you can mass test all RPC endpoints at once.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user