From 23345e7e3548d26c0344037ce6ce632f504291a0 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Tue, 5 May 2026 13:11:34 +0200 Subject: [PATCH] move mortality analyzer to signed extensions --- src/pages/Explorer/Detail/BlockBody.tsx | 10 +-- src/pages/Explorer/Detail/BlockDetail.tsx | 7 +- src/pages/Explorer/Detail/BlockInfo.tsx | 85 +++++++++---------- src/pages/Explorer/Detail/Extrinsic.tsx | 48 +---------- .../Explorer/Detail/MortalityAnalyzer.tsx | 46 ++++++++++ .../Explorer/Detail/SignedExtensions.tsx | 73 ++++++++++++++++ src/pages/Explorer/Detail/blockContext.ts | 4 + .../Extrinsics/Analyzer/ExtrinsicDecoder.tsx | 63 +++++++------- src/pages/Extrinsics/Analyzer/Mortality.tsx | 66 -------------- 9 files changed, 211 insertions(+), 191 deletions(-) create mode 100644 src/pages/Explorer/Detail/MortalityAnalyzer.tsx create mode 100644 src/pages/Explorer/Detail/SignedExtensions.tsx create mode 100644 src/pages/Explorer/Detail/blockContext.ts delete mode 100644 src/pages/Extrinsics/Analyzer/Mortality.tsx diff --git a/src/pages/Explorer/Detail/BlockBody.tsx b/src/pages/Explorer/Detail/BlockBody.tsx index c5b1ac2..61181f2 100644 --- a/src/pages/Explorer/Detail/BlockBody.tsx +++ b/src/pages/Explorer/Detail/BlockBody.tsx @@ -8,14 +8,15 @@ import { getExtrinsicDecoder } from "@polkadot-api/tx-utils" import * as Tabs from "@radix-ui/react-tabs" import { state, useStateObservable } from "@react-rxjs/core" import { toHex } from "polkadot-api/utils" -import { FC, useState } from "react" +import { FC, useContext, useState } from "react" import { useLocation } from "react-router-dom" import { catchError, combineLatest, filter, map, take } from "rxjs" import { twMerge } from "tailwind-merge" -import { BlockInfo, blockInfoState$ } from "../block.state" +import { blockInfoState$ } from "../block.state" import { BlockEvents } from "./BlockEvents" import { BlockStorageDiff } from "./BlockStorageDiff" import { ApplyExtrinsicEvent, Extrinsic } from "./Extrinsic" +import { BlockContext } from "./blockContext" const blockExtrinsics$ = state((hash: string) => { const body$ = blockInfoState$(hash).pipe( @@ -44,9 +45,8 @@ const blockExtrinsics$ = state((hash: string) => { }, []) type Tab = "tx" | "events" | "diff" -export const BlockBody: FC<{ - block: BlockInfo -}> = ({ block }) => { +export const BlockBody: FC = () => { + const block = useContext(BlockContext)! const extrinsics = useStateObservable(blockExtrinsics$(block.hash)) const [selectedTab, setSelectedTab] = useState(null) diff --git a/src/pages/Explorer/Detail/BlockDetail.tsx b/src/pages/Explorer/Detail/BlockDetail.tsx index fc444e7..71e5e0e 100644 --- a/src/pages/Explorer/Detail/BlockDetail.tsx +++ b/src/pages/Explorer/Detail/BlockDetail.tsx @@ -4,6 +4,7 @@ import { useParams } from "react-router-dom" import { blockInfoState$ } from "../block.state" import { BlockBody } from "./BlockBody" import { BlockInfoView } from "./BlockInfo" +import { BlockContext } from "./blockContext" export const BlockDetail = () => { const { hashOrHeight } = useParams() @@ -13,8 +14,10 @@ export const BlockDetail = () => { return (
- - + + + +
) } diff --git a/src/pages/Explorer/Detail/BlockInfo.tsx b/src/pages/Explorer/Detail/BlockInfo.tsx index 9d23a87..0c12aaa 100644 --- a/src/pages/Explorer/Detail/BlockInfo.tsx +++ b/src/pages/Explorer/Detail/BlockInfo.tsx @@ -2,58 +2,57 @@ import { CopyText } from "@/components/Copy" import { Link } from "@/hashParams" import { state, useStateObservable } from "@react-rxjs/core" import { combineKeys } from "@react-rxjs/utils" -import { FC, ReactNode } from "react" +import { FC, ReactNode, useContext } from "react" import { filter, map, startWith, switchMap, take } from "rxjs" -import { - BlockInfo, - blockInfoState$, - blocksByHeight$, - BlockState, -} from "../block.state" +import { blockInfoState$, blocksByHeight$, BlockState } from "../block.state" import { BlockAuthor } from "./BlockAuthor" import { BlockStatusIcon, statusText } from "./BlockState" +import { BlockContext } from "./blockContext" -export const BlockInfoView: FC<{ - block: BlockInfo -}> = ({ block }) => ( -
-
-
-

- #{block.number.toLocaleString()} -

-
- {block.hash} - +export const BlockInfoView: FC = () => { + const block = useContext(BlockContext) + if (!block) return null + + return ( +
+
+
+

+ #{block.number.toLocaleString()} +

+
+ {block.hash} + +
-
-
- -
-
+
+ +
+ -
- - - - - - -
- - {block.header && ( -
- - - +
+ + - - + +
- )} -
-) + + {block.header && ( +
+ + + + + + + +
+ )} + + ) +} const StatusChip: FC<{ state: BlockState }> = ({ state }) => ( diff --git a/src/pages/Explorer/Detail/Extrinsic.tsx b/src/pages/Explorer/Detail/Extrinsic.tsx index 6954f0c..826e032 100644 --- a/src/pages/Explorer/Detail/Extrinsic.tsx +++ b/src/pages/Explorer/Detail/Extrinsic.tsx @@ -8,11 +8,12 @@ import { Link } from "@/hashParams" import { shortStr } from "@/utils" import { SystemEvent } from "@polkadot-api/observable-client" import { DecodedExtrinsic } from "@polkadot-api/tx-utils" -import { Dot, Edit, FileSearch } from "lucide-react" +import { Edit, FileSearch } from "lucide-react" import { Enum, HexString, SS58String } from "polkadot-api" import { toHex } from "polkadot-api/utils" import { FC, useEffect, useRef, useState } from "react" import { twMerge } from "tailwind-merge" +import { SignedExtensions } from "./SignedExtensions" export type ApplyExtrinsicEvent = SystemEvent & { phase: { type: "ApplyExtrinsic" } @@ -93,51 +94,6 @@ export const Extrinsic: FC<{ ) } -export const SignedExtensions: FC<{ extra: Record }> = ({ - extra, -}) => ( -
-

Signed extensions

-
    - {Object.entries(extra).map(([key, value]) => ( - - ))} -
-
-) - -const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => { - const [expanded, setExpanded] = useState(false) - const inlineJson = JSON.stringify(value, (_, v) => - typeof v === "bigint" ? String(v) : v instanceof Uint8Array ? toHex(v) : v, - ) - - if (!inlineJson || inlineJson.length < 40) { - return ( -
  • -
    - - {id} -
    - {inlineJson ? ( -
    - - {inlineJson} -
    - ) : null} -
  • - ) - } - return ( -
  • -
    - setExpanded((e) => !e)} /> - {id} -
    - {expanded && } -
  • - ) -} - export const EventDisplay: FC<{ evt: SystemEvent index: number diff --git a/src/pages/Explorer/Detail/MortalityAnalyzer.tsx b/src/pages/Explorer/Detail/MortalityAnalyzer.tsx new file mode 100644 index 0000000..cecc9b4 --- /dev/null +++ b/src/pages/Explorer/Detail/MortalityAnalyzer.tsx @@ -0,0 +1,46 @@ +import { client$ } from "@/state/chains/chain.state" +import { useStateObservable, withDefault } from "@react-rxjs/core" +import { FC, useContext } from "react" +import { map, switchMap } from "rxjs" +import { BlockContext } from "./blockContext" + +const finalizedNumber$ = client$.pipeState( + switchMap((v) => v.finalizedBlock$), + map((block) => block.number), + withDefault(null), +) + +export const MortalityAnalyzer: FC<{ + mortality: { type: string; value: number } +}> = ({ mortality }) => { + const selectedBlock = useContext(BlockContext) + const finalizedNumber = useStateObservable(finalizedNumber$) + const selectedBlockNumber = selectedBlock?.number ?? finalizedNumber + + if (mortality.type === "Immortal") return
    Immortal
    + const parseResult = /^Mortal(\d+)$/.exec(mortality.type) + if (parseResult === null) return null + const first = BigInt(parseResult[1]) + const second = BigInt(mortality.value) + // from polkadot-sdk primitives runtime generic era fn decode + const encoded = first + (second << 8n) + const period = Number(2n << (encoded % (1n << 4n))) + const factor = period >> 12 || 1 + const phase = Number(encoded >> 4n) * factor + + // From fn birth + const birthBlock = selectedBlockNumber + ? Math.floor((Math.max(selectedBlockNumber, phase) - phase) / period) * + period + + phase + : null + + return ( +
    + period={period} phase={phase}{" "} + {birthBlock != null + ? `currentBlock=${selectedBlockNumber?.toLocaleString()} from=${birthBlock.toLocaleString()} to=${(birthBlock + period).toLocaleString()}` + : null} +
    + ) +} diff --git a/src/pages/Explorer/Detail/SignedExtensions.tsx b/src/pages/Explorer/Detail/SignedExtensions.tsx new file mode 100644 index 0000000..da58892 --- /dev/null +++ b/src/pages/Explorer/Detail/SignedExtensions.tsx @@ -0,0 +1,73 @@ +import { ExpandBtn } from "@/components/Expand" +import { JsonDisplay } from "@/components/JsonDisplay" +import { Dot } from "lucide-react" +import { toHex } from "polkadot-api/utils" +import { ComponentType, FC, useState } from "react" +import { MortalityAnalyzer } from "./MortalityAnalyzer" + +export const SignedExtensions: FC<{ extra: Record }> = ({ + extra, +}) => ( +
    +

    Signed extensions

    +
      + {Object.entries(extra).map(([key, value]) => { + const KnownExtension = knownSignedExtensions[key] + return KnownExtension ? ( + + ) : ( + + ) + })} +
    +
    +) + +const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => { + const [expanded, setExpanded] = useState(false) + const inlineJson = JSON.stringify(value, (_, v) => + typeof v === "bigint" ? String(v) : v instanceof Uint8Array ? toHex(v) : v, + ) + + if (!inlineJson || inlineJson.length < 40) { + return ( +
  • +
    + + {id} +
    + {inlineJson ? ( +
    + - {inlineJson} +
    + ) : null} +
  • + ) + } + return ( +
  • +
    + setExpanded((e) => !e)} /> + {id} +
    + {expanded && } +
  • + ) +} + +export const knownSignedExtensions: Record< + string, + ComponentType<{ id: string; value: unknown }> +> = { + CheckMortality: ({ id, value }) => { + return ( +
  • +
    + + {id} +
    + +
  • + ) + }, +} diff --git a/src/pages/Explorer/Detail/blockContext.ts b/src/pages/Explorer/Detail/blockContext.ts new file mode 100644 index 0000000..1dcafdf --- /dev/null +++ b/src/pages/Explorer/Detail/blockContext.ts @@ -0,0 +1,4 @@ +import { createContext } from "react" +import { BlockInfo } from "../block.state" + +export const BlockContext = createContext(null) diff --git a/src/pages/Extrinsics/Analyzer/ExtrinsicDecoder.tsx b/src/pages/Extrinsics/Analyzer/ExtrinsicDecoder.tsx index 1156135..6ad0b88 100644 --- a/src/pages/Extrinsics/Analyzer/ExtrinsicDecoder.tsx +++ b/src/pages/Extrinsics/Analyzer/ExtrinsicDecoder.tsx @@ -1,25 +1,31 @@ import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary" import { JsonDisplay } from "@/components/JsonDisplay" +import { blockInfoState$ } from "@/pages/Explorer/block.state" +import { BlockContext } from "@/pages/Explorer/Detail/blockContext" +import { SignedExtensions } from "@/pages/Explorer/Detail/SignedExtensions" import { DecodedExtrinsic, getExtrinsicDecoder } from "@polkadot-api/tx-utils" -import { state, useStateObservable } from "@react-rxjs/core" +import { useStateObservable, withDefault } from "@react-rxjs/core" import { HexString, TxCallData } from "polkadot-api" import { toHex } from "polkadot-api/utils" import { FC, useMemo } from "react" -import { map, merge } from "rxjs" -import { Sender, SignedExtensions } from "../../Explorer/Detail/Extrinsic" -import { AnalyzeMortality, analyzeMortality$ } from "./Mortality" +import { map, merge, switchMap } from "rxjs" +import { Sender } from "../../Explorer/Detail/Extrinsic" import { AnalyzePriority, analyzePriority$ } from "./Priority" -import { selectedBlock$ } from "./selectedBlock" +import { selectedBlock$, selectedBlockHex$ } from "./selectedBlock" -export const extDecoder$ = state( - selectedBlock$.pipe( - map((block) => getExtrinsicDecoder(block.ctx.metadataRaw)), - ), +const extDecoder$ = selectedBlock$.pipeState( + map((block) => getExtrinsicDecoder(block.ctx.metadataRaw)), +) + +const selectedBlockInfo$ = selectedBlockHex$.pipeState( + switchMap((hex) => (hex ? blockInfoState$(hex) : [null])), + withDefault(null), ) export const ExtrinsicDecoder: FC<{ extrinsic: HexString }> = ({ extrinsic }) => { + const block = useStateObservable(selectedBlockInfo$) const extrinsicDecoder = useStateObservable(extDecoder$) const decodeResult = useMemo(() => { try { @@ -43,19 +49,24 @@ export const ExtrinsicDecoder: FC<{ const decoded = decodeResult.value return ( -
    -

    - {decoded.type} Transaction v{decoded.version} -

    - {decoded.type === "signed" ? ( - - ) : decoded.type === "general" ? ( -
    TODO
    - ) : ( - - )} - -
    + +
    +

    + {decoded.type} Transaction v{decoded.version} +

    + {decoded.type === "signed" ? ( + + ) : decoded.type === "general" ? ( +
    TODO
    + ) : ( + + )} + +
    +
    ) } @@ -63,7 +74,6 @@ const SignedInfo: FC<{ extrinsic: HexString decoded: DecodedExtrinsic & { type: "signed" } }> = ({ extrinsic, decoded }) => { - const mortality = decoded.extra.CheckMortality const txPayment = decoded.extra.ChargeAssetTxPayment ?? decoded.extra.ChargeTxPayment @@ -71,7 +81,6 @@ const SignedInfo: FC<{
    - {mortality ? : null}
    ) @@ -97,8 +106,4 @@ const CallData: FC<{ call: TxCallData; callData: Uint8Array }> = ({ ) -export const extrinsicDecoder$ = merge( - extDecoder$, - analyzeMortality$, - analyzePriority$, -) +export const extrinsicDecoder$ = merge(extDecoder$, analyzePriority$) diff --git a/src/pages/Extrinsics/Analyzer/Mortality.tsx b/src/pages/Extrinsics/Analyzer/Mortality.tsx deleted file mode 100644 index 25f0c4b..0000000 --- a/src/pages/Extrinsics/Analyzer/Mortality.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { client$ } from "@/state/chains/chain.state" -import { polkadot_people } from "@polkadot-api/descriptors" -import { state, useStateObservable } from "@react-rxjs/core" -import { FC } from "react" -import { combineLatest, map, switchMap } from "rxjs" -import { selectedBlockHex$ } from "./selectedBlock" - -const selectedBlockNumber$ = state( - combineLatest([selectedBlockHex$, client$]).pipe( - switchMap(([selectedBlock, client]) => - selectedBlock - ? (client - .getTypedApi(polkadot_people) - .query.System.Number.getValue({ - at: selectedBlock, - }) - .catch(() => null) as Promise) - : client.finalizedBlock$.pipe(map((block) => block.number)), - ), - ), -) - -export const AnalyzeMortality: FC<{ - mortality: { type: string; value: number } -}> = ({ mortality }) => { - const selectedBlockNumber = useStateObservable(selectedBlockNumber$) - - if (mortality.type === "Immortal") return null - const parseResult = /^Mortal(\d+)$/.exec(mortality.type) - if (parseResult === null) return null - const first = BigInt(parseResult[1]) - const second = BigInt(mortality.value) - // from polkadot-sdk primitives runtime generic era fn decode - const encoded = first + (second << 8n) - const period = Number(2n << (encoded % (1n << 4n))) - const factor = period >> 12 || 1 - const phase = Number(encoded >> 4n) * factor - - // From fn birth - const birthBlock = selectedBlockNumber - ? Math.floor((Math.max(selectedBlockNumber, phase) - phase) / period) * - period + - phase - : null - - const referencedBlocks = birthBlock - ? new Array(5) - .fill(0) - .map((_, i) => (birthBlock - period * i).toLocaleString()) - : null - - return ( -
    - Mortality: period={period} phase={phase}{" "} - {referencedBlocks ? ( - <> - currentBlock={selectedBlockNumber?.toLocaleString()} nextPeriod= - {(birthBlock! + period).toLocaleString()} oldPeriods= - {referencedBlocks.join(" ")} - - ) : null} -
    - ) -} - -export const analyzeMortality$ = selectedBlockNumber$