add block diff to block detail

This commit is contained in:
Victor Oliva
2025-05-17 11:38:47 +02:00
parent cd17789bb5
commit 03907fbb04
11 changed files with 639 additions and 53 deletions

View File

@@ -1,5 +1,76 @@
import {
ChopsticksProvider,
setStorage,
setup,
} from "@acala-network/chopsticks-core"
import { Blockchain } from "@acala-network/chopsticks-core"
import { getSyncProvider } from "@polkadot-api/json-rpc-provider-proxy"
import { BehaviorSubject } from "rxjs"
export const chopsticksInstance$ = new BehaviorSubject<Blockchain | null>(null)
export const createChopsticksProvider = (endpoint: string) =>
getSyncProvider(async () => {
const { ChopsticksProvider, setup } = await import(
"@acala-network/chopsticks-core"
)
chopsticksInstance$.getValue()?.close()
const chain = await setup({
endpoint,
mockSignatureHost: true,
})
chopsticksInstance$.next(chain)
const innerProvider = new ChopsticksProvider(chain)
return (onMessage) => {
return {
send: async (message: string) => {
const parsed = JSON.parse(message)
if (parsed.method === "chainHead_v1_follow") {
const subscription = await innerProvider.subscribe(
"chainHead_v1_followEvent",
parsed.method,
parsed.params,
(err, result) => {
if (err) {
console.error(err)
return
}
onMessage(
JSON.stringify({
jsonrpc: "2.0",
method: "chainHead_v1_followEvent",
params: {
subscription,
result,
},
}),
)
},
)
onMessage(
JSON.stringify({
jsonrpc: "2.0",
id: parsed.id,
result: subscription,
}),
)
return
}
const response = await innerProvider.send(
parsed.method,
parsed.params,
)
onMessage(
JSON.stringify({
jsonrpc: "2.0",
id: parsed.id,
result: response,
}),
)
},
disconnect: () => {
chain?.close()
chopsticksInstance$.next(null)
},
}
}
})

View File

