Merge branch 'release/v2022.08.03-otterscan'

This commit is contained in:
Willian Mitsuda 2022-09-02 11:36:13 -03:00
commit 543ba4db40
No known key found for this signature in database
111 changed files with 4995 additions and 30419 deletions

View File

@ -20,12 +20,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.3.4 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Cache Docker layers - name: Cache Docker layers
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: /tmp/.buildx-cache path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }} key: ${{ runner.os }}-buildx-${{ github.sha }}
@ -33,35 +33,35 @@ jobs:
${{ runner.os }}-buildx- ${{ runner.os }}-buildx-
- name: Docker Setup Buildx - name: Docker Setup Buildx
uses: docker/setup-buildx-action@v1.5.1 uses: docker/setup-buildx-action@v2
with: with:
driver: docker-container driver: docker-container
driver-opts: | driver-opts: |
image=moby/buildkit:master image=moby/buildkit:master
- name: Docker Login - name: Docker Login
uses: docker/login-action@v1.10.0 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Login - name: Docker Login
uses: docker/login-action@v1.10.0 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker Metadata action - name: Docker Metadata action
id: meta id: meta
uses: docker/metadata-action@v3.4.1 uses: docker/metadata-action@v4
with: with:
images: | images: |
otterscan/otterscan otterscan/otterscan
ghcr.io/${{ env.IMAGE_NAME }} ghcr.io/${{ env.IMAGE_NAME }}
- name: Build and push Docker images - name: Build and push Docker images
uses: docker/build-push-action@v2.6.1 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true

2
.gitignore vendored
View File

@ -10,6 +10,7 @@
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store
@ -23,3 +24,4 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
/.vscode /.vscode
.gitsigners

2
.nvmrc
View File

@ -1 +1 @@
v16.13.0 v16.16.0

2
4bytes

@ -1 +1 @@
Subproject commit 9603b20bd08ef0089a5fdca385afd3a3251d662c Subproject commit 5197eb52b81b8594b6c5d3de023e649bec9523ca

View File

@ -1,8 +1,8 @@
FROM node:16.14.0-alpine3.15 AS builder FROM node:16.16.0-alpine3.15 AS builder
WORKDIR /otterscan-build WORKDIR /otterscan-build
COPY ["package.json", "package-lock.json", "/otterscan-build/"] COPY ["package.json", "package-lock.json", "/otterscan-build/"]
RUN npm install RUN npm install
COPY ["run-nginx.sh", "tsconfig.json", "craco.config.js", "tailwind.config.js", "/otterscan-build/"] COPY ["run-nginx.sh", "tsconfig.json", "tsconfig.node.json", "postcss.config.js", "tailwind.config.js", "vite.config.ts", "index.html", "/otterscan-build/"]
COPY ["public", "/otterscan-build/public/"] COPY ["public", "/otterscan-build/public/"]
COPY ["src", "/otterscan-build/src/"] COPY ["src", "/otterscan-build/src/"]
RUN npm run build RUN npm run build
@ -97,7 +97,7 @@ 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.d/default.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 nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /otterscan-build/build /usr/share/nginx/html/ COPY --from=builder /otterscan-build/dist /usr/share/nginx/html/
COPY --from=builder /otterscan-build/run-nginx.sh / COPY --from=builder /otterscan-build/run-nginx.sh /
WORKDIR / WORKDIR /

2
chains

@ -1 +1 @@
Subproject commit 9a908009cd88293a08222f75888d7f1bb82c13c8 Subproject commit b725d8e7cce9ceb97b888c9ad5a1eec9495e194d

View File

@ -1,11 +0,0 @@
// craco.config.js
module.exports = {
style: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
}

View File

@ -6,43 +6,41 @@ It depends heavily on a working Erigon installation with Otterscan patches appli
## Install Erigon ## Install Erigon
You will need an Erigon executing node (`erigon`). Also you will need Erigon RPC daemon (`rpcdaemon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing. You will need an Erigon executing node (`erigon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing.
My personal experience: at the moment of this writing (~block 14,000,000), setting up an archive node takes over 5-6 days and ~1.7 TB of SSD. My personal experience: at the moment of this writing (~block 15,000,000), setting up an archive node takes over 3-7 days (depending on your hardware) and ~1.6 TB of SSD.
They have weekly stable releases, make sure you are running on of them, not development ones. They have weekly alpha releases, make sure you are running one of them, not development ones.
## Install Otterscan-patched rpcdaemon ## Install Otterscan-patched erigon
We rely on custom JSON-RPC APIs which are not available in a standard ETH node. We keep a separated repository containing an Erigon fork here: https://github.com/wmitsuda/erigon. We rely on custom JSON-RPC APIs which are not available in a standard ETH node. We keep a separated repository containing an Erigon fork here: https://github.com/wmitsuda/erigon.
Please follow the instructions in the repository `README` and replace the original Erigon `rpcdaemon` with our patched one. Please follow the instructions in the repository `README` and replace the original Erigon `erigon` with our patched one.
## Enable Otterscan namespace on rpcdaemon ## Enable Otterscan namespace on erigon
When running `rpcdaemon`, make sure to enable the `erigon`, `ots`, `eth` APIs in addition to whatever cli options you are using to start `rpcdaemon`. When running `erigon`, make sure to enable the `erigon`, `ots`, `eth` APIs in addition to whatever cli options you are using to start `erigon`.
`ots` stands for Otterscan and it is the namespace we use for our own custom APIs. `ots` stands for Otterscan and it is the namespace we use for our own custom APIs.
``` ```
<path-to-rpcdaemon-binary>/rpcdaemon --http.api "eth,erigon,ots,<your-other-apis>" --private.api.addr 127.0.0.1:9090 --datadir <erigon-datadir> --http.corsdomain "*" <path-to-erigon-binary>/erigon --http.api "eth,erigon,ots,<your-other-apis>" --datadir <erigon-datadir> --http.corsdomain "*"
``` ```
Be sure to include both `--private.api.addr` and `--datadir` parameter so you run it in dual mode, otherwise the performance will be much worse. Pay attention to the `--http.corsdomain` parameter, CORS is **required** for the browser to call the node directly.
Also pay attention to the `--http.corsdomain` parameter, CORS is **required** for the browser to call the node directly. Now you should have an Erigon node with Otterscan JSON-RPC APIs and CORS enabled.
Now you should have an Erigon node with Otterscan JSON-RPC APIs enabled, running in dual mode with CORS enabled.
## Run Otterscan docker image from Docker Hub ## Run Otterscan docker image from Docker Hub
The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories). The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories).
``` ```
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag> docker run --rm -p 5100:80 --name otterscan -d otterscan/otterscan:<versiontag>
``` ```
This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5000 (see the `-p` docker run parameter). This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5100 (see the `-p` docker run parameter).
To stop Otterscan service, run: To stop Otterscan service, run:
@ -53,11 +51,27 @@ docker stop otterscan
By default it assumes your Erigon node is at `http://127.0.0.1:8545`. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`: By default it assumes your Erigon node is at `http://127.0.0.1:8545`. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`:
``` ```
docker run --rm -p 5000:80 --name otterscan -d --env ERIGON_URL="<your-erigon-node-url>" otterscan/otterscan:<versiontag> docker run --rm -p 5100:80 --name otterscan -d --env ERIGON_URL="<your-erigon-node-url>" otterscan/otterscan:<versiontag>
``` ```
This is the preferred way to run Otterscan. You can read about other ways [here](./other-ways-to-run-otterscan.md). This is the preferred way to run Otterscan. You can read about other ways [here](./other-ways-to-run-otterscan.md).
## (Optional) Enable integration with beacon chain
You can optionally enable displaying extra info from the beacon chain by providing the public URL of your beacon node API.
Enabling the beacon chain API depends on which CL implementation you are using.
> As an example, for Prysm you need to enable CORS and possibly bind the address to the correct network interface with `--grpc-gateway-host="0.0.0.0" --grpc-gateway-corsdomain='*'` and by default it binds it to the port 3500.
When starting the Otterscan process via Docker, you need to add an extra env variable called `BEACON_API_URL` pointing to your beacon node API URL.
Prysm example:
```
docker run --rm -p 5100:80 --name otterscan -d --env BEACON_API_URL="<your-beacon-node-api-url>" otterscan/otterscan:<versiontag>
```
## Validating the installation (all methods) ## Validating the installation (all methods)
You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button. You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button.

View File

@ -16,7 +16,7 @@ Clone Otterscan repo and its submodules. Checkout the tag corresponding to your
git clone --recurse-submodules https://github.com/wmitsuda/otterscan.git git clone --recurse-submodules https://github.com/wmitsuda/otterscan.git
cd otterscan cd otterscan
git checkout <version-tag-otterscan> git checkout <version-tag-otterscan>
DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile . docker buildx build -t otterscan .
``` ```
This will run the entire build process inside a build container, merge the production build of the React app with the 4bytes and trustwallet assets into the same image format it is published in Docker Hub, but locally under the name `otterscan`. This will run the entire build process inside a build container, merge the production build of the React app with the 4bytes and trustwallet assets into the same image format it is published in Docker Hub, but locally under the name `otterscan`.
@ -40,7 +40,7 @@ First, a brief explanation about the app:
These instructions are subjected to changes in future for the sake of simplification. These instructions are subjected to changes in future for the sake of simplification.
Make sure you have a working node 14/npm 7 installation. Make sure you have a working node 16/npm 8 installation.
By default, it assumes your Erigon `rpcdaemon` processs is serving requests at `http://localhost:8545`. You can customize this URL by changing the `public/config.json` file. By default, it assumes your Erigon `rpcdaemon` processs is serving requests at `http://localhost:8545`. You can customize this URL by changing the `public/config.json` file.

25
index.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Erigon based block explorer" />
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<title>Otterscan</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

31747
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,58 +4,54 @@
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0", "@chainlink/contracts": "^0.4.2",
"@chainlink/contracts": "^0.4.0", "@fontsource/fira-code": "^4.5.11",
"@craco/craco": "^6.4.3", "@fontsource/roboto": "^4.5.8",
"@fontsource/fira-code": "^4.5.8", "@fontsource/roboto-mono": "^4.5.8",
"@fontsource/roboto": "^4.5.5", "@fontsource/space-grotesk": "^4.5.9",
"@fontsource/roboto-mono": "^4.5.5", "@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fontsource/space-grotesk": "^4.5.5", "@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.2.0",
"@fortawesome/free-solid-svg-icons": "^6.1.1", "@headlessui/react": "^1.6.6",
"@fortawesome/react-fontawesome": "^0.1.18", "@otterscan/react-qr-reader": "^5.2.0",
"@headlessui/react": "^1.5.0", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0",
"@testing-library/react": "^11.1.0",
"@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": "^16.11.14", "@types/node": "^16.11.56",
"@types/react": "^17.0.43", "@types/react": "^18.0.15",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^18.0.6",
"@types/react-highlight": "^0.12.5", "@types/react-highlight": "^0.12.5",
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^15.5.4",
"chart.js": "^3.7.1", "chart.js": "^3.9.1",
"ethers": "^5.6.2", "ethers": "^5.7.0",
"highlightjs-solidity": "^2.0.5", "highlightjs-solidity": "^2.0.5",
"react": "^17.0.2", "react": "^18.2.0",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",
"react-chartjs-2": "^4.0.0", "react-chartjs-2": "^4.3.1",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.2.3", "react-helmet-async": "^1.3.0",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"serve": "^13.0.2", "swr": "^1.3.0",
"swr": "^1.2.2", "typescript": "^4.8.2",
"typescript": "^4.6.3",
"use-keyboard-shortcut": "^1.1.4", "use-keyboard-shortcut": "^1.1.4",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "vite",
"build": "craco build && compress-cra", "build": "tsc && vite build",
"test": "craco test", "preview": "vite preview",
"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/1 -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/chains/_data/chains:/usr/share/nginx/html/chains/ -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": "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/1 -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/chains/_data/chains:/usr/share/nginx/html/chains/ -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/1 -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/chains/_data/chains:/usr/share/nginx/html/chains/ -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/1 -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/chains/_data/chains:/usr/share/nginx/html/chains/ -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 buildx build -t otterscan .",
"docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan", "docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan",
"docker-stop": "docker stop otterscan" "docker-stop": "docker stop otterscan"
}, },
@ -78,10 +74,12 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^9.8.8", "@vitejs/plugin-react": "^2.0.1",
"compress-create-react-app": "^1.2.1", "autoprefixer": "^10.4.8",
"postcss": "^7.0.39", "postcss": "^8.4.16",
"source-map-explorer": "^2.5.2", "source-map-explorer": "^2.5.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6" "tailwindcss": "^3.1.8",
"vite": "^3.0.9",
"vite-plugin-compression": "^0.5.1"
} }
} }

7
postcss.config.js Normal file
View File

@ -0,0 +1,7 @@
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,4 +1,5 @@
{ {
"erigonURL": "http://localhost:8545", "erigonURL": "http://localhost:8545",
"beaconAPI": null,
"assetsURLPrefix": "http://localhost:3001" "assetsURLPrefix": "http://localhost:3001"
} }

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Erigon based block explorer" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Otterscan</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .), \"assetsURLPrefix\": \"\"}" PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .), \"beaconAPI\": $(echo $BEACON_API_URL | jq -aR .), \"assetsURLPrefix\": \"\"}"
echo $PARAMS > /usr/share/nginx/html/config.json echo $PARAMS > /usr/share/nginx/html/config.json
nginx -g "daemon off;" nginx -g "daemon off;"

View File

@ -1,206 +1,28 @@
import React, { useEffect, useContext, useCallback, useMemo } from "react"; import React from "react";
import { import { useSearchParams } from "react-router-dom";
useParams, import AddressMainPage from "./AddressMainPage";
useNavigate,
Routes,
Route,
useSearchParams,
} from "react-router-dom";
import { Tab } from "@headlessui/react";
import Blockies from "react-blockies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
import Copy from "./components/Copy";
import Faucet from "./components/Faucet";
import NavTab from "./components/NavTab";
import SourcifyLogo from "./sourcify/SourcifyLogo";
import AddressTransactionResults from "./address/AddressTransactionResults";
import Contracts from "./address/Contracts";
import { RuntimeContext } from "./useRuntime";
import { useAppConfigContext } from "./useAppConfig";
import { useAddressOrENS } from "./useResolvedAddresses";
import { useMultipleMetadata } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types";
import { useAddressesWithCode } from "./useErigonHooks";
import { useChainInfo } from "./useChainInfo";
const AddressTransactionByNonce = React.lazy( const AddressTransactionByNonce = React.lazy(
() => () => import("./AddressTransactionByNonce")
import(
/* webpackChunkName: "addresstxbynonce", webpackPrefetch: true */ "./AddressTransactionByNonce"
)
); );
/**
* This is the default handler for /address/* URL path.
*
* It can redirect to different child components depending on search
* query params, so it is not possible to use default path routing
* mechanisms to declarative-model them.
*/
const Address: React.FC = () => { const Address: React.FC = () => {
const { provider } = useContext(RuntimeContext);
const { addressOrName, direction } = useParams();
if (addressOrName === undefined) {
throw new Error("addressOrName couldn't be undefined here");
}
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const urlFixer = useCallback(
(address: ChecksummedAddress) => {
navigate(
`/address/${address}${
direction ? "/" + direction : ""
}?${searchParams.toString()}`,
{ replace: true }
);
},
[navigate, direction, searchParams]
);
const [checksummedAddress, isENS, error] = useAddressOrENS(
addressOrName,
urlFixer
);
useEffect(() => {
if (isENS || checksummedAddress === undefined) {
document.title = `Address ${addressOrName} | Otterscan`;
} else {
document.title = `Address ${checksummedAddress} | Otterscan`;
}
}, [addressOrName, checksummedAddress, isENS]);
const { sourcifySource } = useAppConfigContext();
const checksummedAddressAsArray = useMemo(
() => (checksummedAddress !== undefined ? [checksummedAddress] : []),
[checksummedAddress]
);
const contractAddresses = useAddressesWithCode(
provider,
checksummedAddressAsArray
);
const metadatas = useMultipleMetadata(
undefined,
contractAddresses,
provider?.network.chainId,
sourcifySource
);
const addressMetadata =
checksummedAddress !== undefined
? metadatas[checksummedAddress]
: undefined;
const { network, faucets } = useChainInfo();
// Search address by nonce === transaction @ nonce // Search address by nonce === transaction @ nonce
const [searchParams] = useSearchParams();
const rawNonce = searchParams.get("nonce"); const rawNonce = searchParams.get("nonce");
if (rawNonce !== null) { if (rawNonce !== null) {
return ( return <AddressTransactionByNonce rawNonce={rawNonce} />;
<AddressTransactionByNonce
checksummedAddress={checksummedAddress}
rawNonce={rawNonce}
/>
);
} }
return ( // Standard address main page with tabs
<StandardFrame> return <AddressMainPage />;
{error ? (
<AddressOrENSNameNotFound
addressOrENSName={addressOrName}
supportsENS={provider?.network.ensAddress !== undefined}
/>
) : (
checksummedAddress && (
<>
<StandardSubtitle>
<div className="flex space-x-2 items-baseline">
<Blockies
className="self-center rounded"
seed={checksummedAddress.toLowerCase()}
scale={3}
/>
<span>Address</span>
<span className="font-address text-base text-gray-500">
{checksummedAddress}
</span>
<Copy value={checksummedAddress} rounded />
{/* Only display faucets for testnets who actually have any */}
{network === "testnet" && faucets && faucets.length > 0 && (
<Faucet address={checksummedAddress} rounded />
)}
{isENS && (
<span className="rounded-lg px-2 py-1 bg-gray-200 text-gray-500 text-xs">
ENS: {addressOrName}
</span>
)}
</div>
</StandardSubtitle>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href={`/address/${addressOrName}`}>Overview</NavTab>
{(contractAddresses?.length ?? 0) > 0 && (
<NavTab href={`/address/${addressOrName}/contract`}>
<span
className={`flex items-baseline space-x-2 ${
addressMetadata === undefined ? "italic opacity-50" : ""
}`}
>
<span>Contract</span>
{addressMetadata === undefined ? (
<span className="self-center">
<FontAwesomeIcon
className="animate-spin"
icon={faCircleNotch}
/>
</span>
) : addressMetadata === null ? (
<span className="self-center text-red-500">
<FontAwesomeIcon icon={faQuestionCircle} />
</span>
) : (
<span className="self-center">
<SourcifyLogo />
</span>
)}
</span>
</NavTab>
)}
</Tab.List>
<Tab.Panels>
<Routes>
<Route
index
element={
<AddressTransactionResults address={checksummedAddress} />
}
/>
<Route
path="txs/:direction"
element={
<AddressTransactionResults address={checksummedAddress} />
}
/>
<Route
path="contract"
element={
<Contracts
checksummedAddress={checksummedAddress}
rawMetadata={
contractAddresses !== undefined &&
contractAddresses.length === 0
? null
: addressMetadata
}
/>
}
/>
</Routes>
</Tab.Panels>
</Tab.Group>
</>
)
)}
</StandardFrame>
);
}; };
export default Address; export default Address;

