From 53ceb11e62b9cda3a9027758fc8013616b8a0c44 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Wed, 6 Nov 2024 16:57:55 +0100 Subject: [PATCH] wip: reset states when switching chain --- src/chain.state.ts | 102 +++++++++--------- .../LookupTypeEdit/LookupTypeEdit.tsx | 10 +- src/pages/Constants.tsx | 10 +- src/pages/Explorer/BlockPopover.tsx | 11 +- src/pages/Explorer/BlockTime.tsx | 10 +- src/pages/Explorer/Detail/BlockDetail.tsx | 10 +- src/pages/Extrinsics/Extrinsics.tsx | 16 +-- src/pages/Storage/StorageQuery.tsx | 12 ++- src/utils/withLogs.ts | 32 ++++++ 9 files changed, 128 insertions(+), 85 deletions(-) create mode 100644 src/utils/withLogs.ts diff --git a/src/chain.state.ts b/src/chain.state.ts index 8da67ea..926ba2a 100644 --- a/src/chain.state.ts +++ b/src/chain.state.ts @@ -1,17 +1,18 @@ import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" import { getObservableClient } from "@polkadot-api/observable-client" +import { decAnyMetadata } from "@polkadot-api/substrate-bindings" import { - Binary, - CodecType, - decAnyMetadata, - metadata as metadataCodec, - V14, - V15, -} from "@polkadot-api/substrate-bindings" -import { createClient as createSubstrateClient } from "@polkadot-api/substrate-client" + createClient as createSubstrateClient, + JsonRpcProvider, +} from "@polkadot-api/substrate-client" import { toHex } from "@polkadot-api/utils" -import { state } from "@react-rxjs/core" -import { createClient, PolkadotClient } from "polkadot-api" +import { sinkSuspense, state, SUSPENSE } from "@react-rxjs/core" +import { createSignal } from "@react-rxjs/utils" +import { createClient } from "polkadot-api" +import { chainSpec as ksmChainSpec } from "polkadot-api/chains/ksmcc3" +import { chainSpec as paseoChainSpec } from "polkadot-api/chains/paseo" +import { chainSpec } from "polkadot-api/chains/polkadot" +import { chainSpec as westendChainSpec } from "polkadot-api/chains/westend2" import { withLogsRecorder } from "polkadot-api/logs-provider" import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat" import { getSmProvider } from "polkadot-api/sm-provider" @@ -21,8 +22,9 @@ import SmWorker from "polkadot-api/smoldot/worker?worker" import { getWsProvider } from "polkadot-api/ws-provider/web" import { concat, + EMPTY, + filter, finalize, - from, map, NEVER, Observable, @@ -31,13 +33,8 @@ import { switchMap, tap, } from "rxjs" -import polkadotRawNetworks from "./networks/polkadot.json" import ksmRawNetworks from "./networks/kusama.json" -import { createSignal } from "@react-rxjs/utils" -import { chainSpec } from "polkadot-api/chains/polkadot" -import { chainSpec as ksmChainSpec } from "polkadot-api/chains/ksmcc3" -import { chainSpec as westendChainSpec } from "polkadot-api/chains/westend2" -import { chainSpec as paseoChainSpec } from "polkadot-api/chains/paseo" +import polkadotRawNetworks from "./networks/polkadot.json" export type ChainSource = { id: string } & ( | { @@ -113,44 +110,46 @@ const relayChains = new Set(["polkadot", "kusama", "westend", "paseo"]) const selectedSource$ = selectedChain$.pipe( switchMap( ({ endpoint, network }): Observable | Promise => { + const { id } = network if (endpoint !== "light-client") return of({ + id, type: "websocket", value: endpoint, - } as ChainSource) - const { id } = network + }) if (relayChains.has(id)) { return of({ + id, type: "chainSpec", value: { chainSpec: id }, - } as ChainSource) + }) } return import(`./chainspecs/${id}.ts`).then(({ chainSpec }) => { const parsed = JSON.parse(chainSpec) return { + id, type: "chainSpec", value: { chainSpec, relayChain: network.relayChain || parsed.relayChain || parsed.relay_chain, }, - } as ChainSource + } }) }, ), ) -type AnyMetadata = CodecType - export const chainClient$ = state( selectedSource$.pipe( map((src) => [src.id, getProvider(src)] as const), - switchMap(([id, provider]) => { + switchMap(([id, provider], i) => { const substrateClient = createSubstrateClient(provider) const observableClient = getObservableClient(substrateClient) const chainHead = observableClient.chainHead$(2) const client = createClient(provider) return concat( + i === 0 ? EMPTY : of(SUSPENSE), of({ id, client, substrateClient, observableClient, chainHead }), NEVER, ).pipe( @@ -161,6 +160,7 @@ export const chainClient$ = state( }), ) }), + sinkSuspense(), ), ) export const chainHead$ = state( @@ -171,47 +171,47 @@ export const unsafeApi$ = chainClient$.pipeState( map(({ client }) => client.getUnsafeApi()), ) -export const metadata$ = chainClient$.pipeState( - switchMap(({ id, client }) => { - const metadata = from(getMetadata(client)).pipe( +const uncachedRuntimeCtx$ = chainClient$.pipeState( + switchMap(({ chainHead }) => chainHead.runtime$), + filter((v) => !!v), +) +export const runtimeCtx$ = chainClient$.pipeState( + switchMap(({ id }) => { + const cached = localStorage.getItem(`metadata-${id}`) + + const realCtx$ = uncachedRuntimeCtx$.pipe( tap((v) => { - localStorage.setItem(`metadata-${id}`, toHex(metadataCodec.enc(v))) + localStorage.setItem(`metadata-${id}`, toHex(v.metadataRaw)) }), ) - const cached = localStorage.getItem(`metadata-${id}`) + if (cached) { - return from(metadata).pipe(startWith(decAnyMetadata(cached))) + const metadata = decAnyMetadata(cached) + const lookup = getLookupFn(metadata.metadata.value as any) + const dynamicBuilder = getDynamicBuilder(lookup) + + return realCtx$.pipe( + startWith({ + lookup, + dynamicBuilder, + }), + ) } - return from(metadata) - }), - map((v): V14 | V15 => { - const metadata = v.metadata - if (metadata.tag === "v14" || metadata.tag === "v15") { - return metadata.value - } - throw new Error("Incompatible metadata") + return realCtx$ }), ) -export const lookup$ = metadata$.pipeState(map(getLookupFn)) -export const dynamicBuilder$ = lookup$.pipeState( - map((lookup) => ({ lookup, ...getDynamicBuilder(lookup) })), +export const lookup$ = runtimeCtx$.pipeState(map((ctx) => ctx.lookup)) +export const metadata$ = lookup$.pipe(map((lookup) => lookup.metadata)) +export const dynamicBuilder$ = runtimeCtx$.pipeState( + map((ctx) => ctx.dynamicBuilder), ) -async function getMetadata(client: PolkadotClient): Promise { - const unsafeApi = client.getUnsafeApi() - const versions: number[] = await unsafeApi.apis.Metadata.metadata_versions() - const metadataResponse: Binary = await (versions.includes(15) - ? unsafeApi.apis.Metadata.metadata_at_version(15) - : unsafeApi.apis.Metadata.metadata()) - return decAnyMetadata(metadataResponse.asBytes()) -} - let smoldot: { client: Client relayChains: Record> } | null = null -export function getProvider(source: ChainSource) { +export function getProvider(source: ChainSource): JsonRpcProvider { if (source.type === "websocket") { return withPolkadotSdkCompat(getWsProvider(source.value)) } diff --git a/src/codec-components/LookupTypeEdit/LookupTypeEdit.tsx b/src/codec-components/LookupTypeEdit/LookupTypeEdit.tsx index 6079d65..a128b9b 100644 --- a/src/codec-components/LookupTypeEdit/LookupTypeEdit.tsx +++ b/src/codec-components/LookupTypeEdit/LookupTypeEdit.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$ } from "@/chain.state" +import { runtimeCtx$ } from "@/chain.state" import { byteArraysAreEqual } from "@/utils/byteArray" import { CodecComponentType, CodecComponentValue } from "@codec-components" import { state, useStateObservable } from "@react-rxjs/core" @@ -18,10 +18,10 @@ import { BinaryDisplay } from "./BinaryDisplay" import { FocusPath } from "./FocusPath" const editTypeMetadataProps$ = state( - dynamicBuilder$.pipe( - map((builder) => ({ - builder, - metadata: builder.lookup.metadata, + runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => ({ + builder: dynamicBuilder, + metadata: lookup.metadata, })), ), null, diff --git a/src/pages/Constants.tsx b/src/pages/Constants.tsx index ef5fa8a..c0336a9 100644 --- a/src/pages/Constants.tsx +++ b/src/pages/Constants.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$, lookup$ } from "@/chain.state" +import { lookup$, runtimeCtx$ } from "@/chain.state" import { ViewCodec } from "@/codec-components/ViewCodec" import { ButtonGroup } from "@/components/ButtonGroup" import { DocsRenderer } from "@/components/DocsRenderer" @@ -81,10 +81,10 @@ const PalletConstants: FC<{ } const constantValueProps$ = state( - dynamicBuilder$.pipe( - map((builder) => ({ - builder, - lookup: builder.lookup, + runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => ({ + builder: dynamicBuilder, + lookup, })), ), null, diff --git a/src/pages/Explorer/BlockPopover.tsx b/src/pages/Explorer/BlockPopover.tsx index 9e1e939..7a90caa 100644 --- a/src/pages/Explorer/BlockPopover.tsx +++ b/src/pages/Explorer/BlockPopover.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$ } from "@/chain.state" +import { runtimeCtx$ } from "@/chain.state" import { CircularProgress } from "@/components/CircularProgress" import { groupBy } from "@/lib/groupBy" import { state, useStateObservable } from "@react-rxjs/core" @@ -10,13 +10,14 @@ import { BlockStatusIcon, statusText } from "./Detail/BlockState" import { filterEvt } from "./Events" const maxBlockWeight$ = state( - dynamicBuilder$.pipe( - map((builder) => { - const ct = builder.lookup.metadata.pallets + runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => { + const ct = lookup.metadata.pallets .find((p) => p.name === "System") ?.constants.find((ct) => ct.name === "BlockWeights") if (!ct) return null - return builder.buildDefinition(ct.type).dec(ct.value).max_block as Weight + return dynamicBuilder.buildDefinition(ct.type).dec(ct.value) + .max_block as Weight }), ), null, diff --git a/src/pages/Explorer/BlockTime.tsx b/src/pages/Explorer/BlockTime.tsx index 14e398e..6899c87 100644 --- a/src/pages/Explorer/BlockTime.tsx +++ b/src/pages/Explorer/BlockTime.tsx @@ -1,4 +1,4 @@ -import { chainHead$, dynamicBuilder$ } from "@/chain.state" +import { chainHead$, runtimeCtx$ } from "@/chain.state" import { CircularProgress } from "@/components/CircularProgress" import { state, useStateObservable } from "@react-rxjs/core" import { @@ -20,14 +20,14 @@ const bestBlockTime$ = best$.pipeState( }), ) const targetTime$ = state( - dynamicBuilder$.pipe( - map((builder) => { - const ct = builder.lookup.metadata.pallets + runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => { + const ct = lookup.metadata.pallets .find((p) => p.name === "Babe") ?.constants.find((ct) => ct.name === "ExpectedBlockTime") if (!ct) return null try { - const res = builder.buildDefinition(ct.type).dec(ct.value) + const res = dynamicBuilder.buildDefinition(ct.type).dec(ct.value) return Number(res) } catch (_) { return null diff --git a/src/pages/Explorer/Detail/BlockDetail.tsx b/src/pages/Explorer/Detail/BlockDetail.tsx index 766e710..ba1d9d5 100644 --- a/src/pages/Explorer/Detail/BlockDetail.tsx +++ b/src/pages/Explorer/Detail/BlockDetail.tsx @@ -1,7 +1,8 @@ -import { dynamicBuilder$ } from "@/chain.state" +import { runtimeCtx$ } from "@/chain.state" import { AccountIdDisplay } from "@/components/AccountIdDisplay" import { ExpandBtn } from "@/components/Expand" import { JsonDisplay } from "@/components/JsonDisplay" +import { Loading } from "@/components/Loading" import { groupBy } from "@/lib/groupBy" import { SystemEvent } from "@polkadot-api/observable-client" import * as Tabs from "@radix-ui/react-tabs" @@ -22,11 +23,12 @@ import { twMerge } from "tailwind-merge" import { blockInfoState$, blocksByHeight$, BlockState } from "../block.state" import { BlockStatusIcon, statusText } from "./BlockState" import { createExtrinsicCodec, DecodedExtrinsic } from "./extrinsicDecoder" -import { Loading } from "@/components/Loading" const blockExtrinsics$ = state((hash: string) => { - const decoder$ = dynamicBuilder$.pipe( - map((builder) => createExtrinsicCodec(builder, builder.lookup)), + const decoder$ = runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => + createExtrinsicCodec(dynamicBuilder, lookup), + ), ) const body$ = blockInfoState$(hash).pipe( filter((v) => !!v), diff --git a/src/pages/Extrinsics/Extrinsics.tsx b/src/pages/Extrinsics/Extrinsics.tsx index 8e25cd8..c783502 100644 --- a/src/pages/Extrinsics/Extrinsics.tsx +++ b/src/pages/Extrinsics/Extrinsics.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$ } from "@/chain.state" +import { runtimeCtx$ } from "@/chain.state" import { BinaryDisplay } from "@/codec-components/LookupTypeEdit" import { ButtonGroup } from "@/components/ButtonGroup" import { LoadingMetadata } from "@/components/Loading" @@ -13,17 +13,17 @@ import { JsonMode } from "./JsonMode" import { ExtrinsicModal } from "./SubmitTx/SubmitTx" const extrinsicProps$ = state( - dynamicBuilder$.pipe( - map((builder) => { + runtimeCtx$.pipe( + map(({ dynamicBuilder, lookup }) => { const codecType = - "call" in builder.lookup.metadata.extrinsic - ? builder.lookup.metadata.extrinsic.call + "call" in lookup.metadata.extrinsic + ? lookup.metadata.extrinsic.call : // TODO v14 is this one? - builder.lookup.metadata.extrinsic.type + lookup.metadata.extrinsic.type return { - metadata: builder.lookup.metadata, + metadata: lookup.metadata, codecType, - codec: builder.buildDefinition(codecType), + codec: dynamicBuilder.buildDefinition(codecType), } }), ), diff --git a/src/pages/Storage/StorageQuery.tsx b/src/pages/Storage/StorageQuery.tsx index 3c873ae..3f8ecce 100644 --- a/src/pages/Storage/StorageQuery.tsx +++ b/src/pages/Storage/StorageQuery.tsx @@ -1,4 +1,4 @@ -import { dynamicBuilder$, unsafeApi$ } from "@/chain.state" +import { dynamicBuilder$, runtimeCtx$, unsafeApi$ } from "@/chain.state" import { EditCodec } from "@/codec-components/EditCodec" import { ActionButton } from "@/components/ActionButton" import { BinaryEditButton } from "@/components/BinaryEditButton" @@ -148,7 +148,15 @@ const StorageKeysInput: FC = () => { ) } -const builderState$ = state(dynamicBuilder$, null) +const builderState$ = state( + runtimeCtx$.pipe( + map((ctx) => ({ + ...ctx.dynamicBuilder, + lookup: ctx.lookup, + })), + ), + null, +) const keyInputValue$ = state( (idx: number) => keyValues$.pipe( diff --git a/src/utils/withLogs.ts b/src/utils/withLogs.ts new file mode 100644 index 0000000..6e902b2 --- /dev/null +++ b/src/utils/withLogs.ts @@ -0,0 +1,32 @@ +import { Observable } from "rxjs" + +export const withLogs = + (msg: string, withSubId?: boolean) => + (base: Observable): Observable => { + let nextId = 0 + return new Observable((observer) => { + const id = nextId++ + const log = (...args: any[]) => + console.log(...(withSubId ? [id, ...args] : args)) + log(`withLogs(${msg}): subscribed`) + const sub = base.subscribe({ + next(v) { + log(`withLogs(${msg}): next`, v) + observer.next(v) + }, + error(e) { + log(`withLogs(${msg}): error`, e) + observer.error(e) + }, + complete() { + log(`withLogs(${msg}): complete`) + observer.complete() + }, + }) + + return () => { + log(`withLogs(${msg}): cleanup`) + sub.unsubscribe() + } + }) + }