Parameterize erigon node url

This commit is contained in:
Willian Mitsuda 2021-07-08 16:02:42 -03:00
parent 3e58030a43
commit da16ce8ad6
13 changed files with 216 additions and 86 deletions

3
public/config.json Normal file
View File

@ -0,0 +1,3 @@
{
"erigonURL": "http://localhost:8545"
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect, useMemo, useContext } from "react";
import { useParams, useLocation, useHistory } from "react-router-dom";
import { ethers } from "ethers";
import queryString from "query-string";
@ -12,9 +12,9 @@ import ResultHeader from "./search/ResultHeader";
import PendingResults from "./search/PendingResults";
import TransactionItem from "./search/TransactionItem";
import { SearchController } from "./search/search";
import { ProviderContext } from "./useProvider";
import { useENSCache } from "./useReverseCache";
import { useFeeToggler } from "./search/useFeeToggler";
import { provider } from "./ethersconfig";
type BlockParams = {
addressOrName: string;
@ -26,6 +26,7 @@ type PageParams = {
};
const AddressTransactions: React.FC = () => {
const provider = useContext(ProviderContext);
const params = useParams<BlockParams>();
const location = useLocation<PageParams>();
const history = useHistory();
@ -59,6 +60,9 @@ const AddressTransactions: React.FC = () => {
return;
}
if (!provider) {
return;
}
const resolveName = async () => {
const resolvedAddress = await provider.resolveName(params.addressOrName);
if (resolvedAddress !== null) {
@ -72,20 +76,30 @@ const AddressTransactions: React.FC = () => {
}
};
resolveName();
}, [params.addressOrName, history, params.direction, location.search]);
}, [
provider,
params.addressOrName,
history,
params.direction,
location.search,
]);
const [controller, setController] = useState<SearchController>();
useEffect(() => {
if (!checksummedAddress) {
if (!provider || !checksummedAddress) {
return;
}
const readFirstPage = async () => {
const _controller = await SearchController.firstPage(checksummedAddress);
const _controller = await SearchController.firstPage(
provider,
checksummedAddress
);
setController(_controller);
};
const readMiddlePage = async (next: boolean) => {
const _controller = await SearchController.middlePage(
provider,
checksummedAddress,
hash!,
next
@ -93,15 +107,18 @@ const AddressTransactions: React.FC = () => {
setController(_controller);
};
const readLastPage = async () => {
const _controller = await SearchController.lastPage(checksummedAddress);
const _controller = await SearchController.lastPage(
provider,
checksummedAddress
);
setController(_controller);
};
const prevPage = async () => {
const _controller = await controller!.prevPage(hash!);
const _controller = await controller!.prevPage(provider, hash!);
setController(_controller);
};
const nextPage = async () => {
const _controller = await controller!.nextPage(hash!);
const _controller = await controller!.nextPage(provider, hash!);
setController(_controller);
};
@ -127,10 +144,10 @@ const AddressTransactions: React.FC = () => {
readLastPage();
}
}
}, [checksummedAddress, params.direction, hash, controller]);
}, [provider, checksummedAddress, params.direction, hash, controller]);
const page = useMemo(() => controller?.getPage(), [controller]);
const reverseCache = useENSCache(page);
const reverseCache = useENSCache(provider, page);
document.title = `Address ${params.addressOrName} | Otterscan`;

View File

