Merge branch 'release/v2022.08.03-otterscan'

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

View File

@ -20,12 +20,12 @@ jobs:
steps:
- 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

2
.gitignore vendored
View File

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

2
.nvmrc
View File

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

2
4bytes

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

View File

@ -1,8 +1,8 @@
FROM node:16.14.0-alpine3.15 AS builder
FROM node:16.16.0-alpine3.15 AS builder
WORKDIR /otterscan-build
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

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

View File

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

View File

@ -6,43 +6,41 @@ It depends heavily on a working Erigon installation with Otterscan patches appli
## Install Erigon
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.

View File

@ -16,7 +16,7 @@ Clone Otterscan repo and its submodules. Checkout the tag corresponding to your
git clone --recurse-submodules https://github.com/wmitsuda/otterscan.git
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.

25
index.html Normal file
View File

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

31791
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

7
postcss.config.js Normal file
View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
#!/bin/sh
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;"

View File

@ -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;

170
src/AddressMainPage.tsx Normal file
View File

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

View File

@ -1,24 +1,47 @@
import React, { useContext, useEffect, useState } from "react";
import { 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 (

View File

@ -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();

View File

@ -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} />

47
src/BlockReward.tsx Normal file
View File

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

View File

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

View File

@ -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,28 +90,30 @@ type StepProps = {
msg: string;
};
const Step: React.FC<StepProps> = React.memo(({ type, msg, children }) => (
<>
<div className="flex space-x-2">
{type === "wait" && (
<span className="text-gray-600">
<FontAwesomeIcon icon={faClock} size="1x" />
</span>
)}
{type === "ok" && (
<span className="text-green-600">
<FontAwesomeIcon icon={faCheckCircle} size="1x" />
</span>
)}
{type === "error" && (
<span className="text-red-600">
<FontAwesomeIcon icon={faTimesCircle} size="1x" />
</span>
)}
<span>{msg}</span>
</div>
{children && <div className="ml-7 mt-4 text-sm">{children}</div>}
</>
));
const Step: React.FC<PropsWithChildren<StepProps>> = React.memo(
({ type, msg, children }) => (
<>
<div className="flex space-x-2">
{type === "wait" && (
<span className="text-gray-600">
<FontAwesomeIcon icon={faClock} size="1x" />
</span>
)}
{type === "ok" && (
<span className="text-emerald-600">
<FontAwesomeIcon icon={faCheckCircle} size="1x" />
</span>
)}
{type === "error" && (
<span className="text-red-600">
<FontAwesomeIcon icon={faTimesCircle} size="1x" />
</span>
)}
<span>{msg}</span>
</div>
{children && <div className="ml-7 mt-4 text-sm">{children}</div>}
</>
)
);
export default React.memo(ConnectionErrorPanel);

View File

@ -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>
) : (

View File

@ -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}

View File

@ -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,32 +65,44 @@ 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">
<div className="flex space-x-2 items-baseline text-orange-500 hover:text-orange-700 hover:underline">
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
<span>Check out the special dashboard for EIP-1559</span>
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
</div>
</NavLink>
)}
</div>
{latestBlock && (
<NavLink
className="flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue"
to={blockURL(latestBlock.number)}
>
<div>Latest block: {commify(latestBlock.number)}</div>
<Timestamp value={latestBlock.timestamp} />
<div className="text-lg text-link-blue hover:text-link-blue-hover font-bold">
{provider?.network.chainId !== 11155111 && (
<NavLink to="/special/london">
<div className="flex space-x-2 items-baseline text-orange-500 hover:text-orange-700 hover:underline">
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
<span>Check out the special dashboard for EIP-1559</span>
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
</div>
</NavLink>
)}
</div>
{latestBlock && (
<NavLink
className="flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue"
to={blockURL(latestBlock.number)}
>
<div>Latest block: {commify(latestBlock.number)}</div>
<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>
);
};

View File

@ -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}

View File

@ -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>(

View File

@ -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];

View File

@ -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,

View File

@ -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;

View File

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

View File

@ -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

View File

@ -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;

View File

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

View File

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

View File

@ -1,9 +1,7 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import { 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} />

View File

@ -1,40 +1,19 @@
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 (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
style={docco}
showLineNumbers
>
{content ?? ""}
</SyntaxHighlighter>
);
};
const Contract: React.FC<ContractProps> = ({ content }) => (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
style={docco}
showLineNumbers
>
{content ?? ""}
</SyntaxHighlighter>
);
export default React.memo(Contract);

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext, Fragment } from "react";
import React, { useState, useEffect, useContext } from "react";
import { commify } from "@ethersproject/units";
import { 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,12 +114,20 @@ const Contracts: React.FC<ContractsProps> = ({
</div>
</Menu>
{selected && (
<Contract
checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId}
filename={selected}
source={rawMetadata.sources[selected]}
/>
<>
{match.metadata.sources[selected].content ? (
<Contract
content={match.metadata.sources[selected].content}
/>
) : (
<ContractFromRepo
checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId}
filename={selected}
type={match.type}
/>
)}
</>
)}
</div>
</>

View File

@ -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>

View File

@ -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()) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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}

View File

@ -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>
);

View File

@ -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}

View File

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

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { PropsWithChildren } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { 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}

View File

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

View File

@ -1,10 +1,12 @@
import React from "react";
import React, { useContext } from "react";
import { formatEther } from "@ethersproject/units";
import { 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} />

View File

@ -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} />

View File

@ -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}
/>

View File

@ -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}>

View File

@ -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 ${

View File

@ -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 }) =>

View File

@ -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>
);

View File

@ -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}%` }}

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

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

View File

@ -11,6 +11,7 @@ export enum Direction {
}
export enum Flags {
// 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 (

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

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

View File

@ -1,5 +1,5 @@
import React from "react";
import 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

View File

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

View File

@ -1,8 +1,8 @@
import React from "react";
import { 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";

View File

@ -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}

View File

@ -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>

View File

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

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { PropsWithChildren } from "react";
import { NavLink } from "react-router-dom";
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}` : ""
}`}

View File

@ -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`;
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;
};
const sourcifyFetcher = async (
_: "sourcify",
address: ChecksummedAddress,
chainId: number,
sourcifySource: SourcifySource
): Promise<Match | null | undefined> => {
// Try full match
try {
const url = sourcifyMetadata(
address,
chainId,
sourcifySource,
MatchType.FULL_MATCH
);
const res = await fetch(url);
if (res.ok) {
return {
type: MatchType.FULL_MATCH,
metadata: await res.json(),
};
}
} catch (err) {
console.info(
`error while getting Sourcify full_match metadata: chainId=${chainId} address=${address} err=${err}; falling back to partial_match`
);
}
// 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(),
};
}
return null;
} catch (err) {
console.error(err);
console.warn(
`error while getting Sourcify partial_match metadata: chainId=${chainId} address=${address} err=${err}`
);
return null;
}
};
// TODO: replace every occurrence with the multiple version one
export const useSourcify = (
export const useSourcifyMetadata = (
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(
address,
chainId,
source,
abortController
);
setRawMetadata(_metadata);
};
fetchMetadata();
return () => {
abortController.abort();
};
}, [address, chainId, source]);
return rawMetadata;
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;
};
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)
);
}
const results = await Promise.all(fetchers);
if (abortController.signal.aborted) {
return;
}
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;
const contractFetcher = async (url: string): Promise<string | null> => {
const res = await fetch(url);
if (res.ok) {
return await res.text();
}
return null;
};
export const useContract = (
checksummedAddress: string,
networkId: number,
filename: string,
source: any,
sourcifySource: SourcifySource
sourcifySource: SourcifySource,
type: MatchType
) => {
const [content, setContent] = useState<string>(source.content);
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
const url = sourcifySourceFile(
checksummedAddress,
networkId,
normalizedFilename,
sourcifySource,
type
);
useEffect(() => {
if (source.content) {
return;
}
const abortController = new AbortController();
const readContent = async () => {
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
const url = sourcifySourceFile(
checksummedAddress,
networkId,
normalizedFilename,
sourcifySource
);
const res = await fetch(url, { signal: abortController.signal });
if (res.ok) {
const _content = await res.text();
setContent(_content);
}
};
readContent();
return () => {
abortController.abort();
};
}, [checksummedAddress, networkId, filename, source.content, sourcifySource]);
return content;
const { data, error } = useSWRImmutable(url, contractFetcher);
if (error) {
return undefined;
}
return data;
};
export const useTransactionDescription = (

View File

@ -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}`}

View File

@ -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"
: ""

View File

@ -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"
>

View File

@ -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

View File

@ -1,6 +1,5 @@
import React, { useContext, useMemo, useState } from "react";
import React, { useContext, useState } from "react";
import { Tab } from "@headlessui/react";
import { 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>
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.confirmedData.transactionIndex}
total={txData.confirmedData.blockTransactionCount - 1}
/>
<PercentagePosition
perc={
txData.confirmedData.transactionIndex /
(txData.confirmedData.blockTransactionCount - 1)
}
/>
</div>
{block && (
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.confirmedData.transactionIndex}
total={block.transactionCount - 1}
/>
<PercentagePosition
perc={
txData.confirmedData.transactionIndex /
(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}
/>
</span>
)}
<TransactionDetailsValue
blockTag={txData.confirmedData?.blockNumber}
value={txData.value}
/>
</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>

View File

@ -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>

View File

@ -1,82 +1,28 @@
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 (
<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}
/>
))}
</>
) : (
<div className="text-sm py-4">Transaction didn't emit any logs</div>
)}
</>
)}
</ContentFrame>
);
};
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} />
))}
</>
) : (
<div className="text-sm py-4">Transaction didn't emit any logs</div>
)}
</>
)}
</ContentFrame>
);
export default React.memo(Logs);

View File

@ -1,4 +1,7 @@
import React, { PropsWithChildren, useContext, useState } from "react";
import { NavLink } from "react-router-dom";
import { 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"
to={addressByNonceURL(sender, nonce)}
>
{children}
</NavLink>
<>
<NavLink
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;

View File

@ -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}>

View File

@ -1,24 +1,30 @@
import React from "react";
import React, { useContext } from "react";
import { BigNumber } from "@ethersproject/bignumber";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { 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>

View File

@ -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}
>

View File

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

View File

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

View File

@ -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."

View File

@ -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;

View File

@ -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}`;

