add epoch for relay chains
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import { chainClient$, chainHead$, runtimeCtx$ } from "@/chain.state"
|
||||
import { chainHead$ } from "@/chain.state"
|
||||
import { CircularProgress } from "@/components/CircularProgress"
|
||||
import { groupBy } from "@/lib/groupBy"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import {
|
||||
animationFrames,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { targetBlockTime$ } from "./blockTime.state"
|
||||
|
||||
const best$ = chainHead$.pipeState(switchMap((chainHead) => chainHead.best$))
|
||||
const bestBlockTime$ = best$.pipeState(
|
||||
@@ -23,52 +21,8 @@ const bestBlockTime$ = best$.pipeState(
|
||||
}),
|
||||
)
|
||||
|
||||
const fromConstants$ = runtimeCtx$.pipe(
|
||||
map(({ dynamicBuilder, lookup }) => {
|
||||
const palletsByName = groupBy(lookup.metadata.pallets, (p) => p.name)
|
||||
const getConstant = (pallet: string, constant: string) =>
|
||||
palletsByName[pallet]?.[0]?.constants.find((ct) => ct.name === constant)
|
||||
const ct =
|
||||
getConstant("Babe", "ExpectedBlockTime") ??
|
||||
getConstant("Aura", "SlotDuration")
|
||||
if (!ct) return null
|
||||
try {
|
||||
const res = dynamicBuilder.buildDefinition(ct.type).dec(ct.value)
|
||||
return Number(res)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
const fromCall$ = runtimeCtx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([{ lookup }, { client }]) => {
|
||||
const getFromApi = (apiName: string, apiMethod: string) => {
|
||||
const hasApi =
|
||||
"apis" in lookup.metadata &&
|
||||
lookup.metadata.apis
|
||||
.find((api) => api.name === apiName)
|
||||
?.methods.find((method) => method.name === apiMethod)
|
||||
|
||||
return hasApi ? client.getUnsafeApi().apis[apiName][apiMethod]() : null
|
||||
}
|
||||
|
||||
return (
|
||||
getFromApi("AuraApi", "slot_duration")?.then((r) => Number(r)) ??
|
||||
getFromApi("BabeApi", "configuration")?.then((r) =>
|
||||
Number(r.slot_duration),
|
||||
) ??
|
||||
of(null)
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const targetTime$ = state(
|
||||
fromConstants$.pipe(switchMap((v) => (v ? of(v) : fromCall$))),
|
||||
)
|
||||
|
||||
const timeProps$ = state(
|
||||
combineLatest([bestBlockTime$, targetTime$]).pipe(
|
||||
combineLatest([bestBlockTime$, targetBlockTime$]).pipe(
|
||||
map(([bestBlockTime, targetTime]) => {
|
||||
const time = (Math.round(bestBlockTime / 100) / 10).toFixed(1)
|
||||
const progress = targetTime ? bestBlockTime / targetTime : null
|
||||
|
||||
138
src/pages/Explorer/EpochTime.tsx
Normal file
138
src/pages/Explorer/EpochTime.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { CircularProgress } from "@/components/CircularProgress"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { targetBlockTime$ } from "./blockTime.state"
|
||||
import { chainClient$, chainHead$, runtimeCtx$ } from "@/chain.state"
|
||||
import { groupBy } from "@/lib/groupBy"
|
||||
|
||||
const bestBlock$ = chainHead$.pipeState(
|
||||
// Only count when increasing height
|
||||
switchMap((chainHead) => chainHead.best$),
|
||||
map((block) => block.number),
|
||||
distinctUntilChanged((prev, current) => prev >= current),
|
||||
)
|
||||
|
||||
const epochFromConstant$ = runtimeCtx$.pipe(
|
||||
map(({ lookup, dynamicBuilder }) => {
|
||||
const palletsByName = groupBy(lookup.metadata.pallets, (p) => p.name)
|
||||
const getConstant = (pallet: string, constant: string) =>
|
||||
palletsByName[pallet]?.[0]?.constants.find((ct) => ct.name === constant)
|
||||
const ct = getConstant("Babe", "EpochDuration")
|
||||
if (!ct) return null
|
||||
try {
|
||||
const res = dynamicBuilder.buildDefinition(ct.type).dec(ct.value)
|
||||
return Number(res)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
const epochFromCall$ = runtimeCtx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([{ lookup }, { client }]) => {
|
||||
const getFromApi = (apiName: string, apiMethod: string) => {
|
||||
const hasApi =
|
||||
"apis" in lookup.metadata &&
|
||||
lookup.metadata.apis
|
||||
.find((api) => api.name === apiName)
|
||||
?.methods.find((method) => method.name === apiMethod)
|
||||
|
||||
return hasApi ? client.getUnsafeApi().apis[apiName][apiMethod]() : null
|
||||
}
|
||||
return (
|
||||
getFromApi("BabeApi", "configuration")?.then((r) =>
|
||||
Number(r.epoch_length),
|
||||
) ?? of(null)
|
||||
)
|
||||
}),
|
||||
)
|
||||
const epochDuration$ = state(
|
||||
epochFromConstant$.pipe(switchMap((v) => (v ? of(v) : epochFromCall$))),
|
||||
)
|
||||
|
||||
const epochStartBlock$ = state(
|
||||
runtimeCtx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([{ lookup }, { client }]) => {
|
||||
const pallet = "Babe"
|
||||
const entry = "EpochStart"
|
||||
const hasEntry = lookup.metadata.pallets
|
||||
.find(({ name }) => name === pallet)
|
||||
?.storage?.items.find(({ name }) => name === entry)
|
||||
return hasEntry
|
||||
? client
|
||||
.getUnsafeApi()
|
||||
.query[pallet][entry].watchValue()
|
||||
.pipe(map(([, duration]) => Number(duration)))
|
||||
: of(null)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const timeProps$ = state(
|
||||
combineLatest([
|
||||
bestBlock$,
|
||||
targetBlockTime$,
|
||||
epochDuration$,
|
||||
epochStartBlock$,
|
||||
]).pipe(
|
||||
map(([bestBlock, targetTime, epochDuration, epochStartBlock]) => {
|
||||
if (
|
||||
targetTime == null ||
|
||||
epochDuration == null ||
|
||||
epochStartBlock == null
|
||||
)
|
||||
return {
|
||||
percent: "0%",
|
||||
time: "0",
|
||||
progress: null,
|
||||
}
|
||||
|
||||
const remaining =
|
||||
((epochDuration + epochStartBlock - bestBlock) * targetTime) / 1000
|
||||
const hours = Math.floor(remaining / 3600)
|
||||
const mins = Math.ceil((remaining % 3600) / 60)
|
||||
const time =
|
||||
remaining < 60
|
||||
? "<1 min"
|
||||
: (hours !== 0 ? `${hours}h` : "") + (mins !== 0 ? `${mins}min` : "")
|
||||
const progress = (bestBlock - epochStartBlock) / epochDuration
|
||||
return {
|
||||
time,
|
||||
percent: `${Math.round(progress * 100)}%`,
|
||||
progress: progress ? Math.round(progress * 100) / 100 : null,
|
||||
}
|
||||
}),
|
||||
distinctUntilChanged((a, b) => a.time === b.time),
|
||||
),
|
||||
{
|
||||
percent: "0%",
|
||||
time: "0",
|
||||
progress: null,
|
||||
},
|
||||
)
|
||||
export const EpochRemainingTime = () => {
|
||||
const { time, progress, percent } = useStateObservable(timeProps$)
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<CircularProgress
|
||||
progress={progress}
|
||||
text={progress != null ? percent : ""}
|
||||
/>
|
||||
{progress != null ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<text>{time}</text>
|
||||
<text>remaining</text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { chainHead$ } from "@/chain.state"
|
||||
import { chainHead$, runtimeCtx$ } from "@/chain.state"
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { map, switchMap } from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { BlockTime } from "./BlockTime"
|
||||
import { EpochRemainingTime } from "./EpochTime"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
|
||||
const finalized$ = chainHead$.pipeState(
|
||||
switchMap((chainHead) => chainHead.finalized$),
|
||||
@@ -13,12 +15,30 @@ const best$ = chainHead$.pipeState(
|
||||
map((v) => v.number.toLocaleString()),
|
||||
)
|
||||
|
||||
// epoch is only available for relay chains
|
||||
const hasEpoch$ = runtimeCtx$.pipeState(
|
||||
map(({ lookup }) =>
|
||||
Boolean(
|
||||
lookup.metadata.pallets
|
||||
.find(({ name }) => name === "Babe")
|
||||
?.storage?.items.some(({ name }) => name === "EpochStart"),
|
||||
),
|
||||
),
|
||||
withDefault(false),
|
||||
)
|
||||
|
||||
export const Summary: FC = () => {
|
||||
const hasEpoch = useStateObservable(hasEpoch$)
|
||||
return (
|
||||
<div className="flex gap-4 items-center py-2">
|
||||
<SummaryItem title="Block Time" className="bg-card/0 border-none">
|
||||
<BlockTime />
|
||||
</SummaryItem>
|
||||
{hasEpoch ? (
|
||||
<SummaryItem title="Epoch" className="bg-card/0 border-none">
|
||||
<EpochRemainingTime />
|
||||
</SummaryItem>
|
||||
) : null}
|
||||
<div className="flex-1" />
|
||||
<SummaryItem title="Finalized">{finalized$}</SummaryItem>
|
||||
<SummaryItem title="Best">{best$}</SummaryItem>
|
||||
|
||||
48
src/pages/Explorer/blockTime.state.ts
Normal file
48
src/pages/Explorer/blockTime.state.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { chainClient$, runtimeCtx$ } from "@/chain.state"
|
||||
import { groupBy } from "@/lib/groupBy"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { map, of, switchMap, withLatestFrom } from "rxjs"
|
||||
|
||||
const fromConstants$ = runtimeCtx$.pipe(
|
||||
map(({ dynamicBuilder, lookup }) => {
|
||||
const palletsByName = groupBy(lookup.metadata.pallets, (p) => p.name)
|
||||
const getConstant = (pallet: string, constant: string) =>
|
||||
palletsByName[pallet]?.[0]?.constants.find((ct) => ct.name === constant)
|
||||
const ct =
|
||||
getConstant("Babe", "ExpectedBlockTime") ??
|
||||
getConstant("Aura", "SlotDuration")
|
||||
if (!ct) return null
|
||||
try {
|
||||
const res = dynamicBuilder.buildDefinition(ct.type).dec(ct.value)
|
||||
return Number(res)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
const fromCall$ = runtimeCtx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
switchMap(([{ lookup }, { client }]) => {
|
||||
const getFromApi = (apiName: string, apiMethod: string) => {
|
||||
const hasApi =
|
||||
"apis" in lookup.metadata &&
|
||||
lookup.metadata.apis
|
||||
.find((api) => api.name === apiName)
|
||||
?.methods.find((method) => method.name === apiMethod)
|
||||
|
||||
return hasApi ? client.getUnsafeApi().apis[apiName][apiMethod]() : null
|
||||
}
|
||||
|
||||
return (
|
||||
getFromApi("AuraApi", "slot_duration")?.then((r) => Number(r)) ??
|
||||
getFromApi("BabeApi", "configuration")?.then((r) =>
|
||||
Number(r.slot_duration),
|
||||
) ??
|
||||
of(null)
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const targetBlockTime$ = state(
|
||||
fromConstants$.pipe(switchMap((v) => (v ? of(v) : fromCall$))),
|
||||
)
|
||||
Reference in New Issue
Block a user