ghost-extension-wallet/src/components/Bootnodes.tsx
Uncle Fatso 6906ca83b7
initial commit in remote repository
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2025-07-22 13:53:22 +03:00

270 lines
11 KiB
TypeScript

import { useEffect, useState } from "react"
import { MdDeleteOutline } from "react-icons/md"
import * as environment from "../environment"
import "./Bootnodes.css"
import { Title, Switch } from "."
import { helper } from "@substrate/light-client-extension-helpers/extension-page"
import { wellKnownGenesisHashByChainId } from "../constants"
import {
Select,
SelectValue,
SelectTrigger,
SelectContent,
SelectGroup,
SelectItem,
} from "@/components/ui/select"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
interface BootnodesType {
checked: boolean
bootnode: string
}
const getBootNodes = async (chainId: string) =>
(await helper.getChains()).find(
({ genesisHash }) => genesisHash === wellKnownGenesisHashByChainId[chainId],
)?.bootNodes ?? []
const setBootNodes = async (chainId: string, bootNodes: string[]) =>
helper.setBootNodes(wellKnownGenesisHashByChainId[chainId], bootNodes)
// Add to localstorage the given bootnode for the given chain
const saveToLocalStorage = async (
chainName: string,
bootnode: string,
add: boolean,
def: string[],
) => {
if (def.length === 0) throw new Error("Default Bootnodes should exist.")
let res: string[]
const chainBootnodes = await getBootNodes(chainName)
res = chainBootnodes && Object.keys(chainBootnodes).length > 0
? [...chainBootnodes]
: [...def]
add ? res.push(bootnode) : res.splice(res.indexOf(bootnode), 1)
await setBootNodes(chainName, res)
}
export const Bootnodes = () => {
const [selectedChain, setSelectedChain] = useState<string>("casper_staging_testnet")
const [defaultBn, setDefaultBn] = useState<BootnodesType[]>([])
const [customBn, setCustomBn] = useState<BootnodesType[]>([])
const [customBnInput, setCustomBnInput] = useState<string>("")
const [selectedChainDefaultBn, setSelectedChainDefaultBn] = useState<string[]>([])
const [addMessage, setAddMessage] = useState<any>(undefined)
const [bootnodeMsgClass, setBootnodeMsgClass] = useState<string>()
useEffect(() => {
if (addMessage && !addMessage?.error) {
setBootnodeMsgClass("pb-2 text-accent")
setCustomBnInput("")
} else {
setBootnodeMsgClass("pb-2 text-destructive")
}
}, [addMessage])
useEffect(() => {
Promise.all([
getBootNodes(selectedChain),
environment.getDefaultBootnodes(selectedChain),
]).then(([bootnodes, defaultBootnodes]) => {
console.assert(defaultBootnodes, `Invalid chain name: ${selectedChain}`)
defaultBootnodes ??= []
setSelectedChainDefaultBn(defaultBootnodes)
const tmpDef: BootnodesType[] = []
const tmpCust: BootnodesType[] = []
// When bootnodes do not exist assign and save the local ones
if (!bootnodes?.length) {
setBootNodes(selectedChain, defaultBootnodes)
defaultBootnodes.forEach((b) => {
tmpDef.push({ bootnode: b, checked: true })
})
} else {
bootnodes?.forEach((b) => {
defaultBootnodes?.length && defaultBootnodes?.includes(b)
? tmpDef.push({ bootnode: b, checked: true })
: tmpCust.push({ bootnode: b, checked: true })
})
}
setDefaultBn(tmpDef)
setCustomBn(tmpCust)
})
}, [selectedChain])
const checkMultiAddr = (addr: string) => {
const ws = /\/(ip4|ip6|dns4|dns6|dns)\/([a-zA-Z0-9.-]+)\/tcp\/[0-9]{1,5}(\/(ws|wss|tls\/ws))?\/p2p\/[a-zA-Z1-9^Il0O]+/i
const webrtc = /\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc\/certhash\/(.*?)\/p2p\/[a-zA-Z1-9^Il0O]+/i
if (!ws.test(addr) && !webrtc.test(addr))
throw new Error("Provided multiaddress is not correct.")
}
const alterBootnodes = async (
bootnode: string,
add: boolean,
defaultBootnode: boolean,
) => {
// if bootnode belongs to the list (default) then it does not need to be validated as it
// comes from the chainspecs. It can be saved to the local storage at once.
try {
if (!defaultBootnode) {
// verify bootnode validity
checkMultiAddr(customBnInput)
}
// Check if bootnode already exists in the default and custom lists
if (
selectedChainDefaultBn?.includes(customBnInput) ||
customBn.map((c) => c.bootnode).includes(customBnInput)
) {
setAddMessage({ error: true, message: "Bootnode already exists in the list." })
} else {
await saveToLocalStorage(
selectedChain,
bootnode,
add,
selectedChainDefaultBn,
)
}
const tmp = defaultBootnode ? [...defaultBn] : [...customBn]
const i = tmp.findIndex((b) => b.bootnode === bootnode)
if (i !== -1) {
tmp[i].checked = add
} else {
tmp.push({ bootnode, checked: true })
}
defaultBootnode ? setDefaultBn(tmp) : setCustomBn(tmp)
setCustomBnInput("")
} catch (err) {
setAddMessage({ error: true, message: (err as Error).message.replace(/^\w/, (c) => c.toUpperCase()) })
}
}
return (
<section className="container">
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4">
Networks
</h2>
<div className="p-4 bg-muted rounded">
<Select
onValueChange={(networkName) => {
setSelectedChain(networkName)
setCustomBnInput("")
setAddMessage(undefined)
}}
name={selectedChain}
value={selectedChain}
>
<SelectTrigger className="sm:w-[250px] w-[100%]" data-testid="scheme-select">
<SelectValue placeholder="Select Network" />
</SelectTrigger>
<SelectContent className="sm:w-[250px] w-[100%]">
<SelectGroup>
<SelectItem data-testid="scheme-casper_staging_testnet" value="casper_staging_testnet">
Casper
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Title className="mt-8">Bootnodes</Title>
<Title titleType="small">Default</Title>
<div className="mb-8">
{selectedChainDefaultBn?.map((bn) => (
<div className="flex items-center mb-2 leading-4 wrap">
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
{bn}
</div>
<Switch
className="w-1/2 sm:w-1/12"
bootnode={bn}
alterBootnodes={alterBootnodes}
defaultBootnode={true}
isChecked={defaultBn.map((d) => d.bootnode).includes(bn)}
/>
</div>
))}
</div>
<Title titleType="small" className="text-accent">Custom</Title>
<div className="mb-8">
{customBn.map((c) => (
<div className="flex items-center mb-2 leading-4">
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
{c.bootnode}
</div>
<div className="flex w-1/2 sm:w-1/12 ml-8">
<Button
variant="secondary"
className="flex items-center text-white h-6 bg-red-600 rounded-full hover:bg-red-700"
onClick={async () => {
try {
await saveToLocalStorage(
selectedChain,
c.bootnode,
false,
selectedChainDefaultBn,
)
} catch (e) {
console.log(e)
}
setCustomBn(customBn.filter((f) => f.bootnode !== c.bootnode))
}}
>
<MdDeleteOutline className="text-lg" />
</Button>
</div>
</div>
))}
</div>
<Title>Add custom Bootnode</Title>
<div className="flex flex-col">
<div className="flex sm:flex-row flex-col gap-6 justify-between mb-4">
<Input
type="text"
placeholder="Enter bootnode address"
className="sm:w-5/6 w-full"
value={customBnInput}
onChange={(v) => {
addMessage && setAddMessage(undefined)
setCustomBnInput(v.target.value)
}}
/>
<Button
variant="default"
size="full"
className="sm:w-1/6 w-full"
disabled={!customBnInput}
onClick={() => {
if (
selectedChainDefaultBn?.includes(customBnInput) ||
customBn.map((c) => c.bootnode).includes(customBnInput)
) {
setAddMessage({
error: true,
message: "Bootnode already exists in the list.",
})
} else {
alterBootnodes(
customBnInput,
true,
selectedChainDefaultBn?.includes(customBnInput),
)
}
}}
>
Add
</Button>
</div>
<p className={bootnodeMsgClass}>
{addMessage && Object.keys(addMessage) ? addMessage.message : ""}
</p>
</div>
</div>
</section>
)
}