Merge pull request #101 from polkadot-api/vo/storage-keys
feat: show decoded keys on storage get entries
This commit is contained in:
41
src/ViewValue/EnumDisplay.tsx
Normal file
41
src/ViewValue/EnumDisplay.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAppendTitle } from "@/codec-components/EditCodec/Tree/CEnum"
|
||||
import { Enum } from "polkadot-api"
|
||||
import { FC, useContext, useState } from "react"
|
||||
import { Portal } from "react-portal"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { ChildProvider, TitleContext } from "./TitleContext"
|
||||
import { ViewValue } from "./ViewValue"
|
||||
|
||||
export const EnumDisplay: FC<{
|
||||
value: Enum<Record<string, unknown>>
|
||||
}> = ({ value }) => {
|
||||
const titleContainer = useContext(TitleContext)
|
||||
const titleElement = useAppendTitle(titleContainer, "")
|
||||
const [newElement, setNewElement] = useState<HTMLElement | null>(null)
|
||||
|
||||
const inner = <ViewValue value={value.value} />
|
||||
|
||||
if (titleContainer) {
|
||||
return (
|
||||
<>
|
||||
{titleElement ? (
|
||||
<Portal node={titleElement}>/ {value.type}</Portal>
|
||||
) : null}
|
||||
{inner}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={twMerge("flex flex-col")}>
|
||||
<div className="flex gap-2 overflow-hidden justify-between">
|
||||
<div ref={setNewElement} className="flex gap-1 flex-wrap">
|
||||
<span>{value.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1">
|
||||
<ChildProvider titleElement={newElement}>{inner}</ChildProvider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/ViewValue/ListComponents.tsx
Normal file
52
src/ViewValue/ListComponents.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { ChildProvider } from "./TitleContext"
|
||||
import { ViewValue } from "./ViewValue"
|
||||
|
||||
export const ArrayDisplay: FC<{ value: unknown[] }> = ({ value }) => (
|
||||
<ul className="w-full">
|
||||
{value.length ? (
|
||||
value.map((innerValue, idx) => (
|
||||
<ListItemComponent key={idx} idx={idx}>
|
||||
<ViewValue value={innerValue} />
|
||||
</ListItemComponent>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-foreground/60">(Empty)</span>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
|
||||
const ListItemComponent: FC<
|
||||
PropsWithChildren<{
|
||||
idx: number
|
||||
}>
|
||||
> = ({ idx, children }) => {
|
||||
return (
|
||||
<ChildProvider titleElement={null}>
|
||||
<ListItem idx={idx}>{children}</ListItem>
|
||||
</ChildProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const ListItem: React.FC<
|
||||
PropsWithChildren<{
|
||||
idx: number
|
||||
}>
|
||||
> = ({ idx, children }) => {
|
||||
const title = (
|
||||
<div className="flex items-center">
|
||||
<span className="cursor-pointer flex items-center py-1 gap-1">
|
||||
Item {idx + 1}.
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<li className={"flex flex-col mb-1"}>
|
||||
{title}
|
||||
<div className={"flex-row p-2 items-center border border-border"}>
|
||||
{children}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
75
src/ViewValue/StructDisplay.tsx
Normal file
75
src/ViewValue/StructDisplay.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Dot } from "lucide-react"
|
||||
import React, { FC, PropsWithChildren, useContext, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { ChildProvider, TitleContext } from "./TitleContext"
|
||||
import { ViewValue } from "./ViewValue"
|
||||
import { ExpandBtn } from "@/components/Expand"
|
||||
|
||||
const isComplexNested = (value: unknown) => {
|
||||
if (typeof value !== "object" || !value) return false
|
||||
if (Array.isArray(value)) return value.length > 1
|
||||
|
||||
if (Object.keys(value).length === 2 && "type" in value && "value" in value) {
|
||||
return isComplexNested(value.value)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const StructItem: React.FC<
|
||||
PropsWithChildren<{
|
||||
name: string
|
||||
value: unknown
|
||||
}>
|
||||
> = ({ name, value, children }) => {
|
||||
const [titleElement, setTitleElement] = useState<HTMLElement | null>(null)
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
const isComplexShape = isComplexNested(value)
|
||||
|
||||
return (
|
||||
<li
|
||||
className={twMerge(
|
||||
"flex flex-col transition-all duration-300",
|
||||
isComplexShape ? "cursor-pointer" : "",
|
||||
)}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
>
|
||||
<ChildProvider titleElement={titleElement}>
|
||||
<span className="flex items-center py-1 gap-1">
|
||||
{isComplexShape ? (
|
||||
<ExpandBtn expanded={expanded} />
|
||||
) : (
|
||||
<Dot size={16} />
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="opacity-75">{name}</span>
|
||||
<span ref={setTitleElement} />
|
||||
</span>
|
||||
{isComplexShape ? null : <div>{children}</div>}
|
||||
</span>
|
||||
{isComplexShape && expanded ? <div>{children}</div> : null}
|
||||
</ChildProvider>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export const StructDisplay: FC<{ value: Record<string, unknown> }> = ({
|
||||
value,
|
||||
}) => {
|
||||
const hasParentTitle = !!useContext(TitleContext)
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={twMerge(
|
||||
"flex flex-col w-full",
|
||||
hasParentTitle && "border-l border-border",
|
||||
)}
|
||||
>
|
||||
{Object.entries(value).map(([name, value]) => (
|
||||
<StructItem key={name} name={name} value={value}>
|
||||
<ViewValue value={value} />
|
||||
</StructItem>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
9
src/ViewValue/TitleContext.tsx
Normal file
9
src/ViewValue/TitleContext.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createContext, FC, PropsWithChildren } from "react"
|
||||
|
||||
export const TitleContext = createContext<HTMLElement | null>(null)
|
||||
|
||||
export const ChildProvider: FC<
|
||||
PropsWithChildren<{ titleElement: HTMLElement | null }>
|
||||
> = ({ titleElement, children }) => (
|
||||
<TitleContext.Provider value={titleElement}>{children}</TitleContext.Provider>
|
||||
)
|
||||
51
src/ViewValue/ViewValue.tsx
Normal file
51
src/ViewValue/ViewValue.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BytesDisplay } from "@/codec-components/ViewCodec/CBytes"
|
||||
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
|
||||
import { Binary, getSs58AddressInfo } from "polkadot-api"
|
||||
import { FC } from "react"
|
||||
import { EnumDisplay } from "./EnumDisplay"
|
||||
import { ArrayDisplay } from "./ListComponents"
|
||||
import { StructDisplay } from "./StructDisplay"
|
||||
import {
|
||||
BoolDisplay,
|
||||
EthAccountDisplay,
|
||||
NoneDisplay,
|
||||
NumberDisplay,
|
||||
ResultDisplay,
|
||||
StrDisplay,
|
||||
} from "./view-components"
|
||||
|
||||
export const ViewValue: FC<{
|
||||
value: unknown
|
||||
}> = ({ value }) => {
|
||||
switch (typeof value) {
|
||||
case "string": {
|
||||
const info = getSs58AddressInfo(value)
|
||||
if (info.isValid) return <AccountIdDisplay value={value} />
|
||||
if (value.startsWith("0x") && value.length === 42)
|
||||
return <EthAccountDisplay value={value} />
|
||||
return <StrDisplay value={value} />
|
||||
}
|
||||
case "boolean":
|
||||
return <BoolDisplay value={value} />
|
||||
case "number":
|
||||
case "bigint":
|
||||
return <NumberDisplay value={value} />
|
||||
case "object": {
|
||||
if (value == null) return <>TODO</>
|
||||
if (value instanceof Binary) return <BytesDisplay value={value} />
|
||||
if (Array.isArray(value)) return <ArrayDisplay value={value} />
|
||||
if ("type" in value && typeof value.type === "string" && "value" in value)
|
||||
return <EnumDisplay value={value as any} />
|
||||
if (
|
||||
"success" in value &&
|
||||
typeof value.success === "boolean" &&
|
||||
"value" in value
|
||||
)
|
||||
return <ResultDisplay value={value as any} />
|
||||
return <StructDisplay value={value as any} />
|
||||
}
|
||||
case "undefined":
|
||||
return <NoneDisplay />
|
||||
}
|
||||
return <div className="text-muted-foreground">(Uknown value)</div>
|
||||
}
|
||||
1
src/ViewValue/index.ts
Normal file
1
src/ViewValue/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./ViewValue"
|
||||
18
src/ViewValue/utils.ts
Normal file
18
src/ViewValue/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getEnumInnerVar, isComplex } from "@/utils/shape"
|
||||
import { Var } from "@polkadot-api/metadata-builders"
|
||||
|
||||
export const isComplexNested = (field: Var, value: any): boolean => {
|
||||
if (!isComplex(field.type)) return false
|
||||
|
||||
if (field.type === "enum")
|
||||
return isComplexNested(getEnumInnerVar(field, value.type), value.value)
|
||||
|
||||
if (field.type === "option")
|
||||
return value == null ? false : isComplexNested(field.value, value)
|
||||
|
||||
if (field.type === "sequence" && value.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
33
src/ViewValue/view-components.tsx
Normal file
33
src/ViewValue/view-components.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { HexString } from "polkadot-api"
|
||||
import { FC } from "react"
|
||||
import { ViewValue } from "./ViewValue"
|
||||
|
||||
export const BoolDisplay: FC<{ value: boolean }> = ({ value }) => {
|
||||
return <div className="flex gap-4">{value ? "Yes" : "No"}</div>
|
||||
}
|
||||
|
||||
export const EthAccountDisplay: FC<{ value: HexString }> = ({ value }) => (
|
||||
<span>{value}</span>
|
||||
)
|
||||
|
||||
export const NoneDisplay: FC = () => (
|
||||
<span className="text-foreground/60">None</span>
|
||||
)
|
||||
|
||||
export const ResultDisplay: FC<{
|
||||
value: { success: boolean; value: unknown }
|
||||
}> = ({ value }) => {
|
||||
return (
|
||||
<div>
|
||||
<div>{value.success ? "OK" : "KO"}</div>
|
||||
<ViewValue value={value.value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StrDisplay: FC<{ value: string }> = ({ value }) => (
|
||||
<div>{value}</div>
|
||||
)
|
||||
export const NumberDisplay: FC<{ value: number | bigint }> = ({ value }) => (
|
||||
<div>{String(value)}</div>
|
||||
)
|
||||
@@ -1,13 +1,19 @@
|
||||
import { getBytesFormat } from "@/components/BinaryInput"
|
||||
import { ViewBytes } from "@polkadot-api/react-builder"
|
||||
import { useReportBinary } from "./CopyBinary"
|
||||
import { SwitchBinary } from "@/components/Icons"
|
||||
import { useState } from "react"
|
||||
import { ViewBytes } from "@polkadot-api/react-builder"
|
||||
import { Binary } from "polkadot-api"
|
||||
import { FC, useState } from "react"
|
||||
import { useReportBinary } from "./CopyBinary"
|
||||
|
||||
export const CBytes: ViewBytes = ({ value, encodedValue }) => {
|
||||
useReportBinary(encodedValue)
|
||||
|
||||
return <BytesDisplay value={value} />
|
||||
}
|
||||
|
||||
export const BytesDisplay: FC<{ value: Binary }> = ({ value }) => {
|
||||
const [forceBinary, setForceBinary] = useState(false)
|
||||
|
||||
useReportBinary(encodedValue)
|
||||
const format = getBytesFormat(value)
|
||||
|
||||
return (
|
||||
|
||||
@@ -46,9 +46,16 @@ export const StorageQuery: FC = () => {
|
||||
if (!selectedEntry) return null
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, unsafeApi, keyValues, keysEnabled] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, unsafeApi$, keyValues$, keysEnabled$]),
|
||||
)
|
||||
const [entry, unsafeApi, keyValues, keysEnabled, keyCodec] =
|
||||
await firstValueFrom(
|
||||
combineLatest([
|
||||
selectedEntry$,
|
||||
unsafeApi$,
|
||||
keyValues$,
|
||||
keysEnabled$,
|
||||
keyCodec$,
|
||||
]),
|
||||
)
|
||||
const args = keyValues.slice(0, keysEnabled)
|
||||
const storageEntry = unsafeApi.query[entry!.pallet][entry!.entry]
|
||||
const single = keyValues.length === keysEnabled
|
||||
@@ -64,6 +71,7 @@ export const StorageQuery: FC = () => {
|
||||
single,
|
||||
stream,
|
||||
type: entry!.value,
|
||||
keyCodec: keyCodec!,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,29 +311,30 @@ const StorageKeyInput: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const keyCodec$ = state(
|
||||
combineLatest([dynamicBuilder$, selectedEntry$]).pipe(
|
||||
map(([builder, selectedEntry]) =>
|
||||
selectedEntry
|
||||
? builder.buildStorage(selectedEntry.pallet, selectedEntry.entry).keys
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
export const encodedKey$ = state(
|
||||
combineLatest([
|
||||
dynamicBuilder$,
|
||||
selectedEntry$,
|
||||
keyValues$,
|
||||
keysEnabled$,
|
||||
]).pipe(
|
||||
map(([builder, selectedEntry, keyValues, keysEnabled]) => {
|
||||
combineLatest([keyCodec$, keyValues$, keysEnabled$]).pipe(
|
||||
map(([codec, keyValues, keysEnabled]) => {
|
||||
const args = keyValues.slice(0, keysEnabled)
|
||||
if (
|
||||
keyValues.length < keysEnabled ||
|
||||
!args.every((v) => v !== NOTIN) ||
|
||||
!selectedEntry
|
||||
!codec
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const codec = builder.buildStorage(
|
||||
selectedEntry.pallet,
|
||||
selectedEntry.entry,
|
||||
)
|
||||
try {
|
||||
return codec.keys.enc(...args)
|
||||
return codec.enc(...args)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { dynamicBuilder$, metadata$ } from "@/state/chains/chain.state"
|
||||
import { PathsRoot } from "@/codec-components/common/paths.state"
|
||||
import { ViewCodec } from "@/codec-components/ViewCodec"
|
||||
import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary"
|
||||
import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { JsonDisplay } from "@/components/JsonDisplay"
|
||||
import { dynamicBuilder$, metadata$ } from "@/state/chains/chain.state"
|
||||
import { ViewValue } from "@/ViewValue"
|
||||
import { CodecComponentType, NOTIN } from "@polkadot-api/react-builder"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { PauseCircle, PlayCircle, Trash2 } from "lucide-react"
|
||||
import { Binary } from "polkadot-api"
|
||||
import { FC, useMemo, useState } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import {
|
||||
KeyCodec,
|
||||
removeStorageSubscription,
|
||||
StorageSubscription,
|
||||
storageSubscription$,
|
||||
storageSubscriptionKeys$,
|
||||
stringifyArg,
|
||||
toggleSubscriptionPause,
|
||||
} from "./storage.state"
|
||||
import { PathsRoot } from "@/codec-components/common/paths.state"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export const StorageSubscriptions: FC = () => {
|
||||
const keys = useStateObservable(storageSubscriptionKeys$)
|
||||
@@ -131,24 +134,19 @@ const DecodedResultDisplay: FC<{
|
||||
value: unknown
|
||||
}>
|
||||
|
||||
const renderItem = (keyArgs: unknown[], value: unknown, idx: number) => {
|
||||
const title = keyArgs
|
||||
.slice(storageSubscription.args?.length ?? 0)
|
||||
.map(stringifyArg)
|
||||
.join(", ")
|
||||
return (
|
||||
<div key={idx} className={itemClasses}>
|
||||
<PathsRoot.Provider value={`${subscriptionKey}-${idx}`}>
|
||||
<ValueDisplay
|
||||
mode="decoded"
|
||||
title={title}
|
||||
value={value}
|
||||
type={storageSubscription.type}
|
||||
/>
|
||||
</PathsRoot.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const renderItem = (keyArgs: unknown[], value: unknown, idx: number) => (
|
||||
<div key={idx} className={itemClasses}>
|
||||
<PathsRoot.Provider value={`${subscriptionKey}-${idx}`}>
|
||||
<KeyDisplay value={keyArgs} keyCodec={storageSubscription.keyCodec} />
|
||||
<ValueDisplay
|
||||
mode="decoded"
|
||||
title="Value"
|
||||
value={value}
|
||||
type={storageSubscription.type}
|
||||
/>
|
||||
</PathsRoot.Provider>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (values.length > 10) {
|
||||
return (
|
||||
@@ -251,3 +249,37 @@ export const ValueDisplay: FC<{
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const KeyDisplay: FC<{
|
||||
value: unknown[]
|
||||
keyCodec?: KeyCodec
|
||||
}> = ({ value, keyCodec }) => {
|
||||
const binaryValue = (() => {
|
||||
try {
|
||||
return keyCodec ? Binary.fromHex(keyCodec.enc(...value)).asBytes() : null
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-1 gap-2 overflow-hidden">
|
||||
{binaryValue ? <CopyBinary value={binaryValue} /> : null}
|
||||
<h3 className="overflow-hidden text-ellipsis">Key</h3>
|
||||
</div>
|
||||
<ol className="leading-tight flex gap-1 items-center flex-wrap">
|
||||
{value.map((v, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={cn("px-1 py-0.5", {
|
||||
"border rounded": value.length > 1,
|
||||
})}
|
||||
>
|
||||
<ViewValue value={v} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,10 +150,15 @@ export const selectedEntry$ = state(
|
||||
null,
|
||||
)
|
||||
|
||||
export type KeyCodec = {
|
||||
enc: (...args: any[]) => string
|
||||
dec: (value: string) => any[]
|
||||
}
|
||||
export const [newStorageSubscription$, addStorageSubscription] = createSignal<{
|
||||
name: string
|
||||
args: unknown[] | null
|
||||
type: number
|
||||
keyCodec?: KeyCodec
|
||||
single: boolean
|
||||
stream: Observable<unknown>
|
||||
}>()
|
||||
@@ -166,6 +171,7 @@ export type StorageSubscription = {
|
||||
name: string
|
||||
args: unknown[] | null
|
||||
type: number
|
||||
keyCodec?: KeyCodec
|
||||
single: boolean
|
||||
paused: boolean
|
||||
completed: boolean
|
||||
|
||||
Reference in New Issue
Block a user