diff --git a/README.md b/README.md index cd4a40d..bbfc214 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 diff --git a/package-lock.json b/package-lock.json index c192693..eaa7382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 6392c67..ac50f90 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Block.tsx b/src/Block.tsx index a515036..298ef01 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -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 {" "} in this block diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index 2ce687b..4835fd3 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -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(); - 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 = () => { diff --git a/src/Transaction.tsx b/src/Transaction.tsx index ad940fe..cb5a4e1 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -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(); const { txhash } = params; - const [txData, setTxData] = useState(); - 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(() => { diff --git a/src/components/Nonce.tsx b/src/components/Nonce.tsx new file mode 100644 index 0000000..69f1c17 --- /dev/null +++ b/src/components/Nonce.tsx @@ -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 = ({ value }) => ( + + + + + {value} + +); + +export default React.memo(Nonce); diff --git a/src/components/PercentagePosition.tsx b/src/components/PercentagePosition.tsx new file mode 100644 index 0000000..c3efe8e --- /dev/null +++ b/src/components/PercentagePosition.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +type PercentagePositionProps = { + perc: number; +}; + +const PercentagePosition: React.FC = ({ perc }) => ( +
+
+
+
+
+
+
+
+
+
+); + +export default React.memo(PercentagePosition); diff --git a/src/components/RelativePosition.tsx b/src/components/RelativePosition.tsx new file mode 100644 index 0000000..e952463 --- /dev/null +++ b/src/components/RelativePosition.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +type RelativePositionProps = { + pos: React.ReactNode; + total: React.ReactNode; +}; + +const RelativePosition: React.FC = ({ pos, total }) => ( + + {pos} + / {total} + +); + +export default React.memo(RelativePosition); diff --git a/src/nodeFunctions.ts b/src/nodeFunctions.ts index d160dac..29bd5d5 100644 --- a/src/nodeFunctions.ts +++ b/src/nodeFunctions.ts @@ -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[] = []; diff --git a/src/special/london/Blip.tsx b/src/special/london/Blip.tsx new file mode 100644 index 0000000..4a6d489 --- /dev/null +++ b/src/special/london/Blip.tsx @@ -0,0 +1,33 @@ +import React, { useState } from "react"; +import { Transition } from "@headlessui/react"; + +type BlipProps = { + value: number; +}; + +const Blip: React.FC = ({ value }) => { + const [show, setShow] = useState(true); + + return ( + setShow(false)} + > + {show && value !== 0 && ( +
0 ? "text-green-500" : "text-red-500" + } text-3xl`} + > + {value > 0 ? `+${value}` : `-${value}`} +
+ )} +
+ ); +}; + +export default React.memo(Blip); diff --git a/src/special/london/BlockRow.tsx b/src/special/london/BlockRow.tsx index 6331cdb..83efb0f 100644 --- a/src/special/london/BlockRow.tsx +++ b/src/special/london/BlockRow.tsx @@ -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 = ({ now, block }) => { +const BlockRow: React.FC = ({ 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 = ({ now, block }) => { const totalReward = block.blockReward.add(netFeeReward ?? 0); return ( -
+
@@ -37,7 +39,12 @@ const BlockRow: React.FC = ({ now, block }) => {
{ethers.utils.commify(gasTarget.toString())}
-
{block.baseFeePerGas?.toString()} wei
+
+
+ {block.baseFeePerGas?.toString()} wei + +
+
{ethers.utils.commify(ethers.utils.formatEther(totalReward))} Ether
diff --git a/src/special/london/Blocks.tsx b/src/special/london/Blocks.tsx index 7dc5f9a..7d580d9 100644 --- a/src/special/london/Blocks.tsx +++ b/src/special/london/Blocks.tsx @@ -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 = ({ latestBlock, targetBlockNumber }) => { const { provider } = useContext(RuntimeContext); - const [blocks, setBlock] = useState([]); + const [blocks, setBlocks] = useState([]); const [now, setNow] = useState(Date.now()); + const [toggleChart, setToggleChart] = useState(true); const addBlock = useCallback( async (blockNumber: number) => { @@ -77,7 +54,7 @@ const Blocks: React.FC = ({ 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 = ({ 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 = ({ latestBlock, targetBlockNumber }) => { await addBlock(i); } }; + + setBlocks([]); addPreviousBlocks(); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -139,19 +106,26 @@ const Blocks: React.FC = ({ latestBlock, targetBlockNumber }) => { return (
-
- - - - EIP-1559 is activated. Watch the fees burn. - - - +
+
+ + + + EIP-1559 is activated. Watch the fees burn. + + + +
+
+ +
- +
-
+
@@ -185,7 +159,7 @@ const Blocks: React.FC = ({ latestBlock, targetBlockNumber }) => { Age
- {blocks.map((b, i) => ( + {blocks.map((b, i, all) => ( = ({ latestBlock, targetBlockNumber }) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-10" > - + ))}
diff --git a/src/special/london/chart.ts b/src/special/london/chart.ts new file mode 100644 index 0000000..de37edb --- /dev/null +++ b/src/special/london/chart.ts @@ -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, + }, + ], +}); diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 7346b0c..2946df5 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -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 = ({ )} - -
- - + +
+
+ + + + + +
+
+ + +
- -
- - - - + +
+
+ + + + +
+
+ +
@@ -105,7 +130,7 @@ const Details: React.FC = ({
)} - {internalOps && ( + {internalOps && internalOps.length > 0 && (
{internalOps.map((op, i) => ( = ({ )}
- +
- /{" "} - + } + total={} + />
= ({
N/A - {txData.nonce} - - - {txData.transactionIndex} - -