add epoch for relay chains

This commit is contained in:
Carlo Sala
2024-11-09 14:11:13 +07:00
parent b892f50522
commit bea4499d57
4 changed files with 210 additions and 50 deletions

View File

@@ -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

View 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>
)
}

View File

@@ -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>

View 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$))),
)