refactor: move extrinsic stuff to a separate folder

This commit is contained in:
Victor Oliva
2026-05-07 11:31:18 +02:00
parent e800fe447a
commit 5e43b8a185
23 changed files with 94 additions and 98 deletions

View File

@@ -1,9 +1,9 @@
import { BlockContext } from "@/state/block.state"
import { client$ } from "@/state/chains/chain.state"
import { useStateObservable, withDefault } from "@react-rxjs/core"
import { jsonSerialize } from "polkadot-api/utils"
import { FC, useContext } from "react"
import { map, switchMap } from "rxjs"
import { BlockContext } from "./blockContext"
const finalizedNumber$ = client$.pipeState(
switchMap((v) => v.bestBlocks$),
@@ -29,8 +29,8 @@ export const MortalityAnalyzer: FC<{
)
}
const parseResult = /^Mortal(\d+)$/.exec(mortality.type)
if (parseResult === null) {
const decoded = decodeMortality(mortality)
if (decoded === null) {
return (
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-2">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
@@ -43,14 +43,7 @@ export const MortalityAnalyzer: FC<{
)
}
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
const { period, phase } = decoded
// From fn birth
const birthBlock = selectedBlockNumber
? Math.floor((Math.max(selectedBlockNumber, phase) - phase) / period) *
@@ -69,12 +62,8 @@ export const MortalityAnalyzer: FC<{
return (
<div className="space-y-3 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Lifetime
</div>
</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Lifetime
</div>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
@@ -152,11 +141,7 @@ const Metric: FC<{ label: string; value: string }> = ({ label, value }) => (
const formatOptional = (value: number | null) =>
value == null ? "N/A" : value.toLocaleString()
const getTimelineProgress = (
start: number,
end: number,
value: number,
): number => {
const getTimelineProgress = (start: number, end: number, value: number) => {
if (end <= start) return 0
const raw = ((value - start) / (end - start)) * 100
return Math.max(0, Math.min(100, raw))
@@ -167,8 +152,15 @@ export const InlineMortality: FC<{
}> = ({ mortality }) => {
if (mortality.type === "Immortal") return "Immortal"
const decoded = decodeMortality(mortality)
if (decoded === null) return JSON.stringify(mortality, jsonSerialize)
return JSON.stringify(decoded) + " " + JSON.stringify(mortality)
}
const decodeMortality = (mortality: { type: string; value: number }) => {
const parseResult = /^Mortal(\d+)$/.exec(mortality.type)
if (parseResult === null) return JSON.stringify(mortality, jsonSerialize)
if (!parseResult) return null
const first = BigInt(parseResult[1])
const second = BigInt(mortality.value)
@@ -177,6 +169,5 @@ export const InlineMortality: FC<{
const period = Number(2n << (encoded % (1n << 4n)))
const factor = period >> 12 || 1
const phase = Number(encoded >> 4n) * factor
return JSON.stringify({ period, phase }) + " " + JSON.stringify(mortality)
return { period, phase }
}

View File

@@ -1,5 +1,5 @@
import { TokenAmount } from "@/components/TokenAmount"
import { BlockContext } from "@/pages/Explorer/Detail/blockContext"
import { BlockContext } from "@/state/block.state"
import { client$ } from "@/state/chains/chain.state"
import { polkadot_people } from "@polkadot-api/descriptors"
import { Binary, HexString } from "polkadot-api"
@@ -127,18 +127,12 @@ const getPriority = async (
try {
const [length, weights, queryInfo] = await Promise.all([
typedApi.constants.System.BlockLength({
at,
}),
typedApi.constants.System.BlockWeights({
at,
}),
typedApi.constants.System.BlockLength({ at }),
typedApi.constants.System.BlockWeights({ at }),
typedApi.apis.TransactionPaymentApi.query_info(
Binary.fromOpaque(extrinsic),
extLength,
{
at,
},
{ at },
),
])
@@ -176,6 +170,7 @@ const getPriority = async (
return null
}
}
type PriorityDetails = Awaited<ReturnType<typeof getPriority>>
const usePriorityAnalysis = ({
@@ -192,8 +187,7 @@ const usePriorityAnalysis = ({
useEffect(() => {
let cancelled = false
getPriority(extrinsic, txPayment, blockHex).then((r) => {
if (cancelled) return
setResult(r)
if (!cancelled) setResult(r)
})
return () => {
cancelled = true

View File

@@ -0,0 +1,19 @@
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
import { Enum, HexString, SS58String } from "polkadot-api"
const senderToAddress = (
sender: Enum<{ Id: SS58String }> | SS58String | HexString,
) =>
typeof sender === "string"
? sender
: "type" in sender && sender.type === "Id"
? sender.value
: null
export const Sender: React.FC<{
sender: Enum<{ Id: SS58String }> | SS58String | HexString
}> = ({ sender }) => {
const value = senderToAddress(sender)
if (!value) return null
return value ? <AccountIdDisplay value={value} /> : null
}

View File

@@ -76,7 +76,7 @@ const ExpandableLine: FC<
return (
<li className="space-y-2 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<ExpandBtn
className="shrink-0"
expanded={expanded}
@@ -84,7 +84,7 @@ const ExpandableLine: FC<
/>
{id}
{inlineContent ? (
<div className="max-w-full whitespace-nowrap font-mono text-sm overflow-hidden text-ellipsis">
<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-mono text-sm">
- {inlineContent}
</div>
) : null}
@@ -99,12 +99,12 @@ const InlineLine: FC<{ id: string; inlineContent?: ReactNode }> = ({
inlineContent,
}) => (
<li className="flex items-center gap-2 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<Dot size={16} />
{id}
</div>
{inlineContent ? (
<div className="max-w-full whitespace-nowrap font-mono text-sm overflow-hidden text-ellipsis">
<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-mono text-sm">
- {inlineContent}
</div>
) : null}

View File

@@ -0,0 +1,4 @@
export { MortalityAnalyzer, InlineMortality } from "./MortalityAnalyzer"
export { AnalyzePriority, PriorityValue } from "./Priority"
export { Sender } from "./Sender"
export { SignedExtensions, knownSignedExtensions } from "./SignedExtensions"

View File

@@ -2,11 +2,11 @@ import { CircularProgress } from "@/components/CircularProgress"
import { CopyText } from "@/components/Copy"
import { Link } from "@/hashParams"
import { groupBy } from "@/lib/groupBy"
import { blockInfoState$ } from "@/state/block.state"
import { runtimeCtx$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
import { map } from "rxjs"
import { blockInfoState$ } from "./block.state"
import { BlockStatusIcon, statusText } from "./Detail/BlockState"
import { filterEvt } from "./events.state"

View File

@@ -1,17 +1,17 @@
import { CopyText } from "@/components/Copy"
import { Popover } from "@/components/Popover"
import { Button } from "@/components/ui/button"
import { SearchInput } from "@/components/ui/search-input"
import { Link, useNavigate } from "@/hashParams"
import { BlockInfo, blocksByHeight$, finalized$ } from "@/state/block.state"
import { client$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { Search } from "lucide-react"
import { FC } from "react"
import { combineLatest, debounceTime, map, switchMap } from "rxjs"
import { twMerge } from "tailwind-merge"
import { BlockInfo, blocksByHeight$, finalized$ } from "./block.state"
import { BlockPopover } from "./BlockPopover"
import * as Finalizing from "./FinalizingTable"
import { Button } from "@/components/ui/button"
import { SearchInput } from "@/components/ui/search-input"
import { Search } from "lucide-react"
const best$ = client$.pipeState(
switchMap((client) => client.bestBlocks$.pipe(map(([best]) => best))),
@@ -174,7 +174,7 @@ export const BlockTable = () => {
<button
className={twMerge(
"overflow-hidden text-ellipsis whitespace-nowrap font-mono text-sm",
"text-card-foreground/80 hover:text-card-foreground/100",
"text-card-foreground/80 hover:text-card-foreground",
row.position === 0
? ""
: row.block.number > finalized.number

View File

@@ -1,3 +1,4 @@
import { blockInfoState$, BlockState, finalized$ } from "@/state/block.state"
import { client$ } from "@/state/chains/chain.state"
import { Polkadot_people } from "@polkadot-api/descriptors"
import {
@@ -23,7 +24,6 @@ import {
switchMap,
take,
} from "rxjs"
import { blockInfoState$, BlockState, finalized$ } from "../block.state"
const validatorCache: WeakMap<
PolkadotClient,

View File

@@ -2,6 +2,7 @@ import { Chopsticks } from "@/components/Icons"
import { Loading } from "@/components/Loading"
import { getHashParams } from "@/hashParams"
import { groupBy } from "@/lib/groupBy"
import { BlockContext, blockInfoState$ } from "@/state/block.state"
import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state"
import { Blake2256 } from "@polkadot-api/substrate-bindings"
import { getExtrinsicDecoder } from "@polkadot-api/tx-utils"
@@ -12,11 +13,9 @@ 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 { 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(

View File

@@ -1,10 +1,9 @@
import { Loading } from "@/components/Loading"
import { BlockContext, blockInfoState$ } from "@/state/block.state"
import { useStateObservable } from "@react-rxjs/core"
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()

View File

@@ -1,5 +1,5 @@
import { FC, useState } from "react"
import { BlockInfo } from "../block.state"
import { BlockInfo } from "@/state/block.state"
import { groupBy } from "@/lib/groupBy"
import { EventDisplay } from "./Extrinsic"
import { SystemEvent } from "@polkadot-api/observable-client"

View File

@@ -1,13 +1,17 @@
import { CopyText } from "@/components/Copy"
import { Link } from "@/hashParams"
import {
BlockContext,
blockInfoState$,
blocksByHeight$,
BlockState,
} from "@/state/block.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { FC, ReactNode, useContext } from "react"
import { filter, map, startWith, switchMap, take } from "rxjs"
import { blockInfoState$, blocksByHeight$, BlockState } from "../block.state"
import { BlockAuthor } from "./BlockAuthor"
import { BlockStatusIcon, statusText } from "./BlockState"
import { BlockContext } from "./blockContext"
export const BlockInfoView: FC = () => {
const block = useContext(BlockContext)

View File

@@ -1,5 +1,4 @@
import { ComponentType, FC } from "react"
import { BlockState } from "../block.state"
import { BlockState } from "@/state/block.state"
import {
CircleAlert,
CircleCheck,
@@ -8,6 +7,7 @@ import {
Clock,
LucideProps,
} from "lucide-react"
import { ComponentType, FC } from "react"
import { twMerge } from "tailwind-merge"
const statusIcon: Record<BlockState, ComponentType<LucideProps>> = {

View File

@@ -8,6 +8,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion"
import { groupBy } from "@/lib/groupBy"
import { BlockInfo, blockInfo$ } from "@/state/block.state"
import { dynamicBuilder$, lookup$ } from "@/state/chains/chain.state"
import {
HexString,
@@ -19,12 +20,11 @@ import {
u8,
Vector,
} from "@polkadot-api/substrate-bindings"
import { toHex } from "polkadot-api/utils"
import { state, useStateObservable } from "@react-rxjs/core"
import { Binary, Codec } from "polkadot-api"
import { toHex } from "polkadot-api/utils"
import { FC } from "react"
import { combineLatest, map } from "rxjs"
import { BlockInfo, blockInfo$ } from "../block.state"
const TWOX128_LEN = 32

View File

@@ -1,19 +1,21 @@
import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary"
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
import { CopyText } from "@/components/Copy"
import { ExpandBtn } from "@/components/Expand"
import {
PriorityValue,
Sender,
SignedExtensions,
} from "@/components/Extrinsics"
import { JsonDisplay } from "@/components/JsonDisplay"
import { Link } from "@/hashParams"
import { shortStr } from "@/utils"
import { SystemEvent } from "@polkadot-api/observable-client"
import { DecodedExtrinsic } from "@polkadot-api/tx-utils"
import { Edit, FileSearch } from "lucide-react"
import { Enum, HexString, SS58String } from "polkadot-api"
import { HexString } from "polkadot-api"
import { toHex } from "polkadot-api/utils"
import { FC, ReactNode, useEffect, useRef, useState } from "react"
import { twMerge } from "tailwind-merge"
import { PriorityValue } from "../../Extrinsics/Analyzer/Priority"
import { SignedExtensions } from "./SignedExtensions"
export type ApplyExtrinsicEvent = SystemEvent & {
phase: { type: "ApplyExtrinsic" }
@@ -73,9 +75,11 @@ export const Extrinsic: FC<{
{expanded ? (
<div className="space-y-2 border-t border-foreground/10 px-2 py-2">
<div className="grid gap-2 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)_auto]">
<CompactBlock label="Signer">
<Sender sender={sender} />
</CompactBlock>
{sender ? (
<CompactBlock label="Signer">
<Sender sender={sender} />
</CompactBlock>
) : null}
<CompactBlock label="Extrinsic Hash">
<div className="flex min-w-0 items-center gap-2">
@@ -160,23 +164,6 @@ export const EventDisplay: FC<{
)
}
const senderToAddress = (
sender: Enum<{ Id: SS58String }> | SS58String | HexString,
) =>
typeof sender === "string"
? sender
: "type" in sender && sender.type === "Id"
? sender.value
: null
export const Sender: React.FC<{
sender: Enum<{ Id: SS58String }> | SS58String | HexString
}> = ({ sender }) => {
const value = senderToAddress(sender)
if (!value) return null
return <AccountIdDisplay value={value} />
}
const CompactSection: FC<{ title: string; children: ReactNode }> = ({
title,
children,

View File

@@ -1,4 +0,0 @@
import { createContext } from "react"
import { BlockInfo } from "../block.state"
export const BlockContext = createContext<BlockInfo | null>(null)

View File

@@ -1,8 +1,8 @@
import { Popover } from "@/components/Popover"
import { Link } from "@/hashParams"
import { finalized$ } from "@/state/block.state"
import { useStateObservable } from "@react-rxjs/core"
import { twMerge } from "tailwind-merge"
import { finalized$ } from "./block.state"
import { EventPopover } from "./EventPopover"
import { eventKey, recentEvents$ } from "./events.state"
import * as Finalizing from "./FinalizingTable"

View File

@@ -1,9 +1,9 @@
import { groupBy } from "@/lib/groupBy"
import { blockInfo$, BlockState, recordedBlocks$ } from "@/state/block.state"
import type { SystemEvent } from "@polkadot-api/observable-client"
import { state } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { filter, map, takeWhile } from "rxjs"
import { blockInfo$, BlockState, recordedBlocks$ } from "./block.state"
const blackList = new Set([
"System.*",

View File

@@ -1,4 +1,4 @@
import { blocksByHeight$ } from "./block.state"
import { blocksByHeight$ } from "@/state/block.state"
export * from "./Explorer"
export const explorer$ = blocksByHeight$

View File

@@ -1,9 +1,12 @@
import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary"
import { CopyText } from "@/components/Copy"
import {
AnalyzePriority,
Sender,
SignedExtensions,
} from "@/components/Extrinsics"
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 { BlockContext, blockInfoState$ } from "@/state/block.state"
import { chainClient$ } from "@/state/chains/chain.state"
import { shortStr } from "@/utils"
import { getExtrinsicDecoder } from "@polkadot-api/tx-utils"
@@ -12,8 +15,6 @@ import { HexString, TxCallData } from "polkadot-api"
import { fromHex, toHex } from "polkadot-api/utils"
import { FC, ReactNode, useMemo } from "react"
import { map, switchMap } from "rxjs"
import { Sender } from "../../Explorer/Detail/Extrinsic"
import { AnalyzePriority } from "./Priority"
import { selectedBlock$, selectedBlockHex$ } from "./selectedBlock"
const extDecoder$ = selectedBlock$.pipeState(

View File

@@ -1,4 +1,5 @@
import { Spinner } from "@/components/Icons"
import { blockHash$ } from "@/state/block.state"
import {
CachedRuntime,
runtimeCtx$,
@@ -22,7 +23,6 @@ import {
startWith,
switchMap,
} from "rxjs"
import { blockHash$ } from "../Explorer/block.state"
const [valueChange$, setValue] = createSignal<string>()
const inputValue$ = state(valueChange$, "Latest")

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/tooltip"
import { Link } from "@/hashParams"
import { cn } from "@/lib/utils"
import { BlockState } from "@/state/block.state"
import { shortStr } from "@/utils"
import { ViewValue } from "@/ViewValue"
import { RuntimeContext } from "@polkadot-api/observable-client"
@@ -18,9 +19,9 @@ import { Button } from "@polkahub/ui-components"
import { useStateObservable } from "@react-rxjs/core"
import { ChevronLeft, ChevronRight, StopCircle, Trash2 } from "lucide-react"
import { Enum } from "polkadot-api"
import { fromHex } from "polkadot-api/utils"
import { FC, MouseEvent, ReactNode, useMemo, useState } from "react"
import { Virtuoso } from "react-virtuoso"
import { BlockState } from "../Explorer/block.state"
import { BlockStatusIcon } from "../Explorer/Detail/BlockState"
import {
KeyCodec,
@@ -30,7 +31,6 @@ import {
storageSubscriptionKeys$,
StorageSubscriptionValue,
} from "./storage.state"
import { fromHex } from "polkadot-api/utils"
export const StorageSubscriptions: FC = () => {
const keys = useStateObservable(storageSubscriptionKeys$)

View File

@@ -11,6 +11,7 @@ import {
BlockInfo as RawBlockInfo,
} from "polkadot-api"
import { fromHex } from "polkadot-api/utils"
import { createContext } from "react"
import {
catchError,
combineLatest,
@@ -61,6 +62,7 @@ export interface BlockInfo {
status: BlockState
diff: Record<string, [string | null, string | null]> | null
}
export const BlockContext = createContext<BlockInfo | null>(null)
const finalizedBlocks$ = state(
client$.pipe(