@ -3,40 +3,47 @@ import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./Home";
import Search from "./Search";
import Title from "./Title";
import { useProvider, ProviderContext } from "./useProvider";
const Block = React.lazy(() => import("./Block"));
const BlockTransactions = React.lazy(() => import("./BlockTransactions"));
const AddressTransactions = React.lazy(() => import("./AddressTransactions"));
const Transaction = React.lazy(() => import("./Transaction"));
const App = () => (
<Suspense fallback={<>LOADING</>}>
<Router>
<Switch>
<Route path="/" exact>
<Home />
</Route>
<Route path="/search" exact>
<Search />
</Route>
<Route>
<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>
</Route>
</Switch>
</Router>
</Suspense>
);
const App = () => {
const provider = useProvider();
return (
<Suspense fallback={<>LOADING</>}>
<ProviderContext.Provider value={provider}>
<Router>
<Switch>
<Route path="/" exact>
<Home />
</Route>
<Route path="/search" exact>
<Search />
</Route>
<Route>
<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>
</Route>
</Switch>
</Router>
</ProviderContext.Provider>
</Suspense>
);
};
export default React.memo(App);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useState, useMemo, useContext } from "react";
import { useParams, NavLink } from "react-router-dom";
import { ethers, BigNumber } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,7 +6,6 @@ import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { provider } from "./ethersconfig";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
@ -17,6 +16,7 @@ import BlockLink from "./components/BlockLink";
import AddressOrENSName from "./components/AddressOrENSName";
import TransactionValue from "./components/TransactionValue";
import HexValue from "./components/HexValue";
import { ProviderContext } from "./useProvider";
import { useLatestBlockNumber } from "./useLatestBlock";
type BlockParams = {
@ -34,10 +34,15 @@ interface ExtendedBlock extends ethers.providers.Block {
}
const Block: React.FC = () => {
const provider = useContext(ProviderContext);
const params = useParams<BlockParams>();
const [block, setBlock] = useState<ExtendedBlock>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
let blockPromise: Promise<any>;
if (ethers.utils.isHexString(params.blockNumberOrHash, 32)) {
@ -80,7 +85,7 @@ const Block: React.FC = () => {
setBlock(extBlock);
};
readBlock();
}, [params.blockNumberOrHash]);
}, [provider, params.blockNumberOrHash]);
useEffect(() => {
if (block) {
@ -97,7 +102,7 @@ const Block: React.FC = () => {
}
}, [block]);
const latestBlockNumber = useLatestBlockNumber();
const latestBlockNumber = useLatestBlockNumber(provider);
return (
<StandardFrame>

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useState, useMemo, useContext } from "react";
import { useParams, useLocation } from "react-router";
import { ethers } from "ethers";
import queryString from "query-string";
import { provider } from "./ethersconfig";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
@ -14,6 +13,7 @@ import BlockLink from "./components/BlockLink";
import { ProcessedTransaction } from "./types";
import { PAGE_SIZE } from "./params";
import { useFeeToggler } from "./search/useFeeToggler";
import { ProviderContext } from "./useProvider";
import { useENSCache } from "./useReverseCache";
type BlockParams = {
@ -25,6 +25,7 @@ type PageParams = {
};
const BlockTransactions: React.FC = () => {
const provider = useContext(ProviderContext);
const params = useParams<BlockParams>();
const location = useLocation<PageParams>();
const qs = queryString.parse(location.search);
@ -42,6 +43,10 @@ const BlockTransactions: React.FC = () => {
const [txs, setTxs] = useState<ProcessedTransaction[]>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const [_block, _receipts] = await Promise.all([
provider.getBlockWithTransactions(blockNumber.toNumber()),
@ -94,7 +99,7 @@ const BlockTransactions: React.FC = () => {
setTxs(processedResponses);
};
readBlock();
}, [blockNumber]);
}, [provider, blockNumber]);
const page = useMemo(() => {
if (!txs) {
@ -105,7 +110,7 @@ const BlockTransactions: React.FC = () => {
}, [txs, pageNumber]);
const total = useMemo(() => txs?.length ?? 0, [txs]);
const reverseCache = useENSCache(page);
const reverseCache = useENSCache(provider, page);
document.title = `Block #${blockNumber} Txns | Otterscan`;

View File

@ -1,12 +1,13 @@
import React, { useState } from "react";
import React, { useState, useContext } from "react";
import { NavLink, useHistory } from "react-router-dom";
import { ethers } from "ethers";
import Logo from "./Logo";
import Timestamp from "./components/Timestamp";
import { ProviderContext } from "./useProvider";
import { useLatestBlock } from "./useLatestBlock";
import { ERIGON_NODE } from "./ethersconfig";
const Home: React.FC = () => {
const provider = useContext(ProviderContext);
const [search, setSearch] = useState<string>();
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const history = useHistory();
@ -25,7 +26,7 @@ const Home: React.FC = () => {
history.push(`/search?q=${search}`);
};
const latestBlock = useLatestBlock();
const latestBlock = useLatestBlock(provider);
document.title = "Home | Otterscan";
@ -65,7 +66,11 @@ const Home: React.FC = () => {
</NavLink>
)}
<span className="mx-auto mt-5 text-xs text-gray-500">
Using Erigon node at {ERIGON_NODE}
{provider ? (
<>Using Erigon node at {provider.connection.url}</>
) : (
<>Waiting for the provider...</>
)}
</span>
</form>
</div>

View File

@ -1,4 +1,10 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import React, {
useState,
useEffect,
useCallback,
useMemo,
useContext,
} from "react";
import { Route, Switch, useParams } from "react-router-dom";
import { BigNumber, ethers } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,7 +12,6 @@ import {
faCheckCircle,
faTimesCircle,
} from "@fortawesome/free-solid-svg-icons";
import { provider } from "./ethersconfig";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import Tab from "./components/Tab";
@ -22,6 +27,7 @@ import FormattedBalance from "./components/FormattedBalance";
import TokenTransferItem from "./TokenTransferItem";
import erc20 from "./erc20.json";
import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types";
import { ProviderContext } from "./useProvider";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
@ -31,11 +37,16 @@ type TransactionParams = {
};
const Transaction: React.FC = () => {
const provider = useContext(ProviderContext);
const params = useParams<TransactionParams>();
const { txhash } = params;
const [txData, setTxData] = useState<TransactionData>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const [_response, _receipt] = await Promise.all([
provider.getTransaction(txhash),
@ -109,7 +120,7 @@ const Transaction: React.FC = () => {
});
};
readBlock();
}, [txhash]);
}, [provider, txhash]);
const [transfers, setTransfers] = useState<Transfer[]>();
const sendsEthToMiner = useMemo(() => {
@ -126,7 +137,7 @@ const Transaction: React.FC = () => {
}, [txData, transfers]);
const traceTransfersUsingOtsTrace = useCallback(async () => {
if (!txData) {
if (!provider || !txData) {
return;
}
@ -143,7 +154,7 @@ const Transaction: React.FC = () => {
}
setTransfers(_transfers);
}, [txData]);
}, [provider, txData]);
useEffect(() => {
traceTransfersUsingOtsTrace();
}, [traceTransfersUsingOtsTrace]);

View File

@ -1,9 +0,0 @@
import { ethers } from "ethers";
export const ERIGON_NODE =
process.env.REACT_APP_ERIGON_URL || "http://127.0.0.1:8545";
export const provider = new ethers.providers.JsonRpcProvider(
ERIGON_NODE,
"mainnet"
);

View File

@ -1,5 +1,4 @@
import { ethers } from "ethers";
import { provider } from "../ethersconfig";
import { PAGE_SIZE } from "../params";
import { ProcessedTransaction, TransactionChunk } from "../types";
@ -27,7 +26,10 @@ export class SearchController {
}
}
private static rawToProcessed = (_rawRes: any) => {
private static rawToProcessed = (
provider: ethers.providers.JsonRpcProvider,
_rawRes: any
) => {
const _res: ethers.providers.TransactionResponse[] = _rawRes.txs.map(
(t: any) => provider.formatter.transactionResponse(t)
);
@ -56,6 +58,7 @@ export class SearchController {
};
private static async readBackPage(
provider: ethers.providers.JsonRpcProvider,
address: string,
baseBlock: number
): Promise<TransactionChunk> {
@ -64,10 +67,11 @@ export class SearchController {
baseBlock,
PAGE_SIZE,
]);
return this.rawToProcessed(_rawRes);
return this.rawToProcessed(provider, _rawRes);
}
private static async readForwardPage(
provider: ethers.providers.JsonRpcProvider,
address: string,
baseBlock: number
): Promise<TransactionChunk> {
@ -76,11 +80,14 @@ export class SearchController {
baseBlock,
PAGE_SIZE,
]);
return this.rawToProcessed(_rawRes);
return this.rawToProcessed(provider, _rawRes);
}
static async firstPage(address: string): Promise<SearchController> {
const newTxs = await SearchController.readBackPage(address, 0);
static async firstPage(
provider: ethers.providers.JsonRpcProvider,
address: string
): Promise<SearchController> {
const newTxs = await SearchController.readBackPage(provider, address, 0);
return new SearchController(
address,
newTxs.txs,
@ -91,14 +98,19 @@ export class SearchController {
}
static async middlePage(
provider: ethers.providers.JsonRpcProvider,
address: string,
hash: string,
next: boolean
): Promise<SearchController> {
const tx = await provider.getTransaction(hash);
const newTxs = next
? await SearchController.readBackPage(address, tx.blockNumber!)
: await SearchController.readForwardPage(address, tx.blockNumber!);
? await SearchController.readBackPage(provider, address, tx.blockNumber!)
: await SearchController.readForwardPage(
provider,
address,
tx.blockNumber!
);
return new SearchController(
address,
newTxs.txs,
@ -108,8 +120,11 @@ export class SearchController {
);
}
static async lastPage(address: string): Promise<SearchController> {
const newTxs = await SearchController.readForwardPage(address, 0);
static async lastPage(
provider: ethers.providers.JsonRpcProvider,
address: string
): Promise<SearchController> {
const newTxs = await SearchController.readForwardPage(provider, address, 0);
return new SearchController(
address,
newTxs.txs,
@ -123,7 +138,10 @@ export class SearchController {
return this.txs.slice(this.pageStart, this.pageEnd);
}
async prevPage(hash: string): Promise<SearchController> {
async prevPage(
provider: ethers.providers.JsonRpcProvider,
hash: string
): Promise<SearchController> {
// Already on this page
if (this.txs[this.pageEnd - 1].hash === hash) {
return this;
@ -133,6 +151,7 @@ export class SearchController {
const overflowPage = this.txs.slice(0, this.pageStart);
const baseBlock = this.txs[0].blockNumber;
const prevPage = await SearchController.readForwardPage(
provider,
this.address,
baseBlock
);
@ -148,7 +167,10 @@ export class SearchController {
return this;
}
async nextPage(hash: string): Promise<SearchController> {
async nextPage(
provider: ethers.providers.JsonRpcProvider,
hash: string
): Promise<SearchController> {
// Already on this page
if (this.txs[this.pageStart].hash === hash) {
return this;
@ -158,6 +180,7 @@ export class SearchController {
const overflowPage = this.txs.slice(this.pageEnd);
const baseBlock = this.txs[this.txs.length - 1].blockNumber;
const nextPage = await SearchController.readBackPage(
provider,
this.address,
baseBlock
);

25
src/useErigon.ts Normal file
View File

@ -0,0 +1,25 @@
import { useState, useEffect } from "react";
export type OtterscanConfig = {
erigonURL: string;
};
export const useErigon = (): [boolean?, OtterscanConfig?] => {
const [configOK, setConfigOK] = useState<boolean>();
const [config, setConfig] = useState<OtterscanConfig>();
useEffect(() => {
const readConfig = async () => {
const res = await fetch("/config.json");
if (res.ok) {
const _config: OtterscanConfig = await res.json();
setConfig(_config);
setConfigOK(res.ok);
}
};
readConfig();
}, []);
return [configOK, config];
};

View File

@ -1,11 +1,14 @@
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import { provider } from "./ethersconfig";
export const useLatestBlock = () => {
export const useLatestBlock = (provider?: ethers.providers.JsonRpcProvider) => {
const [latestBlock, setLatestBlock] = useState<ethers.providers.Block>();
useEffect(() => {
if (!provider) {
return;
}
const readLatestBlock = async () => {
const blockNum = await provider.getBlockNumber();
const _raw = await provider.send("erigon_getHeaderByNumber", [blockNum]);
@ -26,15 +29,21 @@ export const useLatestBlock = () => {
return () => {
provider.removeListener("block", listener);
};
}, []);
}, [provider]);
return latestBlock;
};
export const useLatestBlockNumber = () => {
export const useLatestBlockNumber = (
provider?: ethers.providers.JsonRpcProvider
) => {
const [latestBlock, setLatestBlock] = useState<number>();
useEffect(() => {
if (!provider) {
return;
}
const readLatestBlock = async () => {
const blockNum = await provider.getBlockNumber();
setLatestBlock(blockNum);
@ -49,7 +58,7 @@ export const useLatestBlockNumber = () => {
return () => {
provider.removeListener("block", listener);
};
}, []);
}, [provider]);
return latestBlock;
};

26
src/useProvider.ts Normal file
View File

@ -0,0 +1,26 @@
import React from "react";
import { ethers } from "ethers";
import { useErigon } from "./useErigon";
export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545";
export const useProvider = (): ethers.providers.JsonRpcProvider | undefined => {
const [configOK, config] = useErigon();
if (!configOK) {
return undefined;
}
let erigonURL = config?.erigonURL;
if (erigonURL === "") {
console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`);
erigonURL = DEFAULT_ERIGON_URL;
} else {
console.log(`Using configured erigon URL: ${erigonURL}`);
}
return new ethers.providers.JsonRpcProvider(erigonURL, "mainnet");
};
export const ProviderContext = React.createContext<
ethers.providers.JsonRpcProvider | undefined
>(undefined);

View File

@ -1,12 +1,15 @@
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import { ENSReverseCache, ProcessedTransaction } from "./types";
import { provider } from "./ethersconfig";
export const useENSCache = (page?: ProcessedTransaction[]) => {
export const useENSCache = (
provider?: ethers.providers.JsonRpcProvider,
page?: ProcessedTransaction[]
) => {
const [reverseCache, setReverseCache] = useState<ENSReverseCache>();
useEffect(() => {
if (!page) {
if (!provider || !page) {
return;
}
@ -38,7 +41,7 @@ export const useENSCache = (page?: ProcessedTransaction[]) => {
setReverseCache(cache);
};
reverseResolve();
}, [page]);
}, [provider, page]);
return reverseCache;
};