From 337eecee68854325ba16349f3f8b31ec5596a134 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Mon, 12 Jul 2021 17:10:16 -0300 Subject: [PATCH] Add detection of wrong Erigon configuration --- src/App.tsx | 73 +++++++++++---------- src/ConnectionErrorPanel.tsx | 120 +++++++++++++++++++++++++++++++++++ src/types.ts | 8 +++ src/useProvider.ts | 85 ++++++++++++++++++++----- src/useRuntime.ts | 12 ++-- 5 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 src/ConnectionErrorPanel.tsx diff --git a/src/App.tsx b/src/App.tsx index 195f2e8..4a4c323 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,9 @@ import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import Home from "./Home"; import Search from "./Search"; import Title from "./Title"; +import ConnectionErrorPanel from "./ConnectionErrorPanel"; import Footer from "./Footer"; +import { ConnectionStatus } from "./types"; import { RuntimeContext, useRuntime } from "./useRuntime"; const Block = React.lazy(() => import("./Block")); @@ -16,38 +18,45 @@ const App = () => { return ( LOADING}> - -
- - - - - - - - - -
- - <Route path="/block/:blockNumberOrHash" exact> - <Block /> - </Route> - <Route path="/block/:blockNumber/txs" exact> - <BlockTransactions /> - </Route> - <Route path="/tx/:txhash"> - <Transaction /> - </Route> - <Route path="/address/:addressOrName/:direction?"> - <AddressTransactions /> - </Route> - </div> - </Route> - </Switch> - </Router> - <Footer /> - </div> - </RuntimeContext.Provider> + {runtime.connStatus !== ConnectionStatus.CONNECTED ? ( + <ConnectionErrorPanel + connStatus={runtime.connStatus} + config={runtime.config} + /> + ) : ( + <RuntimeContext.Provider value={runtime}> + <div className="h-screen flex flex-col"> + <Router> + <Switch> + <Route path="/" exact> + <Home /> + </Route> + <Route path="/search" exact> + <Search /> + </Route> + <Route> + <div className="mb-auto"> + <Title /> + <Route path="/block/:blockNumberOrHash" exact> + <Block /> + </Route> + <Route path="/block/:blockNumber/txs" exact> + <BlockTransactions /> + </Route> + <Route path="/tx/:txhash"> + <Transaction /> + </Route> + <Route path="/address/:addressOrName/:direction?"> + <AddressTransactions /> + </Route> + </div> + </Route> + </Switch> + </Router> + <Footer /> + </div> + </RuntimeContext.Provider> + )} </Suspense> ); }; diff --git a/src/ConnectionErrorPanel.tsx b/src/ConnectionErrorPanel.tsx new file mode 100644 index 0000000..cd6b1f2 --- /dev/null +++ b/src/ConnectionErrorPanel.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faClock, + faCheckCircle, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons"; +import { ConnectionStatus } from "./types"; +import { OtterscanConfig } from "./useConfig"; + +type ConnectionErrorPanelProps = { + connStatus: ConnectionStatus; + config?: OtterscanConfig; +}; + +const ConnectionErrorPanel: React.FC<ConnectionErrorPanelProps> = ({ + connStatus, + config, +}) => { + console.log("PRINT"); + return ( + <div className="h-screen flex flex-col bg-gray-300 font-sans"> + <div className="m-auto h-60 text-gray-700 text-lg min-w-lg max-w-lg"> + <Step type="wait" msg="Trying to connect to Erigon node..." /> + <div className="flex space-x-2"> + <span className="ml-7 text-base">{config?.erigonURL}</span> + </div> + {connStatus === ConnectionStatus.NOT_ETH_NODE && ( + <Step type="error" msg="It does not seem to be an ETH node"> + <p>Make sure your browser can access the URL above.</p> + <p> + If you want to customize the Erigon rpcdaemon endpoint, please + follow these{" "} + <a + href="https://github.com/wmitsuda/otterscan#run-otterscan-docker-image-from-docker-hub" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </p> + </Step> + )} + {connStatus === ConnectionStatus.NOT_ERIGON && ( + <> + <Step type="ok" msg="It is an ETH node" /> + <Step type="error" msg="It does not seem to be an Erigon node"> + Make sure you rpcdaemon with Otterscan patches is up and running + and the <strong>erigon_</strong> namespace is enabled according to + the{" "} + <a + href="https://github.com/wmitsuda/otterscan#install-otterscan-patches-on-top-of-erigon" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </Step> + </> + )} + {connStatus === ConnectionStatus.NOT_OTTERSCAN_PATCHED && ( + <> + <Step type="ok" msg="It is an Erigon node" /> + <Step + type="error" + msg="It does not seem to contain Otterscan patches" + > + Make sure you compiled rpcdaemon with Otterscan patches and + enabled <strong>ots_</strong> namespace according to the{" "} + <a + href="https://github.com/wmitsuda/otterscan#install-otterscan-patches-on-top-of-erigon" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </Step> + </> + )} + </div> + </div> + ); +}; + +type StepProps = { + type: "wait" | "ok" | "error"; + msg: string; +}; + +const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => ( + <> + <div className="flex space-x-2"> + {type === "wait" && ( + <span className="text-gray-600"> + <FontAwesomeIcon icon={faClock} size="1x" /> + </span> + )} + {type === "ok" && ( + <span className="text-green-600"> + <FontAwesomeIcon icon={faCheckCircle} size="1x" /> + </span> + )} + {type === "error" && ( + <span className="text-red-600"> + <FontAwesomeIcon icon={faTimesCircle} size="1x" /> + </span> + )} + <span>{msg}</span> + </div> + {children && <div className="ml-7 mt-4 text-sm">{children}</div>} + </> +)); + +export default React.memo(ConnectionErrorPanel); diff --git a/src/types.ts b/src/types.ts index 36537f8..9ee8d3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,13 @@ import { ethers, BigNumber } from "ethers"; +export enum ConnectionStatus { + CONNECTING, + NOT_ETH_NODE, + NOT_ERIGON, + NOT_OTTERSCAN_PATCHED, + CONNECTED, +} + export type ProcessedTransaction = { blockNumber: number; timestamp: number; diff --git a/src/useProvider.ts b/src/useProvider.ts index cd4024d..b8eed30 100644 --- a/src/useProvider.ts +++ b/src/useProvider.ts @@ -1,24 +1,79 @@ -import { useMemo } from "react"; +import { useEffect, useState } from "react"; import { ethers } from "ethers"; +import { ConnectionStatus } from "./types"; export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545"; export const useProvider = ( erigonURL?: string -): ethers.providers.JsonRpcProvider | undefined => { - if (erigonURL === "") { - console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`); - erigonURL = DEFAULT_ERIGON_URL; - } else { - console.log(`Using configured erigon URL: ${erigonURL}`); +): [ConnectionStatus, ethers.providers.JsonRpcProvider | undefined] => { + const [connStatus, setConnStatus] = useState<ConnectionStatus>( + ConnectionStatus.CONNECTING + ); + + if (erigonURL !== undefined) { + if (erigonURL === "") { + console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`); + erigonURL = DEFAULT_ERIGON_URL; + } else { + console.log(`Using configured erigon URL: ${erigonURL}`); + } } - const provider = useMemo( - () => new ethers.providers.JsonRpcProvider(erigonURL, "mainnet"), - [erigonURL] - ); - if (!erigonURL) { - return undefined; - } - return provider; + const [provider, setProvider] = useState< + ethers.providers.JsonRpcProvider | undefined + >(); + useEffect(() => { + if (erigonURL === undefined) { + setConnStatus(ConnectionStatus.NOT_ETH_NODE); + setProvider(undefined); + return; + } + setConnStatus(ConnectionStatus.CONNECTING); + + const tryToConnect = async () => { + const provider = new ethers.providers.JsonRpcProvider( + erigonURL, + "mainnet" + ); + + // Check if it is at least a regular ETH node + let blockNumber: number = 0; + try { + blockNumber = await provider.getBlockNumber(); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_ETH_NODE); + setProvider(undefined); + return; + } + + // Check if it is an Erigon node by probing a lightweight method + try { + await provider.send("erigon_getHeaderByNumber", [blockNumber]); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_ERIGON); + setProvider(undefined); + return; + } + + // Check if it has Otterscan patches by probing a lightweight method + try { + // TODO: replace by a custom made method that works in all networks + await provider.send("ots_getTransactionTransfers", [ + "0x793e079fbc427cba0857b4e878194ab508f33983f45415e50af3c3c0e662fdf3", + ]); + setConnStatus(ConnectionStatus.CONNECTED); + setProvider(provider); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_OTTERSCAN_PATCHED); + setProvider(undefined); + } + }; + tryToConnect(); + }, [erigonURL]); + + return [connStatus, provider]; }; diff --git a/src/useRuntime.ts b/src/useRuntime.ts index ed2bcc9..77fef72 100644 --- a/src/useRuntime.ts +++ b/src/useRuntime.ts @@ -2,23 +2,27 @@ import React, { useMemo } from "react"; import { ethers } from "ethers"; import { OtterscanConfig, useConfig } from "./useConfig"; import { useProvider } from "./useProvider"; +import { ConnectionStatus } from "./types"; export type OtterscanRuntime = { config?: OtterscanConfig; + connStatus: ConnectionStatus; provider?: ethers.providers.JsonRpcProvider; }; export const useRuntime = (): OtterscanRuntime => { const [configOK, config] = useConfig(); - const provider = useProvider(configOK ? config?.erigonURL : undefined); + const [connStatus, provider] = useProvider( + configOK ? config?.erigonURL : undefined + ); const runtime = useMemo( - (): OtterscanRuntime => ({ config, provider }), - [config, provider] + (): OtterscanRuntime => ({ config, connStatus, provider }), + [config, connStatus, provider] ); if (!configOK) { - return {}; + return { connStatus: ConnectionStatus.CONNECTING }; } return runtime; };