Merge branch 'feature/erigon-health-check' into develop
This commit is contained in:
commit
931629d16e
69
src/App.tsx
69
src/App.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
@ -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);
|
71
src/Home.tsx
71
src/Home.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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];
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue