Merge branch 'release/v2021.07.05-2-otterscan'

This commit is contained in:
Willian Mitsuda 2021-08-05 00:02:30 -03:00
commit 5a2fabf14f
18 changed files with 689 additions and 326 deletions

View File

@ -86,6 +86,7 @@ We intend to release a compatible rebased version containing our changes every w
```
git fetch --all
git fetch otterscan --tags
git checkout <version-tag-otterscan>
```
@ -161,6 +162,12 @@ Erigon itself is alpha, so I consider this software is also in alpha state, howe
Also there is room for many improvements that are not possible in the current centralized, closed source block explorer offerings and the author of this software would like to have.
## Licensing
This software itself is MIT licensed and redistributes MIT-compatible dependencies.
The Otterscan patches for Erigon are LGPL-3 as required by Geth (since Erigon is a Geth derivative) and kept in a separate repository.
## Getting in touch
### Erigon Discord server

118
package-lock.json generated
View File

@ -18,14 +18,14 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24",
"@types/node": "^14.17.5",
"@types/react": "^17.0.14",
"@types/react": "^17.0.15",
"@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
@ -1354,9 +1354,9 @@
}
},
"node_modules/@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz",
"integrity": "sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA==",
"funding": [
{
"type": "individual",
@ -1435,9 +1435,9 @@
}
},
"node_modules/@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.1.tgz",
"integrity": "sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==",
"funding": [
{
"type": "individual",
@ -2069,9 +2069,9 @@
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz",
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==",
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"dependencies": {
"prop-types": "^15.7.2"
},
@ -3062,9 +3062,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -8037,6 +8037,48 @@
"@ethersproject/wordlists": "5.4.0"
}
},
"node_modules/ethers/node_modules/@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"dependencies": {
"@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"@ethersproject/properties": "^5.4.0"
}
},
"node_modules/ethers/node_modules/@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"dependencies": {
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"bn.js": "^4.11.9"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"license": "MIT"
@ -20042,9 +20084,9 @@
}
},
"@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz",
"integrity": "sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA==",
"requires": {
"@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0",
@ -20083,9 +20125,9 @@
}
},
"@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.1.tgz",
"integrity": "sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==",
"requires": {
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
@ -20445,9 +20487,9 @@
}
},
"@fortawesome/react-fontawesome": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz",
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==",
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"requires": {
"prop-types": "^15.7.2"
}
@ -21087,9 +21129,9 @@
"version": "1.5.4"
},
"@types/react": {
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -24471,6 +24513,30 @@
"@ethersproject/wallet": "5.4.0",
"@ethersproject/web": "5.4.0",
"@ethersproject/wordlists": "5.4.0"
},
"dependencies": {
"@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"requires": {
"@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"@ethersproject/properties": "^5.4.0"
}
},
"@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"requires": {
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"bn.js": "^4.11.9"
}
}
}
},
"eventemitter3": {

View File

@ -14,14 +14,14 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24",
"@types/node": "^14.17.5",
"@types/react": "^17.0.14",
"@types/react": "^17.0.15",
"@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",

View File

@ -83,7 +83,7 @@ const Block: React.FC = () => {
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"
to={blockTxsURL(block.number)}
>
{block.transactions.length} transactions
{block.transactionCount} transactions
</NavLink>{" "}
in this block
</InfoRow>

View File

@ -1,17 +1,13 @@
import React, { useEffect, useState, useMemo, useContext } from "react";
import React, { useMemo, useContext } from "react";
import { useParams, useLocation } from "react-router";
import { ethers } from "ethers";
import queryString from "query-string";
import StandardFrame from "./StandardFrame";
import BlockTransactionHeader from "./block/BlockTransactionHeader";
import BlockTransactionResults from "./block/BlockTransactionResults";
import {
InternalOperation,
OperationType,
ProcessedTransaction,
} from "./types";
import { PAGE_SIZE } from "./params";
import { RuntimeContext } from "./useRuntime";
import { useBlockTransactions } from "./useErigonHooks";
type BlockParams = {
blockNumber: string;
@ -38,87 +34,12 @@ const BlockTransactions: React.FC = () => {
[params.blockNumber]
);
const [txs, setTxs] = useState<ProcessedTransaction[]>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const [_block, _receipts] = await Promise.all([
provider.getBlockWithTransactions(blockNumber.toNumber()),
provider.send("eth_getBlockReceipts", [blockNumber.toNumber()]),
]);
document.title = `Block #${_block.number} Transactions | Otterscan`;
const responses = _block.transactions
.map((t, i): ProcessedTransaction => {
return {
blockNumber: blockNumber.toNumber(),
timestamp: _block.timestamp,
miner: _block.miner,
idx: i,
hash: t.hash,
from: t.from,
to: t.to,
createdContractAddress: _receipts[i].contractAddress,
value: t.value,
fee:
t.type !== 2
? provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.gasPrice!)
: provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!)),
gasPrice:
t.type !== 2
? t.gasPrice!
: t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!),
data: t.data,
status: provider.formatter.number(_receipts[i].status),
};
})
.reverse();
setTxs(responses);
const internalChecks = await Promise.all(
responses.map(async (res) => {
const r: InternalOperation[] = await provider.send(
"ots_getInternalOperations",
[res.hash]
);
for (const op of r) {
if (op.type !== OperationType.TRANSFER) {
continue;
}
if (
res.miner &&
(res.miner === ethers.utils.getAddress(op.from) ||
res.miner === ethers.utils.getAddress(op.to))
) {
return true;
}
}
return false;
})
);
const processedResponses = responses.map((r, i): ProcessedTransaction => {
return { ...r, internalMinerInteraction: internalChecks[i] };
});
setTxs(processedResponses);
};
readBlock();
}, [provider, blockNumber]);
const page = useMemo(() => {
if (!txs) {
return undefined;
}
const pageStart = (pageNumber - 1) * PAGE_SIZE;
return txs.slice(pageStart, pageStart + PAGE_SIZE);
}, [txs, pageNumber]);
const total = useMemo(() => txs?.length ?? 0, [txs]);
const [totalTxs, txs] = useBlockTransactions(
provider,
blockNumber.toNumber(),
pageNumber - 1,
PAGE_SIZE
);
document.title = `Block #${blockNumber} Txns | Otterscan`;
@ -126,8 +47,8 @@ const BlockTransactions: React.FC = () => {
<StandardFrame>
<BlockTransactionHeader blockTag={blockNumber.toNumber()} />
<BlockTransactionResults
page={page}
total={total}
page={txs}
total={totalTxs ?? 0}
pageNumber={pageNumber}
/>
</StandardFrame>

View File

@ -1,19 +1,13 @@
import React, { useState, useEffect, useMemo, useContext } from "react";
import React, { useMemo, useContext } from "react";
import { Route, Switch, useParams } from "react-router-dom";
import { BigNumber, ethers } from "ethers";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import Tab from "./components/Tab";
import Details from "./transaction/Details";
import Logs from "./transaction/Logs";
import erc20 from "./erc20.json";
import { TokenMetas, TokenTransfer, TransactionData } from "./types";
import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations } from "./useErigonHooks";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
import { useInternalOperations, useTxData } from "./useErigonHooks";
type TransactionParams = {
txhash: string;
@ -24,89 +18,7 @@ const Transaction: React.FC = () => {
const params = useParams<TransactionParams>();
const { txhash } = params;
const [txData, setTxData] = useState<TransactionData>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const [_response, _receipt] = await Promise.all([
provider.getTransaction(txhash),
provider.getTransactionReceipt(txhash),
]);
const _block = await provider.getBlock(_receipt.blockNumber);
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: ethers.utils.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[1]), 12)
),
to: ethers.utils.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[2]), 12)
),
value: BigNumber.from(l.data),
});
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token]) {
continue;
}
const erc20Contract = new ethers.Contract(t.token, erc20, provider);
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
}
setTxData({
transactionHash: _receipt.transactionHash,
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
confirmations: _receipt.confirmations,
timestamp: _block.timestamp,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
logs: _receipt.logs,
});
};
readBlock();
}, [provider, txhash]);
const txData = useTxData(provider, txhash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {

21
src/components/Nonce.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons";
type NonceProps = {
value: number;
};
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"
title="Nonce"
>
<span className="text-green-400">
<FontAwesomeIcon icon={faArrowUp} size="1x" />
</span>
<span className="text-green-600">{value}</span>
</span>
);
export default React.memo(Nonce);

View File

@ -0,0 +1,23 @@
import React from "react";
type PercentagePositionProps = {
perc: number;
};
const PercentagePosition: React.FC<PercentagePositionProps> = ({ perc }) => (
<div className="self-center w-40">
<div className="w-full h-5 relative rounded border border-orange-200">
<div className="absolute w-full h-1/2 border-b"></div>
<div className="absolute top-1/4 w-full h-1/2 border-l-2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-1/2 h-1/2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-1/4 h-1/2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-3/4 h-1/2 border-r-2 border-gray-300"></div>
<div
className="absolute h-full border-r-4 border-black"
style={{ width: `${perc * 100}%` }}
></div>
</div>
</div>
);
export default React.memo(PercentagePosition);

View File

@ -0,0 +1,15 @@
import React from "react";
type RelativePositionProps = {
pos: React.ReactNode;
total: React.ReactNode;
};
const RelativePosition: React.FC<RelativePositionProps> = ({ pos, total }) => (
<span className="text-xs">
{pos}
<span className="text-gray-600 text-sm"> / {total}</span>
</span>
);
export default React.memo(RelativePosition);

View File

@ -1,12 +1,12 @@
import { ethers } from "ethers";
import { TransactionData, InternalOperation } from "./types";
import { InternalOperation } from "./types";
export const getInternalOperations = async (
provider: ethers.providers.JsonRpcProvider,
txData: TransactionData
txHash: string
) => {
const rawTransfers = await provider.send("ots_getInternalOperations", [
txData.transactionHash,
txHash,
]);
const _transfers: InternalOperation[] = [];

View File

@ -0,0 +1,33 @@
import React, { useState } from "react";
import { Transition } from "@headlessui/react";
type BlipProps = {
value: number;
};
const Blip: React.FC<BlipProps> = ({ value }) => {
const [show, setShow] = useState<boolean>(true);
return (
<Transition
show
appear
enter="transition transform ease-in duration-1000 translate-x-full pl-3"
enterFrom="opacity-100 translate-y-0"
enterTo="opacity-0 -translate-y-5"
afterEnter={() => setShow(false)}
>
{show && value !== 0 && (
<div
className={`absolute bottom-0 font-bold ${
value > 0 ? "text-green-500" : "text-red-500"
} text-3xl`}
>
{value > 0 ? `+${value}` : `-${value}`}
</div>
)}
</Transition>
);
};
export default React.memo(Blip);

View File

@ -1,17 +1,19 @@
import { ethers } from "ethers";
import React from "react";
import { ethers } from "ethers";
import BlockLink from "../../components/BlockLink";
import TimestampAge from "../../components/TimestampAge";
import { ExtendedBlock } from "../../useErigonHooks";
import Blip from "./Blip";
const ELASTICITY_MULTIPLIER = 2;
type BlockRowProps = {
now: number;
block: ExtendedBlock;
baseFeeDelta: number;
};
const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => {
const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
const gasTarget = block.gasLimit.div(ELASTICITY_MULTIPLIER);
const burntFees =
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
@ -19,7 +21,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => {
const totalReward = block.blockReward.add(netFeeReward ?? 0);
return (
<div className="grid grid-cols-8 px-3 py-2 hover:bg-gray-100">
<div className="grid grid-cols-8 gap-x-2 px-3 py-2 hover:bg-gray-100">
<div>
<BlockLink blockTag={block.number} />
</div>
@ -37,7 +39,12 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => {
<div className="text-right text-gray-400">
{ethers.utils.commify(gasTarget.toString())}
</div>
<div className="text-right">{block.baseFeePerGas?.toString()} wei</div>
<div className="text-right">
<div className="relative">
<span>{block.baseFeePerGas?.toString()} wei</span>
<Blip value={baseFeeDelta} />
</div>
</div>
<div className="text-right col-span-2">
{ethers.utils.commify(ethers.utils.formatEther(totalReward))} Ether
</div>

View File

@ -7,7 +7,6 @@ import React, {
} from "react";
import { ethers } from "ethers";
import { Line } from "react-chartjs-2";
import { ChartData, ChartOptions } from "chart.js";
import { Transition } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@ -20,40 +19,17 @@ import {
import BlockRow from "./BlockRow";
import { ExtendedBlock, readBlock } from "../../useErigonHooks";
import { RuntimeContext } from "../../useRuntime";
import {
burntFeesChartOptions,
burntFeesChartData,
gasChartOptions,
gasChartData,
} from "./chart";
const MAX_BLOCK_HISTORY = 20;
const PREV_BLOCK_COUNT = 15;
const options: ChartOptions = {
animation: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Burnt fees",
},
ticks: {
callback: (v) => `${v} Gwei`,
},
},
},
};
type BlocksProps = {
latestBlock: ethers.providers.Block;
targetBlockNumber: number;
@ -61,8 +37,9 @@ type BlocksProps = {
const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
const { provider } = useContext(RuntimeContext);
const [blocks, setBlock] = useState<ExtendedBlock[]>([]);
const [blocks, setBlocks] = useState<ExtendedBlock[]>([]);
const [now, setNow] = useState<number>(Date.now());
const [toggleChart, setToggleChart] = useState<boolean>(true);
const addBlock = useCallback(
async (blockNumber: number) => {
@ -77,7 +54,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
const extBlock = await readBlock(provider, blockNumber.toString());
setNow(Date.now());
setBlock((_blocks) => {
setBlocks((_blocks) => {
if (_blocks.length > 0 && blockNumber === _blocks[0].number) {
return _blocks;
}
@ -100,23 +77,11 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
addBlock(latestBlock.number);
}, [addBlock, latestBlock]);
const data: ChartData = useMemo(() => {
return {
labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [
{
label: "Burnt fees (Gwei)",
data: blocks
.map((b) => b.gasUsed.mul(b.baseFeePerGas!).toNumber() / 1e9)
.reverse(),
fill: true,
backgroundColor: "#FDBA74",
borderColor: "#F97316",
tension: 0.2,
},
],
};
}, [blocks]);
const data = useMemo(
() => (toggleChart ? gasChartData(blocks) : burntFeesChartData(blocks)),
[toggleChart, blocks]
);
const chartOptions = toggleChart ? gasChartOptions : burntFeesChartOptions;
// On page reload, pre-populate the last N blocks
useEffect(
@ -130,6 +95,8 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
await addBlock(i);
}
};
setBlocks([]);
addPreviousBlocks();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -139,19 +106,26 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
return (
<div className="w-full mb-auto">
<div className="px-9 pt-3 pb-12 divide-y-2">
<div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 ">
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
<span>EIP-1559 is activated. Watch the fees burn.</span>
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
<div className="relative">
<div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 ">
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
<span>EIP-1559 is activated. Watch the fees burn.</span>
<span>
<FontAwesomeIcon icon={faBurn} />
</span>
</div>
<div className="absolute right-0 top-0 border rounded shadow-md px-2 py-1 text-sm text-link-blue hover:bg-gray-50 hover:text-link-blue-hover">
<button onClick={() => setToggleChart(!toggleChart)}>
{toggleChart ? "Gas usage" : "Burnt fees"}
</button>
</div>
</div>
<div>
<Line data={data} height={100} options={options} />
<Line data={data} height={100} options={chartOptions} />
</div>
<div className="mt-5 grid grid-cols-8 px-3 py-2">
<div className="mt-5 grid grid-cols-8 gap-x-2 px-3 py-2">
<div className="flex space-x-1 items-baseline">
<span className="text-gray-500">
<FontAwesomeIcon icon={faCube} />
@ -185,7 +159,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
<span>Age</span>
</div>
</div>
{blocks.map((b, i) => (
{blocks.map((b, i, all) => (
<Transition
key={b.hash}
show={i < MAX_BLOCK_HISTORY}
@ -197,7 +171,15 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-10"
>
<BlockRow now={now} block={b} />
<BlockRow
now={now}
block={b}
baseFeeDelta={
i < all.length - 1
? b.baseFeePerGas!.sub(all[i + 1].baseFeePerGas!).toNumber()
: 0
}
/>
</Transition>
))}
</div>

161
src/special/london/chart.ts Normal file
View File

@ -0,0 +1,161 @@
import { ethers } from "ethers";
import { ChartData, ChartOptions } from "chart.js";
import { ExtendedBlock } from "../../useErigonHooks";
export const burntFeesChartOptions: ChartOptions = {
animation: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Burnt fees",
},
ticks: {
callback: (v) => `${v} Gwei`,
},
},
yBaseFee: {
position: "right",
beginAtZero: true,
title: {
display: true,
text: "Base fee",
},
ticks: {
callback: (v) => `${v} wei`,
},
grid: {
drawOnChartArea: false,
},
},
},
};
export const burntFeesChartData = (blocks: ExtendedBlock[]): ChartData => ({
labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [
{
label: "Burnt fees (Gwei)",
data: blocks
.map((b) => b.gasUsed.mul(b.baseFeePerGas!).toNumber() / 1e9)
.reverse(),
fill: true,
backgroundColor: "#FDBA7470",
borderColor: "#FB923C",
tension: 0.2,
},
{
label: "Base fee (wei)",
data: blocks.map((b) => b.baseFeePerGas!.toNumber()).reverse(),
yAxisID: "yBaseFee",
borderColor: "#38BDF8",
tension: 0.2,
},
],
});
export const gasChartOptions: ChartOptions = {
animation: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Gas",
},
},
yBaseFee: {
position: "right",
beginAtZero: true,
title: {
display: true,
text: "Base fee",
},
ticks: {
callback: (v) => `${v} wei`,
},
grid: {
drawOnChartArea: false,
},
},
},
};
export const gasChartData = (blocks: ExtendedBlock[]): ChartData => ({
labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [
{
label: "Gas used",
data: blocks.map((b) => b.gasUsed.toNumber()).reverse(),
fill: true,
segment: {
backgroundColor: (ctx, x) =>
ctx.p1.parsed.y >
Math.round(blocks[ctx.p1DataIndex].gasLimit.toNumber() / 2)
? "#22C55E70"
: "#EF444470",
borderColor: (ctx) =>
ctx.p1.parsed.y >
Math.round(blocks[ctx.p1DataIndex].gasLimit.toNumber() / 2)
? "#22C55E"
: "#EF4444",
},
tension: 0.2,
},
{
label: "Gas target",
data: blocks.map((b) => Math.round(b.gasLimit.toNumber() / 2)).reverse(),
borderColor: "#FCA5A5",
borderDash: [5, 5],
borderWidth: 2,
tension: 0.2,
pointStyle: "dash",
},
{
label: "Gas limit",
data: blocks.map((b) => b.gasLimit.toNumber()).reverse(),
borderColor: "#B91C1CF0",
tension: 0.2,
pointStyle: "crossRot",
radius: 5,
},
{
label: "Base fee (wei)",
data: blocks.map((b) => b.baseFeePerGas!.toNumber()).reverse(),
yAxisID: "yBaseFee",
borderColor: "#38BDF8",
tension: 0.2,
},
],
});

View File

@ -3,6 +3,7 @@ import { ethers } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCheckCircle,
faCube,
faTimesCircle,
} from "@fortawesome/free-solid-svg-icons";
import ContentFrame from "../ContentFrame";
@ -12,6 +13,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
import AddressHighlighter from "../components/AddressHighlighter";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
import Copy from "../components/Copy";
import Nonce from "../components/Nonce";
import Timestamp from "../components/Timestamp";
import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName";
@ -23,6 +25,8 @@ import TokenTransferItem from "../TokenTransferItem";
import { TransactionData, InternalOperation } from "../types";
import PercentageBar from "../components/PercentageBar";
import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition";
import PercentagePosition from "../components/PercentagePosition";
type DetailsProps = {
txData: TransactionData;
@ -60,25 +64,46 @@ const Details: React.FC<DetailsProps> = ({
</span>
)}
</InfoRow>
<InfoRow title="Block">
<div className="flex items-baseline space-x-2">
<BlockLink blockTag={txData.blockNumber} />
<BlockConfirmations confirmations={txData.confirmations} />
<InfoRow title="Block / Position">
<div className="flex items-baseline divide-x-2 divide-dotted divide-gray-300">
<div className="flex space-x-1 items-baseline mr-3">
<span className="text-orange-500">
<FontAwesomeIcon icon={faCube} />
</span>
<BlockLink blockTag={txData.blockNumber} />
<BlockConfirmations confirmations={txData.confirmations} />
</div>
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.transactionIndex}
total={txData.blockTransactionCount - 1}
/>
<PercentagePosition
perc={
txData.transactionIndex / (txData.blockTransactionCount - 1)
}
/>
</div>
</div>
</InfoRow>
<InfoRow title="Timestamp">
<Timestamp value={txData.timestamp} />
</InfoRow>
<InfoRow title="From">
<div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter address={txData.from}>
<DecoratedAddressLink
address={txData.from}
miner={txData.from === txData.miner}
txFrom
/>
</AddressHighlighter>
<Copy value={txData.from} />
<InfoRow title="From / Nonce">
<div className="flex divide-x-2 divide-dotted divide-gray-300">
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={txData.from}>
<DecoratedAddressLink
address={txData.from}
miner={txData.from === txData.miner}
txFrom
/>
</AddressHighlighter>
<Copy value={txData.from} />
</div>
<div className="flex items-baseline pl-3">
<Nonce value={txData.nonce} />
</div>
</div>
</InfoRow>
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
@ -105,7 +130,7 @@ const Details: React.FC<DetailsProps> = ({
<Copy value={txData.createdContractAddress!} />
</div>
)}
{internalOps && (
{internalOps && internalOps.length > 0 && (
<div className="mt-2 space-y-1">
{internalOps.map((op, i) => (
<InternalTransactionOperation
@ -189,11 +214,13 @@ const Details: React.FC<DetailsProps> = ({
)}
</div>
</InfoRow>
<InfoRow title="Gas Used/Limit">
<InfoRow title="Gas Used / Limit">
<div className="flex space-x-3 items-baseline">
<div>
<GasValue value={txData.gasUsed} /> /{" "}
<GasValue value={txData.gasLimit} />
<RelativePosition
pos={<GasValue value={txData.gasUsed} />}
total={<GasValue value={txData.gasLimit} />}
/>
</div>
<PercentageBar
perc={
@ -226,12 +253,6 @@ const Details: React.FC<DetailsProps> = ({
</div>
</InfoRow>
<InfoRow title="Ether Price">N/A</InfoRow>
<InfoRow title="Nonce">{txData.nonce}</InfoRow>
<InfoRow title="Position in Block">
<span className="rounded px-2 py-1 bg-gray-100 text-gray-500 text-xs">
{txData.transactionIndex}
</span>
</InfoRow>
<InfoRow title="Input Data">
<textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"

View File

@ -40,6 +40,7 @@ export type TransactionData = {
status: boolean;
blockNumber: number;
transactionIndex: number;
blockTransactionCount: number;
confirmations: number;
timestamp: number;
miner?: string;

View File

@ -1,7 +1,19 @@
import { ethers, BigNumber } from "ethers";
import { useState, useEffect } from "react";
import { ethers, BigNumber } from "ethers";
import { BlockWithTransactions } from "@ethersproject/abstract-provider";
import { getInternalOperations } from "./nodeFunctions";
import { TransactionData, InternalOperation } from "./types";
import {
TokenMetas,
TokenTransfer,
TransactionData,
InternalOperation,
ProcessedTransaction,
OperationType,
} from "./types";
import erc20 from "./erc20.json";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
export interface ExtendedBlock extends ethers.providers.Block {
blockReward: BigNumber;
@ -11,6 +23,7 @@ export interface ExtendedBlock extends ethers.providers.Block {
sha3Uncles: string;
stateRoot: string;
totalDifficulty: BigNumber;
transactionCount: number;
}
export const readBlock = async (
@ -19,43 +32,122 @@ export const readBlock = async (
) => {
let blockPromise: Promise<any>;
if (ethers.utils.isHexString(blockNumberOrHash, 32)) {
// TODO: fix
blockPromise = provider.send("eth_getBlockByHash", [
blockNumberOrHash,
false,
]);
} else {
blockPromise = provider.send("eth_getBlockByNumber", [
blockNumberOrHash,
false,
]);
blockPromise = provider.send("ots_getBlockDetails", [blockNumberOrHash]);
}
const [_rawBlock, _rawIssuance, _rawReceipts] = await Promise.all([
blockPromise,
provider.send("erigon_issuance", [blockNumberOrHash]),
provider.send("eth_getBlockReceipts", [blockNumberOrHash]),
]);
const receipts = (_rawReceipts as any[]).map((r) =>
provider.formatter.receipt(r)
);
const fees = receipts.reduce(
(acc, r) => acc.add(r.effectiveGasPrice.mul(r.gasUsed)),
BigNumber.from(0)
);
const _block = provider.formatter.block(_rawBlock);
const _rawBlock = await blockPromise;
const _block = provider.formatter.block(_rawBlock.block);
const _rawIssuance = _rawBlock.issuance;
const fees = provider.formatter.bigNumber(_rawBlock.totalFees);
const extBlock: ExtendedBlock = {
blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0),
unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0),
feeReward: fees,
size: provider.formatter.number(_rawBlock.size),
sha3Uncles: _rawBlock.sha3Uncles,
stateRoot: _rawBlock.stateRoot,
totalDifficulty: provider.formatter.bigNumber(_rawBlock.totalDifficulty),
size: provider.formatter.number(_rawBlock.block.size),
sha3Uncles: _rawBlock.block.sha3Uncles,
stateRoot: _rawBlock.block.stateRoot,
totalDifficulty: provider.formatter.bigNumber(
_rawBlock.block.totalDifficulty
),
transactionCount: provider.formatter.number(
_rawBlock.block.transactionCount
),
..._block,
};
return extBlock;
};
export const useBlockTransactions = (
provider: ethers.providers.JsonRpcProvider | undefined,
blockNumber: number,
pageNumber: number,
pageSize: number
): [number | undefined, ProcessedTransaction[] | undefined] => {
const [totalTxs, setTotalTxs] = useState<number>();
const [txs, setTxs] = useState<ProcessedTransaction[]>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const result = await provider.send("ots_getBlockTransactions", [
blockNumber,
pageNumber,
pageSize,
]);
const _block = provider.formatter.blockWithTransactions(
result.fullblock
) as unknown as BlockWithTransactions;
const _receipts = result.receipts;
const rawTxs = _block.transactions
.map(
(t, i): ProcessedTransaction => ({
blockNumber: blockNumber,
timestamp: _block.timestamp,
miner: _block.miner,
idx: i,
hash: t.hash,
from: t.from,
to: t.to,
createdContractAddress: _receipts[i].contractAddress,
value: t.value,
fee:
t.type !== 2
? provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.gasPrice!)
: provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!)),
gasPrice:
t.type !== 2
? t.gasPrice!
: t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!),
data: t.data,
status: provider.formatter.number(_receipts[i].status),
})
)
.reverse();
setTxs(rawTxs);
setTotalTxs(result.fullblock.transactionCount);
const checkTouchMinerAddr = await Promise.all(
rawTxs.map(async (res) => {
const ops = await getInternalOperations(provider, res.hash);
return (
ops.findIndex(
(op) =>
op.type === OperationType.TRANSFER &&
res.miner !== undefined &&
res.miner === ethers.utils.getAddress(op.to)
) !== -1
);
})
);
const processedTxs = rawTxs.map(
(r, i): ProcessedTransaction => ({
...r,
internalMinerInteraction: checkTouchMinerAddr[i],
})
);
setTxs(processedTxs);
};
readBlock();
}, [provider, blockNumber, pageNumber, pageSize]);
return [totalTxs, txs];
};
export const useBlockData = (
provider: ethers.providers.JsonRpcProvider | undefined,
blockNumberOrHash: string
@ -76,6 +168,99 @@ export const useBlockData = (
return block;
};
export const useTxData = (
provider: ethers.providers.JsonRpcProvider | undefined,
txhash: string
): TransactionData | undefined => {
const [txData, setTxData] = useState<TransactionData>();
useEffect(() => {
if (!provider) {
return;
}
const readTxData = async () => {
const [_response, _receipt] = await Promise.all([
provider.getTransaction(txhash),
provider.getTransactionReceipt(txhash),
]);
const _block = await readBlock(provider, _receipt.blockNumber.toString());
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: ethers.utils.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[1]), 12)
),
to: ethers.utils.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[2]), 12)
),
value: BigNumber.from(l.data),
});
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token]) {
continue;
}
const erc20Contract = new ethers.Contract(t.token, erc20, provider);
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
}
setTxData({
transactionHash: _receipt.transactionHash,
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockTransactionCount: _block.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block.timestamp,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
logs: _receipt.logs,
});
};
readTxData();
}, [provider, txhash]);
return txData;
};
export const useInternalOperations = (
provider: ethers.providers.JsonRpcProvider | undefined,
txData: TransactionData | undefined
@ -88,7 +273,10 @@ export const useInternalOperations = (
return;
}
const _transfers = await getInternalOperations(provider, txData);
const _transfers = await getInternalOperations(
provider,
txData.transactionHash
);
for (const t of _transfers) {
t.from = provider.formatter.address(t.from);
t.to = provider.formatter.address(t.to);

View File

@ -33,7 +33,12 @@ export const useProvider = (
setConnStatus(ConnectionStatus.CONNECTING);
const tryToConnect = async () => {
const provider = new ethers.providers.JsonRpcProvider(erigonURL);
let provider: ethers.providers.JsonRpcProvider;
if (erigonURL?.startsWith("ws://") || erigonURL?.startsWith("wss://")) {
provider = new ethers.providers.WebSocketProvider(erigonURL);
} else {
provider = new ethers.providers.JsonRpcProvider(erigonURL);
}
// Check if it is at least a regular ETH node
let blockNumber: number = 0;