diff --git a/chains b/chains index af79eb7..9a90800 160000 --- a/chains +++ b/chains @@ -1 +1 @@ -Subproject commit af79eb7387b2a4f747bf31cb318d4f1db43c4d84 +Subproject commit 9a908009cd88293a08222f75888d7f1bb82c13c8 diff --git a/package-lock.json b/package-lock.json index 2589006..7cc00c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@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.3", + "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", @@ -33,7 +33,7 @@ "@types/react-highlight": "^0.12.5", "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.7.1", - "ethers": "^5.6.1", + "ethers": "^5.6.2", "highlightjs-solidity": "^2.0.5", "react": "^17.0.2", "react-blockies": "^1.4.1", @@ -42,7 +42,7 @@ "react-error-boundary": "^3.1.4", "react-helmet-async": "^1.2.3", "react-image": "^4.0.3", - "react-router-dom": "^6.2.2", + "react-router-dom": "^6.3.0", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.5.0", "serve": "^13.0.2", @@ -1594,9 +1594,9 @@ } }, "node_modules/@ethersproject/bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.0.tgz", - "integrity": "sha512-3hJPlYemb9V4VLfJF5BfN0+55vltPZSHU3QKUyP9M3Y2TcajbiRrz65UG+xVHOzBereB1b9mn7r12o177xgN7w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz", + "integrity": "sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==", "funding": [ { "type": "individual", @@ -1775,9 +1775,9 @@ ] }, "node_modules/@ethersproject/networks": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.0.tgz", - "integrity": "sha512-DaVzgyThzHgSDLuURhvkp4oviGoGe9iTZW4jMEORHDRCgSZ9K9THGFKqL+qGXqPAYLEgZTf5z2w56mRrPR1MjQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.1.tgz", + "integrity": "sha512-b2rrupf3kCTcc3jr9xOWBuHylSFtbpJf79Ga7QR98ienU2UqGimPGEsYMgbI29KHJfA5Us89XwGVmxrlxmSrMg==", "funding": [ { "type": "individual", @@ -1830,9 +1830,9 @@ } }, "node_modules/@ethersproject/providers": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.1.tgz", - "integrity": "sha512-w8Wx15nH+aVDvnoKCyI1f3x0B5idmk/bDJXMEUqCfdO8Eadd0QpDx9lDMTMmenhOmf9vufLJXjpSm24D3ZnVpg==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.2.tgz", + "integrity": "sha512-6/EaFW/hNWz+224FXwl8+HdMRzVHt8DpPmu5MZaIQqx/K/ELnC9eY236SMV7mleCM3NnEArFwcAAxH5kUUgaRg==", "funding": [ { "type": "individual", @@ -2970,9 +2970,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.3.tgz", - "integrity": "sha512-u5DfKj4wfSt6akfndfu1eG06jsdyA/IUrlX2n3pyq5UXgXMhXY+NJb8eNK/7pqPWAhCKsCGWDdDO0zKMKAYkEA==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", + "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", "dependencies": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -8266,9 +8266,9 @@ } }, "node_modules/ethers": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.1.tgz", - "integrity": "sha512-qtl/2W+dwmUa5Z3JqwsbV3JEBZZHNARe5K/A2ePcNAuhJYnEKIgGOT/O9ouPwBijSqVoQnmQMzi5D48LFNOY2A==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.2.tgz", + "integrity": "sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ==", "funding": [ { "type": "individual", @@ -8287,7 +8287,7 @@ "@ethersproject/base64": "5.6.0", "@ethersproject/basex": "5.6.0", "@ethersproject/bignumber": "5.6.0", - "@ethersproject/bytes": "5.6.0", + "@ethersproject/bytes": "5.6.1", "@ethersproject/constants": "5.6.0", "@ethersproject/contracts": "5.6.0", "@ethersproject/hash": "5.6.0", @@ -8295,10 +8295,10 @@ "@ethersproject/json-wallets": "5.6.0", "@ethersproject/keccak256": "5.6.0", "@ethersproject/logger": "5.6.0", - "@ethersproject/networks": "5.6.0", + "@ethersproject/networks": "5.6.1", "@ethersproject/pbkdf2": "5.6.0", "@ethersproject/properties": "5.6.0", - "@ethersproject/providers": "5.6.1", + "@ethersproject/providers": "5.6.2", "@ethersproject/random": "5.6.0", "@ethersproject/rlp": "5.6.0", "@ethersproject/sha2": "5.6.0", @@ -14692,9 +14692,9 @@ } }, "node_modules/react-router": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.2.tgz", - "integrity": "sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", "dependencies": { "history": "^5.2.0" }, @@ -14703,12 +14703,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.2.tgz", - "integrity": "sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "dependencies": { "history": "^5.2.0", - "react-router": "6.2.2" + "react-router": "6.3.0" }, "peerDependencies": { "react": ">=16.8", @@ -20690,9 +20690,9 @@ } }, "@ethersproject/bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.0.tgz", - "integrity": "sha512-3hJPlYemb9V4VLfJF5BfN0+55vltPZSHU3QKUyP9M3Y2TcajbiRrz65UG+xVHOzBereB1b9mn7r12o177xgN7w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz", + "integrity": "sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==", "requires": { "@ethersproject/logger": "^5.6.0" } @@ -20791,9 +20791,9 @@ "integrity": "sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==" }, "@ethersproject/networks": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.0.tgz", - "integrity": "sha512-DaVzgyThzHgSDLuURhvkp4oviGoGe9iTZW4jMEORHDRCgSZ9K9THGFKqL+qGXqPAYLEgZTf5z2w56mRrPR1MjQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.1.tgz", + "integrity": "sha512-b2rrupf3kCTcc3jr9xOWBuHylSFtbpJf79Ga7QR98ienU2UqGimPGEsYMgbI29KHJfA5Us89XwGVmxrlxmSrMg==", "requires": { "@ethersproject/logger": "^5.6.0" } @@ -20816,9 +20816,9 @@ } }, "@ethersproject/providers": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.1.tgz", - "integrity": "sha512-w8Wx15nH+aVDvnoKCyI1f3x0B5idmk/bDJXMEUqCfdO8Eadd0QpDx9lDMTMmenhOmf9vufLJXjpSm24D3ZnVpg==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.2.tgz", + "integrity": "sha512-6/EaFW/hNWz+224FXwl8+HdMRzVHt8DpPmu5MZaIQqx/K/ELnC9eY236SMV7mleCM3NnEArFwcAAxH5kUUgaRg==", "requires": { "@ethersproject/abstract-provider": "^5.6.0", "@ethersproject/abstract-signer": "^5.6.0", @@ -21506,9 +21506,9 @@ } }, "@testing-library/jest-dom": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.3.tgz", - "integrity": "sha512-u5DfKj4wfSt6akfndfu1eG06jsdyA/IUrlX2n3pyq5UXgXMhXY+NJb8eNK/7pqPWAhCKsCGWDdDO0zKMKAYkEA==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", + "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", "requires": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -25126,9 +25126,9 @@ "version": "1.8.1" }, "ethers": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.1.tgz", - "integrity": "sha512-qtl/2W+dwmUa5Z3JqwsbV3JEBZZHNARe5K/A2ePcNAuhJYnEKIgGOT/O9ouPwBijSqVoQnmQMzi5D48LFNOY2A==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.2.tgz", + "integrity": "sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ==", "requires": { "@ethersproject/abi": "5.6.0", "@ethersproject/abstract-provider": "5.6.0", @@ -25137,7 +25137,7 @@ "@ethersproject/base64": "5.6.0", "@ethersproject/basex": "5.6.0", "@ethersproject/bignumber": "5.6.0", - "@ethersproject/bytes": "5.6.0", + "@ethersproject/bytes": "5.6.1", "@ethersproject/constants": "5.6.0", "@ethersproject/contracts": "5.6.0", "@ethersproject/hash": "5.6.0", @@ -25145,10 +25145,10 @@ "@ethersproject/json-wallets": "5.6.0", "@ethersproject/keccak256": "5.6.0", "@ethersproject/logger": "5.6.0", - "@ethersproject/networks": "5.6.0", + "@ethersproject/networks": "5.6.1", "@ethersproject/pbkdf2": "5.6.0", "@ethersproject/properties": "5.6.0", - "@ethersproject/providers": "5.6.1", + "@ethersproject/providers": "5.6.2", "@ethersproject/random": "5.6.0", "@ethersproject/rlp": "5.6.0", "@ethersproject/sha2": "5.6.0", @@ -29417,20 +29417,20 @@ "version": "0.8.3" }, "react-router": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.2.tgz", - "integrity": "sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", "requires": { "history": "^5.2.0" } }, "react-router-dom": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.2.tgz", - "integrity": "sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "requires": { "history": "^5.2.0", - "react-router": "6.2.2" + "react-router": "6.3.0" } }, "react-scripts": { diff --git a/package.json b/package.json index 0b78604..db92561 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@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.3", + "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", @@ -28,7 +28,7 @@ "@types/react-highlight": "^0.12.5", "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.7.1", - "ethers": "^5.6.1", + "ethers": "^5.6.2", "highlightjs-solidity": "^2.0.5", "react": "^17.0.2", "react-blockies": "^1.4.1", @@ -37,7 +37,7 @@ "react-error-boundary": "^3.1.4", "react-helmet-async": "^1.2.3", "react-image": "^4.0.3", - "react-router-dom": "^6.2.2", + "react-router-dom": "^6.3.0", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.5.0", "serve": "^13.0.2", diff --git a/src/Address.tsx b/src/Address.tsx index f739e7b..2e1d3bb 100644 --- a/src/Address.tsx +++ b/src/Address.tsx @@ -15,6 +15,7 @@ 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"; @@ -25,6 +26,7 @@ import { useAddressOrENS } from "./useResolvedAddresses"; import { useMultipleMetadata } from "./sourcify/useSourcify"; import { ChecksummedAddress } from "./types"; import { useAddressesWithCode } from "./useErigonHooks"; +import { useChainInfo } from "./useChainInfo"; const AddressTransactionByNonce = React.lazy( () => @@ -86,6 +88,8 @@ const Address: React.FC = () => { ? metadatas[checksummedAddress] : undefined; + const { network, faucets } = useChainInfo(); + // Search address by nonce === transaction @ nonce const rawNonce = searchParams.get("nonce"); if (rawNonce !== null) { @@ -119,6 +123,10 @@ const Address: React.FC = () => { {checksummedAddress} + {/* Only display faucets for testnets who actually have any */} + {network === "testnet" && faucets && faucets.length > 0 && ( + + )} {isENS && ( ENS: {addressOrName} diff --git a/src/App.tsx b/src/App.tsx index 81116d7..523dc5e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,10 +27,10 @@ const Transaction = React.lazy( import(/* webpackChunkName: "tx", webpackPrefetch: true */ "./Transaction") ); const London = React.lazy( - () => - import( - /* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London" - ) + () => import(/* webpackChunkName: "london" */ "./special/london/London") +); +const Faucets = React.lazy( + () => import(/* webpackChunkName: "faucets" */ "./Faucets") ); const PageNotFound = React.lazy( () => @@ -74,6 +74,7 @@ const App = () => { path="address/:addressOrName/*" element={
} /> + } /> } /> diff --git a/src/Block.tsx b/src/Block.tsx index 97b14ba..c8afe43 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -34,7 +34,9 @@ const Block: React.FC = () => { if (blockNumberOrHash === undefined) { throw new Error("blockNumberOrHash couldn't be undefined here"); } - const { nativeName, nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { name, symbol }, + } = useChainInfo(); const block = useBlockData(provider, blockNumberOrHash); useEffect(() => { @@ -146,7 +148,7 @@ const Block: React.FC = () => { {" "} - {nativeSymbol} + {symbol} @@ -165,7 +167,7 @@ const Block: React.FC = () => { {extraStr} (Hex:{" "} {block.extraData}) - + diff --git a/src/Faucets.tsx b/src/Faucets.tsx new file mode 100644 index 0000000..9e4527d --- /dev/null +++ b/src/Faucets.tsx @@ -0,0 +1,90 @@ +import React, { useMemo } from "react"; +import { useLocation } from "react-router-dom"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation"; +import { faFaucetDrip } from "@fortawesome/free-solid-svg-icons/faFaucetDrip"; +import ExternalLink from "./components/ExternalLink"; +import ContentFrame from "./ContentFrame"; +import StandardFrame from "./StandardFrame"; +import StandardSubtitle from "./StandardSubtitle"; +import { useChainInfo } from "./useChainInfo"; + +const Faucets: React.FC = () => { + const { network, faucets } = useChainInfo(); + const loc = useLocation(); + const urls = useMemo(() => { + const s = new URLSearchParams(loc.search); + const address = s.get("address"); + + const _urls: string[] = + network === "testnet" + ? faucets.map((u) => + // eslint-disable-next-line no-template-curly-in-string + address !== null ? u.replaceAll("${ADDRESS}", address) : u + ) + : []; + + // Shuffle faucets to avoid UI bias + for (let i = _urls.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [_urls[i], _urls[j]] = [_urls[j], _urls[i]]; + } + + return _urls; + }, [network, faucets, loc]); + + return ( + + Faucets + +
+ {urls.length > 0 && ( +
+ + + The following external links come from + https://github.com/ethereum-lists/chains and are *NOT* endorsed + by us. Use at your own risk. + +
+ )} + {/* Display the shuffling notice only if there are 1+ faucets */} + {urls.length > 1 && ( +
+ + The faucet links below are shuffled on page load. +
+ )} + {urls.length > 0 ? ( +
+ {urls.map((url) => ( +
+ + + {url} + +
+ ))} +
+ ) : ( +
There are no registered faucets.
+ )} +
+
+
+ ); +}; + +export default Faucets; diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx index 2f5bd4d..1e2586a 100644 --- a/src/PriceBox.tsx +++ b/src/PriceBox.tsx @@ -14,7 +14,9 @@ const ETH_FEED_DECIMALS = 8; // TODO: reduce duplication with useETHUSDOracle const PriceBox: React.FC = () => { const { provider } = useContext(RuntimeContext); - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); const latestBlock = useLatestBlock(provider); const maybeOutdated: boolean = @@ -82,9 +84,9 @@ const PriceBox: React.FC = () => { } font-sans text-xs text-gray-800`} > - {nativeSymbol}: ${latestPrice} + {symbol}: ${latestPrice} {latestGasData && ( <> diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 0869a17..5838428 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -1,16 +1,21 @@ -import React from "react"; +import React, { useContext } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; +import { faSackDollar } from "@fortawesome/free-solid-svg-icons/faSackDollar"; 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 { useTokenUSDOracle } from "./usePriceOracle"; type TokenTransferItemProps = { t: TokenTransfer; @@ -24,14 +29,14 @@ const TokenTransferItem: React.FC = ({ tokenMeta, metadatas, }) => { + const { provider } = useContext(RuntimeContext); + const blockNumber = useBlockNumberContext(); + const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token); + return (
- - - -
-
- From +
+
= ({ showCodeIndicator />
-
- To +
+ + + = ({ showCodeIndicator />
-
- For +
+ + + = ({ + {tokenMeta && quote !== undefined && decimals !== undefined && ( + + + + )}
diff --git a/src/TransactionPageContent.tsx b/src/TransactionPageContent.tsx index 31a7974..501ff46 100644 --- a/src/TransactionPageContent.tsx +++ b/src/TransactionPageContent.tsx @@ -10,7 +10,6 @@ import { useInternalOperations, useTxData } from "./useErigonHooks"; import { SelectionContext, useSelection } from "./useSelection"; import { SelectedTransactionContext } from "./useSelectedTransaction"; import { BlockNumberContext } from "./useBlockTagContext"; -import { useETHUSDOracle } from "./usePriceOracle"; import { useAppConfigContext } from "./useAppConfig"; import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify"; @@ -60,11 +59,6 @@ const TransactionPageContent: React.FC = ({ const selectionCtx = useSelection(); - const blockETHUSDPrice = useETHUSDOracle( - provider, - txData?.confirmedData?.blockNumber - ); - const { sourcifySource } = useAppConfigContext(); const metadata = useSourcify( txData?.to, @@ -114,7 +108,6 @@ const TransactionPageContent: React.FC = ({ devDoc={metadata?.output.devdoc} internalOps={internalOps} sendsEthToMiner={sendsEthToMiner} - ethUSDPrice={blockETHUSDPrice} /> } /> diff --git a/src/api/address-resolver/hardcoded-addresses/11155111.json b/src/api/address-resolver/hardcoded-addresses/11155111.json new file mode 100644 index 0000000..e534bca --- /dev/null +++ b/src/api/address-resolver/hardcoded-addresses/11155111.json @@ -0,0 +1,3 @@ +{ + "0xcFe95817aC44C3f8CE75F1EE6EC1431F586AB5A3": "Faucet: FaucETH" +} \ No newline at end of file diff --git a/src/api/address-resolver/index.ts b/src/api/address-resolver/index.ts index 5997e01..313b7cd 100644 --- a/src/api/address-resolver/index.ts +++ b/src/api/address-resolver/index.ts @@ -45,11 +45,11 @@ const resolvers: Record>> = { export const getResolver = ( chainId: number ): IAddressResolver> => { - const resolver = resolvers[chainId]; - if (resolver === undefined) { - return resolver[0]; // default MAGIC NUMBER + const res = resolvers[chainId]; + if (res === undefined) { + return resolvers[0]; // default MAGIC NUMBER } - return resolver; + return res; }; export const resolverRendererRegistry = new Map< diff --git a/src/components/Faucet.tsx b/src/components/Faucet.tsx new file mode 100644 index 0000000..a0480df --- /dev/null +++ b/src/components/Faucet.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFaucetDrip } from "@fortawesome/free-solid-svg-icons/faFaucetDrip"; +import { ChecksummedAddress } from "../types"; + +type FaucetProps = { + address: ChecksummedAddress; + rounded?: boolean; +}; + +const Faucet: React.FC = ({ address, rounded }) => ( + + + +); + +export default React.memo(Faucet); diff --git a/src/components/InternalCreate.tsx b/src/components/InternalCreate.tsx index fbb7f8f..7fa5850 100644 --- a/src/components/InternalCreate.tsx +++ b/src/components/InternalCreate.tsx @@ -1,6 +1,7 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; +import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; import TransactionAddress from "./TransactionAddress"; import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; @@ -15,16 +16,17 @@ const InternalCreate: React.FC = ({ internalOp }) => ( CREATE - Contract + + + + + +
- - (Creator:{" "} - ) -
); diff --git a/src/components/InternalSelfDestruct.tsx b/src/components/InternalSelfDestruct.tsx index 9bcb0ad..bc6029b 100644 --- a/src/components/InternalSelfDestruct.tsx +++ b/src/components/InternalSelfDestruct.tsx @@ -17,7 +17,9 @@ const InternalSelfDestruct: React.FC = ({ txData, internalOp, }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); const toMiner = txData.confirmedData?.miner !== undefined && internalOp.to === txData.confirmedData.miner; @@ -28,7 +30,6 @@ const InternalSelfDestruct: React.FC = ({ SELF DESTRUCT - Contract
@@ -46,7 +47,7 @@ const InternalSelfDestruct: React.FC = ({ TRANSFER - {formatEther(internalOp.value)} {nativeSymbol} + {formatEther(internalOp.value)} {symbol}
To diff --git a/src/components/InternalTransactionOperation.tsx b/src/components/InternalTransactionOperation.tsx index 5870336..3037dd2 100644 --- a/src/components/InternalTransactionOperation.tsx +++ b/src/components/InternalTransactionOperation.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { BigNumber } from "@ethersproject/bignumber"; import InternalTransfer from "./InternalTransfer"; import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalCreate from "./InternalCreate"; @@ -7,22 +8,29 @@ 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 = - ({ txData, internalOp }) => ( - <> - {internalOp.type === OperationType.TRANSFER && ( - - )} - {internalOp.type === OperationType.SELF_DESTRUCT && ( - - )} - {(internalOp.type === OperationType.CREATE || - internalOp.type === OperationType.CREATE2) && ( - - )} - - ); +const InternalTransactionOperation: React.FC< + InternalTransactionOperationProps +> = ({ txData, internalOp, ethUSDPrice }) => ( + <> + {internalOp.type === OperationType.TRANSFER && ( + + )} + {internalOp.type === OperationType.SELF_DESTRUCT && ( + + )} + {(internalOp.type === OperationType.CREATE || + internalOp.type === OperationType.CREATE2) && ( + + )} + +); export default React.memo(InternalTransactionOperation); diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index b73bdf0..af7001d 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -1,9 +1,13 @@ 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"; +import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; +import { faSackDollar } from "@fortawesome/free-solid-svg-icons/faSackDollar"; import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; +import USDAmount from "./USDAmount"; import { RuntimeContext } from "../useRuntime"; import { useHasCode } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; @@ -12,13 +16,18 @@ 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 = ({ txData, internalOp, + ethUSDPrice, }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol, decimals }, + } = useChainInfo(); const fromMiner = txData.confirmedData?.miner !== undefined && internalOp.from === txData.confirmedData.miner; @@ -39,48 +48,69 @@ const InternalTransfer: React.FC = ({ ); return ( -
- - TRANSFER - - - {formatEther(internalOp.value)} {nativeSymbol} - -
- From - -
- +
+
+
+ + TRANSFER + +
+ +
+ +
+
- -
-
- To - -
- -
-
+
+
+ + + + +
+ +
+
+
+
+ + + + + {formatEther(internalOp.value)} {symbol} + + {ethUSDPrice && ( + + + + )} +
); diff --git a/src/components/TransactionValue.tsx b/src/components/TransactionValue.tsx index b4a9d54..53f9300 100644 --- a/src/components/TransactionValue.tsx +++ b/src/components/TransactionValue.tsx @@ -22,16 +22,18 @@ const TransactionValue: React.FC = ({ value, hideUnit, }) => { - const { nativeSymbol, nativeDecimals } = useChainInfo(); - const formattedValue = formatValue(value, nativeDecimals); + const { + nativeCurrency: { symbol, decimals }, + } = useChainInfo(); + const formattedValue = formatValue(value, decimals); return ( {formattedValue} - {!hideUnit && ` ${nativeSymbol}`} + {!hideUnit && ` ${symbol}`} ); }; diff --git a/src/components/USDAmount.tsx b/src/components/USDAmount.tsx new file mode 100644 index 0000000..9ba7221 --- /dev/null +++ b/src/components/USDAmount.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { BigNumber, FixedNumber } from "@ethersproject/bignumber"; +import { commify } from "@ethersproject/units"; + +type USDAmountProps = { + amount: BigNumber; + amountDecimals: number; + quote: BigNumber; + quoteDecimals: number; +}; + +// TODO: fix the duplication mess with other currency display components + +/** + * Basic display of USD amount WITHOUT box decoration, only + * text formatting. + * + * USD amounts are displayed commified with 2 decimals places and $ prefix, + * i.e., "$1,000.00". + */ +const USDAmount: React.FC = ({ + amount, + amountDecimals, + quote, + quoteDecimals, +}) => { + const value = amount.mul(quote); + const decimals = amountDecimals + quoteDecimals; + + return ( + + $ + + {commify( + FixedNumber.fromValue(value, decimals, `ufixed256x${decimals}`) + .round(2) + .toString() + )} + + + ); +}; + +export default React.memo(USDAmount); diff --git a/src/components/USDValue.tsx b/src/components/USDValue.tsx index 9554e41..90da1c2 100644 --- a/src/components/USDValue.tsx +++ b/src/components/USDValue.tsx @@ -10,7 +10,9 @@ type USDValueProps = { }; const USDValue: React.FC = ({ value }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); return ( @@ -24,7 +26,7 @@ const USDValue: React.FC = ({ value }) => { .toString() )} {" "} - / {nativeSymbol} + / {symbol} ) : ( "N/A" diff --git a/src/special/london/BlockRow.tsx b/src/special/london/BlockRow.tsx index 3d1cf8f..3bec637 100644 --- a/src/special/london/BlockRow.tsx +++ b/src/special/london/BlockRow.tsx @@ -16,7 +16,9 @@ type BlockRowProps = { }; const BlockRow: React.FC = ({ now, block, baseFeeDelta }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); const gasTarget = block.gasLimit.div(ELASTICITY_MULTIPLIER); const burntFees = block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); @@ -55,11 +57,10 @@ const BlockRow: React.FC = ({ now, block, baseFeeDelta }) => {
- {commify(formatEther(totalReward))} {nativeSymbol} + {commify(formatEther(totalReward))} {symbol}
- {commify(formatEther(block.gasUsed.mul(block.baseFeePerGas!)))}{" "} - {nativeSymbol} + {commify(formatEther(block.gasUsed.mul(block.baseFeePerGas!)))} {symbol}
diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index e845d5e..e39346b 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -1,7 +1,6 @@ import React, { useContext, useMemo, useState } from "react"; import { Tab } from "@headlessui/react"; import { TransactionDescription } from "@ethersproject/abi"; -import { BigNumber } from "@ethersproject/bignumber"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; @@ -47,6 +46,7 @@ import { RuntimeContext } from "../useRuntime"; import { useContractsMetadata } from "../hooks"; import { useTransactionError } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; +import { useETHUSDOracle } from "../usePriceOracle"; type DetailsProps = { txData: TransactionData; @@ -56,7 +56,6 @@ type DetailsProps = { devDoc?: DevDoc | undefined; internalOps?: InternalOperation[]; sendsEthToMiner: boolean; - ethUSDPrice: BigNumber | undefined; }; const Details: React.FC = ({ @@ -67,7 +66,6 @@ const Details: React.FC = ({ devDoc, internalOps, sendsEthToMiner, - ethUSDPrice, }) => { const hasEIP1559 = txData.confirmedData?.blockBaseFeePerGas !== undefined && @@ -87,7 +85,15 @@ const Details: React.FC = ({ const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined; const { provider } = useContext(RuntimeContext); - const { nativeName, nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { name, symbol }, + } = useChainInfo(); + + const blockETHUSDPrice = useETHUSDOracle( + provider, + txData?.confirmedData?.blockNumber + ); + const addresses = useMemo(() => { const _addresses: ChecksummedAddress[] = []; if (txData.to) { @@ -292,6 +298,7 @@ const Details: React.FC = ({ key={i} txData={txData} internalOp={op} + ethUSDPrice={blockETHUSDPrice} /> ))}
@@ -315,10 +322,13 @@ const Details: React.FC = ({ )} - {nativeSymbol}{" "} - {!txData.value.isZero() && ethUSDPrice && ( + {symbol}{" "} + {!txData.value.isZero() && blockETHUSDPrice && ( - + )} @@ -338,8 +348,7 @@ const Details: React.FC = ({ {txData.type === 2 && ( <> - {" "} - {nativeSymbol} ( + {symbol} ( = ({ Gwei) - {nativeSymbol} ( + {symbol} ( Gwei) @@ -356,7 +365,7 @@ const Details: React.FC = ({
- {nativeSymbol} ( + {symbol} ( Gwei) {sendsEthToMiner && ( @@ -407,13 +416,12 @@ const Details: React.FC = ({
- {" "} - {nativeSymbol}{" "} - {ethUSDPrice && ( + {symbol}{" "} + {blockETHUSDPrice && ( )} @@ -421,8 +429,8 @@ const Details: React.FC = ({ {hasEIP1559 && }
- - + + )} diff --git a/src/transaction/RewardSplit.tsx b/src/transaction/RewardSplit.tsx index d6e47b3..b9268c4 100644 --- a/src/transaction/RewardSplit.tsx +++ b/src/transaction/RewardSplit.tsx @@ -12,7 +12,9 @@ type RewardSplitProps = { }; const RewardSplit: React.FC = ({ txData }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed); const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul( txData.confirmedData!.gasUsed @@ -41,7 +43,7 @@ const RewardSplit: React.FC = ({ txData }) => { {" "} - {nativeSymbol} + {symbol}
@@ -57,7 +59,7 @@ const RewardSplit: React.FC = ({ txData }) => { - {nativeSymbol} + {symbol}
diff --git a/src/transaction/TraceInput.tsx b/src/transaction/TraceInput.tsx index c21adfc..f8f469a 100644 --- a/src/transaction/TraceInput.tsx +++ b/src/transaction/TraceInput.tsx @@ -19,7 +19,9 @@ type TraceInputProps = { }; const TraceInput: React.FC = ({ t }) => { - const { nativeSymbol } = useChainInfo(); + const { + nativeCurrency: { symbol }, + } = useChainInfo(); const raw4Bytes = extract4Bytes(t.input); const fourBytes = use4Bytes(raw4Bytes); const sigText = @@ -57,8 +59,7 @@ const TraceInput: React.FC = ({ t }) => { {t.value && !t.value.isZero() && ( - {"{"}value: {" "} - {nativeSymbol} + {"{"}value: {symbol} {"}"} )} diff --git a/src/useChainInfo.ts b/src/useChainInfo.ts index 0d494b9..dec3798 100644 --- a/src/useChainInfo.ts +++ b/src/useChainInfo.ts @@ -3,15 +3,23 @@ import { chainInfoURL } from "./url"; import { OtterscanRuntime } from "./useRuntime"; export type ChainInfo = { - nativeName: string; - nativeSymbol: string; - nativeDecimals: number; + network: string | undefined; + faucets: string[]; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; }; export const defaultChainInfo: ChainInfo = { - nativeName: "Ether", - nativeSymbol: "ETH", - nativeDecimals: 18, + network: undefined, + faucets: [], + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, }; export const ChainInfoContext = createContext(undefined); @@ -25,24 +33,26 @@ export const useChainInfoFromMetadataFile = ( const [chainInfo, setChainInfo] = useState(undefined); useEffect(() => { - if (chainId === undefined) { + if (assetsURLPrefix === undefined || chainId === undefined) { setChainInfo(undefined); return; } const readChainInfo = async () => { - const res = await fetch(chainInfoURL(assetsURLPrefix!, chainId)); - if (!res.ok) { + 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; } - const info = await res.json(); - - setChainInfo({ - nativeName: info.nativeCurrency.name, - nativeDecimals: info.nativeCurrency.decimals, - nativeSymbol: info.nativeCurrency.symbol, - }); }; readChainInfo(); }, [assetsURLPrefix, chainId]); diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index 2e49afd..f39230f 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -3,6 +3,72 @@ import { JsonRpcProvider, BlockTag } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; +import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; +import { Fetcher } from "swr"; +import useSWRImmutable from "swr/immutable"; +import { ChecksummedAddress } from "./types"; + +const FEED_REGISTRY_MAINNET: ChecksummedAddress = + "0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf"; + +// The USD "token" address for Chainlink feed registry's purposes +const USD = "0x0000000000000000000000000000000000000348"; + +type FeedRegistryFetcherKey = [ChecksummedAddress, BlockTag]; +type FeedRegistryFetcherData = [BigNumber | undefined, number | undefined]; + +const feedRegistryFetcherKey = ( + tokenAddress: ChecksummedAddress, + blockTag: BlockTag | undefined +): FeedRegistryFetcherKey | null => { + if (blockTag === undefined) { + return null; + } + return [tokenAddress, blockTag]; +}; + +const feedRegistryFetcher = + ( + provider: JsonRpcProvider | undefined + ): Fetcher => + async (tokenAddress, blockTag) => { + // It work works on ethereum mainnet and kovan, see: + // https://docs.chain.link/docs/feed-registry/ + if (provider!.network.chainId !== 1) { + throw new Error("FeedRegistry is supported only on mainnet"); + } + + // Let SWR handle error + const feedRegistry = new Contract( + FEED_REGISTRY_MAINNET, + FeedRegistryInterface, + provider + ); + const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { + blockTag, + }); + const quote = BigNumber.from(priceData.answer); + const decimals = await feedRegistry.decimals(tokenAddress, USD, { + blockTag, + }); + return [quote, decimals]; + }; + +export const useTokenUSDOracle = ( + provider: JsonRpcProvider | undefined, + blockTag: BlockTag | undefined, + tokenAddress: ChecksummedAddress +): [BigNumber | undefined, number | undefined] => { + const fetcher = feedRegistryFetcher(provider); + const { data, error } = useSWRImmutable( + feedRegistryFetcherKey(tokenAddress, blockTag), + fetcher + ); + if (error) { + return [undefined, undefined]; + } + return data ?? [undefined, undefined]; +}; export const useETHUSDOracle = ( provider: JsonRpcProvider | undefined, @@ -22,6 +88,7 @@ export const useMultipleETHUSDOracle = ( blockTags: (BlockTag | undefined)[] ) => { const ethFeed = useMemo(() => { + // TODO: it currently is hardcoded to support only mainnet if (!provider || provider.network.chainId !== 1) { return undefined; } diff --git a/topic0 b/topic0 index cb6abe8..e7be61f 160000 --- a/topic0 +++ b/topic0 @@ -1 +1 @@ -Subproject commit cb6abe87055d2e2d54ba6a985903031420c4cbb1 +Subproject commit e7be61f955f53fa2131cd14155d6891993772ff7 diff --git a/trustwallet b/trustwallet index b8d1c58..dd4ba75 160000 --- a/trustwallet +++ b/trustwallet @@ -1 +1 @@ -Subproject commit b8d1c58acfd93b1b698fe5854f2c09bff70e9fdf +Subproject commit dd4ba75fb6da47c3ec1130109c4411c006267d56