diff --git a/src/components/speedtest/speedtest-rpc-client.tsx b/src/components/speedtest/speedtest-rpc-client.tsx index 26c4db54..a2083917 100644 --- a/src/components/speedtest/speedtest-rpc-client.tsx +++ b/src/components/speedtest/speedtest-rpc-client.tsx @@ -14,9 +14,11 @@ */ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ProviderLogo } from "@/components/provider-logo"; import { FEATURED_CHAINS, RPC_DIRECTORY, + directoryEntryForUrl, providerForUrl, type DirectoryChain, } from "@/lib/speedtest/rpc-directory"; @@ -281,6 +283,9 @@ export function SpeedtestRpcClient() { const [inputs, setInputs] = useState(["", ""]); const [chainQuery, setChainQuery] = useState(""); const [pickedChain, setPickedChain] = useState(null); + const [prefillState, setPrefillState] = useState<"idle" | "validating" | "done">("idle"); + const [skippedProviders, setSkippedProviders] = useState([]); + const prefillEpoch = useRef(0); const [durationSec, setDurationSec] = useState(30); const [endpoints, setEndpoints] = useState([]); const [activeId, setActiveId] = useState(null); @@ -303,9 +308,34 @@ export function SpeedtestRpcClient() { }, [chainQuery]); const pickChain = (c: DirectoryChain) => { + // Fill immediately, then validate each endpoint from THIS browser + // (eth_chainId, short timeout) and silently drop the ones that + // reject browser requests — a prefilled endpoint must never show + // up as a dead "no CORS" tile. Manual URLs keep full feedback. + const epoch = ++prefillEpoch.current; setInputs(c.endpoints.map((e) => e.url)); setPickedChain(c.slug); setChainQuery(""); + setSkippedProviders([]); + setPrefillState("validating"); + void (async () => { + const checks = await Promise.all( + c.endpoints.map(async (e) => { + try { + const r = await rpcCall(e.url, "eth_chainId", []); + return { e, ok: r.verdict === "ok" || r.verdict === "http_err" }; + } catch { + return { e, ok: false }; // CORS / network-level rejection + } + }), + ); + if (prefillEpoch.current !== epoch) return; // user picked again + const good = checks.filter((x) => x.ok).map((x) => x.e.url); + const skipped = checks.filter((x) => !x.ok).map((x) => x.e.provider); + if (skipped.length > 0) setInputs(good.length > 0 ? good : [""]); + setSkippedProviders(skipped); + setPrefillState("done"); + })(); }; const validCount = inputs.filter((u) => { @@ -507,13 +537,14 @@ export function SpeedtestRpcClient() { key={slug} type="button" onClick={() => pickChain(c)} - className={`label-mono text-[11px] rounded-full px-3 py-1 border transition-colors ${ + className={`label-mono text-[11px] rounded-full pl-1.5 pr-3 py-1 border transition-colors inline-flex items-center gap-1.5 ${ activeChip ? "border-ink text-paper" : "border-rule text-ink-soft hover:border-ink/40 hover:text-ink" }`} style={activeChip ? { background: "var(--color-ink)", color: "var(--color-paper, #fff)" } : undefined} > + {c.name} ); @@ -535,9 +566,10 @@ export function SpeedtestRpcClient() {