feat: fork with forklift (#167)
* feat: fork with forklift * fix: missing entry input in storage set * fix: use best number to produce new blocks * fix empty block table on chain stop * fix: make network selector highlight the selected chain * fix: handle case where forklift has all storage diff values
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
import type { BlockDiff } from "@/state/block.state"
|
||||
import { chainClient$ } from "@/state/chains/chain.state"
|
||||
import { Blockchain, ChopsticksProvider } from "@acala-network/chopsticks-core"
|
||||
import {
|
||||
getSyncProvider,
|
||||
InnerJsonRpcProvider,
|
||||
} from "@polkadot-api/json-rpc-provider-proxy"
|
||||
import { blockHeader } from "@polkadot-api/substrate-bindings"
|
||||
import { Binary, blockHeader } from "@polkadot-api/substrate-bindings"
|
||||
import type { JsonRpcProvider } from "polkadot-api"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
map,
|
||||
Observable,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
toArray,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
|
||||
export const chopsticksInstance$ = new BehaviorSubject<Blockchain | null>(null)
|
||||
|
||||
@@ -181,3 +193,56 @@ const withChopsticksEnhancer =
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getChopsticksBlockDiff$ = (
|
||||
parent: string,
|
||||
hash: string,
|
||||
): Observable<BlockDiff | null> =>
|
||||
chopsticksInstance$.pipe(
|
||||
take(1),
|
||||
switchMap((chain) => (chain ? chain.getBlock(hash as any) : [null])),
|
||||
switchMap((block) => (block ? block.storageDiff() : [null])),
|
||||
map((diff) =>
|
||||
diff && Object.keys(diff).length > 0
|
||||
? (diff as Record<string, string | null>)
|
||||
: null,
|
||||
),
|
||||
startWith(null),
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([diff, { chainHead }]) => {
|
||||
if (!diff) return [null]
|
||||
|
||||
return chainHead
|
||||
.storageQueries$(
|
||||
parent,
|
||||
Object.keys(diff).map((key) => ({
|
||||
key,
|
||||
type: "value",
|
||||
})),
|
||||
)
|
||||
.pipe(
|
||||
toArray(),
|
||||
map((v) => Object.fromEntries(v.map((v) => [v.key, v.value]))),
|
||||
map((v) => ({
|
||||
...Object.fromEntries(
|
||||
Object.entries(diff)
|
||||
.filter(([key, newValue]) => (v[key] ?? null) !== newValue)
|
||||
.map(([key, newValue]) => {
|
||||
const prevValue = v[key] ?? null
|
||||
return [
|
||||
key,
|
||||
{
|
||||
value: newValue ? Binary.fromHex(newValue) : null,
|
||||
prev: prevValue ? Binary.fromHex(prevValue) : null,
|
||||
},
|
||||
] as const
|
||||
}),
|
||||
),
|
||||
})),
|
||||
)
|
||||
}),
|
||||
catchError((ex) => {
|
||||
console.error(ex)
|
||||
return [null]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -18,11 +18,15 @@ 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 forkliftLogoDark from "./icons/forklift_dark.svg"
|
||||
import forkliftLogoLight from "./icons/forklift_light.svg"
|
||||
import enumSvg from "./icons/enum.svg"
|
||||
import focusSvg from "./icons/focus.svg"
|
||||
import githubLightSvg from "./icons/github_light.svg"
|
||||
import githubDarkSvg from "./icons/github_dark.svg"
|
||||
import switchBinarySvg from "./icons/switch_binary.svg"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { selectedChain$ } from "@/state/chains/chain.state"
|
||||
|
||||
type CustomIconProps = Omit<Props, "ref" | "src"> & { size?: number }
|
||||
const CustomIcon: FC<
|
||||
@@ -64,6 +68,7 @@ export const Enum = customIcon(enumSvg)
|
||||
export const BinaryEdit = customIcon(binarySvg)
|
||||
export const SwitchBinary = customIcon(switchBinarySvg)
|
||||
export const Chopsticks = themeIcon(chopsticksLogoLight, chopsticksLogoDark)
|
||||
export const Forklift = themeIcon(forkliftLogoLight, forkliftLogoDark)
|
||||
export const GithubIcon = themeIcon(githubLightSvg, githubDarkSvg)
|
||||
|
||||
export const Spinner = (props: LucideProps) => (
|
||||
@@ -124,3 +129,19 @@ export const CopyBinaryIcon = ({ size = 16, ...props }: CustomIconProps) => (
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ForkMethodIcon: FC<{
|
||||
size?: number
|
||||
className?: string
|
||||
}> = ({ size, className }) => {
|
||||
const chain = useStateObservable(selectedChain$)
|
||||
const method = chain.forkMethod
|
||||
|
||||
const Icon =
|
||||
method === "chopsticks"
|
||||
? Chopsticks
|
||||
: method === "forklift"
|
||||
? Forklift
|
||||
: null
|
||||
return Icon ? <Icon size={size} className={className} /> : null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="110 110 800 800">
|
||||
<path
|
||||
fill="#fffd"
|
||||
fill-rule="evenodd"
|
||||
d="m201 259-8 24v230l-32 1-18 7-9 20v176l11 20 17 7 22-1-1-32 10-29 24-25 33-13 43 5 29 21 17 30 3 44h120l1-36 11-27 23-24 32-13 31 1 31 15 25 33 5 43h249l13-9 4-22-16-17-186-2V285l-8-13-55-3-14 15-1 345-26-18-1-98-91-241-15-23-27-13H235l-24 10Zm60 39 171-2 6 5 89 225-2 74h-95l-6-8v-30l-7-23-10-14-22-10-127-3Z"
|
||||
/>
|
||||
<circle cx="261.9" cy="723.5" r="65.3" fill="#fffd" />
|
||||
<circle cx="542.1" cy="723.5" r="65" fill="#fffd" />
|
||||
<rect
|
||||
width="149"
|
||||
height="151"
|
||||
x="709"
|
||||
y="510"
|
||||
fill="#e20093"
|
||||
rx="18"
|
||||
ry="18"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="110 110 800 800">
|
||||
<path
|
||||
fill="#000d"
|
||||
fill-rule="evenodd"
|
||||
d="m201 259-8 24v230l-32 1-18 7-9 20v176l11 20 17 7 22-1-1-32 10-29 24-25 33-13 43 5 29 21 17 30 3 44h120l1-36 11-27 23-24 32-13 31 1 31 15 25 33 5 43h249l13-9 4-22-16-17-186-2V285l-8-13-55-3-14 15-1 345-26-18-1-98-91-241-15-23-27-13H235l-24 10Zm60 39 171-2 6 5 89 225-2 74h-95l-6-8v-30l-7-23-10-14-22-10-127-3Z"
|
||||
/>
|
||||
<circle cx="261.9" cy="723.5" r="65.3" fill="#000d" />
|
||||
<circle cx="542.1" cy="723.5" r="65" fill="#000d" />
|
||||
<rect
|
||||
width="149"
|
||||
height="151"
|
||||
x="709"
|
||||
y="510"
|
||||
fill="#e20093"
|
||||
rx="18"
|
||||
ry="18"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
+1
-1
@@ -31,7 +31,7 @@ export const setHashParams = (
|
||||
location.hash = "#" + params.toString()
|
||||
}
|
||||
|
||||
const persistingKeys = ["networkId", "endpoint", "chopsticks"]
|
||||
const persistingKeys = ["networkId", "endpoint", "fork"]
|
||||
|
||||
const usePersistKeys = () => {
|
||||
const location = useLocation()
|
||||
|
||||
@@ -5,13 +5,18 @@ import { BlockInfo, blocksByHeight$, finalized$ } from "@/state/block.state"
|
||||
import { client$ } from "@/state/chains/chain.state"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { FC } from "react"
|
||||
import { combineLatest, debounceTime, map, switchMap } from "rxjs"
|
||||
import { combineLatest, debounceTime, map, repeat, switchMap } from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { BlockPopover } from "./BlockPopover"
|
||||
import * as Finalizing from "./FinalizingTable"
|
||||
|
||||
const best$ = client$.pipeState(
|
||||
switchMap((client) => client.bestBlocks$.pipe(map(([best]) => best))),
|
||||
switchMap((client) =>
|
||||
client.bestBlocks$.pipe(
|
||||
repeat(),
|
||||
map(([best]) => best),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
interface PositionedBlock {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Chopsticks } from "@/components/Icons"
|
||||
import { ForkMethodIcon } from "@/components/Icons"
|
||||
import { Loading } from "@/components/Loading"
|
||||
import { getHashParams } from "@/hashParams"
|
||||
import { groupBy } from "@/lib/groupBy"
|
||||
@@ -122,7 +122,7 @@ export const BlockBody: FC = () => {
|
||||
value="diff"
|
||||
>
|
||||
Diff
|
||||
<Chopsticks
|
||||
<ForkMethodIcon
|
||||
className="inline-block align-middle ml-2"
|
||||
size={20}
|
||||
/>
|
||||
|
||||
@@ -52,26 +52,26 @@ const blockDiff$ = state(
|
||||
|
||||
return Object.entries(block.diff)
|
||||
.map(
|
||||
([key, [prevValue, newValue]]): {
|
||||
([key, { value, prev }]): {
|
||||
key: HexString
|
||||
decodedKey: [string, ...unknown[]]
|
||||
prevValue: HexString | null
|
||||
prevValue: Uint8Array | null | undefined
|
||||
decodedPrevValue: unknown
|
||||
newValue: HexString | null
|
||||
newValue: Uint8Array | null
|
||||
decodedNewValue: unknown
|
||||
} | null => {
|
||||
try {
|
||||
if (wellKnownKeys[key]) {
|
||||
return {
|
||||
key,
|
||||
prevValue,
|
||||
newValue,
|
||||
prevValue: prev,
|
||||
newValue: value,
|
||||
decodedKey: [wellKnownKeys[key].name],
|
||||
decodedNewValue: newValue
|
||||
? wellKnownKeys[key].codec.dec(newValue)
|
||||
decodedNewValue: value
|
||||
? wellKnownKeys[key].codec.dec(value)
|
||||
: null,
|
||||
decodedPrevValue: prevValue
|
||||
? wellKnownKeys[key].codec.dec(prevValue)
|
||||
decodedPrevValue: prev
|
||||
? wellKnownKeys[key].codec.dec(prev)
|
||||
: null,
|
||||
}
|
||||
}
|
||||
@@ -86,18 +86,18 @@ const blockDiff$ = state(
|
||||
if (storageCodec) {
|
||||
return {
|
||||
key,
|
||||
prevValue,
|
||||
newValue,
|
||||
prevValue: prev,
|
||||
newValue: value,
|
||||
decodedKey: [
|
||||
pallet.name,
|
||||
entry,
|
||||
...storageCodec.keys.dec(key),
|
||||
],
|
||||
decodedNewValue: newValue
|
||||
? storageCodec.value.dec(newValue)
|
||||
decodedNewValue: value
|
||||
? storageCodec.value.dec(value)
|
||||
: null,
|
||||
decodedPrevValue: prevValue
|
||||
? storageCodec.value.dec(prevValue)
|
||||
decodedPrevValue: prev
|
||||
? storageCodec.value.dec(prev)
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Chopsticks } from "@/components/Icons"
|
||||
import { ForkMethodIcon } from "@/components/Icons"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
canProduceBlocks$,
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
} from "@/state/chains/chain.state"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { FC, PropsWithChildren, ReactElement, useEffect, useState } from "react"
|
||||
import { firstValueFrom, map, switchMap } from "rxjs"
|
||||
import { firstValueFrom, map, repeat, switchMap } from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { BlockTime } from "./BlockTime"
|
||||
import { EpochRemainingTime } from "./EpochTime"
|
||||
|
||||
const finalizedNum$ = client$.pipeState(
|
||||
switchMap((chainHead) => chainHead.finalizedBlock$),
|
||||
switchMap((chainHead) => chainHead.finalizedBlock$.pipe(repeat())),
|
||||
map((v) => v.number),
|
||||
)
|
||||
const finalized$ = finalizedNum$.pipeState(map((v) => v.toLocaleString()))
|
||||
@@ -77,14 +77,18 @@ const SummaryItem: FC<
|
||||
)
|
||||
}
|
||||
|
||||
const bestNum$ = client$.pipeState(
|
||||
switchMap((chainHead) => chainHead.bestBlocks$.pipe(repeat())),
|
||||
map((v) => v[0].number),
|
||||
)
|
||||
const Jump = () => {
|
||||
const finalized = useStateObservable(finalizedNum$)
|
||||
const [value, setValue] = useState(finalized + 1)
|
||||
const best = useStateObservable(bestNum$)
|
||||
const [value, setValue] = useState(best + 1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setValue((v) => Math.max(v, finalized + 1))
|
||||
}, [finalized])
|
||||
setValue((v) => Math.max(v, best + 1))
|
||||
}, [best])
|
||||
|
||||
return (
|
||||
<SummaryItem title="" className="bg-card/0 border-none text-center">
|
||||
@@ -107,7 +111,7 @@ const Jump = () => {
|
||||
const client = await firstValueFrom(client$)
|
||||
await client._request(
|
||||
"dev_newBlock",
|
||||
value === finalized + 1
|
||||
value === best + 1
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@@ -122,7 +126,7 @@ const Jump = () => {
|
||||
disabled={loading}
|
||||
>
|
||||
New Block{" "}
|
||||
<Chopsticks className="inline-block align-middle ml-1" size={20} />
|
||||
<ForkMethodIcon className="inline-block align-middle ml-1" size={20} />
|
||||
</Button>
|
||||
</SummaryItem>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { CopyText } from "@/components/Copy"
|
||||
import { Chopsticks, Spinner } from "@/components/Icons"
|
||||
import SliderToggle from "@/components/Toggle"
|
||||
import {
|
||||
Chopsticks,
|
||||
Forklift,
|
||||
ForkMethodIcon,
|
||||
Spinner,
|
||||
} from "@/components/Icons"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -24,6 +28,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import {
|
||||
AUTO_RPC_ENDPOINT,
|
||||
currentWsStatus$,
|
||||
ForkMethod,
|
||||
LIGHT_CLIENT_ENDPOINT,
|
||||
Network,
|
||||
networkCategories,
|
||||
@@ -56,9 +61,7 @@ export function NetworkSwitcher({ className }: { className?: string }) {
|
||||
>
|
||||
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-left flex items-center gap-1">
|
||||
{getChainLabel(selectedChain)}
|
||||
{selectedChain.withChopsticks ? (
|
||||
<Chopsticks className="inline-block align-text-top" />
|
||||
) : null}
|
||||
<ForkMethodIcon className="inline-block align-text-top" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{getConnectionLabel(selectedChain, websocketStatus)}
|
||||
</span>
|
||||
@@ -67,7 +70,7 @@ export function NetworkSwitcher({ className }: { className?: string }) {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<NetworkSwitchDialogContent
|
||||
key={`${selectedChain.network.id}-${selectedChain.endpoint}`}
|
||||
key={`${selectedChain.network.id}-${selectedChain.endpoint}-${selectedChain.forkMethod}`}
|
||||
selectedChain={selectedChain}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
@@ -82,16 +85,14 @@ const NetworkSwitchDialogContent: FC<{
|
||||
const [query, setQuery] = useState("")
|
||||
const [network, setNetwork] = useState(selectedChain.network)
|
||||
const [endpoint, setEndpoint] = useState(selectedChain.endpoint)
|
||||
const [withChopsticks, setWithChopsticks] = useState(
|
||||
selectedChain.withChopsticks,
|
||||
)
|
||||
const [forkMethod, setForkMethod] = useState(selectedChain.forkMethod)
|
||||
const customUrl = normalizeWsUrl(query)
|
||||
const canFork = endpoint !== LIGHT_CLIENT_ENDPOINT
|
||||
const forked = canFork && withChopsticks
|
||||
const selectedForkMethod = canFork ? forkMethod : "none"
|
||||
const hasChanged =
|
||||
network.id !== selectedChain.network.id ||
|
||||
endpoint !== selectedChain.endpoint ||
|
||||
forked !== selectedChain.withChopsticks
|
||||
selectedForkMethod !== selectedChain.forkMethod
|
||||
|
||||
const selectNetwork = (
|
||||
next: Network,
|
||||
@@ -100,7 +101,7 @@ const NetworkSwitchDialogContent: FC<{
|
||||
setNetwork(next)
|
||||
setEndpoint(nextEndpoint)
|
||||
if (nextEndpoint === LIGHT_CLIENT_ENDPOINT) {
|
||||
setWithChopsticks(false)
|
||||
setForkMethod("none")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,13 +113,12 @@ const NetworkSwitchDialogContent: FC<{
|
||||
const setConnection = (nextEndpoint: string) => {
|
||||
setEndpoint(nextEndpoint)
|
||||
if (nextEndpoint === LIGHT_CLIENT_ENDPOINT) {
|
||||
setWithChopsticks(false)
|
||||
setForkMethod("none")
|
||||
}
|
||||
}
|
||||
|
||||
const toggleChopsticks = () => {
|
||||
if (endpoint === LIGHT_CLIENT_ENDPOINT) return
|
||||
setWithChopsticks(!withChopsticks)
|
||||
const toggleForkMethod = (method: ForkMethod) => {
|
||||
setForkMethod((current) => (current === method ? "none" : method))
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
@@ -134,7 +134,7 @@ const NetworkSwitchDialogContent: FC<{
|
||||
onChangeChain({
|
||||
network: localNetwork,
|
||||
endpoint,
|
||||
withChopsticks: forked,
|
||||
forkMethod: selectedForkMethod,
|
||||
})
|
||||
onClose()
|
||||
return
|
||||
@@ -144,10 +144,10 @@ const NetworkSwitchDialogContent: FC<{
|
||||
onChangeChain({
|
||||
network: getCustomNetwork(),
|
||||
endpoint,
|
||||
withChopsticks: forked,
|
||||
forkMethod: selectedForkMethod,
|
||||
})
|
||||
} else {
|
||||
onChangeChain({ network, endpoint, withChopsticks: forked })
|
||||
onChangeChain({ network, endpoint, forkMethod: selectedForkMethod })
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
@@ -187,16 +187,26 @@ const NetworkSwitchDialogContent: FC<{
|
||||
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-t pt-3">
|
||||
{canFork ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Chopsticks size={18} />
|
||||
<Label htmlFor="use-chopsticks" className="cursor-pointer">
|
||||
Fork with Chopsticks
|
||||
</Label>
|
||||
<SliderToggle
|
||||
id="use-chopsticks"
|
||||
isToggled={forked}
|
||||
toggle={toggleChopsticks}
|
||||
/>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Fork with
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<ForkMethodOption
|
||||
value="chopsticks"
|
||||
title="Chopsticks"
|
||||
selected={selectedForkMethod === "chopsticks"}
|
||||
icon={<Chopsticks size={18} />}
|
||||
onSelect={toggleForkMethod}
|
||||
/>
|
||||
<ForkMethodOption
|
||||
value="forklift"
|
||||
title="Forklift"
|
||||
selected={selectedForkMethod === "forklift"}
|
||||
icon={<Forklift size={18} />}
|
||||
onSelect={toggleForkMethod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
@@ -217,6 +227,27 @@ const NetworkSwitchDialogContent: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const ForkMethodOption: FC<{
|
||||
value: Exclude<ForkMethod, "none">
|
||||
title: string
|
||||
selected: boolean
|
||||
icon?: React.ReactNode
|
||||
onSelect: (value: ForkMethod) => void
|
||||
}> = ({ value, title, selected, icon, onSelect }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"flex items-center gap-1.5 rounded-md border border-transparent px-2 py-1.5 text-sm",
|
||||
selected && "border-polkadot bg-polkadot/5",
|
||||
)}
|
||||
aria-pressed={selected}
|
||||
onClick={() => onSelect(value)}
|
||||
>
|
||||
{icon}
|
||||
{title}
|
||||
</button>
|
||||
)
|
||||
|
||||
const NetworkList: FC<{
|
||||
query: string
|
||||
selectedNetwork: Network
|
||||
@@ -236,9 +267,13 @@ const NetworkList: FC<{
|
||||
() => networkCategories.filter((category) => category.name !== "Custom"),
|
||||
[],
|
||||
)
|
||||
const selectedValue = getNetworkItemValue(categories, selectedNetwork)
|
||||
|
||||
return (
|
||||
<Command className="flex min-h-0 flex-1 basis-0 flex-col overflow-hidden rounded-md border bg-transparent">
|
||||
<Command
|
||||
defaultValue={selectedValue}
|
||||
className="flex min-h-0 flex-1 basis-0 flex-col overflow-hidden rounded-md border bg-transparent"
|
||||
>
|
||||
<CommandInput
|
||||
value={query}
|
||||
onValueChange={onQueryChange}
|
||||
@@ -297,6 +332,20 @@ const NetworkItem: FC<{
|
||||
</CommandItem>
|
||||
)
|
||||
|
||||
const getNetworkItemValue = (
|
||||
categories: Array<{ name: string; networks: Network[] }>,
|
||||
network: Network,
|
||||
) => {
|
||||
if (network.id === "custom") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const category = categories.find((category) =>
|
||||
category.networks.some((x) => x.id === network.id),
|
||||
)
|
||||
return category ? networkSearchValue(category.name, network) : undefined
|
||||
}
|
||||
|
||||
const ConnectionList: FC<{
|
||||
network: Network
|
||||
endpoint: string
|
||||
@@ -524,10 +573,10 @@ const getConnectionLabel = (
|
||||
selectedChain: SelectedChain,
|
||||
websocketStatus: StatusChange | null,
|
||||
) => {
|
||||
const { network, endpoint, withChopsticks } = selectedChain
|
||||
const { network, endpoint, forkMethod } = selectedChain
|
||||
if (endpoint === LIGHT_CLIENT_ENDPOINT) return "Smoldot"
|
||||
if (endpoint === AUTO_RPC_ENDPOINT) {
|
||||
if (withChopsticks) return null
|
||||
if (forkMethod !== "none") return null
|
||||
if (!websocketStatus) return <Spinner className="inline-block" />
|
||||
|
||||
const isReady = "uri" in websocketStatus
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { Chopsticks } from "@/components/Icons"
|
||||
import { ForkMethodIcon } from "@/components/Icons"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { createState } from "@/lib/externalState"
|
||||
@@ -7,11 +7,11 @@ import { canSetStorage$ } from "@/state/chains/chain.state"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { FC } from "react"
|
||||
import { Route, Routes } from "react-router-dom"
|
||||
import { CenteredScrollContainer } from "../AppShell"
|
||||
import { StorageDecode } from "./StorageDecode"
|
||||
import { StorageQuery } from "./StorageQuery"
|
||||
import { StorageSet } from "./StorageSet"
|
||||
import { StorageSubscriptions } from "./StorageSubscriptions"
|
||||
import { CenteredScrollContainer } from "../AppShell"
|
||||
|
||||
export const Storage = withSubscribe(
|
||||
() => (
|
||||
@@ -54,7 +54,7 @@ const StorageEntry: FC = () => {
|
||||
content: (
|
||||
<>
|
||||
Set Value
|
||||
<Chopsticks
|
||||
<ForkMethodIcon
|
||||
className="inline-block align-middle ml-2"
|
||||
size={20}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LookupTypeEdit } from "@/codec-components/LookupTypeEdit"
|
||||
import { Chopsticks } from "@/components/Icons"
|
||||
import { ForkMethodIcon } from "@/components/Icons"
|
||||
import { MetadataEntryInput } from "@/components/MetadataEntryInput"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { chainClient$, client$, lookup$ } from "@/state/chains/chain.state"
|
||||
import { getTypeComplexity } from "@/utils"
|
||||
@@ -71,6 +72,13 @@ export const StorageSet: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-start w-full overflow-hidden">
|
||||
<MetadataEntryInput
|
||||
state={storageEntryState}
|
||||
labels={{
|
||||
group: "Pallet",
|
||||
item: "Entry",
|
||||
}}
|
||||
/>
|
||||
<KeyInput />
|
||||
<StorageKeysInput disableToggle />
|
||||
{currentValue && currentValue.encodedKey ? (
|
||||
@@ -112,7 +120,7 @@ export const StorageSet: FC = () => {
|
||||
}}
|
||||
>
|
||||
Set Storage
|
||||
<Chopsticks />
|
||||
<ForkMethodIcon />
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
+22
-47
@@ -1,5 +1,6 @@
|
||||
import { chopsticksInstance$ } from "@/chopsticks/chopsticks"
|
||||
import { chainClient$, client$ } from "@/state/chains/chain.state"
|
||||
import { getChopsticksBlockDiff$ } from "@/chopsticks/chopsticks"
|
||||
import { client$, selectedChain$ } from "@/state/chains/chain.state"
|
||||
import { getForkliftBlockDiff$ } from "@/state/forklift"
|
||||
import { SystemEvent } from "@polkadot-api/observable-client"
|
||||
import { blockHeader, SizedHex } from "@polkadot-api/substrate-bindings"
|
||||
import { liftSuspense, state, SUSPENSE } from "@react-rxjs/core"
|
||||
@@ -37,8 +38,6 @@ import {
|
||||
take,
|
||||
takeWhile,
|
||||
tap,
|
||||
toArray,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
|
||||
export const finalized$ = client$.pipeState(
|
||||
@@ -52,6 +51,13 @@ export enum BlockState {
|
||||
Pruned = "pruned",
|
||||
Unknown = "unknown",
|
||||
}
|
||||
export type BlockDiff = Record<
|
||||
string,
|
||||
{
|
||||
value: Uint8Array | null
|
||||
prev: Uint8Array | null
|
||||
}
|
||||
>
|
||||
export interface BlockInfo {
|
||||
hash: string
|
||||
parent: string
|
||||
@@ -60,7 +66,7 @@ export interface BlockInfo {
|
||||
events: SystemEvent[] | null
|
||||
header: BlockHeader | null
|
||||
status: BlockState
|
||||
diff: Record<string, [string | null, string | null]> | null
|
||||
diff: BlockDiff | null
|
||||
}
|
||||
export const BlockContext = createContext<BlockInfo | null>(null)
|
||||
|
||||
@@ -83,7 +89,9 @@ finalizedBlocks$.pipe(liftSuspense(), retry()).subscribe()
|
||||
const MAX_HEIGHT = 600
|
||||
const sharedBlocks$ = client$.pipe(
|
||||
liftSuspense(),
|
||||
switchMap((client) => (client === SUSPENSE ? [] : client.blocks$)),
|
||||
switchMap((client) =>
|
||||
client === SUSPENSE ? [] : client.blocks$.pipe(repeat()),
|
||||
),
|
||||
share(),
|
||||
)
|
||||
export const [blockInfo$, recordedBlocks$] = partitionByKey(
|
||||
@@ -428,47 +436,14 @@ const getBlockStatus$ = (
|
||||
const getBlockDiff$ = (
|
||||
parent: string,
|
||||
hash: string,
|
||||
): Observable<Record<string, [string | null, string | null]> | null> =>
|
||||
chopsticksInstance$.pipe(
|
||||
): Observable<BlockDiff | null> =>
|
||||
selectedChain$.pipe(
|
||||
take(1),
|
||||
switchMap((chain) => (chain ? chain.getBlock(hash as any) : [null])),
|
||||
switchMap((block) => (block ? block.storageDiff() : [null])),
|
||||
map((v) =>
|
||||
v && Object.keys(v).length > 0
|
||||
? (v as Record<string, string | null>)
|
||||
: null,
|
||||
switchMap(({ forkMethod }) =>
|
||||
forkMethod === "chopsticks"
|
||||
? getChopsticksBlockDiff$(parent, hash)
|
||||
: forkMethod === "forklift"
|
||||
? getForkliftBlockDiff$(parent, hash)
|
||||
: [null],
|
||||
),
|
||||
startWith(null),
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([diff, { chainHead }]) => {
|
||||
if (!diff) return [null]
|
||||
|
||||
return chainHead
|
||||
.storageQueries$(
|
||||
parent,
|
||||
Object.keys(diff).map((key) => ({
|
||||
key,
|
||||
type: "value",
|
||||
})),
|
||||
)
|
||||
.pipe(
|
||||
toArray(),
|
||||
map((v) => Object.fromEntries(v.map((v) => [v.key, v.value]))),
|
||||
map(
|
||||
(previousResults): Record<string, [string | null, string | null]> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(diff)
|
||||
.map(([key, newValue]) => [
|
||||
key,
|
||||
[previousResults[key] ?? null, newValue],
|
||||
])
|
||||
.filter(([, [prevVal, newVal]]) => prevVal !== newVal),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
catchError((ex) => {
|
||||
console.error(ex)
|
||||
return [null]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createChopsticksProvider } from "@/chopsticks/chopsticks"
|
||||
import { createForkliftProvider } from "@/state/forklift"
|
||||
import { getHashParams, setHashParams } from "@/hashParams"
|
||||
import { DotAh } from "@polkadot-api/descriptors"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
@@ -68,23 +69,24 @@ import {
|
||||
export type ChainSource = WebsocketSource | SmoldotSource
|
||||
export const LIGHT_CLIENT_ENDPOINT = "light-client"
|
||||
export const AUTO_RPC_ENDPOINT = "auto-rpc"
|
||||
export type ForkMethod = "none" | "chopsticks" | "forklift"
|
||||
|
||||
export type SelectedChain = {
|
||||
network: Network
|
||||
endpoint: string
|
||||
withChopsticks: boolean
|
||||
forkMethod: ForkMethod
|
||||
}
|
||||
export const getChainSource = ({
|
||||
endpoint,
|
||||
network,
|
||||
withChopsticks,
|
||||
forkMethod,
|
||||
}: SelectedChain) =>
|
||||
endpoint === LIGHT_CLIENT_ENDPOINT
|
||||
? createSmoldotSource(network.id, network.relayChain)
|
||||
: createWebsocketSource(
|
||||
network.id,
|
||||
endpoint === AUTO_RPC_ENDPOINT ? shuffleEndpoints(network) : endpoint,
|
||||
withChopsticks,
|
||||
forkMethod,
|
||||
)
|
||||
const shuffleEndpoints = (network: Network) =>
|
||||
Object.values(network.endpoints)
|
||||
@@ -101,9 +103,11 @@ console.log("You can enable JSON-RPC logs by calling `setRpcLogsEnabled(true)`")
|
||||
export const getProvider = (source: ChainSource) => {
|
||||
const provider =
|
||||
source.type === "websocket"
|
||||
? source.withChopsticks
|
||||
? source.forkMethod === "chopsticks"
|
||||
? createChopsticksProvider(source.endpoint)
|
||||
: getWebsocketProvider(source)
|
||||
: source.forkMethod === "forklift"
|
||||
? createForkliftProvider(source.endpoint)
|
||||
: getWebsocketProvider(source)
|
||||
: getSmoldotProvider(source)
|
||||
|
||||
const recorder = withLogsRecorder((msg) => {
|
||||
@@ -118,11 +122,11 @@ export const getProvider = (source: ChainSource) => {
|
||||
|
||||
export const [selectedChainChanged$, onChangeChain] =
|
||||
createSignal<SelectedChain>()
|
||||
selectedChainChanged$.subscribe(({ network, endpoint, withChopsticks }) =>
|
||||
selectedChainChanged$.subscribe(({ network, endpoint, forkMethod }) =>
|
||||
setHashParams({
|
||||
networkId: network.id,
|
||||
endpoint,
|
||||
chopsticks: withChopsticks ? "true" : null,
|
||||
fork: forkMethod === "none" ? null : forkMethod,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -142,16 +146,14 @@ export const isValidUri = (input: string): boolean => {
|
||||
const defaultSelectedChain: SelectedChain = {
|
||||
network: defaultNetwork,
|
||||
endpoint: LIGHT_CLIENT_ENDPOINT,
|
||||
withChopsticks: false,
|
||||
forkMethod: "none",
|
||||
}
|
||||
const getDefaultChain = (): SelectedChain => {
|
||||
const hashParams = getHashParams()
|
||||
if (hashParams.has("networkId") && hashParams.has("endpoint")) {
|
||||
const networkId = hashParams.get("networkId")!
|
||||
const endpoint = hashParams.get("endpoint")!
|
||||
const withChopsticks =
|
||||
hashParams.get("chopsticks") === "true" &&
|
||||
endpoint !== LIGHT_CLIENT_ENDPOINT
|
||||
const forkMethod = getForkMethod(hashParams, endpoint)
|
||||
|
||||
if (networkId === "custom") {
|
||||
if (!isValidUri(endpoint)) return defaultSelectedChain
|
||||
@@ -159,15 +161,27 @@ const getDefaultChain = (): SelectedChain => {
|
||||
return {
|
||||
network: getCustomNetwork(),
|
||||
endpoint,
|
||||
withChopsticks,
|
||||
forkMethod,
|
||||
}
|
||||
}
|
||||
const network = findNetwork(networkId)
|
||||
if (network) return { network, endpoint, withChopsticks }
|
||||
if (network) return { network, endpoint, forkMethod }
|
||||
}
|
||||
|
||||
return defaultSelectedChain
|
||||
}
|
||||
|
||||
const getForkMethod = (
|
||||
hashParams: URLSearchParams,
|
||||
endpoint: string,
|
||||
): ForkMethod => {
|
||||
if (endpoint === LIGHT_CLIENT_ENDPOINT) return "none"
|
||||
|
||||
const fork = hashParams.get("fork")
|
||||
if (fork === "chopsticks" || fork === "forklift") return fork
|
||||
|
||||
return "none"
|
||||
}
|
||||
export const selectedChain$ = state<SelectedChain>(
|
||||
selectedChainChanged$,
|
||||
getDefaultChain(),
|
||||
|
||||
@@ -5,15 +5,15 @@ export interface WebsocketSource {
|
||||
type: "websocket"
|
||||
id: string
|
||||
endpoint: string | string[]
|
||||
withChopsticks: boolean
|
||||
forkMethod: "none" | "chopsticks" | "forklift"
|
||||
}
|
||||
|
||||
export async function createWebsocketSource(
|
||||
id: string,
|
||||
endpoint: string | string[],
|
||||
withChopsticks: boolean,
|
||||
forkMethod: WebsocketSource["forkMethod"],
|
||||
): Promise<WebsocketSource> {
|
||||
return { type: "websocket", id, endpoint, withChopsticks }
|
||||
return { type: "websocket", id, endpoint, forkMethod }
|
||||
}
|
||||
|
||||
export type WsStatusJsonRpcProvider = WsJsonRpcProvider & {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
forklift,
|
||||
Forklift,
|
||||
fromWorker,
|
||||
wsSource,
|
||||
} from "@polkadot-api/forklift"
|
||||
import Worker from "@polkadot-api/forklift/worker?worker"
|
||||
import { Binary, type JsonRpcProvider } from "polkadot-api"
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
map,
|
||||
Observable,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
toArray,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import type { BlockDiff } from "./block.state"
|
||||
import { chainClient$ } from "./chains/chain.state"
|
||||
|
||||
const forkliftInstance$ = new BehaviorSubject<Forklift | null>(null)
|
||||
|
||||
export const createForkliftProvider = (
|
||||
endpoint: string | string[],
|
||||
): JsonRpcProvider => {
|
||||
return (onMsg) => {
|
||||
const worker = new Worker()
|
||||
const instance = forklift(wsSource(endpoint), {
|
||||
mockSignatureHost: true,
|
||||
executor: fromWorker(worker),
|
||||
})
|
||||
forkliftInstance$.next(instance)
|
||||
const connection = instance.serve(onMsg)
|
||||
|
||||
return {
|
||||
send: connection.send,
|
||||
disconnect() {
|
||||
forkliftInstance$.next(null)
|
||||
connection.disconnect()
|
||||
instance.destroy()
|
||||
worker.terminate()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getForkliftBlockDiff$ = (
|
||||
parent: string,
|
||||
hash: string,
|
||||
): Observable<BlockDiff | null> =>
|
||||
forkliftInstance$.pipe(
|
||||
take(1),
|
||||
switchMap((chain) => (chain ? chain.getStorageDiff(hash) : [null])),
|
||||
startWith(null),
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([diff, { chainHead }]) => {
|
||||
if (!diff) return [null]
|
||||
|
||||
const missingPrevValues = Object.entries(diff).filter(
|
||||
([_, { prev }]) => prev === undefined,
|
||||
)
|
||||
if (!missingPrevValues.length) return [diff as BlockDiff]
|
||||
|
||||
return chainHead
|
||||
.storageQueries$(
|
||||
parent,
|
||||
missingPrevValues.map(([key]) => ({ key, type: "value" })),
|
||||
)
|
||||
.pipe(
|
||||
toArray(),
|
||||
catchError(() => [
|
||||
missingPrevValues.map(([key]) => ({ key, value: null })),
|
||||
]),
|
||||
map((v) => ({
|
||||
...(diff as BlockDiff),
|
||||
...Object.fromEntries(
|
||||
v.map((v) => [
|
||||
v.key,
|
||||
{
|
||||
value: diff[v.key].value,
|
||||
prev: v.value == null ? null : Binary.fromHex(v.value),
|
||||
},
|
||||
]),
|
||||
),
|
||||
})),
|
||||
)
|
||||
}),
|
||||
catchError((ex) => {
|
||||
console.error(ex)
|
||||
return [null]
|
||||
}),
|
||||
)
|
||||
@@ -95,7 +95,7 @@ const proxyProvider = createProxyProvider((address) =>
|
||||
const multisigProvider$ = selectedChain$.pipe(
|
||||
switchMap(getChainSource),
|
||||
map((source) => {
|
||||
if (source.type === "websocket" && source.withChopsticks) {
|
||||
if (source.type === "websocket" && source.forkMethod !== "none") {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user