move mortality analyzer to signed extensions
This commit is contained in:
@@ -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<Tab | null>(null)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<BlockInfoView block={block} />
|
||||
<BlockBody block={block} />
|
||||
<BlockContext value={block}>
|
||||
<BlockInfoView />
|
||||
<BlockBody />
|
||||
</BlockContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }) => (
|
||||
<section className="space-y-4 px-4 py-2">
|
||||
<header className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="text-3xl font-semibold tracking-tight">
|
||||
#{block.number.toLocaleString()}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-mono text-foreground/90">
|
||||
<span className="truncate max-w-full">{block.hash}</span>
|
||||
<CopyText className="text-foreground/70" text={block.hash} binary />
|
||||
export const BlockInfoView: FC = () => {
|
||||
const block = useContext(BlockContext)
|
||||
if (!block) return null
|
||||
|
||||
return (
|
||||
<section className="space-y-4 px-4 py-2">
|
||||
<header className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="text-3xl font-semibold tracking-tight">
|
||||
#{block.number.toLocaleString()}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-mono text-foreground/90">
|
||||
<span className="truncate max-w-full">{block.hash}</span>
|
||||
<CopyText className="text-foreground/70" text={block.hash} binary />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
||||
<StatusChip state={block.status} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
||||
<StatusChip state={block.status} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<DetailTile label="Parent block">
|
||||
<BlockLink hash={block.parent} />
|
||||
</DetailTile>
|
||||
<DetailTile label="Children">
|
||||
<BlockChildren hash={block.hash} />
|
||||
</DetailTile>
|
||||
</div>
|
||||
|
||||
{block.header && (
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||
<DetailTile label="Roots">
|
||||
<HashPreview title="state" value={block.header.stateRoot} />
|
||||
<HashPreview title="extrinsic" value={block.header.extrinsicRoot} />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<DetailTile label="Parent block">
|
||||
<BlockLink hash={block.parent} />
|
||||
</DetailTile>
|
||||
<DetailTile label="Block author">
|
||||
<BlockAuthor hash={block.hash} header={block.header} />
|
||||
<DetailTile label="Children">
|
||||
<BlockChildren hash={block.hash} />
|
||||
</DetailTile>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
{block.header && (
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||
<DetailTile label="Roots">
|
||||
<HashPreview title="state" value={block.header.stateRoot} />
|
||||
<HashPreview title="extrinsic" value={block.header.extrinsicRoot} />
|
||||
</DetailTile>
|
||||
<DetailTile label="Block author">
|
||||
<BlockAuthor hash={block.hash} header={block.header} />
|
||||
</DetailTile>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const StatusChip: FC<{ state: BlockState }> = ({ state }) => (
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-foreground/20 bg-foreground/2 px-4 py-2 text-sm font-medium">
|
||||
|
||||
@@ -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<string, unknown> }> = ({
|
||||
extra,
|
||||
}) => (
|
||||
<div className="space-y-2">
|
||||
<h3>Signed extensions</h3>
|
||||
<ul className="space-y-2">
|
||||
{Object.entries(extra).map(([key, value]) => (
|
||||
<SignedExtension key={key} id={key} value={value} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
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 (
|
||||
<li className="flex items-center flex-wrap gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
{inlineJson ? (
|
||||
<div className="whitespace-nowrap">
|
||||
- <span className="font-mono text-sm">{inlineJson}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<ExpandBtn expanded={expanded} onClick={() => setExpanded((e) => !e)} />
|
||||
{id}
|
||||
</div>
|
||||
{expanded && <JsonDisplay src={value} />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export const EventDisplay: FC<{
|
||||
evt: SystemEvent
|
||||
index: number
|
||||
|
||||
46
src/pages/Explorer/Detail/MortalityAnalyzer.tsx
Normal file
46
src/pages/Explorer/Detail/MortalityAnalyzer.tsx
Normal file
@@ -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 <div>Immortal</div>
|
||||
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 (
|
||||
<div>
|
||||
period={period} phase={phase}{" "}
|
||||
{birthBlock != null
|
||||
? `currentBlock=${selectedBlockNumber?.toLocaleString()} from=${birthBlock.toLocaleString()} to=${(birthBlock + period).toLocaleString()}`
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/pages/Explorer/Detail/SignedExtensions.tsx
Normal file
73
src/pages/Explorer/Detail/SignedExtensions.tsx
Normal file
@@ -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<string, unknown> }> = ({
|
||||
extra,
|
||||
}) => (
|
||||
<div className="space-y-2">
|
||||
<h3>Signed extensions</h3>
|
||||
<ul className="space-y-2">
|
||||
{Object.entries(extra).map(([key, value]) => {
|
||||
const KnownExtension = knownSignedExtensions[key]
|
||||
return KnownExtension ? (
|
||||
<KnownExtension key={key} id={key} value={value} />
|
||||
) : (
|
||||
<SignedExtension key={key} id={key} value={value} />
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
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 (
|
||||
<li className="flex items-center flex-wrap gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
{inlineJson ? (
|
||||
<div className="whitespace-nowrap">
|
||||
- <span className="font-mono text-sm">{inlineJson}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<ExpandBtn expanded={expanded} onClick={() => setExpanded((e) => !e)} />
|
||||
{id}
|
||||
</div>
|
||||
{expanded && <JsonDisplay src={value} />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export const knownSignedExtensions: Record<
|
||||
string,
|
||||
ComponentType<{ id: string; value: unknown }>
|
||||
> = {
|
||||
CheckMortality: ({ id, value }) => {
|
||||
return (
|
||||
<li className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
<MortalityAnalyzer mortality={value as any} />
|
||||
</li>
|
||||
)
|
||||
},
|
||||
}
|
||||
4
src/pages/Explorer/Detail/blockContext.ts
Normal file
4
src/pages/Explorer/Detail/blockContext.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createContext } from "react"
|
||||
import { BlockInfo } from "../block.state"
|
||||
|
||||
export const BlockContext = createContext<BlockInfo | null>(null)
|
||||
@@ -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 (
|
||||
<div>
|
||||
<h2 className="capitalize text-xl font-bold">
|
||||
{decoded.type} Transaction v{decoded.version}
|
||||
</h2>
|
||||
{decoded.type === "signed" ? (
|
||||
<SignedInfo extrinsic={extrinsic} decoded={decoded} />
|
||||
) : decoded.type === "general" ? (
|
||||
<div>TODO</div>
|
||||
) : (
|
||||
<AnalyzePriority extrinsic={extrinsic} />
|
||||
)}
|
||||
<CallData call={decoded.call as TxCallData} callData={decoded.callData} />
|
||||
</div>
|
||||
<BlockContext value={block}>
|
||||
<div>
|
||||
<h2 className="capitalize text-xl font-bold">
|
||||
{decoded.type} Transaction v{decoded.version}
|
||||
</h2>
|
||||
{decoded.type === "signed" ? (
|
||||
<SignedInfo extrinsic={extrinsic} decoded={decoded} />
|
||||
) : decoded.type === "general" ? (
|
||||
<div>TODO</div>
|
||||
) : (
|
||||
<AnalyzePriority extrinsic={extrinsic} />
|
||||
)}
|
||||
<CallData
|
||||
call={decoded.call as TxCallData}
|
||||
callData={decoded.callData}
|
||||
/>
|
||||
</div>
|
||||
</BlockContext>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<{
|
||||
<div className="space-y-2 mb-4">
|
||||
<Sender sender={decoded.address} />
|
||||
<SignedExtensions extra={decoded.extra} />
|
||||
{mortality ? <AnalyzeMortality mortality={mortality} /> : null}
|
||||
<AnalyzePriority extrinsic={extrinsic} txPayment={txPayment ?? {}} />
|
||||
</div>
|
||||
)
|
||||
@@ -97,8 +106,4 @@ const CallData: FC<{ call: TxCallData; callData: Uint8Array }> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
export const extrinsicDecoder$ = merge(
|
||||
extDecoder$,
|
||||
analyzeMortality$,
|
||||
analyzePriority$,
|
||||
)
|
||||
export const extrinsicDecoder$ = merge(extDecoder$, analyzePriority$)
|
||||
|
||||
@@ -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<number | null>)
|
||||
: 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 (
|
||||
<div>
|
||||
<b>Mortality:</b> period={period} phase={phase}{" "}
|
||||
{referencedBlocks ? (
|
||||
<>
|
||||
currentBlock={selectedBlockNumber?.toLocaleString()} nextPeriod=
|
||||
{(birthBlock! + period).toLocaleString()} oldPeriods=
|
||||
{referencedBlocks.join(" ")}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const analyzeMortality$ = selectedBlockNumber$
|
||||
Reference in New Issue
Block a user