Merge branch 'release/v2022.08.03-otterscan'
This commit is contained in:
commit
543ba4db40
|
@ -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
|
||||||
|
|
|
@ -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
4bytes
2
4bytes
|
@ -1 +1 @@
|
||||||
Subproject commit 9603b20bd08ef0089a5fdca385afd3a3251d662c
|
Subproject commit 5197eb52b81b8594b6c5d3de023e649bec9523ca
|
|
@ -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
2
chains
|
@ -1 +1 @@
|
||||||
Subproject commit 9a908009cd88293a08222f75888d7f1bb82c13c8
|
Subproject commit b725d8e7cce9ceb97b888c9ad5a1eec9495e194d
|
|
@ -1,11 +0,0 @@
|
||||||
// craco.config.js
|
|
||||||
module.exports = {
|
|
||||||
style: {
|
|
||||||
postcss: {
|
|
||||||
plugins: [
|
|
||||||
require('tailwindcss'),
|
|
||||||
require('autoprefixer'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
// postcss.config.js
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"erigonURL": "http://localhost:8545",
|
"erigonURL": "http://localhost:8545",
|
||||||
|
"beaconAPI": null,
|
||||||
"assetsURLPrefix": "http://localhost:3001"
|
"assetsURLPrefix": "http://localhost:3001"
|
||||||
}
|
}
|
|
@ -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>
|
|
|
@ -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;"
|
||||||
|
|
208
src/Address.tsx
208
src/Address.tsx
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||||
|
|
36
src/App.tsx
36
src/App.tsx
|
@ -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();
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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;
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
27
src/Home.tsx
27
src/Home.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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} />
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
|
@ -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} />
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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 ${
|
||||||
|
|
|
@ -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 }) =>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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}%` }}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
33
src/hooks.ts
33
src/hooks.ts
|
@ -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;
|
|
||||||
};
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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}` : ""
|
||||||
}`}
|
}`}
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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}`}
|
||||||
|
|
|
@ -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"
|
||||||
: ""
|
: ""
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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;
|
||||||
|
|
43
src/url.ts
43
src/url.ts
|
@ -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}`;
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue