Merge remote-tracking branch 'origin/main' into vo/storage-sub
# Conflicts: # src/pages/Explorer/block.state.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.1.0-autogenerated.9612188710233485064",
|
||||
"version": "0.1.0-autogenerated.8379875965201711085",
|
||||
"name": "@polkadot-api/descriptors",
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
+9
-9
@@ -16,10 +16,11 @@
|
||||
"dependencies": {
|
||||
"@acala-network/chopsticks-core": "^1.2.4",
|
||||
"@ledgerhq/hw-transport-webusb": "^6.29.13",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@polkadot-api/descriptors": "file:.papi/descriptors",
|
||||
"@polkadot-api/json-rpc-provider-proxy": "^0.2.6",
|
||||
"@polkadot-api/json-rpc-provider-proxy": "^0.2.7",
|
||||
"@polkadot-api/metadata-builders": "^0.13.7",
|
||||
"@polkadot-api/observable-client": "^0.16.1",
|
||||
"@polkadot-api/observable-client": "^0.16.2",
|
||||
"@polkadot-api/react-builder": "0.3.9",
|
||||
"@polkadot-api/react-components": "^0.4.1",
|
||||
"@polkadot-api/substrate-bindings": "^0.16.5",
|
||||
@@ -40,17 +41,16 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-rxjs/core": "^0.10.8",
|
||||
"@react-rxjs/utils": "^0.9.7",
|
||||
"buffer": "^6.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"polkadot-api": "^1.20.4",
|
||||
"polkadot-api": "^1.20.6",
|
||||
"polkahub": "0.3.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-portal": "^4.3.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"react-svg": "^16.3.0",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"react18-json-view": "^0.2.9",
|
||||
@@ -63,11 +63,11 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.3",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-portal": "^4.0.7",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
|
||||
Generated
+1021
-736
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "chain_getBlock params",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"description": "{} or { hash }.",
|
||||
"maxProperties": 1,
|
||||
"additionalProperties": false,
|
||||
"properties": { "hash": { "$ref": "./common.json#/$defs/hex" } }
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "[] or [hash].",
|
||||
"maxItems": 1,
|
||||
"items": { "$ref": "./common.json#/$defs/hex" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "chain_getBlockHash params",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"description": "[] or [blockNumber].",
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "chain_getHeader params",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"description": "{} or { hash }.",
|
||||
"maxProperties": 1,
|
||||
"additionalProperties": false,
|
||||
"properties": { "hash": { "$ref": "./common.json#/$defs/hex" } }
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "[] or [hash].",
|
||||
"maxItems": 1,
|
||||
"items": { "$ref": "./common.json#/$defs/hex" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "common.json",
|
||||
"$defs": {
|
||||
"hex": {
|
||||
"type": "string",
|
||||
"pattern": "^0x[0-9a-fA-F]*$",
|
||||
"description": "0x-prefixed hex."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "state_getReadProof params",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"description": "[keys] or [keys, at].",
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "array",
|
||||
"description": "Storage keys to prove.",
|
||||
"items": { "$ref": "./common.json#/$defs/hex" }
|
||||
},
|
||||
{ "$ref": "./common.json#/$defs/hex" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "{ keys } or { keys, at } or { keys, hash }.",
|
||||
"required": ["keys"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"description": "Storage keys to prove.",
|
||||
"items": { "$ref": "./common.json#/$defs/hex" }
|
||||
},
|
||||
"at": { "$ref": "./common.json#/$defs/hex" },
|
||||
"hash": { "$ref": "./common.json#/$defs/hex" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
-1
@@ -1,3 +1,4 @@
|
||||
import { VaultTxModal } from "polkahub"
|
||||
import { Navigate, Route, Routes } from "react-router-dom"
|
||||
import { Accounts } from "./pages/Accounts/Accounts"
|
||||
import { Constants } from "./pages/Constants"
|
||||
@@ -9,7 +10,7 @@ import { RpcCalls } from "./pages/RpcCalls"
|
||||
import { RuntimeCalls } from "./pages/RuntimeCalls"
|
||||
import { Storage } from "./pages/Storage"
|
||||
import { Transactions } from "./pages/Transactions"
|
||||
import { VaultTxModal } from "polkahub"
|
||||
import { ViewFns } from "./pages/ViewFns"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -26,6 +27,7 @@ export default function App() {
|
||||
<Route path="rpcCalls/*" element={<RpcCalls />} />
|
||||
<Route path="metadata/*" element={<Metadata />} />
|
||||
<Route path="accounts/*" element={<Accounts />} />
|
||||
<Route path="viewFns/*" element={<ViewFns />} />
|
||||
<Route path="*" element={<Navigate to="/explorer" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lookupToType, TypeIcon, TypeIcons } from "@/components/Icons"
|
||||
import { getLookupFn, Var } from "@polkadot-api/metadata-builders"
|
||||
import { UnifiedMetadata } from "@polkadot-api/substrate-bindings"
|
||||
import { ArrowLeftToLine, ArrowRightToLine } from "lucide-react"
|
||||
import { FC, ReactElement } from "react"
|
||||
import { twMerge as clsx } from "tailwind-merge"
|
||||
|
||||
@@ -9,12 +10,23 @@ export const FocusPath: FC<{
|
||||
typeId: number
|
||||
value: string[] | null
|
||||
onFocus: (value: string[] | null) => void
|
||||
}> = ({ metadata, typeId, value, onFocus }) => {
|
||||
collapsed?: boolean
|
||||
onToggleCollapse?: () => void
|
||||
}> = ({ metadata, typeId, value, onFocus, collapsed, onToggleCollapse }) => {
|
||||
const breadcrumbs = getBreadcrumbs(metadata, typeId, value ?? [], onFocus)
|
||||
|
||||
return (
|
||||
<div className="shrink-0 px-2">
|
||||
<div className="flex max-w-full flex-wrap gap-1 items-center border border-border text-sm px-2 py-1">
|
||||
{onToggleCollapse ? (
|
||||
<button onClick={onToggleCollapse} className="max-sm:hidden">
|
||||
{collapsed ? (
|
||||
<ArrowRightToLine size={16} />
|
||||
) : (
|
||||
<ArrowLeftToLine size={16} />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
{breadcrumbs.map((v, i) => (
|
||||
<span key={i} className="whitespace-nowrap flex gap-1 items-center">
|
||||
{i > 0 && <span className="text-foreground/50 mx-2">></span>}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { EditCodec } from "../EditCodec"
|
||||
import { TreeCodec } from "../EditCodec/Tree"
|
||||
import { BinaryDisplay } from "./BinaryDisplay"
|
||||
import { FocusPath } from "./FocusPath"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const editTypeMetadataProps$ = state(
|
||||
runtimeCtx$.pipe(
|
||||
@@ -41,6 +42,7 @@ export const LookupTypeEdit: FC<{
|
||||
const [treeRef, setTreeRef] = useState<HTMLDivElement | null>(null)
|
||||
const [listRef, setListRef] = useState<HTMLDivElement | null>(null)
|
||||
const [focusingSubtree, setFocusingSubtree] = useState<string[] | null>(null)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!listRef || !treeRef) return
|
||||
@@ -65,6 +67,8 @@ export const LookupTypeEdit: FC<{
|
||||
typeId={type}
|
||||
value={focusingSubtree}
|
||||
onFocus={setFocusingSubtree}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapse={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
)}
|
||||
<SubtreeFocus
|
||||
@@ -78,7 +82,12 @@ export const LookupTypeEdit: FC<{
|
||||
{tree && (
|
||||
<div
|
||||
ref={setTreeRef}
|
||||
className="w-96 sticky top-0 pl-2 pb-16 leading-loose overflow-hidden max-sm:hidden"
|
||||
className={cn(
|
||||
"w-96 sticky top-0 pl-2 pb-16 leading-loose overflow-hidden max-sm:hidden",
|
||||
{
|
||||
hidden: collapsed,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<TreeCodec {...codecProps} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Binary } from "@polkadot-api/substrate-bindings"
|
||||
import { FileUp } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { SwitchBinary } from "./Icons"
|
||||
import { TextInputField } from "./TextInputField"
|
||||
import { useGenericSynchronizeInput } from "./useSynchroniseInput"
|
||||
|
||||
@@ -76,6 +77,19 @@ export const BinaryInput: React.FC<{
|
||||
uploadError ? "border-red-600" : null,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="align-middle mr-2 cursor-pointer text-foreground/90"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (inputValue.startsWith("0x")) {
|
||||
setLocalInput(serializeValue(Binary.fromHex(inputValue)))
|
||||
} else {
|
||||
setLocalInput(Binary.fromText(inputValue).asHex())
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SwitchBinary size={24} />
|
||||
</button>
|
||||
{input}
|
||||
<label title="Load from file" className="text-foreground/80">
|
||||
<FileUp size={16} />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Buffer } from "buffer"
|
||||
globalThis.Buffer = Buffer
|
||||
import { dynamicBuilder$ } from "@/state/chains/chain.state"
|
||||
import { RemoveSubscribe, Subscribe } from "@react-rxjs/core"
|
||||
import { PolkaHubProvider } from "polkahub"
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { client$ } from "@/state/chains/chain.state"
|
||||
import { Polkadot_people } from "@polkadot-api/descriptors"
|
||||
import {
|
||||
_void,
|
||||
Bytes,
|
||||
HexString,
|
||||
Struct,
|
||||
u32,
|
||||
u64,
|
||||
Variant,
|
||||
} from "@polkadot-api/substrate-bindings"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { BlockHeader, PolkadotClient } from "polkadot-api"
|
||||
import { AddressIdentity } from "polkahub"
|
||||
import { FC, useEffect } from "react"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs"
|
||||
import { blockInfo$, BlockState, finalized$ } from "../block.state"
|
||||
|
||||
const validatorCache: WeakMap<
|
||||
PolkadotClient,
|
||||
Record<number, string[]>
|
||||
> = new WeakMap()
|
||||
|
||||
const fetchValidators = (
|
||||
client: PolkadotClient,
|
||||
at: HexString,
|
||||
): Promise<string[]> =>
|
||||
client.getUnsafeApi<Polkadot_people>().query.Session.Validators.getValue({
|
||||
at,
|
||||
})
|
||||
const fetchCachedValidators$ = (client: PolkadotClient, at: HexString) => {
|
||||
const api = client.getUnsafeApi<Polkadot_people>()
|
||||
return from(api.query.Session.CurrentIndex.getValue()).pipe(
|
||||
switchMap(async (idx) => {
|
||||
const cache = validatorCache.get(client) ?? {}
|
||||
validatorCache.set(client, cache)
|
||||
|
||||
const cached = cache[idx]
|
||||
if (cached) return { idx, validators: cached }
|
||||
const validators = await fetchValidators(client, at)
|
||||
return { idx, validators }
|
||||
}),
|
||||
)
|
||||
}
|
||||
const validators$ = state(
|
||||
(block: HexString) =>
|
||||
client$.pipe(
|
||||
switchMap((client) =>
|
||||
combineLatest([
|
||||
fetchCachedValidators$(client, block),
|
||||
blockInfo$(block).pipe(map((v) => v.status)),
|
||||
]).pipe(
|
||||
map(([{ idx, validators }, blockStatus]) => {
|
||||
if (
|
||||
blockStatus === BlockState.Finalized ||
|
||||
blockStatus === BlockState.Unknown
|
||||
) {
|
||||
const cache = validatorCache.get(client) ?? {}
|
||||
validatorCache.set(client, cache)
|
||||
cache[idx] = validators
|
||||
}
|
||||
return validators
|
||||
}),
|
||||
withHodl(() => client.hodlBlock(block)),
|
||||
take(1),
|
||||
catchError(() => [null]),
|
||||
),
|
||||
),
|
||||
),
|
||||
null,
|
||||
)
|
||||
|
||||
// Subscribe to the validators of the finalized block so that best blocks author's of the same session will load faster
|
||||
const finalizedValidators$ = finalized$.pipe(
|
||||
switchMap((block) => validators$(block.hash)),
|
||||
)
|
||||
|
||||
export const BlockAuthor: FC<{ hash: HexString; header: BlockHeader }> = ({
|
||||
hash,
|
||||
header,
|
||||
}) => {
|
||||
const validators = useStateObservable(validators$(hash))
|
||||
const idx = getAuthorityIdx(header)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = finalizedValidators$.subscribe()
|
||||
return () => sub.unsubscribe()
|
||||
}, [])
|
||||
|
||||
if (idx == null || validators == null) return <div className="h-10" />
|
||||
|
||||
return (
|
||||
<AddressIdentity
|
||||
addr={validators[Number(idx % BigInt(validators.length))]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DigestWithVRF = Struct({
|
||||
authority_index: u32,
|
||||
slot: u64,
|
||||
vrf_signature: Struct({
|
||||
pre_output: Bytes(32),
|
||||
proof: Bytes(64),
|
||||
}),
|
||||
})
|
||||
const BabePreDigest = Variant({
|
||||
Uknown: _void,
|
||||
Primary: DigestWithVRF,
|
||||
SecondaryPlain: Struct({
|
||||
authority_index: u32,
|
||||
slot: u64,
|
||||
}),
|
||||
SecondaryVRF: DigestWithVRF,
|
||||
})
|
||||
|
||||
const getAuthorityIdx = (header: BlockHeader) => {
|
||||
const preRuntime = header.digests.find((d) => d.type === "preRuntime")?.value
|
||||
|
||||
switch (preRuntime?.engine) {
|
||||
case "BABE": {
|
||||
const babe = BabePreDigest.dec(preRuntime.payload)
|
||||
if (!babe.value) return null
|
||||
return BigInt(babe.value.authority_index)
|
||||
}
|
||||
case "aura": {
|
||||
return u64.dec(preRuntime.payload)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const withHodl =
|
||||
<T,>(hodl: () => () => void) =>
|
||||
(source$: Observable<T>) =>
|
||||
new Observable<T>((obs) => {
|
||||
obs.add(source$.subscribe(obs))
|
||||
try {
|
||||
const hodled = hodl()
|
||||
obs.add(hodled)
|
||||
} catch (_) {
|
||||
// Nothing, it might happen if the block is not pinned
|
||||
}
|
||||
return obs
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { CopyText } from "@/components/Copy"
|
||||
import { Link } from "@/hashParams"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { combineKeys } from "@react-rxjs/utils"
|
||||
import { FC } from "react"
|
||||
import { FC, ReactNode } from "react"
|
||||
import { filter, map, startWith, switchMap, take } from "rxjs"
|
||||
import {
|
||||
BlockInfo,
|
||||
@@ -10,53 +10,77 @@ import {
|
||||
blocksByHeight$,
|
||||
BlockState,
|
||||
} from "../block.state"
|
||||
import { BlockAuthor } from "./BlockAuthor"
|
||||
import { BlockStatusIcon, statusText } from "./BlockState"
|
||||
|
||||
export const BlockInfoView: FC<{
|
||||
block: BlockInfo
|
||||
}> = ({ block }) => (
|
||||
<div className="p-2">
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<h2 className="font-bold text-xl whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
Block {block.hash}
|
||||
</h2>
|
||||
<p className="text-xl">{block.number.toLocaleString()}</p>
|
||||
</div>
|
||||
<p className="flex gap-1 items-center py-1">
|
||||
Status:
|
||||
<BlockStatusIcon state={block.status} />
|
||||
{statusText[block.status]}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div>Parent</div>
|
||||
<section className="space-y-4 px-4 py-2">
|
||||
<header className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="text-3xl font-semibold tracking-tight">
|
||||
#{block.number.toLocaleString()}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-mono text-foreground/90">
|
||||
<span className="truncate max-w-full">{block.hash}</span>
|
||||
<CopyText className="text-foreground/70" text={block.hash} binary />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
||||
<StatusChip state={block.status} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<DetailTile label="Parent block">
|
||||
<BlockLink hash={block.parent} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div>Children</div>
|
||||
</DetailTile>
|
||||
<DetailTile label="Children">
|
||||
<BlockChildren hash={block.hash} />
|
||||
</div>
|
||||
</DetailTile>
|
||||
</div>
|
||||
|
||||
{block.header && (
|
||||
<div className="text-foreground/80 py-2">
|
||||
<p>
|
||||
State root: {block.header.stateRoot.slice(0, 18)}{" "}
|
||||
<CopyText
|
||||
className="align-middle"
|
||||
text={block.header.stateRoot}
|
||||
binary
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
Extrinsic root: {block.header.extrinsicRoot.slice(0, 18)}{" "}
|
||||
<CopyText
|
||||
className="align-middle"
|
||||
text={block.header.extrinsicRoot}
|
||||
binary
|
||||
/>
|
||||
</p>
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||
<DetailTile label="Roots">
|
||||
<HashPreview title="state" value={block.header.stateRoot} />
|
||||
<HashPreview title="extrinsic" value={block.header.extrinsicRoot} />
|
||||
</DetailTile>
|
||||
<DetailTile label="Block author">
|
||||
<BlockAuthor hash={block.hash} header={block.header} />
|
||||
</DetailTile>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
const StatusChip: FC<{ state: BlockState }> = ({ state }) => (
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-foreground/20 bg-foreground/2 px-4 py-2 text-sm font-medium">
|
||||
<BlockStatusIcon size={20} state={state} />
|
||||
{statusText[state]}
|
||||
</span>
|
||||
)
|
||||
|
||||
const HashPreview: FC<{ title: string; value: string }> = ({
|
||||
title,
|
||||
value,
|
||||
}) => (
|
||||
<div className="font-mono text-sm">
|
||||
{title}: {value.slice(0, 18)}…
|
||||
<CopyText className="align-middle" text={value} binary />
|
||||
</div>
|
||||
)
|
||||
|
||||
const DetailTile: FC<{ label: string; children: ReactNode }> = ({
|
||||
label,
|
||||
children,
|
||||
}) => (
|
||||
<div className="rounded-xl border border-foreground/10 bg-foreground/2 p-4">
|
||||
<p className="text-xs uppercase tracking-widest text-foreground/60">
|
||||
{label}
|
||||
</p>
|
||||
<div className="mt-2 text-base text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { chopsticksInstance$ } from "@/chopsticks/chopsticks"
|
||||
import { chainClient$, client$ } from "@/state/chains/chain.state"
|
||||
import { SystemEvent } from "@polkadot-api/observable-client"
|
||||
import { blockHeader } from "@polkadot-api/substrate-bindings"
|
||||
import { liftSuspense, state } from "@react-rxjs/core"
|
||||
import { combineKeys, partitionByKey } from "@react-rxjs/utils"
|
||||
import { FixedSizeBinary, HexString, PolkadotClient } from "polkadot-api"
|
||||
import {
|
||||
BlockHeader,
|
||||
FixedSizeBinary,
|
||||
HexString,
|
||||
PolkadotClient,
|
||||
BlockInfo as RawBlockInfo,
|
||||
} from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -33,7 +40,6 @@ import {
|
||||
toArray,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { BlockInfo as RawBlockInfo } from "polkadot-api"
|
||||
|
||||
export const finalized$ = client$.pipeState(
|
||||
switchMap((client) => client.finalizedBlock$),
|
||||
@@ -52,13 +58,7 @@ export interface BlockInfo {
|
||||
number: number
|
||||
body: string[] | null
|
||||
events: SystemEvent[] | null
|
||||
header: {
|
||||
parentHash: HexString
|
||||
number: number
|
||||
stateRoot: HexString
|
||||
extrinsicRoot: HexString
|
||||
digests: unknown[]
|
||||
} | null
|
||||
header: BlockHeader | null
|
||||
status: BlockState
|
||||
diff: Record<string, [string | null, string | null]> | null
|
||||
}
|
||||
@@ -175,6 +175,7 @@ const getUnpinnedBlockInfo$ = (hash: string): Observable<BlockInfo> =>
|
||||
),
|
||||
)
|
||||
|
||||
const digestCodec = blockHeader.inner.digests.inner
|
||||
const getUnpinnedBlockInfoFallback$ = (
|
||||
hash: string,
|
||||
client: PolkadotClient,
|
||||
@@ -185,7 +186,7 @@ const getUnpinnedBlockInfoFallback$ = (
|
||||
block: {
|
||||
extrinsics: HexString[]
|
||||
header: {
|
||||
digest: { logs: Array<unknown> }
|
||||
digest: { logs: Array<string> }
|
||||
extrinsicsRoot: string
|
||||
number: HexString
|
||||
parentHash: HexString
|
||||
@@ -223,7 +224,16 @@ const getUnpinnedBlockInfoFallback$ = (
|
||||
body: extrinsics,
|
||||
events: null,
|
||||
header: {
|
||||
digests: header.digest.logs,
|
||||
digests: header.digest.logs
|
||||
.map((log) => {
|
||||
try {
|
||||
return digestCodec.dec(log)
|
||||
} catch (ex) {
|
||||
console.error(ex)
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((v) => v != null),
|
||||
extrinsicRoot: header.extrinsicsRoot,
|
||||
number,
|
||||
parentHash: header.parentHash,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SubtreeFocus } from "@/codec-components/common/SubtreeFocus"
|
||||
import { EditCodec } from "@/codec-components/EditCodec"
|
||||
import { TreeCodec } from "@/codec-components/EditCodec/Tree/index"
|
||||
import { FocusPath } from "@/codec-components/LookupTypeEdit"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
CodecComponentUpdate,
|
||||
CodecComponentValue,
|
||||
@@ -31,6 +32,7 @@ export const EditMode: React.FC<{
|
||||
}, [])
|
||||
|
||||
const [focusingSubtree, setFocusingSubtree] = useState<string[] | null>(null)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start overflow-hidden">
|
||||
@@ -39,6 +41,8 @@ export const EditMode: React.FC<{
|
||||
typeId={codecType}
|
||||
value={focusingSubtree}
|
||||
onFocus={setFocusingSubtree}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapse={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
<SubtreeFocus
|
||||
value={{ callback: setFocusingSubtree, path: focusingSubtree }}
|
||||
@@ -50,7 +54,12 @@ export const EditMode: React.FC<{
|
||||
>
|
||||
<div
|
||||
ref={treeRef}
|
||||
className="min-w-fit w-96 sticky top-0 pl-2 pb-16 leading-loose overflow-hidden max-sm:hidden text-sm"
|
||||
className={cn(
|
||||
"min-w-fit w-96 sticky top-0 pl-2 pb-16 leading-loose overflow-hidden max-sm:hidden text-sm",
|
||||
{
|
||||
hidden: collapsed,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
{!value.value ? (
|
||||
|
||||
@@ -82,6 +82,9 @@ export const Header = () => {
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<NavLink to="/viewFns" onClick={() => setIsOpen(false)}>
|
||||
View Functions
|
||||
</NavLink>
|
||||
<NavLink to="/metadata" onClick={() => setIsOpen(false)}>
|
||||
Metadata
|
||||
</NavLink>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CopyText } from "@/components/Copy"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { UnifiedMetadata } from "@polkadot-api/substrate-bindings"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import { FC, Fragment, useState } from "react"
|
||||
import { LookupLink } from "./Lookup"
|
||||
import { CopyText } from "@/components/Copy"
|
||||
|
||||
type Pallet = UnifiedMetadata["pallets"][number]
|
||||
export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
|
||||
@@ -22,7 +22,7 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
|
||||
/>
|
||||
</label>
|
||||
{pallet && (
|
||||
<>
|
||||
<Fragment key={pallet.name}>
|
||||
<p>Index: {pallet.index}</p>
|
||||
{pallet.storage && (
|
||||
<div>
|
||||
@@ -36,6 +36,12 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
|
||||
<PalletConstants pallet={pallet} />
|
||||
</div>
|
||||
)}
|
||||
{pallet.viewFns.length > 0 && (
|
||||
<div>
|
||||
<h4>View Functions</h4>
|
||||
<PalletViewFns pallet={pallet} />
|
||||
</div>
|
||||
)}
|
||||
{pallet.calls != null && (
|
||||
<div>
|
||||
<h4>Calls</h4>
|
||||
@@ -54,7 +60,7 @@ export const Pallets: FC<{ pallets: Array<Pallet> }> = ({ pallets }) => {
|
||||
<LookupLink id={pallet.errors.type} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -68,10 +74,6 @@ type StorageEntry = Pallet["storage"] extends
|
||||
const PalletStorage: FC<{ pallet: Pallet }> = ({ pallet }) => {
|
||||
const [entry, setEntry] = useState<StorageEntry | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setEntry(null)
|
||||
}, [pallet])
|
||||
|
||||
if (!pallet.storage) return null
|
||||
|
||||
const value =
|
||||
@@ -129,10 +131,6 @@ type ConstantEntry = Pallet["constants"] extends Array<infer R> ? R : never
|
||||
const PalletConstants: FC<{ pallet: Pallet }> = ({ pallet }) => {
|
||||
const [entry, setEntry] = useState<ConstantEntry | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setEntry(null)
|
||||
}, [pallet])
|
||||
|
||||
if (!pallet.constants.length) return null
|
||||
|
||||
return (
|
||||
@@ -166,3 +164,42 @@ const PalletConstants: FC<{ pallet: Pallet }> = ({ pallet }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ViewFnEntry = Pallet["viewFns"] extends Array<infer R> ? R : never
|
||||
const PalletViewFns: FC<{ pallet: Pallet }> = ({ pallet }) => {
|
||||
const [entry, setEntry] = useState<ViewFnEntry | null>(null)
|
||||
|
||||
if (!pallet.viewFns.length) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-2 gap-2">
|
||||
<SearchableSelect
|
||||
value={entry}
|
||||
setValue={setEntry}
|
||||
options={pallet.viewFns.map((c) => ({
|
||||
text: c.name,
|
||||
value: c,
|
||||
}))}
|
||||
/>
|
||||
{entry && (
|
||||
<>
|
||||
<div>
|
||||
<h4>Inputs</h4>
|
||||
<ol>
|
||||
{entry.inputs.map((input) => (
|
||||
<li key={input.name}>
|
||||
<div>{input.name}</div>
|
||||
<LookupLink id={input.type} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Output</h4>
|
||||
<LookupLink id={entry.output} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { chainClient$ } from "@/state/chains/chain.state"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { useState } from "react"
|
||||
import { lazy, Suspense, useRef, useState } from "react"
|
||||
import { firstValueFrom, map, switchMap } from "rxjs"
|
||||
import { RpcCallResults } from "./RpcCallResults"
|
||||
import { addRpcCallQuery } from "./rpcCalls.state"
|
||||
import { useTheme } from "@/ThemeProvider"
|
||||
|
||||
const Editor = lazy(() => import("@monaco-editor/react"))
|
||||
|
||||
const chainRpcMethods$ = state(
|
||||
chainClient$.pipe(
|
||||
@@ -19,11 +21,19 @@ const chainRpcMethods$ = state(
|
||||
),
|
||||
)
|
||||
|
||||
const SCHEMA_URL = window.location.origin
|
||||
export const RpcCalls = withSubscribe(
|
||||
() => {
|
||||
const methods = useStateObservable(chainRpcMethods$)
|
||||
const [method, setMethod] = useState<string | null>(null)
|
||||
const [params, setParams] = useState<string>("[\n\n]")
|
||||
const setSchema = useRef<((method: string | null) => void) | null>(null)
|
||||
const [method, _setMethod] = useState<string | null>(null)
|
||||
const [params, setParams] = useState<string>("[\n \n]")
|
||||
const theme = useTheme()
|
||||
|
||||
const setMethod = (method: string | null) => {
|
||||
_setMethod(method)
|
||||
setSchema.current?.(method)
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const { client } = await firstValueFrom(chainClient$)
|
||||
@@ -54,14 +64,56 @@ export const RpcCalls = withSubscribe(
|
||||
allowCustomValue
|
||||
/>
|
||||
</label>
|
||||
<label className="w-full">
|
||||
<div className="w-full">
|
||||
JSON Payload
|
||||
<Textarea
|
||||
className="w-full font-mono text-sm"
|
||||
value={params}
|
||||
onChange={(e) => setParams(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<Suspense
|
||||
fallback={<div className="border rounded h-80 w-full" />}
|
||||
>
|
||||
<Editor
|
||||
className="border rounded h-80"
|
||||
language="json"
|
||||
onMount={(editor, monaco) => {
|
||||
const model = editor.getModel()
|
||||
if (!model) return
|
||||
|
||||
setSchema.current = (method) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: method
|
||||
? [
|
||||
{
|
||||
uri: `${SCHEMA_URL}/rpc_schemas/${method}.json`,
|
||||
fileMatch: [model.uri.toString()],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
enableSchemaRequest: true,
|
||||
schemaRequest: "ignore",
|
||||
})
|
||||
monaco.languages.json.jsonDefaults.setModeConfiguration(
|
||||
method
|
||||
? { completionItems: true, diagnostics: true }
|
||||
: {
|
||||
completionItems: false,
|
||||
diagnostics: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
setSchema.current(method)
|
||||
}}
|
||||
value={params}
|
||||
loading={<div className="border rounded p-1 h-80 w-full" />}
|
||||
onChange={(v) => setParams(v ?? "[]")}
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
lineNumbers: "off",
|
||||
}}
|
||||
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<ActionButton disabled={!isReady} onClick={submit}>
|
||||
Call
|
||||
</ActionButton>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
dynamicBuilder$,
|
||||
lookup$,
|
||||
unsafeApi$,
|
||||
} from "@/state/chains/chain.state"
|
||||
import {
|
||||
InlineLookupTypeEdit,
|
||||
LookupTypeEdit,
|
||||
} from "@/codec-components/LookupTypeEdit"
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { ExpandBtn } from "@/components/Expand"
|
||||
import { getTypeComplexity } from "@/utils/shape"
|
||||
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { Circle, Dot } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
scan,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { addViewFnCall, selectedEntry$ } from "./viewFns.state"
|
||||
|
||||
export const ViewFnQuery: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
const isReady = useStateObservable(isReady$)
|
||||
|
||||
if (!selectedEntry) return null
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, unsafeApi, inputValues, builder] = await firstValueFrom(
|
||||
combineLatest([
|
||||
selectedEntry$,
|
||||
unsafeApi$,
|
||||
inputValues$,
|
||||
dynamicBuilder$,
|
||||
]),
|
||||
)
|
||||
const decodedValues = inputValues.map((v, i) =>
|
||||
builder
|
||||
.buildDefinition(selectedEntry.inputs[i].type)
|
||||
.dec(v as Uint8Array),
|
||||
)
|
||||
const promise = unsafeApi.view[entry!.pallet][entry!.name](...decodedValues)
|
||||
|
||||
addViewFnCall({
|
||||
name: `${entry!.pallet}.${entry!.name}(…)`,
|
||||
promise,
|
||||
type: entry!.output,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-start w-full">
|
||||
<ViewFnInputValues />
|
||||
<ActionButton disabled={!isReady} onClick={submit}>
|
||||
Call
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [inputValueChange$, setInputValue] = createSignal<{
|
||||
idx: number
|
||||
value: Uint8Array | "partial" | null
|
||||
}>()
|
||||
const inputValues$ = selectedEntry$.pipeState(
|
||||
filter((v) => !!v),
|
||||
map((v) => v.inputs),
|
||||
switchMap((inputs) => {
|
||||
const values: Array<Uint8Array | "partial" | null> = inputs.map(() => null)
|
||||
return inputValueChange$.pipe(
|
||||
scan((acc, change) => {
|
||||
const newValue = [...acc]
|
||||
newValue[change.idx] = change.value
|
||||
return newValue
|
||||
}, values),
|
||||
startWith(values),
|
||||
)
|
||||
}),
|
||||
withDefault([] as Array<Uint8Array | "partial" | null>),
|
||||
)
|
||||
|
||||
const isReady$ = inputValues$.pipeState(
|
||||
map((inputValues) => inputValues.every((v) => v instanceof Uint8Array)),
|
||||
withDefault(false),
|
||||
)
|
||||
|
||||
const ViewFnInputValues: FC = () => {
|
||||
const selectedEntry = useStateObservable(selectedEntry$)
|
||||
if (!selectedEntry || !selectedEntry.inputs.length) return null
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
Inputs
|
||||
<ol className="flex flex-col gap-2">
|
||||
{selectedEntry.inputs.map((input, idx) => (
|
||||
<RuntimeValueInput
|
||||
key={idx}
|
||||
idx={idx}
|
||||
name={input.name}
|
||||
type={input.type}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputValue$ = state(
|
||||
(idx: number) => inputValues$.pipe(map((v) => v[idx])),
|
||||
null,
|
||||
)
|
||||
const lookupState$ = state(lookup$, null)
|
||||
const RuntimeValueInput: FC<{ idx: number; name: string; type: number }> = ({
|
||||
idx,
|
||||
type,
|
||||
name,
|
||||
}) => {
|
||||
const value = useStateObservable(inputValue$(idx))
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const lookup = useStateObservable(lookupState$)
|
||||
|
||||
if (!lookup) return null
|
||||
const shape = lookup(type)
|
||||
const complexity = getTypeComplexity(shape)
|
||||
|
||||
return (
|
||||
<li key={idx} className="border rounded p-2 w-full">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center select-none",
|
||||
complexity !== "inline" && "cursor-pointer",
|
||||
)}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
>
|
||||
{complexity === "inline" ? (
|
||||
<Dot size={16} />
|
||||
) : (
|
||||
<ExpandBtn expanded={expanded} />
|
||||
)}
|
||||
<Circle
|
||||
size={8}
|
||||
strokeWidth={4}
|
||||
className={twMerge(
|
||||
"mr-1",
|
||||
value === null
|
||||
? "text-red-600"
|
||||
: value === "partial"
|
||||
? "text-orange-600"
|
||||
: "text-green-600",
|
||||
)}
|
||||
/>
|
||||
<div className="text-foreground/80">{name}</div>
|
||||
{complexity === "inline" ? (
|
||||
<div className="px-2">
|
||||
<InlineLookupTypeEdit
|
||||
type={type}
|
||||
value={value}
|
||||
onValueChange={(value) => setInputValue({ idx, value })}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{expanded && complexity !== "inline" && (
|
||||
<div className="py-2 max-h-[60svh] overflow-hidden flex flex-col justify-stretch">
|
||||
<LookupTypeEdit
|
||||
type={type}
|
||||
value={value}
|
||||
onValueChange={(value) => setInputValue({ idx, value })}
|
||||
tree={complexity === "tree"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import { ValueDisplay } from "../Storage/StorageSubscriptions"
|
||||
import {
|
||||
removeViewFnResult,
|
||||
ViewFnResult,
|
||||
viewFnResult$,
|
||||
viewFnResultKeys$,
|
||||
} from "./viewFns.state"
|
||||
import { PathsRoot } from "@/codec-components/common/paths.state"
|
||||
|
||||
export const ViewFnResults: FC = () => {
|
||||
const keys = useStateObservable(viewFnResultKeys$)
|
||||
|
||||
if (!keys.length) return null
|
||||
|
||||
return (
|
||||
<div className="p-2 w-full border-t border-border">
|
||||
<h2 className="text-lg text-foreground mb-2">Results</h2>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{keys.map((key) => (
|
||||
<ViewFnResultBox key={key} subscription={key} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ViewFnResultBox: FC<{ subscription: string }> = ({ subscription }) => {
|
||||
const [mode, setMode] = useState<"json" | "decoded">("decoded")
|
||||
const viewFnResult = useStateObservable(viewFnResult$(subscription))
|
||||
if (!viewFnResult) return null
|
||||
|
||||
return (
|
||||
<li className="border rounded bg-card text-card-foreground p-2">
|
||||
<div className="flex justify-between items-center pb-1 overflow-hidden">
|
||||
<h3 className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{viewFnResult.name}
|
||||
</h3>
|
||||
<div className="flex items-center shrink-0 gap-2">
|
||||
<ButtonGroup
|
||||
value={mode}
|
||||
onValueChange={setMode as any}
|
||||
items={[
|
||||
{
|
||||
value: "decoded",
|
||||
content: "Decoded",
|
||||
},
|
||||
{
|
||||
value: "json",
|
||||
content: "JSON",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<button onClick={() => removeViewFnResult(subscription)}>
|
||||
<Trash2
|
||||
size={20}
|
||||
className="text-destructive cursor-pointer hover:text-polkadot-500"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PathsRoot.Provider value={subscription}>
|
||||
<ResultDisplay viewFnResult={viewFnResult} mode={mode} />
|
||||
</PathsRoot.Provider>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const ResultDisplay: FC<{
|
||||
viewFnResult: ViewFnResult
|
||||
mode: "json" | "decoded"
|
||||
}> = ({ viewFnResult, mode }) => {
|
||||
if ("error" in viewFnResult) {
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<div>The call crashed</div>
|
||||
<div>Message: {viewFnResult.error.message ?? "N/A"}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!("result" in viewFnResult)) {
|
||||
return <div className="text-sm text-foreground/50">Loading…</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-h-[60svh] overflow-auto">
|
||||
<ValueDisplay
|
||||
mode={mode}
|
||||
type={viewFnResult.type}
|
||||
value={viewFnResult.result}
|
||||
title={"Result"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { lookup$ } from "@/state/chains/chain.state"
|
||||
import { DocsRenderer } from "@/components/DocsRenderer"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { useEffect, useState } from "react"
|
||||
import { map } from "rxjs"
|
||||
import { ViewFnResults } from "./ViewFnResults"
|
||||
import { selectedEntry$, setSelectedFn } from "./viewFns.state"
|
||||
import { ViewFnQuery } from "./ViewFnQuery"
|
||||
|
||||
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]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
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 (
|
||||
<div 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 />
|
||||
<ViewFnResults />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fallback: <LoadingMetadata />,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ViewFns"
|
||||
@@ -0,0 +1,79 @@
|
||||
import { UnifiedMetadata } from "@polkadot-api/substrate-bindings"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import {
|
||||
createKeyedSignal,
|
||||
createSignal,
|
||||
partitionByKey,
|
||||
toKeySet,
|
||||
} from "@react-rxjs/utils"
|
||||
import {
|
||||
catchError,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs"
|
||||
import { v4 as uuid } from "uuid"
|
||||
|
||||
type Pallet = UnifiedMetadata["pallets"][number]
|
||||
export type ViewFnEntry = Pallet["viewFns"][number] & {
|
||||
pallet: string
|
||||
}
|
||||
|
||||
export const [entryChange$, setSelectedFn] = createSignal<ViewFnEntry | null>()
|
||||
export const selectedEntry$ = state(entryChange$, null)
|
||||
|
||||
export const [newViewFnCall$, addViewFnCall] = createSignal<{
|
||||
name: string
|
||||
type: number
|
||||
promise: Promise<unknown>
|
||||
}>()
|
||||
export const [removeViewFnResult$, removeViewFnResult] =
|
||||
createKeyedSignal<string>()
|
||||
|
||||
export type ViewFnResult = {
|
||||
name: string
|
||||
type: number
|
||||
} & ({ result: unknown } | { error?: any })
|
||||
const [getViewFnSubscription$, viewFnKeyChange$] = partitionByKey(
|
||||
newViewFnCall$,
|
||||
() => uuid(),
|
||||
(src$, id) =>
|
||||
src$.pipe(
|
||||
switchMap(
|
||||
({ promise, ...props }): Observable<ViewFnResult> =>
|
||||
from(promise).pipe(
|
||||
map((result) => ({
|
||||
...props,
|
||||
result,
|
||||
paused: false,
|
||||
})),
|
||||
catchError((ex) => {
|
||||
console.error(ex)
|
||||
return of({
|
||||
...props,
|
||||
error: ex,
|
||||
})
|
||||
}),
|
||||
startWith(props),
|
||||
),
|
||||
),
|
||||
takeUntil(removeViewFnResult$(id)),
|
||||
),
|
||||
)
|
||||
|
||||
export const viewFnResultKeys$ = state(
|
||||
viewFnKeyChange$.pipe(
|
||||
toKeySet(),
|
||||
map((keys) => [...keys].reverse()),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
export const viewFnResult$ = state(
|
||||
(key: string): Observable<ViewFnResult> => getViewFnSubscription$(key),
|
||||
null,
|
||||
)
|
||||
@@ -5,4 +5,6 @@ export const whitelist: Polkadot_peopleWhitelistEntry[] = [
|
||||
"query.Identity.SuperOf",
|
||||
"query.System.Account",
|
||||
"tx.Balances.transfer_keep_alive",
|
||||
"query.Session.Validators",
|
||||
"query.Session.CurrentIndex",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user