import { CopyText } from "@/components/Copy" import { ForkMethodIcon, Spinner, } from "@/components/Icons" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { AUTO_RPC_ENDPOINT, currentWsStatus$, LIGHT_CLIENT_ENDPOINT, Network, networkCategories, onChangeChain, SelectedChain, selectedChain$, } from "@/state/chains/chain.state" import { addCustomNetwork, getCustomNetwork } from "@/state/chains/networks" import { Input } from "@polkahub/ui-components" import { useStateObservable } from "@react-rxjs/core" import { Check, ChevronDown } from "lucide-react" import { StatusChange } from "polkadot-api/ws" import { FC, useMemo, useState } from "react" import { twMerge } from "tailwind-merge" export function NetworkSwitcher({ className }: { className?: string }) { const [open, setOpen] = useState(false) const selectedChain = useStateObservable(selectedChain$) const websocketStatus = useStateObservable(currentWsStatus$) return ( setOpen(false)} /> ) } const NetworkSwitchDialogContent: FC<{ selectedChain: SelectedChain onClose: () => void }> = ({ selectedChain, onClose }) => { const [query, setQuery] = useState("") const [network, setNetwork] = useState(selectedChain.network) const [endpoint, setEndpoint] = useState(selectedChain.endpoint) const customUrl = normalizeWsUrl(query) // Forking is not offered. // // Chopsticks is built on `@polkadot/types`, which cannot decode a Quantus // block at all: it caps fixed arrays at 2048 bytes where ML-DSA signatures are // 5261 and 7219, reads the extrinsic preamble byte as a version, and knows // nothing of this chain's header layout. Forklift resolves its own unpatched // copy of `@polkadot-api/substrate-bindings`, so it hits the very header bug // this console patches around. // // Neither can fork a chain in this list, so the two buttons offered things // that could only fail, and fail without saying why. `forkMethod` stays in // SelectedChain — the hash params and chain.state still read it — but nothing // in the UI can now set it to anything other than "none". const selectedForkMethod = "none" as const const hasChanged = network.id !== selectedChain.network.id || endpoint !== selectedChain.endpoint || selectedForkMethod !== selectedChain.forkMethod const selectNetwork = ( next: Network, nextEndpoint = defaultEndpoint(next), ) => { setNetwork(next) setEndpoint(nextEndpoint) } const selectCustomUrl = (url: string) => { setNetwork(createCustomNetwork(url)) setEndpoint(url) } const setConnection = (nextEndpoint: string) => { setEndpoint(nextEndpoint) } const confirm = () => { if (network.id === "custom") { if ( endpoint.startsWith("ws://localhost") || endpoint.startsWith("ws://127.0.0.1") ) { const localNetwork = networkCategories.find( (cat) => cat.name === "Localhost", )?.networks[0] if (localNetwork) { onChangeChain({ network: localNetwork, endpoint, forkMethod: selectedForkMethod, }) onClose() return } } addCustomNetwork(endpoint) onChangeChain({ network: getCustomNetwork(), endpoint, forkMethod: selectedForkMethod, }) } else { onChangeChain({ network, endpoint, forkMethod: selectedForkMethod }) } onClose() } return ( { if ( evt.target instanceof HTMLElement && (evt.target.tagName === "INPUT" || evt.target.attributes.getNamedItem("cmdk-list")) ) { evt.preventDefault() } }} > Switch Network
) } const NetworkList: FC<{ query: string selectedNetwork: Network customUrl: string | null onQueryChange: (value: string) => void onNetworkSelect: (network: Network) => void onCustomSelect: (url: string) => void }> = ({ query, selectedNetwork, customUrl, onQueryChange, onNetworkSelect, onCustomSelect, }) => { const categories = useMemo( () => networkCategories.filter((category) => category.name !== "Custom"), [], ) const selectedValue = getNetworkItemValue(categories, selectedNetwork) return (
No matching chains or endpoints.
{customUrl ? ( onCustomSelect(customUrl)} /> ) : null} {categories.map((category) => ( {category.networks.map((network) => ( onNetworkSelect(network)} /> ))} ))}
) } const NetworkItem: FC<{ value: string selected: boolean title: string onSelect: () => void }> = ({ value, selected, title, onSelect }) => (
{title}
) const getNetworkItemValue = ( categories: Array<{ name: string; networks: Network[] }>, network: Network, ) => { if (network.id === "custom") { return undefined } const category = categories.find((category) => category.networks.some((x) => x.id === network.id), ) return category ? networkSearchValue(category.name, network) : undefined } const ConnectionList: FC<{ network: Network endpoint: string onEndpointChange: (endpoint: string) => void }> = ({ network, endpoint, onEndpointChange }) => { const entries = Object.entries(network.endpoints) return (
Connection
{network.lightclient ? ( ) : null} {entries.length > 1 && network.id !== "custom" && network.id !== "localhost" ? ( ) : null} {network.id !== "custom" ? entries.map(([name, url]) => ( )) : null} {network.id === "localhost" ? ( url)} onEndpointChange={onEndpointChange} /> ) : null} {network.id === "custom" && endpoint ? ( ) : null}
) } const CustomPortOption: FC<{ endpoint: string knownEndpoints: string[] onEndpointChange: (endpoint: string) => void }> = ({ endpoint, knownEndpoints, onEndpointChange }) => { const isKnownEndpoint = (port: string) => knownEndpoints.some( (knownEndpoint) => getLocalhostPort(knownEndpoint) === port, ) const [port, setPort] = useState(() => { const initialPort = getLocalhostPort(endpoint) return !initialPort || isKnownEndpoint(initialPort) ? "" : initialPort }) const endpointValue = port ? `ws://127.0.0.1:${port}` : "" const value = port && !isKnownEndpoint(port) ? port === getLocalhostPort(endpoint) ? endpoint : endpointValue : `custom-localhost-port-${port || "empty"}` const selected = !!port && getLocalhostPort(endpoint) === port && !isKnownEndpoint(port) return (
{ const nextPort = evt.target.value setPort(nextPort) if (nextPort) onEndpointChange(`ws://127.0.0.1:${nextPort}`) }} />
) } const ConnectionOption: FC<{ value: string selected: boolean title: string subtitle: string badge?: string copy?: string }> = ({ value, selected, title, subtitle, badge, copy }) => (
{copy ? : null}
) const defaultEndpoint = (network: Network) => { if (network.lightclient) return LIGHT_CLIENT_ENDPOINT const endpoints = Object.values(network.endpoints) return network.id === "localhost" || endpoints.length === 1 ? endpoints[0] : AUTO_RPC_ENDPOINT } const createCustomNetwork = (url: string): Network => ({ id: "custom", display: url, lightclient: false, endpoints: { "WebSocket URL": url }, }) const normalizeWsUrl = (value: string) => { const trimmed = value.trim() if (!trimmed) return null const candidate = trimmed.startsWith("localhost:") || trimmed.startsWith("127.0.0.1:") ? `ws://${trimmed}` : trimmed try { const url = new URL(candidate) return url.protocol === "ws:" || url.protocol === "wss:" ? url.toString() : null } catch { return null } } const getLocalhostPort = (value: string) => { try { const url = new URL(value) return isLocalUrl(url) ? url.port : null } catch { return null } } const networkSearchValue = (category: string, network: Network) => [category, network.display, network.id].join(" ") const getChainLabel = ({ network, endpoint }: SelectedChain) => { if (network.id !== "custom") return network.display try { const url = new URL(endpoint) return isLocalUrl(url) ? "Localhost" : url.hostname } catch { return endpoint } } const getConnectionLabel = ( selectedChain: SelectedChain, websocketStatus: StatusChange | null, ) => { const { network, endpoint, forkMethod } = selectedChain if (endpoint === LIGHT_CLIENT_ENDPOINT) return "Smoldot" if (endpoint === AUTO_RPC_ENDPOINT) { if (forkMethod !== "none") return null if (!websocketStatus) return const isReady = "uri" in websocketStatus const activeEndpoint = isReady ? findEndpointName(network, websocketStatus.uri) : null return ( activeEndpoint ?? (isReady ? ( formatUrl(websocketStatus.uri) ) : ( )) ) } return findEndpointName(network, endpoint) ?? formatUrl(endpoint) } const findEndpointName = (network: Network, endpoint: string) => Object.entries(network.endpoints).find(([, url]) => endpoint === url)?.[0] const formatUrl = (value: string) => { try { const url = new URL(value) if (isLocalUrl(url)) return url.port ? `${url.hostname}:${url.port}` : url.hostname return url.hostname } catch { return value } } const isLocalUrl = (url: URL) => url.hostname === "localhost" || url.hostname === "127.0.0.1"