add events table

This commit is contained in:
Victor Oliva
2024-10-31 17:20:58 +01:00
parent 7e8ec16790
commit c34970ea0f
4 changed files with 246 additions and 49 deletions
+36 -28
View File
@@ -1,16 +1,16 @@
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
import { CopyText } from "@/components/Copy"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
import { combineLatest, debounceTime, map, switchMap } from "rxjs"
import { twMerge } from "tailwind-merge"
import { BlockInfo, blocksByHeight$, chainHead$ } from "./block.state"
import { CopyText } from "@/components/Copy"
import {
BlockInfo,
blocksByHeight$,
chainHead$,
finalized$,
} from "./block.state"
const best$ = chainHead$.pipeState(switchMap((chainHead) => chainHead.best$))
const finalized$ = chainHead$.pipeState(
switchMap((chainHead) => chainHead.finalized$),
map((block) => block.number),
withDefault(null),
)
interface PositionedBlock {
block: BlockInfo
@@ -18,6 +18,9 @@ interface PositionedBlock {
branched: number | null
branches: number[]
}
// 1 hour in polkadot
export const MAX_LENGTH = 3600 / 6
const blockTable$ = state(
combineLatest([blocksByHeight$, best$]).pipe(
debounceTime(0),
@@ -34,7 +37,11 @@ const blockTable$ = state(
}
}
}
for (let height = best.number; blocks[height]; height--) {
for (
let height = best.number;
blocks[height] && result.length < MAX_LENGTH;
height--
) {
const competingBlocks = [...blocks[height].values()]
if (competingBlocks.length > 1) {
if (height === best.number) {
@@ -100,23 +107,18 @@ export const BlockTable = () => {
if (!finalized) return null
return (
<div>
<h2 className="font-bold">Recent Blocks</h2>
<table className="border-collapse">
<thead>
<tr>
<th>Number</th>
<th>Forks</th>
<th>Hash</th>
</tr>
</thead>
<div className="w-full p-2 border border-polkadot-800 rounded">
<h2 className="font-bold p-2 border-b border-slate-400 mb-2">
Recent Blocks
</h2>
<table className="border-collapse m-auto">
<tbody>
{rows.map((row, i) => (
<tr
key={row.block.hash}
className={twMerge(
row.block.number <= finalized ? "bg-polkadot-900" : "",
row.block.number === finalized &&
row.block.number <= finalized.number ? "bg-polkadot-900" : "",
row.block.number === finalized.number &&
row.position === 0 &&
"border-t",
)}
@@ -126,9 +128,15 @@ export const BlockTable = () => {
rowSpan={numberSpan(i)}
className={twMerge(
"px-2",
numberSpan(i) > 1 && "border-y border-slate-500",
row.block.number === finalized && "border-t-white",
row.block.number === finalized + 1 && "border-b-white",
numberSpan(i) > 1
? twMerge(
i > 0 ? "border-y" : "border-b",
"border-slate-500",
)
: null,
row.block.number === finalized.number && "border-t-white",
row.block.number === finalized.number + 1 &&
"border-b-white",
)}
>
{row.block.number.toLocaleString()}
@@ -137,14 +145,14 @@ export const BlockTable = () => {
<td className="p-0">
<ForkRenderer row={row} />
</td>
<td className="max-w-xs">
<td className="max-w-xs w-full">
<div className="flex gap-1">
<div
className={twMerge(
"overflow-hidden text-ellipsis whitespace-nowrap tabular-nums text-sm",
"overflow-hidden text-ellipsis whitespace-nowrap font-mono text-sm",
row.position === 0
? ""
: row.block.number > finalized
: row.block.number > finalized.number
? "opacity-80"
: "opacity-50",
)}
@@ -163,7 +171,7 @@ export const BlockTable = () => {
}
const CELL_WIDTH = 20
const CELL_HEIGHT = 36
const CELL_HEIGHT = 40
const CIRCLE_R = 5
const ForkRenderer: FC<{ row: PositionedBlock }> = ({ row }) => {
const totalCells = Math.max(row.position, ...row.branches) + 1
+113
View File
@@ -0,0 +1,113 @@
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { filter, map, takeWhile } from "rxjs"
import { twMerge } from "tailwind-merge"
import {
blockInfo$,
BlockState,
finalized$,
recordedBlocks$,
} from "./block.state"
import { MAX_LENGTH } from "./BlockTable"
export const Events = () => {
const events = useStateObservable(recentEvents$)
const finalized = useStateObservable(finalized$)
const firstFinalized = finalized
? events.find((evt) => evt.number <= finalized?.number)
: events[0]
return (
<div className="w-full p-2 border border-polkadot-800 rounded">
<h2 className="font-bold p-2 border-b border-slate-400 mb-2">
Recent Events
</h2>
<table className="w-full">
<tbody>
{events.map((evt) => (
<tr
key={`${evt.hash}-${evt.index}`}
className={twMerge(
evt === firstFinalized ? "border-t border-white" : "",
finalized && evt.number <= finalized.number
? "bg-polkadot-900"
: "",
)}
>
<td className="p-2 w-full">{`${evt.event.type}.${evt.event.value.type}`}</td>
<td className="p-2 whitespace-nowrap">{`${evt.number}-${evt.extrinsicNumber}`}</td>
</tr>
))}
</tbody>
</table>
{events.length === 0 ? (
<div className="text-slate-400">(No events yet)</div>
) : null}
</div>
)
}
const blackList = new Set([
"System.*",
"Balances.Deposit",
"Treasury.Deposit",
"ParaInclusion.*",
"Balances.Withdraw",
"Balances.Endowed",
"Balances.Locked",
"TransactionPayment.TransactionFeePaid",
"Staking.Rewarded",
])
const recentEvents$ = state(
combineKeys(recordedBlocks$, (key) =>
blockInfo$(key).pipe(
map((block) => ({
status: block.status,
hash: block.hash,
number: block.number,
events: block.events
?.filter((evt) => evt.phase.type === "ApplyExtrinsic")
.filter(
(evt) =>
!blackList.has(`${evt.event.type}.*`) &&
!blackList.has(`${evt.event.type}.${evt.event.value.type}`),
)
.map((evt) => ({
event: evt.event,
extrinsicNumber: (evt.phase as any).value as number,
})),
})),
takeWhile(
(r) =>
(r.status !== BlockState.Finalized &&
r.status !== BlockState.Pruned) ||
r.events == null,
true,
),
filter((result) => Boolean(result.events?.length)),
),
).pipe(
map((events) =>
[...events.values()]
.reverse()
.filter(
(evt) =>
evt.status === BlockState.Best ||
evt.status === BlockState.Finalized,
)
.flatMap(({ status, hash, number, events }) =>
events!.map(({ event, extrinsicNumber }, index) => ({
status,
hash,
number,
event,
extrinsicNumber,
index,
})),
)
.slice(0, MAX_LENGTH),
),
),
[],
)
+5 -1
View File
@@ -2,6 +2,7 @@ import { withSubscribe } from "@/components/withSuspense"
import { blocksByHeight$ } from "./block.state"
import { BlockTable } from "./BlockTable"
import { Summary } from "./Summary"
import { Events } from "./Events"
const explorer$ = blocksByHeight$
@@ -9,7 +10,10 @@ export const Explorer = withSubscribe(
() => (
<div className="overflow-auto">
<Summary />
<BlockTable />
<div className="flex gap-2 items-start flex-wrap lg:flex-nowrap">
<BlockTable />
<Events />
</div>
</div>
),
{
+92 -20
View File
@@ -1,13 +1,18 @@
import { chainClient$ } from "@/chain.state"
import { SystemEvent } from "@polkadot-api/observable-client"
import {
ChainHead$,
PinnedBlocks,
SystemEvent,
} from "@polkadot-api/observable-client"
import { FollowEventWithRuntime } from "@polkadot-api/substrate-client"
import { state } from "@react-rxjs/core"
import { state, withDefault } from "@react-rxjs/core"
import { partitionByKey } from "@react-rxjs/utils"
import {
catchError,
combineLatest,
concat,
concatMap,
filter,
finalize,
forkJoin,
map,
@@ -21,6 +26,7 @@ import {
switchMap,
take,
takeUntil,
takeWhile,
withLatestFrom,
} from "rxjs"
@@ -33,6 +39,17 @@ export const chainHead$ = state(
),
)
export const finalized$ = chainHead$.pipeState(
switchMap((chainHead) => chainHead.finalized$),
withDefault(null),
)
export enum BlockState {
Fork = "fork",
Best = "best",
Finalized = "finalized",
Pruned = "pruned",
}
export interface BlockInfo {
hash: string
parent: string
@@ -40,8 +57,9 @@ export interface BlockInfo {
body: string[] | null
events: SystemEvent[] | null
header: unknown | null
status: BlockState
}
const [blockInfo$, recordedBlocks$] = partitionByKey(
export const [blockInfo$, recordedBlocks$] = partitionByKey(
chainHead$.pipe(
switchMap((chainHead) =>
chainHead.follow$.pipe(
@@ -101,23 +119,27 @@ const [blockInfo$, recordedBlocks$] = partitionByKey(
withLatestFrom(chainHead$),
switchMap(
([{ hash, parent, number }, chainHead]): Observable<BlockInfo> =>
combineLatest({
hash: of(hash),
parent: of(parent),
number: of(number),
body: chainHead.body$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
events: chainHead.eventsAt$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
header: chainHead.header$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
}),
concat(
combineLatest({
hash: of(hash),
parent: of(parent),
number: of(number),
body: chainHead.body$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
events: chainHead.eventsAt$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
header: chainHead.header$(hash).pipe(
startWith(null),
catchError(() => of(null)),
),
status: getBlockStatus$(chainHead, hash, number),
}),
NEVER,
),
),
// Reset when chainHead is changed
takeUntil(chainHead$.pipe(skip(1))),
@@ -178,3 +200,53 @@ function withInitializedNumber() {
}),
)
}
const getBlockStatus$ = (
chainHead: ChainHead$,
hash: string,
number: number,
): Observable<BlockState> =>
chainHead.pinnedBlocks$.pipe(
take(1),
switchMap((pinnedBlocks) => {
const block = (hash: string) => pinnedBlocks.blocks.get(hash)
const blockNum = (hash: string) => block(hash)?.number
const finalized = blockNum(pinnedBlocks.finalized)!
if (number <= finalized) {
// assume we are in the list of finalized blocks on `initialized`
return of(BlockState.Finalized)
}
const getInitialState = (pinnedBlocks: PinnedBlocks) => {
let bestBranch = pinnedBlocks.best
while (blockNum(bestBranch)! > finalized && hash !== bestBranch) {
bestBranch = block(bestBranch)!.parent
}
return block(bestBranch)?.hash === hash
? BlockState.Best
: BlockState.Fork
}
const initialState = getInitialState(pinnedBlocks)
return chainHead.follow$.pipe(
concatMap((evt) => {
switch (evt.type) {
case "bestBlockChanged":
return chainHead.pinnedBlocks$.pipe(take(1), map(getInitialState))
case "finalized":
if (evt.finalizedBlockHashes.includes(hash))
return of(BlockState.Finalized)
if (evt.prunedBlockHashes.includes(hash))
return of(BlockState.Pruned)
}
return of(null)
}),
filter((v) => v !== null),
takeWhile(
(v) => v !== BlockState.Finalized && v !== BlockState.Pruned,
true,
),
startWith(initialState),
)
}),
)