Merge branch 'release/v2021.10.05-otterscan'

This commit is contained in:
Willian Mitsuda 2021-11-01 15:35:40 -03:00
commit 53587cacf4
58 changed files with 1832 additions and 964 deletions

View File

@ -23,12 +23,74 @@ FROM alpine:3.14.0 AS topic0builder
WORKDIR /topic0 WORKDIR /topic0
COPY topic0/with_parameter_names /topic0/ COPY topic0/with_parameter_names /topic0/
FROM nginx:1.21.1-alpine # Add brotli module to official nginx image
RUN apk add jq # Based on: https://github.com/nginxinc/docker-nginx/tree/master/modules
FROM nginx:1.21.3-alpine as nginxbuilder
RUN set -ex \
&& apk update \
&& apk add linux-headers openssl-dev pcre-dev zlib-dev openssl abuild \
musl-dev libxslt libxml2-utils make mercurial gcc unzip git \
xz g++ coreutils \
# allow abuild as a root user \
&& printf "#!/bin/sh\\nSETFATTR=true /usr/bin/abuild -F \"\$@\"\\n" > /usr/local/bin/abuild \
&& chmod +x /usr/local/bin/abuild \
&& hg clone -r ${NGINX_VERSION}-${PKG_RELEASE} https://hg.nginx.org/pkg-oss/ \
&& cd pkg-oss \
&& mkdir /tmp/packages \
&& for module in "brotli"; do \
echo "Building $module for nginx-$NGINX_VERSION"; \
if [ -d /modules/$module ]; then \
echo "Building $module from user-supplied sources"; \
# check if module sources file is there and not empty
if [ ! -s /modules/$module/source ]; then \
echo "No source file for $module in modules/$module/source, exiting"; \
exit 1; \
fi; \
# some modules require build dependencies
if [ -f /modules/$module/build-deps ]; then \
echo "Installing $module build dependencies"; \
apk update && apk add $(cat /modules/$module/build-deps | xargs); \
fi; \
# if a module has a build dependency that is not in a distro, provide a
# shell script to fetch/build/install those
# note that shared libraries produced as a result of this script will
# not be copied from the builder image to the main one so build static
if [ -x /modules/$module/prebuild ]; then \
echo "Running prebuild script for $module"; \
/modules/$module/prebuild; \
fi; \
/pkg-oss/build_module.sh -v $NGINX_VERSION -f -y -o /tmp/packages -n $module $(cat /modules/$module/source); \
BUILT_MODULES="$BUILT_MODULES $(echo $module | tr '[A-Z]' '[a-z]' | tr -d '[/_\-\.\t ]')"; \
elif make -C /pkg-oss/alpine list | grep -E "^$module\s+\d+" > /dev/null; then \
echo "Building $module from pkg-oss sources"; \
cd /pkg-oss/alpine; \
make abuild-module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
apk add $(. ./abuild-module-$module/APKBUILD; echo $makedepends;); \
make module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
find ~/packages -type f -name "*.apk" -exec mv -v {} /tmp/packages/ \;; \
BUILT_MODULES="$BUILT_MODULES $module"; \
else \
echo "Don't know how to build $module module, exiting"; \
exit 1; \
fi; \
done \
&& echo "BUILT_MODULES=\"$BUILT_MODULES\"" > /tmp/packages/modules.env
FROM nginx:1.21.3-alpine
COPY --from=nginxbuilder /tmp/packages /tmp/packages
RUN set -ex \
&& . /tmp/packages/modules.env \
&& for module in $BUILT_MODULES; do \
apk add --no-cache --allow-untrusted /tmp/packages/nginx-module-${module}-${NGINX_VERSION}*.apk; \
done \
&& rm -rf /tmp/packages
RUN apk update && apk add jq
COPY --from=topic0builder /topic0 /usr/share/nginx/html/topic0/ COPY --from=topic0builder /topic0 /usr/share/nginx/html/topic0/
COPY --from=fourbytesbuilder /signatures /usr/share/nginx/html/signatures/ COPY --from=fourbytesbuilder /signatures /usr/share/nginx/html/signatures/
COPY --from=logobuilder /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 nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /otterscan-build/build /usr/share/nginx/html/ COPY --from=builder /otterscan-build/build /usr/share/nginx/html/
COPY --from=builder /otterscan-build/run-nginx.sh / COPY --from=builder /otterscan-build/run-nginx.sh /
WORKDIR / WORKDIR /

View File