170
src/AddressMainPage.tsx Normal file
View File

@ -0,0 +1,170 @@
import React, { useEffect, useCallback, useContext } from "react";
import {
Routes,
Route,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
import { Tab } from "@headlessui/react";
import Blockies from "react-blockies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
import Copy from "./components/Copy";
import Faucet from "./components/Faucet";
import NavTab from "./components/NavTab";
import SourcifyLogo from "./sourcify/SourcifyLogo";
import AddressTransactionResults from "./address/AddressTransactionResults";
import Contracts from "./address/Contracts";
import { RuntimeContext } from "./useRuntime";
import { useHasCode } from "./useErigonHooks";
import { useChainInfo } from "./useChainInfo";
import { useAddressOrENS } from "./useResolvedAddresses";
import { useSourcifyMetadata } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types";
type AddressMainPageProps = {};
const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
const { addressOrName, direction } = useParams();
if (addressOrName === undefined) {
throw new Error("addressOrName couldn't be undefined here");
}
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const urlFixer = useCallback(
(address: ChecksummedAddress) => {
navigate(
`/address/${address}${
direction ? "/" + direction : ""
}?${searchParams.toString()}`,
{ replace: true }
);
},
[navigate, direction, searchParams]
);
const [checksummedAddress, isENS, error] = useAddressOrENS(
addressOrName,
urlFixer
);
const { provider } = useContext(RuntimeContext);
const hasCode = useHasCode(provider, checksummedAddress, "latest");
const match = useSourcifyMetadata(
hasCode ? checksummedAddress : undefined,
provider?.network.chainId
);
const { network, faucets } = useChainInfo();
useEffect(() => {
if (isENS || checksummedAddress === undefined) {
document.title = `Address ${addressOrName} | Otterscan`;
} else {
document.title = `Address ${checksummedAddress} | Otterscan`;
}
}, [addressOrName, checksummedAddress, isENS]);
return (
<StandardFrame>
{error ? (
<AddressOrENSNameNotFound
addressOrENSName={addressOrName}
supportsENS={provider?.network.ensAddress !== undefined}
/>
) : (
checksummedAddress && (
<>
<StandardSubtitle>
<div className="flex space-x-2 items-baseline">
<Blockies
className="self-center rounded"
seed={checksummedAddress.toLowerCase()}
scale={3}
/>
<span>Address</span>
<span className="font-address text-base text-gray-500">
{checksummedAddress}
</span>
<Copy value={checksummedAddress} rounded />
{/* Only display faucets for testnets who actually have any */}
{network === "testnet" && faucets && faucets.length > 0 && (
<Faucet address={checksummedAddress} rounded />
)}
{isENS && (
<span className="rounded-lg px-2 py-1 bg-gray-200 text-gray-500 text-xs">
ENS: {addressOrName}
</span>
)}
</div>
</StandardSubtitle>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href={`/address/${addressOrName}`}>Overview</NavTab>
{hasCode && (
<NavTab href={`/address/${addressOrName}/contract`}>
<span
className={`flex items-baseline space-x-2 ${
match === undefined ? "italic opacity-50" : ""
}`}
>
<span>Contract</span>
{match === undefined ? (
<span className="self-center">
<FontAwesomeIcon
className="animate-spin"
icon={faCircleNotch}
/>
</span>
) : match === null ? (
<span className="self-center text-red-500">
<FontAwesomeIcon icon={faQuestionCircle} />
</span>
) : (
<span className="self-center">
<SourcifyLogo />
</span>
)}
</span>
</NavTab>
)}
</Tab.List>
<Tab.Panels>
<Routes>
<Route
index
element={
<AddressTransactionResults address={checksummedAddress} />
}
/>
<Route
path="txs/:direction"
element={
<AddressTransactionResults address={checksummedAddress} />
}
/>
<Route
path="contract"
element={
<Contracts
checksummedAddress={checksummedAddress}
match={match}
/>
}
/>
</Routes>
</Tab.Panels>
</Tab.Group>
</>
)
)}
</StandardFrame>
);
};
export default AddressMainPage;

View File

