diff --git a/components/RPCList/index.js b/components/RPCList/index.js index 20f9455b61..4c8fc98ea5 100644 --- a/components/RPCList/index.js +++ b/components/RPCList/index.js @@ -6,10 +6,81 @@ import useAddToNetwork from "../../hooks/useAddToNetwork"; import { useLlamaNodesRpcData } from "../../hooks/useLlamaNodesRpcData"; import { FATHOM_DROPDOWN_EVENTS_ID } from "../../hooks/useAnalytics"; import { useRpcStore } from "../../stores"; -import { renderProviderText } from "../../utils"; +import { renderProviderText, containsAny } from "../../utils"; import { Tooltip } from "../../components/Tooltip"; import useAccount from "../../hooks/useAccount"; import { Popover, PopoverDisclosure, usePopoverStore } from "@ariakit/react/popover"; +import { useQuery } from "@tanstack/react-query"; + +// Functions to test trace and archive support + +const SUPPORT_STATUS = { + supported: "supported", + not_supported: "not-supported", + error: "error", + testing: "testing", + unknown: "unknown", +}; + +const testTraceSupport = async (rpcUrl) => { + // Test trace support (with fake tx hash) + + const fakeTxHash = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + const tracePayload = { + jsonrpc: "2.0", + method: "debug_traceTransaction", + params: [fakeTxHash, {}], + id: 1, + }; + + try { + const traceRes = await fetch(rpcUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(tracePayload), + signal: AbortSignal.timeout(7000), + }).then((r) => r.json()); + + const errorMessage = traceRes?.error?.message || traceRes?.message; + + // some RPCs might allow the trace with restrictions, better marks those as 'failed to test' instead of 'not supported' + if (containsAny(errorMessage, ["auth", "rate", "limit", "api", "allowed", "whitelist", "origin"])) { + throw new Error(errorMessage); + } + + const traceSupported = + containsAny(errorMessage, ["transaction not found", `transaction ${fakeTxHash} not found`]) || + traceRes?.result === null; + + return traceSupported ? SUPPORT_STATUS.supported : SUPPORT_STATUS.not_supported; + } catch (error) { + return SUPPORT_STATUS.error; + } +}; + +const testArchiveSupport = async (rpcUrl) => { + // Test archive support + const archivePayload = { + jsonrpc: "2.0", + method: "eth_getBalance", + params: ["0x0000000000000000000000000000000000000000", "0x1"], + id: 1, + }; + + try { + const archiveRes = await fetch(rpcUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(archivePayload), + signal: AbortSignal.timeout(7000), + }).then((r) => r.json()); + + const archiveSupported = Boolean(archiveRes.result); + return archiveSupported ? SUPPORT_STATUS.supported : SUPPORT_STATUS.not_supported; + } catch (error) { + return SUPPORT_STATUS.error; + } +}; export default function RPCList({ chain, lang }) { const [sortChains, setSorting] = useState(true); @@ -72,7 +143,13 @@ export default function RPCList({ chain, lang }) { return { ...rest, - data: { ...data, height, latency: lat, trust, disableConnect }, + data: { + ...data, + height, + latency: lat, + trust, + disableConnect, + }, }; }); }, [chains]); @@ -113,6 +190,8 @@ export default function RPCList({ chain, lang }) { Latency Score Privacy + Trace + Archive @@ -163,6 +242,23 @@ function PrivacyIcon({ tracking, isOpenSource = false }) { return ; } +function SupportIcon({ support }) { + switch (support) { + case SUPPORT_STATUS.supported: + return ; + case SUPPORT_STATUS.not_supported: + return ; + case SUPPORT_STATUS.error: + return ; + case SUPPORT_STATUS.testing: + return ( +
+ ); + default: + return ; + } +} + const Row = ({ values, chain, privacy, lang, className }) => { const t = useTranslations("Common", lang); const { data, isLoading, refetch } = values; @@ -184,6 +280,43 @@ const Row = ({ values, chain, privacy, lang, className }) => { const { mutate: addToNetwork } = useAddToNetwork(); + const traceSupport = useQuery({ + queryKey: ["trace-support", data?.url], + queryFn: () => testTraceSupport(data?.url), + staleTime: 1000 * 60 * 60, + refetchInterval: 1000 * 60 * 60, + refetchOnMount: false, + refetchOnWindowFocus: false, + enabled: !!data?.url, + }); + + const archiveSupport = useQuery({ + queryKey: ["archive-support", data?.url], + queryFn: () => testArchiveSupport(data?.url), + staleTime: 1000 * 60 * 60, + refetchInterval: 1000 * 60 * 60, + refetchOnMount: false, + refetchOnWindowFocus: false, + enabled: !!data?.url, + }); + + const getSupportTooltipContent = (support, type) => { + switch (support) { + case SUPPORT_STATUS.supported: + return `${type} methods are supported`; + case SUPPORT_STATUS.not_supported: + return `${type} methods are not supported`; + case SUPPORT_STATUS.error: + return `Error testing ${type} support`; + case SUPPORT_STATUS.testing: + return `Testing ${type} support...`; + case SUPPORT_STATUS.unknown: + return `${type} support unknown`; + default: + return `${type} support unknown`; + } + }; + return ( @@ -211,6 +344,40 @@ const Row = ({ values, chain, privacy, lang, className }) => { {isLoading ? : } + + + {isLoading ? ( + + ) : ( + + )} + + + + + {isLoading ? ( + + ) : ( + + )} + + {isLoading ? ( diff --git a/utils/index.js b/utils/index.js index 4e2213fd39..77e7827bdd 100644 --- a/utils/index.js +++ b/utils/index.js @@ -94,3 +94,8 @@ export const notTranslation = return en[ns][key]; } }; + +export const containsAny = (str, substrings) => { + if (!str) return false; + return substrings.some((substring) => str.toLowerCase().includes(substring.toLowerCase())); +};