Merge branch 'release/v2021.10.05-otterscan'
This commit is contained in:
commit
53587cacf4
68
Dockerfile
68
Dockerfile
|
@ -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 /
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
34
src/App.tsx
34
src/App.tsx
|
@ -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>
|
||||||
|
|
|
@ -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())}
|
||||||
|
|
15
src/Home.tsx
15
src/Home.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
|
};
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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);
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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")
|
||||||
);
|
);
|
||||||
|
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
@ -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 ">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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
2
topic0
|
@ -1 +1 @@
|
||||||
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f
|
Subproject commit 5026a20b712c1cad66878821c38e1f070e4a3799
|
Loading…
Reference in New Issue