wip: reset states when switching chain

This commit is contained in:
Victor Oliva
2024-11-06 16:58:42 +01:00
parent 3fd958d160
commit 53ceb11e62
9 changed files with 128 additions and 85 deletions
+51 -51
View File
@@ -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<ChainSource> | Promise<ChainSource> => {
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<typeof metadataCodec>
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<AnyMetadata> {
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<string, Promise<Chain>>
} | null = null
export function getProvider(source: ChainSource) {
export function getProvider(source: ChainSource): JsonRpcProvider {
if (source.type === "websocket") {
return withPolkadotSdkCompat(getWsProvider(source.value))
}
@@ -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,
+5 -5
View File
@@ -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,
+6 -5
View File
@@ -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,
+5 -5
View File
@@ -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
+6 -4
View File
@@ -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),
+8 -8
View File
@@ -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),
}
}),
),
+10 -2
View File
@@ -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(
+32
View File
@@ -0,0 +1,32 @@
import { Observable } from "rxjs"
export const withLogs =
(msg: string, withSubId?: boolean) =>
<T>(base: Observable<T>): Observable<T> => {
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()
}
})
}