diff --git a/README.md b/README.md index 9e03faf..f9398a7 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Now you should have an Erigon node with Otterscan jsonrpc APIs enabled, running ### 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: diff --git a/nginx.conf b/nginx.conf index 6060f2b..029fb69 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,10 +4,10 @@ server { #access_log /var/log/nginx/host.access.log main; - location / { + location /signatures { root /usr/share/nginx/html; - index index.html index.htm; - + expires 30d; + # Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/ if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; @@ -31,11 +31,51 @@ server { 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 /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' '*'; + # + # 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' '*' 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; diff --git a/public/config.json b/public/config.json index d602ced..1db5aa7 100644 --- a/public/config.json +++ b/public/config.json @@ -1,3 +1,4 @@ { - "erigonURL": "http://localhost:8545" + "erigonURL": "http://localhost:8545", + "assetsURLPrefix": "http://localhost:3001" } \ No newline at end of file diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index f080b96..e671087 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -12,7 +12,7 @@ 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 { RuntimeContext } from "./useRuntime"; import { useENSCache } from "./useReverseCache"; import { useFeeToggler } from "./search/useFeeToggler"; @@ -26,7 +26,7 @@ type PageParams = { }; const AddressTransactions: React.FC = () => { - const provider = useContext(ProviderContext); + const { provider } = useContext(RuntimeContext); const params = useParams(); const location = useLocation(); const history = useHistory(); diff --git a/src/App.tsx b/src/App.tsx index 122081c..2bf5e2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ 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"; +import { RuntimeContext, useRuntime } from "./useRuntime"; const Block = React.lazy(() => import("./Block")); const BlockTransactions = React.lazy(() => import("./BlockTransactions")); @@ -11,11 +11,11 @@ const AddressTransactions = React.lazy(() => import("./AddressTransactions")); const Transaction = React.lazy(() => import("./Transaction")); const App = () => { - const provider = useProvider(); + const runtime = useRuntime(); return ( LOADING}> - + @@ -41,7 +41,7 @@ const App = () => { - + ); }; diff --git a/src/Block.tsx b/src/Block.tsx index 6f9ab22..963f2f9 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -16,7 +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 { RuntimeContext } from "./useRuntime"; import { useLatestBlockNumber } from "./useLatestBlock"; type BlockParams = { @@ -34,7 +34,7 @@ interface ExtendedBlock extends ethers.providers.Block { } const Block: React.FC = () => { - const provider = useContext(ProviderContext); + const { provider } = useContext(RuntimeContext); const params = useParams(); const [block, setBlock] = useState(); diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index 5e6d4ad..8f8f474 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -13,7 +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 { RuntimeContext } from "./useRuntime"; import { useENSCache } from "./useReverseCache"; type BlockParams = { @@ -25,7 +25,7 @@ type PageParams = { }; const BlockTransactions: React.FC = () => { - const provider = useContext(ProviderContext); + const { provider } = useContext(RuntimeContext); const params = useParams(); const location = useLocation(); const qs = queryString.parse(location.search); diff --git a/src/Home.tsx b/src/Home.tsx index 9cc40fc..8626dce 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -3,11 +3,11 @@ 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 { RuntimeContext } from "./useRuntime"; import { useLatestBlock } from "./useLatestBlock"; const Home: React.FC = () => { - const provider = useContext(ProviderContext); + const { provider } = useContext(RuntimeContext); const [search, setSearch] = useState(); const [canSubmit, setCanSubmit] = useState(false); const history = useHistory(); diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 724fb31..4f8c312 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -27,7 +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"; +import { RuntimeContext } from "./useRuntime"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; @@ -37,7 +37,7 @@ type TransactionParams = { }; const Transaction: React.FC = () => { - const provider = useContext(ProviderContext); + const { provider } = useContext(RuntimeContext); const params = useParams(); const { txhash } = params; diff --git a/src/components/MethodName.tsx b/src/components/MethodName.tsx index 27ad8ca..55ff909 100644 --- a/src/components/MethodName.tsx +++ b/src/components/MethodName.tsx @@ -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 = { data: string; }; const MethodName: React.FC = ({ data }) => { + const runtime = useContext(RuntimeContext); + const [name, setName] = useState(); useEffect(() => { if (data === "0x") { @@ -16,7 +20,13 @@ const MethodName: React.FC = ({ data }) => { // Try to resolve 4bytes name 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) .then(async (res) => { if (!res.ok) { @@ -37,7 +47,7 @@ const MethodName: React.FC = ({ data }) => { // Use the default 4 bytes as name setName(_name); - }, [data]); + }, [runtime, data]); return (
diff --git a/src/components/TokenLogo.tsx b/src/components/TokenLogo.tsx index 738de1c..c6f5b8b 100644 --- a/src/components/TokenLogo.tsx +++ b/src/components/TokenLogo.tsx @@ -1,5 +1,7 @@ -import React, { Suspense } from "react"; +import React, { Suspense, useContext } from "react"; import { useImage } from "react-image"; +import { tokenLogoURL } from "../url"; +import { RuntimeContext } from "../useRuntime"; type TokenLogoProps = { address: string; @@ -13,12 +15,14 @@ const TokenLogo: React.FC = (props) => ( ); const InternalTokenLogo: React.FC = ({ address, name }) => { - const { src } = useImage({ - srcList: [ - `http://localhost:3001/assets/${address}/logo.png`, - "/eth-diamond-black.png", - ], - }); + const { config } = useContext(RuntimeContext); + + const srcList: string[] = []; + if (config) { + srcList.push(tokenLogoURL(config.assetsURLPrefix ?? "", address)); + } + srcList.push("/eth-diamond-black.png"); + const { src } = useImage({ srcList }); return (
diff --git a/src/url.ts b/src/url.ts new file mode 100644 index 0000000..0586dbe --- /dev/null +++ b/src/url.ts @@ -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`; diff --git a/src/useErigon.ts b/src/useConfig.ts similarity index 73% rename from src/useErigon.ts rename to src/useConfig.ts index bd64364..3bb4858 100644 --- a/src/useErigon.ts +++ b/src/useConfig.ts @@ -1,10 +1,11 @@ import { useState, useEffect } from "react"; export type OtterscanConfig = { - erigonURL: string; + erigonURL?: string; + assetsURLPrefix?: string; }; -export const useErigon = (): [boolean?, OtterscanConfig?] => { +export const useConfig = (): [boolean?, OtterscanConfig?] => { const [configOK, setConfigOK] = useState(); const [config, setConfig] = useState(); @@ -14,6 +15,8 @@ export const useErigon = (): [boolean?, OtterscanConfig?] => { if (res.ok) { const _config: OtterscanConfig = await res.json(); + console.info("Loaded app config"); + console.info(_config); setConfig(_config); setConfigOK(res.ok); } diff --git a/src/useProvider.ts b/src/useProvider.ts index 895c16d..cd4024d 100644 --- a/src/useProvider.ts +++ b/src/useProvider.ts @@ -1,16 +1,11 @@ -import React from "react"; +import { useMemo } 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; +export const useProvider = ( + erigonURL?: string +): ethers.providers.JsonRpcProvider | undefined => { if (erigonURL === "") { console.info(`Using default erigon URL: ${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}`); } - 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); diff --git a/src/useRuntime.ts b/src/useRuntime.ts new file mode 100644 index 0000000..ed2bcc9 --- /dev/null +++ b/src/useRuntime.ts @@ -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(null!);