@ -1,24 +1,47 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
import AddressOrENSNameInvalidNonce from "./components/AddressOrENSNameInvalidNonce"; import AddressOrENSNameInvalidNonce from "./components/AddressOrENSNameInvalidNonce";
import AddressOrENSNameNoTx from "./components/AddressOrENSNameNoTx"; import AddressOrENSNameNoTx from "./components/AddressOrENSNameNoTx";
import { ChecksummedAddress } from "./types";
import { transactionURL } from "./url";
import { useTransactionBySenderAndNonce } from "./useErigonHooks"; import { useTransactionBySenderAndNonce } from "./useErigonHooks";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useAddressOrENS } from "./useResolvedAddresses";
import { ChecksummedAddress } from "./types";
import { transactionURL } from "./url";
type AddressTransactionByNonceProps = { type AddressTransactionByNonceProps = {
checksummedAddress: ChecksummedAddress | undefined;
rawNonce: string; rawNonce: string;
}; };
const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({ const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
checksummedAddress,
rawNonce, rawNonce,
}) => { }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const { addressOrName, direction } = useParams();
if (addressOrName === undefined) {
throw new Error("addressOrName couldn't be undefined here");
}
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const urlFixer = useCallback(
(address: ChecksummedAddress) => {
navigate(
`/address/${address}${
direction ? "/" + direction : ""
}?${searchParams.toString()}`,
{ replace: true }
);
},
[navigate, direction, searchParams]
);
const [checksummedAddress, , ensError] = useAddressOrENS(
addressOrName,
urlFixer
);
// Calculate txCount ONLY when asked for latest nonce // Calculate txCount ONLY when asked for latest nonce
const [txCount, setTxCount] = useState<number | undefined>(); const [txCount, setTxCount] = useState<number | undefined>();
useEffect(() => { useEffect(() => {
@ -54,14 +77,21 @@ const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
checksummedAddress, checksummedAddress,
nonce !== undefined && isNaN(nonce) ? undefined : nonce nonce !== undefined && isNaN(nonce) ? undefined : nonce
); );
const navigate = useNavigate();
// Invalid ENS
if (ensError) {
return (
<StandardFrame>
<AddressOrENSNameNotFound
addressOrENSName={addressOrName}
supportsENS={provider?.network.ensAddress !== undefined}
/>
</StandardFrame>
);
}
// Loading... // Loading...
if ( if (checksummedAddress === undefined || nonce === undefined) {
checksummedAddress === undefined ||
nonce === undefined ||
txHash === undefined
) {
return <StandardFrame />; return <StandardFrame />;
} }
@ -86,6 +116,11 @@ const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
); );
} }
// Valid nonce, waiting tx load
if (txHash === undefined) {
return <StandardFrame />;
}
// Valid nonce, but no tx found // Valid nonce, but no tx found
if (txHash === null) { if (txHash === null) {
return ( return (

View File

@ -9,35 +9,13 @@ import { ConnectionStatus } from "./types";
import { RuntimeContext, useRuntime } from "./useRuntime"; import { RuntimeContext, useRuntime } from "./useRuntime";
import { ChainInfoContext, useChainInfoFromMetadataFile } from "./useChainInfo"; import { ChainInfoContext, useChainInfoFromMetadataFile } from "./useChainInfo";
const Block = React.lazy( const Block = React.lazy(() => import("./Block"));
() => import(/* webpackChunkName: "block", webpackPrefetch: true */ "./Block") const BlockTransactions = React.lazy(() => import("./BlockTransactions"));
); const Address = React.lazy(() => import("./Address"));
const BlockTransactions = React.lazy( const Transaction = React.lazy(() => import("./Transaction"));
() => const London = React.lazy(() => import("./special/london/London"));
import( const Faucets = React.lazy(() => import("./Faucets"));
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions" const PageNotFound = React.lazy(() => import("./PageNotFound"));
)
);
const Address = React.lazy(
() =>
import(/* webpackChunkName: "address", webpackPrefetch: true */ "./Address")
);
const Transaction = React.lazy(
() =>
import(/* webpackChunkName: "tx", webpackPrefetch: true */ "./Transaction")
);
const London = React.lazy(
() => import(/* webpackChunkName: "london" */ "./special/london/London")
);
const Faucets = React.lazy(
() => import(/* webpackChunkName: "faucets" */ "./Faucets")
);
const PageNotFound = React.lazy(
() =>
import(
/* webpackChunkName: "notfound", webpackPrefetch: true */ "./PageNotFound"
)
);
const App = () => { const App = () => {
const runtime = useRuntime(); const runtime = useRuntime();

View File

@ -1,6 +1,5 @@
import React, { useEffect, useMemo, useContext } from "react"; import React, { useEffect, useMemo, useContext } from "react";
import { useParams, NavLink } from "react-router-dom"; import { useParams, NavLink } from "react-router-dom";
import { BigNumber } from "@ethersproject/bignumber";
import { commify } from "@ethersproject/units"; import { commify } from "@ethersproject/units";
import { toUtf8String } from "@ethersproject/strings"; import { toUtf8String } from "@ethersproject/strings";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -12,13 +11,13 @@ import ContentFrame from "./ContentFrame";
import BlockNotFound from "./components/BlockNotFound"; import BlockNotFound from "./components/BlockNotFound";
import InfoRow from "./components/InfoRow"; import InfoRow from "./components/InfoRow";
import Timestamp from "./components/Timestamp"; import Timestamp from "./components/Timestamp";
import BlockReward from "./BlockReward";
import GasValue from "./components/GasValue"; import GasValue from "./components/GasValue";
import PercentageBar from "./components/PercentageBar"; import PercentageBar from "./components/PercentageBar";
import BlockLink from "./components/BlockLink"; import BlockLink from "./components/BlockLink";
import DecoratedAddressLink from "./components/DecoratedAddressLink"; import DecoratedAddressLink from "./components/DecoratedAddressLink";
import TransactionValue from "./components/TransactionValue"; import TransactionValue from "./components/TransactionValue";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import ETH2USDValue from "./components/ETH2USDValue";
import USDValue from "./components/USDValue"; import USDValue from "./components/USDValue";
import HexValue from "./components/HexValue"; import HexValue from "./components/HexValue";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
@ -55,7 +54,6 @@ const Block: React.FC = () => {
}, [block]); }, [block]);
const burntFees = const burntFees =
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
const netFeeReward = block?.feeReward ?? BigNumber.from(0);
const gasUsedPerc = const gasUsedPerc =
block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100; block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100;
@ -89,7 +87,7 @@ const Block: React.FC = () => {
</InfoRow> </InfoRow>
<InfoRow title="Transactions"> <InfoRow title="Transactions">
<NavLink <NavLink
className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded-lg px-2 py-1 text-xs" className="bg-link-blue/10 text-link-blue hover:bg-link-blue/100 hover:text-white rounded-lg px-2 py-1 text-xs"
to={blockTxsURL(block.number)} to={blockTxsURL(block.number)}
> >
{block.transactionCount} transactions {block.transactionCount} transactions
@ -100,25 +98,7 @@ const Block: React.FC = () => {
<DecoratedAddressLink address={block.miner} miner /> <DecoratedAddressLink address={block.miner} miner />
</InfoRow> </InfoRow>
<InfoRow title="Block Reward"> <InfoRow title="Block Reward">
<TransactionValue value={block.blockReward.add(netFeeReward)} /> <BlockReward block={block} />
{!netFeeReward.isZero() && (
<>
{" "}
(<TransactionValue value={block.blockReward} hideUnit /> +{" "}
<TransactionValue value={netFeeReward} hideUnit />)
</>
)}
{blockETHUSDPrice && (
<>
{" "}
<span className="px-2 border-yellow-200 border rounded-lg bg-yellow-100 text-yellow-600">
<ETH2USDValue
ethAmount={block.blockReward.add(netFeeReward)}
eth2USDValue={blockETHUSDPrice}
/>
</span>
</>
)}
</InfoRow> </InfoRow>
<InfoRow title="Uncles Reward"> <InfoRow title="Uncles Reward">
<TransactionValue value={block.unclesReward} /> <TransactionValue value={block.unclesReward} />

47
src/BlockReward.tsx Normal file
View File

@ -0,0 +1,47 @@
import React, { useContext } from "react";
import { BigNumber } from "@ethersproject/bignumber";
import TransactionValue from "./components/TransactionValue";
import FiatValue from "./components/FiatValue";
import { RuntimeContext } from "./useRuntime";
import { ExtendedBlock } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
type BlockRewardProps = {
block: ExtendedBlock;
};
const BlockReward: React.FC<BlockRewardProps> = ({ block }) => {
const { provider } = useContext(RuntimeContext);
const eth2USDValue = useETHUSDOracle(provider, block.number);
const netFeeReward = block?.feeReward ?? BigNumber.from(0);
const value = eth2USDValue
? block.blockReward
.add(netFeeReward)
.mul(eth2USDValue)
.div(10 ** 8)
: undefined;
return (
<>
<TransactionValue value={block.blockReward.add(netFeeReward)} />
{!netFeeReward.isZero() && (
<>
{" "}
(<TransactionValue value={block.blockReward} hideUnit /> +{" "}
<TransactionValue value={netFeeReward} hideUnit />)
</>
)}
{value && (
<>
{" "}
<span className="px-2 border-amber-200 border rounded-lg bg-amber-100 text-amber-600">
<FiatValue value={value} />
</span>
</>
)}
</>
);
};
export default BlockReward;

View File

@ -40,7 +40,6 @@ const BlockTransactions: React.FC = () => {
<StandardFrame> <StandardFrame>
<BlockTransactionHeader blockTag={blockNumber.toNumber()} /> <BlockTransactionHeader blockTag={blockNumber.toNumber()} />
<BlockTransactionResults <BlockTransactionResults
blockTag={blockNumber.toNumber()}
page={txs} page={txs}
total={totalTxs ?? 0} total={totalTxs ?? 0}
pageNumber={pageNumber} pageNumber={pageNumber}

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock"; import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
@ -90,7 +90,8 @@ type StepProps = {
msg: string; msg: string;
}; };
const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => ( const Step: React.FC<PropsWithChildren<StepProps>> = React.memo(
({ type, msg, children }) => (
<> <>
<div className="flex space-x-2"> <div className="flex space-x-2">
{type === "wait" && ( {type === "wait" && (
@ -99,7 +100,7 @@ const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => (
</span> </span>
)} )}
{type === "ok" && ( {type === "ok" && (
<span className="text-green-600"> <span className="text-emerald-600">
<FontAwesomeIcon icon={faCheckCircle} size="1x" /> <FontAwesomeIcon icon={faCheckCircle} size="1x" />
</span> </span>
)} )}
@ -112,6 +113,7 @@ const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => (
</div> </div>
{children && <div className="ml-7 mt-4 text-sm">{children}</div>} {children && <div className="ml-7 mt-4 text-sm">{children}</div>}
</> </>
)); )
);
export default React.memo(ConnectionErrorPanel); export default React.memo(ConnectionErrorPanel);

View File

@ -1,10 +1,13 @@
import React from "react"; import React, { PropsWithChildren } from "react";
type ContentFrameProps = { type ContentFrameProps = {
tabs?: boolean; tabs?: boolean;
}; };
const ContentFrame: React.FC<ContentFrameProps> = ({ tabs, children }) => { const ContentFrame: React.FC<PropsWithChildren<ContentFrameProps>> = ({
tabs,
children,
}) => {
return tabs ? ( return tabs ? (
<div className="divide-y border rounded-b-lg px-3 bg-white">{children}</div> <div className="divide-y border rounded-b-lg px-3 bg-white">{children}</div>
) : ( ) : (

View File

@ -39,7 +39,7 @@ const Faucets: React.FC = () => {
<ContentFrame> <ContentFrame>
<div className="py-4 space-y-3"> <div className="py-4 space-y-3">
{urls.length > 0 && ( {urls.length > 0 && (
<div className="flex space-x-2 items-baseline rounded bg-yellow-200 text-red-800 font-bold underline px-2 py-1"> <div className="flex space-x-2 items-baseline rounded bg-amber-200 text-red-800 font-bold underline px-2 py-1">
<FontAwesomeIcon <FontAwesomeIcon
className="self-center" className="self-center"
icon={faTriangleExclamation} icon={faTriangleExclamation}
@ -54,7 +54,7 @@ const Faucets: React.FC = () => {
)} )}
{/* Display the shuffling notice only if there are 1+ faucets */} {/* Display the shuffling notice only if there are 1+ faucets */}
{urls.length > 1 && ( {urls.length > 1 && (
<div className="flex space-x-2 items-baseline rounded bg-yellow-200 text-yellow-700 px-2 py-1"> <div className="flex space-x-2 items-baseline rounded bg-amber-200 text-amber-700 px-2 py-1">
<FontAwesomeIcon <FontAwesomeIcon
className="self-center" className="self-center"
icon={faTriangleExclamation} icon={faTriangleExclamation}

View File

@ -7,9 +7,10 @@ import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode";
import Logo from "./Logo"; import Logo from "./Logo";
import Timestamp from "./components/Timestamp"; import Timestamp from "./components/Timestamp";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useLatestBlock } from "./useLatestBlock"; import { useLatestBlockHeader } from "./useLatestBlock";
import { blockURL } from "./url"; import { blockURL } from "./url";
import { useGenericSearch } from "./search/search"; import { useGenericSearch } from "./search/search";
import { useFinalizedSlot, useSlotTime } from "./useBeacon";
const CameraScanner = React.lazy(() => import("./search/CameraScanner")); const CameraScanner = React.lazy(() => import("./search/CameraScanner"));
@ -17,19 +18,21 @@ const Home: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const [searchRef, handleChange, handleSubmit] = useGenericSearch(); const [searchRef, handleChange, handleSubmit] = useGenericSearch();
const latestBlock = useLatestBlock(provider); const latestBlock = useLatestBlockHeader(provider);
const beaconData = useFinalizedSlot();
const slotTime = useSlotTime(beaconData?.data.header.message.slot);
const [isScanning, setScanning] = useState<boolean>(false); const [isScanning, setScanning] = useState<boolean>(false);
document.title = "Home | Otterscan"; document.title = "Home | Otterscan";
return ( return (
<div className="mx-auto flex flex-col flex-grow pb-5"> <div className="flex flex-col items-center 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"> <div className="grow mt-5 mb-10 max-h-64 flex items-end">
<Logo /> <Logo />
</div> </div>
<form <form
className="flex flex-col" className="flex flex-col w-1/3"
onSubmit={handleSubmit} onSubmit={handleSubmit}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
@ -62,7 +65,6 @@ const Home: React.FC = () => {
Search Search
</button> </button>
</form> </form>
<div className="mx-auto h-32">
<div className="text-lg text-link-blue hover:text-link-blue-hover font-bold"> <div className="text-lg text-link-blue hover:text-link-blue-hover font-bold">
{provider?.network.chainId !== 11155111 && ( {provider?.network.chainId !== 11155111 && (
<NavLink to="/special/london"> <NavLink to="/special/london">
@ -87,7 +89,20 @@ const Home: React.FC = () => {
<Timestamp value={latestBlock.timestamp} /> <Timestamp value={latestBlock.timestamp} />
</NavLink> </NavLink>
)} )}
{beaconData && (
<div className="flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500">
<div>
Finalized slot: {commify(beaconData.data.header.message.slot)}
</div> </div>
{slotTime && <Timestamp value={slotTime} />}
<div>
State root:{" "}
<span className="font-hash">
{beaconData.data.header.message.state_root}
</span>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -2,7 +2,7 @@ import React from "react";
import Otter from "./otter.jpg"; import Otter from "./otter.jpg";
const Logo: React.FC = () => ( const Logo: React.FC = () => (
<div className="mx-auto text-6xl text-link-blue font-title font-bold cursor-default flex items-center space-x-4"> <div className="text-6xl text-link-blue font-title font-bold cursor-default flex items-center justify-center space-x-4">
<img <img
className="rounded-full" className="rounded-full"
src={Otter} src={Otter}

View File

@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import Header from "./Header"; import Header from "./Header";
import { AppConfig, AppConfigContext } from "./useAppConfig"; import { AppConfig, AppConfigContext } from "./useAppConfig";
import { SourcifySource } from "./url"; import { SourcifySource } from "./sourcify/useSourcify";
const Main: React.FC = () => { const Main: React.FC = () => {
const [sourcifySource, setSourcifySource] = useState<SourcifySource>( const [sourcifySource, setSourcifySource] = useState<SourcifySource>(

View File

@ -1,58 +1,28 @@
import React, { useState, useEffect, useMemo, useContext } from "react"; import React, { useMemo, useContext } from "react";
import { Contract } from "@ethersproject/contracts";
import { commify, formatUnits } from "@ethersproject/units"; import { commify, formatUnits } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump"; import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump";
import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { formatValue } from "./components/formatter"; import { formatValue } from "./components/formatter";
import { useLatestBlock } from "./useLatestBlock"; import { useLatestBlockHeader } from "./useLatestBlock";
import { useChainInfo } from "./useChainInfo"; import { useChainInfo } from "./useChainInfo";
import { useETHUSDRawOracle, useFastGasRawOracle } from "./usePriceOracle";
// TODO: encapsulate this magic number
const ETH_FEED_DECIMALS = 8; const ETH_FEED_DECIMALS = 8;
// TODO: reduce duplication with useETHUSDOracle
const PriceBox: React.FC = () => { const PriceBox: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const { const {
nativeCurrency: { symbol }, nativeCurrency: { symbol },
} = useChainInfo(); } = useChainInfo();
const latestBlock = useLatestBlock(provider); const latestBlock = useLatestBlockHeader(provider);
const maybeOutdated: boolean = const maybeOutdated: boolean =
latestBlock !== undefined && latestBlock !== undefined &&
Date.now() / 1000 - latestBlock.timestamp > 3600; Date.now() / 1000 - latestBlock.timestamp > 3600;
const ethFeed = useMemo(
() =>
provider &&
new Contract("eth-usd.data.eth", AggregatorV3Interface, provider),
[provider]
);
const gasFeed = useMemo(
() =>
provider &&
new Contract("fast-gas-gwei.data.eth", AggregatorV3Interface, provider),
[provider]
);
const [latestPriceData, setLatestPriceData] = useState<any>();
const [latestGasData, setLatestGasData] = useState<any>();
useEffect(() => {
if (!ethFeed || !gasFeed) {
return;
}
const readData = async () => {
const [priceData, gasData] = await Promise.all([
ethFeed.latestRoundData(),
await gasFeed.latestRoundData(),
]);
setLatestPriceData(priceData);
setLatestGasData(gasData);
};
readData();
}, [ethFeed, gasFeed]);
const latestPriceData = useETHUSDRawOracle(provider, "latest");
const [latestPrice, latestPriceTimestamp] = useMemo(() => { const [latestPrice, latestPriceTimestamp] = useMemo(() => {
if (!latestPriceData) { if (!latestPriceData) {
return [undefined, undefined]; return [undefined, undefined];
@ -65,6 +35,7 @@ const PriceBox: React.FC = () => {
return [formattedPrice, timestamp]; return [formattedPrice, timestamp];
}, [latestPriceData]); }, [latestPriceData]);
const latestGasData = useFastGasRawOracle(provider, "latest");
const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => { const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => {
if (!latestGasData) { if (!latestGasData) {
return [undefined, undefined]; return [undefined, undefined];

View File

@ -1,9 +1,9 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { Menu } from "@headlessui/react"; import { Menu } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars } from "@fortawesome/free-solid-svg-icons/faBars"; import { faBars } from "@fortawesome/free-solid-svg-icons/faBars";
import { SourcifySource } from "./url";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { SourcifySource } from "./sourcify/useSourcify";
const SourcifyMenu: React.FC = () => { const SourcifyMenu: React.FC = () => {
const { sourcifySource, setSourcifySource } = useAppConfigContext(); const { sourcifySource, setSourcifySource } = useAppConfigContext();
@ -41,7 +41,7 @@ type SourcifyMenuItemProps = {
onClick: () => void; onClick: () => void;
}; };
const SourcifyMenuItem: React.FC<SourcifyMenuItemProps> = ({ const SourcifyMenuItem: React.FC<PropsWithChildren<SourcifyMenuItemProps>> = ({
checked = false, checked = false,
onClick, onClick,
children, children,

View File

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

View File

@ -1,6 +1,6 @@
import React from "react"; import React, { PropsWithChildren } from "react";
const StandardSubtitle: React.FC = ({ children }) => ( const StandardSubtitle: React.FC<PropsWithChildren> = ({ children }) => (
<div className="pb-2 text-xl text-gray-700">{children}</div> <div className="pb-2 text-xl text-gray-700">{children}</div>
); );

View File

@ -6,32 +6,21 @@ import TransactionAddress from "./components/TransactionAddress";
import ValueHighlighter from "./components/ValueHighlighter"; import ValueHighlighter from "./components/ValueHighlighter";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import USDAmount from "./components/USDAmount"; import USDAmount from "./components/USDAmount";
import {
AddressContext,
ChecksummedAddress,
TokenMeta,
TokenTransfer,
} from "./types";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useBlockNumberContext } from "./useBlockTagContext"; import { useBlockNumberContext } from "./useBlockTagContext";
import { Metadata } from "./sourcify/useSourcify"; import { useTokenMetadata } from "./useErigonHooks";
import { useTokenUSDOracle } from "./usePriceOracle"; import { useTokenUSDOracle } from "./usePriceOracle";
import { AddressContext, TokenTransfer } from "./types";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
tokenMeta?: TokenMeta | null | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
// TODO: handle partial const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ t }) => {
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t,
tokenMeta,
metadatas,
}) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const blockNumber = useBlockNumberContext(); const blockNumber = useBlockNumberContext();
const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token); const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token);
const tokenMeta = useTokenMetadata(provider, t.token);
return ( return (
<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">
@ -40,7 +29,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress <TransactionAddress
address={t.from} address={t.from}
addressCtx={AddressContext.FROM} addressCtx={AddressContext.FROM}
metadata={metadatas[t.from]}
showCodeIndicator showCodeIndicator
/> />
</div> </div>
@ -51,7 +39,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress <TransactionAddress
address={t.to} address={t.to}
addressCtx={AddressContext.TO} addressCtx={AddressContext.TO}
metadata={metadatas[t.to]}
showCodeIndicator showCodeIndicator
/> />
</div> </div>
@ -67,7 +54,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
/> />
</ValueHighlighter> </ValueHighlighter>
</span> </span>
<TransactionAddress address={t.token} metadata={metadatas[t.token]} /> <TransactionAddress address={t.token} />
{tokenMeta && quote !== undefined && decimals !== undefined && ( {tokenMeta && quote !== undefined && decimals !== undefined && (
<span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600"> <span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600">
<USDAmount <USDAmount

View File

@ -1,13 +1,76 @@
import React from "react"; import React, { useContext, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams, Route, Routes } from "react-router-dom";
import TransactionPageContent from "./TransactionPageContent"; import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { BlockNumberContext } from "./useBlockTagContext";
const Details = React.lazy(() => import("./transaction/Details"));
const Logs = React.lazy(() => import("./transaction/Logs"));
const Trace = React.lazy(() => import("./transaction/Trace"));
const Transaction: React.FC = () => { const Transaction: React.FC = () => {
const { txhash } = useParams(); const { txhash: txHash } = useParams();
if (txhash === undefined) { if (txHash === undefined) {
throw new Error("txhash couldn't be undefined here"); throw new Error("txhash couldn't be undefined here");
} }
return <TransactionPageContent txHash={txhash} />;
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const selectionCtx = useSelection();
useEffect(() => {
if (txData) {
document.title = `Transaction ${txData.transactionHash} | Otterscan`;
}
}, [txData]);
return (
<SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not
found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route index element={<Details txData={txData} />} />
<Route path="logs" element={<Logs txData={txData} />} />
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</BlockNumberContext.Provider>
</SelectedTransactionContext.Provider>
);
}; };
export default Transaction; export default Transaction;

View File

@ -1,129 +0,0 @@
import React, { useContext, useMemo } from "react";
import { Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { BlockNumberContext } from "./useBlockTagContext";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
type TransactionPageContentProps = {
txHash: string;
};
const TransactionPageContent: React.FC<TransactionPageContentProps> = ({
txHash,
}) => {
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not
found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData &&
` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
/>
}
/>
<Route
path="logs"
element={<Logs txData={txData} metadata={metadata} />}
/>
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</BlockNumberContext.Provider>
</SelectedTransactionContext.Provider>
);
};
export default TransactionPageContent;

View File

@ -0,0 +1,32 @@
import React, { useContext } from "react";
import { BigNumber } from "@ethersproject/bignumber";
import TransactionValue from "../components/TransactionValue";
import FiatValue from "../components/FiatValue";
import { RuntimeContext } from "../useRuntime";
import { useETHUSDOracle } from "../usePriceOracle";
type AddressBalanceProps = {
balance: BigNumber;
};
const AddressBalance: React.FC<AddressBalanceProps> = ({ balance }) => {
const { provider } = useContext(RuntimeContext);
const eth2USDValue = useETHUSDOracle(provider, "latest");
const fiatValue =
!balance.isZero() && eth2USDValue !== undefined
? balance.mul(eth2USDValue).div(10 ** 8)
: undefined;
return (
<>
<TransactionValue value={balance} />
{fiatValue && (
<span className="px-2 border-emerald-200 border rounded-lg bg-emerald-100 text-emerald-600">
<FiatValue value={fiatValue} />
</span>
)}
</>
);
};
export default AddressBalance;

View File

@ -1,9 +1,7 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useContext, useEffect, useMemo, useState } from "react";
import { BlockTag } from "@ethersproject/providers";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow"; import InfoRow from "../components/InfoRow";
import TransactionValue from "../components/TransactionValue"; import AddressBalance from "./AddressBalance";
import ETH2USDValue from "../components/ETH2USDValue";
import TransactionAddress from "../components/TransactionAddress"; import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import TransactionLink from "../components/TransactionLink"; import TransactionLink from "../components/TransactionLink";
@ -14,11 +12,9 @@ import TransactionItem from "../search/TransactionItem";
import UndefinedPageControl from "../search/UndefinedPageControl"; import UndefinedPageControl from "../search/UndefinedPageControl";
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 { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { useContractsMetadata } from "../hooks";
import { useAddressBalance, useContractCreator } from "../useErigonHooks"; import { useAddressBalance, useContractCreator } from "../useErigonHooks";
import { BlockNumberContext } from "../useBlockTagContext"; import { BlockNumberContext } from "../useBlockTagContext";
@ -99,35 +95,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
const page = useMemo(() => controller?.getPage(), [controller]); const page = useMemo(() => controller?.getPage(), [controller]);
// Extract block number from all txs on current page
// TODO: dedup blockTags
const blockTags: BlockTag[] = useMemo(() => {
if (!page) {
return ["latest"];
}
const blockTags: BlockTag[] = page.map((t) => t.blockNumber);
blockTags.push("latest");
return blockTags;
}, [page]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
// Calculate Sourcify metadata for all addresses that appear on this page results
const addresses = useMemo(() => {
const _addresses = [address];
if (page) {
for (const t of page) {
if (t.to) {
_addresses.push(t.to);
}
if (t.createdContractAddress) {
_addresses.push(t.createdContractAddress);
}
}
}
return _addresses;
}, [address, page]);
const metadatas = useContractsMetadata(addresses, provider);
const balance = useAddressBalance(provider, address); const balance = useAddressBalance(provider, address);
const creator = useContractCreator(provider, address); const creator = useContractCreator(provider, address);
@ -138,15 +105,7 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
{balance && ( {balance && (
<InfoRow title="Balance"> <InfoRow title="Balance">
<div className="space-x-2"> <div className="space-x-2">
<TransactionValue value={balance} /> <AddressBalance balance={balance} />
{!balance.isZero() && priceMap["latest"] !== undefined && (
<span className="px-2 border-green-200 border rounded-lg bg-green-100 text-green-600">
<ETH2USDValue
ethAmount={balance}
eth2USDValue={priceMap["latest"]}
/>
</span>
)}
</div> </div>
</InfoRow> </InfoRow>
)} )}
@ -180,8 +139,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
tx={tx} tx={tx}
selectedAddress={address} selectedAddress={address}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/> />
))} ))}
<NavBar address={address} page={page} controller={controller} /> <NavBar address={address} page={page} controller={controller} />

View File

@ -1,31 +1,11 @@
import React from "react"; import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init"; import { SyntaxHighlighter, docco } from "../highlight-init";
import { useContract } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
type ContractProps = { type ContractProps = {
checksummedAddress: string; content: any;
networkId: number;
filename: string;
source: any;
}; };
const Contract: React.FC<ContractProps> = ({ const Contract: React.FC<ContractProps> = ({ content }) => (
checksummedAddress,
networkId,
filename,
source,
}) => {
const { sourcifySource } = useAppConfigContext();
const content = useContract(
checksummedAddress,
networkId,
filename,
source,
sourcifySource
);
return (
<SyntaxHighlighter <SyntaxHighlighter
className="w-full h-full border font-code text-base" className="w-full h-full border font-code text-base"
language="solidity" language="solidity"
@ -35,6 +15,5 @@ const Contract: React.FC<ContractProps> = ({
{content ?? ""} {content ?? ""}
</SyntaxHighlighter> </SyntaxHighlighter>
); );
};
export default React.memo(Contract); export default React.memo(Contract);

View File

@ -0,0 +1,40 @@
import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init";
import { MatchType, useContract } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
type ContractFromRepoProps = {
checksummedAddress: string;
networkId: number;
filename: string;
type: MatchType;
};
const ContractFromRepo: React.FC<ContractFromRepoProps> = ({
checksummedAddress,
networkId,
filename,
type,
}) => {
const { sourcifySource } = useAppConfigContext();
const content = useContract(
checksummedAddress,
networkId,
filename,
sourcifySource,
type
);
return (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
style={docco}
showLineNumbers
>
{content ?? ""}
</SyntaxHighlighter>
);
};
export default React.memo(ContractFromRepo);

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext, Fragment } from "react"; import React, { useState, useEffect, useContext } from "react";
import { commify } from "@ethersproject/units"; import { commify } from "@ethersproject/units";
import { Menu } from "@headlessui/react"; import { Menu } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,46 +6,47 @@ 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 Contract from "./Contract"; import Contract from "./Contract";
import ContractFromRepo from "./ContractFromRepo";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { Metadata } from "../sourcify/useSourcify"; import { Match, MatchType } from "../sourcify/useSourcify";
import ExternalLink from "../components/ExternalLink"; import ExternalLink from "../components/ExternalLink";
import { openInRemixURL } from "../url"; import { openInRemixURL } from "../url";
import ContractABI from "./ContractABI"; import ContractABI from "./ContractABI";
type ContractsProps = { type ContractsProps = {
checksummedAddress: string; checksummedAddress: string;
rawMetadata: Metadata | null | undefined; match: Match | null | undefined;
}; };
const Contracts: React.FC<ContractsProps> = ({ const Contracts: React.FC<ContractsProps> = ({ checksummedAddress, match }) => {
checksummedAddress,
rawMetadata,
}) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const [selected, setSelected] = useState<string>(); const [selected, setSelected] = useState<string>();
useEffect(() => { useEffect(() => {
if (rawMetadata) { if (match) {
setSelected(Object.keys(rawMetadata.sources)[0]); setSelected(Object.keys(match.metadata.sources)[0]);
} }
}, [rawMetadata]); }, [match]);
const optimizer = rawMetadata?.settings?.optimizer; const optimizer = match?.metadata.settings?.optimizer;
return ( return (
<ContentFrame tabs> <ContentFrame tabs>
{rawMetadata && ( {match && (
<> <>
<InfoRow title="Match">
{match.type === MatchType.FULL_MATCH ? "Full" : "Partial"}
</InfoRow>
<InfoRow title="Language"> <InfoRow title="Language">
<span>{rawMetadata.language}</span> <span>{match.metadata.language}</span>
</InfoRow> </InfoRow>
<InfoRow title="Compiler"> <InfoRow title="Compiler">
<span>{rawMetadata.compiler.version}</span> <span>{match.metadata.compiler.version}</span>
</InfoRow> </InfoRow>
<InfoRow title="Optimizer Enabled"> <InfoRow title="Optimizer Enabled">
{optimizer?.enabled ? ( {optimizer?.enabled ? (
<span> <span>
<span className="font-bold text-green-600">Yes</span> with{" "} <span className="font-bold text-emerald-600">Yes</span> with{" "}
<span className="font-bold text-green-600"> <span className="font-bold text-emerald-600">
{commify(optimizer?.runs)} {commify(optimizer?.runs)}
</span>{" "} </span>{" "}
runs runs
@ -57,19 +58,19 @@ const Contracts: React.FC<ContractsProps> = ({
</> </>
)} )}
<div className="py-5"> <div className="py-5">
{rawMetadata === undefined && ( {match === undefined && (
<span>Getting data from Sourcify repository...</span> <span>Getting data from Sourcify repository...</span>
)} )}
{rawMetadata === null && ( {match === null && (
<span> <span>
Address is not a contract or couldn't find contract metadata in Address is not a contract or couldn't find contract metadata in
Sourcify repository. Sourcify repository.
</span> </span>
)} )}
{rawMetadata !== undefined && rawMetadata !== null && ( {match !== undefined && match !== null && (
<> <>
{rawMetadata.output.abi && ( {match.metadata.output.abi && (
<ContractABI abi={rawMetadata.output.abi} /> <ContractABI abi={match.metadata.output.abi} />
)} )}
<div> <div>
<Menu> <Menu>
@ -95,13 +96,13 @@ const Contracts: React.FC<ContractsProps> = ({
</div> </div>
<div className="relative"> <div className="relative">
<Menu.Items className="absolute border p-1 rounded-b bg-white flex flex-col"> <Menu.Items className="absolute border p-1 rounded-b bg-white flex flex-col">
{Object.entries(rawMetadata.sources).map(([k]) => ( {Object.entries(match.metadata.sources).map(([k]) => (
<Menu.Item key={k}> <Menu.Item key={k}>
<button <button
className={`flex text-sm px-2 py-1 ${ className={`flex text-sm px-2 py-1 ${
selected === k selected === k
? "font-bold bg-gray-200 text-gray-500" ? "font-bold bg-gray-200 text-gray-500"
: "hover:border-orange-200 hover:text-gray-500 text-gray-400 transition-transform transition-colors duration-75" : "hover:text-gray-500 text-gray-400 transition-colors duration-75"
}`} }`}
onClick={() => setSelected(k)} onClick={() => setSelected(k)}
> >
@ -113,13 +114,21 @@ const Contracts: React.FC<ContractsProps> = ({
</div> </div>
</Menu> </Menu>
{selected && ( {selected && (
<>
{match.metadata.sources[selected].content ? (
<Contract <Contract
content={match.metadata.sources[selected].content}
/>
) : (
<ContractFromRepo
checksummedAddress={checksummedAddress} checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId} networkId={provider!.network.chainId}
filename={selected} filename={selected}
source={rawMetadata.sources[selected]} type={match.type}
/> />
)} )}
</>
)}
</div> </div>
</> </>
)} )}

View File

@ -28,14 +28,14 @@ const DecodedFragment: React.FC<DecodedFragmentProps> = ({
fragmentType = "function"; fragmentType = "function";
sig = intf.getSighash(fragment); sig = intf.getSighash(fragment);
letter = "F"; letter = "F";
letterBg = "bg-purple-500"; letterBg = "bg-violet-500";
hashBg = "bg-purple-50"; hashBg = "bg-violet-50";
} else if (EventFragment.isEventFragment(fragment)) { } else if (EventFragment.isEventFragment(fragment)) {
fragmentType = "event"; fragmentType = "event";
sig = intf.getEventTopic(fragment); sig = intf.getEventTopic(fragment);
letter = "E"; letter = "E";
letterBg = "bg-green-300"; letterBg = "bg-emerald-300";
hashBg = "bg-green-50"; hashBg = "bg-emerald-50";
} else if (ConstructorFragment.isConstructorFragment(fragment)) { } else if (ConstructorFragment.isConstructorFragment(fragment)) {
fragmentType = "constructor"; fragmentType = "constructor";
letter = "C"; letter = "C";
@ -49,7 +49,7 @@ const DecodedFragment: React.FC<DecodedFragmentProps> = ({
</span> </span>
{letter && ( {letter && (
<span <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}`} className={`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} {letter}
</span> </span>

View File

@ -1,18 +1,18 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { Interface } from "@ethersproject/abi"; import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import erc20 from "../../erc20.json"; import erc20 from "../../erc20.json";
import { TokenMeta } from "../../types"; import { TokenMeta } from "../../types";
const erc20Interface = new Interface(erc20); const ERC20_PROTOTYPE = new Contract(AddressZero, erc20);
export class ERCTokenResolver implements IAddressResolver<TokenMeta> { export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
async resolveAddress( async resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<TokenMeta | undefined> { ): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20Interface, provider); const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address);
try { try {
const name = (await erc20Contract.name()) as string; const name = (await erc20Contract.name()) as string;
if (!name.trim()) { if (!name.trim()) {

View File

@ -12,6 +12,11 @@ const UNISWAP_V1_FACTORY_ABI = [
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
const UNISWAP_V1_FACTORY_PROTOTYPE = new Contract(
UNISWAP_V1_FACTORY,
UNISWAP_V1_FACTORY_ABI
);
export type UniswapV1TokenMeta = { export type UniswapV1TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -28,11 +33,7 @@ export class UniswapV1Resolver implements IAddressResolver<UniswapV1PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV1PairMeta | undefined> { ): Promise<UniswapV1PairMeta | undefined> {
const factoryContract = new Contract( const factoryContract = UNISWAP_V1_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V1_FACTORY,
UNISWAP_V1_FACTORY_ABI,
provider
);
try { try {
// First, probe the getToken() function; if it responds with an UniswapV1 exchange // First, probe the getToken() function; if it responds with an UniswapV1 exchange

View File

@ -1,5 +1,6 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import { ChecksummedAddress, TokenMeta } from "../../types"; import { ChecksummedAddress, TokenMeta } from "../../types";
import { ERCTokenResolver } from "./ERCTokenResolver"; import { ERCTokenResolver } from "./ERCTokenResolver";
@ -16,6 +17,16 @@ const UNISWAP_V2_PAIR_ABI = [
"function token1() external view returns (address)", "function token1() external view returns (address)",
]; ];
const UNISWAP_V2_FACTORY_PROTOTYPE = new Contract(
UNISWAP_V2_FACTORY,
UNISWAP_V2_FACTORY_ABI
);
const UNISWAP_V2_PAIR_PROTOTYPE = new Contract(
AddressZero,
UNISWAP_V2_PAIR_ABI
);
export type UniswapV2TokenMeta = { export type UniswapV2TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -33,12 +44,9 @@ export class UniswapV2Resolver implements IAddressResolver<UniswapV2PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV2PairMeta | undefined> { ): Promise<UniswapV2PairMeta | undefined> {
const pairContract = new Contract(address, UNISWAP_V2_PAIR_ABI, provider); const pairContract =
const factoryContract = new Contract( UNISWAP_V2_PAIR_PROTOTYPE.connect(provider).attach(address);
UNISWAP_V2_FACTORY, const factoryContract = UNISWAP_V2_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V2_FACTORY_ABI,
provider
);
try { try {
// First, probe the factory() function; if it responds with UniswapV2 factory // First, probe the factory() function; if it responds with UniswapV2 factory

View File

@ -1,5 +1,6 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import { ChecksummedAddress, TokenMeta } from "../../types"; import { ChecksummedAddress, TokenMeta } from "../../types";
import { ERCTokenResolver } from "./ERCTokenResolver"; import { ERCTokenResolver } from "./ERCTokenResolver";
@ -17,6 +18,16 @@ const UNISWAP_V3_PAIR_ABI = [
"function fee() external view returns (uint24)", "function fee() external view returns (uint24)",
]; ];
const UNISWAP_V3_FACTORY_PROTOTYPE = new Contract(
UNISWAP_V3_FACTORY,
UNISWAP_V3_FACTORY_ABI
);
const UNISWAP_V3_PAIR_PROTOTYPE = new Contract(
AddressZero,
UNISWAP_V3_PAIR_ABI
);
export type UniswapV3TokenMeta = { export type UniswapV3TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -35,12 +46,9 @@ export class UniswapV3Resolver implements IAddressResolver<UniswapV3PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV3PairMeta | undefined> { ): Promise<UniswapV3PairMeta | undefined> {
const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider); const poolContract =
const factoryContract = new Contract( UNISWAP_V3_PAIR_PROTOTYPE.connect(provider).attach(address);
UNISWAP_V3_FACTORY, const factoryContract = UNISWAP_V3_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V3_FACTORY_ABI,
provider
);
try { try {
// First, probe the factory() function; if it responds with UniswapV2 factory // First, probe the factory() function; if it responds with UniswapV2 factory

View File

@ -1,54 +1,27 @@
import React, { useContext, useMemo } from "react"; import React from "react";
import { BlockTag } from "@ethersproject/abstract-provider";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import PageControl from "../search/PageControl"; import PageControl from "../search/PageControl";
import ResultHeader from "../search/ResultHeader"; import ResultHeader from "../search/ResultHeader";
import PendingResults from "../search/PendingResults"; import PendingResults from "../search/PendingResults";
import TransactionItem from "../search/TransactionItem"; import TransactionItem from "../search/TransactionItem";
import { useFeeToggler } from "../search/useFeeToggler"; import { useFeeToggler } from "../search/useFeeToggler";
import { RuntimeContext } from "../useRuntime";
import { SelectionContext, useSelection } from "../useSelection"; import { SelectionContext, useSelection } from "../useSelection";
import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params"; import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { useContractsMetadata } from "../hooks";
type BlockTransactionResultsProps = { type BlockTransactionResultsProps = {
blockTag: BlockTag;
page?: ProcessedTransaction[]; page?: ProcessedTransaction[];
total: number; total: number;
pageNumber: number; pageNumber: number;
}; };
const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
blockTag,
page, page,
total, total,
pageNumber, pageNumber,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const selectionCtx = useSelection(); const selectionCtx = useSelection();
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
const addresses = useMemo((): ChecksummedAddress[] => {
if (!page) {
return [];
}
const _addresses: ChecksummedAddress[] = [];
for (const t of page) {
if (t.to) {
_addresses.push(t.to);
}
if (t.createdContractAddress) {
_addresses.push(t.createdContractAddress);
}
}
return _addresses;
}, [page]);
const metadatas = useContractsMetadata(addresses, provider);
return ( return (
<ContentFrame> <ContentFrame>
@ -73,13 +46,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
{page ? ( {page ? (
<SelectionContext.Provider value={selectionCtx}> <SelectionContext.Provider value={selectionCtx}>
{page.map((tx) => ( {page.map((tx) => (
<TransactionItem <TransactionItem key={tx.hash} tx={tx} feeDisplay={feeDisplay} />
key={tx.hash}
tx={tx}
feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/>
))} ))}
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">

View File

@ -1,3 +1,4 @@
import { PropsWithChildren } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { BlockTag } from "@ethersproject/abstract-provider"; import { BlockTag } from "@ethersproject/abstract-provider";
import { blockURL } from "../url"; import { blockURL } from "../url";
@ -8,7 +9,7 @@ type NavButtonProps = {
urlBuilder?: (blockNumber: BlockTag) => string; urlBuilder?: (blockNumber: BlockTag) => string;
}; };
const NavButton: React.FC<NavButtonProps> = ({ const NavButton: React.FC<PropsWithChildren<NavButtonProps>> = ({
blockNum, blockNum,
disabled, disabled,
urlBuilder, urlBuilder,
@ -16,7 +17,7 @@ const NavButton: React.FC<NavButtonProps> = ({
}) => { }) => {
if (disabled) { if (disabled) {
return ( return (
<span className="bg-link-blue bg-opacity-10 text-gray-400 rounded px-2 py-1 text-xs"> <span className="bg-link-blue/10 text-gray-400 rounded px-2 py-1 text-xs">
{children} {children}
</span> </span>
); );
@ -24,7 +25,7 @@ const NavButton: React.FC<NavButtonProps> = ({
return ( return (
<NavLink <NavLink
className="transition-colors bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded px-2 py-1 text-xs" className="transition-colors bg-link-blue/10 text-link-blue hover:bg-link-blue/100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded px-2 py-1 text-xs"
to={urlBuilder ? urlBuilder(blockNum) : blockURL(blockNum)} to={urlBuilder ? urlBuilder(blockNum) : blockURL(blockNum)}
> >
{children} {children}

View File

@ -1,4 +1,4 @@
import React, { useContext } from "react"; import React, { PropsWithChildren, useContext } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
@ -8,8 +8,8 @@ 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 SourcifyLogo from "../sourcify/SourcifyLogo"; import SourcifyLogo from "../sourcify/SourcifyLogo";
import PlainAddress from "./PlainAddress"; import PlainAddress from "./PlainAddress";
import { Metadata } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useSourcifyMetadata } from "../sourcify/useSourcify";
import { useResolvedAddress } from "../useResolvedAddresses"; import { useResolvedAddress } from "../useResolvedAddresses";
import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types"; import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types";
import { resolverRendererRegistry } from "../api/address-resolver"; import { resolverRendererRegistry } from "../api/address-resolver";
@ -23,7 +23,6 @@ type DecoratedAddressLinkProps = {
selfDestruct?: boolean | undefined; selfDestruct?: boolean | undefined;
txFrom?: boolean | undefined; txFrom?: boolean | undefined;
txTo?: boolean | undefined; txTo?: boolean | undefined;
metadata?: Metadata | null | undefined;
eoa?: boolean | undefined; eoa?: boolean | undefined;
}; };
@ -36,9 +35,11 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
selfDestruct, selfDestruct,
txFrom, txFrom,
txTo, txTo,
metadata,
eoa, eoa,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const match = useSourcifyMetadata(address, provider?.network.chainId);
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;
@ -47,13 +48,13 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
txFrom ? "bg-skin-from" : "" txFrom ? "bg-skin-from" : ""
} ${txTo ? "bg-skin-to" : ""} ${ } ${txTo ? "bg-skin-to" : ""} ${
mint ? "italic text-green-500 hover:text-green-700" : "" mint ? "italic text-emerald-500 hover:text-emerald-700" : ""
} ${burn ? "line-through text-orange-500 hover:text-orange-700" : ""} ${ } ${burn ? "line-through text-orange-500 hover:text-orange-700" : ""} ${
selfDestruct ? "line-through opacity-70 hover:opacity-100" : "" selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""
}`} }`}
> >
{creation && ( {creation && (
<span className="text-yellow-300" title="Contract creation"> <span className="text-amber-300" title="Contract creation">
<FontAwesomeIcon icon={faStar} size="1x" /> <FontAwesomeIcon icon={faStar} size="1x" />
</span> </span>
)} )}
@ -63,7 +64,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
</span> </span>
)} )}
{mint && ( {mint && (
<span className="text-green-500" title="Mint address"> <span className="text-emerald-500" title="Mint address">
<FontAwesomeIcon icon={faMoneyBillAlt} size="1x" /> <FontAwesomeIcon icon={faMoneyBillAlt} size="1x" />
</span> </span>
)} )}
@ -73,13 +74,13 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
</span> </span>
)} )}
{miner && ( {miner && (
<span className="text-yellow-400" title="Miner address"> <span className="text-amber-400" title="Miner address">
<FontAwesomeIcon icon={faCoins} size="1x" /> <FontAwesomeIcon icon={faCoins} size="1x" />
</span> </span>
)} )}
{metadata && ( {match && (
<NavLink <NavLink
className="self-center flex-shrink-0 flex items-center" className="self-center shrink-0 flex items-center"
to={`/address/${address}/contract`} to={`/address/${address}/contract`}
> >
<SourcifyLogo /> <SourcifyLogo />
@ -156,11 +157,11 @@ type AddressLegendProps = {
title: string; title: string;
}; };
const AddressLegend: React.FC<AddressLegendProps> = ({ title, children }) => ( const AddressLegend: React.FC<PropsWithChildren<AddressLegendProps>> = ({
<span title,
className="text-xs text-gray-400 text-opacity-70 not-italic" children,
title={title} }) => (
> <span className="text-xs text-gray-400/70 not-italic" title={title}>
{children} {children}
</span> </span>
); );

View File

@ -48,7 +48,7 @@ type ContentProps = {
const Content: React.FC<ContentProps> = ({ linkable, name }) => ( const Content: React.FC<ContentProps> = ({ linkable, name }) => (
<> <>
<img <img
className={`self-center ${linkable ? "" : "filter grayscale"}`} className={`self-center ${linkable ? "" : "grayscale"}`}
src={ENSLogo} src={ENSLogo}
alt="ENS Logo" alt="ENS Logo"
width={12} width={12}

View File

@ -1,33 +0,0 @@
import React from "react";
import { BigNumber, FixedNumber } from "@ethersproject/bignumber";
import { commify } from "@ethersproject/units";
type ETH2USDValueProps = {
ethAmount: BigNumber;
eth2USDValue: BigNumber;
};
/**
* Basic display of ETH -> USD values WITHOUT box decoration, only
* text formatting.
*
* USD amounts are displayed commified with 2 decimals places and $ prefix,
* i.e., "$1,000.00".
*/
const ETH2USDValue: React.FC<ETH2USDValueProps> = ({
ethAmount,
eth2USDValue,
}) => {
const value = ethAmount.mul(eth2USDValue).div(10 ** 8);
return (
<span className="text-xs">
$
<span className="font-balance">
{commify(FixedNumber.fromValue(value, 18).round(2).toString())}
</span>
</span>
);
};
export default React.memo(ETH2USDValue);

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt"; import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
@ -6,7 +6,10 @@ type ExternalLinkProps = {
href: string; href: string;
}; };
const ExternalLink: React.FC<ExternalLinkProps> = ({ href, children }) => ( const ExternalLink: React.FC<PropsWithChildren<ExternalLinkProps>> = ({
href,
children,
}) => (
<a <a
className="text-link-blue hover:text-link-blue-hover" className="text-link-blue hover:text-link-blue-hover"
href={href} href={href}

View File

@ -0,0 +1,25 @@
import React from "react";
import { BigNumber, FixedNumber } from "@ethersproject/bignumber";
import { commify } from "@ethersproject/units";
type FiatValueProps = {
value: BigNumber;
};
/**
* Basic display of ETH -> USD values WITHOUT box decoration, only
* text formatting.
*
* USD amounts are displayed commified with 2 decimals places and $ prefix,
* i.e., "$1,000.00".
*/
const FiatValue: React.FC<FiatValueProps> = ({ value }) => (
<span className="text-xs">
$
<span className="font-balance">
{commify(FixedNumber.fromValue(value, 18).round(2).toString())}
</span>
</span>
);
export default FiatValue;

View File

@ -1,10 +1,12 @@
import React from "react"; import React, { useContext } from "react";
import { formatEther } from "@ethersproject/units"; import { formatEther } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; 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 TransactionAddress from "./TransactionAddress"; import TransactionAddress from "./TransactionAddress";
import { RuntimeContext } from "../useRuntime";
import { useBlockDataFromTransaction } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
@ -17,12 +19,12 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
txData, txData,
internalOp, internalOp,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol }, nativeCurrency: { symbol },
} = useChainInfo(); } = useChainInfo();
const toMiner = const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return ( return (
<> <>
@ -54,7 +56,7 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
<AddressHighlighter address={internalOp.to}> <AddressHighlighter address={internalOp.to}>
<div <div
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" toMiner ? "rounded px-2 py-1 bg-amber-100" : ""
}`} }`}
> >
<DecoratedAddressLink address={internalOp.to} miner={toMiner} /> <DecoratedAddressLink address={internalOp.to} miner={toMiner} />

View File

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { BigNumber } from "@ethersproject/bignumber";
import InternalTransfer from "./InternalTransfer"; import InternalTransfer from "./InternalTransfer";
import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalSelfDestruct from "./InternalSelfDestruct";
import InternalCreate from "./InternalCreate"; import InternalCreate from "./InternalCreate";
@ -8,20 +7,14 @@ import { TransactionData, InternalOperation, OperationType } from "../types";
type InternalTransactionOperationProps = { type InternalTransactionOperationProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
// TODO: migrate all this logic to SWR
ethUSDPrice: BigNumber | undefined;
}; };
const InternalTransactionOperation: React.FC< const InternalTransactionOperation: React.FC<
InternalTransactionOperationProps InternalTransactionOperationProps
> = ({ txData, internalOp, ethUSDPrice }) => ( > = ({ txData, internalOp }) => (
<> <>
{internalOp.type === OperationType.TRANSFER && ( {internalOp.type === OperationType.TRANSFER && (
<InternalTransfer <InternalTransfer txData={txData} internalOp={internalOp} />
txData={txData}
internalOp={internalOp}
ethUSDPrice={ethUSDPrice}
/>
)} )}
{internalOp.type === OperationType.SELF_DESTRUCT && ( {internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct txData={txData} internalOp={internalOp} /> <InternalSelfDestruct txData={txData} internalOp={internalOp} />

View File

@ -1,5 +1,4 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { BigNumber } from "@ethersproject/bignumber";
import { formatEther } from "@ethersproject/units"; import { formatEther } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
@ -9,33 +8,34 @@ import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import USDAmount from "./USDAmount"; import USDAmount from "./USDAmount";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
type InternalTransferProps = { type InternalTransferProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
// TODO: migrate all this logic to SWR
ethUSDPrice: BigNumber | undefined;
}; };
const InternalTransfer: React.FC<InternalTransferProps> = ({ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
internalOp, internalOp,
ethUSDPrice,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol, decimals }, nativeCurrency: { symbol, decimals },
} = useChainInfo(); } = useChainInfo();
const fromMiner = const fromMiner =
txData.confirmedData?.miner !== undefined && block?.miner !== undefined && internalOp.from === block.miner;
internalOp.from === txData.confirmedData.miner; const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
const { provider } = useContext(RuntimeContext); const blockETHUSDPrice = useETHUSDOracle(
provider,
txData.confirmedData?.blockNumber
);
const fromHasCode = useHasCode( const fromHasCode = useHasCode(
provider, provider,
internalOp.from, internalOp.from,
@ -58,7 +58,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
<AddressHighlighter address={internalOp.from}> <AddressHighlighter address={internalOp.from}>
<div <div
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" fromMiner ? "rounded px-2 py-1 bg-amber-100" : ""
}`} }`}
> >
<DecoratedAddressLink <DecoratedAddressLink
@ -79,7 +79,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
<AddressHighlighter address={internalOp.to}> <AddressHighlighter address={internalOp.to}>
<div <div
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" toMiner ? "rounded px-2 py-1 bg-amber-100" : ""
}`} }`}
> >
<DecoratedAddressLink <DecoratedAddressLink
@ -99,12 +99,12 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
<span> <span>
{formatEther(internalOp.value)} {symbol} {formatEther(internalOp.value)} {symbol}
</span> </span>
{ethUSDPrice && ( {blockETHUSDPrice && (
<span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600"> <span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600">
<USDAmount <USDAmount
amount={internalOp.value} amount={internalOp.value}
amountDecimals={decimals} amountDecimals={decimals}
quote={ethUSDPrice} quote={blockETHUSDPrice}
// TODO: migrate to SWR and standardize this magic number // TODO: migrate to SWR and standardize this magic number
quoteDecimals={8} quoteDecimals={8}
/> />

View File

@ -11,7 +11,7 @@ const MethodName: React.FC<MethodNameProps> = ({ data }) => {
return ( return (
<div <div
className={`${ className={`${
isSimpleTransfer ? "bg-yellow-100" : "bg-blue-50" isSimpleTransfer ? "bg-amber-100" : "bg-blue-50"
} rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max`} } rounded-lg px-3 py-1 min-h-full flex items-baseline text-xs max-w-max`}
> >
<p className="truncate" title={methodTitle}> <p className="truncate" title={methodTitle}>

View File

@ -1,11 +1,14 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
type ModeTabProps = { type ModeTabProps = {
disabled?: boolean | undefined; disabled?: boolean | undefined;
}; };
const ModeTab: React.FC<ModeTabProps> = ({ disabled, children }) => ( const ModeTab: React.FC<PropsWithChildren<ModeTabProps>> = ({
disabled,
children,
}) => (
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`border rounded-lg px-2 py-1 bg-gray-100 ${ `border rounded-lg px-2 py-1 bg-gray-100 ${

View File

@ -1,4 +1,4 @@
import React, { Fragment } from "react"; import React, { Fragment, PropsWithChildren } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
@ -6,7 +6,10 @@ type NavTabProps = {
href: string; href: string;
}; };
const NavTab: React.FC<NavTabProps> = ({ href, children }) => ( const NavTab: React.FC<PropsWithChildren<NavTabProps>> = ({
href,
children,
}) => (
<Tab as={Fragment}> <Tab as={Fragment}>
<NavLink <NavLink
className={({ isActive }) => className={({ isActive }) =>

View File

@ -9,13 +9,13 @@ type NonceProps = {
const Nonce: React.FC<NonceProps> = ({ value }) => ( const Nonce: React.FC<NonceProps> = ({ value }) => (
<span <span
className="flex items-baseline space-x-2 rounded-lg px-2 py-1 bg-green-50 text-xs" className="flex items-baseline space-x-2 rounded-lg px-2 py-1 bg-emerald-50 text-xs"
title="Nonce" title="Nonce"
> >
<span className="text-green-400"> <span className="text-emerald-400">
<FontAwesomeIcon icon={faArrowUp} size="1x" /> <FontAwesomeIcon icon={faArrowUp} size="1x" />
</span> </span>
<span className="text-green-600">{commify(value)}</span> <span className="text-emerald-600">{commify(value)}</span>
</span> </span>
); );

View File

@ -6,7 +6,7 @@ type PercentageBarProps = {
const PercentageBar: React.FC<PercentageBarProps> = ({ perc }) => ( const PercentageBar: React.FC<PercentageBarProps> = ({ perc }) => (
<div className="self-center w-40 border rounded border-gray-200"> <div className="self-center w-40 border rounded border-gray-200">
<div className="w-full h-5 rounded bg-gradient-to-r from-red-400 via-yellow-300 to-green-400 relative"> <div className="w-full h-5 rounded bg-gradient-to-r from-red-400 via-amber-300 to-emerald-400 relative">
<div <div
className="absolute top-0 right-0 bg-white h-full rounded-r" className="absolute top-0 right-0 bg-white h-full rounded-r"
style={{ width: `${100 - perc}%` }} style={{ width: `${100 - perc}%` }}

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import React, { PropsWithChildren, useMemo } from "react";
import { import {
useSelectionContext, useSelectionContext,
OptionalSelection, OptionalSelection,
@ -62,18 +62,17 @@ type HighlighterBoxProps = {
deselect: () => void; deselect: () => void;
}; };
const HighlighterBox: React.FC<HighlighterBoxProps> = React.memo( const HighlighterBox: React.FC<PropsWithChildren<HighlighterBoxProps>> =
({ selected, select, deselect, children }) => ( React.memo(({ selected, select, deselect, children }) => (
<div <div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${ className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selected ? "border-orange-400 bg-yellow-100" : "border-transparent" selected ? "border-orange-400 bg-amber-100" : "border-transparent"
}`} }`}
onMouseEnter={select} onMouseEnter={select}
onMouseLeave={deselect} onMouseLeave={deselect}
> >
{children} {children}
</div> </div>
) ));
);
export default SelectionHighlighter; export default SelectionHighlighter;

View File

@ -74,7 +74,7 @@ const Content: React.FC<ContentProps> = ({
}) => ( }) => (
<> <>
<div <div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} className={`self-center w-5 h-5 ${linkable ? "" : "grayscale"}`}
> >
<TokenLogo chainId={chainId} address={address} name={name} /> <TokenLogo chainId={chainId} address={address} name={name} />
</div> </div>

View File

@ -4,21 +4,18 @@ import DecoratedAddressLink from "./DecoratedAddressLink";
import { useSelectedTransaction } from "../useSelectedTransaction"; import { useSelectedTransaction } from "../useSelectedTransaction";
import { useBlockNumberContext } from "../useBlockTagContext"; import { useBlockNumberContext } from "../useBlockTagContext";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
import { Metadata } from "../sourcify/useSourcify";
import { AddressContext, ChecksummedAddress } from "../types"; import { AddressContext, ChecksummedAddress } from "../types";
type TransactionAddressProps = { type TransactionAddressProps = {
address: ChecksummedAddress; address: ChecksummedAddress;
addressCtx?: AddressContext | undefined; addressCtx?: AddressContext | undefined;
metadata?: Metadata | null | undefined;
showCodeIndicator?: boolean; showCodeIndicator?: boolean;
}; };
const TransactionAddress: React.FC<TransactionAddressProps> = ({ const TransactionAddress: React.FC<TransactionAddressProps> = ({
address, address,
addressCtx, addressCtx,
metadata,
showCodeIndicator = false, showCodeIndicator = false,
}) => { }) => {
const txData = useSelectedTransaction(); const txData = useSelectedTransaction();
@ -26,6 +23,8 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
const creation = address === txData?.confirmedData?.createdContractAddress; const creation = address === txData?.confirmedData?.createdContractAddress;
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const blockNumber = useBlockNumberContext(); const blockNumber = useBlockNumberContext();
const toHasCode = useHasCode( const toHasCode = useHasCode(
provider, provider,
@ -42,11 +41,10 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
<DecoratedAddressLink <DecoratedAddressLink
address={address} address={address}
addressCtx={addressCtx} addressCtx={addressCtx}
miner={address === txData?.confirmedData?.miner} miner={address === block?.miner}
txFrom={address === txData?.from} txFrom={address === txData?.from}
txTo={address === txData?.to || creation} txTo={address === txData?.to || creation}
creation={creation} creation={creation}
metadata={metadata}
eoa={ eoa={
showCodeIndicator && blockNumber !== undefined showCodeIndicator && blockNumber !== undefined
? !toHasCode ? !toHasCode

View File

@ -0,0 +1,41 @@
import React, { useContext } from "react";
import { BlockTag } from "@ethersproject/providers";
import { BigNumber } from "@ethersproject/bignumber";
import FormattedBalance from "./FormattedBalance";
import { RuntimeContext } from "../useRuntime";
import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle";
import FiatValue from "./FiatValue";
type TransactionDetailsValueProps = {
blockTag: BlockTag | undefined;
value: BigNumber;
};
const TransactionDetailsValue: React.FC<TransactionDetailsValueProps> = ({
blockTag,
value,
}) => {
const { provider } = useContext(RuntimeContext);
const blockETHUSDPrice = useETHUSDOracle(provider, blockTag);
const {
nativeCurrency: { symbol },
} = useChainInfo();
const fiatValue =
!value.isZero() && blockETHUSDPrice !== undefined
? value.mul(blockETHUSDPrice).div(10 ** 8)
: undefined;
return (
<>
<FormattedBalance value={value} /> {symbol}{" "}
{fiatValue && (
<span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
<FiatValue value={fiatValue} />
</span>
)}
</>
);
};
export default TransactionDetailsValue;

View File

@ -11,6 +11,7 @@ export enum Direction {
} }
export enum Flags { export enum Flags {
// Means the transaction internal sends ETH to the miner, e.g. flashbots
MINER, MINER,
} }
@ -23,15 +24,15 @@ const TransactionDirection: React.FC<TransactionDirectionProps> = ({
direction, direction,
flags, flags,
}) => { }) => {
let bgColor = "bg-green-50"; let bgColor = "bg-emerald-50";
let fgColor = "text-green-500"; let fgColor = "text-emerald-500";
let msg: string | null = null; let msg: string | null = null;
if (direction === Direction.IN) { if (direction === Direction.IN) {
msg = "IN"; msg = "IN";
} else if (direction === Direction.OUT) { } else if (direction === Direction.OUT) {
bgColor = "bg-yellow-100"; bgColor = "bg-amber-100";
fgColor = "text-yellow-600"; fgColor = "text-amber-600";
msg = "OUT"; msg = "OUT";
} else if (direction === Direction.SELF) { } else if (direction === Direction.SELF) {
bgColor = "bg-gray-200"; bgColor = "bg-gray-200";
@ -39,12 +40,12 @@ const TransactionDirection: React.FC<TransactionDirectionProps> = ({
msg = "SELF"; msg = "SELF";
} else if (direction === Direction.INTERNAL) { } else if (direction === Direction.INTERNAL) {
msg = "INT"; msg = "INT";
bgColor = "bg-green-100"; bgColor = "bg-emerald-100";
} }
if (flags === Flags.MINER) { if (flags === Flags.MINER) {
bgColor = "bg-yellow-50"; bgColor = "bg-amber-50";
fgColor = "text-yellow-400"; fgColor = "text-amber-400";
} }
return ( return (

View File

@ -77,7 +77,7 @@ const Content: React.FC<ContentProps> = ({
}) => ( }) => (
<> <>
<div <div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} className={`self-center w-5 h-5 ${linkable ? "" : "grayscale"}`}
> >
<TokenLogo chainId={chainId} address={address} name={name} /> <TokenLogo chainId={chainId} address={address} name={name} />
</div> </div>

View File

@ -94,7 +94,7 @@ const Content: React.FC<ContentProps> = ({
}) => ( }) => (
<> <>
<div <div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} className={`self-center w-5 h-5 ${linkable ? "" : "grayscale"}`}
> >
<TokenLogo chainId={chainId} address={address} name={name} /> <TokenLogo chainId={chainId} address={address} name={name} />
</div> </div>

View File

@ -102,7 +102,7 @@ const Content: React.FC<ContentProps> = ({
}) => ( }) => (
<> <>
<div <div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} className={`self-center w-5 h-5 ${linkable ? "" : "grayscale"}`}
> >
<TokenLogo chainId={chainId} address={address} name={name} /> <TokenLogo chainId={chainId} address={address} name={name} />
</div> </div>

View File

@ -25,7 +25,7 @@ const ValueHighlighter: React.FC<ValueHighlighterProps> = ({
selection !== null && selection !== null &&
selection.type === "value" && selection.type === "value" &&
selection.content === value.toString() selection.content === value.toString()
? "border-orange-400 bg-yellow-100" ? "border-orange-400 bg-amber-100"
: "border-transparent" : "border-transparent"
}`} }`}
onMouseEnter={select} onMouseEnter={select}

View File

@ -1,33 +0,0 @@
import { useMemo } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { ChecksummedAddress } from "./types";
import { Metadata, useMultipleMetadata } from "./sourcify/useSourcify";
import { useAppConfigContext } from "./useAppConfig";
import { useAddressesWithCode } from "./useErigonHooks";
export const useDedupedAddresses = (
addresses: ChecksummedAddress[]
): ChecksummedAddress[] => {
return useMemo(() => {
const deduped = new Set(addresses);
return [...deduped];
}, [addresses]);
};
export const useContractsMetadata = (
addresses: ChecksummedAddress[],
provider: JsonRpcProvider | undefined,
baseMetadatas?: Record<string, Metadata | null>
) => {
const deduped = useDedupedAddresses(addresses);
const contracts = useAddressesWithCode(provider, deduped);
const { sourcifySource } = useAppConfigContext();
const metadatas = useMultipleMetadata(
baseMetadatas,
contracts,
provider?.network.chainId,
sourcifySource
);
return metadatas;
};

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { HelmetProvider, Helmet } from "react-helmet-async"; 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";
@ -9,7 +9,9 @@ import "./index.css";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
ReactDOM.render( const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<React.StrictMode> <React.StrictMode>
<HelmetProvider> <HelmetProvider>
<Helmet> <Helmet>
@ -23,8 +25,7 @@ ReactDOM.render(
</Helmet> </Helmet>
<App /> <App />
</HelmetProvider> </HelmetProvider>
</React.StrictMode>, </React.StrictMode>
document.getElementById("root")
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -1,23 +0,0 @@
import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address";
import { InternalOperation } from "./types";
export const getInternalOperations = async (
provider: JsonRpcProvider,
txHash: string
) => {
const rawTransfers = await provider.send("ots_getInternalOperations", [
txHash,
]);
const _transfers: InternalOperation[] = [];
for (const t of rawTransfers) {
_transfers.push({
type: t.type,
from: getAddress(t.from),
to: getAddress(t.to),
value: t.value,
});
}
return _transfers;
};

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { isAddress } from "@ethersproject/address"; import { isAddress } from "@ethersproject/address";
import { QrReader } from "@blackbox-vision/react-qr-reader"; import { QrReader } from "@otterscan/react-qr-reader";
import { OnResultFunction } from "@blackbox-vision/react-qr-reader/dist-types/types"; import { OnResultFunction } from "@otterscan/react-qr-reader/dist-types/types";
import { BarcodeFormat } from "@zxing/library"; import { BarcodeFormat } from "@zxing/library";
import { Dialog } from "@headlessui/react"; import { Dialog } from "@headlessui/react";

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
type PageButtonProps = { type PageButtonProps = {
@ -6,14 +6,14 @@ type PageButtonProps = {
disabled?: boolean; disabled?: boolean;
}; };
const PageButton: React.FC<PageButtonProps> = ({ const PageButton: React.FC<PropsWithChildren<PageButtonProps>> = ({
goToPage, goToPage,
disabled, disabled,
children, children,
}) => { }) => {
if (disabled) { if (disabled) {
return ( return (
<span className="bg-link-blue bg-opacity-10 text-gray-400 rounded-lg px-3 py-2 text-xs"> <span className="bg-link-blue/10 text-gray-400 rounded-lg px-3 py-2 text-xs">
{children} {children}
</span> </span>
); );
@ -21,7 +21,7 @@ const PageButton: React.FC<PageButtonProps> = ({
return ( return (
<NavLink <NavLink
className="transition-colors bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded-lg px-3 py-2 text-xs" className="transition-colors bg-link-blue/10 text-link-blue hover:bg-link-blue/100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded-lg px-3 py-2 text-xs"
to={`?p=${goToPage}`} to={`?p=${goToPage}`}
> >
{children} {children}

View File

@ -1,6 +1,4 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { BlockTag } from "@ethersproject/abstract-provider";
import { BigNumber } from "@ethersproject/bignumber";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
@ -14,28 +12,23 @@ import TransactionDirection, {
Flags, Flags,
} from "../components/TransactionDirection"; } from "../components/TransactionDirection";
import TransactionValue from "../components/TransactionValue"; import TransactionValue from "../components/TransactionValue";
import { ChecksummedAddress, ProcessedTransaction } from "../types"; import TransactionItemFiatFee from "./TransactionItemFiatFee";
import { ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useHasCode, useSendsToMiner } from "../useErigonHooks";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue";
import { Metadata } from "../sourcify/useSourcify";
type TransactionItemProps = { type TransactionItemProps = {
tx: ProcessedTransaction; tx: ProcessedTransaction;
selectedAddress?: string; selectedAddress?: string;
feeDisplay: FeeDisplay; feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
const TransactionItem: React.FC<TransactionItemProps> = ({ const TransactionItem: React.FC<TransactionItemProps> = ({
tx, tx,
selectedAddress, selectedAddress,
feeDisplay, feeDisplay,
priceMap,
metadatas,
}) => { }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const toHasCode = useHasCode( const toHasCode = useHasCode(
@ -43,6 +36,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
tx.to ?? undefined, tx.to ?? undefined,
tx.blockNumber - 1 tx.blockNumber - 1
); );
const [sendsToMiner] = useSendsToMiner(provider, tx.hash, tx.miner);
let direction: Direction | undefined; let direction: Direction | undefined;
if (selectedAddress) { if (selectedAddress) {
@ -60,14 +54,12 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
} }
} }
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; const flash = tx.gasPrice.isZero() && sendsToMiner;
return ( return (
<div <div
className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${ className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${
flash flash ? "bg-amber-100 hover:bg-amber-200" : "hover:bg-skin-table-hover"
? "bg-yellow-100 hover:bg-yellow-200"
: "hover:bg-skin-table-hover"
} px-2 py-3`} } px-2 py-3`}
> >
<div className="col-span-2 flex space-x-1 items-baseline"> <div className="col-span-2 flex space-x-1 items-baseline">
@ -100,7 +92,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<span> <span>
<TransactionDirection <TransactionDirection
direction={direction} direction={direction}
flags={tx.internalMinerInteraction ? Flags.MINER : undefined} flags={sendsToMiner ? Flags.MINER : undefined}
/> />
</span> </span>
</span> </span>
@ -115,7 +107,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.to} address={tx.to}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.to} miner={tx.miner === tx.to}
metadata={metadatas[tx.to]}
eoa={toHasCode === undefined ? undefined : !toHasCode} eoa={toHasCode === undefined ? undefined : !toHasCode}
/> />
</AddressHighlighter> </AddressHighlighter>
@ -125,7 +116,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.createdContractAddress!} address={tx.createdContractAddress!}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
creation creation
metadata={metadatas[tx.createdContractAddress!]}
eoa={false} eoa={false}
/> />
</AddressHighlighter> </AddressHighlighter>
@ -137,15 +127,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
</span> </span>
<span className="font-balance text-xs text-gray-500 truncate"> <span className="font-balance text-xs text-gray-500 truncate">
{feeDisplay === FeeDisplay.TX_FEE && formatValue(tx.fee, 18)} {feeDisplay === FeeDisplay.TX_FEE && formatValue(tx.fee, 18)}
{feeDisplay === FeeDisplay.TX_FEE_USD && {feeDisplay === FeeDisplay.TX_FEE_USD && (
(priceMap[tx.blockNumber] ? ( <TransactionItemFiatFee blockTag={tx.blockNumber} fee={tx.fee} />
<ETH2USDValue )}
ethAmount={tx.fee}
eth2USDValue={priceMap[tx.blockNumber]}
/>
) : (
"N/A"
))}
{feeDisplay === FeeDisplay.GAS_PRICE && formatValue(tx.gasPrice, 9)} {feeDisplay === FeeDisplay.GAS_PRICE && formatValue(tx.gasPrice, 9)}
</span> </span>
</div> </div>

View File

@ -0,0 +1,25 @@
import React, { useContext } from "react";
import { BlockTag } from "@ethersproject/providers";
import { BigNumber } from "@ethersproject/bignumber";
import FiatValue from "../components/FiatValue";
import { RuntimeContext } from "../useRuntime";
import { useETHUSDOracle } from "../usePriceOracle";
type TransactionItemFiatFeeProps = {
blockTag: BlockTag;
fee: BigNumber;
};
const TransactionItemFiatFee: React.FC<TransactionItemFiatFeeProps> = ({
blockTag,
fee,
}) => {
const { provider } = useContext(RuntimeContext);
const eth2USDValue = useETHUSDOracle(provider, blockTag);
const fiatValue =
eth2USDValue !== undefined ? fee.mul(eth2USDValue).div(10 ** 8) : undefined;
return fiatValue ? <FiatValue value={fiatValue} /> : <>N/A</>;
};
export default TransactionItemFiatFee;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
type UndefinedPageButtonProps = { type UndefinedPageButtonProps = {
@ -8,16 +8,12 @@ type UndefinedPageButtonProps = {
disabled?: boolean; disabled?: boolean;
}; };
const UndefinedPageButton: React.FC<UndefinedPageButtonProps> = ({ const UndefinedPageButton: React.FC<
address, PropsWithChildren<UndefinedPageButtonProps>
direction, > = ({ address, direction, hash, disabled, children }) => {
hash,
disabled,
children,
}) => {
if (disabled) { if (disabled) {
return ( return (
<span className="bg-link-blue bg-opacity-10 text-gray-400 rounded-lg px-3 py-2 text-xs"> <span className="bg-link-blue/10 text-gray-400 rounded-lg px-3 py-2 text-xs">
{children} {children}
</span> </span>
); );
@ -25,7 +21,7 @@ const UndefinedPageButton: React.FC<UndefinedPageButtonProps> = ({
return ( return (
<NavLink <NavLink
className="transition-colors bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded-lg px-3 py-2 text-xs" className="transition-colors bg-link-blue/10 text-link-blue hover:bg-link-blue/100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded-lg px-3 py-2 text-xs"
to={`/address/${address}/txs/${direction}${ to={`/address/${address}/txs/${direction}${
direction === "prev" || direction === "next" ? `?h=${hash}` : "" direction === "prev" || direction === "next" ? `?h=${hash}` : ""
}`} }`}

View File

@ -1,8 +1,9 @@
import { useState, useEffect, useMemo } from "react"; import { useMemo } from "react";
import { Interface } from "@ethersproject/abi"; import { Interface } from "@ethersproject/abi";
import { ErrorDescription } from "@ethersproject/abi/lib/interface"; import { ErrorDescription } from "@ethersproject/abi/lib/interface";
import useSWRImmutable from "swr/immutable";
import { ChecksummedAddress, TransactionData } from "../types"; import { ChecksummedAddress, TransactionData } from "../types";
import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url"; import { useAppConfigContext } from "../useAppConfig";
export type UserMethod = { export type UserMethod = {
notice?: string | undefined; notice?: string | undefined;
@ -80,148 +81,164 @@ export type Metadata = {
}; };
}; };
const fetchSourcifyMetadata = async ( export enum SourcifySource {
// Resolve trusted IPNS for root IPFS
IPFS_IPNS,
// Centralized Sourcify servers
CENTRAL_SERVER,
}
const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) {
return defaultIpfsGatewayPrefix;
}
if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix;
}
throw new Error(`Unknown Sourcify integration source code: ${source}`);
};
/**
* Builds a complete Sourcify metadata.json URL given the contract address
* and chain.
*/
export const sourcifyMetadata = (
address: ChecksummedAddress, address: ChecksummedAddress,
chainId: number, chainId: number,
source: SourcifySource, source: SourcifySource,
abortController: AbortController type: MatchType
): Promise<Metadata | null> => { ) =>
try { `${resolveSourcifySource(source)}/contracts/${
const metadataURL = sourcifyMetadata(address, chainId, source); type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
const result = await fetch(metadataURL, { }/${chainId}/${address}/metadata.json`;
signal: abortController.signal,
}); export const sourcifySourceFile = (
if (result.ok) { address: ChecksummedAddress,
return await result.json(); chainId: number,
filepath: string,
source: SourcifySource,
type: MatchType
) =>
`${resolveSourcifySource(source)}/contracts/${
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
}/${chainId}/${address}/sources/${filepath}`;
export enum MatchType {
FULL_MATCH,
PARTIAL_MATCH,
} }
return null; export type Match = {
} catch (err) { type: MatchType;
console.error(err); metadata: Metadata;
return null;
}
}; };
// TODO: replace every occurrence with the multiple version one const sourcifyFetcher = async (
export const useSourcify = ( _: "sourcify",
address: ChecksummedAddress | undefined, address: ChecksummedAddress,
chainId: number | undefined, chainId: number,
source: SourcifySource sourcifySource: SourcifySource
): Metadata | null | undefined => { ): Promise<Match | null | undefined> => {
const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>(); // Try full match
try {
useEffect(() => { const url = sourcifyMetadata(
if (!address || chainId === undefined) {
return;
}
setRawMetadata(undefined);
const abortController = new AbortController();
const fetchMetadata = async () => {
const _metadata = await fetchSourcifyMetadata(
address, address,
chainId, chainId,
source, sourcifySource,
abortController MatchType.FULL_MATCH
); );
setRawMetadata(_metadata); const res = await fetch(url);
if (res.ok) {
return {
type: MatchType.FULL_MATCH,
metadata: await res.json(),
}; };
fetchMetadata();
return () => {
abortController.abort();
};
}, [address, chainId, source]);
return rawMetadata;
};
export const useMultipleMetadata = (
baseMetadatas: Record<string, Metadata | null> | undefined,
addresses: ChecksummedAddress[] | undefined,
chainId: number | undefined,
source: SourcifySource
): Record<ChecksummedAddress, Metadata | null | undefined> => {
const [rawMetadata, setRawMetadata] = useState<
Record<string, Metadata | null | undefined>
>({});
useEffect(() => {
if (addresses === undefined || chainId === undefined) {
return;
} }
setRawMetadata({}); } catch (err) {
console.info(
const abortController = new AbortController(); `error while getting Sourcify full_match metadata: chainId=${chainId} address=${address} err=${err}; falling back to partial_match`
const fetchMetadata = async (_addresses: string[]) => {
const fetchers: Promise<Metadata | null>[] = [];
for (const address of _addresses) {
fetchers.push(
fetchSourcifyMetadata(address, chainId, source, abortController)
); );
} }
const results = await Promise.all(fetchers); // Fallback to try partial match
if (abortController.signal.aborted) { try {
return; const url = sourcifyMetadata(
address,
chainId,
sourcifySource,
MatchType.PARTIAL_MATCH
);
const res = await fetch(url);
if (res.ok) {
return {
type: MatchType.PARTIAL_MATCH,
metadata: await res.json(),
};
} }
let metadatas: Record<string, Metadata | null> = {}; return null;
if (baseMetadatas) { } catch (err) {
metadatas = { ...baseMetadatas }; console.warn(
`error while getting Sourcify partial_match metadata: chainId=${chainId} address=${address} err=${err}`
);
return null;
} }
for (let i = 0; i < results.length; i++) {
metadatas[_addresses[i]] = results[i];
}
setRawMetadata(metadatas);
}; };
const filtered = addresses.filter((a) => baseMetadatas?.[a] === undefined); export const useSourcifyMetadata = (
fetchMetadata(filtered); address: ChecksummedAddress | undefined,
chainId: number | undefined
return () => { ): Match | null | undefined => {
abortController.abort(); const { sourcifySource } = useAppConfigContext();
const metadataURL = () =>
address === undefined || chainId === undefined
? null
: ["sourcify", address, chainId, sourcifySource];
const { data, error } = useSWRImmutable<Match | null | undefined>(
metadataURL,
sourcifyFetcher
);
if (error) {
return null;
}
return data;
}; };
}, [baseMetadatas, addresses, chainId, source]);
return rawMetadata; const contractFetcher = async (url: string): Promise<string | null> => {
const res = await fetch(url);
if (res.ok) {
return await res.text();
}
return null;
}; };
export const useContract = ( export const useContract = (
checksummedAddress: string, checksummedAddress: string,
networkId: number, networkId: number,
filename: string, filename: string,
source: any, sourcifySource: SourcifySource,
sourcifySource: SourcifySource type: MatchType
) => { ) => {
const [content, setContent] = useState<string>(source.content);
useEffect(() => {
if (source.content) {
return;
}
const abortController = new AbortController();
const readContent = async () => {
const normalizedFilename = filename.replaceAll(/[@:]/g, "_"); const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
const url = sourcifySourceFile( const url = sourcifySourceFile(
checksummedAddress, checksummedAddress,
networkId, networkId,
normalizedFilename, normalizedFilename,
sourcifySource sourcifySource,
type
); );
const res = await fetch(url, { signal: abortController.signal });
if (res.ok) { const { data, error } = useSWRImmutable(url, contractFetcher);
const _content = await res.text(); if (error) {
setContent(_content); return undefined;
} }
}; return data;
readContent();
return () => {
abortController.abort();
};
}, [checksummedAddress, networkId, filename, source.content, sourcifySource]);
return content;
}; };
export const useTransactionDescription = ( export const useTransactionDescription = (

View File

@ -12,7 +12,7 @@ const Blip: React.FC<BlipProps> = ({ value }) => {
<Transition <Transition
show show
appear appear
enter="transition transform ease-in duration-1000 translate-x-full pl-3" enter="transition ease-in duration-1000 translate-x-full pl-3"
enterFrom="opacity-100 translate-y-0" enterFrom="opacity-100 translate-y-0"
enterTo="opacity-0 -translate-y-5" enterTo="opacity-0 -translate-y-5"
afterEnter={() => setShow(false)} afterEnter={() => setShow(false)}
@ -20,7 +20,7 @@ const Blip: React.FC<BlipProps> = ({ value }) => {
{show && value !== 0 && ( {show && value !== 0 && (
<div <div
className={`absolute bottom-0 font-bold ${ className={`absolute bottom-0 font-bold ${
value > 0 ? "text-green-500" : "text-red-500" value > 0 ? "text-emerald-500" : "text-red-500"
} text-3xl`} } text-3xl`}
> >
{value > 0 ? `+${value}` : `${value}`} {value > 0 ? `+${value}` : `${value}`}

View File

@ -33,7 +33,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
<div <div
className={`text-right ${ className={`text-right ${
block.gasUsed.gt(gasTarget) block.gasUsed.gt(gasTarget)
? "text-green-500" ? "text-emerald-500"
: block.gasUsed.lt(gasTarget) : block.gasUsed.lt(gasTarget)
? "text-red-500" ? "text-red-500"
: "" : ""

View File

@ -124,7 +124,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
); );
return ( return (
<div className="w-full flex-grow"> <div className="w-full 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 ">
@ -161,7 +161,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
<div className="text-right">Gas target</div> <div className="text-right">Gas target</div>
<div className="text-right">Base fee</div> <div className="text-right">Base fee</div>
<div className="text-right col-span-2 flex space-x-1 justify-end items-baseline"> <div className="text-right col-span-2 flex space-x-1 justify-end items-baseline">
<span className="text-yellow-400"> <span className="text-amber-400">
<FontAwesomeIcon icon={faCoins} /> <FontAwesomeIcon icon={faCoins} />
</span> </span>
<span>Rewards</span> <span>Rewards</span>
@ -184,10 +184,10 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
key={b.hash} key={b.hash}
show={i < MAX_BLOCK_HISTORY} show={i < MAX_BLOCK_HISTORY}
appear appear
enter="transition transform ease-out duration-500" enter="transition ease-out duration-500"
enterFrom="opacity-0 -translate-y-10" enterFrom="opacity-0 -translate-y-10"
enterTo="opacity-100 translate-y-0" enterTo="opacity-100 translate-y-0"
leave="transition transform ease-out duration-1000" leave="transition ease-out duration-1000"
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-10" leaveTo="opacity-0 translate-y-10"
> >

View File

@ -1,5 +1,5 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { useLatestBlock } from "../../useLatestBlock"; import { useLatestBlockHeader } from "../../useLatestBlock";
import { RuntimeContext } from "../../useRuntime"; import { RuntimeContext } from "../../useRuntime";
import Countdown from "./Countdown"; import Countdown from "./Countdown";
import Blocks from "./Blocks"; import Blocks from "./Blocks";
@ -7,9 +7,9 @@ import { londonBlockNumber } from "./params";
const London: React.FC = () => { const London: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const block = useLatestBlock(provider); const block = useLatestBlockHeader(provider);
if (!provider || !block) { if (!provider || !block) {
return <div className="flex-grow"></div>; return <div className="grow"></div>;
} }
// Display countdown // Display countdown

View File

@ -1,6 +1,5 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useContext, useState } from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { TransactionDescription } from "@ethersproject/abi";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
@ -18,18 +17,15 @@ import NavNonce from "./NavNonce";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
import InternalTransactionOperation from "../components/InternalTransactionOperation"; import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
import TransactionDetailsValue from "../components/TransactionDetailsValue";
import TransactionType from "../components/TransactionType"; import TransactionType from "../components/TransactionType";
import TransactionFee from "./TransactionFee";
import RewardSplit from "./RewardSplit"; import RewardSplit from "./RewardSplit";
import GasValue from "../components/GasValue"; import GasValue from "../components/GasValue";
import USDValue from "../components/USDValue"; import USDValue from "../components/USDValue";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import ETH2USDValue from "../components/ETH2USDValue";
import TokenTransferItem from "../TokenTransferItem"; import TokenTransferItem from "../TokenTransferItem";
import { import { TransactionData } from "../types";
TransactionData,
InternalOperation,
ChecksummedAddress,
} from "../types";
import PercentageBar from "../components/PercentageBar"; import PercentageBar from "../components/PercentageBar";
import ExternalLink from "../components/ExternalLink"; import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition"; import RelativePosition from "../components/RelativePosition";
@ -41,35 +37,31 @@ import {
use4Bytes, use4Bytes,
useTransactionDescription, useTransactionDescription,
} from "../use4Bytes"; } from "../use4Bytes";
import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify"; import {
useError,
useSourcifyMetadata,
useTransactionDescription as useSourcifyTransactionDescription,
} from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks"; import {
import { useTransactionError } from "../useErigonHooks"; useBlockDataFromTransaction,
useSendsToMiner,
useTokenTransfers,
useTransactionError,
} from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle"; import { useETHUSDOracle } from "../usePriceOracle";
type DetailsProps = { type DetailsProps = {
txData: TransactionData; txData: TransactionData;
txDesc: TransactionDescription | null | undefined;
toMetadata: Metadata | null | undefined;
userDoc?: UserDoc | undefined;
devDoc?: DevDoc | undefined;
internalOps?: InternalOperation[];
sendsEthToMiner: boolean;
}; };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({ txData }) => {
txData, const { provider } = useContext(RuntimeContext);
txDesc, const block = useBlockDataFromTransaction(provider, txData);
toMetadata,
userDoc,
devDoc,
internalOps,
sendsEthToMiner,
}) => {
const hasEIP1559 = const hasEIP1559 =
txData.confirmedData?.blockBaseFeePerGas !== undefined && block?.baseFeePerGas !== undefined && block?.baseFeePerGas !== null;
txData.confirmedData?.blockBaseFeePerGas !== null;
const fourBytes = const fourBytes =
txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x"; txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x";
@ -80,11 +72,24 @@ const Details: React.FC<DetailsProps> = ({
txData.value txData.value
); );
const [sendsEthToMiner, internalOps] = useSendsToMiner(
provider,
txData.confirmedData ? txData.transactionHash : undefined,
block?.miner
);
const tokenTransfers = useTokenTransfers(txData);
const match = useSourcifyMetadata(txData?.to, provider?.network.chainId);
const metadata = match?.metadata;
const txDesc = useSourcifyTransactionDescription(metadata, txData);
const userDoc = metadata?.output.userdoc;
const devDoc = metadata?.output.devdoc;
const resolvedTxDesc = txDesc ?? fourBytesTxDesc; const resolvedTxDesc = txDesc ?? fourBytesTxDesc;
const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined; const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined;
const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined; const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined;
const { provider } = useContext(RuntimeContext);
const { const {
nativeCurrency: { name, symbol }, nativeCurrency: { name, symbol },
} = useChainInfo(); } = useChainInfo();
@ -94,28 +99,12 @@ const Details: React.FC<DetailsProps> = ({
txData?.confirmedData?.blockNumber txData?.confirmedData?.blockNumber
); );
const addresses = useMemo(() => {
const _addresses: ChecksummedAddress[] = [];
if (txData.to) {
_addresses.push(txData.to);
}
if (txData.confirmedData?.createdContractAddress) {
_addresses.push(txData.confirmedData.createdContractAddress);
}
for (const t of txData.tokenTransfers) {
_addresses.push(t.from);
_addresses.push(t.to);
_addresses.push(t.token);
}
return _addresses;
}, [txData]);
const metadatas = useContractsMetadata(addresses, provider);
const [errorMsg, outputData, isCustomError] = useTransactionError( const [errorMsg, outputData, isCustomError] = useTransactionError(
provider, provider,
txData.transactionHash txData.transactionHash
); );
const errorDescription = useError( const errorDescription = useError(
toMetadata, metadata,
isCustomError ? outputData : undefined isCustomError ? outputData : undefined
); );
const userError = errorDescription const userError = errorDescription
@ -138,7 +127,7 @@ const Details: React.FC<DetailsProps> = ({
{txData.confirmedData === undefined ? ( {txData.confirmedData === undefined ? (
<span className="italic text-gray-400">Pending</span> <span className="italic text-gray-400">Pending</span>
) : txData.confirmedData.status ? ( ) : txData.confirmedData.status ? (
<span className="flex items-baseline w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> <span className="flex items-baseline w-min rounded-lg space-x-1 px-3 py-1 bg-emerald-50 text-emerald-500 text-xs">
<FontAwesomeIcon <FontAwesomeIcon
className="self-center" className="self-center"
icon={faCheckCircle} icon={faCheckCircle}
@ -235,22 +224,24 @@ const Details: React.FC<DetailsProps> = ({
confirmations={txData.confirmedData.confirmations} confirmations={txData.confirmedData.confirmations}
/> />
</div> </div>
{block && (
<div className="flex space-x-2 items-baseline pl-3"> <div className="flex space-x-2 items-baseline pl-3">
<RelativePosition <RelativePosition
pos={txData.confirmedData.transactionIndex} pos={txData.confirmedData.transactionIndex}
total={txData.confirmedData.blockTransactionCount - 1} total={block.transactionCount - 1}
/> />
<PercentagePosition <PercentagePosition
perc={ perc={
txData.confirmedData.transactionIndex / txData.confirmedData.transactionIndex /
(txData.confirmedData.blockTransactionCount - 1) (block.transactionCount - 1)
} }
/> />
</div> </div>
)}
</div> </div>
</InfoRow> </InfoRow>
<InfoRow title="Timestamp"> <InfoRow title="Timestamp">
<Timestamp value={txData.confirmedData.timestamp} /> {block && <Timestamp value={block.timestamp} />}
</InfoRow> </InfoRow>
</> </>
)} )}
@ -269,11 +260,7 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}> <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
{txData.to ? ( {txData.to ? (
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress <TransactionAddress address={txData.to} showCodeIndicator />
address={txData.to}
metadata={metadatas?.[txData.to]}
showCodeIndicator
/>
<Copy value={txData.to} /> <Copy value={txData.to} />
</div> </div>
) : txData.confirmedData === undefined ? ( ) : txData.confirmedData === undefined ? (
@ -284,9 +271,6 @@ const Details: React.FC<DetailsProps> = ({
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress <TransactionAddress
address={txData.confirmedData?.createdContractAddress!} address={txData.confirmedData?.createdContractAddress!}
metadata={
metadatas?.[txData.confirmedData?.createdContractAddress!]
}
/> />
<Copy value={txData.confirmedData.createdContractAddress!} /> <Copy value={txData.confirmedData.createdContractAddress!} />
</div> </div>
@ -298,7 +282,6 @@ const Details: React.FC<DetailsProps> = ({
key={i} key={i}
txData={txData} txData={txData}
internalOp={op} internalOp={op}
ethUSDPrice={blockETHUSDPrice}
/> />
))} ))}
</div> </div>
@ -309,28 +292,18 @@ const Details: React.FC<DetailsProps> = ({
<MethodName data={txData.data} /> <MethodName data={txData.data} />
</InfoRow> </InfoRow>
)} )}
{txData.tokenTransfers.length > 0 && ( {tokenTransfers && tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> <InfoRow title={`Tokens Transferred (${tokenTransfers.length})`}>
{txData.tokenTransfers.map((t, i) => ( {tokenTransfers.map((t, i) => (
<TokenTransferItem <TokenTransferItem key={i} t={t} />
key={i}
t={t}
tokenMeta={txData.tokenMetas[t.token]}
metadatas={metadatas}
/>
))} ))}
</InfoRow> </InfoRow>
)} )}
<InfoRow title="Value"> <InfoRow title="Value">
<FormattedBalance value={txData.value} /> {symbol}{" "} <TransactionDetailsValue
{!txData.value.isZero() && blockETHUSDPrice && ( blockTag={txData.confirmedData?.blockNumber}
<span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from"> value={txData.value}
<ETH2USDValue
ethAmount={txData.value}
eth2USDValue={blockETHUSDPrice}
/> />
</span>
)}
</InfoRow> </InfoRow>
<InfoRow <InfoRow
title={ title={
@ -369,7 +342,7 @@ const Details: React.FC<DetailsProps> = ({
<FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei) <FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei)
</span> </span>
{sendsEthToMiner && ( {sendsEthToMiner && (
<span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1"> <span className="rounded text-amber-500 bg-amber-100 text-xs px-2 py-1">
Flashbots Flashbots
</span> </span>
)} )}
@ -397,18 +370,10 @@ const Details: React.FC<DetailsProps> = ({
</div> </div>
</InfoRow> </InfoRow>
)} )}
{txData.confirmedData && hasEIP1559 && ( {block && hasEIP1559 && (
<InfoRow title="Block Base Fee"> <InfoRow title="Block Base Fee">
<FormattedBalance <FormattedBalance value={block.baseFeePerGas!} decimals={9} /> Gwei (
value={txData.confirmedData.blockBaseFeePerGas!} <FormattedBalance value={block.baseFeePerGas!} decimals={0} /> wei)
decimals={9}
/>{" "}
Gwei (
<FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!}
decimals={0}
/>{" "}
wei)
</InfoRow> </InfoRow>
)} )}
{txData.confirmedData && ( {txData.confirmedData && (
@ -416,15 +381,7 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title="Transaction Fee"> <InfoRow title="Transaction Fee">
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<FormattedBalance value={txData.confirmedData.fee} /> {symbol}{" "} <TransactionFee confirmedData={txData.confirmedData} />
{blockETHUSDPrice && (
<span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
<ETH2USDValue
ethAmount={txData.confirmedData.fee}
eth2USDValue={blockETHUSDPrice}
/>
</span>
)}
</div> </div>
{hasEIP1559 && <RewardSplit txData={txData} />} {hasEIP1559 && <RewardSplit txData={txData} />}
</div> </div>

View File

@ -1,6 +1,6 @@
import React, { useMemo } from "react"; import React, { useContext, useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider"; import { Log } from "@ethersproject/abstract-provider";
import { Fragment, Interface, LogDescription } from "@ethersproject/abi"; import { Fragment, Interface } from "@ethersproject/abi";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import TransactionAddress from "../components/TransactionAddress"; import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
@ -8,16 +8,35 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature"; import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { useTopic0 } from "../useTopic0"; import { useTopic0 } from "../useTopic0";
import { ChecksummedAddress } from "../types"; import { RuntimeContext } from "../useRuntime";
import { Metadata } from "../sourcify/useSourcify"; import { useSourcifyMetadata } from "../sourcify/useSourcify";
type LogEntryProps = { type LogEntryProps = {
log: Log; log: Log;
logDesc: LogDescription | null | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => { const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
const { provider } = useContext(RuntimeContext);
const match = useSourcifyMetadata(log.address, provider?.network.chainId);
const logDesc = useMemo(() => {
if (!match) {
return match;
}
const abi = match.metadata.output.abi;
const intf = new Interface(abi as any);
try {
return intf.parseLog({
topics: log.topics,
data: log.data,
});
} catch (err) {
console.warn("Couldn't find function signature", err);
return null;
}
}, [log, match]);
const rawTopic0 = log.topics[0]; const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0); const topic0 = useTopic0(rawTopic0);
@ -47,7 +66,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
return ( return (
<div className="flex space-x-10 py-5"> <div className="flex space-x-10 py-5">
<div> <div>
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> <span className="rounded-full w-12 h-12 flex items-center justify-center bg-emerald-50 text-emerald-500">
{log.logIndex} {log.logIndex}
</span> </span>
</div> </div>
@ -56,10 +75,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
<div className="font-bold text-right">Address</div> <div className="font-bold text-right">Address</div>
<div className="col-span-11 mr-auto"> <div className="col-span-11 mr-auto">
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress <TransactionAddress address={log.address} />
address={log.address}
metadata={metadatas[log.address]}
/>
<Copy value={log.address} /> <Copy value={log.address} />
</div> </div>
</div> </div>

View File

@ -1,73 +1,20 @@
import React, { useContext, useMemo } from "react"; import React from "react";
import { Interface } from "@ethersproject/abi";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import LogEntry from "./LogEntry"; import LogEntry from "./LogEntry";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { Metadata } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks";
type LogsProps = { type LogsProps = {
txData: TransactionData; txData: TransactionData;
metadata: Metadata | null | undefined;
}; };
const Logs: React.FC<LogsProps> = ({ txData, metadata }) => { const Logs: React.FC<LogsProps> = ({ txData }) => (
const baseMetadatas = useMemo((): Record<string, Metadata | null> => {
if (!txData.to || metadata === undefined) {
return {};
}
const md: Record<string, Metadata | null> = {};
md[txData.to] = metadata;
return md;
}, [txData.to, metadata]);
const logAddresses = useMemo(
() => txData.confirmedData?.logs.map((l) => l.address) ?? [],
[txData]
);
const { provider } = useContext(RuntimeContext);
const metadatas = useContractsMetadata(logAddresses, provider, baseMetadatas);
const logDescs = useMemo(() => {
if (!txData) {
return undefined;
}
return txData.confirmedData?.logs.map((l) => {
const mt = metadatas[l.address];
if (!mt) {
return mt;
}
const abi = mt.output.abi;
const intf = new Interface(abi as any);
try {
return intf.parseLog({
topics: l.topics,
data: l.data,
});
} catch (err) {
console.warn("Couldn't find function signature", err);
return null;
}
});
}, [metadatas, txData]);
return (
<ContentFrame tabs> <ContentFrame tabs>
{txData.confirmedData && ( {txData.confirmedData && (
<> <>
{txData.confirmedData.logs.length > 0 ? ( {txData.confirmedData.logs.length > 0 ? (
<> <>
{txData.confirmedData.logs.map((l, i) => ( {txData.confirmedData.logs.map((l, i) => (
<LogEntry <LogEntry key={i} log={l} />
key={i}
log={l}
logDesc={logDescs?.[i]}
metadatas={metadatas}
/>
))} ))}
</> </>
) : ( ) : (
@ -77,6 +24,5 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
)} )}
</ContentFrame> </ContentFrame>
); );
};
export default React.memo(Logs); export default React.memo(Logs);

View File

@ -1,4 +1,7 @@
import React, { PropsWithChildren, useContext, useState } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { RuntimeContext } from "../useRuntime";
import { useTransactionBySenderAndNonce } from "../useErigonHooks";
import { ChecksummedAddress } from "../types"; import { ChecksummedAddress } from "../types";
import { addressByNonceURL } from "../url"; import { addressByNonceURL } from "../url";
@ -9,28 +12,53 @@ type NavButtonProps = {
disabled?: boolean; disabled?: boolean;
}; };
const NavButton: React.FC<NavButtonProps> = ({ const NavButton: React.FC<PropsWithChildren<NavButtonProps>> = ({
sender, sender,
nonce, nonce,
disabled, disabled,
children, children,
}) => { }) => {
const [prefetch, setPrefetch] = useState<boolean>(false);
if (disabled) { if (disabled) {
return ( return (
<span className="bg-link-blue bg-opacity-10 text-gray-300 rounded px-2 py-1 text-xs"> <span className="bg-link-blue/10 text-gray-300 rounded px-2 py-1 text-xs">
{children} {children}
</span> </span>
); );
} }
return ( return (
<>
<NavLink <NavLink
className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded px-2 py-1 text-xs" className="bg-link-blue/10 text-link-blue hover:bg-link-blue/100 hover:text-white rounded px-2 py-1 text-xs"
to={addressByNonceURL(sender, nonce)} to={addressByNonceURL(sender, nonce)}
onMouseOver={() => setPrefetch(true)}
> >
{children} {children}
</NavLink> </NavLink>
{prefetch && <Prefetcher checksummedAddress={sender} nonce={nonce} />}
</>
); );
}; };
type PrefetcherProps = {
checksummedAddress: ChecksummedAddress;
nonce: number;
};
const Prefetcher: React.FC<PrefetcherProps> = ({
checksummedAddress,
nonce,
}) => {
const { provider } = useContext(RuntimeContext);
const _txHash = useTransactionBySenderAndNonce(
provider,
checksummedAddress,
nonce
);
return <></>;
};
export default NavButton; export default NavButton;

View File

@ -1,15 +1,11 @@
import React, { useContext, useEffect } from "react"; import React, { useContext } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight"; import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
import NavButton from "./NavButton"; import NavButton from "./NavButton";
import { ChecksummedAddress } from "../types"; import { ChecksummedAddress } from "../types";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { import { useTransactionCount } from "../useErigonHooks";
prefetchTransactionBySenderAndNonce,
useTransactionCount,
} from "../useErigonHooks";
import { useSWRConfig } from "swr";
type NavNonceProps = { type NavNonceProps = {
sender: ChecksummedAddress; sender: ChecksummedAddress;
@ -20,25 +16,6 @@ const NavNonce: React.FC<NavNonceProps> = ({ sender, nonce }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const count = useTransactionCount(provider, sender); const count = useTransactionCount(provider, sender);
// Prefetch
const swrConfig = useSWRConfig();
useEffect(() => {
if (!provider || !sender || nonce === undefined || count === undefined) {
return;
}
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce - 1);
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce + 1);
if (count > 0) {
prefetchTransactionBySenderAndNonce(
swrConfig,
provider,
sender,
count - 1
);
}
}, [swrConfig, provider, sender, nonce, count]);
return ( return (
<div className="pl-2 self-center flex space-x-1"> <div className="pl-2 self-center flex space-x-1">
<NavButton sender={sender} nonce={nonce - 1} disabled={nonce === 0}> <NavButton sender={sender} nonce={nonce - 1} disabled={nonce === 0}>

View File

@ -1,24 +1,30 @@
import React from "react"; import React, { useContext } from "react";
import { BigNumber } from "@ethersproject/bignumber";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import PercentageGauge from "../components/PercentageGauge"; import PercentageGauge from "../components/PercentageGauge";
import { TransactionData } from "../types"; import { RuntimeContext } from "../useRuntime";
import { useBlockDataFromTransaction } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { TransactionData } from "../types";
type RewardSplitProps = { type RewardSplitProps = {
txData: TransactionData; txData: TransactionData;
}; };
const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => { const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol }, nativeCurrency: { symbol },
} = useChainInfo(); } = useChainInfo();
const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed); const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed);
const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul( const burntFees = block
txData.confirmedData!.gasUsed ? block.baseFeePerGas!.mul(txData.confirmedData!.gasUsed)
); : BigNumber.from(0);
const minerReward = paidFees.sub(burntFees); const minerReward = paidFees.sub(burntFees);
const burntPerc = const burntPerc =
@ -49,13 +55,13 @@ const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
</div> </div>
<PercentageGauge <PercentageGauge
perc={minerPerc} perc={minerPerc}
bgColor="bg-yellow-100" bgColor="bg-amber-100"
bgColorPerc="bg-yellow-300" bgColorPerc="bg-amber-300"
textColor="text-yellow-700" textColor="text-amber-700"
/> />
<div className="flex items-baseline space-x-1"> <div className="flex items-baseline space-x-1">
<span className="flex space-x-1"> <span className="flex space-x-1">
<span className="text-yellow-300" title="Miner fees"> <span className="text-amber-300" title="Miner fees">
<FontAwesomeIcon icon={faCoins} size="1x" /> <FontAwesomeIcon icon={faCoins} size="1x" />
</span> </span>
<span> <span>

View File

@ -17,13 +17,13 @@ const TraceItem: React.FC<TraceItemProps> = ({ t, last }) => {
return ( return (
<> <>
<div className="flex relative"> <div className="flex relative">
<div className="absolute border-l border-b w-5 h-6 transform -translate-y-3"></div> <div className="absolute border-l border-b w-5 h-6 -translate-y-3"></div>
{!last && ( {!last && (
<div className="absolute left-0 border-l w-5 h-full transform translate-y-3"></div> <div className="absolute left-0 border-l w-5 h-full translate-y-3"></div>
)} )}
{t.children && ( {t.children && (
<Switch <Switch
className="absolute left-0 bg-white transform -translate-x-1/2 text-gray-500" className="absolute left-0 bg-white -translate-x-1/2 text-gray-500"
checked={expanded} checked={expanded}
onChange={setExpanded} onChange={setExpanded}
> >

View File

@ -0,0 +1,36 @@
import React, { useContext } from "react";
import FormattedBalance from "../components/FormattedBalance";
import FiatValue from "../components/FiatValue";
import { RuntimeContext } from "../useRuntime";
import { useETHUSDOracle } from "../usePriceOracle";
import { useChainInfo } from "../useChainInfo";
import { ConfirmedTransactionData } from "../types";
type TransactionFeeProps = {
confirmedData: ConfirmedTransactionData;
};
const TransactionFee: React.FC<TransactionFeeProps> = ({ confirmedData }) => {
const { provider } = useContext(RuntimeContext);
const blockETHUSDPrice = useETHUSDOracle(provider, confirmedData.blockNumber);
const {
nativeCurrency: { symbol },
} = useChainInfo();
const fiatValue =
blockETHUSDPrice !== undefined
? confirmedData.fee.mul(blockETHUSDPrice).div(10 ** 8)
: undefined;
return (
<>
<FormattedBalance value={confirmedData.fee} /> {symbol}{" "}
{fiatValue && (
<span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
<FiatValue value={fiatValue} />
</span>
)}
</>
);
};
export default TransactionFee;

View File

@ -5,7 +5,7 @@ type BooleanDecoderProps = {
}; };
const BooleanDecoder: React.FC<BooleanDecoderProps> = ({ r }) => ( const BooleanDecoder: React.FC<BooleanDecoderProps> = ({ r }) => (
<span className={`${r ? "text-green-700" : "text-red-700"}`}> <span className={`${r ? "text-emerald-700" : "text-red-700"}`}>
{r.toString()} {r.toString()}
</span> </span>
); );

View File

@ -27,7 +27,7 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
<th className="col-span-8 pr-1">value</th> <th className="col-span-8 pr-1">value</th>
</tr> </tr>
{!hasParamNames && ( {!hasParamNames && (
<tr className="grid grid-cols-12 text-left gap-x-2 py-2 bg-yellow-100 text-red-700"> <tr className="grid grid-cols-12 text-left gap-x-2 py-2 bg-amber-100 text-red-700">
<th className="col-span-12 px-1"> <th className="col-span-12 px-1">
{paramTypes.length > 0 && paramTypes[0].name !== null {paramTypes.length > 0 && paramTypes[0].name !== null
? "Parameter names are estimated." ? "Parameter names are estimated."

View File

@ -18,7 +18,6 @@ export type ProcessedTransaction = {
from?: string; from?: string;
to: string | null; to: string | null;
createdContractAddress?: string; createdContractAddress?: string;
internalMinerInteraction?: boolean;
value: BigNumber; value: BigNumber;
fee: BigNumber; fee: BigNumber;
gasPrice: BigNumber; gasPrice: BigNumber;
@ -37,8 +36,6 @@ export type TransactionData = {
from: string; from: string;
to?: string; to?: string;
value: BigNumber; value: BigNumber;
tokenTransfers: TokenTransfer[];
tokenMetas: TokenMetas;
type: number; type: number;
maxFeePerGas?: BigNumber | undefined; maxFeePerGas?: BigNumber | undefined;
maxPriorityFeePerGas?: BigNumber | undefined; maxPriorityFeePerGas?: BigNumber | undefined;
@ -53,11 +50,7 @@ export type ConfirmedTransactionData = {
status: boolean; status: boolean;
blockNumber: number; blockNumber: number;
transactionIndex: number; transactionIndex: number;
blockBaseFeePerGas?: BigNumber | undefined | null;
blockTransactionCount: number;
confirmations: number; confirmations: number;
timestamp: number;
miner: string;
createdContractAddress?: string; createdContractAddress?: string;
fee: BigNumber; fee: BigNumber;
gasUsed: BigNumber; gasUsed: BigNumber;

View File

@ -29,48 +29,5 @@ export const transactionURL = (txHash: string) => `/tx/${txHash}`;
export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) => export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) =>
`/address/${address}?nonce=${nonce}`; `/address/${address}?nonce=${nonce}`;
export enum SourcifySource {
// Resolve trusted IPNS for root IPFS
IPFS_IPNS,
// Centralized Sourcify servers
CENTRAL_SERVER,
}
const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) {
return defaultIpfsGatewayPrefix;
}
if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix;
}
throw new Error(`Unknown Sourcify integration source code: ${source}`);
};
export const sourcifyMetadata = (
address: ChecksummedAddress,
chainId: number,
source: SourcifySource
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/metadata.json`;
export const sourcifySourceFile = (
address: ChecksummedAddress,
chainId: number,
filepath: string,
source: SourcifySource
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/sources/${filepath}`;
export const openInRemixURL = (checksummedAddress: string, networkId: number) => export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`; `https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;

View File

@ -5,6 +5,7 @@ import {
TransactionDescription, TransactionDescription,
} from "@ethersproject/abi"; } from "@ethersproject/abi";
import { BigNumberish } from "@ethersproject/bignumber"; import { BigNumberish } from "@ethersproject/bignumber";
import { Fetcher } from "swr";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url"; import { fourBytesURL } from "./url";
@ -29,16 +30,33 @@ export const extract4Bytes = (rawInput: string): string | null => {
return rawInput.slice(0, 10); return rawInput.slice(0, 10);
}; };
const fetch4Bytes = async ( type FourBytesKey = [id: "4bytes", fourBytes: string];
assetsURLPrefix: string, type FourBytesFetcher = Fetcher<
fourBytes: string FourBytesEntry | null | undefined,
): Promise<FourBytesEntry | null> => { FourBytesKey
>;
const fourBytesFetcher =
(assetsURLPrefix: string): FourBytesFetcher =>
async (_, key) => {
if (key === null || key === "0x") {
return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
const fourBytes = key.slice(2);
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try { try {
const res = await fetch(signatureURL); const res = await fetch(signatureURL);
if (!res.ok) { if (!res.ok) {
console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`);
return null; return null;
} }
@ -54,7 +72,8 @@ const fetch4Bytes = async (
}; };
return entry; return entry;
} catch (err) { } catch (err) {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err); // Network error or something wrong with URL config;
// silence and don't try it again
return null; return null;
} }
}; };
@ -75,26 +94,10 @@ export const use4Bytes = (
const { config } = useContext(RuntimeContext); const { config } = useContext(RuntimeContext);
const assetsURLPrefix = config?.assetsURLPrefix; const assetsURLPrefix = config?.assetsURLPrefix;
const fourBytesKey = assetsURLPrefix !== undefined ? rawFourBytes : null;
const fourBytesFetcher = (key: string | null) => { const fetcher = fourBytesFetcher(assetsURLPrefix!);
if (key === null || key === "0x") { const { data, error } = useSWRImmutable(["4bytes", fourBytesKey], fetcher);
return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
return fetch4Bytes(assetsURLPrefix!, key.slice(2));
};
const { data, error } = useSWRImmutable<FourBytesEntry | null | undefined>(
assetsURLPrefix !== undefined ? rawFourBytes : null,
fourBytesFetcher
);
return error ? undefined : data; return error ? undefined : data;
}; };

View File

@ -1,5 +1,5 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { SourcifySource } from "./url"; import { SourcifySource } from "./sourcify/useSourcify";
export type AppConfig = { export type AppConfig = {
sourcifySource: SourcifySource; sourcifySource: SourcifySource;

66
src/useBeacon.ts Normal file
View File

@ -0,0 +1,66 @@
import { useContext } from "react";
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime";
// 12s
const SLOT_TIME = 12;
// TODO: remove duplication with other json fetchers
const jsonFetcher = async (url: string) => {
try {
const res = await fetch(url);
if (res.ok) {
return res.json();
}
return null;
} catch (err) {
console.warn(`error while getting beacon data: url=${url} err=${err}`);
return null;
}
};
export const useFinalizedSlot = () => {
const { config } = useContext(RuntimeContext);
// Each slot is 12s, so program SWR to revalidate at this interval
const { data, error } = useSWR(
config?.beaconAPI
? `${config?.beaconAPI}/eth/v1/beacon/headers/finalized`
: null,
jsonFetcher,
{
revalidateOnFocus: false,
refreshInterval: SLOT_TIME * 1000,
}
);
if (error) {
return undefined;
}
return data;
};
export const useBeaconGenesis = () => {
const { config } = useContext(RuntimeContext);
const { data, error } = useSWRImmutable(
config?.beaconAPI ? `${config?.beaconAPI}/eth/v1/beacon/genesis` : null,
jsonFetcher
);
if (error) {
return undefined;
}
return data;
};
export const useSlotTime = (slot: number | undefined): number | undefined => {
const genesis = useBeaconGenesis();
if (slot === undefined || genesis === undefined) {
return undefined;
}
const rawDate = genesis.data.genesis_time;
return parseInt(rawDate) + slot * SLOT_TIME;
};

View File

@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext } from "react";
import useSWRImmutable from "swr/immutable";
import { chainInfoURL } from "./url"; import { chainInfoURL } from "./url";
import { OtterscanRuntime } from "./useRuntime"; import { OtterscanRuntime } from "./useRuntime";
@ -24,40 +25,33 @@ export const defaultChainInfo: ChainInfo = {
export const ChainInfoContext = createContext<ChainInfo | undefined>(undefined); export const ChainInfoContext = createContext<ChainInfo | undefined>(undefined);
const chainInfoFetcher = async (assetsURLPrefix: string, chainId: number) => {
const url = chainInfoURL(assetsURLPrefix, chainId);
const res = await fetch(url);
if (!res.ok) {
return defaultChainInfo;
}
const info: ChainInfo = await res.json();
return info;
};
export const useChainInfoFromMetadataFile = ( export const useChainInfoFromMetadataFile = (
runtime: OtterscanRuntime | undefined runtime: OtterscanRuntime | undefined
): ChainInfo | undefined => { ): ChainInfo | undefined => {
const assetsURLPrefix = runtime?.config?.assetsURLPrefix; const assetsURLPrefix = runtime?.config?.assetsURLPrefix;
const chainId = runtime?.provider?.network.chainId; const chainId = runtime?.provider?.network.chainId;
const [chainInfo, setChainInfo] = useState<ChainInfo | undefined>(undefined); const { data, error } = useSWRImmutable(
assetsURLPrefix !== undefined && chainId !== undefined
useEffect(() => { ? [assetsURLPrefix, chainId]
if (assetsURLPrefix === undefined || chainId === undefined) { : null,
setChainInfo(undefined); chainInfoFetcher
return; );
if (error) {
return defaultChainInfo;
} }
return data;
const readChainInfo = async () => {
try {
const res = await fetch(chainInfoURL(assetsURLPrefix, chainId));
if (!res.ok) {
setChainInfo(defaultChainInfo);
return;
}
const info: ChainInfo = await res.json();
setChainInfo(info);
} catch (err) {
// ignore
setChainInfo(defaultChainInfo);
return;
}
};
readChainInfo();
}, [assetsURLPrefix, chainId]);
return chainInfo;
}; };
export const useChainInfo = (): ChainInfo => { export const useChainInfo = (): ChainInfo => {

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
export type OtterscanConfig = { export type OtterscanConfig = {
erigonURL?: string; erigonURL?: string;
beaconAPI?: string;
assetsURLPrefix?: string; assetsURLPrefix?: string;
}; };

Some files were not shown because too many files have changed in this diff Show More