Merge branch 'release/v2022.08.03-otterscan'
This commit is contained in:
commit
543ba4db40
|
@ -20,12 +20,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
@ -33,35 +33,35 @@ jobs:
|
|||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v1.5.1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver: docker-container
|
||||
driver-opts: |
|
||||
image=moby/buildkit:master
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Docker Metadata action
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3.4.1
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
otterscan/otterscan
|
||||
ghcr.io/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v2.6.1
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
@ -23,3 +24,4 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
/.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
|
||||
COPY ["package.json", "package-lock.json", "/otterscan-build/"]
|
||||
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 ["src", "/otterscan-build/src/"]
|
||||
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 nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=builder /otterscan-build/build /usr/share/nginx/html/
|
||||
COPY --from=builder /otterscan-build/dist /usr/share/nginx/html/
|
||||
COPY --from=builder /otterscan-build/run-nginx.sh /
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
<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 enabled, running in dual mode with CORS enabled.
|
||||
Now you should have an Erigon node with Otterscan JSON-RPC APIs and CORS enabled.
|
||||
|
||||
## Run Otterscan docker image from Docker Hub
|
||||
|
||||
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:
|
||||
|
||||
|
@ -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`:
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
## (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)
|
||||
|
||||
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
|
||||
cd 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`.
|
||||
|
@ -40,7 +40,7 @@ First, a brief explanation about the app:
|
|||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blackbox-vision/react-qr-reader": "^5.0.0",
|
||||
"@chainlink/contracts": "^0.4.0",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fontsource/fira-code": "^4.5.8",
|
||||
"@fontsource/roboto": "^4.5.5",
|
||||
"@fontsource/roboto-mono": "^4.5.5",
|
||||
"@fontsource/space-grotesk": "^4.5.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@chainlink/contracts": "^0.4.2",
|
||||
"@fontsource/fira-code": "^4.5.11",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@fontsource/space-grotesk": "^4.5.9",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@otterscan/react-qr-reader": "^5.2.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^16.11.14",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/node": "^16.11.56",
|
||||
"@types/react": "^18.0.15",
|
||||
"@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-syntax-highlighter": "^13.5.2",
|
||||
"chart.js": "^3.7.1",
|
||||
"ethers": "^5.6.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.4",
|
||||
"chart.js": "^3.9.1",
|
||||
"ethers": "^5.7.0",
|
||||
"highlightjs-solidity": "^2.0.5",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-chartjs-2": "^4.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.2.3",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-image": "^4.0.3",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"serve": "^13.0.2",
|
||||
"swr": "^1.2.2",
|
||||
"typescript": "^4.6.3",
|
||||
"swr": "^1.3.0",
|
||||
"typescript": "^4.8.2",
|
||||
"use-keyboard-shortcut": "^1.1.4",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build && compress-cra",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"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-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",
|
||||
"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-stop": "docker stop otterscan"
|
||||
},
|
||||
|
@ -78,10 +74,12 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^9.8.8",
|
||||
"compress-create-react-app": "^1.2.1",
|
||||
"postcss": "^7.0.39",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"postcss": "^8.4.16",
|
||||
"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",
|
||||
"beaconAPI": null,
|
||||
"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
|
||||
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
|
||||
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 {
|
||||
useParams,
|
||||
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";
|
||||
import React from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import AddressMainPage from "./AddressMainPage";
|
||||
|
||||
const AddressTransactionByNonce = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "addresstxbynonce", webpackPrefetch: true */ "./AddressTransactionByNonce"
|
||||
)
|
||||
() => import("./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 { 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
|
||||
const [searchParams] = useSearchParams();
|
||||
const rawNonce = searchParams.get("nonce");
|
||||
if (rawNonce !== null) {
|
||||
return (
|
||||
<AddressTransactionByNonce
|
||||
checksummedAddress={checksummedAddress}
|
||||
rawNonce={rawNonce}
|
||||
/>
|
||||
);
|
||||
return <AddressTransactionByNonce rawNonce={rawNonce} />;
|
||||
}
|
||||
|
||||
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>
|
||||
{(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>
|
||||
);
|
||||
// Standard address main page with tabs
|
||||
return <AddressMainPage />;
|
||||
};
|
||||
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import StandardFrame from "./StandardFrame";
|
||||
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
|
||||
import AddressOrENSNameInvalidNonce from "./components/AddressOrENSNameInvalidNonce";
|
||||
import AddressOrENSNameNoTx from "./components/AddressOrENSNameNoTx";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
import { transactionURL } from "./url";
|
||||
import { useTransactionBySenderAndNonce } from "./useErigonHooks";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useAddressOrENS } from "./useResolvedAddresses";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
import { transactionURL } from "./url";
|
||||
|
||||
type AddressTransactionByNonceProps = {
|
||||
checksummedAddress: ChecksummedAddress | undefined;
|
||||
rawNonce: string;
|
||||
};
|
||||
|
||||
const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
|
||||
checksummedAddress,
|
||||
rawNonce,
|
||||
}) => {
|
||||
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
|
||||
const [txCount, setTxCount] = useState<number | undefined>();
|
||||
useEffect(() => {
|
||||
|
@ -54,14 +77,21 @@ const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
|
|||
checksummedAddress,
|
||||
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...
|
||||
if (
|
||||
checksummedAddress === undefined ||
|
||||
nonce === undefined ||
|
||||
txHash === undefined
|
||||
) {
|
||||
if (checksummedAddress === undefined || nonce === undefined) {
|
||||
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
|
||||
if (txHash === null) {
|
||||
return (
|
||||
|
|
36
src/App.tsx
36
src/App.tsx
|
@ -9,35 +9,13 @@ import { ConnectionStatus } from "./types";
|
|||
import { RuntimeContext, useRuntime } from "./useRuntime";
|
||||
import { ChainInfoContext, useChainInfoFromMetadataFile } from "./useChainInfo";
|
||||
|
||||
const Block = React.lazy(
|
||||
() => import(/* webpackChunkName: "block", webpackPrefetch: true */ "./Block")
|
||||
);
|
||||
const BlockTransactions = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
|
||||
)
|
||||
);
|
||||
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 Block = React.lazy(() => import("./Block"));
|
||||
const BlockTransactions = React.lazy(() => import("./BlockTransactions"));
|
||||
const Address = React.lazy(() => import("./Address"));
|
||||
const Transaction = React.lazy(() => import("./Transaction"));
|
||||
const London = React.lazy(() => import("./special/london/London"));
|
||||
const Faucets = React.lazy(() => import("./Faucets"));
|
||||
const PageNotFound = React.lazy(() => import("./PageNotFound"));
|
||||
|
||||
const App = () => {
|
||||
const runtime = useRuntime();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useEffect, useMemo, useContext } from "react";
|
||||
import { useParams, NavLink } from "react-router-dom";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { commify } from "@ethersproject/units";
|
||||
import { toUtf8String } from "@ethersproject/strings";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
@ -12,13 +11,13 @@ import ContentFrame from "./ContentFrame";
|
|||
import BlockNotFound from "./components/BlockNotFound";
|
||||
import InfoRow from "./components/InfoRow";
|
||||
import Timestamp from "./components/Timestamp";
|
||||
import BlockReward from "./BlockReward";
|
||||
import GasValue from "./components/GasValue";
|
||||
import PercentageBar from "./components/PercentageBar";
|
||||
import BlockLink from "./components/BlockLink";
|
||||
import DecoratedAddressLink from "./components/DecoratedAddressLink";
|
||||
import TransactionValue from "./components/TransactionValue";
|
||||
import FormattedBalance from "./components/FormattedBalance";
|
||||
import ETH2USDValue from "./components/ETH2USDValue";
|
||||
import USDValue from "./components/USDValue";
|
||||
import HexValue from "./components/HexValue";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
|
@ -55,7 +54,6 @@ const Block: React.FC = () => {
|
|||
}, [block]);
|
||||
const burntFees =
|
||||
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
|
||||
const netFeeReward = block?.feeReward ?? BigNumber.from(0);
|
||||
const gasUsedPerc =
|
||||
block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100;
|
||||
|
||||
|
@ -89,7 +87,7 @@ const Block: React.FC = () => {
|
|||
</InfoRow>
|
||||
<InfoRow title="Transactions">
|
||||
<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)}
|
||||
>
|
||||
{block.transactionCount} transactions
|
||||
|
@ -100,25 +98,7 @@ const Block: React.FC = () => {
|
|||
<DecoratedAddressLink address={block.miner} miner />
|
||||
</InfoRow>
|
||||
<InfoRow title="Block Reward">
|
||||
<TransactionValue value={block.blockReward.add(netFeeReward)} />
|
||||
{!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>
|
||||
</>
|
||||
)}
|
||||
<BlockReward block={block} />
|
||||
</InfoRow>
|
||||
<InfoRow title="Uncles Reward">
|
||||
<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>
|
||||
<BlockTransactionHeader blockTag={blockNumber.toNumber()} />
|
||||
<BlockTransactionResults
|
||||
blockTag={blockNumber.toNumber()}
|
||||
page={txs}
|
||||
total={totalTxs ?? 0}
|
||||
pageNumber={pageNumber}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
|
||||
|
@ -90,7 +90,8 @@ type StepProps = {
|
|||
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">
|
||||
{type === "wait" && (
|
||||
|
@ -99,7 +100,7 @@ const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => (
|
|||
</span>
|
||||
)}
|
||||
{type === "ok" && (
|
||||
<span className="text-green-600">
|
||||
<span className="text-emerald-600">
|
||||
<FontAwesomeIcon icon={faCheckCircle} size="1x" />
|
||||
</span>
|
||||
)}
|
||||
|
@ -112,6 +113,7 @@ const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => (
|
|||
</div>
|
||||
{children && <div className="ml-7 mt-4 text-sm">{children}</div>}
|
||||
</>
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
export default React.memo(ConnectionErrorPanel);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
type ContentFrameProps = {
|
||||
tabs?: boolean;
|
||||
};
|
||||
|
||||
const ContentFrame: React.FC<ContentFrameProps> = ({ tabs, children }) => {
|
||||
const ContentFrame: React.FC<PropsWithChildren<ContentFrameProps>> = ({
|
||||
tabs,
|
||||
children,
|
||||
}) => {
|
||||
return tabs ? (
|
||||
<div className="divide-y border rounded-b-lg px-3 bg-white">{children}</div>
|
||||
) : (
|
||||
|
|
|
@ -39,7 +39,7 @@ const Faucets: React.FC = () => {
|
|||
<ContentFrame>
|
||||
<div className="py-4 space-y-3">
|
||||
{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
|
||||
className="self-center"
|
||||
icon={faTriangleExclamation}
|
||||
|
@ -54,7 +54,7 @@ const Faucets: React.FC = () => {
|
|||
)}
|
||||
{/* Display the shuffling notice only if there are 1+ faucets */}
|
||||
{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
|
||||
className="self-center"
|
||||
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 Timestamp from "./components/Timestamp";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useLatestBlock } from "./useLatestBlock";
|
||||
import { useLatestBlockHeader } from "./useLatestBlock";
|
||||
import { blockURL } from "./url";
|
||||
import { useGenericSearch } from "./search/search";
|
||||
import { useFinalizedSlot, useSlotTime } from "./useBeacon";
|
||||
|
||||
const CameraScanner = React.lazy(() => import("./search/CameraScanner"));
|
||||
|
||||
|
@ -17,19 +18,21 @@ const Home: React.FC = () => {
|
|||
const { provider } = useContext(RuntimeContext);
|
||||
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);
|
||||
|
||||
document.title = "Home | Otterscan";
|
||||
|
||||
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)} />}
|
||||
<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 />
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-col"
|
||||
className="flex flex-col w-1/3"
|
||||
onSubmit={handleSubmit}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
|
@ -62,7 +65,6 @@ const Home: React.FC = () => {
|
|||
Search
|
||||
</button>
|
||||
</form>
|
||||
<div className="mx-auto h-32">
|
||||
<div className="text-lg text-link-blue hover:text-link-blue-hover font-bold">
|
||||
{provider?.network.chainId !== 11155111 && (
|
||||
<NavLink to="/special/london">
|
||||
|
@ -87,7 +89,20 @@ const Home: React.FC = () => {
|
|||
<Timestamp value={latestBlock.timestamp} />
|
||||
</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>
|
||||
{slotTime && <Timestamp value={slotTime} />}
|
||||
<div>
|
||||
State root:{" "}
|
||||
<span className="font-hash">
|
||||
{beaconData.data.header.message.state_root}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import Otter from "./otter.jpg";
|
||||
|
||||
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
|
||||
className="rounded-full"
|
||||
src={Otter}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react";
|
|||
import { Outlet } from "react-router-dom";
|
||||
import Header from "./Header";
|
||||
import { AppConfig, AppConfigContext } from "./useAppConfig";
|
||||
import { SourcifySource } from "./url";
|
||||
import { SourcifySource } from "./sourcify/useSourcify";
|
||||
|
||||
const Main: React.FC = () => {
|
||||
const [sourcifySource, setSourcifySource] = useState<SourcifySource>(
|
||||
|
|
|
@ -1,58 +1,28 @@
|
|||
import React, { useState, useEffect, useMemo, useContext } from "react";
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import React, { useMemo, useContext } from "react";
|
||||
import { commify, formatUnits } from "@ethersproject/units";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump";
|
||||
import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { formatValue } from "./components/formatter";
|
||||
import { useLatestBlock } from "./useLatestBlock";
|
||||
import { useLatestBlockHeader } from "./useLatestBlock";
|
||||
import { useChainInfo } from "./useChainInfo";
|
||||
import { useETHUSDRawOracle, useFastGasRawOracle } from "./usePriceOracle";
|
||||
|
||||
// TODO: encapsulate this magic number
|
||||
const ETH_FEED_DECIMALS = 8;
|
||||
|
||||
// TODO: reduce duplication with useETHUSDOracle
|
||||
const PriceBox: React.FC = () => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const {
|
||||
nativeCurrency: { symbol },
|
||||
} = useChainInfo();
|
||||
const latestBlock = useLatestBlock(provider);
|
||||
const latestBlock = useLatestBlockHeader(provider);
|
||||
|
||||
const maybeOutdated: boolean =
|
||||
latestBlock !== undefined &&
|
||||
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(() => {
|
||||
if (!latestPriceData) {
|
||||
return [undefined, undefined];
|
||||
|
@ -65,6 +35,7 @@ const PriceBox: React.FC = () => {
|
|||
return [formattedPrice, timestamp];
|
||||
}, [latestPriceData]);
|
||||
|
||||
const latestGasData = useFastGasRawOracle(provider, "latest");
|
||||
const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => {
|
||||
if (!latestGasData) {
|
||||
return [undefined, undefined];
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBars } from "@fortawesome/free-solid-svg-icons/faBars";
|
||||
import { SourcifySource } from "./url";
|
||||
import { useAppConfigContext } from "./useAppConfig";
|
||||
import { SourcifySource } from "./sourcify/useSourcify";
|
||||
|
||||
const SourcifyMenu: React.FC = () => {
|
||||
const { sourcifySource, setSourcifySource } = useAppConfigContext();
|
||||
|
@ -41,7 +41,7 @@ type SourcifyMenuItemProps = {
|
|||
onClick: () => void;
|
||||
};
|
||||
|
||||
const SourcifyMenuItem: React.FC<SourcifyMenuItemProps> = ({
|
||||
const SourcifyMenuItem: React.FC<PropsWithChildren<SourcifyMenuItemProps>> = ({
|
||||
checked = false,
|
||||
onClick,
|
||||
children,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
const StandardFrame: React.FC = ({ children }) => (
|
||||
<div className="flex-grow bg-gray-100 px-9 pt-3 pb-12">{children}</div>
|
||||
const StandardFrame: React.FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="grow bg-gray-100 px-9 pt-3 pb-12">{children}</div>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,32 +6,21 @@ import TransactionAddress from "./components/TransactionAddress";
|
|||
import ValueHighlighter from "./components/ValueHighlighter";
|
||||
import FormattedBalance from "./components/FormattedBalance";
|
||||
import USDAmount from "./components/USDAmount";
|
||||
import {
|
||||
AddressContext,
|
||||
ChecksummedAddress,
|
||||
TokenMeta,
|
||||
TokenTransfer,
|
||||
} from "./types";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useBlockNumberContext } from "./useBlockTagContext";
|
||||
import { Metadata } from "./sourcify/useSourcify";
|
||||
import { useTokenMetadata } from "./useErigonHooks";
|
||||
import { useTokenUSDOracle } from "./usePriceOracle";
|
||||
import { AddressContext, TokenTransfer } from "./types";
|
||||
|
||||
type TokenTransferItemProps = {
|
||||
t: TokenTransfer;
|
||||
tokenMeta?: TokenMeta | null | undefined;
|
||||
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
|
||||
};
|
||||
|
||||
// TODO: handle partial
|
||||
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
||||
t,
|
||||
tokenMeta,
|
||||
metadatas,
|
||||
}) => {
|
||||
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ t }) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const blockNumber = useBlockNumberContext();
|
||||
const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token);
|
||||
const tokenMeta = useTokenMetadata(provider, t.token);
|
||||
|
||||
return (
|
||||
<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
|
||||
address={t.from}
|
||||
addressCtx={AddressContext.FROM}
|
||||
metadata={metadatas[t.from]}
|
||||
showCodeIndicator
|
||||
/>
|
||||
</div>
|
||||
|
@ -51,7 +39,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
|||
<TransactionAddress
|
||||
address={t.to}
|
||||
addressCtx={AddressContext.TO}
|
||||
metadata={metadatas[t.to]}
|
||||
showCodeIndicator
|
||||
/>
|
||||
</div>
|
||||
|
@ -67,7 +54,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
|||
/>
|
||||
</ValueHighlighter>
|
||||
</span>
|
||||
<TransactionAddress address={t.token} metadata={metadatas[t.token]} />
|
||||
<TransactionAddress address={t.token} />
|
||||
{tokenMeta && quote !== undefined && decimals !== undefined && (
|
||||
<span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600">
|
||||
<USDAmount
|
||||
|
|
|
@ -1,13 +1,76 @@
|
|||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import TransactionPageContent from "./TransactionPageContent";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { useParams, 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 { 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 { txhash } = useParams();
|
||||
if (txhash === undefined) {
|
||||
const { txhash: txHash } = useParams();
|
||||
if (txHash === undefined) {
|
||||
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;
|
||||
|
|
|
@ -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 { BlockTag } from "@ethersproject/providers";
|
||||
import ContentFrame from "../ContentFrame";
|
||||
import InfoRow from "../components/InfoRow";
|
||||
import TransactionValue from "../components/TransactionValue";
|
||||
import ETH2USDValue from "../components/ETH2USDValue";
|
||||
import AddressBalance from "./AddressBalance";
|
||||
import TransactionAddress from "../components/TransactionAddress";
|
||||
import Copy from "../components/Copy";
|
||||
import TransactionLink from "../components/TransactionLink";
|
||||
|
@ -14,11 +12,9 @@ import TransactionItem from "../search/TransactionItem";
|
|||
import UndefinedPageControl from "../search/UndefinedPageControl";
|
||||
import { useFeeToggler } from "../search/useFeeToggler";
|
||||
import { SelectionContext, useSelection } from "../useSelection";
|
||||
import { useMultipleETHUSDOracle } from "../usePriceOracle";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { ChecksummedAddress, ProcessedTransaction } from "../types";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
import { useAddressBalance, useContractCreator } from "../useErigonHooks";
|
||||
import { BlockNumberContext } from "../useBlockTagContext";
|
||||
|
||||
|
@ -99,35 +95,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
|
||||
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 creator = useContractCreator(provider, address);
|
||||
|
||||
|
@ -138,15 +105,7 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
{balance && (
|
||||
<InfoRow title="Balance">
|
||||
<div className="space-x-2">
|
||||
<TransactionValue value={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>
|
||||
)}
|
||||
<AddressBalance balance={balance} />
|
||||
</div>
|
||||
</InfoRow>
|
||||
)}
|
||||
|
@ -180,8 +139,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
tx={tx}
|
||||
selectedAddress={address}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
))}
|
||||
<NavBar address={address} page={page} controller={controller} />
|
||||
|
|
|
@ -1,31 +1,11 @@
|
|||
import React from "react";
|
||||
import { SyntaxHighlighter, docco } from "../highlight-init";
|
||||
import { useContract } from "../sourcify/useSourcify";
|
||||
import { useAppConfigContext } from "../useAppConfig";
|
||||
|
||||
type ContractProps = {
|
||||
checksummedAddress: string;
|
||||
networkId: number;
|
||||
filename: string;
|
||||
source: any;
|
||||
content: any;
|
||||
};
|
||||
|
||||
const Contract: React.FC<ContractProps> = ({
|
||||
checksummedAddress,
|
||||
networkId,
|
||||
filename,
|
||||
source,
|
||||
}) => {
|
||||
const { sourcifySource } = useAppConfigContext();
|
||||
const content = useContract(
|
||||
checksummedAddress,
|
||||
networkId,
|
||||
filename,
|
||||
source,
|
||||
sourcifySource
|
||||
);
|
||||
|
||||
return (
|
||||
const Contract: React.FC<ContractProps> = ({ content }) => (
|
||||
<SyntaxHighlighter
|
||||
className="w-full h-full border font-code text-base"
|
||||
language="solidity"
|
||||
|
@ -34,7 +14,6 @@ const Contract: React.FC<ContractProps> = ({
|
|||
>
|
||||
{content ?? ""}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
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 { Menu } from "@headlessui/react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
@ -6,46 +6,47 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
|||
import ContentFrame from "../ContentFrame";
|
||||
import InfoRow from "../components/InfoRow";
|
||||
import Contract from "./Contract";
|
||||
import ContractFromRepo from "./ContractFromRepo";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { Match, MatchType } from "../sourcify/useSourcify";
|
||||
import ExternalLink from "../components/ExternalLink";
|
||||
import { openInRemixURL } from "../url";
|
||||
import ContractABI from "./ContractABI";
|
||||
|
||||
type ContractsProps = {
|
||||
checksummedAddress: string;
|
||||
rawMetadata: Metadata | null | undefined;
|
||||
match: Match | null | undefined;
|
||||
};
|
||||
|
||||
const Contracts: React.FC<ContractsProps> = ({
|
||||
checksummedAddress,
|
||||
rawMetadata,
|
||||
}) => {
|
||||
const Contracts: React.FC<ContractsProps> = ({ checksummedAddress, match }) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
|
||||
const [selected, setSelected] = useState<string>();
|
||||
useEffect(() => {
|
||||
if (rawMetadata) {
|
||||
setSelected(Object.keys(rawMetadata.sources)[0]);
|
||||
if (match) {
|
||||
setSelected(Object.keys(match.metadata.sources)[0]);
|
||||
}
|
||||
}, [rawMetadata]);
|
||||
const optimizer = rawMetadata?.settings?.optimizer;
|
||||
}, [match]);
|
||||
const optimizer = match?.metadata.settings?.optimizer;
|
||||
|
||||
return (
|
||||
<ContentFrame tabs>
|
||||
{rawMetadata && (
|
||||
{match && (
|
||||
<>
|
||||
<InfoRow title="Match">
|
||||
{match.type === MatchType.FULL_MATCH ? "Full" : "Partial"}
|
||||
</InfoRow>
|
||||
<InfoRow title="Language">
|
||||
<span>{rawMetadata.language}</span>
|
||||
<span>{match.metadata.language}</span>
|
||||
</InfoRow>
|
||||
<InfoRow title="Compiler">
|
||||
<span>{rawMetadata.compiler.version}</span>
|
||||
<span>{match.metadata.compiler.version}</span>
|
||||
</InfoRow>
|
||||
<InfoRow title="Optimizer Enabled">
|
||||
{optimizer?.enabled ? (
|
||||
<span>
|
||||
<span className="font-bold text-green-600">Yes</span> with{" "}
|
||||
<span className="font-bold text-green-600">
|
||||
<span className="font-bold text-emerald-600">Yes</span> with{" "}
|
||||
<span className="font-bold text-emerald-600">
|
||||
{commify(optimizer?.runs)}
|
||||
</span>{" "}
|
||||
runs
|
||||
|
@ -57,19 +58,19 @@ const Contracts: React.FC<ContractsProps> = ({
|
|||
</>
|
||||
)}
|
||||
<div className="py-5">
|
||||
{rawMetadata === undefined && (
|
||||
{match === undefined && (
|
||||
<span>Getting data from Sourcify repository...</span>
|
||||
)}
|
||||
{rawMetadata === null && (
|
||||
{match === null && (
|
||||
<span>
|
||||
Address is not a contract or couldn't find contract metadata in
|
||||
Sourcify repository.
|
||||
</span>
|
||||
)}
|
||||
{rawMetadata !== undefined && rawMetadata !== null && (
|
||||
{match !== undefined && match !== null && (
|
||||
<>
|
||||
{rawMetadata.output.abi && (
|
||||
<ContractABI abi={rawMetadata.output.abi} />
|
||||
{match.metadata.output.abi && (
|
||||
<ContractABI abi={match.metadata.output.abi} />
|
||||
)}
|
||||
<div>
|
||||
<Menu>
|
||||
|
@ -95,13 +96,13 @@ const Contracts: React.FC<ContractsProps> = ({
|
|||
</div>
|
||||
<div className="relative">
|
||||
<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}>
|
||||
<button
|
||||
className={`flex text-sm px-2 py-1 ${
|
||||
selected === k
|
||||
? "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)}
|
||||
>
|
||||
|
@ -113,13 +114,21 @@ const Contracts: React.FC<ContractsProps> = ({
|
|||
</div>
|
||||
</Menu>
|
||||
{selected && (
|
||||
<>
|
||||
{match.metadata.sources[selected].content ? (
|
||||
<Contract
|
||||
content={match.metadata.sources[selected].content}
|
||||
/>
|
||||
) : (
|
||||
<ContractFromRepo
|
||||
checksummedAddress={checksummedAddress}
|
||||
networkId={provider!.network.chainId}
|
||||
filename={selected}
|
||||
source={rawMetadata.sources[selected]}
|
||||
type={match.type}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -28,14 +28,14 @@ const DecodedFragment: React.FC<DecodedFragmentProps> = ({
|
|||
fragmentType = "function";
|
||||
sig = intf.getSighash(fragment);
|
||||
letter = "F";
|
||||
letterBg = "bg-purple-500";
|
||||
hashBg = "bg-purple-50";
|
||||
letterBg = "bg-violet-500";
|
||||
hashBg = "bg-violet-50";
|
||||
} else if (EventFragment.isEventFragment(fragment)) {
|
||||
fragmentType = "event";
|
||||
sig = intf.getEventTopic(fragment);
|
||||
letter = "E";
|
||||
letterBg = "bg-green-300";
|
||||
hashBg = "bg-green-50";
|
||||
letterBg = "bg-emerald-300";
|
||||
hashBg = "bg-emerald-50";
|
||||
} else if (ConstructorFragment.isConstructorFragment(fragment)) {
|
||||
fragmentType = "constructor";
|
||||
letter = "C";
|
||||
|
@ -49,7 +49,7 @@ const DecodedFragment: React.FC<DecodedFragmentProps> = ({
|
|||
</span>
|
||||
{letter && (
|
||||
<span
|
||||
className={`flex-shrink-0 text-xs font-code border border-gray-300 rounded-full w-5 h-5 self-center flex items-center justify-center text-white font-bold ${letterBg}`}
|
||||
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}
|
||||
</span>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import { Interface } from "@ethersproject/abi";
|
||||
import { AddressZero } from "@ethersproject/constants";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
import erc20 from "../../erc20.json";
|
||||
import { TokenMeta } from "../../types";
|
||||
|
||||
const erc20Interface = new Interface(erc20);
|
||||
const ERC20_PROTOTYPE = new Contract(AddressZero, erc20);
|
||||
|
||||
export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
|
||||
async resolveAddress(
|
||||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<TokenMeta | undefined> {
|
||||
const erc20Contract = new Contract(address, erc20Interface, provider);
|
||||
const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address);
|
||||
try {
|
||||
const name = (await erc20Contract.name()) as string;
|
||||
if (!name.trim()) {
|
||||
|
|
|
@ -12,6 +12,11 @@ const UNISWAP_V1_FACTORY_ABI = [
|
|||
|
||||
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
const UNISWAP_V1_FACTORY_PROTOTYPE = new Contract(
|
||||
UNISWAP_V1_FACTORY,
|
||||
UNISWAP_V1_FACTORY_ABI
|
||||
);
|
||||
|
||||
export type UniswapV1TokenMeta = {
|
||||
address: ChecksummedAddress;
|
||||
} & TokenMeta;
|
||||
|
@ -28,11 +33,7 @@ export class UniswapV1Resolver implements IAddressResolver<UniswapV1PairMeta> {
|
|||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<UniswapV1PairMeta | undefined> {
|
||||
const factoryContract = new Contract(
|
||||
UNISWAP_V1_FACTORY,
|
||||
UNISWAP_V1_FACTORY_ABI,
|
||||
provider
|
||||
);
|
||||
const factoryContract = UNISWAP_V1_FACTORY_PROTOTYPE.connect(provider);
|
||||
|
||||
try {
|
||||
// First, probe the getToken() function; if it responds with an UniswapV1 exchange
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import { AddressZero } from "@ethersproject/constants";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
import { ChecksummedAddress, TokenMeta } from "../../types";
|
||||
import { ERCTokenResolver } from "./ERCTokenResolver";
|
||||
|
@ -16,6 +17,16 @@ const UNISWAP_V2_PAIR_ABI = [
|
|||
"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 = {
|
||||
address: ChecksummedAddress;
|
||||
} & TokenMeta;
|
||||
|
@ -33,12 +44,9 @@ export class UniswapV2Resolver implements IAddressResolver<UniswapV2PairMeta> {
|
|||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<UniswapV2PairMeta | undefined> {
|
||||
const pairContract = new Contract(address, UNISWAP_V2_PAIR_ABI, provider);
|
||||
const factoryContract = new Contract(
|
||||
UNISWAP_V2_FACTORY,
|
||||
UNISWAP_V2_FACTORY_ABI,
|
||||
provider
|
||||
);
|
||||
const pairContract =
|
||||
UNISWAP_V2_PAIR_PROTOTYPE.connect(provider).attach(address);
|
||||
const factoryContract = UNISWAP_V2_FACTORY_PROTOTYPE.connect(provider);
|
||||
|
||||
try {
|
||||
// First, probe the factory() function; if it responds with UniswapV2 factory
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import { AddressZero } from "@ethersproject/constants";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
import { ChecksummedAddress, TokenMeta } from "../../types";
|
||||
import { ERCTokenResolver } from "./ERCTokenResolver";
|
||||
|
@ -17,6 +18,16 @@ const UNISWAP_V3_PAIR_ABI = [
|
|||
"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 = {
|
||||
address: ChecksummedAddress;
|
||||
} & TokenMeta;
|
||||
|
@ -35,12 +46,9 @@ export class UniswapV3Resolver implements IAddressResolver<UniswapV3PairMeta> {
|
|||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<UniswapV3PairMeta | undefined> {
|
||||
const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider);
|
||||
const factoryContract = new Contract(
|
||||
UNISWAP_V3_FACTORY,
|
||||
UNISWAP_V3_FACTORY_ABI,
|
||||
provider
|
||||
);
|
||||
const poolContract =
|
||||
UNISWAP_V3_PAIR_PROTOTYPE.connect(provider).attach(address);
|
||||
const factoryContract = UNISWAP_V3_FACTORY_PROTOTYPE.connect(provider);
|
||||
|
||||
try {
|
||||
// First, probe the factory() function; if it responds with UniswapV2 factory
|
||||
|
|
|
@ -1,54 +1,27 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import { BlockTag } from "@ethersproject/abstract-provider";
|
||||
import React from "react";
|
||||
import ContentFrame from "../ContentFrame";
|
||||
import PageControl from "../search/PageControl";
|
||||
import ResultHeader from "../search/ResultHeader";
|
||||
import PendingResults from "../search/PendingResults";
|
||||
import TransactionItem from "../search/TransactionItem";
|
||||
import { useFeeToggler } from "../search/useFeeToggler";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { SelectionContext, useSelection } from "../useSelection";
|
||||
import { ChecksummedAddress, ProcessedTransaction } from "../types";
|
||||
import { ProcessedTransaction } from "../types";
|
||||
import { PAGE_SIZE } from "../params";
|
||||
import { useMultipleETHUSDOracle } from "../usePriceOracle";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
|
||||
type BlockTransactionResultsProps = {
|
||||
blockTag: BlockTag;
|
||||
page?: ProcessedTransaction[];
|
||||
total: number;
|
||||
pageNumber: number;
|
||||
};
|
||||
|
||||
const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
||||
blockTag,
|
||||
page,
|
||||
total,
|
||||
pageNumber,
|
||||
}) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const selectionCtx = useSelection();
|
||||
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 (
|
||||
<ContentFrame>
|
||||
|
@ -73,13 +46,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
|||
{page ? (
|
||||
<SelectionContext.Provider value={selectionCtx}>
|
||||
{page.map((tx) => (
|
||||
<TransactionItem
|
||||
key={tx.hash}
|
||||
tx={tx}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
<TransactionItem key={tx.hash} tx={tx} feeDisplay={feeDisplay} />
|
||||
))}
|
||||
<div className="flex justify-between items-baseline py-3">
|
||||
<div className="text-sm text-gray-500">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { PropsWithChildren } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { BlockTag } from "@ethersproject/abstract-provider";
|
||||
import { blockURL } from "../url";
|
||||
|
@ -8,7 +9,7 @@ type NavButtonProps = {
|
|||
urlBuilder?: (blockNumber: BlockTag) => string;
|
||||
};
|
||||
|
||||
const NavButton: React.FC<NavButtonProps> = ({
|
||||
const NavButton: React.FC<PropsWithChildren<NavButtonProps>> = ({
|
||||
blockNum,
|
||||
disabled,
|
||||
urlBuilder,
|
||||
|
@ -16,7 +17,7 @@ const NavButton: React.FC<NavButtonProps> = ({
|
|||
}) => {
|
||||
if (disabled) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
@ -24,7 +25,7 @@ const NavButton: React.FC<NavButtonProps> = ({
|
|||
|
||||
return (
|
||||
<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)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from "react";
|
||||
import React, { PropsWithChildren, useContext } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 SourcifyLogo from "../sourcify/SourcifyLogo";
|
||||
import PlainAddress from "./PlainAddress";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useSourcifyMetadata } from "../sourcify/useSourcify";
|
||||
import { useResolvedAddress } from "../useResolvedAddresses";
|
||||
import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types";
|
||||
import { resolverRendererRegistry } from "../api/address-resolver";
|
||||
|
@ -23,7 +23,6 @@ type DecoratedAddressLinkProps = {
|
|||
selfDestruct?: boolean | undefined;
|
||||
txFrom?: boolean | undefined;
|
||||
txTo?: boolean | undefined;
|
||||
metadata?: Metadata | null | undefined;
|
||||
eoa?: boolean | undefined;
|
||||
};
|
||||
|
||||
|
@ -36,9 +35,11 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
|||
selfDestruct,
|
||||
txFrom,
|
||||
txTo,
|
||||
metadata,
|
||||
eoa,
|
||||
}) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const match = useSourcifyMetadata(address, provider?.network.chainId);
|
||||
|
||||
const mint = addressCtx === AddressContext.FROM && 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 ${
|
||||
txFrom ? "bg-skin-from" : ""
|
||||
} ${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" : ""} ${
|
||||
selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""
|
||||
}`}
|
||||
>
|
||||
{creation && (
|
||||
<span className="text-yellow-300" title="Contract creation">
|
||||
<span className="text-amber-300" title="Contract creation">
|
||||
<FontAwesomeIcon icon={faStar} size="1x" />
|
||||
</span>
|
||||
)}
|
||||
|
@ -63,7 +64,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
|||
</span>
|
||||
)}
|
||||
{mint && (
|
||||
<span className="text-green-500" title="Mint address">
|
||||
<span className="text-emerald-500" title="Mint address">
|
||||
<FontAwesomeIcon icon={faMoneyBillAlt} size="1x" />
|
||||
</span>
|
||||
)}
|
||||
|
@ -73,13 +74,13 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
|||
</span>
|
||||
)}
|
||||
{miner && (
|
||||
<span className="text-yellow-400" title="Miner address">
|
||||
<span className="text-amber-400" title="Miner address">
|
||||
<FontAwesomeIcon icon={faCoins} size="1x" />
|
||||
</span>
|
||||
)}
|
||||
{metadata && (
|
||||
{match && (
|
||||
<NavLink
|
||||
className="self-center flex-shrink-0 flex items-center"
|
||||
className="self-center shrink-0 flex items-center"
|
||||
to={`/address/${address}/contract`}
|
||||
>
|
||||
<SourcifyLogo />
|
||||
|
@ -156,11 +157,11 @@ type AddressLegendProps = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
const AddressLegend: React.FC<AddressLegendProps> = ({ title, children }) => (
|
||||
<span
|
||||
className="text-xs text-gray-400 text-opacity-70 not-italic"
|
||||
title={title}
|
||||
>
|
||||
const AddressLegend: React.FC<PropsWithChildren<AddressLegendProps>> = ({
|
||||
title,
|
||||
children,
|
||||
}) => (
|
||||
<span className="text-xs text-gray-400/70 not-italic" title={title}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -48,7 +48,7 @@ type ContentProps = {
|
|||
const Content: React.FC<ContentProps> = ({ linkable, name }) => (
|
||||
<>
|
||||
<img
|
||||
className={`self-center ${linkable ? "" : "filter grayscale"}`}
|
||||
className={`self-center ${linkable ? "" : "grayscale"}`}
|
||||
src={ENSLogo}
|
||||
alt="ENS Logo"
|
||||
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 { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
|
||||
|
||||
|
@ -6,7 +6,10 @@ type ExternalLinkProps = {
|
|||
href: string;
|
||||
};
|
||||
|
||||
const ExternalLink: React.FC<ExternalLinkProps> = ({ href, children }) => (
|
||||
const ExternalLink: React.FC<PropsWithChildren<ExternalLinkProps>> = ({
|
||||
href,
|
||||
children,
|
||||
}) => (
|
||||
<a
|
||||
className="text-link-blue hover:text-link-blue-hover"
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
|
||||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import TransactionAddress from "./TransactionAddress";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useBlockDataFromTransaction } from "../useErigonHooks";
|
||||
import { useChainInfo } from "../useChainInfo";
|
||||
import { TransactionData, InternalOperation } from "../types";
|
||||
|
||||
|
@ -17,12 +19,12 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
|
|||
txData,
|
||||
internalOp,
|
||||
}) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useBlockDataFromTransaction(provider, txData);
|
||||
const {
|
||||
nativeCurrency: { symbol },
|
||||
} = useChainInfo();
|
||||
const toMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
internalOp.to === txData.confirmedData.miner;
|
||||
const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -54,7 +56,7 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
|
|||
<AddressHighlighter address={internalOp.to}>
|
||||
<div
|
||||
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} />
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import InternalTransfer from "./InternalTransfer";
|
||||
import InternalSelfDestruct from "./InternalSelfDestruct";
|
||||
import InternalCreate from "./InternalCreate";
|
||||
|
@ -8,20 +7,14 @@ import { TransactionData, InternalOperation, OperationType } from "../types";
|
|||
type InternalTransactionOperationProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
// TODO: migrate all this logic to SWR
|
||||
ethUSDPrice: BigNumber | undefined;
|
||||
};
|
||||
|
||||
const InternalTransactionOperation: React.FC<
|
||||
InternalTransactionOperationProps
|
||||
> = ({ txData, internalOp, ethUSDPrice }) => (
|
||||
> = ({ txData, internalOp }) => (
|
||||
<>
|
||||
{internalOp.type === OperationType.TRANSFER && (
|
||||
<InternalTransfer
|
||||
txData={txData}
|
||||
internalOp={internalOp}
|
||||
ethUSDPrice={ethUSDPrice}
|
||||
/>
|
||||
<InternalTransfer txData={txData} internalOp={internalOp} />
|
||||
)}
|
||||
{internalOp.type === OperationType.SELF_DESTRUCT && (
|
||||
<InternalSelfDestruct txData={txData} internalOp={internalOp} />
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useContext } from "react";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { formatEther } from "@ethersproject/units";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
|
||||
|
@ -9,33 +8,34 @@ import AddressHighlighter from "./AddressHighlighter";
|
|||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import USDAmount from "./USDAmount";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useHasCode } from "../useErigonHooks";
|
||||
import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
|
||||
import { useChainInfo } from "../useChainInfo";
|
||||
import { useETHUSDOracle } from "../usePriceOracle";
|
||||
import { TransactionData, InternalOperation } from "../types";
|
||||
|
||||
type InternalTransferProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
// TODO: migrate all this logic to SWR
|
||||
ethUSDPrice: BigNumber | undefined;
|
||||
};
|
||||
|
||||
const InternalTransfer: React.FC<InternalTransferProps> = ({
|
||||
txData,
|
||||
internalOp,
|
||||
ethUSDPrice,
|
||||
}) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useBlockDataFromTransaction(provider, txData);
|
||||
|
||||
const {
|
||||
nativeCurrency: { symbol, decimals },
|
||||
} = useChainInfo();
|
||||
const fromMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
internalOp.from === txData.confirmedData.miner;
|
||||
const toMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
internalOp.to === txData.confirmedData.miner;
|
||||
block?.miner !== undefined && internalOp.from === block.miner;
|
||||
const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
|
||||
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const blockETHUSDPrice = useETHUSDOracle(
|
||||
provider,
|
||||
txData.confirmedData?.blockNumber
|
||||
);
|
||||
const fromHasCode = useHasCode(
|
||||
provider,
|
||||
internalOp.from,
|
||||
|
@ -58,7 +58,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
|
|||
<AddressHighlighter address={internalOp.from}>
|
||||
<div
|
||||
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
|
||||
|
@ -79,7 +79,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
|
|||
<AddressHighlighter address={internalOp.to}>
|
||||
<div
|
||||
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
|
||||
|
@ -99,12 +99,12 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
|
|||
<span>
|
||||
{formatEther(internalOp.value)} {symbol}
|
||||
</span>
|
||||
{ethUSDPrice && (
|
||||
{blockETHUSDPrice && (
|
||||
<span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600">
|
||||
<USDAmount
|
||||
amount={internalOp.value}
|
||||
amountDecimals={decimals}
|
||||
quote={ethUSDPrice}
|
||||
quote={blockETHUSDPrice}
|
||||
// TODO: migrate to SWR and standardize this magic number
|
||||
quoteDecimals={8}
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,7 @@ const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
|||
return (
|
||||
<div
|
||||
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`}
|
||||
>
|
||||
<p className="truncate" title={methodTitle}>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
type ModeTabProps = {
|
||||
disabled?: boolean | undefined;
|
||||
};
|
||||
|
||||
const ModeTab: React.FC<ModeTabProps> = ({ disabled, children }) => (
|
||||
const ModeTab: React.FC<PropsWithChildren<ModeTabProps>> = ({
|
||||
disabled,
|
||||
children,
|
||||
}) => (
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`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 { Tab } from "@headlessui/react";
|
||||
|
||||
|
@ -6,7 +6,10 @@ type NavTabProps = {
|
|||
href: string;
|
||||
};
|
||||
|
||||
const NavTab: React.FC<NavTabProps> = ({ href, children }) => (
|
||||
const NavTab: React.FC<PropsWithChildren<NavTabProps>> = ({
|
||||
href,
|
||||
children,
|
||||
}) => (
|
||||
<Tab as={Fragment}>
|
||||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
|
|
|
@ -9,13 +9,13 @@ type NonceProps = {
|
|||
|
||||
const Nonce: React.FC<NonceProps> = ({ value }) => (
|
||||
<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"
|
||||
>
|
||||
<span className="text-green-400">
|
||||
<span className="text-emerald-400">
|
||||
<FontAwesomeIcon icon={faArrowUp} size="1x" />
|
||||
</span>
|
||||
<span className="text-green-600">{commify(value)}</span>
|
||||
<span className="text-emerald-600">{commify(value)}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ type PercentageBarProps = {
|
|||
|
||||
const PercentageBar: React.FC<PercentageBarProps> = ({ perc }) => (
|
||||
<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
|
||||
className="absolute top-0 right-0 bg-white h-full rounded-r"
|
||||
style={{ width: `${100 - perc}%` }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo } from "react";
|
||||
import React, { PropsWithChildren, useMemo } from "react";
|
||||
import {
|
||||
useSelectionContext,
|
||||
OptionalSelection,
|
||||
|
@ -62,18 +62,17 @@ type HighlighterBoxProps = {
|
|||
deselect: () => void;
|
||||
};
|
||||
|
||||
const HighlighterBox: React.FC<HighlighterBoxProps> = React.memo(
|
||||
({ selected, select, deselect, children }) => (
|
||||
const HighlighterBox: React.FC<PropsWithChildren<HighlighterBoxProps>> =
|
||||
React.memo(({ selected, select, deselect, children }) => (
|
||||
<div
|
||||
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}
|
||||
onMouseLeave={deselect}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
export default SelectionHighlighter;
|
||||
|
|
|
@ -74,7 +74,7 @@ const Content: React.FC<ContentProps> = ({
|
|||
}) => (
|
||||
<>
|
||||
<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} />
|
||||
</div>
|
||||
|
|
|
@ -4,21 +4,18 @@ import DecoratedAddressLink from "./DecoratedAddressLink";
|
|||
import { useSelectedTransaction } from "../useSelectedTransaction";
|
||||
import { useBlockNumberContext } from "../useBlockTagContext";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useHasCode } from "../useErigonHooks";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
|
||||
import { AddressContext, ChecksummedAddress } from "../types";
|
||||
|
||||
type TransactionAddressProps = {
|
||||
address: ChecksummedAddress;
|
||||
addressCtx?: AddressContext | undefined;
|
||||
metadata?: Metadata | null | undefined;
|
||||
showCodeIndicator?: boolean;
|
||||
};
|
||||
|
||||
const TransactionAddress: React.FC<TransactionAddressProps> = ({
|
||||
address,
|
||||
addressCtx,
|
||||
metadata,
|
||||
showCodeIndicator = false,
|
||||
}) => {
|
||||
const txData = useSelectedTransaction();
|
||||
|
@ -26,6 +23,8 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
|
|||
const creation = address === txData?.confirmedData?.createdContractAddress;
|
||||
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useBlockDataFromTransaction(provider, txData);
|
||||
|
||||
const blockNumber = useBlockNumberContext();
|
||||
const toHasCode = useHasCode(
|
||||
provider,
|
||||
|
@ -42,11 +41,10 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
|
|||
<DecoratedAddressLink
|
||||
address={address}
|
||||
addressCtx={addressCtx}
|
||||
miner={address === txData?.confirmedData?.miner}
|
||||
miner={address === block?.miner}
|
||||
txFrom={address === txData?.from}
|
||||
txTo={address === txData?.to || creation}
|
||||
creation={creation}
|
||||
metadata={metadata}
|
||||
eoa={
|
||||
showCodeIndicator && blockNumber !== undefined
|
||||
? !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 {
|
||||
// Means the transaction internal sends ETH to the miner, e.g. flashbots
|
||||
MINER,
|
||||
}
|
||||
|
||||
|
@ -23,15 +24,15 @@ const TransactionDirection: React.FC<TransactionDirectionProps> = ({
|
|||
direction,
|
||||
flags,
|
||||
}) => {
|
||||
let bgColor = "bg-green-50";
|
||||
let fgColor = "text-green-500";
|
||||
let bgColor = "bg-emerald-50";
|
||||
let fgColor = "text-emerald-500";
|
||||
let msg: string | null = null;
|
||||
|
||||
if (direction === Direction.IN) {
|
||||
msg = "IN";
|
||||
} else if (direction === Direction.OUT) {
|
||||
bgColor = "bg-yellow-100";
|
||||
fgColor = "text-yellow-600";
|
||||
bgColor = "bg-amber-100";
|
||||
fgColor = "text-amber-600";
|
||||
msg = "OUT";
|
||||
} else if (direction === Direction.SELF) {
|
||||
bgColor = "bg-gray-200";
|
||||
|
@ -39,12 +40,12 @@ const TransactionDirection: React.FC<TransactionDirectionProps> = ({
|
|||
msg = "SELF";
|
||||
} else if (direction === Direction.INTERNAL) {
|
||||
msg = "INT";
|
||||
bgColor = "bg-green-100";
|
||||
bgColor = "bg-emerald-100";
|
||||
}
|
||||
|
||||
if (flags === Flags.MINER) {
|
||||
bgColor = "bg-yellow-50";
|
||||
fgColor = "text-yellow-400";
|
||||
bgColor = "bg-amber-50";
|
||||
fgColor = "text-amber-400";
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -77,7 +77,7 @@ const Content: React.FC<ContentProps> = ({
|
|||
}) => (
|
||||
<>
|
||||
<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} />
|
||||
</div>
|
||||
|
|
|
@ -94,7 +94,7 @@ const Content: React.FC<ContentProps> = ({
|
|||
}) => (
|
||||
<>
|
||||
<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} />
|
||||
</div>
|
||||
|
|
|
@ -102,7 +102,7 @@ const Content: React.FC<ContentProps> = ({
|
|||
}) => (
|
||||
<>
|
||||
<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} />
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ const ValueHighlighter: React.FC<ValueHighlighterProps> = ({
|
|||
selection !== null &&
|
||||
selection.type === "value" &&
|
||||
selection.content === value.toString()
|
||||
? "border-orange-400 bg-yellow-100"
|
||||
? "border-orange-400 bg-amber-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
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 ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { HelmetProvider, Helmet } from "react-helmet-async";
|
||||
import "@fontsource/space-grotesk/index.css";
|
||||
import "@fontsource/roboto/index.css";
|
||||
|
@ -9,7 +9,9 @@ import "./index.css";
|
|||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
|
||||
ReactDOM.render(
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
|
@ -23,8 +25,7 @@ ReactDOM.render(
|
|||
</Helmet>
|
||||
<App />
|
||||
</HelmetProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 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 { useNavigate } from "react-router-dom";
|
||||
import { isAddress } from "@ethersproject/address";
|
||||
import { QrReader } from "@blackbox-vision/react-qr-reader";
|
||||
import { OnResultFunction } from "@blackbox-vision/react-qr-reader/dist-types/types";
|
||||
import { QrReader } from "@otterscan/react-qr-reader";
|
||||
import { OnResultFunction } from "@otterscan/react-qr-reader/dist-types/types";
|
||||
import { BarcodeFormat } from "@zxing/library";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
type PageButtonProps = {
|
||||
|
@ -6,14 +6,14 @@ type PageButtonProps = {
|
|||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const PageButton: React.FC<PageButtonProps> = ({
|
||||
const PageButton: React.FC<PropsWithChildren<PageButtonProps>> = ({
|
||||
goToPage,
|
||||
disabled,
|
||||
children,
|
||||
}) => {
|
||||
if (disabled) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ const PageButton: React.FC<PageButtonProps> = ({
|
|||
|
||||
return (
|
||||
<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}`}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useContext } from "react";
|
||||
import { BlockTag } from "@ethersproject/abstract-provider";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
import MethodName from "../components/MethodName";
|
||||
|
@ -14,28 +12,23 @@ import TransactionDirection, {
|
|||
Flags,
|
||||
} from "../components/TransactionDirection";
|
||||
import TransactionValue from "../components/TransactionValue";
|
||||
import { ChecksummedAddress, ProcessedTransaction } from "../types";
|
||||
import TransactionItemFiatFee from "./TransactionItemFiatFee";
|
||||
import { ProcessedTransaction } from "../types";
|
||||
import { FeeDisplay } from "./useFeeToggler";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useHasCode } from "../useErigonHooks";
|
||||
import { useHasCode, useSendsToMiner } from "../useErigonHooks";
|
||||
import { formatValue } from "../components/formatter";
|
||||
import ETH2USDValue from "../components/ETH2USDValue";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
|
||||
type TransactionItemProps = {
|
||||
tx: ProcessedTransaction;
|
||||
selectedAddress?: string;
|
||||
feeDisplay: FeeDisplay;
|
||||
priceMap: Record<BlockTag, BigNumber>;
|
||||
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
|
||||
};
|
||||
|
||||
const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
tx,
|
||||
selectedAddress,
|
||||
feeDisplay,
|
||||
priceMap,
|
||||
metadatas,
|
||||
}) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const toHasCode = useHasCode(
|
||||
|
@ -43,6 +36,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
tx.to ?? undefined,
|
||||
tx.blockNumber - 1
|
||||
);
|
||||
const [sendsToMiner] = useSendsToMiner(provider, tx.hash, tx.miner);
|
||||
|
||||
let direction: Direction | undefined;
|
||||
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 (
|
||||
<div
|
||||
className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${
|
||||
flash
|
||||
? "bg-yellow-100 hover:bg-yellow-200"
|
||||
: "hover:bg-skin-table-hover"
|
||||
flash ? "bg-amber-100 hover:bg-amber-200" : "hover:bg-skin-table-hover"
|
||||
} px-2 py-3`}
|
||||
>
|
||||
<div className="col-span-2 flex space-x-1 items-baseline">
|
||||
|
@ -100,7 +92,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
<span>
|
||||
<TransactionDirection
|
||||
direction={direction}
|
||||
flags={tx.internalMinerInteraction ? Flags.MINER : undefined}
|
||||
flags={sendsToMiner ? Flags.MINER : undefined}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -115,7 +107,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
address={tx.to}
|
||||
selectedAddress={selectedAddress}
|
||||
miner={tx.miner === tx.to}
|
||||
metadata={metadatas[tx.to]}
|
||||
eoa={toHasCode === undefined ? undefined : !toHasCode}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
|
@ -125,7 +116,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
address={tx.createdContractAddress!}
|
||||
selectedAddress={selectedAddress}
|
||||
creation
|
||||
metadata={metadatas[tx.createdContractAddress!]}
|
||||
eoa={false}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
|
@ -137,15 +127,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
</span>
|
||||
<span className="font-balance text-xs text-gray-500 truncate">
|
||||
{feeDisplay === FeeDisplay.TX_FEE && formatValue(tx.fee, 18)}
|
||||
{feeDisplay === FeeDisplay.TX_FEE_USD &&
|
||||
(priceMap[tx.blockNumber] ? (
|
||||
<ETH2USDValue
|
||||
ethAmount={tx.fee}
|
||||
eth2USDValue={priceMap[tx.blockNumber]}
|
||||
/>
|
||||
) : (
|
||||
"N/A"
|
||||
))}
|
||||
{feeDisplay === FeeDisplay.TX_FEE_USD && (
|
||||
<TransactionItemFiatFee blockTag={tx.blockNumber} fee={tx.fee} />
|
||||
)}
|
||||
{feeDisplay === FeeDisplay.GAS_PRICE && formatValue(tx.gasPrice, 9)}
|
||||
</span>
|
||||
</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";
|
||||
|
||||
type UndefinedPageButtonProps = {
|
||||
|
@ -8,16 +8,12 @@ type UndefinedPageButtonProps = {
|
|||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const UndefinedPageButton: React.FC<UndefinedPageButtonProps> = ({
|
||||
address,
|
||||
direction,
|
||||
hash,
|
||||
disabled,
|
||||
children,
|
||||
}) => {
|
||||
const UndefinedPageButton: React.FC<
|
||||
PropsWithChildren<UndefinedPageButtonProps>
|
||||
> = ({ address, direction, hash, disabled, children }) => {
|
||||
if (disabled) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
@ -25,7 +21,7 @@ const UndefinedPageButton: React.FC<UndefinedPageButtonProps> = ({
|
|||
|
||||
return (
|
||||
<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}${
|
||||
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 { ErrorDescription } from "@ethersproject/abi/lib/interface";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import { ChecksummedAddress, TransactionData } from "../types";
|
||||
import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url";
|
||||
import { useAppConfigContext } from "../useAppConfig";
|
||||
|
||||
export type UserMethod = {
|
||||
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,
|
||||
chainId: number,
|
||||
source: SourcifySource,
|
||||
abortController: AbortController
|
||||
): Promise<Metadata | null> => {
|
||||
try {
|
||||
const metadataURL = sourcifyMetadata(address, chainId, source);
|
||||
const result = await fetch(metadataURL, {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (result.ok) {
|
||||
return await result.json();
|
||||
}
|
||||
type: MatchType
|
||||
) =>
|
||||
`${resolveSourcifySource(source)}/contracts/${
|
||||
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
|
||||
}/${chainId}/${address}/metadata.json`;
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
export const sourcifySourceFile = (
|
||||
address: ChecksummedAddress,
|
||||
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,
|
||||
}
|
||||
|
||||
export type Match = {
|
||||
type: MatchType;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
// TODO: replace every occurrence with the multiple version one
|
||||
export const useSourcify = (
|
||||
address: ChecksummedAddress | undefined,
|
||||
chainId: number | undefined,
|
||||
source: SourcifySource
|
||||
): Metadata | null | undefined => {
|
||||
const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!address || chainId === undefined) {
|
||||
return;
|
||||
}
|
||||
setRawMetadata(undefined);
|
||||
|
||||
const abortController = new AbortController();
|
||||
const fetchMetadata = async () => {
|
||||
const _metadata = await fetchSourcifyMetadata(
|
||||
const sourcifyFetcher = async (
|
||||
_: "sourcify",
|
||||
address: ChecksummedAddress,
|
||||
chainId: number,
|
||||
sourcifySource: SourcifySource
|
||||
): Promise<Match | null | undefined> => {
|
||||
// Try full match
|
||||
try {
|
||||
const url = sourcifyMetadata(
|
||||
address,
|
||||
chainId,
|
||||
source,
|
||||
abortController
|
||||
sourcifySource,
|
||||
MatchType.FULL_MATCH
|
||||
);
|
||||
setRawMetadata(_metadata);
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
return {
|
||||
type: MatchType.FULL_MATCH,
|
||||
metadata: await res.json(),
|
||||
};
|
||||
fetchMetadata();
|
||||
}
|
||||
} catch (err) {
|
||||
console.info(
|
||||
`error while getting Sourcify full_match metadata: chainId=${chainId} address=${address} err=${err}; falling back to partial_match`
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
// Fallback to try partial match
|
||||
try {
|
||||
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(),
|
||||
};
|
||||
}, [address, chainId, source]);
|
||||
|
||||
return rawMetadata;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`error while getting Sourcify partial_match metadata: chainId=${chainId} address=${address} err=${err}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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({});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const fetchMetadata = async (_addresses: string[]) => {
|
||||
const fetchers: Promise<Metadata | null>[] = [];
|
||||
for (const address of _addresses) {
|
||||
fetchers.push(
|
||||
fetchSourcifyMetadata(address, chainId, source, abortController)
|
||||
export const useSourcifyMetadata = (
|
||||
address: ChecksummedAddress | undefined,
|
||||
chainId: number | undefined
|
||||
): Match | null | undefined => {
|
||||
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;
|
||||
};
|
||||
|
||||
const results = await Promise.all(fetchers);
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
const contractFetcher = async (url: string): Promise<string | null> => {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
return await res.text();
|
||||
}
|
||||
let metadatas: Record<string, Metadata | null> = {};
|
||||
if (baseMetadatas) {
|
||||
metadatas = { ...baseMetadatas };
|
||||
}
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
metadatas[_addresses[i]] = results[i];
|
||||
}
|
||||
setRawMetadata(metadatas);
|
||||
};
|
||||
|
||||
const filtered = addresses.filter((a) => baseMetadatas?.[a] === undefined);
|
||||
fetchMetadata(filtered);
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [baseMetadatas, addresses, chainId, source]);
|
||||
|
||||
return rawMetadata;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useContract = (
|
||||
checksummedAddress: string,
|
||||
networkId: number,
|
||||
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 url = sourcifySourceFile(
|
||||
checksummedAddress,
|
||||
networkId,
|
||||
normalizedFilename,
|
||||
sourcifySource
|
||||
sourcifySource,
|
||||
type
|
||||
);
|
||||
const res = await fetch(url, { signal: abortController.signal });
|
||||
if (res.ok) {
|
||||
const _content = await res.text();
|
||||
setContent(_content);
|
||||
|
||||
const { data, error } = useSWRImmutable(url, contractFetcher);
|
||||
if (error) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
readContent();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [checksummedAddress, networkId, filename, source.content, sourcifySource]);
|
||||
|
||||
return content;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useTransactionDescription = (
|
||||
|
|
|
@ -12,7 +12,7 @@ const Blip: React.FC<BlipProps> = ({ value }) => {
|
|||
<Transition
|
||||
show
|
||||
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"
|
||||
enterTo="opacity-0 -translate-y-5"
|
||||
afterEnter={() => setShow(false)}
|
||||
|
@ -20,7 +20,7 @@ const Blip: React.FC<BlipProps> = ({ value }) => {
|
|||
{show && value !== 0 && (
|
||||
<div
|
||||
className={`absolute bottom-0 font-bold ${
|
||||
value > 0 ? "text-green-500" : "text-red-500"
|
||||
value > 0 ? "text-emerald-500" : "text-red-500"
|
||||
} text-3xl`}
|
||||
>
|
||||
{value > 0 ? `+${value}` : `${value}`}
|
||||
|
|
|
@ -33,7 +33,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
|
|||
<div
|
||||
className={`text-right ${
|
||||
block.gasUsed.gt(gasTarget)
|
||||
? "text-green-500"
|
||||
? "text-emerald-500"
|
||||
: block.gasUsed.lt(gasTarget)
|
||||
? "text-red-500"
|
||||
: ""
|
||||
|
|
|
@ -124,7 +124,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
|
|||
);
|
||||
|
||||
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="relative">
|
||||
<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">Base fee</div>
|
||||
<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} />
|
||||
</span>
|
||||
<span>Rewards</span>
|
||||
|
@ -184,10 +184,10 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
|
|||
key={b.hash}
|
||||
show={i < MAX_BLOCK_HISTORY}
|
||||
appear
|
||||
enter="transition transform ease-out duration-500"
|
||||
enter="transition ease-out duration-500"
|
||||
enterFrom="opacity-0 -translate-y-10"
|
||||
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"
|
||||
leaveTo="opacity-0 translate-y-10"
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { useLatestBlock } from "../../useLatestBlock";
|
||||
import { useLatestBlockHeader } from "../../useLatestBlock";
|
||||
import { RuntimeContext } from "../../useRuntime";
|
||||
import Countdown from "./Countdown";
|
||||
import Blocks from "./Blocks";
|
||||
|
@ -7,9 +7,9 @@ import { londonBlockNumber } from "./params";
|
|||
|
||||
const London: React.FC = () => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useLatestBlock(provider);
|
||||
const block = useLatestBlockHeader(provider);
|
||||
if (!provider || !block) {
|
||||
return <div className="flex-grow"></div>;
|
||||
return <div className="grow"></div>;
|
||||
}
|
||||
|
||||
// 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 { TransactionDescription } from "@ethersproject/abi";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
|
||||
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
|
||||
|
@ -18,18 +17,15 @@ import NavNonce from "./NavNonce";
|
|||
import Timestamp from "../components/Timestamp";
|
||||
import InternalTransactionOperation from "../components/InternalTransactionOperation";
|
||||
import MethodName from "../components/MethodName";
|
||||
import TransactionDetailsValue from "../components/TransactionDetailsValue";
|
||||
import TransactionType from "../components/TransactionType";
|
||||
import TransactionFee from "./TransactionFee";
|
||||
import RewardSplit from "./RewardSplit";
|
||||
import GasValue from "../components/GasValue";
|
||||
import USDValue from "../components/USDValue";
|
||||
import FormattedBalance from "../components/FormattedBalance";
|
||||
import ETH2USDValue from "../components/ETH2USDValue";
|
||||
import TokenTransferItem from "../TokenTransferItem";
|
||||
import {
|
||||
TransactionData,
|
||||
InternalOperation,
|
||||
ChecksummedAddress,
|
||||
} from "../types";
|
||||
import { TransactionData } from "../types";
|
||||
import PercentageBar from "../components/PercentageBar";
|
||||
import ExternalLink from "../components/ExternalLink";
|
||||
import RelativePosition from "../components/RelativePosition";
|
||||
|
@ -41,35 +37,31 @@ import {
|
|||
use4Bytes,
|
||||
useTransactionDescription,
|
||||
} from "../use4Bytes";
|
||||
import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify";
|
||||
import {
|
||||
useError,
|
||||
useSourcifyMetadata,
|
||||
useTransactionDescription as useSourcifyTransactionDescription,
|
||||
} from "../sourcify/useSourcify";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
import { useTransactionError } from "../useErigonHooks";
|
||||
import {
|
||||
useBlockDataFromTransaction,
|
||||
useSendsToMiner,
|
||||
useTokenTransfers,
|
||||
useTransactionError,
|
||||
} from "../useErigonHooks";
|
||||
import { useChainInfo } from "../useChainInfo";
|
||||
import { useETHUSDOracle } from "../usePriceOracle";
|
||||
|
||||
type DetailsProps = {
|
||||
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> = ({
|
||||
txData,
|
||||
txDesc,
|
||||
toMetadata,
|
||||
userDoc,
|
||||
devDoc,
|
||||
internalOps,
|
||||
sendsEthToMiner,
|
||||
}) => {
|
||||
const Details: React.FC<DetailsProps> = ({ txData }) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useBlockDataFromTransaction(provider, txData);
|
||||
|
||||
const hasEIP1559 =
|
||||
txData.confirmedData?.blockBaseFeePerGas !== undefined &&
|
||||
txData.confirmedData?.blockBaseFeePerGas !== null;
|
||||
block?.baseFeePerGas !== undefined && block?.baseFeePerGas !== null;
|
||||
|
||||
const fourBytes =
|
||||
txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x";
|
||||
|
@ -80,11 +72,24 @@ const Details: React.FC<DetailsProps> = ({
|
|||
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 userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined;
|
||||
const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined;
|
||||
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const {
|
||||
nativeCurrency: { name, symbol },
|
||||
} = useChainInfo();
|
||||
|
@ -94,28 +99,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||
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(
|
||||
provider,
|
||||
txData.transactionHash
|
||||
);
|
||||
const errorDescription = useError(
|
||||
toMetadata,
|
||||
metadata,
|
||||
isCustomError ? outputData : undefined
|
||||
);
|
||||
const userError = errorDescription
|
||||
|
@ -138,7 +127,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||
{txData.confirmedData === undefined ? (
|
||||
<span className="italic text-gray-400">Pending</span>
|
||||
) : 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
|
||||
className="self-center"
|
||||
icon={faCheckCircle}
|
||||
|
@ -235,22 +224,24 @@ const Details: React.FC<DetailsProps> = ({
|
|||
confirmations={txData.confirmedData.confirmations}
|
||||
/>
|
||||
</div>
|
||||
{block && (
|
||||
<div className="flex space-x-2 items-baseline pl-3">
|
||||
<RelativePosition
|
||||
pos={txData.confirmedData.transactionIndex}
|
||||
total={txData.confirmedData.blockTransactionCount - 1}
|
||||
total={block.transactionCount - 1}
|
||||
/>
|
||||
<PercentagePosition
|
||||
perc={
|
||||
txData.confirmedData.transactionIndex /
|
||||
(txData.confirmedData.blockTransactionCount - 1)
|
||||
(block.transactionCount - 1)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InfoRow>
|
||||
<InfoRow title="Timestamp">
|
||||
<Timestamp value={txData.confirmedData.timestamp} />
|
||||
{block && <Timestamp value={block.timestamp} />}
|
||||
</InfoRow>
|
||||
</>
|
||||
)}
|
||||
|
@ -269,11 +260,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
|
||||
{txData.to ? (
|
||||
<div className="flex items-baseline space-x-2 -ml-1">
|
||||
<TransactionAddress
|
||||
address={txData.to}
|
||||
metadata={metadatas?.[txData.to]}
|
||||
showCodeIndicator
|
||||
/>
|
||||
<TransactionAddress address={txData.to} showCodeIndicator />
|
||||
<Copy value={txData.to} />
|
||||
</div>
|
||||
) : txData.confirmedData === undefined ? (
|
||||
|
@ -284,9 +271,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<div className="flex items-baseline space-x-2 -ml-1">
|
||||
<TransactionAddress
|
||||
address={txData.confirmedData?.createdContractAddress!}
|
||||
metadata={
|
||||
metadatas?.[txData.confirmedData?.createdContractAddress!]
|
||||
}
|
||||
/>
|
||||
<Copy value={txData.confirmedData.createdContractAddress!} />
|
||||
</div>
|
||||
|
@ -298,7 +282,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
key={i}
|
||||
txData={txData}
|
||||
internalOp={op}
|
||||
ethUSDPrice={blockETHUSDPrice}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -309,28 +292,18 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<MethodName data={txData.data} />
|
||||
</InfoRow>
|
||||
)}
|
||||
{txData.tokenTransfers.length > 0 && (
|
||||
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}>
|
||||
{txData.tokenTransfers.map((t, i) => (
|
||||
<TokenTransferItem
|
||||
key={i}
|
||||
t={t}
|
||||
tokenMeta={txData.tokenMetas[t.token]}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
{tokenTransfers && tokenTransfers.length > 0 && (
|
||||
<InfoRow title={`Tokens Transferred (${tokenTransfers.length})`}>
|
||||
{tokenTransfers.map((t, i) => (
|
||||
<TokenTransferItem key={i} t={t} />
|
||||
))}
|
||||
</InfoRow>
|
||||
)}
|
||||
<InfoRow title="Value">
|
||||
<FormattedBalance value={txData.value} /> {symbol}{" "}
|
||||
{!txData.value.isZero() && blockETHUSDPrice && (
|
||||
<span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
|
||||
<ETH2USDValue
|
||||
ethAmount={txData.value}
|
||||
eth2USDValue={blockETHUSDPrice}
|
||||
<TransactionDetailsValue
|
||||
blockTag={txData.confirmedData?.blockNumber}
|
||||
value={txData.value}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</InfoRow>
|
||||
<InfoRow
|
||||
title={
|
||||
|
@ -369,7 +342,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei)
|
||||
</span>
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
|
@ -397,18 +370,10 @@ const Details: React.FC<DetailsProps> = ({
|
|||
</div>
|
||||
</InfoRow>
|
||||
)}
|
||||
{txData.confirmedData && hasEIP1559 && (
|
||||
{block && hasEIP1559 && (
|
||||
<InfoRow title="Block Base Fee">
|
||||
<FormattedBalance
|
||||
value={txData.confirmedData.blockBaseFeePerGas!}
|
||||
decimals={9}
|
||||
/>{" "}
|
||||
Gwei (
|
||||
<FormattedBalance
|
||||
value={txData.confirmedData.blockBaseFeePerGas!}
|
||||
decimals={0}
|
||||
/>{" "}
|
||||
wei)
|
||||
<FormattedBalance value={block.baseFeePerGas!} decimals={9} /> Gwei (
|
||||
<FormattedBalance value={block.baseFeePerGas!} decimals={0} /> wei)
|
||||
</InfoRow>
|
||||
)}
|
||||
{txData.confirmedData && (
|
||||
|
@ -416,15 +381,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<InfoRow title="Transaction Fee">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<FormattedBalance value={txData.confirmedData.fee} /> {symbol}{" "}
|
||||
{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>
|
||||
)}
|
||||
<TransactionFee confirmedData={txData.confirmedData} />
|
||||
</div>
|
||||
{hasEIP1559 && <RewardSplit txData={txData} />}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
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 TransactionAddress from "../components/TransactionAddress";
|
||||
import Copy from "../components/Copy";
|
||||
|
@ -8,16 +8,35 @@ import ModeTab from "../components/ModeTab";
|
|||
import DecodedParamsTable from "./decoder/DecodedParamsTable";
|
||||
import DecodedLogSignature from "./decoder/DecodedLogSignature";
|
||||
import { useTopic0 } from "../useTopic0";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useSourcifyMetadata } from "../sourcify/useSourcify";
|
||||
|
||||
type LogEntryProps = {
|
||||
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 topic0 = useTopic0(rawTopic0);
|
||||
|
||||
|
@ -47,7 +66,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
|
|||
return (
|
||||
<div className="flex space-x-10 py-5">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -56,10 +75,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
|
|||
<div className="font-bold text-right">Address</div>
|
||||
<div className="col-span-11 mr-auto">
|
||||
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
|
||||
<TransactionAddress
|
||||
address={log.address}
|
||||
metadata={metadatas[log.address]}
|
||||
/>
|
||||
<TransactionAddress address={log.address} />
|
||||
<Copy value={log.address} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,73 +1,20 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import { Interface } from "@ethersproject/abi";
|
||||
import React from "react";
|
||||
import ContentFrame from "../ContentFrame";
|
||||
import LogEntry from "./LogEntry";
|
||||
import { TransactionData } from "../types";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
|
||||
type LogsProps = {
|
||||
txData: TransactionData;
|
||||
metadata: Metadata | null | undefined;
|
||||
};
|
||||
|
||||
const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
|
||||
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 (
|
||||
const Logs: React.FC<LogsProps> = ({ txData }) => (
|
||||
<ContentFrame tabs>
|
||||
{txData.confirmedData && (
|
||||
<>
|
||||
{txData.confirmedData.logs.length > 0 ? (
|
||||
<>
|
||||
{txData.confirmedData.logs.map((l, i) => (
|
||||
<LogEntry
|
||||
key={i}
|
||||
log={l}
|
||||
logDesc={logDescs?.[i]}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
<LogEntry key={i} log={l} />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
|
@ -76,7 +23,6 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
|
|||
</>
|
||||
)}
|
||||
</ContentFrame>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
export default React.memo(Logs);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import React, { PropsWithChildren, useContext, useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useTransactionBySenderAndNonce } from "../useErigonHooks";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
import { addressByNonceURL } from "../url";
|
||||
|
||||
|
@ -9,28 +12,53 @@ type NavButtonProps = {
|
|||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const NavButton: React.FC<NavButtonProps> = ({
|
||||
const NavButton: React.FC<PropsWithChildren<NavButtonProps>> = ({
|
||||
sender,
|
||||
nonce,
|
||||
disabled,
|
||||
children,
|
||||
}) => {
|
||||
const [prefetch, setPrefetch] = useState<boolean>(false);
|
||||
|
||||
if (disabled) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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)}
|
||||
onMouseOver={() => setPrefetch(true)}
|
||||
>
|
||||
{children}
|
||||
</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;
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import React, { useContext, useEffect } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
|
||||
import NavButton from "./NavButton";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import {
|
||||
prefetchTransactionBySenderAndNonce,
|
||||
useTransactionCount,
|
||||
} from "../useErigonHooks";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useTransactionCount } from "../useErigonHooks";
|
||||
|
||||
type NavNonceProps = {
|
||||
sender: ChecksummedAddress;
|
||||
|
@ -20,25 +16,6 @@ const NavNonce: React.FC<NavNonceProps> = ({ sender, nonce }) => {
|
|||
const { provider } = useContext(RuntimeContext);
|
||||
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 (
|
||||
<div className="pl-2 self-center flex space-x-1">
|
||||
<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 { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
|
||||
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
|
||||
import FormattedBalance from "../components/FormattedBalance";
|
||||
import PercentageGauge from "../components/PercentageGauge";
|
||||
import { TransactionData } from "../types";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useBlockDataFromTransaction } from "../useErigonHooks";
|
||||
import { useChainInfo } from "../useChainInfo";
|
||||
import { TransactionData } from "../types";
|
||||
|
||||
type RewardSplitProps = {
|
||||
txData: TransactionData;
|
||||
};
|
||||
|
||||
const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const block = useBlockDataFromTransaction(provider, txData);
|
||||
|
||||
const {
|
||||
nativeCurrency: { symbol },
|
||||
} = useChainInfo();
|
||||
const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed);
|
||||
const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul(
|
||||
txData.confirmedData!.gasUsed
|
||||
);
|
||||
const burntFees = block
|
||||
? block.baseFeePerGas!.mul(txData.confirmedData!.gasUsed)
|
||||
: BigNumber.from(0);
|
||||
|
||||
const minerReward = paidFees.sub(burntFees);
|
||||
const burntPerc =
|
||||
|
@ -49,13 +55,13 @@ const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
|
|||
</div>
|
||||
<PercentageGauge
|
||||
perc={minerPerc}
|
||||
bgColor="bg-yellow-100"
|
||||
bgColorPerc="bg-yellow-300"
|
||||
textColor="text-yellow-700"
|
||||
bgColor="bg-amber-100"
|
||||
bgColorPerc="bg-amber-300"
|
||||
textColor="text-amber-700"
|
||||
/>
|
||||
<div className="flex items-baseline 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" />
|
||||
</span>
|
||||
<span>
|
||||
|
|
|
@ -17,13 +17,13 @@ const TraceItem: React.FC<TraceItemProps> = ({ t, last }) => {
|
|||
return (
|
||||
<>
|
||||
<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 && (
|
||||
<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 && (
|
||||
<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}
|
||||
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 }) => (
|
||||
<span className={`${r ? "text-green-700" : "text-red-700"}`}>
|
||||
<span className={`${r ? "text-emerald-700" : "text-red-700"}`}>
|
||||
{r.toString()}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
|
|||
<th className="col-span-8 pr-1">value</th>
|
||||
</tr>
|
||||
{!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">
|
||||
{paramTypes.length > 0 && paramTypes[0].name !== null
|
||||
? "Parameter names are estimated."
|
||||
|
|
|
@ -18,7 +18,6 @@ export type ProcessedTransaction = {
|
|||
from?: string;
|
||||
to: string | null;
|
||||
createdContractAddress?: string;
|
||||
internalMinerInteraction?: boolean;
|
||||
value: BigNumber;
|
||||
fee: BigNumber;
|
||||
gasPrice: BigNumber;
|
||||
|
@ -37,8 +36,6 @@ export type TransactionData = {
|
|||
from: string;
|
||||
to?: string;
|
||||
value: BigNumber;
|
||||
tokenTransfers: TokenTransfer[];
|
||||
tokenMetas: TokenMetas;
|
||||
type: number;
|
||||
maxFeePerGas?: BigNumber | undefined;
|
||||
maxPriorityFeePerGas?: BigNumber | undefined;
|
||||
|
@ -53,11 +50,7 @@ export type ConfirmedTransactionData = {
|
|||
status: boolean;
|
||||
blockNumber: number;
|
||||
transactionIndex: number;
|
||||
blockBaseFeePerGas?: BigNumber | undefined | null;
|
||||
blockTransactionCount: number;
|
||||
confirmations: number;
|
||||
timestamp: number;
|
||||
miner: string;
|
||||
createdContractAddress?: string;
|
||||
fee: 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) =>
|
||||
`/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) =>
|
||||
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
TransactionDescription,
|
||||
} from "@ethersproject/abi";
|
||||
import { BigNumberish } from "@ethersproject/bignumber";
|
||||
import { Fetcher } from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { fourBytesURL } from "./url";
|
||||
|
@ -29,16 +30,33 @@ export const extract4Bytes = (rawInput: string): string | null => {
|
|||
return rawInput.slice(0, 10);
|
||||
};
|
||||
|
||||
const fetch4Bytes = async (
|
||||
assetsURLPrefix: string,
|
||||
fourBytes: string
|
||||
): Promise<FourBytesEntry | null> => {
|
||||
type FourBytesKey = [id: "4bytes", fourBytes: string];
|
||||
type FourBytesFetcher = Fetcher<
|
||||
FourBytesEntry | null | undefined,
|
||||
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);
|
||||
|
||||
try {
|
||||
const res = await fetch(signatureURL);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -54,10 +72,11 @@ const fetch4Bytes = async (
|
|||
};
|
||||
return entry;
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract 4bytes DB info
|
||||
|
@ -75,26 +94,10 @@ export const use4Bytes = (
|
|||
|
||||
const { config } = useContext(RuntimeContext);
|
||||
const assetsURLPrefix = config?.assetsURLPrefix;
|
||||
const fourBytesKey = assetsURLPrefix !== undefined ? rawFourBytes : null;
|
||||
|
||||
const fourBytesFetcher = (key: string | null) => {
|
||||
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;
|
||||
}
|
||||
|
||||
return fetch4Bytes(assetsURLPrefix!, key.slice(2));
|
||||
};
|
||||
|
||||
const { data, error } = useSWRImmutable<FourBytesEntry | null | undefined>(
|
||||
assetsURLPrefix !== undefined ? rawFourBytes : null,
|
||||
fourBytesFetcher
|
||||
);
|
||||
const fetcher = fourBytesFetcher(assetsURLPrefix!);
|
||||
const { data, error } = useSWRImmutable(["4bytes", fourBytesKey], fetcher);
|
||||
return error ? undefined : data;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { SourcifySource } from "./url";
|
||||
import { SourcifySource } from "./sourcify/useSourcify";
|
||||
|
||||
export type AppConfig = {
|
||||
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 { OtterscanRuntime } from "./useRuntime";
|
||||
|
||||
|
@ -24,40 +25,33 @@ export const defaultChainInfo: ChainInfo = {
|
|||
|
||||
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 = (
|
||||
runtime: OtterscanRuntime | undefined
|
||||
): ChainInfo | undefined => {
|
||||
const assetsURLPrefix = runtime?.config?.assetsURLPrefix;
|
||||
const chainId = runtime?.provider?.network.chainId;
|
||||
|
||||
const [chainInfo, setChainInfo] = useState<ChainInfo | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (assetsURLPrefix === undefined || chainId === undefined) {
|
||||
setChainInfo(undefined);
|
||||
return;
|
||||
const { data, error } = useSWRImmutable(
|
||||
assetsURLPrefix !== undefined && chainId !== undefined
|
||||
? [assetsURLPrefix, chainId]
|
||||
: null,
|
||||
chainInfoFetcher
|
||||
);
|
||||
if (error) {
|
||||
return defaultChainInfo;
|
||||
}
|
||||
|
||||
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;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useChainInfo = (): ChainInfo => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
|||
|
||||
export type OtterscanConfig = {
|
||||
erigonURL?: string;
|
||||
beaconAPI?: string;
|
||||
assetsURLPrefix?: string;
|
||||
};
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue