Merge branch 'feature/erigon-health-check' into develop

This commit is contained in:
Willian Mitsuda 2021-07-12 17:12:30 -03:00
commit 931629d16e
8 changed files with 297 additions and 88 deletions

View File

@ -3,6 +3,9 @@ import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./Home"; import Home from "./Home";
import Search from "./Search"; import Search from "./Search";
import Title from "./Title"; import Title from "./Title";
import ConnectionErrorPanel from "./ConnectionErrorPanel";
import Footer from "./Footer";
import { ConnectionStatus } from "./types";
import { RuntimeContext, useRuntime } from "./useRuntime"; import { RuntimeContext, useRuntime } from "./useRuntime";
const Block = React.lazy(() => import("./Block")); const Block = React.lazy(() => import("./Block"));
@ -15,33 +18,45 @@ const App = () => {
return ( return (
<Suspense fallback={<>LOADING</>}> <Suspense fallback={<>LOADING</>}>
<RuntimeContext.Provider value={runtime}> {runtime.connStatus !== ConnectionStatus.CONNECTED ? (
<Router> <ConnectionErrorPanel
<Switch> connStatus={runtime.connStatus}
<Route path="/" exact> config={runtime.config}
<Home /> />
</Route> ) : (
<Route path="/search" exact> <RuntimeContext.Provider value={runtime}>
<Search /> <div className="h-screen flex flex-col">
</Route> <Router>
<Route> <Switch>
<Title /> <Route path="/" exact>
<Route path="/block/:blockNumberOrHash" exact> <Home />
<Block /> </Route>
</Route> <Route path="/search" exact>
<Route path="/block/:blockNumber/txs" exact> <Search />
<BlockTransactions /> </Route>
</Route> <Route>
<Route path="/tx/:txhash"> <div className="mb-auto">
<Transaction /> <Title />
</Route> <Route path="/block/:blockNumberOrHash" exact>
<Route path="/address/:addressOrName/:direction?"> <Block />
<AddressTransactions /> </Route>
</Route> <Route path="/block/:blockNumber/txs" exact>
</Route> <BlockTransactions />
</Switch> </Route>
</Router> <Route path="/tx/:txhash">
</RuntimeContext.Provider> <Transaction />
</Route>
<Route path="/address/:addressOrName/:direction?">
<AddressTransactions />
</Route>
</div>
</Route>
</Switch>
</Router>
<Footer />
</div>
</RuntimeContext.Provider>
)}
</Suspense> </Suspense>
); );
}; };

View File

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

18
src/Footer.tsx Normal file
View File

@ -0,0 +1,18 @@
import React, { useContext } from "react";
import { RuntimeContext } from "./useRuntime";
const Footer: React.FC = () => {
const { provider } = useContext(RuntimeContext);
return (
<div className="w-full px-2 py-1 border-t border-t-gray-100 text-xs bg-link-blue text-gray-200 text-center">
{provider ? (
<>Using Erigon node at {provider.connection.url}</>
) : (
<>Waiting for the provider...</>
)}
</div>
);
};
export default React.memo(Footer);

View File

@ -31,49 +31,38 @@ const Home: React.FC = () => {
document.title = "Home | Otterscan"; document.title = "Home | Otterscan";
return ( return (
<div className="h-screen flex m-auto"> <div className="m-auto">
<div className="flex flex-col m-auto"> <Logo />
<Logo /> <form
<form className="flex flex-col"
className="flex flex-col m-auto" onSubmit={handleSubmit}
onSubmit={handleSubmit} autoComplete="off"
autoComplete="off" spellCheck={false}
spellCheck={false} >
<input
className="w-full border rounded focus:outline-none px-2 py-1 mb-10"
type="text"
size={50}
placeholder="Search by address / txn hash / block number / ENS name"
onChange={handleChange}
autoFocus
></input>
<button
className="mx-auto px-3 py-1 mb-10 rounded bg-gray-100 hover:bg-gray-200 focus:outline-none"
type="submit"
> >
<input Search
className="w-full border rounded focus:outline-none px-2 py-1 mb-10" </button>
type="text" {latestBlock && (
size={50} <NavLink
placeholder="Search by address / txn hash / block number / ENS name" className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue"
onChange={handleChange} to={`/block/${latestBlock.number}`}
autoFocus
></input>
<button
className="mx-auto px-3 py-1 mb-10 rounded bg-gray-100 hover:bg-gray-200 focus:outline-none"
type="submit"
> >
Search <div>Latest block: {ethers.utils.commify(latestBlock.number)}</div>
</button> <Timestamp value={latestBlock.timestamp} />
{latestBlock && ( </NavLink>
<NavLink )}
className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue" </form>
to={`/block/${latestBlock.number}`}
>
<div>
Latest block: {ethers.utils.commify(latestBlock.number)}
</div>
<Timestamp value={latestBlock.timestamp} />
</NavLink>
)}
<span className="mx-auto mt-5 text-xs text-gray-500">
{provider ? (
<>Using Erigon node at {provider.connection.url}</>
) : (
<>Waiting for the provider...</>
)}
</span>
</form>
</div>
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const Logo: React.FC = () => ( const Logo: React.FC = () => (
<div className="mx-auto -mt-32 mb-16 text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4"> <div className="mx-auto mb-16 text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4">
<img <img
className="rounded-full" className="rounded-full"
src="/otter.jpg" src="/otter.jpg"

View File

@ -1,5 +1,13 @@
import { ethers, BigNumber } from "ethers"; import { ethers, BigNumber } from "ethers";
export enum ConnectionStatus {
CONNECTING,
NOT_ETH_NODE,
NOT_ERIGON,
NOT_OTTERSCAN_PATCHED,
CONNECTED,
}
export type ProcessedTransaction = { export type ProcessedTransaction = {
blockNumber: number; blockNumber: number;
timestamp: number; timestamp: number;

View File

@ -1,24 +1,79 @@
import { useMemo } from "react"; import { useEffect, useState } from "react";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { ConnectionStatus } from "./types";
export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545"; export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545";
export const useProvider = ( export const useProvider = (
erigonURL?: string erigonURL?: string
): ethers.providers.JsonRpcProvider | undefined => { ): [ConnectionStatus, ethers.providers.JsonRpcProvider | undefined] => {
if (erigonURL === "") { const [connStatus, setConnStatus] = useState<ConnectionStatus>(
console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`); ConnectionStatus.CONNECTING
erigonURL = DEFAULT_ERIGON_URL; );
} else {
console.log(`Using configured erigon URL: ${erigonURL}`); 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( const [provider, setProvider] = useState<
() => new ethers.providers.JsonRpcProvider(erigonURL, "mainnet"), ethers.providers.JsonRpcProvider | undefined
[erigonURL] >();
); useEffect(() => {
if (!erigonURL) { if (erigonURL === undefined) {
return undefined; setConnStatus(ConnectionStatus.NOT_ETH_NODE);
} setProvider(undefined);
return provider; 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];
}; };

View File

@ -2,23 +2,27 @@ import React, { useMemo } from "react";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { OtterscanConfig, useConfig } from "./useConfig"; import { OtterscanConfig, useConfig } from "./useConfig";
import { useProvider } from "./useProvider"; import { useProvider } from "./useProvider";
import { ConnectionStatus } from "./types";
export type OtterscanRuntime = { export type OtterscanRuntime = {
config?: OtterscanConfig; config?: OtterscanConfig;
connStatus: ConnectionStatus;
provider?: ethers.providers.JsonRpcProvider; provider?: ethers.providers.JsonRpcProvider;
}; };
export const useRuntime = (): OtterscanRuntime => { export const useRuntime = (): OtterscanRuntime => {
const [configOK, config] = useConfig(); const [configOK, config] = useConfig();
const provider = useProvider(configOK ? config?.erigonURL : undefined); const [connStatus, provider] = useProvider(
configOK ? config?.erigonURL : undefined
);
const runtime = useMemo( const runtime = useMemo(
(): OtterscanRuntime => ({ config, provider }), (): OtterscanRuntime => ({ config, connStatus, provider }),
[config, provider] [config, connStatus, provider]
); );
if (!configOK) { if (!configOK) {
return {}; return { connStatus: ConnectionStatus.CONNECTING };
} }
return runtime; return runtime;
}; };