@@ -1,9 +1,5 @@
import { ReactSVG, Props } from "react-svg"
import focusSvg from "./icons/focus.svg"
import enumSvg from "./icons/enum.svg"
import binarySvg from "./icons/binary.svg"
import walletConnectSvg from "./icons/walletConnect.svg"
import { useEffect, useRef } from "react"
import { useTheme } from "@/ThemeProvider"
import { LookupEntry } from "@polkadot-api/metadata-builders"
import {
Ban,
Binary,
@@ -16,38 +12,57 @@ import {
LucideProps,
User,
} from "lucide-react"
import { LookupEntry } from "@polkadot-api/metadata-builders"
import { FC, useEffect, useRef } from "react"
import { Props, ReactSVG } from "react-svg"
import { twMerge } from "tailwind-merge"
import binarySvg from "./icons/binary.svg"
import chopsticksLogoDark from "./icons/chopsticks_dark.svg"
import chopsticksLogoLight from "./icons/chopsticks_light.svg"
import enumSvg from "./icons/enum.svg"
import focusSvg from "./icons/focus.svg"
import walletConnectSvg from "./icons/walletConnect.svg"
type CustomIconProps = Omit<Props, "ref" | "src"> & { size?: number }
const customIcon =
(url: string) =>
({ size = 16, ...props }: CustomIconProps) => {
const ref = useRef<SVGSVGElement | null>(null)
useEffect(() => {
if (!ref.current) return
ref.current.setAttribute("width", String(size))
ref.current.setAttribute("height", String(size))
}, [size])
return (
<ReactSVG
{...props}
src={url}
beforeInjection={(svg) => {
ref.current = svg
svg.setAttribute("width", String(size))
svg.setAttribute("height", String(size))
}}
/>
)
const CustomIcon: FC<
CustomIconProps & {
url: string
}
> = ({ size = 16, url, ...props }) => {
const ref = useRef<SVGSVGElement | null>(null)
useEffect(() => {
if (!ref.current) return
ref.current.setAttribute("width", String(size))
ref.current.setAttribute("height", String(size))
}, [size])
return (
<ReactSVG
{...props}
src={url}
beforeInjection={(svg) => {
ref.current = svg
svg.setAttribute("width", String(size))
svg.setAttribute("height", String(size))
}}
/>
)
}
const customIcon = (url: string) => (props: CustomIconProps) => (
<CustomIcon url={url} {...props} />
)
const themeIcon = (light: string, dark: string) => (props: CustomIconProps) => {
const theme = useTheme()
return <CustomIcon url={theme === "light" ? light : dark} {...props} />
}
export const Focus = customIcon(focusSvg)
export const Enum = customIcon(enumSvg)
export const BinaryEdit = customIcon(binarySvg)
export const WalletConnect = customIcon(walletConnectSvg)
export const Chopsticks = themeIcon(chopsticksLogoLight, chopsticksLogoDark)
export const Spinner = (props: LucideProps) => (
<LoaderCircle

View File

@@ -9,7 +9,8 @@ import { useTheme } from "@/ThemeProvider"
export const JsonDisplay: FC<{
src: unknown
}> = ({ src }) => {
collapsed?: boolean
}> = ({ src, ...props }) => {
const theme = useTheme()
return (
@@ -30,6 +31,7 @@ export const JsonDisplay: FC<{
2,
)
}
{...props}
/>
)
}

View File

@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="31"
height="37"
viewBox="0 0 31 37"
fill="none"
version="1.1"
id="svg6"
sodipodi:docname="chopsticks.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="9.0017274"
inkscape:cx="16.774558"
inkscape:cy="16.663468"
inkscape:window-width="1392"
inkscape:window-height="1051"
inkscape:window-x="2793"
inkscape:window-y="57"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<circle
cx="15.444743"
cy="18.474794"
r="15"
fill="url(#paint0_linear_3000_4054)"
id="circle1" />
<circle
cx="15.444743"
cy="18.474794"
r="15"
fill="url(#paint1_linear_3000_4054)"
fill-opacity="0.8"
id="circle2" />
<rect
x="-2.6022379"
y="5.396699"
width="3"
height="36"
rx="1.5"
transform="rotate(-30)"
fill="white"
fill-opacity="0.7"
id="rect2" />
<rect
x="12.785393"
y="4.2307425"
width="3"
height="36"
rx="1.5"
transform="rotate(-15)"
fill="white"
fill-opacity="0.7"
id="rect3" />
<defs
id="defs6">
<linearGradient
id="paint0_linear_3000_4054"
x1="62.285"
y1="74.922897"
x2="24.7505"
y2="36.729698"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25.555257,-32.999808)">
<stop
stop-color="#E40C5B"
stop-opacity="0.88"
id="stop3" />
<stop
offset="1"
stop-color="#FF4C3B"
id="stop4" />
</linearGradient>
<linearGradient
id="paint1_linear_3000_4054"
x1="22.985001"
y1="31.403601"
x2="67.765999"
y2="45.6506"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25.555257,-32.999808)">
<stop
stop-color="white"
stop-opacity="0.53"
id="stop5" />
<stop
offset="1"
stop-color="white"
stop-opacity="0"
id="stop6" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="31"
height="37"
viewBox="0 0 31 37"
fill="none"
version="1.1"
id="svg6"
sodipodi:docname="chopsticks.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="9.0017274"
inkscape:cx="16.774558"
inkscape:cy="16.663468"
inkscape:window-width="1392"
inkscape:window-height="1051"
inkscape:window-x="2793"
inkscape:window-y="57"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<circle
cx="15.444743"
cy="18.474794"
r="15"
fill="url(#paint0_linear_3000_4054)"
id="circle1" />
<circle
cx="15.444743"
cy="18.474794"
r="15"
fill="url(#paint1_linear_3000_4054)"
fill-opacity="0.8"
id="circle2" />
<rect
x="-2.6022379"
y="5.396699"
width="3"
height="36"
rx="1.5"
transform="rotate(-30)"
fill="#232429"
id="rect2" />
<rect
x="12.785393"
y="4.2307425"
width="3"
height="36"
rx="1.5"
transform="rotate(-15)"
fill="#232429"
id="rect3" />
<defs
id="defs6">
<linearGradient
id="paint0_linear_3000_4054"
x1="62.285"
y1="74.922897"
x2="24.7505"
y2="36.729698"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25.555257,-32.999808)">
<stop
stop-color="#E40C5B"
stop-opacity="0.88"
id="stop3" />
<stop
offset="1"
stop-color="#FF4C3B"
id="stop4" />
</linearGradient>
<linearGradient
id="paint1_linear_3000_4054"
x1="22.985001"
y1="31.403601"
x2="67.765999"
y2="45.6506"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25.555257,-32.999808)">
<stop
stop-color="white"
stop-opacity="0.53"
id="stop5" />
<stop
offset="1"
stop-color="white"
stop-opacity="0"
id="stop6" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -31,7 +31,7 @@ export const setHashParams = (
location.hash = "#" + params.toString()
}
const persistingKeys = ["networkId", "endpoint"]
const persistingKeys = ["networkId", "endpoint", "chopsticks"]
const usePersistKeys = () => {
const location = useLocation()

View File

@@ -1,3 +1,4 @@
import { chopsticksInstance$ } from "@/chopsticks/chopsticks"
import { Loading } from "@/components/Loading"
import { groupBy } from "@/lib/groupBy"
import { runtimeCtx$ } from "@/state/chains/chain.state"
@@ -5,12 +6,21 @@ import * as Tabs from "@radix-ui/react-tabs"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC, useState } from "react"
import { useLocation, useParams } from "react-router-dom"
import { combineLatest, distinctUntilChanged, filter, map, take } from "rxjs"
import {
combineLatest,
distinctUntilChanged,
filter,
map,
switchMap,
take,
} from "rxjs"
import { twMerge } from "tailwind-merge"
import { BlockInfo, blockInfoState$ } from "../block.state"
import { BlockEvents } from "./BlockEvents"
import { BlockStorageDiff } from "./BlockStorageDiff"
import { ApplyExtrinsicEvent, Extrinsic } from "./Extrinsic"
import { createExtrinsicCodec, DecodedExtrinsic } from "./extrinsicDecoder"
import { BlockEvents } from "./BlockEvents"
import { Chopsticks } from "@/components/Icons"
const blockExtrinsics$ = state((hash: string) => {
const decoder$ = runtimeCtx$.pipe(
@@ -32,13 +42,32 @@ const blockExtrinsics$ = state((hash: string) => {
)
}, [])
type Tab = "signed" | "unsigned" | "events"
const blockHasDiff$ = state(
(hash: string) =>
chopsticksInstance$.pipe(
switchMap((chain) => {
if (!chain) return [null]
return chain.getBlock(hash as any)
}),
switchMap((block) => {
if (!block) return [null]
return block.storageDiff()
}),
map((v) => Boolean(v && Object.keys(v).length > 0)),
),
false,
)
type Tab = "signed" | "unsigned" | "events" | "diff"
export const BlockBody: FC<{
block: BlockInfo
}> = ({ block }) => {
const { hash } = useParams()
const [selectedTab, setSelectedTab] = useState<Tab | null>(null)
const extrinsics = useStateObservable(blockExtrinsics$(hash ?? ""))
const diff = useStateObservable(blockHasDiff$(hash ?? ""))
const location = useLocation()
const hashParams = new URLSearchParams(location.hash.slice(1))
const eventParam = hashParams.get("event")
@@ -102,7 +131,7 @@ export const BlockBody: FC<{
</Tabs.Trigger>
<Tabs.Trigger
className={twMerge(
"bg-secondary text-secondary-foreground/80 px-4 py-2 hover:text-polkadot-500 border-t border-r rounded-tr border-polkadot-200",
"bg-secondary text-secondary-foreground/80 px-4 py-2 hover:text-polkadot-500 border-t border-r border-polkadot-200",
"disabled:text-secondary-foreground/50 disabled:pointer-events-none",
"data-[state=active]:font-bold data-[state=active]:text-secondary-foreground",
)}
@@ -113,7 +142,7 @@ export const BlockBody: FC<{
</Tabs.Trigger>
<Tabs.Trigger
className={twMerge(
"bg-secondary text-secondary-foreground/80 px-4 py-2 hover:text-polkadot-500 border-t border-r rounded-tr border-polkadot-200",
"bg-secondary text-secondary-foreground/80 px-4 py-2 hover:text-polkadot-500 border-t border-r last:rounded-tr border-polkadot-200",
"disabled:text-secondary-foreground/50 disabled:pointer-events-none",
"data-[state=active]:font-bold data-[state=active]:text-secondary-foreground",
)}
@@ -121,6 +150,22 @@ export const BlockBody: FC<{
>
Events
</Tabs.Trigger>
{diff && (
<Tabs.Trigger
className={twMerge(
"bg-secondary text-secondary-foreground/80 px-4 py-2 hover:text-polkadot-500 border-t border-r rounded-tr border-polkadot-200",
"disabled:text-secondary-foreground/50 disabled:bg-secondary/50 disabled:pointer-events-none",
"data-[state=active]:font-bold data-[state=active]:text-secondary-foreground",
)}
value="diff"
>
Diff
<Chopsticks
className="inline-block align-middle ml-2"
size={20}
/>
</Tabs.Trigger>
)}
</Tabs.List>
<Tabs.Content value="signed" className="py-2">
<ol>
@@ -149,6 +194,9 @@ export const BlockBody: FC<{
<Tabs.Content value="events" className="py-2">
<BlockEvents block={block} highlightedEvent={defaultEventOpen} />
</Tabs.Content>
<Tabs.Content value="diff" className="py-2">
<BlockStorageDiff block={block} />
</Tabs.Content>
</Tabs.Root>
</div>
)

View File

@@ -0,0 +1,217 @@
import { FC } from "react"
import { BlockInfo } from "../block.state"
import { Binary, Codec } from "polkadot-api"
import {
HexString,
Struct,
Tuple,
Twox128,
u32,
u64,
u8,
Vector,
} from "@polkadot-api/substrate-bindings"
import { state, useStateObservable } from "@react-rxjs/core"
import { dynamicBuilder$, lookup$ } from "@/state/chains/chain.state"
import { chopsticksInstance$ } from "@/chopsticks/chopsticks"
import { switchMap, map, combineLatest } from "rxjs"
import { toHex } from "@polkadot-api/utils"
import { groupBy } from "@/lib/groupBy"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { bytesToString } from "@/components/BinaryInput"
import { CopyText } from "@/components/Copy"
import { JsonDisplay } from "@/components/JsonDisplay"
const storageDiff$ = (hash: string) =>
chopsticksInstance$.pipe(
switchMap((chain) => {
if (!chain) return [null]
return chain.getBlock(hash as any)
}),
switchMap((block) => {
if (!block) return [null]
return block.storageDiff()
}),
map((v) =>
v && Object.keys(v).length > 0
? (v as Record<string, string | null>)
: null,
),
// map((v) => v ?? testDiff),
)
const TWOX128_LEN = 32
const blockDiff$ = state(
(hash: string) =>
// TODO for current block
combineLatest([lookup$, dynamicBuilder$, storageDiff$(hash)]).pipe(
map(([lookup, dynamicBuilder, diff]) => {
if (!diff) return null
const palletKeys = Object.fromEntries(
lookup.metadata.pallets.map((pallet) => [
toHex(Twox128(Binary.fromText(pallet.name).asBytes())).slice(2),
{
name: pallet.name,
entries: Object.fromEntries(
pallet.storage?.items.map((item) => [
toHex(Twox128(Binary.fromText(item.name).asBytes())).slice(2),
item.name,
]) ?? [],
),
},
]),
)
return Object.entries(diff)
.filter(([, newValue]) => newValue !== null)
.map(
([key, newValue]): {
key: HexString
decodedKey: [string, ...unknown[]]
newValue: HexString
decodedNewValue: unknown
} | null => {
try {
if (wellKnownKeys[key]) {
return {
key,
newValue: newValue!,
decodedKey: [wellKnownKeys[key].name],
decodedNewValue: wellKnownKeys[key].codec.dec(newValue!),
}
}
const pallet = palletKeys[key.slice(2, 2 + TWOX128_LEN)]
if (pallet) {
const entry =
pallet.entries[
key.slice(2 + TWOX128_LEN, 2 + TWOX128_LEN * 2)
]
const storageCodec =
entry && dynamicBuilder.buildStorage(pallet.name, entry)
if (storageCodec) {
return {
key,
newValue: newValue!,
decodedKey: [
pallet.name,
entry,
...storageCodec.keys.dec(key),
],
decodedNewValue: storageCodec.value.dec(newValue!),
}
}
}
console.warn("uknown key", key)
} catch (ex) {
console.error(ex)
}
return null
},
)
.filter((v) => !!v)
}),
map((diffResult) => {
if (!diffResult) return null
const groups = groupBy(diffResult, (v) => v.decodedKey[0])
return Object.entries(groups)
.map(([name, group]) => ({
name,
changes: group.sort((a, b) => a.key.localeCompare(b.key)),
}))
.sort((a, b) => a.name.localeCompare(b.name))
}),
),
null,
)
export const BlockStorageDiff: FC<{
block: BlockInfo
}> = ({ block }) => {
const diff = useStateObservable(blockDiff$(block.hash))
if (!diff) return null
return (
<Accordion type="multiple">
{diff.map(({ name, changes }) => (
<AccordionItem key={name} value={name}>
<AccordionTrigger>{name}</AccordionTrigger>
<AccordionContent className="space-y-4">
{changes.map((change) => (
<div key={change.key}>
<div>
<CopyText binary text={change.key} />{" "}
{change.decodedKey
.map((v) =>
typeof v === "object"
? `(${JSON.stringify(v, (_, v) =>
typeof v === "bigint"
? `${v}n`
: v instanceof Binary
? bytesToString(v)
: v,
)})`
: String(v),
)
.join(".")}
</div>
<JsonDisplay collapsed src={change.decodedNewValue} />
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}
const strToHex = (v: string) => Binary.fromText(v).asHex()
const wellKnownKeys: Record<
string,
{
name: string
codec: Codec<any>
}
> = {
[strToHex(":code")]: {
name: ":code",
codec: Vector(u8),
},
[strToHex(":heappages")]: {
name: ":heappages",
codec: u64,
},
[strToHex(":extrinsic_index")]: {
name: ":extrinsic_index",
codec: u32,
},
[strToHex(":intrablock_entropy")]: {
name: ":intrablock_entropy",
codec: Vector(u8, 32),
},
[strToHex(":transaction_level:")]: {
name: ":transaction_level:",
codec: u32,
},
[strToHex(":grandpa_authorities")]: {
name: ":grandpa_authorities",
codec: Tuple(
u8,
Vector(
Struct({
id: Vector(u8, 32),
weight: u64,
}),
),
),
},
}

View File

@@ -1,5 +1,6 @@
import { CommandPopover } from "@/components/CommandPopover"
import { CopyText } from "@/components/Copy"
import { Chopsticks } from "@/components/Icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
@@ -29,7 +30,7 @@ import {
} from "@/state/chains/chain.state"
import { addCustomNetwork, getCustomNetwork } from "@/state/chains/networks"
import { useStateObservable } from "@react-rxjs/core"
import { Check, ChevronDown, Server } from "lucide-react"
import { Check, ChevronDown } from "lucide-react"
import { FC, useState } from "react"
export function NetworkSwitcher() {
@@ -68,10 +69,14 @@ const NetworkSwitchDialogContent: FC<{
const currentRpc = selectedChain.endpoint ?? "light-client"
const [selectedRpc, setSelectedRpc] = useState<string>(currentRpc)
const [enteredText, setEnteredText] = useState<string>("")
const [withChopsticks, setWithChopsticks] = useState(
selectedChain.withChopsticks ?? false,
)
const hasChanged =
selectedNetwork.id !== selectedChain.network.id ||
selectedRpc !== currentRpc
selectedRpc !== currentRpc ||
selectedChain.withChopsticks !== withChopsticks
const handleNetworkSelect = (network: Network) => {
if (network === selectedNetwork) return
@@ -85,14 +90,20 @@ const NetworkSwitchDialogContent: FC<{
}
const handleConfirm = () => {
const chopsticksEnabled = selectedRpc !== "light-client" && withChopsticks
if (selectedNetwork.id === "custom-network") {
addCustomNetwork(selectedRpc)
onChangeChain({ network: getCustomNetwork(), endpoint: selectedRpc })
onChangeChain({
network: getCustomNetwork(),
endpoint: selectedRpc,
withChopsticks: chopsticksEnabled,
})
setEnteredText("")
} else {
onChangeChain({
network: selectedNetwork,
endpoint: selectedRpc,
withChopsticks: chopsticksEnabled,
})
}
onClose()
@@ -185,7 +196,7 @@ const NetworkSwitchDialogContent: FC<{
<ConnectionOption
value="light-client"
isSelected={selectedRpc === "light-client"}
name="Light Client (smoldot)"
name="Smoldot"
type="light"
/>
) : null}
@@ -208,7 +219,7 @@ const NetworkSwitchDialogContent: FC<{
<div className="mt-4 p-3 border rounded-md bg-muted/30">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Server className="h-4 w-4 text-muted-foreground" />
<Chopsticks size={20} />
<Label
htmlFor="use-chopsticks"
className="font-medium cursor-pointer"
@@ -216,10 +227,14 @@ const NetworkSwitchDialogContent: FC<{
Fork with Chopsticks
</Label>
</div>
<Switch id="use-chopsticks" />
<Switch
id="use-chopsticks"
checked={withChopsticks}
onCheckedChange={() => setWithChopsticks(!withChopsticks)}
/>
</div>
<p className="text-xs text-muted-foreground mt-1 ml-6">
Create a local development fork of this chain
Create a local fork of this chain
</p>
</div>
)}
@@ -265,7 +280,7 @@ const ConnectionOption: FC<{
)}
<p className="text-xs text-muted-foreground">
{type === "light"
? "Browser light client"
? "Light client for a decentralized experience"
: url?.includes("127.0.0.1")
? "Local RPC node"
: "Remote RPC node"}

View File

@@ -37,20 +37,23 @@ import {
} from "./websocket"
import { getHashParams, setHashParams } from "@/hashParams"
import { withLogsRecorder } from "polkadot-api/logs-provider"
import { createChopsticksProvider } from "@/chopsticks/chopsticks"
export type ChainSource = WebsocketSource | SmoldotSource
export type SelectedChain = {
network: Network
endpoint: string
withChopsticks: boolean
}
export const getChainSource = ({
endpoint,
network: { id, relayChain },
withChopsticks,
}: SelectedChain) =>
endpoint === "light-client"
? createSmoldotSource(id, relayChain)
: createWebsocketSource(id, endpoint)
: createWebsocketSource(id, endpoint, withChopsticks)
const setRpcLogsEnabled = (enabled: boolean) =>
localStorage.setItem("rpc-logs", String(enabled))
@@ -61,7 +64,9 @@ console.log("You can enable JSON-RPC logs by calling `setRpcLogsEnabled(true)`")
export const getProvider = (source: ChainSource) => {
const provider =
source.type === "websocket"
? getWebsocketProvider(source)
? source.withChopsticks
? createChopsticksProvider(source.endpoint)
: getWebsocketProvider(source)
: getSmoldotProvider(source)
return withLogsRecorder((msg) => {
@@ -96,22 +101,25 @@ export const isValidUri = (input: string): boolean => {
const defaultSelectedChain: SelectedChain = {
network: defaultNetwork,
endpoint: "light-client",
withChopsticks: false,
}
const getDefaultChain = (): SelectedChain => {
const hashParams = getHashParams()
if (hashParams.has("networkId") && hashParams.has("endpoint")) {
const networkId = hashParams.get("networkId")!
const endpoint = hashParams.get("endpoint")!
if (networkId === "custom") {
if (!isValidUri(endpoint)) return defaultSelectedChain
addCustomNetwork(endpoint)
return {
network: getCustomNetwork(),
endpoint,
withChopsticks: false,
}
}
const network = findNetwork(networkId)
if (network) return { network, endpoint }
if (network) return { network, endpoint, withChopsticks: false }
}
return defaultSelectedChain

View File

@@ -6,13 +6,15 @@ export interface WebsocketSource {
type: "websocket"
id: string
endpoint: string
withChopsticks: boolean
}
export async function createWebsocketSource(
id: string,
endpoint: string,
withChopsticks: boolean,
): Promise<WebsocketSource> {
return { type: "websocket", id, endpoint }
return { type: "websocket", id, endpoint, withChopsticks }
}
export function getWebsocketProvider(source: WebsocketSource): JsonRpcProvider {