Merge branch 'feature/docker-fixes' into develop
This commit is contained in:
commit
c34af4481d
|
@ -111,7 +111,7 @@ Now you should have an Erigon node with Otterscan jsonrpc APIs enabled, running
|
||||||
|
|
||||||
### Run Otterscan docker image from Docker Hub
|
### Run Otterscan docker image from Docker Hub
|
||||||
|
|
||||||
TODO: publish Otterscan official images as soon as it is validated.
|
The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories).
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag>
|
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag>
|
||||||
|
|
46
nginx.conf
46
nginx.conf
|
@ -4,10 +4,10 @@ server {
|
||||||
|
|
||||||
#access_log /var/log/nginx/host.access.log main;
|
#access_log /var/log/nginx/host.access.log main;
|
||||||
|
|
||||||
location / {
|
location /signatures {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
expires 30d;
|
||||||
|
|
||||||
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
|
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header 'Access-Control-Allow-Origin' '*';
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
@ -31,11 +31,51 @@ server {
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
if ($request_method = 'GET') {
|
if ($request_method = 'GET') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location /assets {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 30d;
|
||||||
|
|
||||||
|
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header 'Access-Control-Allow-Origin' '*';
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
#
|
||||||
|
# Om nom nom cookies
|
||||||
|
#
|
||||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
|
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';
|
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' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
#error_page 404 /404.html;
|
#error_page 404 /404.html;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"erigonURL": "http://localhost:8545"
|
"erigonURL": "http://localhost:8545",
|
||||||
|
"assetsURLPrefix": "http://localhost:3001"
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ import ResultHeader from "./search/ResultHeader";
|
||||||
import PendingResults from "./search/PendingResults";
|
import PendingResults from "./search/PendingResults";
|
||||||
import TransactionItem from "./search/TransactionItem";
|
import TransactionItem from "./search/TransactionItem";
|
||||||
import { SearchController } from "./search/search";
|
import { SearchController } from "./search/search";
|
||||||
import { ProviderContext } from "./useProvider";
|
import { RuntimeContext } from "./useRuntime";
|
||||||
import { useENSCache } from "./useReverseCache";
|
import { useENSCache } from "./useReverseCache";
|
||||||
import { useFeeToggler } from "./search/useFeeToggler";
|
import { useFeeToggler } from "./search/useFeeToggler";
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ type PageParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddressTransactions: React.FC = () => {
|
const AddressTransactions: React.FC = () => {
|
||||||
const provider = useContext(ProviderContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const params = useParams<BlockParams>();
|
const params = useParams<BlockParams>();
|
||||||
const location = useLocation<PageParams>();
|
const location = useLocation<PageParams>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 { useProvider, ProviderContext } from "./useProvider";
|
import { RuntimeContext, useRuntime } from "./useRuntime";
|
||||||
|
|
||||||
const Block = React.lazy(() => import("./Block"));
|
const Block = React.lazy(() => import("./Block"));
|
||||||
const BlockTransactions = React.lazy(() => import("./BlockTransactions"));
|
const BlockTransactions = React.lazy(() => import("./BlockTransactions"));
|
||||||
|
@ -11,11 +11,11 @@ const AddressTransactions = React.lazy(() => import("./AddressTransactions"));
|
||||||
const Transaction = React.lazy(() => import("./Transaction"));
|
const Transaction = React.lazy(() => import("./Transaction"));
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const provider = useProvider();
|
const runtime = useRuntime();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<>LOADING</>}>
|
<Suspense fallback={<>LOADING</>}>
|
||||||
<ProviderContext.Provider value={provider}>
|
<RuntimeContext.Provider value={runtime}>
|
||||||
<Router>
|
<Router>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" exact>
|
<Route path="/" exact>
|
||||||
|
@ -41,7 +41,7 @@ const App = () => {
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
</ProviderContext.Provider>
|
</RuntimeContext.Provider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import BlockLink from "./components/BlockLink";
|
||||||
import AddressOrENSName from "./components/AddressOrENSName";
|
import AddressOrENSName from "./components/AddressOrENSName";
|
||||||
import TransactionValue from "./components/TransactionValue";
|
import TransactionValue from "./components/TransactionValue";
|
||||||
import HexValue from "./components/HexValue";
|
import HexValue from "./components/HexValue";
|
||||||
import { ProviderContext } from "./useProvider";
|
import { RuntimeContext } from "./useRuntime";
|
||||||
import { useLatestBlockNumber } from "./useLatestBlock";
|
import { useLatestBlockNumber } from "./useLatestBlock";
|
||||||
|
|
||||||
type BlockParams = {
|
type BlockParams = {
|
||||||
|
@ -34,7 +34,7 @@ interface ExtendedBlock extends ethers.providers.Block {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Block: React.FC = () => {
|
const Block: React.FC = () => {
|
||||||
const provider = useContext(ProviderContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const params = useParams<BlockParams>();
|
const params = useParams<BlockParams>();
|
||||||
|
|
||||||
const [block, setBlock] = useState<ExtendedBlock>();
|
const [block, setBlock] = useState<ExtendedBlock>();
|
||||||
|
|
|
@ -13,7 +13,7 @@ import BlockLink from "./components/BlockLink";
|
||||||
import { ProcessedTransaction } from "./types";
|
import { ProcessedTransaction } from "./types";
|
||||||
import { PAGE_SIZE } from "./params";
|
import { PAGE_SIZE } from "./params";
|
||||||
import { useFeeToggler } from "./search/useFeeToggler";
|
import { useFeeToggler } from "./search/useFeeToggler";
|
||||||
import { ProviderContext } from "./useProvider";
|
import { RuntimeContext } from "./useRuntime";
|
||||||
import { useENSCache } from "./useReverseCache";
|
import { useENSCache } from "./useReverseCache";
|
||||||
|
|
||||||
type BlockParams = {
|
type BlockParams = {
|
||||||
|
@ -25,7 +25,7 @@ type PageParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlockTransactions: React.FC = () => {
|
const BlockTransactions: React.FC = () => {
|
||||||
const provider = useContext(ProviderContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const params = useParams<BlockParams>();
|
const params = useParams<BlockParams>();
|
||||||
const location = useLocation<PageParams>();
|
const location = useLocation<PageParams>();
|
||||||
const qs = queryString.parse(location.search);
|
const qs = queryString.parse(location.search);
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { NavLink, useHistory } from "react-router-dom";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import Timestamp from "./components/Timestamp";
|
import Timestamp from "./components/Timestamp";
|
||||||
import { ProviderContext } from "./useProvider";
|
import { RuntimeContext } from "./useRuntime";
|
||||||
import { useLatestBlock } from "./useLatestBlock";
|
import { useLatestBlock } from "./useLatestBlock";
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const provider = useContext(ProviderContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const [search, setSearch] = useState<string>();
|
const [search, setSearch] = useState<string>();
|
||||||
const [canSubmit, setCanSubmit] = useState<boolean>(false);
|
const [canSubmit, setCanSubmit] = useState<boolean>(false);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
|
@ -27,7 +27,7 @@ import FormattedBalance from "./components/FormattedBalance";
|
||||||
import TokenTransferItem from "./TokenTransferItem";
|
import TokenTransferItem from "./TokenTransferItem";
|
||||||
import erc20 from "./erc20.json";
|
import erc20 from "./erc20.json";
|
||||||
import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types";
|
import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types";
|
||||||
import { ProviderContext } from "./useProvider";
|
import { RuntimeContext } from "./useRuntime";
|
||||||
|
|
||||||
const TRANSFER_TOPIC =
|
const TRANSFER_TOPIC =
|
||||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
||||||
|
@ -37,7 +37,7 @@ type TransactionParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Transaction: React.FC = () => {
|
const Transaction: React.FC = () => {
|
||||||
const provider = useContext(ProviderContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const params = useParams<TransactionParams>();
|
const params = useParams<TransactionParams>();
|
||||||
const { txhash } = params;
|
const { txhash } = params;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useContext } from "react";
|
||||||
|
import { fourBytesURL } from "../url";
|
||||||
|
import { RuntimeContext } from "../useRuntime";
|
||||||
|
|
||||||
type MethodNameProps = {
|
type MethodNameProps = {
|
||||||
data: string;
|
data: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
||||||
|
const runtime = useContext(RuntimeContext);
|
||||||
|
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data === "0x") {
|
if (data === "0x") {
|
||||||
|
@ -16,7 +20,13 @@ const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
||||||
|
|
||||||
// Try to resolve 4bytes name
|
// Try to resolve 4bytes name
|
||||||
const fourBytes = _name.slice(2);
|
const fourBytes = _name.slice(2);
|
||||||
const signatureURL = `http://localhost:3001/signatures/${fourBytes}`;
|
const { config } = runtime;
|
||||||
|
if (!config) {
|
||||||
|
setName(_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureURL = fourBytesURL(config.assetsURLPrefix ?? "", fourBytes);
|
||||||
fetch(signatureURL)
|
fetch(signatureURL)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -37,7 +47,7 @@ const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
||||||
|
|
||||||
// Use the default 4 bytes as name
|
// Use the default 4 bytes as name
|
||||||
setName(_name);
|
setName(_name);
|
||||||
}, [data]);
|
}, [runtime, data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-50 rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max">
|
<div className="bg-blue-50 rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max">
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { Suspense } from "react";
|
import React, { Suspense, useContext } from "react";
|
||||||
import { useImage } from "react-image";
|
import { useImage } from "react-image";
|
||||||
|
import { tokenLogoURL } from "../url";
|
||||||
|
import { RuntimeContext } from "../useRuntime";
|
||||||
|
|
||||||
type TokenLogoProps = {
|
type TokenLogoProps = {
|
||||||
address: string;
|
address: string;
|
||||||
|
@ -13,12 +15,14 @@ const TokenLogo: React.FC<TokenLogoProps> = (props) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const InternalTokenLogo: React.FC<TokenLogoProps> = ({ address, name }) => {
|
const InternalTokenLogo: React.FC<TokenLogoProps> = ({ address, name }) => {
|
||||||
const { src } = useImage({
|
const { config } = useContext(RuntimeContext);
|
||||||
srcList: [
|
|
||||||
`http://localhost:3001/assets/${address}/logo.png`,
|
const srcList: string[] = [];
|
||||||
"/eth-diamond-black.png",
|
if (config) {
|
||||||
],
|
srcList.push(tokenLogoURL(config.assetsURLPrefix ?? "", address));
|
||||||
});
|
}
|
||||||
|
srcList.push("/eth-diamond-black.png");
|
||||||
|
const { src } = useImage({ srcList });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-5 h-5">
|
<div className="flex items-center justify-center w-5 h-5">
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const fourBytesURL = (
|
||||||
|
assetsURLPrefix: string,
|
||||||
|
fourBytes: string
|
||||||
|
): string => `${assetsURLPrefix}/signatures/${fourBytes}`;
|
||||||
|
|
||||||
|
export const tokenLogoURL = (
|
||||||
|
assetsURLPrefix: string,
|
||||||
|
address: string
|
||||||
|
): string => `${assetsURLPrefix}/assets/${address}/logo.png`;
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
export type OtterscanConfig = {
|
export type OtterscanConfig = {
|
||||||
erigonURL: string;
|
erigonURL?: string;
|
||||||
|
assetsURLPrefix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useErigon = (): [boolean?, OtterscanConfig?] => {
|
export const useConfig = (): [boolean?, OtterscanConfig?] => {
|
||||||
const [configOK, setConfigOK] = useState<boolean>();
|
const [configOK, setConfigOK] = useState<boolean>();
|
||||||
const [config, setConfig] = useState<OtterscanConfig>();
|
const [config, setConfig] = useState<OtterscanConfig>();
|
||||||
|
|
||||||
|
@ -14,6 +15,8 @@ export const useErigon = (): [boolean?, OtterscanConfig?] => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const _config: OtterscanConfig = await res.json();
|
const _config: OtterscanConfig = await res.json();
|
||||||
|
console.info("Loaded app config");
|
||||||
|
console.info(_config);
|
||||||
setConfig(_config);
|
setConfig(_config);
|
||||||
setConfigOK(res.ok);
|
setConfigOK(res.ok);
|
||||||
}
|
}
|
|
@ -1,16 +1,11 @@
|
||||||
import React from "react";
|
import { useMemo } from "react";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import { useErigon } from "./useErigon";
|
|
||||||
|
|
||||||
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 = (): ethers.providers.JsonRpcProvider | undefined => {
|
export const useProvider = (
|
||||||
const [configOK, config] = useErigon();
|
erigonURL?: string
|
||||||
if (!configOK) {
|
): ethers.providers.JsonRpcProvider | undefined => {
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let erigonURL = config?.erigonURL;
|
|
||||||
if (erigonURL === "") {
|
if (erigonURL === "") {
|
||||||
console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`);
|
console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`);
|
||||||
erigonURL = DEFAULT_ERIGON_URL;
|
erigonURL = DEFAULT_ERIGON_URL;
|
||||||
|
@ -18,9 +13,12 @@ export const useProvider = (): ethers.providers.JsonRpcProvider | undefined => {
|
||||||
console.log(`Using configured erigon URL: ${erigonURL}`);
|
console.log(`Using configured erigon URL: ${erigonURL}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ethers.providers.JsonRpcProvider(erigonURL, "mainnet");
|
const provider = useMemo(
|
||||||
|
() => new ethers.providers.JsonRpcProvider(erigonURL, "mainnet"),
|
||||||
|
[erigonURL]
|
||||||
|
);
|
||||||
|
if (!erigonURL) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProviderContext = React.createContext<
|
|
||||||
ethers.providers.JsonRpcProvider | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { OtterscanConfig, useConfig } from "./useConfig";
|
||||||
|
import { useProvider } from "./useProvider";
|
||||||
|
|
||||||
|
export type OtterscanRuntime = {
|
||||||
|
config?: OtterscanConfig;
|
||||||
|
provider?: ethers.providers.JsonRpcProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRuntime = (): OtterscanRuntime => {
|
||||||
|
const [configOK, config] = useConfig();
|
||||||
|
const provider = useProvider(configOK ? config?.erigonURL : undefined);
|
||||||
|
|
||||||
|
const runtime = useMemo(
|
||||||
|
(): OtterscanRuntime => ({ config, provider }),
|
||||||
|
[config, provider]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!configOK) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RuntimeContext = React.createContext<OtterscanRuntime>(null!);
|
Loading…
Reference in New Issue