Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 169 additions & 2 deletions components/RPCList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -113,6 +190,8 @@ export default function RPCList({ chain, lang }) {
<th className="px-3 py-1 font-medium border">Latency</th>
<th className="px-3 py-1 font-medium border">Score</th>
<th className="px-3 py-1 font-medium border">Privacy</th>
<th className="px-3 py-1 font-medium border">Trace</th>
<th className="px-3 py-1 font-medium border">Archive</th>
<th className="px-3 py-1 font-medium border"></th>
</tr>
</thead>
Expand Down Expand Up @@ -163,6 +242,23 @@ function PrivacyIcon({ tracking, isOpenSource = false }) {
return <EmptyIcon />;
}

function SupportIcon({ support }) {
switch (support) {
case SUPPORT_STATUS.supported:
return <GreenIcon />;
case SUPPORT_STATUS.not_supported:
return <RedIcon />;
case SUPPORT_STATUS.error:
return <OrangeIcon />;
case SUPPORT_STATUS.testing:
return (
<div className="w-4 h-4 mx-auto animate-spin rounded-full border-2 border-gray-300 border-t-blue-600"></div>
);
default:
return <EmptyIcon />;
}
}

const Row = ({ values, chain, privacy, lang, className }) => {
const t = useTranslations("Common", lang);
const { data, isLoading, refetch } = values;
Expand All @@ -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 (
<tr className={className}>
<td className="border px-3 py-1 max-w-[40ch]">
Expand Down Expand Up @@ -211,6 +344,40 @@ const Row = ({ values, chain, privacy, lang, className }) => {
{isLoading ? <Shimmer /> : <PrivacyIcon tracking={privacy?.tracking} isOpenSource={privacy?.isOpenSource} />}
</Tooltip>
</td>
<td className="px-3 py-1 text-sm border">
<Tooltip
content={getSupportTooltipContent(
traceSupport.isLoading ? SUPPORT_STATUS.testing : traceSupport?.data ?? SUPPORT_STATUS.unknown,
"Trace",
)}
>
{isLoading ? (
<Shimmer />
) : (
<SupportIcon
support={traceSupport.isLoading ? SUPPORT_STATUS.testing : traceSupport?.data ?? SUPPORT_STATUS.unknown}
/>
)}
</Tooltip>
</td>
<td className="px-3 py-1 text-sm border">
<Tooltip
content={getSupportTooltipContent(
archiveSupport.isLoading ? SUPPORT_STATUS.testing : archiveSupport?.data ?? SUPPORT_STATUS.unknown,
"Archive",
)}
>
{isLoading ? (
<Shimmer />
) : (
<SupportIcon
support={
archiveSupport.isLoading ? SUPPORT_STATUS.testing : archiveSupport?.data ?? SUPPORT_STATUS.unknown
}
/>
)}
</Tooltip>
</td>
<td className="px-3 py-1 text-sm text-center border">
{isLoading ? (
<Shimmer />
Expand Down
5 changes: 5 additions & 0 deletions utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
};