@ -2,6 +2,8 @@ server {
listen 80; listen 80;
server_name localhost; server_name localhost;
gzip_static on;
#access_log /var/log/nginx/host.access.log main; #access_log /var/log/nginx/host.access.log main;
location /static { location /static {
@ -115,6 +117,8 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
try_files $uri /index.html; try_files $uri /index.html;
brotli_static on;
} }
#error_page 404 /404.html; #error_page 404 /404.html;

32
nginx/nginx.conf Normal file
View File

@ -0,0 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
load_module modules/ngx_http_brotli_static_module.so;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

1471
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0", "@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.2", "@chainlink/contracts": "^0.2.2",
"@craco/craco": "^6.3.0", "@craco/craco": "^6.4.0",
"@fontsource/fira-code": "^4.5.1", "@fontsource/fira-code": "^4.5.1",
"@fontsource/roboto": "^4.5.1", "@fontsource/roboto": "^4.5.1",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
@ -22,21 +22,22 @@
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.30", "@types/react": "^17.0.33",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.10",
"@types/react-highlight": "^0.12.5", "@types/react-highlight": "^0.12.5",
"@types/react-router-dom": "^5.3.1", "@types/react-router-dom": "^5.3.2",
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.5.1", "chart.js": "^3.5.1",
"ethers": "^5.4.7", "ethers": "^5.5.1",
"highlightjs-solidity": "^2.0.1", "highlightjs-solidity": "^2.0.1",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",
"react-chartjs-2": "^3.0.5", "react-chartjs-2": "^3.3.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3", "react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.1.2",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
@ -48,12 +49,12 @@
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build && compress-cra",
"test": "craco test", "test": "craco test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"source-map-explorer": "source-map-explorer build/static/js/*.js", "source-map-explorer": "source-map-explorer build/static/js/*.js",
"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)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine", "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)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf -v$(pwd)/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine", "assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf -v$(pwd)/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-stop": "docker stop otterscan-assets", "assets-stop": "docker stop otterscan-assets",
"docker-build": "DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .", "docker-build": "DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .",
"docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan", "docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan",
@ -79,6 +80,7 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^9.8.8", "autoprefixer": "^9.8.8",
"compress-create-react-app": "^1.1.3",
"postcss": "^7.0.39", "postcss": "^7.0.39",
"source-map-explorer": "^2.5.2", "source-map-explorer": "^2.5.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6" "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6"

View File

@ -26,12 +26,13 @@ import PendingResults from "./search/PendingResults";
import TransactionItem from "./search/TransactionItem"; import TransactionItem from "./search/TransactionItem";
import { SearchController } from "./search/search"; import { SearchController } from "./search/search";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useENSCache } from "./useReverseCache"; import { pageCollector, useResolvedAddresses } from "./useResolvedAddresses";
import { useFeeToggler } from "./search/useFeeToggler"; import { useFeeToggler } from "./search/useFeeToggler";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useMultipleETHUSDOracle } from "./usePriceOracle"; import { useMultipleETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { useSourcify } from "./useSourcify"; import { useSourcify } from "./useSourcify";
import SourcifyLogo from "./sourcify.svg";
type BlockParams = { type BlockParams = {
addressOrName: string; addressOrName: string;
@ -164,7 +165,8 @@ const AddressTransactions: React.FC = () => {
}, [provider, checksummedAddress, params.direction, hash, controller]); }, [provider, checksummedAddress, params.direction, hash, controller]);
const page = useMemo(() => controller?.getPage(), [controller]); const page = useMemo(() => controller?.getPage(), [controller]);
const reverseCache = useENSCache(provider, page); const addrCollector = useMemo(() => pageCollector(page), [page]);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
const blockTags: BlockTag[] = useMemo(() => { const blockTags: BlockTag[] = useMemo(() => {
if (!page) { if (!page) {
@ -240,7 +242,7 @@ const AddressTransactions: React.FC = () => {
) : ( ) : (
<span className="self-center text-green-500"> <span className="self-center text-green-500">
<img <img
src="/sourcify.svg" src={SourcifyLogo}
alt="Sourcify logo" alt="Sourcify logo"
title="Verified by Sourcify" title="Verified by Sourcify"
width={16} width={16}
@ -276,13 +278,13 @@ const AddressTransactions: React.FC = () => {
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler} feeDisplayToggler={feeDisplayToggler}
/> />
{controller ? ( {page ? (
<SelectionContext.Provider value={selectionCtx}> <SelectionContext.Provider value={selectionCtx}>
{controller.getPage().map((tx) => ( {page.map((tx) => (
<TransactionItem <TransactionItem
key={tx.hash} key={tx.hash}
tx={tx} tx={tx}
ensCache={reverseCache} resolvedAddresses={resolvedAddresses}
selectedAddress={checksummedAddress} selectedAddress={checksummedAddress}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}

View File

@ -5,17 +5,37 @@ import Home from "./Home";
import Search from "./Search"; import Search from "./Search";
import Title from "./Title"; import Title from "./Title";
import ConnectionErrorPanel from "./ConnectionErrorPanel"; import ConnectionErrorPanel from "./ConnectionErrorPanel";
import London from "./special/london/London";
import Footer from "./Footer"; import Footer from "./Footer";
import { ConnectionStatus } from "./types"; import { ConnectionStatus } from "./types";
import { RuntimeContext, useRuntime } from "./useRuntime"; import { RuntimeContext, useRuntime } from "./useRuntime";
import { AppConfig, AppConfigContext } from "./useAppConfig"; import { AppConfig, AppConfigContext } from "./useAppConfig";
import { SourcifySource } from "./url"; import { SourcifySource } from "./url";
const Block = React.lazy(() => import("./Block")); const Block = React.lazy(
const BlockTransactions = React.lazy(() => import("./BlockTransactions")); () => import(/* webpackChunkName: "block", webpackPrefetch: true */ "./Block")
const AddressTransactions = React.lazy(() => import("./AddressTransactions")); );
const Transaction = React.lazy(() => import("./Transaction")); const BlockTransactions = React.lazy(
() =>
import(
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
)
);
const AddressTransactions = React.lazy(
() =>
import(
/* webpackChunkName: "address", webpackPrefetch: true */ "./AddressTransactions"
)
);
const Transaction = React.lazy(
() =>
import(/* webpackChunkName: "tx", webpackPrefetch: true */ "./Transaction")
);
const London = React.lazy(
() =>
import(
/* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London"
)
);
const App = () => { const App = () => {
const runtime = useRuntime(); const runtime = useRuntime();
@ -30,7 +50,7 @@ const App = () => {
}, [sourcifySource, setSourcifySource]); }, [sourcifySource, setSourcifySource]);
return ( return (
<Suspense fallback={<>LOADING</>}> <Suspense fallback={null}>
{runtime.connStatus !== ConnectionStatus.CONNECTED ? ( {runtime.connStatus !== ConnectionStatus.CONNECTED ? (
<ConnectionErrorPanel <ConnectionErrorPanel
connStatus={runtime.connStatus} connStatus={runtime.connStatus}
@ -53,7 +73,6 @@ const App = () => {
</Route> </Route>
<Route> <Route>
<AppConfigContext.Provider value={appConfig}> <AppConfigContext.Provider value={appConfig}>
<div className="mb-auto">
<Title /> <Title />
<Route path="/block/:blockNumberOrHash" exact> <Route path="/block/:blockNumberOrHash" exact>
<Block /> <Block />
@ -67,7 +86,6 @@ const App = () => {
<Route path="/address/:addressOrName/:direction?"> <Route path="/address/:addressOrName/:direction?">
<AddressTransactions /> <AddressTransactions />
</Route> </Route>
</div>
</AppConfigContext.Provider> </AppConfigContext.Provider>
</Route> </Route>
</Switch> </Switch>

View File

@ -166,7 +166,7 @@ const Block: React.FC = () => {
<USDValue value={blockETHUSDPrice} /> <USDValue value={blockETHUSDPrice} />
</InfoRow> </InfoRow>
<InfoRow title="Difficult"> <InfoRow title="Difficult">
{block.difficulty ? commify(block.difficulty) : "?"} {commify(block._difficulty.toString())}
</InfoRow> </InfoRow>
<InfoRow title="Total Difficult"> <InfoRow title="Total Difficult">
{commify(block.totalDifficulty.toString())} {commify(block.totalDifficulty.toString())}

View File

@ -5,12 +5,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode"; import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode";
import Logo from "./Logo"; import Logo from "./Logo";
import CameraScanner from "./search/CameraScanner";
import Timestamp from "./components/Timestamp"; import Timestamp from "./components/Timestamp";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useLatestBlock } from "./useLatestBlock"; import { useLatestBlock } from "./useLatestBlock";
import { blockURL } from "./url"; import { blockURL } from "./url";
const CameraScanner = React.lazy(() => import("./search/CameraScanner"));
const Home: React.FC = () => { const Home: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const [search, setSearch] = useState<string>(); const [search, setSearch] = useState<string>();
@ -37,9 +38,11 @@ const Home: React.FC = () => {
document.title = "Home | Otterscan"; document.title = "Home | Otterscan";
return ( return (
<div className="m-auto"> <div className="mx-auto flex flex-col flex-grow pb-5">
{isScanning && <CameraScanner turnOffScan={() => setScanning(false)} />} {isScanning && <CameraScanner turnOffScan={() => setScanning(false)} />}
<div className="m-5 mb-10 flex items-end flex-grow max-h-64">
<Logo /> <Logo />
</div>
<form <form
className="flex flex-col" className="flex flex-col"
onSubmit={handleSubmit} onSubmit={handleSubmit}
@ -70,7 +73,9 @@ const Home: React.FC = () => {
> >
Search Search
</button> </button>
<div className="mx-auto mt-5 mb-5 text-lg text-link-blue hover:text-link-blue-hover font-bold"> </form>
<div className="mx-auto h-32">
<div className="text-lg text-link-blue hover:text-link-blue-hover font-bold">
<NavLink to="/special/london"> <NavLink to="/special/london">
<div className="flex space-x-2 items-baseline text-orange-500 hover:text-orange-700 hover:underline"> <div className="flex space-x-2 items-baseline text-orange-500 hover:text-orange-700 hover:underline">
<span> <span>
@ -85,14 +90,14 @@ const Home: React.FC = () => {
</div> </div>
{latestBlock && ( {latestBlock && (
<NavLink <NavLink
className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue" className="flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue"
to={blockURL(latestBlock.number)} to={blockURL(latestBlock.number)}
> >
<div>Latest block: {commify(latestBlock.number)}</div> <div>Latest block: {commify(latestBlock.number)}</div>
<Timestamp value={latestBlock.timestamp} /> <Timestamp value={latestBlock.timestamp} />
</NavLink> </NavLink>
)} )}
</form> </div>
</div> </div>
); );
}; };

View File

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import Otter from "./otter.jpg";
const Logo: React.FC = () => ( const Logo: React.FC = () => (
<div className="mx-auto mb-16 text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4"> <div className="mx-auto text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4">
<img <img
className="rounded-full" className="rounded-full"
src="/otter.jpg" src={Otter}
width={96} width={96}
height={96} height={96}
alt="An otter scanning" alt="An otter scanning"

View File

@ -30,14 +30,6 @@ const SourcifyMenu: React.FC = () => {
> >
Sourcify Servers Sourcify Servers
</SourcifyMenuItem> </SourcifyMenuItem>
<SourcifyMenuItem
checked={sourcifySource === SourcifySource.CUSTOM_SNAPSHOT_SERVER}
onClick={() =>
setSourcifySource(SourcifySource.CUSTOM_SNAPSHOT_SERVER)
}
>
Local Snapshot
</SourcifyMenuItem>
</Menu.Items> </Menu.Items>
</div> </div>
</Menu> </Menu>

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const StandardFrame: React.FC = ({ children }) => ( const StandardFrame: React.FC = ({ children }) => (
<div className="bg-gray-100 px-9 pt-3 pb-12">{children}</div> <div className="flex-grow bg-gray-100 px-9 pt-3 pb-12">{children}</div>
); );
export default StandardFrame; export default StandardFrame;

View File

@ -4,9 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode"; import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode";
import useKeyboardShortcut from "use-keyboard-shortcut"; import useKeyboardShortcut from "use-keyboard-shortcut";
import PriceBox from "./PriceBox"; import PriceBox from "./PriceBox";
import CameraScanner from "./search/CameraScanner";
import SourcifyMenu from "./SourcifyMenu"; import SourcifyMenu from "./SourcifyMenu";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import Otter from "./otter.jpg";
const CameraScanner = React.lazy(() => import("./search/CameraScanner"));
const Title: React.FC = () => { const Title: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
@ -43,7 +45,7 @@ const Title: React.FC = () => {
<div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2"> <div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2">
<img <img
className="rounded-full" className="rounded-full"
src="/otter.jpg" src={Otter}
width={32} width={32}
height={32} height={32}
alt="An otter scanning" alt="An otter scanning"

View File

@ -11,11 +11,13 @@ import {
TokenTransfer, TokenTransfer,
TransactionData, TransactionData,
} from "./types"; } from "./types";
import { ResolvedAddresses } from "./api/address-resolver";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
txData: TransactionData; txData: TransactionData;
tokenMeta?: TokenMeta | undefined; tokenMeta?: TokenMeta | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
// TODO: handle partial // TODO: handle partial
@ -23,6 +25,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t, t,
txData, txData,
tokenMeta, tokenMeta,
resolvedAddresses,
}) => ( }) => (
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
<span className="text-gray-500"> <span className="text-gray-500">
@ -64,10 +67,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<AddressHighlighter address={t.token}> <AddressHighlighter address={t.token}>
<DecoratedAddressLink <DecoratedAddressLink
address={t.token} address={t.token}
text={ resolvedAddresses={resolvedAddresses}
tokenMeta ? `${tokenMeta.name} (${tokenMeta.symbol})` : undefined
}
tokenMeta={tokenMeta}
/> />
</AddressHighlighter> </AddressHighlighter>
</div> </div>

View File

@ -5,14 +5,30 @@ import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle"; import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame"; import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab"; import NavTab from "./components/NavTab";
import Details from "./transaction/Details";
import Logs from "./transaction/Logs";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations, useTxData } from "./useErigonHooks"; import { useInternalOperations, useTxData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle"; import { useETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./useSourcify"; import { useSourcify, useTransactionDescription } from "./useSourcify";
import {
transactionDataCollector,
useResolvedAddresses,
} from "./useResolvedAddresses";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
type TransactionParams = { type TransactionParams = {
txhash: string; txhash: string;
@ -24,6 +40,11 @@ const Transaction: React.FC = () => {
const { txhash } = params; const { txhash } = params;
const txData = useTxData(provider, txhash); const txData = useTxData(provider, txhash);
const addrCollector = useMemo(
() => transactionDataCollector(txData),
[txData]
);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
const internalOps = useInternalOperations(provider, txData); const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => { const sendsEthToMiner = useMemo(() => {
@ -77,6 +98,7 @@ const Transaction: React.FC = () => {
)} )}
</Tab.List> </Tab.List>
</Tab.Group> </Tab.Group>
<React.Suspense fallback={null}>
<Switch> <Switch>
<Route path="/tx/:txhash/" exact> <Route path="/tx/:txhash/" exact>
<Details <Details
@ -87,12 +109,18 @@ const Transaction: React.FC = () => {
internalOps={internalOps} internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner} sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice} ethUSDPrice={blockETHUSDPrice}
resolvedAddresses={resolvedAddresses}
/> />
</Route> </Route>
<Route path="/tx/:txhash/logs/" exact> <Route path="/tx/:txhash/logs/" exact>
<Logs txData={txData} metadata={metadata} /> <Logs
txData={txData}
metadata={metadata}
resolvedAddresses={resolvedAddresses}
/>
</Route> </Route>
</Switch> </Switch>
</React.Suspense>
</SelectionContext.Provider> </SelectionContext.Provider>
)} )}
</StandardFrame> </StandardFrame>

View File

@ -1,24 +0,0 @@
import React from "react";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import hljs from "highlight.js";
import docco from "react-syntax-highlighter/dist/esm/styles/hljs/docco";
import hljsDefineSolidity from "highlightjs-solidity";
hljsDefineSolidity(hljs);
type ABIProps = {
abi: any[];
};
const ABI: React.FC<ABIProps> = ({ abi }) => (
<SyntaxHighlighter
className="w-full h-60 border font-code text-base"
language="json"
style={docco}
showLineNumbers
>
{JSON.stringify(abi, null, " ") ?? ""}
</SyntaxHighlighter>
);
export default React.memo(ABI);

View File

@ -1,12 +1,7 @@
import React from "react"; import React from "react";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; import { SyntaxHighlighter, docco } from "../highlight-init";
import hljs from "highlight.js";
import docco from "react-syntax-highlighter/dist/esm/styles/hljs/docco";
import { useContract } from "../useSourcify"; import { useContract } from "../useSourcify";
import hljsDefineSolidity from "highlightjs-solidity";
import { useAppConfigContext } from "../useAppConfig"; import { useAppConfigContext } from "../useAppConfig";
hljsDefineSolidity(hljs);
type ContractProps = { type ContractProps = {
checksummedAddress: string; checksummedAddress: string;

View File

@ -0,0 +1,35 @@
import React from "react";
import { Tab } from "@headlessui/react";
import ModeTab from "../components/ModeTab";
import Copy from "../components/Copy";
import DecodedABI from "./DecodedABI";
import RawABI from "./RawABI";
type ContractABIProps = {
abi: any[];
};
const ContractABI: React.FC<ContractABIProps> = ({ abi }) => (
<div className="mb-3">
<Tab.Group>
<Tab.List className="flex items-baseline space-x-1 mb-1">
<div className="flex items-baseline space-x-2 text-sm pr-2 py-1">
<span>ABI</span>
<Copy value={JSON.stringify(abi)} />
</div>
<ModeTab>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<DecodedABI abi={abi} />
</Tab.Panel>
<Tab.Panel>
<RawABI abi={abi} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
);
export default React.memo(ContractABI);

View File

@ -5,13 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow"; import InfoRow from "../components/InfoRow";
import Copy from "../components/Copy";
import ABI from "./ABI";
import Contract from "./Contract"; import Contract from "./Contract";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { Metadata } from "../useSourcify"; import { Metadata } from "../useSourcify";
import ExternalLink from "../components/ExternalLink"; import ExternalLink from "../components/ExternalLink";
import { openInRemixURL } from "../url"; import { openInRemixURL } from "../url";
import ContractABI from "./ContractABI";
type ContractsProps = { type ContractsProps = {
checksummedAddress: string; checksummedAddress: string;
@ -70,13 +69,7 @@ const Contracts: React.FC<ContractsProps> = ({
{rawMetadata !== undefined && rawMetadata !== null && ( {rawMetadata !== undefined && rawMetadata !== null && (
<> <>
{rawMetadata.output.abi && ( {rawMetadata.output.abi && (
<div className="mb-3"> <ContractABI abi={rawMetadata.output.abi} />
<div className="flex space-x-2 text-sm border-l border-r border-t rounded-t px-2 py-1">
<span>ABI</span>
<Copy value={JSON.stringify(rawMetadata.output.abi)} />
</div>
<ABI abi={rawMetadata.output.abi} />
</div>
)} )}
<div> <div>
<Menu> <Menu>

View File

@ -0,0 +1,20 @@
import { Interface } from "@ethersproject/abi";
import React from "react";
import DecodedFragment from "./DecodedFragment";
type DecodedABIProps = {
abi: any[];
};
const DecodedABI: React.FC<DecodedABIProps> = ({ abi }) => {
const intf = new Interface(abi);
return (
<div className="border overflow-x-auto">
{intf.fragments.map((f, i) => (
<DecodedFragment key={i} intf={intf} fragment={f} />
))}
</div>
);
};
export default React.memo(DecodedABI);

View File

@ -0,0 +1,78 @@
import React from "react";
import {
ConstructorFragment,
EventFragment,
Fragment,
FunctionFragment,
Interface,
} from "@ethersproject/abi";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight";
type DecodedFragmentProps = {
intf: Interface;
fragment: Fragment;
};
const DecodedFragment: React.FC<DecodedFragmentProps> = ({
intf,
fragment,
}) => {
let fragmentType: "constructor" | "event" | "function" | undefined;
let sig: string | undefined;
let letter: string | undefined;
let letterBg: string | undefined;
let hashBg: string | undefined;
if (FunctionFragment.isFunctionFragment(fragment)) {
fragmentType = "function";
sig = intf.getSighash(fragment);
letter = "F";
letterBg = "bg-purple-500";
hashBg = "bg-purple-50";
} else if (EventFragment.isEventFragment(fragment)) {
fragmentType = "event";
sig = intf.getEventTopic(fragment);
letter = "E";
letterBg = "bg-green-300";
hashBg = "bg-green-50";
} else if (ConstructorFragment.isConstructorFragment(fragment)) {
fragmentType = "constructor";
letter = "C";
letterBg = "bg-blue-500";
}
return (
<div className="flex flex-wrap items-baseline space-x-2 px-2 py-1 hover:bg-gray-100">
<span className="text-gray-500">
<FontAwesomeIcon icon={faCaretRight} size="1x" />
</span>
{letter && (
<span
className={`flex-shrink-0 text-xs font-code border border-gray-300 rounded-full w-5 h-5 self-center flex items-center justify-center text-white font-bold ${letterBg}`}
>
{letter}
</span>
)}
<span className="text-sm font-code whitespace-nowrap">
{fragment.format("full")}
</span>
{sig && (
<span
className={`text-xs border rounded-xl px-2 pt-1 font-code text-gray-600 ${hashBg}`}
title={
fragmentType === "function"
? "Method Selector"
: fragmentType === "event"
? "Topic Hash"
: ""
}
>
{sig}
</span>
)}
</div>
);
};
export default React.memo(DecodedFragment);

19
src/address/RawABI.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init";
type RawABIProps = {
abi: any[];
};
const RawABI: React.FC<RawABIProps> = ({ abi }) => (
<SyntaxHighlighter
className="w-full h-60 border font-code text-base"
language="json"
style={docco}
showLineNumbers
>
{JSON.stringify(abi, null, " ") ?? ""}
</SyntaxHighlighter>
);
export default React.memo(RawABI);

View File

@ -0,0 +1,28 @@
import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver";
export type SelectedResolvedName<T> = [IAddressResolver<T>, T] | null;
export class CompositeAddressResolver<T = any>
implements IAddressResolver<SelectedResolvedName<T>>
{
private resolvers: IAddressResolver<T>[] = [];
addResolver(resolver: IAddressResolver<T>) {
this.resolvers.push(resolver);
}
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<SelectedResolvedName<T> | undefined> {
for (const r of this.resolvers) {
const resolvedAddress = await r.resolveAddress(provider, address);
if (resolvedAddress !== undefined) {
return [r, resolvedAddress];
}
}
return null;
}
}

View File

@ -0,0 +1,15 @@
import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver";
export class ENSAddressResolver implements IAddressResolver<string> {
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<string | undefined> {
const name = await provider.lookupAddress(address);
if (name === null) {
return undefined;
}
return name;
}
}

View File

@ -0,0 +1,30 @@
import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts";
import { IAddressResolver } from "./address-resolver";
import erc20 from "../../erc20.json";
import { TokenMeta } from "../../types";
export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20, provider);
try {
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
return {
name,
symbol,
decimals,
};
} catch (err) {
// Ignore on purpose; this indicates the probe failed and the address
// is not a token
}
return undefined;
}
}

View File

@ -0,0 +1,16 @@
import React from "react";
import { BaseProvider } from "@ethersproject/providers";
export interface IAddressResolver<T> {
resolveAddress(
provider: BaseProvider,
address: string
): Promise<T | undefined>;
}
export type ResolvedAddressRenderer<T> = (
address: string,
resolvedAddress: T,
linkable: boolean,
dontOverrideColors: boolean
) => React.ReactElement;

View File

@ -0,0 +1,57 @@
import { BaseProvider } from "@ethersproject/providers";
import { ensRenderer } from "../../components/ENSName";
import { tokenRenderer } from "../../components/TokenName";
import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver";
import {
CompositeAddressResolver,
SelectedResolvedName,
} from "./CompositeAddressResolver";
import { ENSAddressResolver } from "./ENSAddressResolver";
import { ERCTokenResolver } from "./ERCTokenResolver";
export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>;
// Create and configure the main resolver
export const ensResolver = new ENSAddressResolver();
export const ercTokenResolver = new ERCTokenResolver();
const _mainResolver = new CompositeAddressResolver();
_mainResolver.addResolver(ensResolver);
_mainResolver.addResolver(ercTokenResolver);
export const mainResolver: IAddressResolver<SelectedResolvedName<any>> =
_mainResolver;
export const resolverRendererRegistry = new Map<
IAddressResolver<any>,
ResolvedAddressRenderer<any>
>();
resolverRendererRegistry.set(ensResolver, ensRenderer);
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
// TODO: implement progressive resolving
export const batchPopulate = async (
provider: BaseProvider,
addresses: string[],
currentMap: ResolvedAddresses | undefined
): Promise<ResolvedAddresses> => {
const solvers: Promise<SelectedResolvedName<any> | undefined>[] = [];
const unresolvedAddresses = addresses.filter(
(a) => currentMap?.[a] === undefined
);
for (const a of unresolvedAddresses) {
solvers.push(mainResolver.resolveAddress(provider, a));
}
const resultMap: ResolvedAddresses = currentMap ? { ...currentMap } : {};
const results = await Promise.all(solvers);
for (let i = 0; i < results.length; i++) {
const r = results[i];
if (r === undefined) {
continue;
}
resultMap[unresolvedAddresses[i]] = r;
}
return resultMap;
};

View File

@ -8,7 +8,7 @@ import TransactionItem from "../search/TransactionItem";
import { useFeeToggler } from "../search/useFeeToggler"; import { useFeeToggler } from "../search/useFeeToggler";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { SelectionContext, useSelection } from "../useSelection"; import { SelectionContext, useSelection } from "../useSelection";
import { useENSCache } from "../useReverseCache"; import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
import { ProcessedTransaction } from "../types"; import { ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params"; import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useMultipleETHUSDOracle } from "../usePriceOracle";
@ -29,7 +29,8 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const selectionCtx = useSelection(); const selectionCtx = useSelection();
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const reverseCache = useENSCache(provider, page); const addrCollector = useMemo(() => pageCollector(page), [page]);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
const blockTags = useMemo(() => [blockTag], [blockTag]); const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags); const priceMap = useMultipleETHUSDOracle(provider, blockTags);
@ -59,7 +60,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
<TransactionItem <TransactionItem
key={tx.hash} key={tx.hash}
tx={tx} tx={tx}
ensCache={reverseCache} resolvedAddresses={resolvedAddresses}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}
/> />

View File

@ -1,13 +0,0 @@
import React from "react";
type AddressProps = {
address: string;
};
const Address: React.FC<AddressProps> = ({ address }) => (
<span className="font-address text-gray-400 truncate" title={address}>
<span className="truncate">{address}</span>
</span>
);
export default React.memo(Address);

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useMemo } from "react";
import { useSelectionContext } from "../useSelection"; import { useSelectionContext } from "../useSelection";
type AddressHighlighterProps = React.PropsWithChildren<{ type AddressHighlighterProps = React.PropsWithChildren<{
@ -10,12 +10,15 @@ const AddressHighlighter: React.FC<AddressHighlighterProps> = ({
children, children,
}) => { }) => {
const [selection, setSelection] = useSelectionContext(); const [selection, setSelection] = useSelectionContext();
const select = () => { const [select, deselect] = useMemo(() => {
const _select = () => {
setSelection({ type: "address", content: address }); setSelection({ type: "address", content: address });
}; };
const deselect = () => { const _deselect = () => {
setSelection(null); setSelection(null);
}; };
return [_select, _deselect];
}, [setSelection, address]);
return ( return (
<div <div

View File

@ -1,27 +0,0 @@
import React from "react";
import { NavLink } from "react-router-dom";
type AddressLinkProps = {
address: string;
text?: string;
dontOverrideColors?: boolean;
};
const AddressLink: React.FC<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
>
<span className="truncate" title={text ?? address}>
{text ?? address}
</span>
</NavLink>
);
export default React.memo(AddressLink);

View File

@ -1,51 +1,49 @@
import React from "react"; import React from "react";
import Address from "./Address"; import {
import AddressLink from "./AddressLink"; ResolvedAddresses,
import ENSName from "./ENSName"; resolverRendererRegistry,
import ENSNameLink from "./ENSNameLink"; } from "../api/address-resolver";
import PlainAddress from "./PlainAddress";
type AddressOrENSNameProps = { type AddressOrENSNameProps = {
address: string; address: string;
ensName?: string;
selectedAddress?: string; selectedAddress?: string;
text?: string;
dontOverrideColors?: boolean; dontOverrideColors?: boolean;
resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
address, address,
ensName,
selectedAddress, selectedAddress,
text,
dontOverrideColors, dontOverrideColors,
}) => ( resolvedAddresses,
<> }) => {
{address === selectedAddress ? ( const resolvedAddress = resolvedAddresses?.[address];
<> const linkable = address !== selectedAddress;
{ensName ? (
<ENSName name={ensName} address={address} />
) : (
<Address address={address} />
)}
</>
) : (
<>
{ensName ? (
<ENSNameLink
name={ensName}
address={address}
dontOverrideColors={dontOverrideColors}
/>
) : (
<AddressLink
address={address}
text={text}
dontOverrideColors={dontOverrideColors}
/>
)}
</>
)}
</>
);
export default React.memo(AddressOrENSName); if (!resolvedAddress) {
return (
<PlainAddress
address={address}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
}
const [resolver, resolvedName] = resolvedAddress;
const renderer = resolverRendererRegistry.get(resolver);
if (renderer === undefined) {
return (
<PlainAddress
address={address}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
}
return renderer(address, resolvedName, linkable, !!dontOverrideColors);
};
export default AddressOrENSName;

View File

@ -5,36 +5,32 @@ import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt"; import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt";
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import TokenLogo from "./TokenLogo";
import AddressOrENSName from "./AddressOrENSName"; import AddressOrENSName from "./AddressOrENSName";
import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types"; import { AddressContext, ZERO_ADDRESS } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type DecoratedAddressLinkProps = { type DecoratedAddressLinkProps = {
address: string; address: string;
ensName?: string;
selectedAddress?: string; selectedAddress?: string;
text?: string;
addressCtx?: AddressContext; addressCtx?: AddressContext;
creation?: boolean; creation?: boolean;
miner?: boolean; miner?: boolean;
selfDestruct?: boolean; selfDestruct?: boolean;
txFrom?: boolean; txFrom?: boolean;
txTo?: boolean; txTo?: boolean;
tokenMeta?: TokenMeta; resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
address, address,
ensName,
selectedAddress, selectedAddress,
text,
addressCtx, addressCtx,
creation, creation,
miner, miner,
selfDestruct, selfDestruct,
txFrom, txFrom,
txTo, txTo,
tokenMeta, resolvedAddresses,
}) => { }) => {
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS; const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;
@ -74,20 +70,14 @@ const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
<FontAwesomeIcon icon={faCoins} size="1x" /> <FontAwesomeIcon icon={faCoins} size="1x" />
</span> </span>
)} )}
{tokenMeta && (
<div className="self-center">
<TokenLogo address={address} name={tokenMeta.name} />
</div>
)}
<AddressOrENSName <AddressOrENSName
address={address} address={address}
ensName={ensName}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
text={text}
dontOverrideColors={mint || burn} dontOverrideColors={mint || burn}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
); );
}; };
export default React.memo(DecoratedAddresssLink); export default React.memo(DecoratedAddressLink);

View File

@ -1,25 +1,75 @@
import React from "react"; import React from "react";
import { NavLink } from "react-router-dom";
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
import ENSLogo from "./ensLogo.svg"; import ENSLogo from "./ensLogo.svg";
type ENSNameProps = { type ENSNameProps = {
name: string; name: string;
address: string; address: string;
linkable: boolean;
dontOverrideColors?: boolean;
}; };
const ENSName: React.FC<ENSNameProps> = ({ name, address }) => ( const ENSName: React.FC<ENSNameProps> = ({
name,
address,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<Content linkable={true} name={name} />
</NavLink>
);
}
return (
<div <div
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate" className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
title={`${name}: ${address}`} title={`${name}: ${address}`}
> >
<Content linkable={false} name={name} />
</div>
);
};
type ContentProps = {
linkable: boolean;
name: string;
};
const Content: React.FC<ContentProps> = ({ linkable, name }) => (
<>
<img <img
className="self-center filter grayscale" className={`self-center ${linkable ? "" : "filter grayscale"}`}
src={ENSLogo} src={ENSLogo}
alt="ENS Logo" alt="ENS Logo"
width={12} width={12}
height={12} height={12}
/> />
<span className="truncate">{name}</span> <span className="truncate">{name}</span>
</div> </>
); );
export default React.memo(ENSName); export const ensRenderer: ResolvedAddressRenderer<string> = (
address,
resolvedAddress,
linkable,
dontOverrideColors
) => (
<ENSName
address={address}
name={resolvedAddress}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
export default ENSName;

View File

@ -1,34 +0,0 @@
import React from "react";
import { NavLink } from "react-router-dom";
import ENSLogo from "./ensLogo.svg";
type ENSNameLinkProps = {
name: string;
address: string;
dontOverrideColors?: boolean;
};
const ENSNameLink: React.FC<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<img
className="self-center"
src={ENSLogo}
alt="ENS Logo"
width={12}
height={12}
/>
<span className="truncate">{name}</span>
</NavLink>
);
export default React.memo(ENSNameLink);

View File

@ -3,17 +3,23 @@ import InternalTransfer from "./InternalTransfer";
import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalSelfDestruct from "./InternalSelfDestruct";
import InternalCreate from "./InternalCreate"; import InternalCreate from "./InternalCreate";
import { TransactionData, InternalOperation, OperationType } from "../types"; import { TransactionData, InternalOperation, OperationType } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransactionOperationProps = { type InternalTransactionOperationProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> = const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
({ txData, internalOp }) => ( ({ txData, internalOp, resolvedAddresses }) => (
<> <>
{internalOp.type === OperationType.TRANSFER && ( {internalOp.type === OperationType.TRANSFER && (
<InternalTransfer txData={txData} internalOp={internalOp} /> <InternalTransfer
txData={txData}
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
{internalOp.type === OperationType.SELF_DESTRUCT && ( {internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct txData={txData} internalOp={internalOp} /> <InternalSelfDestruct txData={txData} internalOp={internalOp} />

View File

@ -5,15 +5,18 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransferProps = { type InternalTransferProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransfer: React.FC<InternalTransferProps> = ({ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
internalOp, internalOp,
resolvedAddresses,
}) => { }) => {
const fromMiner = const fromMiner =
txData.confirmedData?.miner !== undefined && txData.confirmedData?.miner !== undefined &&

View File

@ -0,0 +1,36 @@
import React from "react";
import { NavLink } from "react-router-dom";
type PlainAddressProps = {
address: string;
linkable: boolean;
dontOverrideColors: boolean | undefined;
};
const PlainAddress: React.FC<PlainAddressProps> = ({
address,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
title={address}
>
{address}
</NavLink>
);
}
return (
<span className="font-address text-gray-400 truncate" title={address}>
{address}
</span>
);
};
export default PlainAddress;

View File

@ -9,7 +9,7 @@ type TokenLogoProps = {
}; };
const TokenLogo: React.FC<TokenLogoProps> = (props) => ( const TokenLogo: React.FC<TokenLogoProps> = (props) => (
<Suspense fallback={<></>}> <Suspense fallback={null}>
<InternalTokenLogo {...props} /> <InternalTokenLogo {...props} />
</Suspense> </Suspense>
); );

View File

@ -0,0 +1,91 @@
import React from "react";
import { NavLink } from "react-router-dom";
import TokenLogo from "./TokenLogo";
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
import { TokenMeta } from "../types";
type TokenNameProps = {
address: string;
name: string;
symbol: string;
linkable: boolean;
dontOverrideColors?: boolean;
};
const TokenName: React.FC<TokenNameProps> = ({
address,
name,
symbol,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${address}`}
title={`${name} (${symbol}): ${address}`}
>
<Content
address={address}
linkable={true}
name={name}
symbol={symbol}
/>
</NavLink>
);
}
return (
<div
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
title={`${name} (${symbol}): ${address}`}
>
<Content address={address} linkable={false} name={name} symbol={symbol} />
</div>
);
};
type ContentProps = {
address: string;
name: string;
symbol: string;
linkable: boolean;
};
const Content: React.FC<ContentProps> = ({
address,
name,
symbol,
linkable,
}) => (
<>
<div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`}
>
<TokenLogo address={address} name={name} />
</div>
<span className="truncate">
{name} ({symbol})
</span>
</>
);
export const tokenRenderer: ResolvedAddressRenderer<TokenMeta> = (
address,
tokenMeta,
linkable,
dontOverrideColors
) => (
<TokenName
address={address}
name={tokenMeta.name}
symbol={tokenMeta.symbol}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
export default TokenName;

14
src/highlight-init.ts Normal file
View File

@ -0,0 +1,14 @@
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
// @ts-ignore
import hljs from "highlight.js/lib/core";
// @ts-ignore
import json from "highlight.js/lib/languages/json";
import docco from "react-syntax-highlighter/dist/esm/styles/hljs/docco";
import hljsDefineSolidity from "highlightjs-solidity";
hljsDefineSolidity(hljs);
hljs.registerLanguage("json", json);
export { SyntaxHighlighter, docco };

View File

@ -1,15 +1,28 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { HelmetProvider, Helmet } from "react-helmet-async";
import "@fontsource/space-grotesk/index.css"; import "@fontsource/space-grotesk/index.css";
import "@fontsource/roboto/index.css"; import "@fontsource/roboto/index.css";
import "@fontsource/roboto-mono/index.css"; import "@fontsource/roboto-mono/index.css";
import spaceGrotesk from "@fontsource/space-grotesk/files/space-grotesk-latin-400-normal.woff2";
import "./index.css"; import "./index.css";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<HelmetProvider>
<Helmet>
<link
rel="preload"
href={spaceGrotesk}
as="font"
type="font/woff2"
crossOrigin="true"
/>
</Helmet>
<App /> <App />
</HelmetProvider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root") document.getElementById("root")
); );

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,3 +1,5 @@
/// <reference types="react-scripts" /> /// <reference types="react-scripts" />
declare module "use-keyboard-shortcut"; declare module "use-keyboard-shortcut";
declare module "highlightjs-solidity"; declare module "highlightjs-solidity";
declare module "*.woff";
declare module "*.woff2";

View File

@ -14,14 +14,15 @@ import TransactionDirection, {
Flags, Flags,
} from "../components/TransactionDirection"; } from "../components/TransactionDirection";
import TransactionValue from "../components/TransactionValue"; import TransactionValue from "../components/TransactionValue";
import { ENSReverseCache, ProcessedTransaction } from "../types"; import { ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue"; import ETH2USDValue from "../components/ETH2USDValue";
import { ResolvedAddresses } from "../api/address-resolver";
type TransactionItemProps = { type TransactionItemProps = {
tx: ProcessedTransaction; tx: ProcessedTransaction;
ensCache?: ENSReverseCache; resolvedAddresses?: ResolvedAddresses;
selectedAddress?: string; selectedAddress?: string;
feeDisplay: FeeDisplay; feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>; priceMap: Record<BlockTag, BigNumber>;
@ -29,7 +30,7 @@ type TransactionItemProps = {
const TransactionItem: React.FC<TransactionItemProps> = ({ const TransactionItem: React.FC<TransactionItemProps> = ({
tx, tx,
ensCache, resolvedAddresses,
selectedAddress, selectedAddress,
feeDisplay, feeDisplay,
priceMap, priceMap,
@ -50,12 +51,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
} }
} }
const ensFrom = ensCache && tx.from && ensCache[tx.from];
const ensTo = ensCache && tx.to && ensCache[tx.to];
const ensCreated =
ensCache &&
tx.createdContractAddress &&
ensCache[tx.createdContractAddress];
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction;
return ( return (
@ -87,9 +82,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<AddressHighlighter address={tx.from}> <AddressHighlighter address={tx.from}>
<DecoratedAddressLink <DecoratedAddressLink
address={tx.from} address={tx.from}
ensName={ensFrom}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.from} miner={tx.miner === tx.from}
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}
@ -107,18 +102,18 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<AddressHighlighter address={tx.to}> <AddressHighlighter address={tx.to}>
<DecoratedAddressLink <DecoratedAddressLink
address={tx.to} address={tx.to}
ensName={ensTo}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.to} miner={tx.miner === tx.to}
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
) : ( ) : (
<AddressHighlighter address={tx.createdContractAddress!}> <AddressHighlighter address={tx.createdContractAddress!}>
<DecoratedAddressLink <DecoratedAddressLink
address={tx.createdContractAddress!} address={tx.createdContractAddress!}
ensName={ensCreated}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
creation creation
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}
@ -144,4 +139,4 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
); );
}; };
export default React.memo(TransactionItem); export default TransactionItem;

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -103,7 +103,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
); );
return ( return (
<div className="w-full mb-auto"> <div className="w-full flex-grow">
<div className="px-9 pt-3 pb-12 divide-y-2"> <div className="px-9 pt-3 pb-12 divide-y-2">
<div className="relative"> <div className="relative">
<div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 "> <div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 ">

View File

@ -9,7 +9,7 @@ const London: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const block = useLatestBlock(provider); const block = useLatestBlock(provider);
if (!provider || !block) { if (!provider || !block) {
return <></>; return <div className="flex-grow"></div>;
} }
// Display countdown // Display countdown

View File

@ -2,7 +2,7 @@ import { commify } from "@ethersproject/units";
import { ChartData, ChartOptions } from "chart.js"; import { ChartData, ChartOptions } from "chart.js";
import { ExtendedBlock } from "../../useErigonHooks"; import { ExtendedBlock } from "../../useErigonHooks";
export const burntFeesChartOptions: ChartOptions = { export const burntFeesChartOptions: ChartOptions<"line"> = {
animation: false, animation: false,
plugins: { plugins: {
legend: { legend: {
@ -45,7 +45,9 @@ export const burntFeesChartOptions: ChartOptions = {
}, },
}; };
export const burntFeesChartData = (blocks: ExtendedBlock[]): ChartData => ({ export const burntFeesChartData = (
blocks: ExtendedBlock[]
): ChartData<"line"> => ({
labels: blocks.map((b) => b.number.toString()).reverse(), labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [ datasets: [
{ {
@ -68,7 +70,7 @@ export const burntFeesChartData = (blocks: ExtendedBlock[]): ChartData => ({
], ],
}); });
export const gasChartOptions: ChartOptions = { export const gasChartOptions: ChartOptions<"line"> = {
animation: false, animation: false,
interaction: { interaction: {
mode: "index", mode: "index",
@ -112,7 +114,7 @@ export const gasChartOptions: ChartOptions = {
}, },
}; };
export const gasChartData = (blocks: ExtendedBlock[]): ChartData => ({ export const gasChartData = (blocks: ExtendedBlock[]): ChartData<"line"> => ({
labels: blocks.map((b) => b.number.toString()).reverse(), labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [ datasets: [
{ {
@ -148,7 +150,7 @@ export const gasChartData = (blocks: ExtendedBlock[]): ChartData => ({
borderColor: "#B91C1CF0", borderColor: "#B91C1CF0",
tension: 0.2, tension: 0.2,
pointStyle: "crossRot", pointStyle: "crossRot",
radius: 5, pointRadius: 5,
}, },
{ {
label: "Base fee (wei)", label: "Base fee (wei)",

View File

@ -38,6 +38,7 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedParamsTable from "./decoder/DecodedParamsTable";
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes";
import { DevDoc, UserDoc } from "../useSourcify"; import { DevDoc, UserDoc } from "../useSourcify";
import { ResolvedAddresses } from "../api/address-resolver";
type DetailsProps = { type DetailsProps = {
txData: TransactionData; txData: TransactionData;
@ -47,6 +48,7 @@ type DetailsProps = {
internalOps?: InternalOperation[]; internalOps?: InternalOperation[];
sendsEthToMiner: boolean; sendsEthToMiner: boolean;
ethUSDPrice: BigNumber | undefined; ethUSDPrice: BigNumber | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
@ -57,6 +59,7 @@ const Details: React.FC<DetailsProps> = ({
internalOps, internalOps,
sendsEthToMiner, sendsEthToMiner,
ethUSDPrice, ethUSDPrice,
resolvedAddresses,
}) => { }) => {
const hasEIP1559 = const hasEIP1559 =
txData.confirmedData?.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== undefined &&
@ -154,6 +157,7 @@ const Details: React.FC<DetailsProps> = ({
address={txData.from} address={txData.from}
miner={txData.from === txData.confirmedData?.miner} miner={txData.from === txData.confirmedData?.miner}
txFrom txFrom
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.from} /> <Copy value={txData.from} />
@ -171,6 +175,7 @@ const Details: React.FC<DetailsProps> = ({
address={txData.to} address={txData.to}
miner={txData.to === txData.confirmedData?.miner} miner={txData.to === txData.confirmedData?.miner}
txTo txTo
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.to} /> <Copy value={txData.to} />
@ -188,6 +193,7 @@ const Details: React.FC<DetailsProps> = ({
address={txData.confirmedData.createdContractAddress!} address={txData.confirmedData.createdContractAddress!}
creation creation
txTo txTo
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.confirmedData.createdContractAddress!} /> <Copy value={txData.confirmedData.createdContractAddress!} />
@ -200,6 +206,7 @@ const Details: React.FC<DetailsProps> = ({
key={i} key={i}
txData={txData} txData={txData}
internalOp={op} internalOp={op}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</div> </div>
@ -219,6 +226,7 @@ const Details: React.FC<DetailsProps> = ({
t={t} t={t}
txData={txData} txData={txData}
tokenMeta={txData.tokenMetas[t.token]} tokenMeta={txData.tokenMetas[t.token]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</div> </div>

View File

@ -10,14 +10,21 @@ import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature"; import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { useTopic0 } from "../useTopic0"; import { useTopic0 } from "../useTopic0";
import { ResolvedAddresses } from "../api/address-resolver";
type LogEntryProps = { type LogEntryProps = {
txData: TransactionData; txData: TransactionData;
log: Log; log: Log;
logDesc: LogDescription | null | undefined; logDesc: LogDescription | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => { const LogEntry: React.FC<LogEntryProps> = ({
txData,
log,
logDesc,
resolvedAddresses,
}) => {
const rawTopic0 = log.topics[0]; const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0); const topic0 = useTopic0(rawTopic0);
@ -62,6 +69,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
miner={log.address === txData.confirmedData?.miner} miner={log.address === txData.confirmedData?.miner}
txFrom={log.address === txData.from} txFrom={log.address === txData.from}
txTo={log.address === txData.to} txTo={log.address === txData.to}
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={log.address} /> <Copy value={log.address} />

View File

@ -5,13 +5,15 @@ import LogEntry from "./LogEntry";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { useAppConfigContext } from "../useAppConfig"; import { useAppConfigContext } from "../useAppConfig";
import { Metadata, useMultipleMetadata } from "../useSourcify"; import { Metadata, useMultipleMetadata } from "../useSourcify";
import { ResolvedAddresses } from "../api/address-resolver";
type LogsProps = { type LogsProps = {
txData: TransactionData; txData: TransactionData;
metadata: Metadata | null | undefined; metadata: Metadata | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const Logs: React.FC<LogsProps> = ({ txData, metadata }) => { const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
const baseMetadatas = useMemo((): Record<string, Metadata | null> => { const baseMetadatas = useMemo((): Record<string, Metadata | null> => {
if (!txData.to || metadata === undefined) { if (!txData.to || metadata === undefined) {
return {}; return {};
@ -70,6 +72,7 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
txData={txData} txData={txData}
log={l} log={l}
logDesc={logDescs?.[i]} logDesc={logDescs?.[i]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</> </>

View File

@ -32,10 +32,6 @@ export type TransactionChunk = {
lastPage: boolean; lastPage: boolean;
}; };
export type ENSReverseCache = {
[address: string]: string;
};
export type TransactionData = { export type TransactionData = {
transactionHash: string; transactionHash: string;
from: string; from: string;

View File

@ -23,16 +23,12 @@ export enum SourcifySource {
// Centralized Sourcify servers // Centralized Sourcify servers
CENTRAL_SERVER, CENTRAL_SERVER,
// Snapshot server
CUSTOM_SNAPSHOT_SERVER,
} }
const sourcifyIPNS = const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p"; "k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`; const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`; const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const snapshotPrefix = "http://localhost:3006";
const resolveSourcifySource = (source: SourcifySource) => { const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) { if (source === SourcifySource.IPFS_IPNS) {
@ -41,7 +37,8 @@ const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.CENTRAL_SERVER) { if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix; return sourcifyHttpRepoPrefix;
} }
return snapshotPrefix;
throw new Error(`Unknown Sourcify intergration source code: ${source}`);
}; };
export const sourcifyMetadata = ( export const sourcifyMetadata = (

View File

@ -0,0 +1,92 @@
import { useState, useEffect, useRef } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { ProcessedTransaction, TransactionData } from "./types";
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
export type AddressCollector = () => string[];
export const pageCollector =
(page: ProcessedTransaction[] | undefined): AddressCollector =>
() => {
if (!page) {
return [];
}
const uniqueAddresses = new Set<string>();
for (const tx of page) {
if (tx.from) {
uniqueAddresses.add(tx.from);
}
if (tx.to) {
uniqueAddresses.add(tx.to);
}
}
return Array.from(uniqueAddresses);
};
export const transactionDataCollector =
(txData: TransactionData | null | undefined): AddressCollector =>
() => {
if (!txData) {
return [];
}
const uniqueAddresses = new Set<string>();
// Standard fields
uniqueAddresses.add(txData.from);
if (txData.to) {
uniqueAddresses.add(txData.to);
}
if (txData.confirmedData?.createdContractAddress) {
uniqueAddresses.add(txData.confirmedData?.createdContractAddress);
}
// Dig token transfers
for (const t of txData.tokenTransfers) {
uniqueAddresses.add(t.from);
uniqueAddresses.add(t.to);
uniqueAddresses.add(t.token);
}
// Dig log addresses
if (txData.confirmedData) {
for (const l of txData.confirmedData.logs) {
uniqueAddresses.add(l.address);
// TODO: find a way to dig over decoded address log attributes
}
}
return Array.from(uniqueAddresses);
};
export const useResolvedAddresses = (
provider: JsonRpcProvider | undefined,
addrCollector: AddressCollector
) => {
const [names, setNames] = useState<ResolvedAddresses>();
const ref = useRef<ResolvedAddresses | undefined>();
useEffect(() => {
ref.current = names;
});
useEffect(
() => {
if (!provider) {
return;
}
const populate = async () => {
const _addresses = addrCollector();
const _names = await batchPopulate(provider, _addresses, ref.current);
setNames(_names);
};
populate();
},
// DON'T put names variables in dependency array; this is intentional; useRef
[provider, addrCollector]
);
return names;
};

View File

@ -1,47 +0,0 @@
import { useState, useEffect } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { ENSReverseCache, ProcessedTransaction } from "./types";
export const useENSCache = (
provider?: JsonRpcProvider,
page?: ProcessedTransaction[]
) => {
const [reverseCache, setReverseCache] = useState<ENSReverseCache>();
useEffect(() => {
if (!provider || !page) {
return;
}
const addrSet = new Set<string>();
for (const tx of page) {
if (tx.from) {
addrSet.add(tx.from);
}
if (tx.to) {
addrSet.add(tx.to);
}
}
const addresses = Array.from(addrSet);
const reverseResolve = async () => {
const solvers: Promise<string>[] = [];
for (const a of addresses) {
solvers.push(provider.lookupAddress(a));
}
const results = await Promise.all(solvers);
const cache: ENSReverseCache = {};
for (let i = 0; i < results.length; i++) {
if (results[i] === null) {
continue;
}
cache[addresses[i]] = results[i];
}
setReverseCache(cache);
};
reverseResolve();
}, [provider, page]);
return reverseCache;
};

View File

@ -1,23 +1,29 @@
import React, { useState, useContext } from "react"; import {
Dispatch,
SetStateAction,
createContext,
useState,
useContext,
} from "react";
export type Selection = { export type Selection = {
type: "address" | "value"; type: "address" | "value";
content: string; content: string;
}; };
type OptionalSelection = Selection | null;
export const useSelection = (): [ export const useSelection = (): [
Selection | null, OptionalSelection,
React.Dispatch<React.SetStateAction<Selection | null>> Dispatch<SetStateAction<OptionalSelection>>
] => { ] => {
const [selection, setSelection] = useState<Selection | null>(null); return useState<OptionalSelection>(null);
return [selection, setSelection];
}; };
export const SelectionContext = React.createContext< export const SelectionContext = createContext<ReturnType<typeof useSelection>>(
ReturnType<typeof useSelection> null!
>(null!); );
export const useSelectionContext = () => { export const useSelectionContext = () => {
const ctx = useContext(SelectionContext); return useContext(SelectionContext);
return ctx;
}; };

2
topic0

@ -1 +1 @@
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f Subproject commit 5026a20b712c1cad66878821c38e1f070e4a3799