@@ -45,7 +45,6 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.456.0",
|
||||
"polkadot-api": "^1.7.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -109,9 +109,6 @@ importers:
|
||||
cmdk:
|
||||
specifier: 1.0.4
|
||||
version: 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
idb-keyval:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
lucide-react:
|
||||
specifier: ^0.456.0
|
||||
version: 0.456.0(react@18.3.1)
|
||||
|
||||
10
src/main.tsx
10
src/main.tsx
@@ -5,7 +5,6 @@ import { BrowserRouter } from "react-router-dom"
|
||||
import { merge } from "rxjs"
|
||||
import App from "./App.tsx"
|
||||
import { dynamicBuilder$ } from "@/state/chains/chain.state"
|
||||
import { persistSyncState$ } from "./state/chains/chain.state.ts"
|
||||
import { TooltipProvider } from "./components/Tooltip.tsx"
|
||||
import "./index.css"
|
||||
import { explorer$ } from "./pages/Explorer"
|
||||
@@ -13,14 +12,7 @@ import { transactions$ } from "./pages/Transactions"
|
||||
import { ThemeProvider } from "./ThemeProvider.tsx"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<Subscribe
|
||||
source$={merge(
|
||||
dynamicBuilder$,
|
||||
explorer$,
|
||||
transactions$,
|
||||
persistSyncState$,
|
||||
)}
|
||||
>
|
||||
<Subscribe source$={merge(dynamicBuilder$, explorer$, transactions$)}>
|
||||
<RemoveSubscribe>
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
|
||||
0
src/smoldot.ts
Normal file
0
src/smoldot.ts
Normal file
@@ -1,14 +1,12 @@
|
||||
import { cyclingLocalCache } from "@/utils/cyclingLocalCache"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||
import { decAnyMetadata } from "@polkadot-api/substrate-bindings"
|
||||
import { decAnyMetadata, HexString } from "@polkadot-api/substrate-bindings"
|
||||
import { createClient as createSubstrateClient } from "@polkadot-api/substrate-client"
|
||||
import { toHex } from "@polkadot-api/utils"
|
||||
import { sinkSuspense, state, SUSPENSE } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { createClient } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
concat,
|
||||
EMPTY,
|
||||
filter,
|
||||
@@ -25,15 +23,12 @@ import {
|
||||
createSmoldotSource,
|
||||
getSmoldotProvider,
|
||||
SmoldotSource,
|
||||
RelayChain,
|
||||
relayChains,
|
||||
} from "./smoldot"
|
||||
import {
|
||||
createWebsocketSource,
|
||||
getWebsocketProvider,
|
||||
WebsocketSource,
|
||||
} from "./websocket"
|
||||
import { setCachedSmoldotDb } from "./smoldot.cache"
|
||||
|
||||
export type ChainSource = WebsocketSource | SmoldotSource
|
||||
|
||||
@@ -93,18 +88,34 @@ export const unsafeApi$ = chainClient$.pipeState(
|
||||
map(({ client }) => client.getUnsafeApi()),
|
||||
)
|
||||
|
||||
export const uncachedRuntimeCtx$ = chainClient$.pipeState(
|
||||
const uncachedRuntimeCtx$ = chainClient$.pipeState(
|
||||
switchMap(({ chainHead }) => chainHead.runtime$),
|
||||
filter((v) => !!v),
|
||||
)
|
||||
|
||||
const [getCachedMetadata, setCachedMetadata] =
|
||||
cyclingLocalCache("metadata-cache")
|
||||
const getMetadataCache = () => {
|
||||
const cached = localStorage.getItem(`metadata-cache`)
|
||||
return new Map<string, { time: number; data: HexString }>(
|
||||
cached ? JSON.parse(cached) : [],
|
||||
)
|
||||
}
|
||||
const getCachedMetadata = (id: string) =>
|
||||
getMetadataCache().get(id)?.data ?? null
|
||||
const setCachedMetadata = (id: string, data: HexString) => {
|
||||
const cached = getMetadataCache()
|
||||
cached.set(id, { time: Date.now(), data })
|
||||
if (cached.size > 3) {
|
||||
const oldest = [...cached.entries()].reduce((a, b) =>
|
||||
a[1].time < b[1].time ? a : b,
|
||||
)[0]
|
||||
cached.delete(oldest)
|
||||
}
|
||||
localStorage.setItem("metadata-cache", JSON.stringify([...cached.entries()]))
|
||||
}
|
||||
export const runtimeCtx$ = chainClient$.pipeState(
|
||||
switchMap(({ id }) =>
|
||||
getCachedMetadata(id).then((cached) => ({ id, cached })),
|
||||
),
|
||||
switchMap(({ id, cached }) => {
|
||||
switchMap(({ id }) => {
|
||||
const cached = getCachedMetadata(id)
|
||||
|
||||
const realCtx$ = uncachedRuntimeCtx$.pipe(
|
||||
tap((v) => {
|
||||
setCachedMetadata(id, toHex(v.metadataRaw))
|
||||
@@ -133,23 +144,4 @@ export const dynamicBuilder$ = runtimeCtx$.pipeState(
|
||||
map((ctx) => ctx.dynamicBuilder),
|
||||
)
|
||||
|
||||
export const persistSyncState$ = chainClient$.pipe(
|
||||
switchMap(({ id, client }) => {
|
||||
if (!relayChains.includes(id as RelayChain)) return EMPTY
|
||||
|
||||
return uncachedRuntimeCtx$.pipe(
|
||||
switchMap(() =>
|
||||
client._request<string, []>("chainHead_unstable_finalizedDatabase", []),
|
||||
),
|
||||
catchError(() => EMPTY),
|
||||
filter((v) => !!v),
|
||||
map((db) => ({
|
||||
id,
|
||||
db,
|
||||
})),
|
||||
)
|
||||
}),
|
||||
tap(({ id, db }) => setCachedSmoldotDb(id, db)),
|
||||
)
|
||||
|
||||
export { networkCategories, type Network } from "./networks"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { cyclingLocalCache } from "@/utils/cyclingLocalCache"
|
||||
|
||||
export const [getCachedSmoldotDb, setCachedSmoldotDb] =
|
||||
cyclingLocalCache("smoldot-db")
|
||||
@@ -4,7 +4,6 @@ import { getSmProvider } from "polkadot-api/sm-provider"
|
||||
import { Chain } from "polkadot-api/smoldot"
|
||||
import { startFromWorker } from "polkadot-api/smoldot/from-worker"
|
||||
import SmWorker from "polkadot-api/smoldot/worker?worker"
|
||||
import { getCachedSmoldotDb } from "./smoldot.cache"
|
||||
|
||||
const [dotChainSpec, ksmChainSpec, paseoChainSpec, westendChainSpec] = [
|
||||
import("polkadot-api/chains/polkadot"),
|
||||
@@ -12,18 +11,13 @@ const [dotChainSpec, ksmChainSpec, paseoChainSpec, westendChainSpec] = [
|
||||
import("polkadot-api/chains/paseo"),
|
||||
import("polkadot-api/chains/westend2"),
|
||||
].map((x) => x.then((y) => y.chainSpec))
|
||||
|
||||
const smoldot = startFromWorker(new SmWorker(), {
|
||||
logCallback: (level, target, message) => {
|
||||
console.debug("smoldot[%s(%s)] %s", target, level, message)
|
||||
},
|
||||
})
|
||||
|
||||
export const relayChains = ["polkadot", "kusama", "paseo", "westend"] as const
|
||||
export type RelayChain = (typeof relayChains)[number]
|
||||
|
||||
const relaySmoldotChains: Record<
|
||||
RelayChain,
|
||||
const relayChains: Record<
|
||||
string,
|
||||
{ chainSpec: Promise<string>; chain: Promise<Chain> | null }
|
||||
> = {
|
||||
polkadot: { chainSpec: dotChainSpec, chain: null },
|
||||
@@ -31,39 +25,21 @@ const relaySmoldotChains: Record<
|
||||
paseo: { chainSpec: paseoChainSpec, chain: null },
|
||||
westend: { chainSpec: westendChainSpec, chain: null },
|
||||
}
|
||||
const createChain = async (
|
||||
id: string,
|
||||
chainSpec: string | Promise<string>,
|
||||
relayChain?: Promise<Chain>,
|
||||
) => {
|
||||
const databaseContent =
|
||||
(id in relaySmoldotChains && (await getCachedSmoldotDb(id))) || undefined
|
||||
const solvedChain = relayChain ? await relayChain : undefined
|
||||
return smoldot.addChain({
|
||||
chainSpec: await chainSpec,
|
||||
databaseContent,
|
||||
potentialRelayChains: solvedChain ? [solvedChain] : undefined,
|
||||
})
|
||||
}
|
||||
const getRelayChain = async (name: RelayChain) => {
|
||||
if (!relaySmoldotChains[name].chain) {
|
||||
relaySmoldotChains[name].chain = createChain(
|
||||
name,
|
||||
relaySmoldotChains[name].chainSpec,
|
||||
)
|
||||
const getRelayChain = async (name: string) => {
|
||||
if (!relayChains[name].chain) {
|
||||
relayChains[name].chain = smoldot.addChain({
|
||||
chainSpec: await relayChains[name].chainSpec,
|
||||
})
|
||||
}
|
||||
return relaySmoldotChains[name].chain
|
||||
return relayChains[name].chain
|
||||
}
|
||||
|
||||
// Pre-initialize all relay chains
|
||||
relayChains.forEach(getRelayChain)
|
||||
|
||||
export interface SmoldotSource {
|
||||
type: "chainSpec"
|
||||
id: string
|
||||
value: {
|
||||
chainSpec: string
|
||||
relayChain?: RelayChain
|
||||
relayChain?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +47,8 @@ export function createSmoldotSource(
|
||||
id: string,
|
||||
relayChain?: string,
|
||||
): Promise<SmoldotSource> {
|
||||
if (id in relaySmoldotChains) {
|
||||
return relaySmoldotChains[id as RelayChain].chainSpec.then((chainSpec) => ({
|
||||
if (id in relayChains) {
|
||||
return relayChains[id].chainSpec.then((chainSpec) => ({
|
||||
id,
|
||||
type: "chainSpec",
|
||||
value: { chainSpec },
|
||||
@@ -93,12 +69,18 @@ export function createSmoldotSource(
|
||||
|
||||
export function getSmoldotProvider(source: SmoldotSource): JsonRpcProvider {
|
||||
const chain = source.value.relayChain
|
||||
? createChain(
|
||||
source.id,
|
||||
source.value.chainSpec,
|
||||
getRelayChain(source.value.relayChain),
|
||||
? getRelayChain(source.value.relayChain).then((chain) =>
|
||||
smoldot.addChain({
|
||||
chainSpec: source.value.chainSpec,
|
||||
potentialRelayChains: [chain],
|
||||
}),
|
||||
)
|
||||
: createChain(source.id, source.value.chainSpec)
|
||||
: smoldot.addChain({
|
||||
chainSpec: source.value.chainSpec,
|
||||
})
|
||||
|
||||
return withLogsRecorder((v) => console.debug(v), getSmProvider(chain))
|
||||
return withLogsRecorder(
|
||||
(v) => v.includes("initialized") && console.debug(v),
|
||||
getSmProvider(chain),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { get, set } from "idb-keyval"
|
||||
|
||||
export const cyclingLocalCache = (key: string, limit = 10) => {
|
||||
const getCache = async () => {
|
||||
try {
|
||||
const entries = await get(key)
|
||||
return new Map<string, { time: number; data: string }>(entries)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const getCachedItem = (id: string) =>
|
||||
id === "metadata-cache"
|
||||
? Promise.resolve(null)
|
||||
: getCache().then((cache) => cache?.get(id)?.data ?? null)
|
||||
const setCachedItem = async (id: string, data: string) => {
|
||||
const cached = await getCache()
|
||||
if (!cached) return
|
||||
|
||||
cached.set(id, { time: Date.now(), data })
|
||||
if (cached.size > limit) {
|
||||
const oldest = [...cached.entries()].reduce((a, b) =>
|
||||
a[1].time < b[1].time ? a : b,
|
||||
)[0]
|
||||
cached.delete(oldest)
|
||||
}
|
||||
|
||||
try {
|
||||
await set(key, [...cached.entries()])
|
||||
} catch (_) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return [getCachedItem, setCachedItem] as const
|
||||
}
|
||||
Reference in New Issue
Block a user