diff --git a/public/config.json b/public/config.json new file mode 100644 index 0000000..d602ced --- /dev/null +++ b/public/config.json @@ -0,0 +1,3 @@ +{ + "erigonURL": "http://localhost:8545" +} \ No newline at end of file diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index 03011cf..f080b96 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -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(); const location = useLocation(); 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(); 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`; diff --git a/src/App.tsx b/src/App.tsx index 9082f6f..122081c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( - 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> - </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); diff --git a/src/Block.tsx b/src/Block.tsx index 9fd6e25..6f9ab22 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -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> diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index e5712c0..5e6d4ad 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -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`; diff --git a/src/Home.tsx b/src/Home.tsx index 82c3cdb..9cc40fc 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -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> diff --git a/src/Transaction.tsx b/src/Transaction.tsx index a461501..724fb31 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -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]); diff --git a/src/ethersconfig.ts b/src/ethersconfig.ts deleted file mode 100644 index a7a98a1..0000000 --- a/src/ethersconfig.ts +++ /dev/null @@ -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" -); diff --git a/src/search/search.ts b/src/search/search.ts index 3fbcb3e..a78eb81 100644 --- a/src/search/search.ts +++ b/src/search/search.ts @@ -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 ); diff --git a/src/useErigon.ts b/src/useErigon.ts new file mode 100644 index 0000000..bd64364 --- /dev/null +++ b/src/useErigon.ts @@ -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]; +}; diff --git a/src/useLatestBlock.ts b/src/useLatestBlock.ts index fba4011..5ed444b 100644 --- a/src/useLatestBlock.ts +++ b/src/useLatestBlock.ts @@ -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; }; diff --git a/src/useProvider.ts b/src/useProvider.ts new file mode 100644 index 0000000..895c16d --- /dev/null +++ b/src/useProvider.ts @@ -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); diff --git a/src/useReverseCache.ts b/src/useReverseCache.ts index 9451d88..4188904 100644 --- a/src/useReverseCache.ts +++ b/src/useReverseCache.ts @@ -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; };