Files
qapi/src/chain.state.ts
T

257 lines
7.0 KiB
TypeScript

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<string, string>
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<SelectedChain>()
export const selectedChain$ = state<SelectedChain>(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<ChainSource> | Promise<ChainSource> => {
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<typeof metadataCodec>
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<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) {
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))
}