add block diff to block detail
This commit is contained in:
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
105
src/components/icons/chopsticks_dark.svg
Normal file
105
src/components/icons/chopsticks_dark.svg
Normal 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 |
103
src/components/icons/chopsticks_light.svg
Normal file
103
src/components/icons/chopsticks_light.svg
Normal 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 |
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
217
src/pages/Explorer/Detail/BlockStorageDiff.tsx
Normal file
217
src/pages/Explorer/Detail/BlockStorageDiff.tsx
Normal 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,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user