From 8e2f873f1288e1f976e79a5fd62f2feaf7652738 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Wed, 12 Nov 2025 11:01:20 -0300 Subject: [PATCH 1/2] feat: show decoded keys on storage get entries --- src/ViewValue/EnumDisplay.tsx | 41 +++++++++++++ src/ViewValue/ListComponents.tsx | 52 +++++++++++++++++ src/ViewValue/StructDisplay.tsx | 49 ++++++++++++++++ src/ViewValue/TitleContext.tsx | 9 +++ src/ViewValue/ViewValue.tsx | 51 ++++++++++++++++ src/ViewValue/index.ts | 1 + src/ViewValue/utils.ts | 18 ++++++ src/ViewValue/view-components.tsx | 33 +++++++++++ src/codec-components/ViewCodec/CBytes.tsx | 14 +++-- src/pages/Storage/StorageQuery.tsx | 41 ++++++++----- src/pages/Storage/StorageSubscriptions.tsx | 68 +++++++++++++++------- src/pages/Storage/storage.state.ts | 6 ++ 12 files changed, 342 insertions(+), 41 deletions(-) create mode 100644 src/ViewValue/EnumDisplay.tsx create mode 100644 src/ViewValue/ListComponents.tsx create mode 100644 src/ViewValue/StructDisplay.tsx create mode 100644 src/ViewValue/TitleContext.tsx create mode 100644 src/ViewValue/ViewValue.tsx create mode 100644 src/ViewValue/index.ts create mode 100644 src/ViewValue/utils.ts create mode 100644 src/ViewValue/view-components.tsx diff --git a/src/ViewValue/EnumDisplay.tsx b/src/ViewValue/EnumDisplay.tsx new file mode 100644 index 0000000..b9a66ed --- /dev/null +++ b/src/ViewValue/EnumDisplay.tsx @@ -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> +}> = ({ value }) => { + const titleContainer = useContext(TitleContext) + const titleElement = useAppendTitle(titleContainer, "") + const [newElement, setNewElement] = useState(null) + + const inner = + + if (titleContainer) { + return ( + <> + {titleElement ? ( + / {value.type} + ) : null} + {inner} + + ) + } + + return ( +
+
+
+ {value.type} +
+
+
+ {inner} +
+
+ ) +} diff --git a/src/ViewValue/ListComponents.tsx b/src/ViewValue/ListComponents.tsx new file mode 100644 index 0000000..70ec5eb --- /dev/null +++ b/src/ViewValue/ListComponents.tsx @@ -0,0 +1,52 @@ +import { FC, PropsWithChildren } from "react" +import { ChildProvider } from "./TitleContext" +import { ViewValue } from "./ViewValue" + +export const ArrayDisplay: FC<{ value: unknown[] }> = ({ value }) => ( +
    + {value.length ? ( + value.map((innerValue, idx) => ( + + + + )) + ) : ( + (Empty) + )} +
+) + +const ListItemComponent: FC< + PropsWithChildren<{ + idx: number + }> +> = ({ idx, children }) => { + return ( + + {children} + + ) +} + +const ListItem: React.FC< + PropsWithChildren<{ + idx: number + }> +> = ({ idx, children }) => { + const title = ( +
+ + Item {idx + 1}. + +
+ ) + + return ( +
  • + {title} +
    + {children} +
    +
  • + ) +} diff --git a/src/ViewValue/StructDisplay.tsx b/src/ViewValue/StructDisplay.tsx new file mode 100644 index 0000000..c20321e --- /dev/null +++ b/src/ViewValue/StructDisplay.tsx @@ -0,0 +1,49 @@ +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" + +const StructItem: React.FC< + PropsWithChildren<{ + name: string + }> +> = ({ name, children }) => { + const [titleElement, setTitleElement] = useState(null) + + return ( +
  • + + + + + {name} + + +
    {children}
    +
    +
    +
  • + ) +} + +export const StructDisplay: FC<{ value: Record }> = ({ + value, +}) => { + const hasParentTitle = !!useContext(TitleContext) + + return ( +
      + {Object.entries(value).map(([name, value]) => ( + + + + ))} +
    + ) +} diff --git a/src/ViewValue/TitleContext.tsx b/src/ViewValue/TitleContext.tsx new file mode 100644 index 0000000..95157d6 --- /dev/null +++ b/src/ViewValue/TitleContext.tsx @@ -0,0 +1,9 @@ +import { createContext, FC, PropsWithChildren } from "react" + +export const TitleContext = createContext(null) + +export const ChildProvider: FC< + PropsWithChildren<{ titleElement: HTMLElement | null }> +> = ({ titleElement, children }) => ( + {children} +) diff --git a/src/ViewValue/ViewValue.tsx b/src/ViewValue/ViewValue.tsx new file mode 100644 index 0000000..6f2b161 --- /dev/null +++ b/src/ViewValue/ViewValue.tsx @@ -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 + if (value.startsWith("0x") && value.length === 42) + return + return + } + case "boolean": + return + case "number": + case "bigint": + return + case "object": { + if (value == null) return <>TODO + if (value instanceof Binary) return + if (Array.isArray(value)) return + if ("type" in value && typeof value.type === "string" && "value" in value) + return + if ( + "success" in value && + typeof value.success === "boolean" && + "value" in value + ) + return + return + } + case "undefined": + return + } + return
    (Uknown value)
    +} diff --git a/src/ViewValue/index.ts b/src/ViewValue/index.ts new file mode 100644 index 0000000..9c3e477 --- /dev/null +++ b/src/ViewValue/index.ts @@ -0,0 +1 @@ +export * from "./ViewValue" diff --git a/src/ViewValue/utils.ts b/src/ViewValue/utils.ts new file mode 100644 index 0000000..b9edf94 --- /dev/null +++ b/src/ViewValue/utils.ts @@ -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 +} diff --git a/src/ViewValue/view-components.tsx b/src/ViewValue/view-components.tsx new file mode 100644 index 0000000..f22c6e5 --- /dev/null +++ b/src/ViewValue/view-components.tsx @@ -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
    {value ? "Yes" : "No"}
    +} + +export const EthAccountDisplay: FC<{ value: HexString }> = ({ value }) => ( + {value} +) + +export const NoneDisplay: FC = () => ( + None +) + +export const ResultDisplay: FC<{ + value: { success: boolean; value: unknown } +}> = ({ value }) => { + return ( +
    +
    {value.success ? "OK" : "KO"}
    + +
    + ) +} + +export const StrDisplay: FC<{ value: string }> = ({ value }) => ( +
    {value}
    +) +export const NumberDisplay: FC<{ value: number | bigint }> = ({ value }) => ( +
    {String(value)}
    +) diff --git a/src/codec-components/ViewCodec/CBytes.tsx b/src/codec-components/ViewCodec/CBytes.tsx index 4dbf8c5..1f2d4fd 100644 --- a/src/codec-components/ViewCodec/CBytes.tsx +++ b/src/codec-components/ViewCodec/CBytes.tsx @@ -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 +} + +export const BytesDisplay: FC<{ value: Binary }> = ({ value }) => { const [forceBinary, setForceBinary] = useState(false) - useReportBinary(encodedValue) const format = getBytesFormat(value) return ( diff --git a/src/pages/Storage/StorageQuery.tsx b/src/pages/Storage/StorageQuery.tsx index 58330a9..8eb6610 100644 --- a/src/pages/Storage/StorageQuery.tsx +++ b/src/pages/Storage/StorageQuery.tsx @@ -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 } diff --git a/src/pages/Storage/StorageSubscriptions.tsx b/src/pages/Storage/StorageSubscriptions.tsx index 3851328..579d606 100644 --- a/src/pages/Storage/StorageSubscriptions.tsx +++ b/src/pages/Storage/StorageSubscriptions.tsx @@ -1,22 +1,24 @@ -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" export const StorageSubscriptions: FC = () => { const keys = useStateObservable(storageSubscriptionKeys$) @@ -131,24 +133,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 ( -
    - - - -
    - ) - } + const renderItem = (keyArgs: unknown[], value: unknown, idx: number) => ( +
    + + + + +
    + ) if (values.length > 10) { return ( @@ -251,3 +248,32 @@ export const ValueDisplay: FC<{ ) } + +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 ( +
    +
    + {binaryValue ? : null} +

    Key

    +
    +
      + {value.map((v, i) => ( +
    1. + +
    2. + ))} +
    +
    + ) +} diff --git a/src/pages/Storage/storage.state.ts b/src/pages/Storage/storage.state.ts index bc112d6..ab27401 100644 --- a/src/pages/Storage/storage.state.ts +++ b/src/pages/Storage/storage.state.ts @@ -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 }>() @@ -166,6 +171,7 @@ export type StorageSubscription = { name: string args: unknown[] | null type: number + keyCodec?: KeyCodec single: boolean paused: boolean completed: boolean From 58a414ce52f621a40498f5a9c0955edde70a116b Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Wed, 12 Nov 2025 16:49:35 -0300 Subject: [PATCH 2/2] fix struct values getting inlined --- src/ViewValue/StructDisplay.tsx | 36 +++++++++++++++++++--- src/pages/Storage/StorageSubscriptions.tsx | 8 ++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/ViewValue/StructDisplay.tsx b/src/ViewValue/StructDisplay.tsx index c20321e..12241ab 100644 --- a/src/ViewValue/StructDisplay.tsx +++ b/src/ViewValue/StructDisplay.tsx @@ -3,25 +3,51 @@ 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, children }) => { +> = ({ name, value, children }) => { const [titleElement, setTitleElement] = useState(null) + const [expanded, setExpanded] = useState(true) + + const isComplexShape = isComplexNested(value) return ( -
  • +
  • setExpanded((e) => !e)} + > - + {isComplexShape ? ( + + ) : ( + + )} {name} -
    {children}
    + {isComplexShape ? null :
    {children}
    }
    + {isComplexShape && expanded ?
    {children}
    : null}
  • ) @@ -40,7 +66,7 @@ export const StructDisplay: FC<{ value: Record }> = ({ )} > {Object.entries(value).map(([name, value]) => ( - + ))} diff --git a/src/pages/Storage/StorageSubscriptions.tsx b/src/pages/Storage/StorageSubscriptions.tsx index 579d606..c140c10 100644 --- a/src/pages/Storage/StorageSubscriptions.tsx +++ b/src/pages/Storage/StorageSubscriptions.tsx @@ -19,6 +19,7 @@ import { storageSubscriptionKeys$, toggleSubscriptionPause, } from "./storage.state" +import { cn } from "@/lib/utils" export const StorageSubscriptions: FC = () => { const keys = useStateObservable(storageSubscriptionKeys$) @@ -269,7 +270,12 @@ const KeyDisplay: FC<{
      {value.map((v, i) => ( -
    1. +
    2. 1, + })} + >
    3. ))}