From e0e42a5cf1b5ada9883a8b851a66954c0eb4cef6 Mon Sep 17 00:00:00 2001 From: Josep M Sobrepere Date: Thu, 25 Sep 2025 04:58:35 +0800 Subject: [PATCH] fix: block status (#86) --- src/pages/Explorer/Detail/BlockBody.tsx | 23 ++--- src/pages/Explorer/Detail/BlockInfo.tsx | 1 + src/pages/Explorer/Detail/BlockState.tsx | 4 + src/pages/Explorer/block.state.ts | 123 +++++++++++++++++------ 4 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/pages/Explorer/Detail/BlockBody.tsx b/src/pages/Explorer/Detail/BlockBody.tsx index d1b2368..673b0b6 100644 --- a/src/pages/Explorer/Detail/BlockBody.tsx +++ b/src/pages/Explorer/Detail/BlockBody.tsx @@ -5,15 +5,8 @@ import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state" import * as Tabs from "@radix-ui/react-tabs" import { state, useStateObservable } from "@react-rxjs/core" import { FC, useState } from "react" -import { useLocation, useParams } from "react-router-dom" -import { - catchError, - combineLatest, - distinctUntilChanged, - filter, - map, - take, -} from "rxjs" +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 { BlockEvents } from "./BlockEvents" @@ -25,15 +18,14 @@ import { fromHex, toHex } from "@polkadot-api/utils" const blockExtrinsics$ = state((hash: string) => { const body$ = blockInfoState$(hash).pipe( - filter((v) => !!v), - map((v) => v.body), - filter((v) => !!v), - distinctUntilChanged(), + filter((x) => !!x?.body), + map((v) => v!.body!), + take(1), ) return combineLatest([ body$, - runtimeCtxAt$(hash).pipeState(catchError(() => runtimeCtx$)), + runtimeCtxAt$(hash).pipe(catchError(() => runtimeCtx$)), ]).pipe( take(1), map(([body, { txDecoder }]) => @@ -50,9 +42,8 @@ type Tab = "tx" | "events" | "diff" export const BlockBody: FC<{ block: BlockInfo }> = ({ block }) => { - const { hash } = useParams() + const extrinsics = useStateObservable(blockExtrinsics$(block.hash)) const [selectedTab, setSelectedTab] = useState(null) - const extrinsics = useStateObservable(blockExtrinsics$(hash ?? "")) const location = useLocation() const hashParams = getHashParams(location) diff --git a/src/pages/Explorer/Detail/BlockInfo.tsx b/src/pages/Explorer/Detail/BlockInfo.tsx index 13dbf11..e819965 100644 --- a/src/pages/Explorer/Detail/BlockInfo.tsx +++ b/src/pages/Explorer/Detail/BlockInfo.tsx @@ -101,6 +101,7 @@ const statusValue: Record = { [BlockState.Best]: 2, [BlockState.Fork]: 1, [BlockState.Pruned]: 0, + [BlockState.Unknown]: -1, } const BlockChildren: FC<{ hash: string }> = ({ hash }) => { diff --git a/src/pages/Explorer/Detail/BlockState.tsx b/src/pages/Explorer/Detail/BlockState.tsx index a027a89..b04caa1 100644 --- a/src/pages/Explorer/Detail/BlockState.tsx +++ b/src/pages/Explorer/Detail/BlockState.tsx @@ -3,6 +3,7 @@ import { BlockState } from "../block.state" import { CircleAlert, CircleCheck, + CircleQuestionMark, CircleX, Clock, LucideProps, @@ -14,12 +15,14 @@ const statusIcon: Record> = { [BlockState.Fork]: CircleAlert, [BlockState.Finalized]: CircleCheck, [BlockState.Pruned]: CircleX, + [BlockState.Unknown]: CircleQuestionMark, } const statusClassName: Record = { [BlockState.Best]: "text-blue-400", [BlockState.Fork]: "text-orange-400", [BlockState.Finalized]: "text-green-400", [BlockState.Pruned]: "text-red-400", + [BlockState.Unknown]: "text-orange-400", } export const BlockStatusIcon: FC< @@ -38,4 +41,5 @@ export const statusText: Record = { [BlockState.Fork]: "Fork", [BlockState.Finalized]: "Finalized", [BlockState.Pruned]: "Pruned", + [BlockState.Unknown]: "Unknown", } diff --git a/src/pages/Explorer/block.state.ts b/src/pages/Explorer/block.state.ts index daadf7d..8ecb84e 100644 --- a/src/pages/Explorer/block.state.ts +++ b/src/pages/Explorer/block.state.ts @@ -2,14 +2,13 @@ import { chopsticksInstance$ } from "@/chopsticks/chopsticks" import { chainClient$, client$ } from "@/state/chains/chain.state" import { SystemEvent } from "@polkadot-api/observable-client" import { state } from "@react-rxjs/core" -import { partitionByKey, toKeySet } from "@react-rxjs/utils" +import { combineKeys, partitionByKey } from "@react-rxjs/utils" import { HexString, PolkadotClient } from "polkadot-api" import { catchError, combineLatest, concat, defer, - distinctUntilChanged, EMPTY, filter, forkJoin, @@ -34,6 +33,7 @@ import { toArray, withLatestFrom, } from "rxjs" +import { BlockInfo as RawBlockInfo } from "polkadot-api" export const finalized$ = client$.pipeState( switchMap((client) => client.finalizedBlock$), @@ -44,6 +44,7 @@ export enum BlockState { Best = "best", Finalized = "finalized", Pruned = "pruned", + Unknown = "unknown", } export interface BlockInfo { hash: string @@ -62,6 +63,22 @@ export interface BlockInfo { diff: Record | null } +const finalizedBlocks$ = state( + client$.pipe( + switchMap((client) => { + const data = new Map() + return client.finalizedBlock$.pipe( + scan((acc, current: { hash: string; number: number }) => { + acc.set(current.hash, current.number) + return acc + }, data), + startWith(data), + ) + }), + ), +) +finalizedBlocks$.subscribe() + export const [blockInfo$, recordedBlocks$] = partitionByKey( client$.pipe(switchMap((client) => client.blocks$)), (v) => v.hash, @@ -101,7 +118,7 @@ export const [blockInfo$, recordedBlocks$] = partitionByKey( return of(null) }), ), - status: getBlockStatus$(client, hash, number), + status: getBlockStatus$(client, hash, number, parent), diff: getBlockDiff$(parent, hash), }), NEVER, @@ -122,20 +139,34 @@ const getUnpinnedBlockInfo$ = (hash: string): Observable => client$.pipe( switchMap((client) => combineLatest({ - header: client.getBlockHeader(hash), + headerAndStatus: from(client.getBlockHeader(hash)).pipe( + mergeMap((header) => + getBlockStatus$( + client, + hash, + header.number, + header.parentHash, + ).pipe( + map((status) => ({ + header, + status, + })), + ), + ), + ), body: client.watchBlockBody(hash), events: client.getUnsafeApi().query.System.Events.getValue({ at: hash, }), }).pipe( - map(({ header, body, events }) => ({ + map(({ headerAndStatus: { header, status }, body, events }) => ({ hash: hash, parent: header.parentHash, number: header.number, body, events, header, - status: BlockState.Finalized, + status, diff: null, })), catchError(() => getUnpinnedBlockInfoFallback$(hash, client)), @@ -171,11 +202,22 @@ const getUnpinnedBlockInfoFallback$ = ( filter((v) => !!v), take(1), catchError(() => EMPTY), + mergeMap((res) => { + const header = res.block.header + const number = Number(header.number) + return getBlockStatus$(client, hash, number, header.parentHash).pipe( + map((status) => ({ + ...res, + number, + status, + })), + ) + }), ) return throughRpc$.pipe( map( - ({ block: { extrinsics, header } }): BlockInfo => ({ + ({ block: { extrinsics, header }, status, number }): BlockInfo => ({ hash, parent: header.parentHash, body: extrinsics, @@ -183,26 +225,31 @@ const getUnpinnedBlockInfoFallback$ = ( header: { digests: header.digest.logs, extrinsicRoot: header.extrinsicsRoot, - number: Number(header.number), + number, parentHash: header.parentHash, stateRoot: header.stateRoot, }, - number: Number(header.number), - status: BlockState.Finalized, + number, + status, diff: null, }), ), ) } +export const inMemoryBlocks$ = state( + combineKeys(recordedBlocks$, (key) => + blockInfo$(key).pipe(startWith(null)), + ).pipe(repeat()), +) +inMemoryBlocks$.subscribe() + export const blockInfoState$ = state( (hash: string) => - recordedBlocks$.pipe( - toKeySet(), - map((blocks) => blocks.has(hash)), - distinctUntilChanged(), - switchMap((exists) => - exists ? blockInfo$(hash) : getUnpinnedBlockInfo$(hash), + inMemoryBlocks$.pipe( + take(1), + switchMap((blocks) => + blocks.has(hash) ? blockInfo$(hash) : getUnpinnedBlockInfo$(hash), ), ), null, @@ -269,27 +316,39 @@ export const blocksByHeight$ = state( ), ) +const getBlockStatus = ( + best: Array, + finBlocks: Map, + number: number, + hash: string, +) => { + const finalized = best[best.length - 1] + if (finalized.number === number) + return finalized.hash === hash ? BlockState.Finalized : BlockState.Pruned + + return finalized.number < number + ? best.some((b) => b.hash === hash) + ? BlockState.Best + : BlockState.Fork + : finBlocks.has(hash) + ? BlockState.Finalized + : BlockState.Unknown +} + const getBlockStatus$ = ( client: PolkadotClient, hash: string, number: number, + parent: string, ): Observable => - merge( - client.finalizedBlock$.pipe( - take(1), - // If the latest finalized is ahead, assume it is finalized (?) - filter((b) => b.number > number), - map(() => BlockState.Finalized), - ), - combineLatest([client.bestBlocks$, client.finalizedBlock$]).pipe( - map(([best, finalized]) => { - if (finalized.hash === hash) return BlockState.Finalized - if (finalized.number === number) return BlockState.Pruned - if (best.some((b) => b.hash === hash)) return BlockState.Best - return BlockState.Fork - }), - ), - ).pipe( + client.bestBlocks$.pipe( + withLatestFrom(finalizedBlocks$), + map(([best, finBlocks]) => { + const status = getBlockStatus(best, finBlocks, number, hash) + if (status === BlockState.Finalized && !finBlocks.has(parent)) + finBlocks.set(parent, number - 1) + return status + }), takeWhile( (v) => v !== BlockState.Finalized && v !== BlockState.Pruned, true,