runtime query at block, populate args with selected query (#148)
* refactor: abstract metadata entry input * runtime query at block, populate args with selected query * fix infinte update rerender, parameters not loading up * exclude storage-less pallets from storage
This commit is contained in:
161
src/components/MetadataEntryInput.tsx
Normal file
161
src/components/MetadataEntryInput.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { BlockPicker, selectedBlock$ } from "@/pages/Storage/BlockPicker"
|
||||
import { CachedRuntime } from "@/state/chains/chain.state"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { createSignal, mergeWithKey } from "@react-rxjs/utils"
|
||||
import { Info } from "lucide-react"
|
||||
import { FC } from "react"
|
||||
import { combineLatest, defer, map, scan } from "rxjs"
|
||||
import { DocsRenderer } from "./DocsRenderer"
|
||||
import { Popover } from "./Popover"
|
||||
import { SearchableSelect } from "./Select"
|
||||
|
||||
export const createMetadataEntryState = <T extends { docs?: string[] }>(
|
||||
getEntries: (ctx: CachedRuntime) => Record<string, string[]>,
|
||||
initialValue: (entries: Record<string, string[]>) => {
|
||||
group: string | null
|
||||
item: string | null
|
||||
},
|
||||
getEntry: (ctx: CachedRuntime, entry: { group: string; item: string }) => T,
|
||||
) => {
|
||||
const [entryChange$, selectEntry] = createSignal<{
|
||||
group?: string | null
|
||||
item?: string | null
|
||||
}>()
|
||||
|
||||
const groupEntries$ = state(
|
||||
selectedBlock$.pipe(map(({ ctx }) => getEntries(ctx))),
|
||||
{},
|
||||
)
|
||||
|
||||
const emptyEntry = {
|
||||
group: null as string | null,
|
||||
item: null as string | null,
|
||||
}
|
||||
const initialValue$ = groupEntries$.pipe(map(initialValue))
|
||||
const partialSelection$ = defer(() =>
|
||||
mergeWithKey({ entryChange$, initialValue$ }).pipe(
|
||||
scan((acc, evt) => {
|
||||
if (evt.type === "initialValue$") {
|
||||
if (!acc.group) return evt.payload
|
||||
return acc
|
||||
}
|
||||
return {
|
||||
group: evt.payload.group ?? acc.group,
|
||||
item: evt.payload.item ?? acc.item,
|
||||
}
|
||||
}, emptyEntry),
|
||||
),
|
||||
)
|
||||
|
||||
const partialEntry$ = state(
|
||||
combineLatest([partialSelection$, groupEntries$]).pipe(
|
||||
map(([partialSelection, entries]) => {
|
||||
const result = { ...partialSelection }
|
||||
|
||||
let selectedGroup = result.group ? entries[result.group] : null
|
||||
if (!selectedGroup) {
|
||||
result.group = Object.keys(entries)[0] ?? null
|
||||
if (!result.group) return emptyEntry
|
||||
|
||||
selectedGroup = entries[result.group] ?? null
|
||||
}
|
||||
if (!result.item || !selectedGroup.includes(result.item)) {
|
||||
result.item = selectedGroup?.[0] ?? null
|
||||
}
|
||||
return result
|
||||
}),
|
||||
),
|
||||
emptyEntry,
|
||||
)
|
||||
|
||||
const selectedEntry$ = state(
|
||||
combineLatest([partialEntry$, selectedBlock$]).pipe(
|
||||
map(([partialEntry, { ctx }]): T | null => {
|
||||
const { group, item } = partialEntry
|
||||
if (!group || !item) return null
|
||||
const entries = getEntries(ctx)
|
||||
if (!entries[group]?.includes(item)) return null
|
||||
|
||||
return getEntry(ctx, { group, item })
|
||||
}),
|
||||
),
|
||||
null,
|
||||
)
|
||||
|
||||
return {
|
||||
selectEntry,
|
||||
groupEntries$,
|
||||
partialEntry$,
|
||||
selectedEntry$,
|
||||
}
|
||||
}
|
||||
export type MetadataEntryState = ReturnType<typeof createMetadataEntryState>
|
||||
|
||||
export const MetadataEntryInput: FC<{
|
||||
state: MetadataEntryState
|
||||
labels: {
|
||||
group: string
|
||||
item: string
|
||||
}
|
||||
}> = ({ state, labels }) => {
|
||||
const entries = useStateObservable(state.groupEntries$)
|
||||
const partialEntry = useStateObservable(state.partialEntry$)
|
||||
const selectedEntry = useStateObservable(state.selectedEntry$)
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-2 @3xl:grid-cols-3 max-w-2xl">
|
||||
<label>
|
||||
Block
|
||||
<BlockPicker />
|
||||
</label>
|
||||
<label>
|
||||
{labels.group}
|
||||
<SearchableSelect
|
||||
value={partialEntry.group}
|
||||
setValue={(v) => state.selectEntry({ group: v })}
|
||||
options={Object.keys(entries).map((e) => ({
|
||||
text: e,
|
||||
value: e,
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
{partialEntry.group && entries[partialEntry.group] && (
|
||||
<label className="max-w-52">
|
||||
<div className="flex items-center justify-between">
|
||||
{labels.item}
|
||||
{selectedEntry?.docs?.length ? (
|
||||
<Popover
|
||||
content={
|
||||
<DocsRenderer
|
||||
docs={selectedEntry.docs}
|
||||
className="max-h-none"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<Info size={16} />
|
||||
</button>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
<SearchableSelect
|
||||
value={partialEntry.item}
|
||||
setValue={(v) => state.selectEntry({ item: v })}
|
||||
options={
|
||||
entries[partialEntry.group].map((s) => ({
|
||||
text: s,
|
||||
value: s,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { Circle, Dot } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
@@ -21,10 +22,14 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { addRuntimeCallQuery, selectedEntry$ } from "./runtimeCalls.state"
|
||||
import { selectedBlock$ } from "../Storage/BlockPicker"
|
||||
import {
|
||||
addRuntimeCallQuery,
|
||||
runtimeCallEntryState,
|
||||
} from "./runtimeCalls.state"
|
||||
|
||||
export const RuntimeCallQuery: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(runtimeCallEntryState.selectedEntry$)
|
||||
const isReady = useStateObservable(isReady$)
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -33,10 +38,27 @@ export const RuntimeCallQuery: FC = () => {
|
||||
const submit = async () => {
|
||||
const [entry, inputValues, builder, block] = await firstValueFrom(
|
||||
combineLatest([
|
||||
selectedEntry$,
|
||||
runtimeCallEntryState.selectedEntry$,
|
||||
inputValues$,
|
||||
dynamicBuilder$,
|
||||
client$.pipe(switchMap((client) => client.finalizedBlock$)),
|
||||
selectedBlock$.pipe(
|
||||
switchMap((block) =>
|
||||
block.hash
|
||||
? [
|
||||
{
|
||||
latest: false,
|
||||
hash: block.hash,
|
||||
},
|
||||
]
|
||||
: client$.pipe(
|
||||
switchMap((client) => client.finalizedBlock$),
|
||||
map((v) => ({
|
||||
latest: true,
|
||||
hash: v.hash,
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
)
|
||||
const decodedValues = inputValues.map((v, i) =>
|
||||
@@ -46,6 +68,7 @@ export const RuntimeCallQuery: FC = () => {
|
||||
)
|
||||
|
||||
const id = await addRuntimeCallQuery({
|
||||
latestBlock: block.latest,
|
||||
blockHash: block.hash,
|
||||
api: entry!.api,
|
||||
method: entry!.name,
|
||||
@@ -64,13 +87,16 @@ export const RuntimeCallQuery: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const [inputValueChange$, setInputValue] = createSignal<{
|
||||
export const [inputValueChange$, setInputValue] = createSignal<{
|
||||
idx: number
|
||||
value: Uint8Array | "partial" | null
|
||||
}>()
|
||||
const inputValues$ = selectedEntry$.pipeState(
|
||||
const inputValues$ = runtimeCallEntryState.selectedEntry$.pipeState(
|
||||
filter((v) => !!v),
|
||||
map((v) => v.inputs),
|
||||
distinctUntilChanged(
|
||||
(a, b) => a.length === b.length && a.every((v, i) => b[i].type === v.type),
|
||||
),
|
||||
switchMap((inputs) => {
|
||||
const values: Array<Uint8Array | "partial" | null> = inputs.map(() => null)
|
||||
return inputValueChange$.pipe(
|
||||
@@ -91,7 +117,7 @@ const isReady$ = inputValues$.pipeState(
|
||||
)
|
||||
|
||||
const RuntimeInputValues: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(runtimeCallEntryState.selectedEntry$)
|
||||
if (!selectedEntry || !selectedEntry.inputs.length) return null
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,13 +3,18 @@ import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { JsonDisplay } from "@/components/JsonDisplay"
|
||||
import { workspaceEntryCtxOrAdd$ } from "@/components/Workspace"
|
||||
import { runtimeCtx$ } from "@/state/chains/chain.state"
|
||||
import { shortStr } from "@/utils"
|
||||
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { FC, useMemo, useState } from "react"
|
||||
import { FC, useEffect, useMemo, useState } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { filter, firstValueFrom } from "rxjs"
|
||||
import { setBlockHashValue } from "../Storage/BlockPicker"
|
||||
import { ValueDisplay } from "../Storage/StorageSubscriptions"
|
||||
import { setInputValue } from "./RuntimeCallQuery"
|
||||
import { RuntimeCallWorkspaceContext } from "./RuntimeCallWorkspaceEntry"
|
||||
import {
|
||||
idToRuntimeQuery,
|
||||
runtimeCallEntryState,
|
||||
runtimeCallToWorkspaceEntry,
|
||||
} from "./runtimeCalls.state"
|
||||
|
||||
@@ -39,6 +44,7 @@ const runtimeCallCtx$ = state(
|
||||
|
||||
const RuntimeCallResultBox: FC<{ id: string }> = ({ id }) => {
|
||||
const context = useStateObservable(runtimeCallCtx$(id))
|
||||
useSynchronizeInputs(id)
|
||||
|
||||
return context ? <RuntimeCallResultContent id={id} context={context} /> : null
|
||||
}
|
||||
@@ -56,6 +62,10 @@ const RuntimeCallResultContent: FC<{
|
||||
{context.api}.{context.method}
|
||||
</h3>
|
||||
<div className="flex items-center shrink-0 gap-2">
|
||||
<div className="text-xs text-center">
|
||||
<p>Block</p>
|
||||
<p>{shortStr(context.blockHash, 6)}</p>
|
||||
</div>
|
||||
<ButtonGroup
|
||||
value={mode}
|
||||
onValueChange={setMode as any}
|
||||
@@ -125,3 +135,36 @@ const ResultDisplay: FC<{
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useSynchronizeInputs = (id: string) => {
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const run = async () => {
|
||||
const params = await idToRuntimeQuery(id)
|
||||
if (cancelled) return
|
||||
setBlockHashValue(params.latestBlock ? "Latest" : params.blockHash)
|
||||
runtimeCallEntryState.selectEntry({
|
||||
group: params.api,
|
||||
item: params.method,
|
||||
})
|
||||
// Let entry settle
|
||||
await firstValueFrom(
|
||||
runtimeCallEntryState.selectedEntry$.pipe(
|
||||
filter(
|
||||
(v) => !!v && v.api === params.api && v.name === params.method,
|
||||
),
|
||||
),
|
||||
)
|
||||
if (cancelled) return
|
||||
const encodedArgs = params.args.map((arg, i) =>
|
||||
params.codec.inner[i].enc(arg),
|
||||
)
|
||||
encodedArgs.forEach((value, idx) => setInputValue({ idx, value }))
|
||||
}
|
||||
run()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [id])
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FC } from "react"
|
||||
import type { RuntimeCallResult } from "./runtimeCalls.state"
|
||||
|
||||
export type RuntimeCallWorkspaceContext = {
|
||||
blockHash: string
|
||||
api: string
|
||||
method: string
|
||||
result$: DefaultedStateObservable<RuntimeCallResult | null>
|
||||
|
||||
@@ -1,105 +1,28 @@
|
||||
import { lookup$ } from "@/state/chains/chain.state"
|
||||
import { DocsRenderer } from "@/components/DocsRenderer"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { MetadataEntryInput } from "@/components/MetadataEntryInput"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Route, Routes } from "react-router-dom"
|
||||
import { map } from "rxjs"
|
||||
import { CenteredScrollContainer } from "../AppShell"
|
||||
import { RuntimeCallQuery } from "./RuntimeCallQuery"
|
||||
import { RuntimeCallResults } from "./RuntimeCallResults"
|
||||
import { selectedEntry$, setSelectedMethod } from "./runtimeCalls.state"
|
||||
import { CenteredScrollContainer } from "../AppShell"
|
||||
|
||||
const metadataRuntimeCalls$ = state(
|
||||
lookup$.pipe(
|
||||
map((lookup) => ({
|
||||
lookup,
|
||||
entries: Object.fromEntries(
|
||||
lookup.metadata.apis.map((p) => [
|
||||
p.name,
|
||||
Object.fromEntries(p.methods.map((method) => [method.name, method])),
|
||||
]),
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
import { runtimeCallEntryState } from "./runtimeCalls.state"
|
||||
|
||||
export const RuntimeCalls = withSubscribe(
|
||||
() => {
|
||||
const { lookup, entries } = useStateObservable(metadataRuntimeCalls$)
|
||||
const [api, setApi] = useState<string | null>("Core")
|
||||
const [method, setMethod] = useState<string | null>("Version")
|
||||
const entry = useStateObservable(selectedEntry$)
|
||||
|
||||
const selectedApi =
|
||||
(api && lookup.metadata.apis.find((p) => p.name === api)) || null
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setMethod((prev) => {
|
||||
if (!selectedApi?.methods[0]) return null
|
||||
return selectedApi.methods.some((v) => v.name === prev)
|
||||
? prev
|
||||
: selectedApi.methods[0].name
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[selectedApi?.name],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const selectedMethod =
|
||||
(method && selectedApi?.methods.find((it) => it.name === method)) ||
|
||||
null
|
||||
setSelectedMethod(
|
||||
selectedMethod ? { ...selectedMethod, api: selectedApi!.name } : null,
|
||||
)
|
||||
}, [selectedApi, method])
|
||||
|
||||
return (
|
||||
<CenteredScrollContainer className="p-4 pb-0 flex flex-col gap-2 items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<label>
|
||||
API
|
||||
<SearchableSelect
|
||||
value={api}
|
||||
setValue={(v) => setApi(v)}
|
||||
options={Object.keys(entries).map((e) => ({
|
||||
text: e,
|
||||
value: e,
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
{selectedApi && api && (
|
||||
<label>
|
||||
Method
|
||||
<SearchableSelect
|
||||
value={method}
|
||||
setValue={(v) => setMethod(v)}
|
||||
options={
|
||||
Object.keys(entries[api]).map((s) => ({
|
||||
text: s,
|
||||
value: s,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{!!entry?.docs.length && (
|
||||
<div className="w-full">
|
||||
Docs
|
||||
<DocsRenderer docs={entry.docs} />
|
||||
</div>
|
||||
)}
|
||||
<RuntimeCallQuery />
|
||||
<Routes>
|
||||
<Route path=":callId" element={<RuntimeCallResults />} />
|
||||
</Routes>
|
||||
</CenteredScrollContainer>
|
||||
)
|
||||
},
|
||||
() => (
|
||||
<CenteredScrollContainer className="p-4 pb-0 flex flex-col gap-2 items-start">
|
||||
<MetadataEntryInput
|
||||
state={runtimeCallEntryState}
|
||||
labels={{
|
||||
group: "API",
|
||||
item: "Method",
|
||||
}}
|
||||
/>
|
||||
<RuntimeCallQuery />
|
||||
<Routes>
|
||||
<Route path=":callId" element={<RuntimeCallResults />} />
|
||||
</Routes>
|
||||
</CenteredScrollContainer>
|
||||
),
|
||||
{
|
||||
fallback: <LoadingMetadata />,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createMetadataEntryState } from "@/components/MetadataEntryInput"
|
||||
import { pushWorkspaceEntry, WorkspaceEntryData } from "@/components/Workspace"
|
||||
import { getHashParams } from "@/hashParams"
|
||||
import { runtimeCtxAt$, unsafeApi$ } from "@/state/chains/chain.state"
|
||||
import { RuntimeContext } from "@polkadot-api/observable-client"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { ServerCog } from "lucide-react"
|
||||
import { Binary, HexString, ResultPayload } from "polkadot-api"
|
||||
import { Binary, Codec, HexString, ResultPayload } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -31,12 +32,35 @@ export type RuntimeCallMetadataMethod = {
|
||||
docs: string[]
|
||||
}
|
||||
|
||||
export const [entryChange$, setSelectedMethod] =
|
||||
createSignal<RuntimeCallMetadataMethod | null>()
|
||||
export const selectedEntry$ = state(entryChange$, null)
|
||||
export const runtimeCallEntryState = createMetadataEntryState(
|
||||
(ctx) =>
|
||||
Object.fromEntries(
|
||||
ctx.lookup.metadata.apis.map((api) => [
|
||||
api.name,
|
||||
api.methods.map((method) => method.name),
|
||||
]),
|
||||
),
|
||||
() => {
|
||||
const params = getHashParams()
|
||||
const group = params.get("api") ?? "Core"
|
||||
const item = params.get("method") ?? "Version"
|
||||
return { item, group }
|
||||
},
|
||||
(ctx, entry): RuntimeCallMetadataMethod => {
|
||||
const api = ctx.lookup.metadata.apis.find(
|
||||
(api) => api.name === entry.group,
|
||||
)!
|
||||
const method = api.methods.find((i) => i.name === entry.item)!
|
||||
return {
|
||||
api: api.name,
|
||||
...method,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
type RuntimeCallQuery = {
|
||||
blockHash: HexString
|
||||
latestBlock: boolean
|
||||
api: string
|
||||
method: string
|
||||
args: unknown[]
|
||||
@@ -49,7 +73,7 @@ const runtimeQueryToId = (
|
||||
const codec = ctx.dynamicBuilder.buildRuntimeCall(query.api, query.method)
|
||||
|
||||
return [
|
||||
query.blockHash,
|
||||
(query.latestBlock ? "latest_" : "") + query.blockHash,
|
||||
query.api,
|
||||
query.method,
|
||||
Binary.toHex(codec.args.enc(query.args)),
|
||||
@@ -57,16 +81,27 @@ const runtimeQueryToId = (
|
||||
}
|
||||
export const idToRuntimeQuery = async (
|
||||
id: string,
|
||||
): Promise<RuntimeCallQuery> => {
|
||||
const [blockHash, api, method, args] = id.split(":")
|
||||
): Promise<
|
||||
RuntimeCallQuery & {
|
||||
codec: Codec<any> & {
|
||||
inner: Codec<any>[]
|
||||
}
|
||||
}
|
||||
> => {
|
||||
const [blockHashStr, api, method, args] = id.split(":")
|
||||
const latestBlock = blockHashStr.startsWith("latest_")
|
||||
const blockHash = blockHashStr.replace("latest_", "")
|
||||
|
||||
const ctx = await firstValueFrom(runtimeCtxAt$(blockHash))
|
||||
const codec = ctx.dynamicBuilder.buildRuntimeCall(api, method).args
|
||||
|
||||
return {
|
||||
latestBlock,
|
||||
blockHash,
|
||||
api,
|
||||
method,
|
||||
args: codec.dec(args),
|
||||
codec,
|
||||
}
|
||||
}
|
||||
export const runtimeCallToWorkspaceEntry = async (
|
||||
@@ -98,6 +133,7 @@ export const runtimeCallToWorkspaceEntry = async (
|
||||
const context: RuntimeCallWorkspaceContext = {
|
||||
api: query.api,
|
||||
method: query.method,
|
||||
blockHash: query.blockHash,
|
||||
result$,
|
||||
}
|
||||
const id = runtimeQueryToId(ctx, query)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { MetadataEntryInput } from "@/components/MetadataEntryInput"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useNavigate } from "@/hashParams"
|
||||
import { createState } from "@/lib/externalState"
|
||||
import { NOTIN } from "@polkadot-api/react-builder"
|
||||
@@ -7,13 +9,14 @@ import { Enum } from "polkadot-api"
|
||||
import { FC } from "react"
|
||||
import { combineLatest, firstValueFrom, map } from "rxjs"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import { addStorageSubscription, selectedEntry$ } from "./storage.state"
|
||||
import { StorageEntryPicker } from "./StorageEntryPicker"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { addStorageSubscription, storageEntryState } from "./storage.state"
|
||||
|
||||
export const [value$, setValue] = createState("")
|
||||
|
||||
const valueDecoder$ = combineLatest([selectedEntry$, selectedBlock$]).pipe(
|
||||
const valueDecoder$ = combineLatest([
|
||||
storageEntryState.selectedEntry$,
|
||||
selectedBlock$,
|
||||
]).pipe(
|
||||
map(([selectedEntry, { ctx }]) =>
|
||||
selectedEntry
|
||||
? ctx.dynamicBuilder.buildDefinition(selectedEntry.value).dec
|
||||
@@ -41,7 +44,7 @@ export const StorageDecode: FC = () => {
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, { hash }] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, selectedBlock$]),
|
||||
combineLatest([storageEntryState.selectedEntry$, selectedBlock$]),
|
||||
)
|
||||
|
||||
const id = await addStorageSubscription({
|
||||
@@ -55,7 +58,13 @@ export const StorageDecode: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
<StorageEntryPicker />
|
||||
<MetadataEntryInput
|
||||
state={storageEntryState}
|
||||
labels={{
|
||||
group: "Pallet",
|
||||
item: "Entry",
|
||||
}}
|
||||
/>
|
||||
<label className="block">
|
||||
Data
|
||||
<Textarea
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { DocsRenderer } from "@/components/DocsRenderer"
|
||||
import { Popover } from "@/components/Popover"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { Info } from "lucide-react"
|
||||
import { map } from "rxjs"
|
||||
import { BlockPicker, selectedBlock$ } from "./BlockPicker"
|
||||
import { partialEntry$, selectedEntry$, selectEntry } from "./storage.state"
|
||||
|
||||
const metadataStorage$ = state(
|
||||
selectedBlock$.pipe(
|
||||
map(({ ctx }) => ({
|
||||
lookup: ctx.lookup,
|
||||
entries: Object.fromEntries(
|
||||
ctx.lookup.metadata.pallets
|
||||
.filter((p) => p.storage)
|
||||
.map((p) => [
|
||||
p.name,
|
||||
Object.fromEntries(
|
||||
p.storage!.items.map((item) => [item.name, item.type]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
export const StorageEntryPicker = () => {
|
||||
const { entries } = useStateObservable(metadataStorage$)
|
||||
const partialEntry = useStateObservable(partialEntry$)
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-2 @3xl:grid-cols-3 max-w-2xl">
|
||||
<label>
|
||||
Block
|
||||
<BlockPicker />
|
||||
</label>
|
||||
<label>
|
||||
Pallet
|
||||
<SearchableSelect
|
||||
value={partialEntry.pallet}
|
||||
setValue={(v) => selectEntry({ pallet: v })}
|
||||
options={Object.keys(entries).map((e) => ({
|
||||
text: e,
|
||||
value: e,
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
{partialEntry.pallet && entries[partialEntry.pallet] && (
|
||||
<label className="max-w-52">
|
||||
<div className="flex items-center justify-between">
|
||||
Entry
|
||||
{selectedEntry?.docs.length ? (
|
||||
<Popover
|
||||
content={
|
||||
<DocsRenderer
|
||||
docs={selectedEntry.docs}
|
||||
className="max-h-none"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<Info size={16} />
|
||||
</button>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
<SearchableSelect
|
||||
value={partialEntry.entry}
|
||||
setValue={(v) => selectEntry({ entry: v })}
|
||||
options={
|
||||
Object.keys(entries[partialEntry.pallet]).map((s) => ({
|
||||
text: s,
|
||||
value: s,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EditCodec } from "@/codec-components/EditCodec"
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { BinaryEditButton } from "@/components/BinaryEditButton"
|
||||
import { MetadataEntryInput } from "@/components/MetadataEntryInput"
|
||||
import SliderToggle from "@/components/Toggle"
|
||||
import { useNavigate } from "@/hashParams"
|
||||
import {
|
||||
@@ -29,12 +30,7 @@ import {
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import { decodeKey } from "./decodeKey"
|
||||
import {
|
||||
addStorageSubscription,
|
||||
selectedEntry$,
|
||||
selectEntry,
|
||||
} from "./storage.state"
|
||||
import { StorageEntryPicker } from "./StorageEntryPicker"
|
||||
import { addStorageSubscription, storageEntryState } from "./storage.state"
|
||||
|
||||
export const StorageQuery: FC = () => {
|
||||
const isReady = useStateObservable(isReady$)
|
||||
@@ -42,7 +38,12 @@ export const StorageQuery: FC = () => {
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, keyValues, keysEnabled, { hash }] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, keyValues$, argsEnabled$, selectedBlock$]),
|
||||
combineLatest([
|
||||
storageEntryState.selectedEntry$,
|
||||
keyValues$,
|
||||
argsEnabled$,
|
||||
selectedBlock$,
|
||||
]),
|
||||
)
|
||||
const args = keyValues.slice(0, keysEnabled)
|
||||
|
||||
@@ -57,7 +58,13 @@ export const StorageQuery: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-start w-full">
|
||||
<StorageEntryPicker />
|
||||
<MetadataEntryInput
|
||||
state={storageEntryState}
|
||||
labels={{
|
||||
group: "Pallet",
|
||||
item: "Entry",
|
||||
}}
|
||||
/>
|
||||
<StorageKeysInput />
|
||||
<KeyInput />
|
||||
<ActionButton disabled={!isReady} onClick={submit}>
|
||||
@@ -67,14 +74,14 @@ export const StorageQuery: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const keys$ = selectedEntry$.pipeState(
|
||||
const keys$ = storageEntryState.selectedEntry$.pipeState(
|
||||
filter((e) => !!e),
|
||||
map((entry) => entry.key),
|
||||
withDefault([] as number[]),
|
||||
distinctUntilChanged((a, b) => a.join(",") === b.join(",")),
|
||||
)
|
||||
|
||||
const hashers$ = selectedEntry$.pipeState(
|
||||
const hashers$ = storageEntryState.selectedEntry$.pipeState(
|
||||
filter((e) => !!e),
|
||||
map((entry) => entry.hashers),
|
||||
withDefault([] as string[]),
|
||||
@@ -307,7 +314,7 @@ const StorageArgInput: FC<{
|
||||
}
|
||||
|
||||
const keyCodec$ = state(
|
||||
combineLatest([selectedBlock$, selectedEntry$]).pipe(
|
||||
combineLatest([selectedBlock$, storageEntryState.selectedEntry$]).pipe(
|
||||
map(([{ ctx }, selectedEntry]) =>
|
||||
selectedEntry
|
||||
? ctx.dynamicBuilder.buildStorage(
|
||||
@@ -376,7 +383,7 @@ const keyInput$ = state(
|
||||
export const KeyInput: FC = () => {
|
||||
const keyInput = useStateObservable(keyInput$)
|
||||
const builder = useStateObservable(builderState$)
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(storageEntryState.selectedEntry$)
|
||||
const keysEnabled = useStateObservable(argsEnabled$)
|
||||
|
||||
if (!builder || !selectedEntry) return null
|
||||
@@ -411,9 +418,9 @@ export const KeyInput: FC = () => {
|
||||
decoded.pallet.name !== selectedEntry.pallet ||
|
||||
decoded.item.name !== selectedEntry.entry
|
||||
) {
|
||||
selectEntry({
|
||||
pallet: decoded.pallet.name,
|
||||
entry: decoded.item.name,
|
||||
storageEntryState.selectEntry({
|
||||
group: decoded.pallet.name,
|
||||
item: decoded.item.name,
|
||||
})
|
||||
newKeysEnabled =
|
||||
decoded.item.type.tag === "plain"
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Chopsticks } from "@/components/Icons"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { chainClient$, client$, lookup$ } from "@/state/chains/chain.state"
|
||||
import { getTypeComplexity } from "@/utils"
|
||||
import { fromHex, toHex } from "polkadot-api/utils"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { fromHex, toHex } from "polkadot-api/utils"
|
||||
import { FC, useState } from "react"
|
||||
import {
|
||||
combineLatest,
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { selectedEntry$ } from "./storage.state"
|
||||
import { encodedKey$, KeyInput, StorageKeysInput } from "./StorageQuery"
|
||||
import { storageEntryState } from "./storage.state"
|
||||
|
||||
const [setValue$, setValue] = createSignal<Uint8Array | "partial" | null>()
|
||||
const currentValue$ = state(
|
||||
@@ -33,7 +33,7 @@ const currentValue$ = state(
|
||||
).pipe(
|
||||
withLatestFrom(
|
||||
lookup$,
|
||||
selectedEntry$.pipe(filter((v) => v != null)),
|
||||
storageEntryState.selectedEntry$.pipe(filter((v) => v != null)),
|
||||
),
|
||||
map(([v, lookup, entry]) => {
|
||||
if (v != null) return fromHex(v)
|
||||
@@ -59,7 +59,7 @@ const currentValue$ = state(
|
||||
)
|
||||
|
||||
export const StorageSet: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(storageEntryState.selectedEntry$)
|
||||
const lookup = useStateObservable(lookup$)
|
||||
const currentValue = useStateObservable(currentValue$)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@@ -26,7 +26,7 @@ import { setMode } from "./Storage"
|
||||
import {
|
||||
idToStorageSubscription,
|
||||
KeyCodec,
|
||||
selectEntry,
|
||||
storageEntryState,
|
||||
storageSubscriptionToWorkspaceEntry,
|
||||
StorageSubscriptionValue,
|
||||
stringifyArg,
|
||||
@@ -490,9 +490,9 @@ const useSynchronizeInputs = (id: string) => {
|
||||
const params = await idToStorageSubscription(id)
|
||||
if (cancelled) return
|
||||
setBlockHashValue(params.blockHash ?? "Latest")
|
||||
selectEntry({
|
||||
pallet: params.pallet,
|
||||
entry: params.item,
|
||||
storageEntryState.selectEntry({
|
||||
group: params.pallet,
|
||||
item: params.item,
|
||||
})
|
||||
setMode(params.value.type)
|
||||
// Let entry settle
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bytesToString } from "@/components/BinaryInput"
|
||||
import { createMetadataEntryState } from "@/components/MetadataEntryInput"
|
||||
import { pushWorkspaceEntry, WorkspaceEntryData } from "@/components/Workspace"
|
||||
import { getHashParams } from "@/hashParams"
|
||||
import {
|
||||
@@ -15,13 +16,11 @@ import {
|
||||
RuntimeContext,
|
||||
} from "@polkadot-api/observable-client"
|
||||
import { DefaultedStateObservable, state } from "@react-rxjs/core"
|
||||
import { createSignal, mergeWithKey } from "@react-rxjs/utils"
|
||||
import { DatabaseSearch } from "lucide-react"
|
||||
import { Binary, Enum, HexString } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
combineLatestWith,
|
||||
distinct,
|
||||
EMPTY,
|
||||
endWith,
|
||||
@@ -41,7 +40,6 @@ import {
|
||||
take,
|
||||
takeUntil,
|
||||
} from "rxjs"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import { StorageWorkspaceEntry } from "./StorageWorkspaceEntry"
|
||||
import { getEntry, getStorageItem } from "./decodeKey"
|
||||
|
||||
@@ -54,112 +52,53 @@ export type StorageMetadataEntry = {
|
||||
hashers: string[]
|
||||
}
|
||||
|
||||
export const [entryChange$, selectEntry] = createSignal<{
|
||||
pallet?: string | null
|
||||
entry?: string | null
|
||||
}>()
|
||||
|
||||
const getPalletEntries = (
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">,
|
||||
) =>
|
||||
Object.fromEntries(
|
||||
ctx.lookup.metadata.pallets.map((p) => [p.name, p.storage?.items ?? []]),
|
||||
ctx.lookup.metadata.pallets
|
||||
.filter((p) => p.storage?.items.length)
|
||||
.map((p) => [p.name, p.storage?.items.map((item) => item.name) ?? []]),
|
||||
)
|
||||
|
||||
const palletEntries$ = selectedBlock$.pipe(
|
||||
map(({ ctx }) => getPalletEntries(ctx)),
|
||||
)
|
||||
|
||||
const initialValue$ = palletEntries$.pipe(
|
||||
map(() => {
|
||||
export const storageEntryState = createMetadataEntryState(
|
||||
getPalletEntries,
|
||||
() => {
|
||||
const params = getHashParams()
|
||||
const pallet = params.get("pallet") ?? "System"
|
||||
const entry = params.get("entry") ?? "Account"
|
||||
return { entry, pallet }
|
||||
}),
|
||||
)
|
||||
|
||||
export const partialEntry$ = state(
|
||||
mergeWithKey({ entryChange$, initialValue$ }).pipe(
|
||||
combineLatestWith(palletEntries$),
|
||||
scan(
|
||||
(acc, [evt, pallets]) => {
|
||||
const newValue =
|
||||
evt.type === "entryChange$"
|
||||
? { ...acc, ...evt.payload }
|
||||
: {
|
||||
pallet: acc.pallet ?? evt.payload.pallet,
|
||||
entry: acc.entry ?? evt.payload.entry,
|
||||
}
|
||||
let selectedPallet = newValue.pallet ? pallets[newValue.pallet] : null
|
||||
if (!selectedPallet) {
|
||||
newValue.pallet = Object.keys(pallets)[0] ?? null
|
||||
selectedPallet = pallets[newValue.pallet] ?? null
|
||||
}
|
||||
if (!selectedPallet?.find((it) => it.name === newValue.entry)) {
|
||||
newValue.entry = selectedPallet?.[0]?.name ?? null
|
||||
}
|
||||
return newValue
|
||||
},
|
||||
{
|
||||
pallet: null as string | null,
|
||||
entry: null as string | null,
|
||||
},
|
||||
),
|
||||
),
|
||||
{
|
||||
pallet: null,
|
||||
entry: null,
|
||||
const group = params.get("pallet") ?? "System"
|
||||
const item = params.get("entry") ?? "Account"
|
||||
return { item, group }
|
||||
},
|
||||
)
|
||||
(ctx, entry): StorageMetadataEntry => {
|
||||
const pallet = ctx.lookup.metadata.pallets.find(
|
||||
(p) => p.name === entry.group,
|
||||
)!
|
||||
const item = pallet.storage!.items.find((i) => i.name === entry.item)!
|
||||
|
||||
export const selectedEntry$ = state(
|
||||
combineLatest([
|
||||
partialEntry$,
|
||||
selectedBlock$.pipe(
|
||||
map(({ ctx }) => {
|
||||
const entries = getPalletEntries(ctx)
|
||||
return { ctx, entries }
|
||||
}),
|
||||
),
|
||||
]).pipe(
|
||||
map(([partialEntry, { ctx, entries }]): StorageMetadataEntry | null => {
|
||||
const entry = partialEntry.pallet
|
||||
? entries[partialEntry.pallet]?.find(
|
||||
(v) => v.name === partialEntry.entry,
|
||||
)
|
||||
: null
|
||||
if (!entry?.type) return null
|
||||
|
||||
const { type, docs } = entry
|
||||
const pallet = partialEntry.pallet!
|
||||
|
||||
const { keys } = getEntry(ctx, type)
|
||||
const key = keys.map((v) => v.type)
|
||||
const hashers = keys.map((v) => v.hasher)
|
||||
|
||||
if (type.tag === "plain") {
|
||||
return {
|
||||
value: type.value,
|
||||
key,
|
||||
pallet,
|
||||
entry: entry.name,
|
||||
docs,
|
||||
hashers,
|
||||
}
|
||||
}
|
||||
const { keys } = getEntry(ctx, item.type)
|
||||
const key = keys.map((v) => v.type)
|
||||
const hashers = keys.map((v) => v.hasher)
|
||||
|
||||
if (item.type.tag === "plain") {
|
||||
return {
|
||||
value: type.value.value,
|
||||
value: item.type.value,
|
||||
key,
|
||||
pallet,
|
||||
entry: entry.name,
|
||||
docs,
|
||||
pallet: pallet.name,
|
||||
entry: item.name,
|
||||
docs: item.docs,
|
||||
hashers,
|
||||
}
|
||||
}),
|
||||
),
|
||||
null,
|
||||
}
|
||||
|
||||
return {
|
||||
value: item.type.value.value,
|
||||
key,
|
||||
pallet: pallet.name,
|
||||
entry: item.name,
|
||||
docs: item.docs,
|
||||
hashers,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export type KeyCodec = {
|
||||
|
||||
Reference in New Issue
Block a user