View File

@ -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,35 +30,53 @@ export const extract4Bytes = (rawInput: string): string | null => {
return rawInput.slice(0, 10);
};
const fetch4Bytes = async (
assetsURLPrefix: string,
fourBytes: string
): Promise<FourBytesEntry | null> => {
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
type FourBytesKey = [id: "4bytes", fourBytes: string];
type FourBytesFetcher = Fetcher<
FourBytesEntry | null | undefined,
FourBytesKey
>;
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`);
return null;
const fourBytesFetcher =
(assetsURLPrefix: string): FourBytesFetcher =>
async (_, key) => {
if (key === null || key === "0x") {
return undefined;
}
// Get only the first occurrence, for now ignore alternative param names
const sigs = await res.text();
const sig = sigs.split(";")[0];
const cut = sig.indexOf("(");
const method = sig.slice(0, cut);
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
const entry: FourBytesEntry = {
name: method,
signature: sig,
};
return entry;
} catch (err) {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
return null;
}
};
const fourBytes = key.slice(2);
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`);
return null;
}
// Get only the first occurrence, for now ignore alternative param names
const sigs = await res.text();
const sig = sigs.split(";")[0];
const cut = sig.indexOf("(");
const method = sig.slice(0, cut);
const entry: FourBytesEntry = {
name: method,
signature: sig,
};
return entry;
} catch (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;
};

View File

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

66
src/useBeacon.ts Normal file
View File

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

View File

@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from "react";
import { createContext, useContext } from "react";
import useSWRImmutable from "swr/immutable";
import { chainInfoURL } from "./url";
import { 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 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;
const { data, error } = useSWRImmutable(
assetsURLPrefix !== undefined && chainId !== undefined
? [assetsURLPrefix, chainId]
: null,
chainInfoFetcher
);
if (error) {
return defaultChainInfo;
}
return data;
};
export const useChainInfo = (): ChainInfo => {

View File

@ -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