diff --git a/src/components/pages/profile/ProfileNetworkDisplay.tsx b/src/components/pages/profile/ProfileNetworkDisplay.tsx index da8278d7..de6d8875 100644 --- a/src/components/pages/profile/ProfileNetworkDisplay.tsx +++ b/src/components/pages/profile/ProfileNetworkDisplay.tsx @@ -94,20 +94,6 @@ const ProfileNetworkDisplay: React.FC = ({ profile } )} - {/* RPC Endpoints - Available for tier 2+ */} - {tier >= 2 && profile.rpc?.public && profile.rpc.public.length > 0 && ( -
-

Public RPC Endpoints

-
- {profile.rpc.public.map((rpcUrl) => ( -
- {rpcUrl} -
- ))} -
-
- )} - {/* Profile Markdown Content - Always shown for networks */} {profile.profileMarkdown && (
diff --git a/src/components/pages/settings/index.tsx b/src/components/pages/settings/index.tsx index 612fa7c2..deac66ad 100644 --- a/src/components/pages/settings/index.tsx +++ b/src/components/pages/settings/index.tsx @@ -8,11 +8,13 @@ import { useSettings } from "../../../context/SettingsContext"; import { useMetaMaskExplorer } from "../../../hooks/useMetaMaskExplorer"; import { SUPPORTED_LANGUAGES } from "../../../i18n"; import { clearSupportersCache } from "../../../services/MetadataService"; +import type { MetadataRpcEndpoint } from "../../../services/MetadataService"; import type { AIProvider, PromptVersion, RPCUrls, RpcUrlsContextType } from "../../../types"; import { AI_PROVIDERS, AI_PROVIDER_ORDER } from "../../../config/aiProviders"; import { clearAICache } from "../../common/AIAnalysis/aiCache"; import { logger } from "../../../utils/logger"; import { getChainIdFromNetwork } from "../../../utils/networkResolver"; +import { clearMetadataRpcCache, getMetadataEndpointMap } from "../../../utils/rpcStorage"; // Infura network slugs by chain ID const INFURA_NETWORKS: Record = { @@ -47,12 +49,6 @@ const getAlchemyUrl = (chainId: number, apiKey: string): string | null => { const isInfuraUrl = (url: string): boolean => url.includes("infura.io"); const isAlchemyUrl = (url: string): boolean => url.includes("alchemy.com"); -const getProviderBadge = (url: string): string | null => { - if (isInfuraUrl(url)) return "INFURA"; - if (isAlchemyUrl(url)) return "ALCHEMY"; - return null; -}; - const Settings: React.FC = () => { const { t, i18n } = useTranslation("settings"); const { rpcUrls, setRpcUrls } = useContext(AppContext); @@ -68,7 +64,6 @@ const Settings: React.FC = () => { networkId: string; index: number; } | null>(null); - const [fetchingNetworkId, setFetchingNetworkId] = useState(null); const [expandedChains, setExpandedChains] = useState>(new Set()); const [localApiKeys, setLocalApiKeys] = useState({ infura: settings.apiKeys?.infura || "", @@ -119,6 +114,8 @@ const Settings: React.FC = () => { const clearAllCaches = useCallback(() => { // Clear metadata service caches clearSupportersCache(); + // Clear metadata RPC cache + clearMetadataRpcCache(); // Clear localStorage caches if any localStorage.removeItem("openscan_cache"); // Clear AI analysis cache @@ -204,56 +201,59 @@ const Settings: React.FC = () => { return ""; }; - // Fetch RPCs from Chainlist API (only for EVM networks) - // networkId: CAIP-2 format (e.g., "eip155:1"), chainId: numeric for Chainlist API - const fetchFromChainlist = useCallback(async (networkId: string, chainId: number | undefined) => { - // Only EVM networks with numeric chainId are supported by Chainlist - if (chainId === undefined) return; - setFetchingNetworkId(networkId); - try { - const response = await fetch("https://chainlist.org/rpcs.json"); - if (!response.ok) throw new Error("Failed to fetch from Chainlist"); - - const chains = await response.json(); - const chain = chains.find((c: { chainId: number }) => c.chainId === chainId); - - if (!chain?.rpc) { - throw new Error(`No RPCs found for chain ${chainId}`); + // Build URL→endpoint lookup map from cached metadata + const metadataUrlMap = useMemo(() => { + const endpointMap = getMetadataEndpointMap(); + const urlMap = new Map(); + for (const endpoints of Object.values(endpointMap)) { + for (const ep of endpoints) { + urlMap.set(ep.url, ep); } + } + return urlMap; + }, []); - // Filter for tracking: "none" and extract URLs - const newUrls = chain.rpc - .filter((rpc: { tracking?: string }) => rpc.tracking === "none") - .map((rpc: { url: string }) => rpc.url) - .filter((url: string) => url && !url.includes("${")) - .filter((url: string) => !url.startsWith("wss://")); + /** + * Get CSS class for an RPC tag based on metadata endpoint properties + * - rpc-tracking: has tracking (tracking !== "none") → yellow + * - rpc-opensource: no tracking + open source → green + * - rpc-private: no tracking + not open source → light green + * - no class: URL not found in metadata (user-added) + */ + const getRpcTagClass = useCallback( + (url: string): string => { + // Personal API key URLs have tracking enabled + if (isInfuraUrl(url) || isAlchemyUrl(url)) return "rpc-tracking"; + const ep = metadataUrlMap.get(url); + if (!ep) return ""; + if (ep.tracking !== "none") return "rpc-tracking"; + if (ep.isOpenSource) return "rpc-opensource"; + return "rpc-private"; + }, + [metadataUrlMap], + ); - if (newUrls.length === 0) { - throw new Error(`No privacy-friendly RPCs found for chain ${chainId}`); + /** + * Get display label for an RPC tag. + * Priority: Infura/Alchemy personal → metadata provider name → hostname from URL + */ + const getRpcTagLabel = useCallback( + (url: string): string => { + if (isInfuraUrl(url)) return "Infura Personal"; + if (isAlchemyUrl(url)) return "Alchemy Personal"; + const ep = metadataUrlMap.get(url); + if (ep?.provider && ep.provider.toLowerCase() !== "unknown") return ep.provider; + try { + return new URL(url).hostname; + } catch { + return url; } + }, + [metadataUrlMap], + ); - // Merge with existing URLs, avoiding duplicates (store by networkId) - setLocalRpc((prev) => { - const currentValue = prev[networkId]; - const existingUrls = Array.isArray(currentValue) - ? currentValue - : typeof currentValue === "string" - ? currentValue - .split(",") - .map((s) => s.trim()) - .filter(Boolean) - : []; - const mergedUrls = Array.from(new Set([...existingUrls, ...newUrls])); - return { - ...prev, - [networkId]: mergedUrls, - }; - }); - } catch (error) { - logger.error("Error fetching from Chainlist:", error); - } finally { - setFetchingNetworkId(null); - } + const copyToClipboard = useCallback((url: string) => { + navigator.clipboard.writeText(url); }, []); // Handle setting OpenScan as MetaMask default explorer @@ -862,6 +862,18 @@ const Settings: React.FC = () => {

🔗 {t("rpcEndpoints.title")}

{t("rpcEndpoints.description")}

+
+ + {t("rpcEndpoints.legendOpensource")} + + + {t("rpcEndpoints.legendPrivate")} + + + {t("rpcEndpoints.legendTracking")} + +
+
{chainConfigs.map((chain) => { const isExpanded = expandedChains.has(chain.id); @@ -921,22 +933,6 @@ const Settings: React.FC = () => { {/* Collapsible Content */} {isExpanded && (
- {/* Only show Chainlist fetch for EVM networks */} - {chain.chainId !== undefined && ( -
- -
- )} - { // biome-ignore lint/a11y/noStaticElementInteractions: Drag-and-drop requires these handlers
handleDragStart(chain.id, idx)} onDragOver={handleDragOver} onDrop={() => handleDrop(chain.id, idx)} onDragEnd={() => setDraggedItem(null)} + onClick={() => copyToClipboard(url)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") copyToClipboard(url); + }} > {idx + 1} - {getProviderBadge(url) ? ( - - {getProviderBadge(url)} - - ) : ( - {url} - )} + + {getRpcTagLabel(url)} +