diff --git a/docs/install.md b/docs/install.md index e092834..c3ab6fa 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,43 +6,41 @@ It depends heavily on a working Erigon installation with Otterscan patches appli ## Install Erigon -You will need an Erigon executing node (`erigon`). Also you will need Erigon RPC daemon (`rpcdaemon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing. +You will need an Erigon executing node (`erigon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing. -My personal experience: at the moment of this writing (~block 14,000,000), setting up an archive node takes over 5-6 days and ~1.7 TB of SSD. +My personal experience: at the moment of this writing (~block 15,000,000), setting up an archive node takes over 3-7 days (depending on your hardware) and ~1.6 TB of SSD. -They have weekly stable releases, make sure you are running on of them, not development ones. +They have weekly alpha releases, make sure you are running one of them, not development ones. -## Install Otterscan-patched rpcdaemon +## Install Otterscan-patched erigon We rely on custom JSON-RPC APIs which are not available in a standard ETH node. We keep a separated repository containing an Erigon fork here: https://github.com/wmitsuda/erigon. -Please follow the instructions in the repository `README` and replace the original Erigon `rpcdaemon` with our patched one. +Please follow the instructions in the repository `README` and replace the original Erigon `erigon` with our patched one. -## Enable Otterscan namespace on rpcdaemon +## Enable Otterscan namespace on erigon -When running `rpcdaemon`, make sure to enable the `erigon`, `ots`, `eth` APIs in addition to whatever cli options you are using to start `rpcdaemon`. +When running `erigon`, make sure to enable the `erigon`, `ots`, `eth` APIs in addition to whatever cli options you are using to start `erigon`. `ots` stands for Otterscan and it is the namespace we use for our own custom APIs. ``` -/rpcdaemon --http.api "eth,erigon,ots," --private.api.addr 127.0.0.1:9090 --datadir --http.corsdomain "*" +/erigon --http.api "eth,erigon,ots," --datadir --http.corsdomain "*" ``` -Be sure to include both `--private.api.addr` and `--datadir` parameter so you run it in dual mode, otherwise the performance will be much worse. +Pay attention to the `--http.corsdomain` parameter, CORS is **required** for the browser to call the node directly. -Also pay attention to the `--http.corsdomain` parameter, CORS is **required** for the browser to call the node directly. - -Now you should have an Erigon node with Otterscan JSON-RPC APIs enabled, running in dual mode with CORS enabled. +Now you should have an Erigon node with Otterscan JSON-RPC APIs and CORS enabled. ## Run Otterscan docker image from Docker Hub The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories). ``` -docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan: +docker run --rm -p 5100:80 --name otterscan -d otterscan/otterscan: ``` -This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5000 (see the `-p` docker run parameter). +This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5100 (see the `-p` docker run parameter). To stop Otterscan service, run: @@ -53,11 +51,27 @@ docker stop otterscan By default it assumes your Erigon node is at `http://127.0.0.1:8545`. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`: ``` -docker run --rm -p 5000:80 --name otterscan -d --env ERIGON_URL="" otterscan/otterscan: +docker run --rm -p 5100:80 --name otterscan -d --env ERIGON_URL="" otterscan/otterscan: ``` This is the preferred way to run Otterscan. You can read about other ways [here](./other-ways-to-run-otterscan.md). +## (Optional) Enable integration with beacon chain + +You can optionally enable displaying extra info from the beacon chain by providing the public URL of your beacon node API. + +Enabling the beacon chain API depends on which CL implementation you are using. + +> As an example, for Prysm you need to enable CORS and possibly bind the address to the correct network interface with `--grpc-gateway-host="0.0.0.0" --grpc-gateway-corsdomain='*'` and by default it binds it to the port 3500. + +When starting the Otterscan process via Docker, you need to add an extra env variable called `BEACON_API_URL` pointing to your beacon node API URL. + +Prysm example: + +``` +docker run --rm -p 5100:80 --name otterscan -d --env BEACON_API_URL="" otterscan/otterscan: +``` + ## Validating the installation (all methods) You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button. 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/run-nginx.sh b/run-nginx.sh index 604c8b4..b95bdad 100755 --- a/run-nginx.sh +++ b/run-nginx.sh @@ -1,4 +1,4 @@ #!/bin/sh -PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .), \"assetsURLPrefix\": \"\"}" +PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .), \"beaconAPI\": $(echo $BEACON_API_URL | jq -aR .), \"assetsURLPrefix\": \"\"}" echo $PARAMS > /usr/share/nginx/html/config.json nginx -g "daemon off;" 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; };