diff --git a/src/components/RuntimeMetadataLoader.tsx b/src/components/RuntimeMetadataLoader.tsx new file mode 100644 index 00000000..1d21e5f2 --- /dev/null +++ b/src/components/RuntimeMetadataLoader.tsx @@ -0,0 +1,151 @@ +/** @jsxImportSource @emotion/react */ +import { LinearProgress } from "@mui/material"; +import { css, Theme } from "@emotion/react"; + +import { ReactComponent as Logo } from "../assets/calamar-logo-export-05.svg"; +import Background from "../assets/main-screen-bgr.svg"; + +import { Footer } from "./Footer"; + +import { usePreloadRuntimeMetadata } from "../hooks/usePreloadRuntimeMetadata"; +import { Outlet } from "react-router-dom"; + +const containerStyle = (theme: Theme) => css` + --content-min-height: 900px; + + width: 100%; + margin: 0; + display: flex; + flex-direction: column; + align-items: stretch; + + ${theme.breakpoints.up("sm")} { + --content-min-height: 1000px; + } + + ${theme.breakpoints.up("md")} { + --content-min-height: 1100px; + } + + ${theme.breakpoints.up("lg")} { + --content-min-height: 1200px; + } + + ${theme.breakpoints.up("xl")} { + --content-min-height: 1300px; + } +`; + +const contentStyle = css` + position: relative; + flex: 1 1 auto; + min-height: var(--content-min-height); +`; + +const backgroundStyle = css` + position: absolute; + top: 0; + margin: 0; + width: 100%; + height: 100%; + min-height: 100vh; + z-index: -1; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: var(--content-min-height); + background-color: white; + background-position: center bottom; + background-size: 100% auto; + background-repeat: no-repeat; + background-image: url(${Background}); + } + + &::after { + content: ''; + position: absolute; + top: var(--content-min-height); + left: 0; + right: 0; + bottom: 0; + background-color: #9af0f7; + } +`; + +const logoStyle = css` + width: 420px; + margin: 40px auto; + display: block; + max-width: 100%; +`; + +const subtitleStyle = (theme: Theme) => css` + position: relative; + top: -100px; + padding: 0 16px; + font-size: 16px; + text-align: center; + + ${theme.breakpoints.down("sm")} { + top: -70px; + } +`; + +const footerStyle = css` + flex: 0 0 auto; + + > div { + max-width: 1000px; + } +`; + +const metadatLoadingStyle = css` + max-width: 500px; + margin: 0 auto; + padding: 0 16px; + + text-align: center; +`; + +const metadataProgressStyle = css` + margin-bottom: 16px; + height: 8px; + + border-radius: 4px; + background-color: #e1fbfd; + + .MuiLinearProgress-bar { + background-color: #7acbdd; + } +`; + +export const RuntimeMetadataLoader = () => { + const metadataPreload = usePreloadRuntimeMetadata(); + + if (metadataPreload.loading) { + return ( +
+
+
+ +
Block explorer for Polkadot & Kusama ecosystem
+
+ + Loading latest runtime metadata ... +
+
+
+
+ ); + } + + return ; +}; diff --git a/src/hooks/usePreloadRuntimeMetadata.ts b/src/hooks/usePreloadRuntimeMetadata.ts new file mode 100644 index 00000000..266ff3b9 --- /dev/null +++ b/src/hooks/usePreloadRuntimeMetadata.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { getNetworks } from "../services/networksService"; +import { getLatestRuntimeSpecVersion } from "../services/runtimeSpecService"; +import { loadRuntimeMetadata } from "../services/runtimeMetadataService"; + +export function usePreloadRuntimeMetadata() { + const [progress, setProgress] = useState(localStorage.getItem("runtime-metadata-preloaded") ? 100 : 0); + + useEffect(() => { + Promise.allSettled(getNetworks().map(async (it) => { + try { + const specVersion = await getLatestRuntimeSpecVersion(it.name); + await loadRuntimeMetadata(it.name, specVersion); + } catch (e) { + // pass + } + + setProgress(prev => prev + 100 / getNetworks().length); + })).then(() => { + setProgress(100); + localStorage.setItem("runtime-metadata-preloaded", "true"); + }); + }, []); + + return { + loading: progress < 100, + progress + }; +} diff --git a/src/router.tsx b/src/router.tsx index 6563fef3..d3f79af0 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,7 +1,8 @@ import { LoaderFunctionArgs, Navigate, RouteObject, createBrowserRouter, redirect } from "react-router-dom"; import { ResultLayout } from "./components/ResultLayout"; -import { getNetwork } from "./services/networksService"; +import { RuntimeMetadataLoader } from "./components/RuntimeMetadataLoader"; + import { encodeAddress } from "./utils/address"; import { AccountPage } from "./screens/account"; @@ -20,6 +21,7 @@ import { simplifyCallId } from "./services/callsService"; import { simplifyBlockId } from "./services/blocksService"; import { simplifyExtrinsicId } from "./services/extrinsicsService"; import { simplifyEventId } from "./services/eventsService"; +import { getNetwork } from "./services/networksService"; import { normalizeCallName, normalizeConstantName, normalizeErrorName, normalizeEventName, normalizePalletName, normalizeStorageName } from "./services/runtimeMetadataService"; const networkLoader = ({ params }: LoaderFunctionArgs) => { @@ -35,252 +37,257 @@ const networkLoader = ({ params }: LoaderFunctionArgs) => { export const routes: RouteObject[] = [ { - path: "/", - element: , - }, - { - element: , + element: , children: [ { - path: "search/:tab?", - element: , - errorElement: , + index: true, + element: , }, { - id: "network", - path: ":network", - loader: networkLoader, - errorElement: , + element: , children: [ { - path: ":tab?", - element: , + path: "search/:tab?", + element: , + errorElement: , }, { - path: "block/:id/:tab?", - element: , - loader: (args) => { - const { params } = args; - const { id } = params as { id: string }; + id: "network", + path: ":network", + loader: networkLoader, + errorElement: , + children: [ + { + path: ":tab?", + element: , + }, + { + path: "block/:id/:tab?", + element: , + loader: (args) => { + const { params } = args; + const { id } = params as { id: string }; - const simplifiedId = simplifyBlockId(id); + const simplifiedId = simplifyBlockId(id); - if (id !== simplifiedId) { - const { network } = networkLoader(args); - return redirect(`/${network.name}/block/${simplifiedId}`); - } + if (id !== simplifiedId) { + const { network } = networkLoader(args); + return redirect(`/${network.name}/block/${simplifiedId}`); + } - return null; - } - }, - { - path: "extrinsic/:id/:tab?", - element: , - loader: (args) => { - const { params } = args; - const { id } = params as { id: string }; + return null; + } + }, + { + path: "extrinsic/:id/:tab?", + element: , + loader: (args) => { + const { params } = args; + const { id } = params as { id: string }; - const simplifiedId = simplifyExtrinsicId(id); + const simplifiedId = simplifyExtrinsicId(id); - if (id !== simplifiedId) { - const { network } = networkLoader(args); - return redirect(`/${network.name}/extrinsic/${simplifiedId}`); - } + if (id !== simplifiedId) { + const { network } = networkLoader(args); + return redirect(`/${network.name}/extrinsic/${simplifiedId}`); + } - return null; - } - }, - { - path: "call/:id/:tab?", - element: , - loader: (args) => { - const { params } = args; - const { id } = params as { id: string }; + return null; + } + }, + { + path: "call/:id/:tab?", + element: , + loader: (args) => { + const { params } = args; + const { id } = params as { id: string }; - const simplifiedId = simplifyCallId(id); + const simplifiedId = simplifyCallId(id); - if (id !== simplifiedId) { - const { network } = networkLoader(args); - return redirect(`/${network.name}/call/${simplifiedId}`); - } + if (id !== simplifiedId) { + const { network } = networkLoader(args); + return redirect(`/${network.name}/call/${simplifiedId}`); + } - return null; - } - }, - { - path: "event/:id", - element: , - loader: (args) => { - const { params } = args; - const { id } = params as { id: string }; + return null; + } + }, + { + path: "event/:id", + element: , + loader: (args) => { + const { params } = args; + const { id } = params as { id: string }; - const simplifiedId = simplifyEventId(id); + const simplifiedId = simplifyEventId(id); - if (id !== simplifiedId) { - const { network } = networkLoader(args); - return redirect(`/${network.name}/event/${simplifiedId}`); - } + if (id !== simplifiedId) { + const { network } = networkLoader(args); + return redirect(`/${network.name}/event/${simplifiedId}`); + } - return null; - } - }, - { - path: "account/:address/:tab?", - element: , - loader: (args) => { - const { params } = args; - const { address } = params as { address: string }; + return null; + } + }, + { + path: "account/:address/:tab?", + element: , + loader: (args) => { + const { params } = args; + const { address } = params as { address: string }; - const { network } = networkLoader(args); + const { network } = networkLoader(args); - const encodedAddress = encodeAddress(address, network.prefix); - if (address !== encodedAddress) { - return redirect(`/${network.name}/account/${encodedAddress}`); - } + const encodedAddress = encodeAddress(address, network.prefix); + if (address !== encodedAddress) { + return redirect(`/${network.name}/account/${encodedAddress}`); + } - return null; - } - }, - { - path: "latest-extrinsics", - element: , - }, - { - path: "runtime", - children: [ + return null; + } + }, { - index: true, - element: , + path: "latest-extrinsics", + element: , }, { - path: ":specVersion", + path: "runtime", children: [ { index: true, - element: , + element: , }, { - id: "runtime-pallet", - path: ":palletName", - loader: async (args) => { - const { params } = args; - const { specVersion, palletName } = params as { specVersion: string, palletName: string }; - - const { network } = networkLoader(args); - - console.log("pallet loader", specVersion, palletName); - - return { - palletName: await normalizePalletName(network, palletName, specVersion) - }; - }, + path: ":specVersion", children: [ { - path: ":tab?", - element: , + index: true, + element: , }, { - id: "runtime-call", - path: "calls/:callName", + id: "runtime-pallet", + path: ":palletName", loader: async (args) => { const { params } = args; - const { specVersion, palletName, callName } = params as { specVersion: string, palletName: string, callName: string }; + const { specVersion, palletName } = params as { specVersion: string, palletName: string }; const { network } = networkLoader(args); - const callFullName = `${palletName}.${callName}`; - const normalizedCallFullName = await normalizeCallName(network, callFullName, specVersion); + console.log("pallet loader", specVersion, palletName); return { - callName: normalizedCallFullName.split(".")[1] as string + palletName: await normalizePalletName(network, palletName, specVersion) }; }, - element: + children: [ + { + path: ":tab?", + element: , + }, + { + id: "runtime-call", + path: "calls/:callName", + loader: async (args) => { + const { params } = args; + const { specVersion, palletName, callName } = params as { specVersion: string, palletName: string, callName: string }; + + const { network } = networkLoader(args); + + const callFullName = `${palletName}.${callName}`; + const normalizedCallFullName = await normalizeCallName(network, callFullName, specVersion); + + return { + callName: normalizedCallFullName.split(".")[1] as string + }; + }, + element: + }, + { + id: "runtime-event", + path: "events/:eventName", + loader: async (args) => { + const { params } = args; + const { specVersion, palletName, eventName } = params as { specVersion: string, palletName: string, eventName: string }; + + const { network } = networkLoader(args); + + const eventFullName = `${palletName}.${eventName}`; + const normalizedEventFullName = await normalizeEventName(network, eventFullName, specVersion); + + return { + eventName: normalizedEventFullName.split(".")[1] as string + }; + }, + element: + }, + { + id: "runtime-constant", + path: "constants/:constantName", + loader: async (args) => { + const { params } = args; + const { specVersion, palletName, constantName } = params as { specVersion: string, palletName: string, constantName: string }; + + const { network } = networkLoader(args); + + const constantFullName = `${palletName}.${constantName}`; + const normalizedConstantFullName = await normalizeConstantName(network, constantFullName, specVersion); + + return { + constantName: normalizedConstantFullName.split(".")[1] as string + }; + }, + element: + }, + { + id: "runtime-storage", + path: "storages/:storageName", + loader: async (args) => { + const { params } = args; + const { specVersion, palletName, storageName } = params as { specVersion: string, palletName: string, storageName: string }; + + const { network } = networkLoader(args); + + const storageFullName = `${palletName}.${storageName}`; + const normalizedStorageFullName = await normalizeStorageName(network, storageFullName, specVersion); + + return { + storageName: normalizedStorageFullName.split(".")[1] as string + }; + }, + element: + }, + { + id: "runtime-error", + path: "errors/:errorName", + loader: async (args) => { + const { params } = args; + const { specVersion, palletName, errorName } = params as { specVersion: string, palletName: string, errorName: string }; + + const { network } = networkLoader(args); + + const errorFullName = `${palletName}.${errorName}`; + const normalizedErrorFullName = await normalizeErrorName(network, errorFullName, specVersion); + + return { + errorName: normalizedErrorFullName.split(".")[1] as string + }; + }, + element: + } + ] }, - { - id: "runtime-event", - path: "events/:eventName", - loader: async (args) => { - const { params } = args; - const { specVersion, palletName, eventName } = params as { specVersion: string, palletName: string, eventName: string }; - - const { network } = networkLoader(args); - - const eventFullName = `${palletName}.${eventName}`; - const normalizedEventFullName = await normalizeEventName(network, eventFullName, specVersion); - - return { - eventName: normalizedEventFullName.split(".")[1] as string - }; - }, - element: - }, - { - id: "runtime-constant", - path: "constants/:constantName", - loader: async (args) => { - const { params } = args; - const { specVersion, palletName, constantName } = params as { specVersion: string, palletName: string, constantName: string }; - - const { network } = networkLoader(args); - - const constantFullName = `${palletName}.${constantName}`; - const normalizedConstantFullName = await normalizeConstantName(network, constantFullName, specVersion); - - return { - constantName: normalizedConstantFullName.split(".")[1] as string - }; - }, - element: - }, - { - id: "runtime-storage", - path: "storages/:storageName", - loader: async (args) => { - const { params } = args; - const { specVersion, palletName, storageName } = params as { specVersion: string, palletName: string, storageName: string }; - - const { network } = networkLoader(args); - - const storageFullName = `${palletName}.${storageName}`; - const normalizedStorageFullName = await normalizeStorageName(network, storageFullName, specVersion); - - return { - storageName: normalizedStorageFullName.split(".")[1] as string - }; - }, - element: - }, - { - id: "runtime-error", - path: "errors/:errorName", - loader: async (args) => { - const { params } = args; - const { specVersion, palletName, errorName } = params as { specVersion: string, palletName: string, errorName: string }; - - const { network } = networkLoader(args); - - const errorFullName = `${palletName}.${errorName}`; - const normalizedErrorFullName = await normalizeErrorName(network, errorFullName, specVersion); - - return { - errorName: normalizedErrorFullName.split(".")[1] as string - }; - }, - element: - } ] - }, + } ] - } - ] - }, - { - path: "*", - element: , + }, + { + path: "*", + element: , + }, + ], }, - ], - }, + ] + } ] } ]; diff --git a/src/screens/home.tsx b/src/screens/home.tsx index 6738635b..4f0c34db 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -8,6 +8,7 @@ import SearchInput from "../components/SearchInput"; import { Footer } from "../components/Footer"; import { Card } from "../components/Card"; import { ButtonLink } from "../components/ButtonLink"; + import { useNetworkGroups } from "../hooks/useNetworkGroups"; const containerStyle = (theme: Theme) => css` diff --git a/src/services/runtimeMetadataService.ts b/src/services/runtimeMetadataService.ts index 56b5265f..a617598b 100644 --- a/src/services/runtimeMetadataService.ts +++ b/src/services/runtimeMetadataService.ts @@ -159,7 +159,7 @@ export async function normalizeErrorName(network: Network, name: string, specVer /*** PRIVATE ***/ -async function loadRuntimeMetadata(network: string, specVersion: string) { +export async function loadRuntimeMetadata(network: string, specVersion: string) { await self.navigator.locks.request(`runtime-metadata/${network}/${specVersion}`, async () => { const spec = await runtimeMetadataRepository.specs.get([network, specVersion]);