From 08d8cded314b2d229e50f632e3f0baa6e0fc7a7a Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Mon, 29 Aug 2022 15:52:25 -0300 Subject: [PATCH] Add beacon chain finalized state info --- public/config.json | 1 + src/Home.tsx | 17 ++++++++++++ src/useBeacon.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++ src/useConfig.ts | 1 + 4 files changed, 85 insertions(+) create mode 100644 src/useBeacon.ts diff --git a/public/config.json b/public/config.json index 1db5aa7..5ed5706 100644 --- a/public/config.json +++ b/public/config.json @@ -1,4 +1,5 @@ { "erigonURL": "http://localhost:8545", + "beaconAPI": null, "assetsURLPrefix": "http://localhost:3001" } \ No newline at end of file diff --git a/src/Home.tsx b/src/Home.tsx index 474c3d4..1476674 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -10,6 +10,7 @@ import { RuntimeContext } from "./useRuntime"; import { useLatestBlockHeader } from "./useLatestBlock"; import { blockURL } from "./url"; import { useGenericSearch } from "./search/search"; +import { useFinalizedSlot, useSlotTime } from "./useBeacon"; const CameraScanner = React.lazy(() => import("./search/CameraScanner")); @@ -18,6 +19,8 @@ const Home: React.FC = () => { const [searchRef, handleChange, handleSubmit] = useGenericSearch(); const latestBlock = useLatestBlockHeader(provider); + const beaconData = useFinalizedSlot(); + const slotTime = useSlotTime(beaconData?.data.header.message.slot); const [isScanning, setScanning] = useState(false); document.title = "Home | Otterscan"; @@ -87,6 +90,20 @@ const Home: React.FC = () => { )} + {beaconData && ( +
+
+ Finalized slot: {commify(beaconData.data.header.message.slot)} +
+ {slotTime && } +
+ State root:{" "} + + {beaconData.data.header.message.state_root} + +
+
+ )} ); diff --git a/src/useBeacon.ts b/src/useBeacon.ts new file mode 100644 index 0000000..44854d6 --- /dev/null +++ b/src/useBeacon.ts @@ -0,0 +1,66 @@ +import { useContext } from "react"; +import useSWR from "swr"; +import useSWRImmutable from "swr/immutable"; +import { RuntimeContext } from "./useRuntime"; + +// 12s +const SLOT_TIME = 12; + +// TODO: remove duplication with other json fetchers +const jsonFetcher = async (url: string) => { + try { + const res = await fetch(url); + if (res.ok) { + return res.json(); + } + return null; + } catch (err) { + console.warn(`error while getting beacon data: url=${url} err=${err}`); + return null; + } +}; + +export const useFinalizedSlot = () => { + const { config } = useContext(RuntimeContext); + + // Each slot is 12s, so program SWR to revalidate at this interval + const { data, error } = useSWR( + config?.beaconAPI + ? `${config?.beaconAPI}/eth/v1/beacon/headers/finalized` + : null, + jsonFetcher, + { + revalidateOnFocus: false, + refreshInterval: SLOT_TIME * 1000, + } + ); + + if (error) { + return undefined; + } + return data; +}; + +export const useBeaconGenesis = () => { + const { config } = useContext(RuntimeContext); + + const { data, error } = useSWRImmutable( + config?.beaconAPI ? `${config?.beaconAPI}/eth/v1/beacon/genesis` : null, + jsonFetcher + ); + + if (error) { + return undefined; + } + return data; +}; + +export const useSlotTime = (slot: number | undefined): number | undefined => { + const genesis = useBeaconGenesis(); + if (slot === undefined || genesis === undefined) { + return undefined; + } + + const rawDate = genesis.data.genesis_time; + return parseInt(rawDate) + slot * SLOT_TIME; +}; diff --git a/src/useConfig.ts b/src/useConfig.ts index 3bb4858..e6faa1a 100644 --- a/src/useConfig.ts +++ b/src/useConfig.ts @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; export type OtterscanConfig = { erigonURL?: string; + beaconAPI?: string; assetsURLPrefix?: string; };