add events table
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
[],
|
||||
)
|
||||
@@ -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>
|
||||
),
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user