Skip to content

Commit

Permalink
Add API for updating bootnodes and getting default ones from chainspe…
Browse files Browse the repository at this point in the history
…cs (#1294)
  • Loading branch information
wirednkod authored Nov 18, 2022
1 parent f475c74 commit 48f5831
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 7 deletions.
4 changes: 4 additions & 0 deletions projects/extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Add new bootnodes screen in Options page, that allows the user to enable/disable/add/remove the bootnodes of the well-known chains. ([#1294](https://github.com/paritytech/substrate-connect/pull/1294))

### Changed

- Update @substrate/smoldot-light to [version 0.7.7](https://github.com/paritytech/smoldot/blob/main/bin/wasm-node/CHANGELOG.md#077---2022-11-11). ([#1345](https://github.com/paritytech/substrate-connect/pull/1345))
Expand Down
38 changes: 38 additions & 0 deletions projects/extension/src/components/Bootnodes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.networkSelect {
display: grid;
grid-template-areas: "select";
align-items: center;
position: relative;

min-width: 15ch;
max-width: 30ch;

border: 1px solid #999;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;

font-size: 1rem;
cursor: pointer;
line-height: 1.2;

margin-bottom: 1rem;
}

.networkSelect select {
cursor: pointer;
background-color: white;
}

.networkSelect::after {
grid-area: select;
}

.networkSelect:focus + .focus {
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border: 2px solid var(--select-focus);
border-radius: inherit;
}
257 changes: 257 additions & 0 deletions projects/extension/src/components/Bootnodes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import React, { useEffect, useState } from "react"
import { MdDeleteOutline } from "react-icons/md"
import * as environment from "../environment"

import "./Bootnodes.css"
import { Title, Switch } from "."
interface BootnodesType {
checked: boolean
bootnode: string
}

// 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 environment.get({
type: "bootnodes",
chainName,
})
res =
chainBootnodes && Object.keys(chainBootnodes).length > 0
? [...chainBootnodes]
: [...def]
add ? res.push(bootnode) : res.splice(res.indexOf(bootnode), 1)
await environment.set({ type: "bootnodes", chainName }, res)
}

export const Bootnodes = () => {
const [selectedChain, setSelectedChain] = useState<string>("polkadot")
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(() => {
// Load default Bootnodes
const defChains = environment.getDefaultBootnodes(selectedChain)
console.assert(defChains, "Invalid chain name: " + selectedChain)
setSelectedChainDefaultBn(defChains || [])
}, [selectedChain])

useEffect(() => {
if (addMessage && !addMessage?.error) {
setBootnodeMsgClass("pb-2 text-green-600")
setCustomBnInput("")
} else {
setBootnodeMsgClass("pb-2 text-red-600")
}
}, [addMessage])

useEffect(() => {
environment.getBootnodes(selectedChain).then((bootnodes) => {
const tmpDef: BootnodesType[] = []
const tmpCust: BootnodesType[] = []
// When bootnodes do not exist assign and save the local ones
if (!bootnodes?.length) {
environment.set(
{ type: "bootnodes", chainName: selectedChain },
selectedChainDefaultBn,
)
selectedChainDefaultBn.forEach((b) => {
tmpDef.push({ bootnode: b, checked: true })
})
} else {
bootnodes?.forEach((b) => {
const defaultBootnodes =
environment.getDefaultBootnodes(selectedChain)
defaultBootnodes?.length && defaultBootnodes?.includes(b)
? tmpDef.push({ bootnode: b, checked: true })
: tmpCust.push({ bootnode: b, checked: true })
})
}
setDefaultBn(tmpDef)
setCustomBn(tmpCust)
})
}, [selectedChain, selectedChainDefaultBn])

const checkMultiAddr = (addr: string) => {
const ws =
/\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/[0-9]{0,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="mx-0 md:mx-12 xl:mx-36 2xl:mx-64 font-roboto max-w-5xl">
<div className="font-inter font-bold text-3xl pb-4">Bootnodes</div>
<div className="bg-white border border-neutral-200 p-4 rounded-md">
{/* Network selection */}
<Title>Network</Title>
<div className="networkSelect">
<select
onChange={(v) => {
setSelectedChain(v.target.value)
setCustomBnInput("")
setAddMessage(undefined)
}}
>
<option value="polkadot">Polkadot</option>
<option value="ksmcc3">Kusama</option>
<option value="westend2">Westend</option>
<option value="rococo_v2_2">Rococo</option>
</select>
<span className="focus"></span>
</div>
<Title>Bootnodes</Title>
<Title titleType="small">Default</Title>
<div className="mb-8">
{selectedChainDefaultBn?.map((bn) => (
<div className="leading-4 flex items-center mb-2 wrap">
<div className="text-ellipsis overflow-hidden whitespace-nowrap w-11/12">
{bn}
</div>
<Switch
bootnode={bn}
alterBootnodes={alterBootnodes}
defaultBootnode={true}
isChecked={defaultBn.map((d) => d.bootnode).includes(bn)}
/>
</div>
))}
</div>
<Title titleType="small">Custom</Title>
<div className="mb-8">
{customBn.map((c) => (
<div className="leading-4 flex items-center mb-2">
<div className="text-ellipsis overflow-hidden whitespace-nowrap w-11/12">
{c.bootnode}
</div>
<button
className="flex bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded-full items-center"
onClick={async () => {
await saveToLocalStorage(
selectedChain,
c.bootnode,
false,
selectedChainDefaultBn,
)
setCustomBn(customBn.filter((f) => f.bootnode !== c.bootnode))
}}
>
<MdDeleteOutline className="text-base" />
<p>Remove</p>
</button>
</div>
))}
</div>
<Title>Add custom Bootnode</Title>
<div className="flex flex-col">
<div className="flex flex-row mb-4 justify-between">
<input
type="text"
className="w-10/12 block px-2 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding
border border-solid border-gray-300 rounded focus:text-gray-700 focus:bg-white focus:border-[#24cc85]
focus:outline-none"
placeholder="Enter bootnode address"
value={customBnInput}
onChange={(v) => {
addMessage && setAddMessage(undefined)
setCustomBnInput(v.target.value)
}}
/>
<div className="w-2/12">
<button
className="py-1.5 text-sm px-8 border border-[#24cc85] rounded text-[#24cc85] hover:text-white
hover:bg-[#24cc85] capitalize ml-4 disabled:border-gray-200 disabled:text-white disabled:bg-gray-200"
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>
</div>
<p className={bootnodeMsgClass}>
{addMessage && Object.keys(addMessage) ? addMessage.message : ""}
</p>
</div>
</div>
</section>
)
}
4 changes: 2 additions & 2 deletions projects/extension/src/components/BraveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export const BraveModal = ({ show, isOptions }: Props) => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
clipRule="evenodd"
></path>
</svg>
<span className="sr-only">Close modal</span>
Expand Down
49 changes: 49 additions & 0 deletions projects/extension/src/components/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from "react"

type manipulateBootnodeType = (
bootnode: string,
add: boolean,
defaultBootnode: boolean,
) => void

interface SwitchProps {
bootnode: string
alterBootnodes: manipulateBootnodeType
defaultBootnode: boolean
isChecked: boolean
}

const Switch = ({
bootnode,
alterBootnodes,
defaultBootnode,
isChecked,
}: SwitchProps) => {
const [checked, setChecked] = useState<boolean>(isChecked)

useEffect(() => {
setChecked(isChecked)
}, [isChecked])

return (
<div className="flex w-1/12 ml-8" key={bootnode}>
<label className="inline-flex relative items-center mr-5 cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={checked}
readOnly
/>
<div
onClick={() => {
alterBootnodes(bootnode, !checked, defaultBootnode)
setChecked(!checked)
}}
className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-green-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#16DB9A]"
></div>
</label>
</div>
)
}

export { Switch }
Loading

0 comments on commit 48f5831

Please sign in to comment.