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 (
)
}
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"