From f79817fc5d0d9b866d08c6ecc691169a53b21dc8 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Fri, 9 Jul 2021 21:11:52 -0300 Subject: [PATCH 01/27] Using preprocessed icons on production docker images; was able to reduce image size from 170 -> 55MB; merging into develop to evaluate during next dev cycle --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index eecfd43..b58204e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,16 @@ COPY ["public", "/otterscan-build/public"] COPY ["src", "/otterscan-build/src"] RUN npm run build +FROM alpine:3.14.0 AS logobuilder +RUN apk add imagemagick parallel +WORKDIR /assets +COPY trustwallet/blockchains/ethereum/assets /assets/ +RUN find . -name logo.png | parallel magick convert {} -filter Lanczos -resize 32x32 {} + 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 --from=logobuilder /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 / From 70c29b562fd92b514bfba10658013340a32fd183 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Sat, 10 Jul 2021 03:17:07 -0300 Subject: [PATCH 02/27] Extract footer --- src/App.tsx | 50 +++++++++++++++++++---------------- src/Footer.tsx | 18 +++++++++++++ src/Home.tsx | 71 +++++++++++++++++++++----------------------------- src/Logo.tsx | 2 +- 4 files changed, 77 insertions(+), 64 deletions(-) create mode 100644 src/Footer.tsx diff --git a/src/App.tsx b/src/App.tsx index 2bf5e2a..195f2e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +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 Footer from "./Footer"; import { RuntimeContext, useRuntime } from "./useRuntime"; const Block = React.lazy(() => import("./Block")); @@ -16,31 +17,36 @@ const App = () => { return ( LOADING}> - - - - - - - - - - - <Route path="/block/:blockNumberOrHash" exact> - <Block /> + <div className="h-screen flex flex-col"> + <Router> + <Switch> + <Route path="/" exact> + <Home /> </Route> - <Route path="/block/:blockNumber/txs" exact> - <BlockTransactions /> + <Route path="/search" exact> + <Search /> </Route> - <Route path="/tx/:txhash"> - <Transaction /> + <Route> + <div className="mb-auto"> + <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> + </div> </Route> - <Route path="/address/:addressOrName/:direction?"> - <AddressTransactions /> - </Route> - </Route> - </Switch> - </Router> + </Switch> + </Router> + <Footer /> + </div> </RuntimeContext.Provider> </Suspense> ); diff --git a/src/Footer.tsx b/src/Footer.tsx new file mode 100644 index 0000000..61d59d7 --- /dev/null +++ b/src/Footer.tsx @@ -0,0 +1,18 @@ +import React, { useContext } from "react"; +import { RuntimeContext } from "./useRuntime"; + +const Footer: React.FC = () => { + const { provider } = useContext(RuntimeContext); + + return ( + <div className="w-full px-2 py-1 border-t border-t-gray-100 text-xs bg-link-blue text-gray-200 text-center"> + {provider ? ( + <>Using Erigon node at {provider.connection.url}</> + ) : ( + <>Waiting for the provider...</> + )} + </div> + ); +}; + +export default React.memo(Footer); diff --git a/src/Home.tsx b/src/Home.tsx index 8626dce..7d74fcd 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -31,49 +31,38 @@ const Home: React.FC = () => { document.title = "Home | Otterscan"; return ( - <div className="h-screen flex m-auto"> - <div className="flex flex-col m-auto"> - <Logo /> - <form - className="flex flex-col m-auto" - onSubmit={handleSubmit} - autoComplete="off" - spellCheck={false} + <div className="m-auto"> + <Logo /> + <form + className="flex flex-col" + onSubmit={handleSubmit} + autoComplete="off" + spellCheck={false} + > + <input + className="w-full border rounded focus:outline-none px-2 py-1 mb-10" + type="text" + size={50} + placeholder="Search by address / txn hash / block number / ENS name" + onChange={handleChange} + autoFocus + ></input> + <button + className="mx-auto px-3 py-1 mb-10 rounded bg-gray-100 hover:bg-gray-200 focus:outline-none" + type="submit" > - <input - className="w-full border rounded focus:outline-none px-2 py-1 mb-10" - type="text" - size={50} - placeholder="Search by address / txn hash / block number / ENS name" - onChange={handleChange} - autoFocus - ></input> - <button - className="mx-auto px-3 py-1 mb-10 rounded bg-gray-100 hover:bg-gray-200 focus:outline-none" - type="submit" + Search + </button> + {latestBlock && ( + <NavLink + className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue" + to={`/block/${latestBlock.number}`} > - Search - </button> - {latestBlock && ( - <NavLink - className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue" - to={`/block/${latestBlock.number}`} - > - <div> - Latest block: {ethers.utils.commify(latestBlock.number)} - </div> - <Timestamp value={latestBlock.timestamp} /> - </NavLink> - )} - <span className="mx-auto mt-5 text-xs text-gray-500"> - {provider ? ( - <>Using Erigon node at {provider.connection.url}</> - ) : ( - <>Waiting for the provider...</> - )} - </span> - </form> - </div> + <div>Latest block: {ethers.utils.commify(latestBlock.number)}</div> + <Timestamp value={latestBlock.timestamp} /> + </NavLink> + )} + </form> </div> ); }; diff --git a/src/Logo.tsx b/src/Logo.tsx index 9d00d2c..905ee2c 100644 --- a/src/Logo.tsx +++ b/src/Logo.tsx @@ -1,7 +1,7 @@ import React from "react"; const Logo: React.FC = () => ( - <div className="mx-auto -mt-32 mb-16 text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4"> + <div className="mx-auto mb-16 text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4"> <img className="rounded-full" src="/otter.jpg" From 2b0505584ecd84874fef5351fb5e6d2e04a98d30 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Sat, 10 Jul 2021 23:39:21 -0300 Subject: [PATCH 03/27] Change the instruction for adding remote; some users were having problems with ssh checkout --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9398a7..b8d6220 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Add our forked Erigon git tree as an additional remote and checkout the correspo The repository with Otterscan patches is [here](https://github.com/wmitsuda/erigon). ``` -git remote add otterscan git@github.com:wmitsuda/erigon.git +git remote add otterscan https://github.com/wmitsuda/erigon.git ``` Checkout the tag corresponding to the stable version you are running. For each supported Erigon version, there should be a corresponding tag containing Otterscan patches. From 337eecee68854325ba16349f3f8b31ec5596a134 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Mon, 12 Jul 2021 17:10:16 -0300 Subject: [PATCH 04/27] Add detection of wrong Erigon configuration --- src/App.tsx | 73 +++++++++++---------- src/ConnectionErrorPanel.tsx | 120 +++++++++++++++++++++++++++++++++++ src/types.ts | 8 +++ src/useProvider.ts | 85 ++++++++++++++++++++----- src/useRuntime.ts | 12 ++-- 5 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 src/ConnectionErrorPanel.tsx diff --git a/src/App.tsx b/src/App.tsx index 195f2e8..4a4c323 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,9 @@ import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import Home from "./Home"; import Search from "./Search"; import Title from "./Title"; +import ConnectionErrorPanel from "./ConnectionErrorPanel"; import Footer from "./Footer"; +import { ConnectionStatus } from "./types"; import { RuntimeContext, useRuntime } from "./useRuntime"; const Block = React.lazy(() => import("./Block")); @@ -16,38 +18,45 @@ const App = () => { return ( <Suspense fallback={<>LOADING</>}> - <RuntimeContext.Provider value={runtime}> - <div className="h-screen flex flex-col"> - <Router> - <Switch> - <Route path="/" exact> - <Home /> - </Route> - <Route path="/search" exact> - <Search /> - </Route> - <Route> - <div className="mb-auto"> - <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> - </div> - </Route> - </Switch> - </Router> - <Footer /> - </div> - </RuntimeContext.Provider> + {runtime.connStatus !== ConnectionStatus.CONNECTED ? ( + <ConnectionErrorPanel + connStatus={runtime.connStatus} + config={runtime.config} + /> + ) : ( + <RuntimeContext.Provider value={runtime}> + <div className="h-screen flex flex-col"> + <Router> + <Switch> + <Route path="/" exact> + <Home /> + </Route> + <Route path="/search" exact> + <Search /> + </Route> + <Route> + <div className="mb-auto"> + <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> + </div> + </Route> + </Switch> + </Router> + <Footer /> + </div> + </RuntimeContext.Provider> + )} </Suspense> ); }; diff --git a/src/ConnectionErrorPanel.tsx b/src/ConnectionErrorPanel.tsx new file mode 100644 index 0000000..cd6b1f2 --- /dev/null +++ b/src/ConnectionErrorPanel.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faClock, + faCheckCircle, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons"; +import { ConnectionStatus } from "./types"; +import { OtterscanConfig } from "./useConfig"; + +type ConnectionErrorPanelProps = { + connStatus: ConnectionStatus; + config?: OtterscanConfig; +}; + +const ConnectionErrorPanel: React.FC<ConnectionErrorPanelProps> = ({ + connStatus, + config, +}) => { + console.log("PRINT"); + return ( + <div className="h-screen flex flex-col bg-gray-300 font-sans"> + <div className="m-auto h-60 text-gray-700 text-lg min-w-lg max-w-lg"> + <Step type="wait" msg="Trying to connect to Erigon node..." /> + <div className="flex space-x-2"> + <span className="ml-7 text-base">{config?.erigonURL}</span> + </div> + {connStatus === ConnectionStatus.NOT_ETH_NODE && ( + <Step type="error" msg="It does not seem to be an ETH node"> + <p>Make sure your browser can access the URL above.</p> + <p> + If you want to customize the Erigon rpcdaemon endpoint, please + follow these{" "} + <a + href="https://github.com/wmitsuda/otterscan#run-otterscan-docker-image-from-docker-hub" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </p> + </Step> + )} + {connStatus === ConnectionStatus.NOT_ERIGON && ( + <> + <Step type="ok" msg="It is an ETH node" /> + <Step type="error" msg="It does not seem to be an Erigon node"> + Make sure you rpcdaemon with Otterscan patches is up and running + and the <strong>erigon_</strong> namespace is enabled according to + the{" "} + <a + href="https://github.com/wmitsuda/otterscan#install-otterscan-patches-on-top-of-erigon" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </Step> + </> + )} + {connStatus === ConnectionStatus.NOT_OTTERSCAN_PATCHED && ( + <> + <Step type="ok" msg="It is an Erigon node" /> + <Step + type="error" + msg="It does not seem to contain Otterscan patches" + > + Make sure you compiled rpcdaemon with Otterscan patches and + enabled <strong>ots_</strong> namespace according to the{" "} + <a + href="https://github.com/wmitsuda/otterscan#install-otterscan-patches-on-top-of-erigon" + target="_blank" + rel="noreferrer noopener" + className="font-bold text-blue-800 hover:underline" + > + instructions + </a> + . + </Step> + </> + )} + </div> + </div> + ); +}; + +type StepProps = { + type: "wait" | "ok" | "error"; + msg: string; +}; + +const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => ( + <> + <div className="flex space-x-2"> + {type === "wait" && ( + <span className="text-gray-600"> + <FontAwesomeIcon icon={faClock} size="1x" /> + </span> + )} + {type === "ok" && ( + <span className="text-green-600"> + <FontAwesomeIcon icon={faCheckCircle} size="1x" /> + </span> + )} + {type === "error" && ( + <span className="text-red-600"> + <FontAwesomeIcon icon={faTimesCircle} size="1x" /> + </span> + )} + <span>{msg}</span> + </div> + {children && <div className="ml-7 mt-4 text-sm">{children}</div>} + </> +)); + +export default React.memo(ConnectionErrorPanel); diff --git a/src/types.ts b/src/types.ts index 36537f8..9ee8d3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,13 @@ import { ethers, BigNumber } from "ethers"; +export enum ConnectionStatus { + CONNECTING, + NOT_ETH_NODE, + NOT_ERIGON, + NOT_OTTERSCAN_PATCHED, + CONNECTED, +} + export type ProcessedTransaction = { blockNumber: number; timestamp: number; diff --git a/src/useProvider.ts b/src/useProvider.ts index cd4024d..b8eed30 100644 --- a/src/useProvider.ts +++ b/src/useProvider.ts @@ -1,24 +1,79 @@ -import { useMemo } from "react"; +import { useEffect, useState } from "react"; import { ethers } from "ethers"; +import { ConnectionStatus } from "./types"; export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545"; export const useProvider = ( erigonURL?: string -): ethers.providers.JsonRpcProvider | undefined => { - if (erigonURL === "") { - console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`); - erigonURL = DEFAULT_ERIGON_URL; - } else { - console.log(`Using configured erigon URL: ${erigonURL}`); +): [ConnectionStatus, ethers.providers.JsonRpcProvider | undefined] => { + const [connStatus, setConnStatus] = useState<ConnectionStatus>( + ConnectionStatus.CONNECTING + ); + + if (erigonURL !== undefined) { + if (erigonURL === "") { + console.info(`Using default erigon URL: ${DEFAULT_ERIGON_URL}`); + erigonURL = DEFAULT_ERIGON_URL; + } else { + console.log(`Using configured erigon URL: ${erigonURL}`); + } } - const provider = useMemo( - () => new ethers.providers.JsonRpcProvider(erigonURL, "mainnet"), - [erigonURL] - ); - if (!erigonURL) { - return undefined; - } - return provider; + const [provider, setProvider] = useState< + ethers.providers.JsonRpcProvider | undefined + >(); + useEffect(() => { + if (erigonURL === undefined) { + setConnStatus(ConnectionStatus.NOT_ETH_NODE); + setProvider(undefined); + return; + } + setConnStatus(ConnectionStatus.CONNECTING); + + const tryToConnect = async () => { + const provider = new ethers.providers.JsonRpcProvider( + erigonURL, + "mainnet" + ); + + // Check if it is at least a regular ETH node + let blockNumber: number = 0; + try { + blockNumber = await provider.getBlockNumber(); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_ETH_NODE); + setProvider(undefined); + return; + } + + // Check if it is an Erigon node by probing a lightweight method + try { + await provider.send("erigon_getHeaderByNumber", [blockNumber]); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_ERIGON); + setProvider(undefined); + return; + } + + // Check if it has Otterscan patches by probing a lightweight method + try { + // TODO: replace by a custom made method that works in all networks + await provider.send("ots_getTransactionTransfers", [ + "0x793e079fbc427cba0857b4e878194ab508f33983f45415e50af3c3c0e662fdf3", + ]); + setConnStatus(ConnectionStatus.CONNECTED); + setProvider(provider); + } catch (err) { + console.log(err); + setConnStatus(ConnectionStatus.NOT_OTTERSCAN_PATCHED); + setProvider(undefined); + } + }; + tryToConnect(); + }, [erigonURL]); + + return [connStatus, provider]; }; diff --git a/src/useRuntime.ts b/src/useRuntime.ts index ed2bcc9..77fef72 100644 --- a/src/useRuntime.ts +++ b/src/useRuntime.ts @@ -2,23 +2,27 @@ import React, { useMemo } from "react"; import { ethers } from "ethers"; import { OtterscanConfig, useConfig } from "./useConfig"; import { useProvider } from "./useProvider"; +import { ConnectionStatus } from "./types"; export type OtterscanRuntime = { config?: OtterscanConfig; + connStatus: ConnectionStatus; provider?: ethers.providers.JsonRpcProvider; }; export const useRuntime = (): OtterscanRuntime => { const [configOK, config] = useConfig(); - const provider = useProvider(configOK ? config?.erigonURL : undefined); + const [connStatus, provider] = useProvider( + configOK ? config?.erigonURL : undefined + ); const runtime = useMemo( - (): OtterscanRuntime => ({ config, provider }), - [config, provider] + (): OtterscanRuntime => ({ config, connStatus, provider }), + [config, connStatus, provider] ); if (!configOK) { - return {}; + return { connStatus: ConnectionStatus.CONNECTING }; } return runtime; }; From 613c49b1ad33ff414ed47c093fe478d65e48345c Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Mon, 12 Jul 2021 17:47:37 -0300 Subject: [PATCH 05/27] Fix missing key attr; used a dummy key because it is decorative --- src/components/HexValue.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/HexValue.tsx b/src/components/HexValue.tsx index 3b62fcf..b07b473 100644 --- a/src/components/HexValue.tsx +++ b/src/components/HexValue.tsx @@ -14,6 +14,7 @@ const HexValue: React.FC<HexValueProps> = ({ value }) => { <> {shards.map((s, i) => ( <span + key={i} className={`font-hash ${ i % 2 === 0 ? "text-black" : "text-gray-400" }`} From 47dc05f1c502651a03923cb606527a23d580a881 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Mon, 12 Jul 2021 17:48:14 -0300 Subject: [PATCH 06/27] Remove debug code --- src/ConnectionErrorPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ConnectionErrorPanel.tsx b/src/ConnectionErrorPanel.tsx index cd6b1f2..c03ce84 100644 --- a/src/ConnectionErrorPanel.tsx +++ b/src/ConnectionErrorPanel.tsx @@ -17,7 +17,6 @@ const ConnectionErrorPanel: React.FC<ConnectionErrorPanelProps> = ({ connStatus, config, }) => { - console.log("PRINT"); return ( <div className="h-screen flex flex-col bg-gray-300 font-sans"> <div className="m-auto h-60 text-gray-700 text-lg min-w-lg max-w-lg"> From 7f39ba523205883c5ac6403aa0d6b3015a94e91b Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Mon, 12 Jul 2021 19:13:12 -0300 Subject: [PATCH 07/27] Extract custom hook for 4bytes resolution --- src/components/MethodName.tsx | 51 ++++------------------------------- src/use4Bytes.ts | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 src/use4Bytes.ts diff --git a/src/components/MethodName.tsx b/src/components/MethodName.tsx index 55ff909..446265b 100644 --- a/src/components/MethodName.tsx +++ b/src/components/MethodName.tsx @@ -1,58 +1,17 @@ -import React, { useState, useEffect, useContext } from "react"; -import { fourBytesURL } from "../url"; -import { RuntimeContext } from "../useRuntime"; +import React from "react"; +import { use4Bytes } from "../use4Bytes"; type MethodNameProps = { data: string; }; const MethodName: React.FC<MethodNameProps> = ({ data }) => { - const runtime = useContext(RuntimeContext); - - const [name, setName] = useState<string>(); - useEffect(() => { - if (data === "0x") { - setName("Transfer"); - return; - } - - let _name = data.slice(0, 10); - - // Try to resolve 4bytes name - const fourBytes = _name.slice(2); - const { config } = runtime; - if (!config) { - setName(_name); - return; - } - - const signatureURL = fourBytesURL(config.assetsURLPrefix ?? "", fourBytes); - fetch(signatureURL) - .then(async (res) => { - if (!res.ok) { - console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); - return; - } - - const sig = await res.text(); - const cut = sig.indexOf("("); - let method = sig.slice(0, cut); - method = method.charAt(0).toUpperCase() + method.slice(1); - setName(method); - return; - }) - .catch((err) => { - console.error(`Couldn't fetch signature URL ${signatureURL}`, err); - }); - - // Use the default 4 bytes as name - setName(_name); - }, [runtime, data]); + const methodName = use4Bytes(data); return ( <div className="bg-blue-50 rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max"> - <p className="truncate" title={name}> - {name} + <p className="truncate" title={methodName}> + {methodName} </p> </div> ); diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts new file mode 100644 index 0000000..833de27 --- /dev/null +++ b/src/use4Bytes.ts @@ -0,0 +1,49 @@ +import { useState, useEffect, useContext } from "react"; +import { RuntimeContext } from "./useRuntime"; +import { fourBytesURL } from "./url"; + +export const use4Bytes = (data: string) => { + const runtime = useContext(RuntimeContext); + + const [name, setName] = useState<string>(); + useEffect(() => { + if (data === "0x") { + setName("Transfer"); + return; + } + + let _name = data.slice(0, 10); + + // Try to resolve 4bytes name + const fourBytes = _name.slice(2); + const { config } = runtime; + if (!config) { + setName(_name); + return; + } + + const signatureURL = fourBytesURL(config.assetsURLPrefix ?? "", fourBytes); + fetch(signatureURL) + .then(async (res) => { + if (!res.ok) { + console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); + return; + } + + const sig = await res.text(); + const cut = sig.indexOf("("); + let method = sig.slice(0, cut); + method = method.charAt(0).toUpperCase() + method.slice(1); + setName(method); + return; + }) + .catch((err) => { + console.error(`Couldn't fetch signature URL ${signatureURL}`, err); + }); + + // Use the default 4 bytes as name + setName(_name); + }, [runtime, data]); + + return name; +}; From 9128973a9b63d282207a135c6df9ca62dff9f6b3 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 02:19:33 -0300 Subject: [PATCH 08/27] Add global method name cache --- src/use4Bytes.ts | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 833de27..11f29d2 100644 --- a/src/use4Bytes.ts +++ b/src/use4Bytes.ts @@ -2,31 +2,30 @@ import { useState, useEffect, useContext } from "react"; import { RuntimeContext } from "./useRuntime"; import { fourBytesURL } from "./url"; +const cache = new Map<string, string | null>(); + export const use4Bytes = (data: string) => { const runtime = useContext(RuntimeContext); + const assetsURLPrefix = runtime.config?.assetsURLPrefix; + + let rawFourBytes = data.slice(0, 10); const [name, setName] = useState<string>(); + const [fourBytes, setFourBytes] = useState<string>(); useEffect(() => { - if (data === "0x") { - setName("Transfer"); + if (assetsURLPrefix === undefined || fourBytes === undefined) { return; } - let _name = data.slice(0, 10); - - // Try to resolve 4bytes name - const fourBytes = _name.slice(2); - const { config } = runtime; - if (!config) { - setName(_name); - return; - } - - const signatureURL = fourBytesURL(config.assetsURLPrefix ?? "", fourBytes); + const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); fetch(signatureURL) .then(async (res) => { if (!res.ok) { console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); + + // Use the default 4 bytes as name + setName(rawFourBytes); + cache.set(fourBytes, null); return; } @@ -35,15 +34,39 @@ export const use4Bytes = (data: string) => { let method = sig.slice(0, cut); method = method.charAt(0).toUpperCase() + method.slice(1); setName(method); + cache.set(fourBytes, method); return; }) .catch((err) => { console.error(`Couldn't fetch signature URL ${signatureURL}`, err); - }); - // Use the default 4 bytes as name - setName(_name); - }, [runtime, data]); + // Use the default 4 bytes as name + setName(rawFourBytes); + }); + }, [rawFourBytes, assetsURLPrefix, fourBytes]); + + if (data === "0x") { + return "Transfer"; + } + if (assetsURLPrefix === undefined) { + return rawFourBytes; + } + + // Try to resolve 4bytes name + const entry = cache.get(rawFourBytes.slice(2)); + if (entry === null) { + return rawFourBytes; + } + if (entry !== undefined) { + // Simulates LRU + cache.delete(entry); + cache.set(rawFourBytes.slice(2), entry); + return entry; + } + if (name === undefined && fourBytes === undefined) { + setFourBytes(rawFourBytes.slice(2)); + return ""; + } return name; }; From 693e0c661134211086fe2a14202579e462df107e Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 02:36:09 -0300 Subject: [PATCH 09/27] Differentiate simple eth transfers from transfer method calls --- src/components/MethodName.tsx | 17 ++++++++++++++--- src/use4Bytes.ts | 6 ++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/MethodName.tsx b/src/components/MethodName.tsx index 446265b..1ed596d 100644 --- a/src/components/MethodName.tsx +++ b/src/components/MethodName.tsx @@ -6,11 +6,22 @@ type MethodNameProps = { }; const MethodName: React.FC<MethodNameProps> = ({ data }) => { - const methodName = use4Bytes(data); + const rawFourBytes = data.slice(0, 10); + const methodName = use4Bytes(rawFourBytes); + const isSimpleTransfer = data === "0x"; + const methodTitle = isSimpleTransfer + ? "ETH Transfer" + : methodName === rawFourBytes + ? methodName + : `${methodName} [${rawFourBytes}]`; return ( - <div className="bg-blue-50 rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max"> - <p className="truncate" title={methodName}> + <div + className={`${ + isSimpleTransfer ? "bg-yellow-100" : "bg-blue-50" + } rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max`} + > + <p className="truncate" title={methodTitle}> {methodName} </p> </div> diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 11f29d2..dad9f75 100644 --- a/src/use4Bytes.ts +++ b/src/use4Bytes.ts @@ -4,12 +4,10 @@ import { fourBytesURL } from "./url"; const cache = new Map<string, string | null>(); -export const use4Bytes = (data: string) => { +export const use4Bytes = (rawFourBytes: string) => { const runtime = useContext(RuntimeContext); const assetsURLPrefix = runtime.config?.assetsURLPrefix; - let rawFourBytes = data.slice(0, 10); - const [name, setName] = useState<string>(); const [fourBytes, setFourBytes] = useState<string>(); useEffect(() => { @@ -45,7 +43,7 @@ export const use4Bytes = (data: string) => { }); }, [rawFourBytes, assetsURLPrefix, fourBytes]); - if (data === "0x") { + if (rawFourBytes === "0x") { return "Transfer"; } if (assetsURLPrefix === undefined) { From 1bf47266a471e2a8c6041584258968ef50ec7acb Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 02:56:43 -0300 Subject: [PATCH 10/27] Display method name on transaction details page --- src/Transaction.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 4f8c312..e2b6390 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -22,6 +22,7 @@ import AddressLink from "./components/AddressLink"; import Copy from "./components/Copy"; import Timestamp from "./components/Timestamp"; import InternalTransfer from "./components/InternalTransfer"; +import MethodName from "./components/MethodName"; import GasValue from "./components/GasValue"; import FormattedBalance from "./components/FormattedBalance"; import TokenTransferItem from "./TokenTransferItem"; @@ -232,7 +233,9 @@ const Transaction: React.FC = () => { </div> )} </InfoRow> - <InfoRow title="Transaction Action"></InfoRow> + <InfoRow title="Transaction Action"> + <MethodName data={txData.data} /> + </InfoRow> {txData.tokenTransfers.length > 0 && ( <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`} From 5f04ce8018a31c95162ef9f22ed521cebd9993a0 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 18:12:49 -0300 Subject: [PATCH 11/27] First attempt at showing chainlink eth/usd and fast gas price feeds info --- package-lock.json | 11 +++++++ package.json | 1 + src/PriceBox.tsx | 79 +++++++++++++++++++++++++++++++++++++++++++++++ src/Title.tsx | 44 ++++++++++++++------------ 4 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 src/PriceBox.tsx diff --git a/package-lock.json b/package-lock.json index 9fe273e..df0002c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@chainlink/contracts": "^0.2.1", "@craco/craco": "^6.2.0", "@fontsource/fira-code": "^4.5.0", "@fontsource/roboto": "^4.5.0", @@ -1203,6 +1204,11 @@ "version": "0.2.3", "license": "MIT" }, + "node_modules/@chainlink/contracts": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz", + "integrity": "sha512-mAQgPQKiqW3tLMlp31NgcnXpwG3lttgKU0izAqKiirJ9LH7rQ+O0oHIVR5Qp2yuqgmfbLsgfdLo4GcVC8IFz3Q==" + }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "license": "Apache-2.0", @@ -19913,6 +19919,11 @@ "@bcoe/v8-coverage": { "version": "0.2.3" }, + "@chainlink/contracts": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz", + "integrity": "sha512-mAQgPQKiqW3tLMlp31NgcnXpwG3lttgKU0izAqKiirJ9LH7rQ+O0oHIVR5Qp2yuqgmfbLsgfdLo4GcVC8IFz3Q==" + }, "@cnakazawa/watch": { "version": "1.0.4", "requires": { diff --git a/package.json b/package.json index d76b66f..09b401f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "license": "MIT", "dependencies": { + "@chainlink/contracts": "^0.2.1", "@craco/craco": "^6.2.0", "@fontsource/fira-code": "^4.5.0", "@fontsource/roboto": "^4.5.0", diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx new file mode 100644 index 0000000..be78555 --- /dev/null +++ b/src/PriceBox.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useMemo, useContext } from "react"; +import { ethers } from "ethers"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faGasPump } from "@fortawesome/free-solid-svg-icons"; +import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; +import { RuntimeContext } from "./useRuntime"; + +const ETH_FEED_DECIMALS = 8; + +const PriceBox: React.FC = () => { + const { provider } = useContext(RuntimeContext); + const ethFeed = useMemo( + () => + provider && + new ethers.Contract("eth-usd.data.eth", AggregatorV3Interface, provider), + [provider] + ); + const gasFeed = useMemo( + () => + provider && + new ethers.Contract( + "fast-gas-gwei.data.eth", + AggregatorV3Interface, + provider + ), + [provider] + ); + + const [latestPriceData, setLatestPriceData] = useState<any>(); + const [latestGasData, setLatestGasData] = useState<any>(); + useEffect(() => { + if (!ethFeed || !gasFeed) { + return; + } + + const readData = async () => { + const [priceData, gasData] = await Promise.all([ + ethFeed.latestRoundData(), + await gasFeed.latestRoundData(), + ]); + setLatestPriceData(priceData); + setLatestGasData(gasData); + }; + readData(); + }, [ethFeed, gasFeed]); + + return ( + <> + {latestPriceData && ( + <div className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800"> + <span> + Eth: $ + <span className="font-balance"> + {ethers.utils.commify( + ethers.utils.formatUnits( + latestPriceData.answer, + ETH_FEED_DECIMALS + ) + )} + </span> + </span> + {latestGasData && ( + <> + <span>|</span> + <span className="text-gray-400"> + <FontAwesomeIcon icon={faGasPump} size="1x" /> + <span className="ml-1"> + {ethers.utils.formatUnits(latestGasData.answer, "gwei")} Gwei + </span> + </span> + </> + )} + </div> + )} + </> + ); +}; + +export default React.memo(PriceBox); diff --git a/src/Title.tsx b/src/Title.tsx index c8aab8f..c64fdbd 100644 --- a/src/Title.tsx +++ b/src/Title.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef } from "react"; import { Link, useHistory } from "react-router-dom"; import useKeyboardShortcut from "use-keyboard-shortcut"; +import PriceBox from "./PriceBox"; const Title: React.FC = () => { const [search, setSearch] = useState<string>(); @@ -41,27 +42,30 @@ const Title: React.FC = () => { <span>Otterscan</span> </div> </Link> - <form - className="flex" - onSubmit={handleSubmit} - autoComplete="off" - spellCheck={false} - > - <input - className="w-full border-t border-b border-l rounded-l focus:outline-none px-2 py-1 text-sm" - type="text" - size={60} - placeholder='Type "/" to search by address / txn hash / block number / ENS name' - onChange={handleChange} - ref={searchRef} - /> - <button - className="rounded-r border-t border-b border-r bg-gray-100 hover:bg-gray-200 focus:outline-none px-2 py-1 text-sm text-gray-500" - type="submit" + <div className="flex items-baseline space-x-3"> + <PriceBox /> + <form + className="flex" + onSubmit={handleSubmit} + autoComplete="off" + spellCheck={false} > - Search - </button> - </form> + <input + className="w-full border-t border-b border-l rounded-l focus:outline-none px-2 py-1 text-sm" + type="text" + size={60} + placeholder='Type "/" to search by address / txn hash / block number / ENS name' + onChange={handleChange} + ref={searchRef} + /> + <button + className="rounded-r border-t border-b border-r bg-gray-100 hover:bg-gray-200 focus:outline-none px-2 py-1 text-sm text-gray-500" + type="submit" + > + Search + </button> + </form> + </div> </div> ); }; From cd1991b0bc7cc03932cb3bc7f09b4d8d24d091a7 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 18:28:56 -0300 Subject: [PATCH 12/27] Add kudos to ethers --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b8d6220..ee1ba63 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,8 @@ To [Trust Wallet](https://github.com/trustwallet/assets) who sponsor and make av To the owners of the [4bytes repository](https://github.com/ethereum-lists/4bytes) that we import and use to translate the method selectors to human-friendly strings. +To [Ethers](https://github.com/ethers-io/ethers.js/) which is the client library we used to interact with the ETH node. It is high level enough to hide most jsonrpc particularities, but flexible enough to allow easy interaction with custom jsonrpc methods. + ## Future Erigon keeps evolving at a fast pace, with weekly releases, sometimes with (necessary) breaking changes. From a36749ee3d3721c29fa93581db0a8283a5306b5a Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 18:55:32 -0300 Subject: [PATCH 13/27] Apply value formatting --- src/PriceBox.tsx | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx index be78555..933d046 100644 --- a/src/PriceBox.tsx +++ b/src/PriceBox.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGasPump } from "@fortawesome/free-solid-svg-icons"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import { RuntimeContext } from "./useRuntime"; +import { formatValue } from "./components/formatter"; const ETH_FEED_DECIMALS = 8; @@ -44,29 +45,43 @@ const PriceBox: React.FC = () => { readData(); }, [ethFeed, gasFeed]); + const [latestPrice, timestamp] = useMemo(() => { + if (!latestPriceData) { + return [undefined, undefined]; + } + + const price = latestPriceData.answer.div(10 ** (ETH_FEED_DECIMALS - 2)); + const formattedPrice = ethers.utils.commify( + ethers.utils.formatUnits(price, 2) + ); + + const timestamp = new Date(latestPriceData.updatedAt * 1000); + return [formattedPrice, timestamp]; + }, [latestPriceData]); + + const latestGasPrice = useMemo(() => { + if (!latestGasData) { + return undefined; + } + return formatValue(latestGasData.answer, 9); + }, [latestGasData]); + return ( <> {latestPriceData && ( - <div className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800"> + <div + className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800" + title={`Updated at: ${timestamp?.toString()}`} + > <span> - Eth: $ - <span className="font-balance"> - {ethers.utils.commify( - ethers.utils.formatUnits( - latestPriceData.answer, - ETH_FEED_DECIMALS - ) - )} - </span> + Eth: $<span className="font-balance">{latestPrice}</span> </span> {latestGasData && ( <> <span>|</span> <span className="text-gray-400"> <FontAwesomeIcon icon={faGasPump} size="1x" /> - <span className="ml-1"> - {ethers.utils.formatUnits(latestGasData.answer, "gwei")} Gwei - </span> + <span className="ml-1">{latestGasPrice} Gwei</span> </span> </> )} From d2555f14cb4a220e7858d4512c2333fbee54bb4f Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 19:07:42 -0300 Subject: [PATCH 14/27] Display last eth/usd price and fast gas price updates separately on hover tooltip --- src/PriceBox.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx index 933d046..1598276 100644 --- a/src/PriceBox.tsx +++ b/src/PriceBox.tsx @@ -45,7 +45,7 @@ const PriceBox: React.FC = () => { readData(); }, [ethFeed, gasFeed]); - const [latestPrice, timestamp] = useMemo(() => { + const [latestPrice, latestPriceTimestamp] = useMemo(() => { if (!latestPriceData) { return [undefined, undefined]; } @@ -59,27 +59,32 @@ const PriceBox: React.FC = () => { return [formattedPrice, timestamp]; }, [latestPriceData]); - const latestGasPrice = useMemo(() => { + const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => { if (!latestGasData) { - return undefined; + return [undefined, undefined]; } - return formatValue(latestGasData.answer, 9); + + const formattedGas = formatValue(latestGasData.answer, 9); + const timestamp = new Date(latestGasData.updatedAt * 1000); + return [formattedGas, timestamp]; }, [latestGasData]); return ( <> {latestPriceData && ( - <div - className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800" - title={`Updated at: ${timestamp?.toString()}`} - > - <span> + <div className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800"> + <span + title={`ETH/USD last updated at: ${latestPriceTimestamp?.toString()}`} + > Eth: $<span className="font-balance">{latestPrice}</span> </span> {latestGasData && ( <> <span>|</span> - <span className="text-gray-400"> + <span + className="text-gray-400" + title={`Fast gas price last updated at: ${latestGasPriceTimestamp?.toString()}`} + > <FontAwesomeIcon icon={faGasPump} size="1x" /> <span className="ml-1">{latestGasPrice} Gwei</span> </span> From 2c650f06f7754a99db6f2d462e7d4105d70124bb Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Tue, 13 Jul 2021 19:23:39 -0300 Subject: [PATCH 15/27] Signal possibly outdated feed info based on the tip of the chain --- src/PriceBox.tsx | 12 +++++++++++- tailwind.config.js | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx index 1598276..af5e461 100644 --- a/src/PriceBox.tsx +++ b/src/PriceBox.tsx @@ -5,11 +5,17 @@ import { faGasPump } from "@fortawesome/free-solid-svg-icons"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import { RuntimeContext } from "./useRuntime"; import { formatValue } from "./components/formatter"; +import { useLatestBlock } from "./useLatestBlock"; const ETH_FEED_DECIMALS = 8; const PriceBox: React.FC = () => { const { provider } = useContext(RuntimeContext); + const latestBlock = useLatestBlock(provider); + + const maybeOutdated: boolean = + latestBlock !== undefined && + Date.now() / 1000 - latestBlock.timestamp > 3600; const ethFeed = useMemo( () => provider && @@ -72,7 +78,11 @@ const PriceBox: React.FC = () => { return ( <> {latestPriceData && ( - <div className="flex rounded-lg px-2 py-1 space-x-2 bg-gray-100 font-sans text-xs text-gray-800"> + <div + className={`flex rounded-lg px-2 py-1 space-x-2 ${ + maybeOutdated ? "bg-orange-200" : "bg-gray-100" + } font-sans text-xs text-gray-800`} + > <span title={`ETH/USD last updated at: ${latestPriceTimestamp?.toString()}`} > diff --git a/tailwind.config.js b/tailwind.config.js index 6db5164..3f72cce 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,3 +1,5 @@ +const colors = require("tailwindcss/colors"); + module.exports = { purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], darkMode: false, // or 'media' or 'class' @@ -6,6 +8,7 @@ module.exports = { colors: { "link-blue": "#3498db", "link-blue-hover": "#0468ab", + orange: colors.orange, }, fontFamily: { sans: ["Roboto"], From 05719eb4ae2aeb30ad5d643203c4ea5fa611c37d Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 03:52:31 -0300 Subject: [PATCH 16/27] Add selection support for addresses --- src/AddressTransactions.tsx | 7 +++-- src/BlockTransactions.tsx | 7 +++-- src/search/TransactionItem.tsx | 57 +++++++++++++++++++++++++++------- src/useSelection.ts | 23 ++++++++++++++ 4 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 src/useSelection.ts diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index e671087..076d9be 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -15,6 +15,7 @@ import { SearchController } from "./search/search"; import { RuntimeContext } from "./useRuntime"; import { useENSCache } from "./useReverseCache"; import { useFeeToggler } from "./search/useFeeToggler"; +import { SelectionContext, useSelection } from "./useSelection"; type BlockParams = { addressOrName: string; @@ -153,6 +154,8 @@ const AddressTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); + const selection = useSelection(); + return ( <StandardFrame> {error ? ( @@ -204,7 +207,7 @@ const AddressTransactions: React.FC = () => { feeDisplayToggler={feeDisplayToggler} /> {controller ? ( - <> + <SelectionContext.Provider value={selection}> {controller.getPage().map((tx) => ( <TransactionItem key={tx.hash} @@ -228,7 +231,7 @@ const AddressTransactions: React.FC = () => { nextHash={page ? page[page.length - 1].hash : ""} /> </div> - </> + </SelectionContext.Provider> ) : ( <PendingResults /> )} diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index 8f8f474..e8cbc52 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -15,6 +15,7 @@ import { PAGE_SIZE } from "./params"; import { useFeeToggler } from "./search/useFeeToggler"; import { RuntimeContext } from "./useRuntime"; import { useENSCache } from "./useReverseCache"; +import { SelectionContext, useSelection } from "./useSelection"; type BlockParams = { blockNumber: string; @@ -116,6 +117,8 @@ const BlockTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); + const selection = useSelection(); + return ( <StandardFrame> <StandardSubtitle>Transactions</StandardSubtitle> @@ -142,7 +145,7 @@ const BlockTransactions: React.FC = () => { feeDisplayToggler={feeDisplayToggler} /> {page ? ( - <> + <SelectionContext.Provider value={selection}> {page.map((tx) => ( <TransactionItem key={tx.hash} @@ -161,7 +164,7 @@ const BlockTransactions: React.FC = () => { total={total} /> </div> - </> + </SelectionContext.Provider> ) : ( <PendingResults /> )} diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 7cfaf76..516554f 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -14,6 +14,7 @@ import TransactionValue from "../components/TransactionValue"; import { ENSReverseCache, ProcessedTransaction } from "../types"; import { FeeDisplay } from "./useFeeToggler"; import { formatValue } from "../components/formatter"; +import { useSelectionContext } from "../useSelection"; type TransactionItemProps = { tx: ProcessedTransaction; @@ -45,6 +46,14 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ const ensTo = ensCache && tx.to && ensCache[tx.to]; const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; + const [selection, setSelection] = useSelectionContext(); + const select = (address: string) => { + setSelection({ type: "address", content: address }); + }; + const deselect = () => { + setSelection(null); + }; + return ( <div className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${ @@ -69,12 +78,24 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ <span className="col-span-2 flex justify-between items-baseline space-x-2 pr-2"> <span className="truncate" title={tx.from}> {tx.from && ( - <AddressOrENSName - address={tx.from} - ensName={ensFrom} - selectedAddress={selectedAddress} - minerAddress={tx.miner} - /> + <div + className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ + selection !== null && + selection.type === "address" && + selection.content === tx.from + ? "border-orange-400 bg-yellow-100" + : "border-transparent" + }`} + onMouseEnter={() => select(tx.from!)} + onMouseLeave={deselect} + > + <AddressOrENSName + address={tx.from} + ensName={ensFrom} + selectedAddress={selectedAddress} + minerAddress={tx.miner} + /> + </div> )} </span> <span> @@ -86,12 +107,24 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ </span> <span className="col-span-2 truncate" title={tx.to}> {tx.to && ( - <AddressOrENSName - address={tx.to} - ensName={ensTo} - selectedAddress={selectedAddress} - minerAddress={tx.miner} - /> + <div + className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ + selection !== null && + selection.type === "address" && + selection.content === tx.to + ? "border-orange-400 bg-yellow-100" + : "border-transparent" + }`} + onMouseEnter={() => select(tx.to!)} + onMouseLeave={deselect} + > + <AddressOrENSName + address={tx.to} + ensName={ensTo} + selectedAddress={selectedAddress} + minerAddress={tx.miner} + /> + </div> )} </span> <span className="col-span-2 truncate"> diff --git a/src/useSelection.ts b/src/useSelection.ts new file mode 100644 index 0000000..ed66746 --- /dev/null +++ b/src/useSelection.ts @@ -0,0 +1,23 @@ +import React, { useState, useContext } from "react"; + +export type Selection = { + type: string; + content: string; +}; + +export const useSelection = (): [ + Selection | null, + React.Dispatch<React.SetStateAction<Selection | null>> +] => { + const [selection, setSelection] = useState<Selection | null>(null); + return [selection, setSelection]; +}; + +export const SelectionContext = React.createContext< + ReturnType<typeof useSelection> +>(null!); + +export const useSelectionContext = () => { + const ctx = useContext(SelectionContext); + return ctx; +}; From 4b3ab2ec8aa342a018e979180e8db0c0306fdb9e Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 04:24:28 -0300 Subject: [PATCH 17/27] Extract highlighter decorator component --- src/AddressTransactions.tsx | 4 +-- src/BlockTransactions.tsx | 4 +-- src/components/AddressHighlighter.tsx | 37 ++++++++++++++++++++++++++ src/search/TransactionItem.tsx | 38 ++++----------------------- 4 files changed, 46 insertions(+), 37 deletions(-) create mode 100644 src/components/AddressHighlighter.tsx diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index 076d9be..4727f7c 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -154,7 +154,7 @@ const AddressTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); - const selection = useSelection(); + const selectionCtx = useSelection(); return ( <StandardFrame> @@ -207,7 +207,7 @@ const AddressTransactions: React.FC = () => { feeDisplayToggler={feeDisplayToggler} /> {controller ? ( - <SelectionContext.Provider value={selection}> + <SelectionContext.Provider value={selectionCtx}> {controller.getPage().map((tx) => ( <TransactionItem key={tx.hash} diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index e8cbc52..7578d31 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -117,7 +117,7 @@ const BlockTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); - const selection = useSelection(); + const selectionCtx = useSelection(); return ( <StandardFrame> @@ -145,7 +145,7 @@ const BlockTransactions: React.FC = () => { feeDisplayToggler={feeDisplayToggler} /> {page ? ( - <SelectionContext.Provider value={selection}> + <SelectionContext.Provider value={selectionCtx}> {page.map((tx) => ( <TransactionItem key={tx.hash} diff --git a/src/components/AddressHighlighter.tsx b/src/components/AddressHighlighter.tsx new file mode 100644 index 0000000..78524a7 --- /dev/null +++ b/src/components/AddressHighlighter.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useSelectionContext } from "../useSelection"; + +type AddressHighlighterProps = { + address: string; +}; + +const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ + address, + children, +}) => { + const [selection, setSelection] = useSelectionContext(); + const select = () => { + setSelection({ type: "address", content: address }); + }; + const deselect = () => { + setSelection(null); + }; + + return ( + <div + className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ + selection !== null && + selection.type === "address" && + selection.content === address + ? "border-orange-400 bg-yellow-100" + : "border-transparent" + }`} + onMouseEnter={select} + onMouseLeave={deselect} + > + {children} + </div> + ); +}; + +export default AddressHighlighter; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 516554f..ca4fbed 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -6,6 +6,7 @@ import BlockLink from "../components/BlockLink"; import TransactionLink from "../components/TransactionLink"; import AddressOrENSName from "../components/AddressOrENSName"; import TimestampAge from "../components/TimestampAge"; +import AddressHighlighter from "../components/AddressHighlighter"; import TransactionDirection, { Direction, Flags, @@ -14,7 +15,6 @@ import TransactionValue from "../components/TransactionValue"; import { ENSReverseCache, ProcessedTransaction } from "../types"; import { FeeDisplay } from "./useFeeToggler"; import { formatValue } from "../components/formatter"; -import { useSelectionContext } from "../useSelection"; type TransactionItemProps = { tx: ProcessedTransaction; @@ -46,14 +46,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ const ensTo = ensCache && tx.to && ensCache[tx.to]; const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; - const [selection, setSelection] = useSelectionContext(); - const select = (address: string) => { - setSelection({ type: "address", content: address }); - }; - const deselect = () => { - setSelection(null); - }; - return ( <div className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${ @@ -78,24 +70,14 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ <span className="col-span-2 flex justify-between items-baseline space-x-2 pr-2"> <span className="truncate" title={tx.from}> {tx.from && ( - <div - className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ - selection !== null && - selection.type === "address" && - selection.content === tx.from - ? "border-orange-400 bg-yellow-100" - : "border-transparent" - }`} - onMouseEnter={() => select(tx.from!)} - onMouseLeave={deselect} - > + <AddressHighlighter address={tx.from}> <AddressOrENSName address={tx.from} ensName={ensFrom} selectedAddress={selectedAddress} minerAddress={tx.miner} /> - </div> + </AddressHighlighter> )} </span> <span> @@ -107,24 +89,14 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ </span> <span className="col-span-2 truncate" title={tx.to}> {tx.to && ( - <div - className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ - selection !== null && - selection.type === "address" && - selection.content === tx.to - ? "border-orange-400 bg-yellow-100" - : "border-transparent" - }`} - onMouseEnter={() => select(tx.to!)} - onMouseLeave={deselect} - > + <AddressHighlighter address={tx.to}> <AddressOrENSName address={tx.to} ensName={ensTo} selectedAddress={selectedAddress} minerAddress={tx.miner} /> - </div> + </AddressHighlighter> )} </span> <span className="col-span-2 truncate"> From e2e94a4d30183d33aed00a94e928bdf7d6771223 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 04:43:58 -0300 Subject: [PATCH 18/27] Apply highlighter decorator to transaction details page --- src/TokenTransferItem.tsx | 11 ++++-- src/Transaction.tsx | 28 +++++++++----- src/components/AddressHighlighter.tsx | 2 +- src/components/InternalTransfer.tsx | 53 +++++++++++++++------------ 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 64a42f6..b341f0d 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -1,8 +1,9 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons"; -import AddressLink from "./components/AddressLink"; +import AddressHighlighter from "./components/AddressHighlighter"; import AddressOrENSName from "./components/AddressOrENSName"; +import AddressLink from "./components/AddressLink"; import TokenLogo from "./components/TokenLogo"; import FormattedBalance from "./components/FormattedBalance"; import { TokenMetas, TokenTransfer } from "./types"; @@ -23,11 +24,15 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ <div className="grid grid-cols-5"> <div className="flex space-x-2"> <span className="font-bold">From</span> - <AddressOrENSName address={t.from} /> + <AddressHighlighter address={t.from}> + <AddressOrENSName address={t.from} /> + </AddressHighlighter> </div> <div className="flex space-x-2"> <span className="font-bold">To</span> - <AddressOrENSName address={t.to} /> + <AddressHighlighter address={t.to}> + <AddressOrENSName address={t.to} /> + </AddressHighlighter> </div> <div className="col-span-3 flex space-x-2"> <span className="font-bold">For</span> diff --git a/src/Transaction.tsx b/src/Transaction.tsx index e2b6390..4235c3e 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -17,6 +17,7 @@ import StandardSubtitle from "./StandardSubtitle"; import Tab from "./components/Tab"; import ContentFrame from "./ContentFrame"; import BlockLink from "./components/BlockLink"; +import AddressHighlighter from "./components/AddressHighlighter"; import AddressOrENSName from "./components/AddressOrENSName"; import AddressLink from "./components/AddressLink"; import Copy from "./components/Copy"; @@ -29,6 +30,7 @@ import TokenTransferItem from "./TokenTransferItem"; import erc20 from "./erc20.json"; import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types"; import { RuntimeContext } from "./useRuntime"; +import { SelectionContext, useSelection } from "./useSelection"; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; @@ -160,11 +162,13 @@ const Transaction: React.FC = () => { traceTransfersUsingOtsTrace(); }, [traceTransfersUsingOtsTrace]); + const selectionCtx = useSelection(); + return ( <StandardFrame> <StandardSubtitle>Transaction Details</StandardSubtitle> {txData && ( - <> + <SelectionContext.Provider value={selectionCtx}> <div className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white"> <Tab href={`/tx/${txhash}`}>Overview</Tab> <Tab href={`/tx/${txhash}/logs`}> @@ -206,19 +210,23 @@ const Transaction: React.FC = () => { </InfoRow> <InfoRow title="From"> <div className="flex items-baseline space-x-2"> - <AddressOrENSName - address={txData.from} - minerAddress={txData.miner} - /> + <AddressHighlighter address={txData.from}> + <AddressOrENSName + address={txData.from} + minerAddress={txData.miner} + /> + </AddressHighlighter> <Copy value={txData.from} /> </div> </InfoRow> <InfoRow title="Interacted With (To)"> <div className="flex items-baseline space-x-2"> - <AddressOrENSName - address={txData.to} - minerAddress={txData.miner} - /> + <AddressHighlighter address={txData.to}> + <AddressOrENSName + address={txData.to} + minerAddress={txData.miner} + /> + </AddressHighlighter> <Copy value={txData.to} /> </div> {transfers && ( @@ -350,7 +358,7 @@ const Transaction: React.FC = () => { </ContentFrame> </Route> </Switch> - </> + </SelectionContext.Provider> )} </StandardFrame> ); diff --git a/src/components/AddressHighlighter.tsx b/src/components/AddressHighlighter.tsx index 78524a7..114157c 100644 --- a/src/components/AddressHighlighter.tsx +++ b/src/components/AddressHighlighter.tsx @@ -19,7 +19,7 @@ const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ return ( <div - className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 ${ + className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${ selection !== null && selection.type === "address" && selection.content === address diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index ac8f7d7..e2f19d1 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -2,6 +2,7 @@ import React from "react"; import { ethers } from "ethers"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleRight, faCoins } from "@fortawesome/free-solid-svg-icons"; +import AddressHighlighter from "./AddressHighlighter"; import AddressLink from "./AddressLink"; import { TransactionData, Transfer } from "../types"; @@ -25,31 +26,35 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({ </span> <span>{ethers.utils.formatEther(transfer.value)} Ether</span> <span className="text-gray-500">From</span> - <div - className={`flex items-baseline space-x-1 ${ - fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" - }`} - > - {fromMiner && ( - <span className="text-yellow-400" title="Miner address"> - <FontAwesomeIcon icon={faCoins} size="1x" /> - </span> - )} - <AddressLink address={transfer.from} /> - </div> + <AddressHighlighter address={transfer.from}> + <div + className={`flex items-baseline space-x-1 ${ + fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" + }`} + > + {fromMiner && ( + <span className="text-yellow-400" title="Miner address"> + <FontAwesomeIcon icon={faCoins} size="1x" /> + </span> + )} + <AddressLink address={transfer.from} /> + </div> + </AddressHighlighter> <span className="text-gray-500">To</span> - <div - className={`flex items-baseline space-x-1 px-2 py-1 ${ - toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" - }`} - > - {toMiner && ( - <span className="text-yellow-400" title="Miner address"> - <FontAwesomeIcon icon={faCoins} size="1x" /> - </span> - )} - <AddressLink address={transfer.to} /> - </div> + <AddressHighlighter address={transfer.to}> + <div + className={`flex items-baseline space-x-1 px-2 py-1 ${ + toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" + }`} + > + {toMiner && ( + <span className="text-yellow-400" title="Miner address"> + <FontAwesomeIcon icon={faCoins} size="1x" /> + </span> + )} + <AddressLink address={transfer.to} /> + </div> + </AddressHighlighter> </div> ); }; From f3213b3c6939eaf66ff5472b29a29e01a6ea5338 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 15:35:53 -0300 Subject: [PATCH 19/27] Fix spacing and alignment due to addresss highlight feature --- src/TokenTransferItem.tsx | 8 ++-- src/Transaction.tsx | 4 +- src/components/InternalTransfer.tsx | 64 +++++++++++++++-------------- src/search/ResultHeader.tsx | 4 +- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index b341f0d..4b1fc62 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -21,20 +21,20 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ <span className="text-gray-500"> <FontAwesomeIcon icon={faCaretRight} size="1x" /> </span> - <div className="grid grid-cols-5"> - <div className="flex space-x-2"> + <div className="grid grid-cols-5 gap-x-1"> + <div className="flex space-x-1"> <span className="font-bold">From</span> <AddressHighlighter address={t.from}> <AddressOrENSName address={t.from} /> </AddressHighlighter> </div> - <div className="flex space-x-2"> + <div className="flex space-x-1"> <span className="font-bold">To</span> <AddressHighlighter address={t.to}> <AddressOrENSName address={t.to} /> </AddressHighlighter> </div> - <div className="col-span-3 flex space-x-2"> + <div className="col-span-3 flex space-x-1"> <span className="font-bold">For</span> <span> <FormattedBalance diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 4235c3e..6c646fb 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -209,7 +209,7 @@ const Transaction: React.FC = () => { <Timestamp value={txData.timestamp} /> </InfoRow> <InfoRow title="From"> - <div className="flex items-baseline space-x-2"> + <div className="flex items-baseline space-x-2 -ml-1"> <AddressHighlighter address={txData.from}> <AddressOrENSName address={txData.from} @@ -220,7 +220,7 @@ const Transaction: React.FC = () => { </div> </InfoRow> <InfoRow title="Interacted With (To)"> - <div className="flex items-baseline space-x-2"> + <div className="flex items-baseline space-x-2 -ml-1"> <AddressHighlighter address={txData.to}> <AddressOrENSName address={txData.to} diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index e2f19d1..c135a43 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -25,36 +25,40 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({ <FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER </span> <span>{ethers.utils.formatEther(transfer.value)} Ether</span> - <span className="text-gray-500">From</span> - <AddressHighlighter address={transfer.from}> - <div - className={`flex items-baseline space-x-1 ${ - fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" - }`} - > - {fromMiner && ( - <span className="text-yellow-400" title="Miner address"> - <FontAwesomeIcon icon={faCoins} size="1x" /> - </span> - )} - <AddressLink address={transfer.from} /> - </div> - </AddressHighlighter> - <span className="text-gray-500">To</span> - <AddressHighlighter address={transfer.to}> - <div - className={`flex items-baseline space-x-1 px-2 py-1 ${ - toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" - }`} - > - {toMiner && ( - <span className="text-yellow-400" title="Miner address"> - <FontAwesomeIcon icon={faCoins} size="1x" /> - </span> - )} - <AddressLink address={transfer.to} /> - </div> - </AddressHighlighter> + <div className="flex items-baseline"> + <span className="text-gray-500">From</span> + <AddressHighlighter address={transfer.from}> + <div + className={`flex items-baseline space-x-1 ${ + fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" + }`} + > + {fromMiner && ( + <span className="text-yellow-400" title="Miner address"> + <FontAwesomeIcon icon={faCoins} size="1x" /> + </span> + )} + <AddressLink address={transfer.from} /> + </div> + </AddressHighlighter> + </div> + <div className="flex items-baseline"> + <span className="text-gray-500">To</span> + <AddressHighlighter address={transfer.to}> + <div + className={`flex items-baseline space-x-1 ${ + toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" + }`} + > + {toMiner && ( + <span className="text-yellow-400" title="Miner address"> + <FontAwesomeIcon icon={faCoins} size="1x" /> + </span> + )} + <AddressLink address={transfer.to} /> + </div> + </AddressHighlighter> + </div> </div> ); }; diff --git a/src/search/ResultHeader.tsx b/src/search/ResultHeader.tsx index 0b63c90..13880bd 100644 --- a/src/search/ResultHeader.tsx +++ b/src/search/ResultHeader.tsx @@ -15,8 +15,8 @@ const ResultHeader: React.FC<ResultHeaderProps> = ({ <div>Method</div> <div>Block</div> <div>Age</div> - <div className="col-span-2">From</div> - <div className="col-span-2">To</div> + <div className="col-span-2 ml-1">From</div> + <div className="col-span-2 ml-1">To</div> <div className="col-span-2">Value</div> <div> <button From 4c72fd6966edec1a1414375cabb7b96465b44587 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 16:17:27 -0300 Subject: [PATCH 20/27] Small component extraction --- src/Transaction.tsx | 12 +----------- src/components/AddressHighlighter.tsx | 6 +++--- src/components/InfoRow.tsx | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 src/components/InfoRow.tsx diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 6c646fb..e18a163 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -16,6 +16,7 @@ import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import Tab from "./components/Tab"; import ContentFrame from "./ContentFrame"; +import InfoRow from "./components/InfoRow"; import BlockLink from "./components/BlockLink"; import AddressHighlighter from "./components/AddressHighlighter"; import AddressOrENSName from "./components/AddressOrENSName"; @@ -364,15 +365,4 @@ const Transaction: React.FC = () => { ); }; -type InfoRowProps = { - title: string; -}; - -const InfoRow: React.FC<InfoRowProps> = ({ title, children }) => ( - <div className="grid grid-cols-4 py-4 text-sm"> - <div>{title}:</div> - <div className="col-span-3">{children}</div> - </div> -); - export default React.memo(Transaction); diff --git a/src/components/AddressHighlighter.tsx b/src/components/AddressHighlighter.tsx index 114157c..1b7df0d 100644 --- a/src/components/AddressHighlighter.tsx +++ b/src/components/AddressHighlighter.tsx @@ -1,9 +1,9 @@ import React from "react"; import { useSelectionContext } from "../useSelection"; -type AddressHighlighterProps = { +type AddressHighlighterProps = React.PropsWithChildren<{ address: string; -}; +}>; const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ address, @@ -34,4 +34,4 @@ const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ ); }; -export default AddressHighlighter; +export default React.memo(AddressHighlighter); diff --git a/src/components/InfoRow.tsx b/src/components/InfoRow.tsx new file mode 100644 index 0000000..d3758b3 --- /dev/null +++ b/src/components/InfoRow.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +type InfoRowProps = React.PropsWithChildren<{ + title: string; +}>; + +const InfoRow: React.FC<InfoRowProps> = ({ title, children }) => ( + <div className="grid grid-cols-4 py-4 text-sm"> + <div>{title}:</div> + <div className="col-span-3">{children}</div> + </div> +); + +export default React.memo(InfoRow); From 1e095b6dd3bacce0819519f572d0061974335d6b Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 16:30:28 -0300 Subject: [PATCH 21/27] Extract subcomponents --- src/Transaction.tsx | 209 ++---------------------------------- src/transaction/Details.tsx | 144 +++++++++++++++++++++++++ src/transaction/Logs.tsx | 57 ++++++++++ 3 files changed, 212 insertions(+), 198 deletions(-) create mode 100644 src/transaction/Details.tsx create mode 100644 src/transaction/Logs.tsx diff --git a/src/Transaction.tsx b/src/Transaction.tsx index e18a163..3f450e9 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -7,27 +7,11 @@ import React, { } from "react"; import { Route, Switch, useParams } from "react-router-dom"; import { BigNumber, ethers } from "ethers"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faCheckCircle, - faTimesCircle, -} from "@fortawesome/free-solid-svg-icons"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import Tab from "./components/Tab"; -import ContentFrame from "./ContentFrame"; -import InfoRow from "./components/InfoRow"; -import BlockLink from "./components/BlockLink"; -import AddressHighlighter from "./components/AddressHighlighter"; -import AddressOrENSName from "./components/AddressOrENSName"; -import AddressLink from "./components/AddressLink"; -import Copy from "./components/Copy"; -import Timestamp from "./components/Timestamp"; -import InternalTransfer from "./components/InternalTransfer"; -import MethodName from "./components/MethodName"; -import GasValue from "./components/GasValue"; -import FormattedBalance from "./components/FormattedBalance"; -import TokenTransferItem from "./TokenTransferItem"; +import Details from "./transaction/Details"; +import Logs from "./transaction/Logs"; import erc20 from "./erc20.json"; import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types"; import { RuntimeContext } from "./useRuntime"; @@ -140,7 +124,7 @@ const Transaction: React.FC = () => { return false; }, [txData, transfers]); - const traceTransfersUsingOtsTrace = useCallback(async () => { + const traceTransfers = useCallback(async () => { if (!provider || !txData) { return; } @@ -160,8 +144,8 @@ const Transaction: React.FC = () => { setTransfers(_transfers); }, [provider, txData]); useEffect(() => { - traceTransfersUsingOtsTrace(); - }, [traceTransfersUsingOtsTrace]); + traceTransfers(); + }, [traceTransfers]); const selectionCtx = useSelection(); @@ -178,185 +162,14 @@ const Transaction: React.FC = () => { </div> <Switch> <Route path="/tx/:txhash/" exact> - <ContentFrame tabs> - <InfoRow title="Transaction Hash"> - <div className="flex items-baseline space-x-2"> - <span className="font-hash">{txData.transactionHash}</span> - <Copy value={txData.transactionHash} /> - </div> - </InfoRow> - <InfoRow title="Status"> - {txData.status ? ( - <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> - <FontAwesomeIcon icon={faCheckCircle} size="1x" /> - <span>Success</span> - </span> - ) : ( - <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-red-50 text-red-500 text-xs"> - <FontAwesomeIcon icon={faTimesCircle} size="1x" /> - <span>Fail</span> - </span> - )} - </InfoRow> - <InfoRow title="Block"> - <div className="flex items-baseline space-x-2"> - <BlockLink blockTag={txData.blockNumber} /> - <span className="rounded text-xs bg-gray-100 text-gray-500 px-2 py-1"> - {txData.confirmations} Block Confirmations - </span> - </div> - </InfoRow> - <InfoRow title="Timestamp"> - <Timestamp value={txData.timestamp} /> - </InfoRow> - <InfoRow title="From"> - <div className="flex items-baseline space-x-2 -ml-1"> - <AddressHighlighter address={txData.from}> - <AddressOrENSName - address={txData.from} - minerAddress={txData.miner} - /> - </AddressHighlighter> - <Copy value={txData.from} /> - </div> - </InfoRow> - <InfoRow title="Interacted With (To)"> - <div className="flex items-baseline space-x-2 -ml-1"> - <AddressHighlighter address={txData.to}> - <AddressOrENSName - address={txData.to} - minerAddress={txData.miner} - /> - </AddressHighlighter> - <Copy value={txData.to} /> - </div> - {transfers && ( - <div className="mt-2 space-y-1"> - {transfers.map((t, i) => ( - <InternalTransfer - key={i} - txData={txData} - transfer={t} - /> - ))} - </div> - )} - </InfoRow> - <InfoRow title="Transaction Action"> - <MethodName data={txData.data} /> - </InfoRow> - {txData.tokenTransfers.length > 0 && ( - <InfoRow - title={`Tokens Transferred (${txData.tokenTransfers.length})`} - > - <div className="space-y-2"> - {txData.tokenTransfers.map((t, i) => ( - <TokenTransferItem - key={i} - t={t} - tokenMetas={txData.tokenMetas} - /> - ))} - </div> - </InfoRow> - )} - <InfoRow title="Value"> - <span className="rounded bg-gray-100 px-2 py-1 text-xs"> - {ethers.utils.formatEther(txData.value)} Ether - </span> - </InfoRow> - <InfoRow title="Transaction Fee"> - <FormattedBalance value={txData.fee} /> Ether - </InfoRow> - <InfoRow title="Gas Price"> - <div className="flex items-baseline space-x-1"> - <span> - <FormattedBalance value={txData.gasPrice} /> Ether ( - <FormattedBalance - value={txData.gasPrice} - decimals={9} - />{" "} - Gwei) - </span> - {sendsEthToMiner && ( - <span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1"> - Flashbots - </span> - )} - </div> - </InfoRow> - <InfoRow title="Ether Price">N/A</InfoRow> - <InfoRow title="Gas Limit"> - <GasValue value={txData.gasLimit} /> - </InfoRow> - <InfoRow title="Gas Used by Transaction"> - <GasValue value={txData.gasUsed} /> ( - {(txData.gasUsedPerc * 100).toFixed(2)}%) - </InfoRow> - <InfoRow title="Nonce">{txData.nonce}</InfoRow> - <InfoRow title="Position in Block"> - <span className="rounded px-2 py-1 bg-gray-100 text-gray-500 text-xs"> - {txData.transactionIndex} - </span> - </InfoRow> - <InfoRow title="Input Data"> - <textarea - className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" - value={txData.data} - readOnly - /> - </InfoRow> - </ContentFrame> + <Details + txData={txData} + transfers={transfers} + sendsEthToMiner={sendsEthToMiner} + /> </Route> <Route path="/tx/:txhash/logs/" exact> - <ContentFrame tabs> - <div className="text-sm py-4"> - Transaction Receipt Event Logs - </div> - {txData && - txData.logs.map((l, i) => ( - <div className="flex space-x-10 py-5" key={i}> - <div> - <span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> - {l.logIndex} - </span> - </div> - <div className="w-full space-y-2"> - <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> - <div className="font-bold text-right">Address</div> - <div className="col-span-11"> - <AddressLink address={l.address} /> - </div> - </div> - {l.topics.map((t, i) => ( - <div - className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm" - key={i} - > - <div className="text-right"> - {i === 0 && "Topics"} - </div> - <div className="flex space-x-2 items-center col-span-11 font-mono"> - <span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs"> - {i} - </span> - <span>{t}</span> - </div> - </div> - ))} - <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> - <div className="text-right pt-2">Data</div> - <div className="col-span-11"> - <textarea - className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2" - value={l.data} - /> - </div> - </div> - </div> - </div> - ))} - </ContentFrame> + <Logs txData={txData} /> </Route> </Switch> </SelectionContext.Provider> diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx new file mode 100644 index 0000000..f5022da --- /dev/null +++ b/src/transaction/Details.tsx @@ -0,0 +1,144 @@ +import React from "react"; +import { ethers } from "ethers"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCheckCircle, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons"; +import ContentFrame from "../ContentFrame"; +import InfoRow from "../components/InfoRow"; +import BlockLink from "../components/BlockLink"; +import AddressHighlighter from "../components/AddressHighlighter"; +import AddressOrENSName from "../components/AddressOrENSName"; +import Copy from "../components/Copy"; +import Timestamp from "../components/Timestamp"; +import InternalTransfer from "../components/InternalTransfer"; +import MethodName from "../components/MethodName"; +import GasValue from "../components/GasValue"; +import FormattedBalance from "../components/FormattedBalance"; +import TokenTransferItem from "../TokenTransferItem"; +import { TransactionData, Transfer } from "../types"; + +type DetailsProps = { + txData: TransactionData; + transfers?: Transfer[]; + sendsEthToMiner: boolean; +}; + +const Details: React.FC<DetailsProps> = ({ + txData, + transfers, + sendsEthToMiner, +}) => ( + <ContentFrame tabs> + <InfoRow title="Transaction Hash"> + <div className="flex items-baseline space-x-2"> + <span className="font-hash">{txData.transactionHash}</span> + <Copy value={txData.transactionHash} /> + </div> + </InfoRow> + <InfoRow title="Status"> + {txData.status ? ( + <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> + <FontAwesomeIcon icon={faCheckCircle} size="1x" /> + <span>Success</span> + </span> + ) : ( + <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-red-50 text-red-500 text-xs"> + <FontAwesomeIcon icon={faTimesCircle} size="1x" /> + <span>Fail</span> + </span> + )} + </InfoRow> + <InfoRow title="Block"> + <div className="flex items-baseline space-x-2"> + <BlockLink blockTag={txData.blockNumber} /> + <span className="rounded text-xs bg-gray-100 text-gray-500 px-2 py-1"> + {txData.confirmations} Block Confirmations + </span> + </div> + </InfoRow> + <InfoRow title="Timestamp"> + <Timestamp value={txData.timestamp} /> + </InfoRow> + <InfoRow title="From"> + <div className="flex items-baseline space-x-2 -ml-1"> + <AddressHighlighter address={txData.from}> + <AddressOrENSName address={txData.from} minerAddress={txData.miner} /> + </AddressHighlighter> + <Copy value={txData.from} /> + </div> + </InfoRow> + <InfoRow title="Interacted With (To)"> + <div className="flex items-baseline space-x-2 -ml-1"> + <AddressHighlighter address={txData.to}> + <AddressOrENSName address={txData.to} minerAddress={txData.miner} /> + </AddressHighlighter> + <Copy value={txData.to} /> + </div> + {transfers && ( + <div className="mt-2 space-y-1"> + {transfers.map((t, i) => ( + <InternalTransfer key={i} txData={txData} transfer={t} /> + ))} + </div> + )} + </InfoRow> + <InfoRow title="Transaction Action"> + <MethodName data={txData.data} /> + </InfoRow> + {txData.tokenTransfers.length > 0 && ( + <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> + <div className="space-y-2"> + {txData.tokenTransfers.map((t, i) => ( + <TokenTransferItem key={i} t={t} tokenMetas={txData.tokenMetas} /> + ))} + </div> + </InfoRow> + )} + <InfoRow title="Value"> + <span className="rounded bg-gray-100 px-2 py-1 text-xs"> + {ethers.utils.formatEther(txData.value)} Ether + </span> + </InfoRow> + <InfoRow title="Transaction Fee"> + <FormattedBalance value={txData.fee} /> Ether + </InfoRow> + <InfoRow title="Gas Price"> + <div className="flex items-baseline space-x-1"> + <span> + <FormattedBalance value={txData.gasPrice} /> Ether ( + <FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei) + </span> + {sendsEthToMiner && ( + <span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1"> + Flashbots + </span> + )} + </div> + </InfoRow> + <InfoRow title="Ether Price">N/A</InfoRow> + <InfoRow title="Gas Limit"> + <GasValue value={txData.gasLimit} /> + </InfoRow> + <InfoRow title="Gas Used by Transaction"> + <GasValue value={txData.gasUsed} /> ( + {(txData.gasUsedPerc * 100).toFixed(2)}%) + </InfoRow> + <InfoRow title="Nonce">{txData.nonce}</InfoRow> + <InfoRow title="Position in Block"> + <span className="rounded px-2 py-1 bg-gray-100 text-gray-500 text-xs"> + {txData.transactionIndex} + </span> + </InfoRow> + <InfoRow title="Input Data"> + <textarea + className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" + value={txData.data} + readOnly + /> + </InfoRow> + </ContentFrame> +); + +export default React.memo(Details); diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx new file mode 100644 index 0000000..eda12be --- /dev/null +++ b/src/transaction/Logs.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import ContentFrame from "../ContentFrame"; +import AddressLink from "../components/AddressLink"; +import { TransactionData } from "../types"; + +type LogsProps = { + txData: TransactionData; +}; + +const Logs: React.FC<LogsProps> = ({ txData }) => ( + <ContentFrame tabs> + <div className="text-sm py-4">Transaction Receipt Event Logs</div> + {txData && + txData.logs.map((l, i) => ( + <div className="flex space-x-10 py-5" key={i}> + <div> + <span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> + {l.logIndex} + </span> + </div> + <div className="w-full space-y-2"> + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="font-bold text-right">Address</div> + <div className="col-span-11"> + <AddressLink address={l.address} /> + </div> + </div> + {l.topics.map((t, i) => ( + <div + className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm" + key={i} + > + <div className="text-right">{i === 0 && "Topics"}</div> + <div className="flex space-x-2 items-center col-span-11 font-mono"> + <span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs"> + {i} + </span> + <span>{t}</span> + </div> + </div> + ))} + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="text-right pt-2">Data</div> + <div className="col-span-11"> + <textarea + className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2" + value={l.data} + /> + </div> + </div> + </div> + </div> + ))} + </ContentFrame> +); + +export default React.memo(Logs); From 8f51e6433a1d367ee177742a729cd9e0ff610bbf Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 16:57:08 -0300 Subject: [PATCH 22/27] Extract subcomponents --- src/BlockTransactionHeader.tsx | 21 +++++++++ src/BlockTransactionResults.tsx | 78 +++++++++++++++++++++++++++++++++ src/BlockTransactions.tsx | 72 ++++-------------------------- 3 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 src/BlockTransactionHeader.tsx create mode 100644 src/BlockTransactionResults.tsx diff --git a/src/BlockTransactionHeader.tsx b/src/BlockTransactionHeader.tsx new file mode 100644 index 0000000..65a8288 --- /dev/null +++ b/src/BlockTransactionHeader.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ethers } from "ethers"; +import StandardSubtitle from "./StandardSubtitle"; +import BlockLink from "./components/BlockLink"; + +type BlockTransactionHeaderProps = { + blockTag: ethers.providers.BlockTag; +}; + +const BlockTransactionHeader: React.FC<BlockTransactionHeaderProps> = ({ + blockTag, +}) => ( + <> + <StandardSubtitle>Transactions</StandardSubtitle> + <div className="pb-2 text-sm text-gray-500"> + For Block <BlockLink blockTag={blockTag} /> + </div> + </> +); + +export default React.memo(BlockTransactionHeader); diff --git a/src/BlockTransactionResults.tsx b/src/BlockTransactionResults.tsx new file mode 100644 index 0000000..74e2783 --- /dev/null +++ b/src/BlockTransactionResults.tsx @@ -0,0 +1,78 @@ +import React, { useContext } from "react"; +import ContentFrame from "./ContentFrame"; +import PageControl from "./search/PageControl"; +import ResultHeader from "./search/ResultHeader"; +import PendingResults from "./search/PendingResults"; +import TransactionItem from "./search/TransactionItem"; +import { useFeeToggler } from "./search/useFeeToggler"; +import { RuntimeContext } from "./useRuntime"; +import { SelectionContext, useSelection } from "./useSelection"; +import { useENSCache } from "./useReverseCache"; +import { ProcessedTransaction } from "./types"; +import { PAGE_SIZE } from "./params"; + +type BlockTransactionResultsProps = { + page?: ProcessedTransaction[]; + total: number; + pageNumber: number; +}; + +const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({ + page, + total, + pageNumber, +}) => { + const selectionCtx = useSelection(); + const [feeDisplay, feeDisplayToggler] = useFeeToggler(); + const { provider } = useContext(RuntimeContext); + const reverseCache = useENSCache(provider, page); + + return ( + <ContentFrame> + <div className="flex justify-between items-baseline py-3"> + <div className="text-sm text-gray-500"> + {page === undefined ? ( + <>Waiting for search results...</> + ) : ( + <>A total of {total} transactions found</> + )} + </div> + <PageControl + pageNumber={pageNumber} + pageSize={PAGE_SIZE} + total={total} + /> + </div> + <ResultHeader + feeDisplay={feeDisplay} + feeDisplayToggler={feeDisplayToggler} + /> + {page ? ( + <SelectionContext.Provider value={selectionCtx}> + {page.map((tx) => ( + <TransactionItem + key={tx.hash} + tx={tx} + ensCache={reverseCache} + feeDisplay={feeDisplay} + /> + ))} + <div className="flex justify-between items-baseline py-3"> + <div className="text-sm text-gray-500"> + A total of {total} transactions found + </div> + <PageControl + pageNumber={pageNumber} + pageSize={PAGE_SIZE} + total={total} + /> + </div> + </SelectionContext.Provider> + ) : ( + <PendingResults /> + )} + </ContentFrame> + ); +}; + +export default React.memo(BlockTransactionResults); diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index 7578d31..8ded619 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -3,19 +3,11 @@ import { useParams, useLocation } from "react-router"; import { ethers } from "ethers"; import queryString from "query-string"; import StandardFrame from "./StandardFrame"; -import StandardSubtitle from "./StandardSubtitle"; -import ContentFrame from "./ContentFrame"; -import PageControl from "./search/PageControl"; -import ResultHeader from "./search/ResultHeader"; -import PendingResults from "./search/PendingResults"; -import TransactionItem from "./search/TransactionItem"; -import BlockLink from "./components/BlockLink"; +import BlockTransactionHeader from "./BlockTransactionHeader"; +import BlockTransactionResults from "./BlockTransactionResults"; import { ProcessedTransaction } from "./types"; import { PAGE_SIZE } from "./params"; -import { useFeeToggler } from "./search/useFeeToggler"; import { RuntimeContext } from "./useRuntime"; -import { useENSCache } from "./useReverseCache"; -import { SelectionContext, useSelection } from "./useSelection"; type BlockParams = { blockNumber: string; @@ -111,64 +103,16 @@ const BlockTransactions: React.FC = () => { }, [txs, pageNumber]); const total = useMemo(() => txs?.length ?? 0, [txs]); - const reverseCache = useENSCache(provider, page); - document.title = `Block #${blockNumber} Txns | Otterscan`; - const [feeDisplay, feeDisplayToggler] = useFeeToggler(); - - const selectionCtx = useSelection(); - return ( <StandardFrame> - <StandardSubtitle>Transactions</StandardSubtitle> - <div className="pb-2 text-sm text-gray-500"> - For Block <BlockLink blockTag={blockNumber.toNumber()} /> - </div> - <ContentFrame> - <div className="flex justify-between items-baseline py-3"> - <div className="text-sm text-gray-500"> - {page === undefined ? ( - <>Waiting for search results...</> - ) : ( - <>A total of {total} transactions found</> - )} - </div> - <PageControl - pageNumber={pageNumber} - pageSize={PAGE_SIZE} - total={total} - /> - </div> - <ResultHeader - feeDisplay={feeDisplay} - feeDisplayToggler={feeDisplayToggler} - /> - {page ? ( - <SelectionContext.Provider value={selectionCtx}> - {page.map((tx) => ( - <TransactionItem - key={tx.hash} - tx={tx} - ensCache={reverseCache} - feeDisplay={feeDisplay} - /> - ))} - <div className="flex justify-between items-baseline py-3"> - <div className="text-sm text-gray-500"> - A total of {total} transactions found - </div> - <PageControl - pageNumber={pageNumber} - pageSize={PAGE_SIZE} - total={total} - /> - </div> - </SelectionContext.Provider> - ) : ( - <PendingResults /> - )} - </ContentFrame> + <BlockTransactionHeader blockTag={blockNumber.toNumber()} /> + <BlockTransactionResults + page={page} + total={total} + pageNumber={pageNumber} + /> </StandardFrame> ); }; From 772904c527700db9460e39d66c0386889addb6e5 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 18:09:51 -0300 Subject: [PATCH 23/27] Install npm@7 in the builder image; alpine comes with npm@6 --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 526a90f..d4aa214 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM node:12.22.3-alpine AS builder +RUN npm i -g npm@7.19.1 WORKDIR /otterscan-build COPY ["package.json", "package-lock.json", "/otterscan-build/"] RUN npm install From 54bc06a7ad754d610899be6f2af1208b8f2bd1a0 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 18:10:07 -0300 Subject: [PATCH 24/27] Updated build instructions --- docs/other-ways-to-run-otterscan.md | 6 +++--- package.json | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/other-ways-to-run-otterscan.md b/docs/other-ways-to-run-otterscan.md index 0ebd85c..a23c31e 100644 --- a/docs/other-ways-to-run-otterscan.md +++ b/docs/other-ways-to-run-otterscan.md @@ -16,7 +16,7 @@ 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> -docker build -t otterscan -f Dockerfile . +DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile . ``` 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`. @@ -47,13 +47,13 @@ By default, it assumes your Erigon `rpcdaemon` processs is serving requests at ` Start serving 4bytes and trustwallet assets at `localhost:3001` using a dockerized nginx: ``` -npm run start-assets +npm run assets-start ``` To stop it, run: ``` -npm run stop-assets +npm run assets-stop ``` To run Otterscan development build: diff --git a/package.json b/package.json index 09b401f..76504bb 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,11 @@ "build": "craco build", "test": "craco test", "eject": "react-scripts eject", - "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_BUILDKIT=1 docker build -t otterscan -f Dockerfile .", - "start-docker": "docker run --rm -p 5000:80 --name otterscan -d otterscan", - "stop-docker": "docker stop otterscan" + "assets-start": "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", + "assets-stop": "docker stop otterscan-assets", + "docker-build": "DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .", + "docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan", + "docker-stop": "docker stop otterscan" }, "eslintConfig": { "extends": [ From 3f95d0112633ea22c830e1cede043b09cc424052 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 18:17:38 -0300 Subject: [PATCH 25/27] Add aggressive caching for /static directory --- nginx.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nginx.conf b/nginx.conf index 029fb69..55189ba 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,6 +4,11 @@ server { #access_log /var/log/nginx/host.access.log main; + location /static { + root /usr/share/nginx/html; + expires max; + } + location /signatures { root /usr/share/nginx/html; expires 30d; From da7b039c220ccf04c9d0b3a3704f572e0c558f09 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Wed, 14 Jul 2021 22:51:33 -0300 Subject: [PATCH 26/27] Fix highlighting on to column --- src/search/TransactionItem.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index ca4fbed..3829010 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -68,7 +68,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ </span> <TimestampAge timestamp={tx.timestamp} /> <span className="col-span-2 flex justify-between items-baseline space-x-2 pr-2"> - <span className="truncate" title={tx.from}> + <span className="truncate"> {tx.from && ( <AddressHighlighter address={tx.from}> <AddressOrENSName @@ -87,17 +87,19 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ /> </span> </span> - <span className="col-span-2 truncate" title={tx.to}> - {tx.to && ( - <AddressHighlighter address={tx.to}> - <AddressOrENSName - address={tx.to} - ensName={ensTo} - selectedAddress={selectedAddress} - minerAddress={tx.miner} - /> - </AddressHighlighter> - )} + <span className="col-span-2 flex items-baseline" title={tx.to}> + <span className="truncate"> + {tx.to && ( + <AddressHighlighter address={tx.to}> + <AddressOrENSName + address={tx.to} + ensName={ensTo} + selectedAddress={selectedAddress} + minerAddress={tx.miner} + /> + </AddressHighlighter> + )} + </span> </span> <span className="col-span-2 truncate"> <TransactionValue value={tx.value} /> From 81f14dc0b6001f221a3a824f8c689a9d492e7c55 Mon Sep 17 00:00:00 2001 From: Willian Mitsuda <wmitsuda@gmail.com> Date: Thu, 15 Jul 2021 02:40:11 -0300 Subject: [PATCH 27/27] Add API level check --- src/ConnectionErrorPanel.tsx | 6 +++--- src/params.ts | 2 ++ src/useProvider.ts | 15 +++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ConnectionErrorPanel.tsx b/src/ConnectionErrorPanel.tsx index c03ce84..47b1584 100644 --- a/src/ConnectionErrorPanel.tsx +++ b/src/ConnectionErrorPanel.tsx @@ -66,10 +66,10 @@ const ConnectionErrorPanel: React.FC<ConnectionErrorPanelProps> = ({ <Step type="ok" msg="It is an Erigon node" /> <Step type="error" - msg="It does not seem to contain Otterscan patches" + msg="It does not seem to contain up-to-date Otterscan patches" > - Make sure you compiled rpcdaemon with Otterscan patches and - enabled <strong>ots_</strong> namespace according to the{" "} + Make sure you compiled rpcdaemon with compatible Otterscan patches + and enabled <strong>ots_</strong> namespace according to the{" "} <a href="https://github.com/wmitsuda/otterscan#install-otterscan-patches-on-top-of-erigon" target="_blank" diff --git a/src/params.ts b/src/params.ts index 45d559e..75d31c3 100644 --- a/src/params.ts +++ b/src/params.ts @@ -1 +1,3 @@ +export const MIN_API_LEVEL = 1; + export const PAGE_SIZE = 25; diff --git a/src/useProvider.ts b/src/useProvider.ts index b8eed30..9ce1445 100644 --- a/src/useProvider.ts +++ b/src/useProvider.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { ethers } from "ethers"; import { ConnectionStatus } from "./types"; +import { MIN_API_LEVEL } from "./params"; export const DEFAULT_ERIGON_URL = "http://127.0.0.1:8545"; @@ -60,12 +61,14 @@ export const useProvider = ( // Check if it has Otterscan patches by probing a lightweight method try { - // TODO: replace by a custom made method that works in all networks - await provider.send("ots_getTransactionTransfers", [ - "0x793e079fbc427cba0857b4e878194ab508f33983f45415e50af3c3c0e662fdf3", - ]); - setConnStatus(ConnectionStatus.CONNECTED); - setProvider(provider); + const level = await provider.send("ots_getApiLevel", []); + if (level < MIN_API_LEVEL) { + setConnStatus(ConnectionStatus.NOT_OTTERSCAN_PATCHED); + setProvider(undefined); + } else { + setConnStatus(ConnectionStatus.CONNECTED); + setProvider(provider); + } } catch (err) { console.log(err); setConnStatus(ConnectionStatus.NOT_OTTERSCAN_PATCHED);