270 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|