feat: viewFn query at block, populate args with selected query (#149)
This commit is contained in:
@@ -13,6 +13,7 @@ import { Circle, Dot } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
@@ -21,10 +22,11 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { addViewFnCall, selectedEntry$ } from "./viewFns.state"
|
||||
import { selectedBlock$ } from "../Storage/BlockPicker"
|
||||
import { addViewFnCall, viewFnEntryState } from "./viewFns.state"
|
||||
|
||||
export const ViewFnQuery: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(viewFnEntryState.selectedEntry$)
|
||||
const isReady = useStateObservable(isReady$)
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -33,10 +35,27 @@ export const ViewFnQuery: FC = () => {
|
||||
const submit = async () => {
|
||||
const [entry, inputValues, builder, block] = await firstValueFrom(
|
||||
combineLatest([
|
||||
selectedEntry$,
|
||||
viewFnEntryState.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 +65,7 @@ export const ViewFnQuery: FC = () => {
|
||||
)
|
||||
|
||||
const id = await addViewFnCall({
|
||||
latestBlock: block.latest,
|
||||
blockHash: block.hash,
|
||||
pallet: entry!.pallet,
|
||||
name: entry!.name,
|
||||
@@ -64,13 +84,16 @@ export const ViewFnQuery: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const [inputValueChange$, setInputValue] = createSignal<{
|
||||
export const [inputValueChange$, setInputValue] = createSignal<{
|
||||
idx: number
|
||||
value: Uint8Array | "partial" | null
|
||||
}>()
|
||||
const inputValues$ = selectedEntry$.pipeState(
|
||||
const inputValues$ = viewFnEntryState.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 +114,7 @@ const isReady$ = inputValues$.pipeState(
|
||||
)
|
||||
|
||||
const ViewFnInputValues: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const selectedEntry = useStateObservable(viewFnEntryState.selectedEntry$)
|
||||
if (!selectedEntry || !selectedEntry.inputs.length) return null
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,12 +3,20 @@ 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 "./ViewFnQuery"
|
||||
import { ViewFnWorkspaceContext } from "./ViewFnWorkspaceEntry"
|
||||
import { idToViewFnCall, viewFnCallToWorkspaceEntry } from "./viewFns.state"
|
||||
import {
|
||||
idToViewFnCall,
|
||||
viewFnCallToWorkspaceEntry,
|
||||
viewFnEntryState,
|
||||
} from "./viewFns.state"
|
||||
|
||||
export const ViewFnResults: FC = () => {
|
||||
const { callId } = useParams()
|
||||
@@ -36,6 +44,7 @@ const viewFnCtx$ = state(
|
||||
|
||||
const ViewFnResultBox: FC<{ id: string }> = ({ id }) => {
|
||||
const context = useStateObservable(viewFnCtx$(id))
|
||||
useSynchronizeInputs(id)
|
||||
|
||||
return context ? <ViewFnResultContent id={id} context={context} /> : null
|
||||
}
|
||||
@@ -53,6 +62,10 @@ const ViewFnResultContent: FC<{
|
||||
{context.pallet}.{context.name}
|
||||
</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}
|
||||
@@ -122,3 +135,36 @@ const ResultDisplay: FC<{
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useSynchronizeInputs = (id: string) => {
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const run = async () => {
|
||||
const params = await idToViewFnCall(id)
|
||||
if (cancelled) return
|
||||
setBlockHashValue(params.latestBlock ? "Latest" : params.blockHash)
|
||||
viewFnEntryState.selectEntry({
|
||||
group: params.pallet,
|
||||
item: params.name,
|
||||
})
|
||||
// Let entry settle
|
||||
await firstValueFrom(
|
||||
viewFnEntryState.selectedEntry$.pipe(
|
||||
filter(
|
||||
(v) => !!v && v.pallet === params.pallet && v.name === params.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
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 { ViewFnResult } from "./viewFns.state"
|
||||
|
||||
export type ViewFnWorkspaceContext = {
|
||||
blockHash: string
|
||||
pallet: string
|
||||
name: string
|
||||
result$: DefaultedStateObservable<ViewFnResult | null>
|
||||
|
||||
+17
-104
@@ -1,115 +1,28 @@
|
||||
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 { lookup$ } from "@/state/chains/chain.state"
|
||||
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 { ViewFnQuery } from "./ViewFnQuery"
|
||||
import { ViewFnResults } from "./ViewFnResults"
|
||||
import { selectedEntry$, setSelectedFn } from "./viewFns.state"
|
||||
|
||||
const metadataViewFns$ = state(
|
||||
lookup$.pipe(
|
||||
map((lookup) => ({
|
||||
lookup,
|
||||
entries: Object.fromEntries(
|
||||
lookup.metadata.pallets
|
||||
.filter((p) => p.viewFns.length)
|
||||
.map((p) => [
|
||||
p.name,
|
||||
Object.fromEntries(
|
||||
p.viewFns.map((method) => [method.name, method]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
import { viewFnEntryState } from "./viewFns.state"
|
||||
|
||||
export const ViewFns = withSubscribe(
|
||||
() => {
|
||||
const { lookup, entries } = useStateObservable(metadataViewFns$)
|
||||
const defaultPallet = Object.keys(entries)[0] ?? null
|
||||
const defaultFn = defaultPallet
|
||||
? Object.keys(entries[defaultPallet])[0]
|
||||
: null
|
||||
const [pallet, setPallet] = useState<string | null>(defaultPallet)
|
||||
const [fnName, setFnName] = useState<string | null>(defaultFn)
|
||||
const entry = useStateObservable(selectedEntry$)
|
||||
|
||||
const selectedPallet =
|
||||
(pallet && lookup.metadata.pallets.find((p) => p.name === pallet)) || null
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setFnName((prev) => {
|
||||
if (!selectedPallet?.viewFns[0]) return null
|
||||
return selectedPallet.viewFns.some((v) => v.name === prev)
|
||||
? prev
|
||||
: selectedPallet.viewFns[0].name
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[selectedPallet?.name],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const selectedMethod =
|
||||
(fnName && selectedPallet?.viewFns.find((it) => it.name === fnName)) ||
|
||||
null
|
||||
setSelectedFn(
|
||||
selectedMethod
|
||||
? { ...selectedMethod, pallet: selectedPallet!.name }
|
||||
: null,
|
||||
)
|
||||
}, [selectedPallet, fnName])
|
||||
|
||||
return (
|
||||
<CenteredScrollContainer className="p-4 pb-0 flex flex-col gap-2 items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<label>
|
||||
Pallet
|
||||
<SearchableSelect
|
||||
value={pallet}
|
||||
setValue={(v) => setPallet(v)}
|
||||
options={Object.keys(entries).map((e) => ({
|
||||
text: e,
|
||||
value: e,
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
{selectedPallet && pallet && (
|
||||
<label>
|
||||
Function
|
||||
<SearchableSelect
|
||||
value={fnName}
|
||||
setValue={(v) => setFnName(v)}
|
||||
options={
|
||||
Object.keys(entries[pallet]).map((s) => ({
|
||||
text: s,
|
||||
value: s,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{!!entry?.docs.length && (
|
||||
<div className="w-full">
|
||||
Docs
|
||||
<DocsRenderer docs={entry.docs} />
|
||||
</div>
|
||||
)}
|
||||
<ViewFnQuery />
|
||||
<Routes>
|
||||
<Route path=":callId" element={<ViewFnResults />} />
|
||||
</Routes>
|
||||
</CenteredScrollContainer>
|
||||
)
|
||||
},
|
||||
() => (
|
||||
<CenteredScrollContainer className="p-4 pb-0 flex flex-col gap-2 items-start">
|
||||
<MetadataEntryInput
|
||||
state={viewFnEntryState}
|
||||
labels={{
|
||||
group: "Pallet",
|
||||
item: "Function",
|
||||
}}
|
||||
/>
|
||||
<ViewFnQuery />
|
||||
<Routes>
|
||||
<Route path=":callId" element={<ViewFnResults />} />
|
||||
</Routes>
|
||||
</CenteredScrollContainer>
|
||||
),
|
||||
{
|
||||
fallback: <LoadingMetadata />,
|
||||
},
|
||||
|
||||
@@ -1,11 +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 { UnifiedMetadata } from "@polkadot-api/substrate-bindings"
|
||||
import { shareLatest, state } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { SquareFunction } from "lucide-react"
|
||||
import { Binary, HexString, ResultPayload } from "polkadot-api"
|
||||
import { Binary, Codec, HexString, ResultPayload } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -21,16 +21,52 @@ import {
|
||||
ViewFnWorkspaceEntry,
|
||||
} from "./ViewFnWorkspaceEntry"
|
||||
|
||||
type Pallet = UnifiedMetadata["pallets"][number]
|
||||
export type ViewFnEntry = Pallet["viewFns"][number] & {
|
||||
export type ViewFnEntry = {
|
||||
pallet: string
|
||||
name: string
|
||||
inputs: {
|
||||
name: string
|
||||
type: number
|
||||
}[]
|
||||
output: number
|
||||
docs: string[]
|
||||
}
|
||||
|
||||
export const [entryChange$, setSelectedFn] = createSignal<ViewFnEntry | null>()
|
||||
export const selectedEntry$ = state(entryChange$, null)
|
||||
export const viewFnEntryState = createMetadataEntryState(
|
||||
(ctx) =>
|
||||
Object.fromEntries(
|
||||
ctx.lookup.metadata.pallets
|
||||
.filter((pallet) => pallet.viewFns.length)
|
||||
.map((pallet) => [
|
||||
pallet.name,
|
||||
pallet.viewFns.map((viewFn) => viewFn.name),
|
||||
]),
|
||||
),
|
||||
(entries) => {
|
||||
const params = getHashParams()
|
||||
const group = params.get("pallet") ?? Object.keys(entries)[0] ?? null
|
||||
const item =
|
||||
params.get("fn") ??
|
||||
params.get("function") ??
|
||||
(group ? entries[group]?.[0] : null) ??
|
||||
null
|
||||
return { item, group }
|
||||
},
|
||||
(ctx, entry): ViewFnEntry => {
|
||||
const pallet = ctx.lookup.metadata.pallets.find(
|
||||
(pallet) => pallet.name === entry.group,
|
||||
)!
|
||||
const viewFn = pallet.viewFns.find((i) => i.name === entry.item)!
|
||||
return {
|
||||
pallet: pallet.name,
|
||||
...viewFn,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
type ViewFnCall = {
|
||||
blockHash: HexString
|
||||
latestBlock: boolean
|
||||
pallet: string
|
||||
name: string
|
||||
args: unknown[]
|
||||
@@ -43,20 +79,31 @@ const viewFnCallToId = (
|
||||
const codec = ctx.dynamicBuilder.buildViewFn(call.pallet, call.name).args
|
||||
|
||||
return [
|
||||
call.blockHash,
|
||||
(call.latestBlock ? "latest_" : "") + call.blockHash,
|
||||
call.pallet,
|
||||
call.name,
|
||||
Binary.toHex(codec.enc(call.args)),
|
||||
].join(":")
|
||||
}
|
||||
|
||||
export const idToViewFnCall = async (id: string): Promise<ViewFnCall> => {
|
||||
const [blockHash, pallet, name, encodedArgs] = id.split(":")
|
||||
export const idToViewFnCall = async (
|
||||
id: string,
|
||||
): Promise<
|
||||
ViewFnCall & {
|
||||
codec: Codec<any> & {
|
||||
inner: Codec<any>[]
|
||||
}
|
||||
}
|
||||
> => {
|
||||
const [blockHashStr, pallet, name, encodedArgs] = id.split(":")
|
||||
const latestBlock = blockHashStr.startsWith("latest_")
|
||||
const blockHash = blockHashStr.replace("latest_", "")
|
||||
|
||||
const ctx = await firstValueFrom(runtimeCtxAt$(blockHash))
|
||||
const codec = ctx.dynamicBuilder.buildViewFn(pallet, name).args
|
||||
const args = codec.dec(encodedArgs)
|
||||
|
||||
return { blockHash, pallet, name, args }
|
||||
return { latestBlock, blockHash, pallet, name, args, codec }
|
||||
}
|
||||
|
||||
export const viewFnCallToWorkspaceEntry = async (
|
||||
@@ -89,6 +136,7 @@ export const viewFnCallToWorkspaceEntry = async (
|
||||
const context: ViewFnWorkspaceContext = {
|
||||
pallet: call.pallet,
|
||||
name: call.name,
|
||||
blockHash: call.blockHash,
|
||||
result$,
|
||||
}
|
||||
const id = viewFnCallToId(ctx, call)
|
||||
|
||||
Reference in New Issue
Block a user