import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" import { getObservableClient } from "@polkadot-api/observable-client" import { Binary, CodecType, decAnyMetadata, metadata as metadataCodec, V14, V15, } from "@polkadot-api/substrate-bindings" import { createClient as createSubstrateClient } from "@polkadot-api/substrate-client" import { toHex } from "@polkadot-api/utils" import { state } from "@react-rxjs/core" import { createClient, PolkadotClient } from "polkadot-api" import { withLogsRecorder } from "polkadot-api/logs-provider" import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat" import { getSmProvider } from "polkadot-api/sm-provider" import { Chain, Client } from "polkadot-api/smoldot" import { startFromWorker } from "polkadot-api/smoldot/from-worker" import SmWorker from "polkadot-api/smoldot/worker?worker" import { getWsProvider } from "polkadot-api/ws-provider/web" import { concat, finalize, from, map, NEVER, Observable, of, startWith, 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" export type ChainSource = { id: string } & ( | { type: "chainSpec" value: { chainSpec: string relayChain?: string } } | { type: "websocket" value: string } ) export type Network = { id: string display: string endpoints: Record lightclient: boolean relayChain?: string } export type NetworkCategory = { name: string networks: Network[] } export type SelectedChain = { network: Network endpoint: string } const polkadot = polkadotRawNetworks.map( (x): Network => ({ endpoints: x.rpcs as any, lightclient: x.hasChainSpecs, id: x.id, display: x.display, relayChain: "polkadot", }), ) const kusama = ksmRawNetworks.map( (x): Network => ({ endpoints: x.rpcs as any, lightclient: x.hasChainSpecs, id: x.id, display: x.display, relayChain: "kusama", }), ) export const networkCategories: NetworkCategory[] = [ { name: "Polkadot", networks: polkadot, }, { name: "Kusama", networks: kusama, }, ] export const [selectedChainChanged$, onChangeChain] = createSignal() export const selectedChain$ = state(selectedChainChanged$, { network: polkadot[0], endpoint: "light-client", }) selectedChain$.subscribe() const relayChains = new Set(["polkadot", "kusama", "westend", "paseo"]) const selectedSource$ = selectedChain$.pipe( switchMap( ({ endpoint, network }): Observable | Promise => { if (endpoint !== "light-client") return of({ type: "websocket", value: endpoint, } as ChainSource) const { id } = network if (relayChains.has(id)) { return of({ type: "chainSpec", value: { chainSpec: id }, } as ChainSource) } return import(`./chainspecs/${id}.ts`).then(({ chainSpec }) => { const parsed = JSON.parse(chainSpec) return { 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]) => { const substrateClient = createSubstrateClient(provider) const observableClient = getObservableClient(substrateClient) const chainHead = observableClient.chainHead$(2) const client = createClient(provider) return concat( of({ id, client, substrateClient, observableClient, chainHead }), NEVER, ).pipe( finalize(() => { chainHead.unfollow() client.destroy() observableClient.destroy() }), ) }), ), ) export const chainHead$ = state( chainClient$.pipe(map(({ chainHead }) => chainHead)), ) export const unsafeApi$ = chainClient$.pipeState( map(({ client }) => client.getUnsafeApi()), ) export const metadata$ = chainClient$.pipeState( switchMap(({ id, client }) => { const metadata = from(getMetadata(client)).pipe( tap((v) => { localStorage.setItem(`metadata-${id}`, toHex(metadataCodec.enc(v))) }), ) const cached = localStorage.getItem(`metadata-${id}`) if (cached) { return from(metadata).pipe(startWith(decAnyMetadata(cached))) } 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") }), ) export const lookup$ = metadata$.pipeState(map(getLookupFn)) export const dynamicBuilder$ = lookup$.pipeState( map((lookup) => ({ lookup, ...getDynamicBuilder(lookup) })), ) 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) { if (source.type === "websocket") { return withPolkadotSdkCompat(getWsProvider(source.value)) } if (!smoldot) { const client = startFromWorker(new SmWorker(), { logCallback: (level, target, message) => { console.debug("[%s(%s)] %s", target, level, message) }, }) smoldot = { client, relayChains: { polkadot: client.addChain({ chainSpec: chainSpec, }), kusama: client.addChain({ chainSpec: ksmChainSpec, }), westend: client.addChain({ chainSpec: westendChainSpec, }), paseo: client.addChain({ chainSpec: paseoChainSpec, }), }, } } const chain = source.value.relayChain ? smoldot.relayChains[source.value.relayChain].then((chain) => { return smoldot!.client.addChain({ chainSpec: source.value.chainSpec, potentialRelayChains: [chain], }) }) : smoldot.relayChains[source.value.chainSpec] || smoldot.client.addChain({ chainSpec: source.value.chainSpec, }) return withLogsRecorder(console.debug, getSmProvider(chain)) }