Merge branch 'feature/docker-experiments' into develop
This commit is contained in:
commit
8d43f6908c
|
@ -0,0 +1,6 @@
|
|||
.git
|
||||
node_modules
|
||||
4bytes
|
||||
!4bytes/signatures
|
||||
trustwallet
|
||||
!trustwallet/blockchains/ethereum/assets
|
|
@ -0,0 +1,19 @@
|
|||
FROM node:12.22.3-alpine AS builder
|
||||
WORKDIR /otterscan-build
|
||||
COPY ["package.json", "package-lock.json", "/otterscan-build"]
|
||||
RUN npm install
|
||||
COPY ["run-nginx.sh", "tsconfig.json", "craco.config.js", "tailwind.config.js", "/otterscan-build"]
|
||||
COPY ["public", "/otterscan-build/public"]
|
||||
COPY ["src", "/otterscan-build/src"]
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.21.1-alpine
|
||||
RUN apk add jq
|
||||
COPY 4bytes/signatures /usr/share/nginx/html/signatures/
|
||||
COPY trustwallet/blockchains/ethereum/assets /usr/share/nginx/html/assets/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /otterscan-build/build /usr/share/nginx/html/
|
||||
COPY --from=builder /otterscan-build/run-nginx.sh /
|
||||
WORKDIR /
|
||||
|
||||
CMD ["/run-nginx.sh"]
|
69
README.md
69
README.md
|
@ -107,9 +107,35 @@ Also pay attention to the `--http.corsdomain` parameter, CORS is required for th
|
|||
|
||||
Now you should have an Erigon node with Otterscan jsonrpc APIs enabled, running in dual mode with CORS enabled.
|
||||
|
||||
### Clone Otterscan repository and build the project
|
||||
### Run Otterscan docker image from Docker Hub
|
||||
|
||||
Make sure you have a working node 12/npm installation.
|
||||
TODO: publish Otterscan official images as soon as it is validated.
|
||||
|
||||
```
|
||||
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag>
|
||||
```
|
||||
|
||||
This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5000 (see the `-p` docker run parameter).
|
||||
|
||||
To stop Otterscan service, run:
|
||||
|
||||
```
|
||||
docker stop otterscan
|
||||
```
|
||||
|
||||
By default it assumes your Erigon node is at http://127.0.0.1:8545. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`:
|
||||
|
||||
```
|
||||
docker run --rm -p 5000:80 --name otterscan -d --env ERIGON_URL="<your-erigon-node-url>" otterscan/otterscan:<versiontag>
|
||||
```
|
||||
|
||||
### (Alternative 1) Build Otterscan docker image locally and run it
|
||||
|
||||
If you don't want to download from Docker Hub, you can build the docker image from the sources and run it.
|
||||
|
||||
If you just want to build the image locally, there is no need to install the development toolchain, just make sure you have a recent working Docker installation.
|
||||
|
||||
The entire build process will take place inside the docker multi-stage build.
|
||||
|
||||
Clone Otterscan repo and its submodules. Checkout the tag corresponding to your Erigon + Otterscan patches. It uses the same version tag from Erigon + Otterscan repo, i.e., if you built the `v2021.07.01-otterscan`, you should build the `v2021.07.01-otterscan` of Otterscan.
|
||||
|
||||
|
@ -117,19 +143,19 @@ Clone Otterscan repo and its submodules. Checkout the tag corresponding to your
|
|||
git clone --recurse-submodules git@github.com:wmitsuda/otterscan.git
|
||||
cd otterscan
|
||||
git checkout <version-tag-otterscan>
|
||||
npm install
|
||||
npm run build
|
||||
docker build -t otterscan -f Dockerfile .
|
||||
```
|
||||
|
||||
By default, it assumes your Erigon `rpcdaemon` processs is serving requests at http://127.0.0.1:8545. You can customize this URL by specifying the `REACT_APP_ERIGON_URL` environment variable at build time (it needs to be done at build time because the build process generates a static website).
|
||||
This will run the entire build process inside a build container, merge the production build of the React app with the 4bytes and trustwallet assets into the same image format it is published in Docker Hub, but locally under the name `otterscan`.
|
||||
|
||||
To do that, export the variable before running `npm run build`:
|
||||
Then you can start/stop it using the commands:
|
||||
|
||||
```
|
||||
export REACT_APP_ERIGON_URL=<rpcdaemon-url>
|
||||
docker run --rm -p 5000:80 --name otterscan -d otterscan
|
||||
docker stop otterscan
|
||||
```
|
||||
|
||||
### Run it from the source
|
||||
### (Alternative 2) Run a development build from the source
|
||||
|
||||
First, a brief explanation about the app:
|
||||
|
||||
|
@ -141,25 +167,38 @@ First, a brief explanation about the app:
|
|||
|
||||
These instructions are subjected to changes in future for the sake of simplification.
|
||||
|
||||
Open a new terminal and start the 4bytes method decoding service:
|
||||
Make sure you have a working node 12/npm installation.
|
||||
|
||||
By default, it assumes your Erigon `rpcdaemon` processs is serving requests at http://127.0.0.1:8545. You can customize this URL by specifying the `REACT_APP_ERIGON_URL` environment variable at build time (it needs to be done at build time because the build process generates a static website).
|
||||
|
||||
To do that, export the variable before running `npm run build`:
|
||||
|
||||
```
|
||||
npm run serve-4bytes
|
||||
export REACT_APP_ERIGON_URL=<rpcdaemon-url>
|
||||
```
|
||||
|
||||
Open another terminal and start the trustwallet assets service:
|
||||
Start serving 4bytes and trustwallet assets at `localhost:3001` using a dockerized nginx:
|
||||
|
||||
```
|
||||
npm run serve-trustwallet-assets
|
||||
npm run start-assets
|
||||
```
|
||||
|
||||
In another terminal start the Otterscan app:
|
||||
To stop it, run:
|
||||
|
||||
```
|
||||
npm run serve
|
||||
npm run stop-assets
|
||||
```
|
||||
|
||||
Otterscan should now be running at http://localhost:5000/.
|
||||
To run Otterscan development build:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Otterscan should now be running at http://localhost:3000/.
|
||||
|
||||
## Validating the installation (all methods)
|
||||
|
||||
**You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button.**
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
#access_log /var/log/nginx/host.access.log main;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
#
|
||||
# Om nom nom cookies
|
||||
#
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
|
||||
|
||||
#
|
||||
# Custom headers and headers various browsers *should* be OK with but aren't
|
||||
#
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||
|
||||
#
|
||||
# Tell client that this pre-flight info is valid for 20 days
|
||||
#
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
if ($request_method = 'GET') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||
}
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
|
@ -42,9 +42,11 @@
|
|||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject",
|
||||
"serve-4bytes": "serve -p 3001 -C -c serve-4bytes.json",
|
||||
"serve-trustwallet-assets": "serve -p 3002 -C -c serve-trustwallet-assets.json",
|
||||
"serve": "serve -s build"
|
||||
"start-assets": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/signatures:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
|
||||
"stop-assets": "docker stop otterscan-assets",
|
||||
"build-docker": "docker build -t otterscan -f Dockerfile .",
|
||||
"start-docker": "docker run --rm -p 5000:80 --name otterscan -d otterscan",
|
||||
"stop-docker": "docker stop otterscan"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"erigonURL": "http://localhost:8545"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .)}"
|
||||
echo $PARAMS > /usr/share/nginx/html/config.json
|
||||
nginx -g "daemon off;"
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"public": "4bytes/signatures",
|
||||
"headers": [
|
||||
{
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=600"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"directoryListing": false
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"public": "trustwallet/blockchains/ethereum/assets",
|
||||
"headers": [
|
||||
{
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=600"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"directoryListing": false
|
||||
}
|
|
@ -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`;
|
||||
|
||||
|
|
65
src/App.tsx
65
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 = () => (
|
||||
<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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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`;
|
||||
|
||||
|
|
13
src/Home.tsx
13
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>
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -16,7 +16,7 @@ const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
|||
|
||||
// Try to resolve 4bytes name
|
||||
const fourBytes = _name.slice(2);
|
||||
const signatureURL = `http://localhost:3001/${fourBytes}`;
|
||||
const signatureURL = `http://localhost:3001/signatures/${fourBytes}`;
|
||||
fetch(signatureURL)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -15,7 +15,7 @@ const TokenLogo: React.FC<TokenLogoProps> = (props) => (
|
|||
const InternalTokenLogo: React.FC<TokenLogoProps> = ({ address, name }) => {
|
||||
const { src } = useImage({
|
||||
srcList: [
|
||||
`http://localhost:3002/${address}/logo.png`,
|
||||
`http://localhost:3001/assets/${address}/logo.png`,
|
||||
"/eth-diamond-black.png",
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue