diff --git a/4bytes b/4bytes index 3fde3a1..8113821 160000 --- a/4bytes +++ b/4bytes @@ -1 +1 @@ -Subproject commit 3fde3a1b2e002736e9a5e0c35df5bbe386a18d55 +Subproject commit 81138213c12364db38d5acbb40c0cf46f2ecdbfe diff --git a/README.md b/README.md index a31ef23..cd4a40d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ However, you will see that we made many UI improvements. ## Install instructions -This software is currently available as compile-only form. +This software is currently distributed as a docker image. It depends heavily on a working Erigon installation with Otterscan patches applied, so let's begin with it first. diff --git a/package-lock.json b/package-lock.json index 71853e7..c192693 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", + "@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", @@ -28,10 +29,12 @@ "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.8", + "chart.js": "^3.5.0", "ethers": "^5.4.1", "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", + "react-chartjs-2": "^3.0.4", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-image": "^4.0.3", @@ -45,7 +48,7 @@ "devDependencies": { "autoprefixer": "^9.8.6", "postcss": "^7.0.36", - "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4" + "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6" } }, "node_modules/@babel/code-frame": { @@ -2077,15 +2080,6 @@ "react": ">=16.x" } }, - "node_modules/@fullhuman/postcss-purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", - "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", - "dev": true, - "dependencies": { - "purgecss": "^3.1.3" - } - }, "node_modules/@hapi/address": { "version": "2.1.4", "license": "BSD-3-Clause" @@ -2115,6 +2109,18 @@ "@hapi/hoek": "^8.3.0" } }, + "node_modules/@headlessui/react": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz", + "integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "license": "ISC", @@ -5510,6 +5516,11 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz", + "integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA==" + }, "node_modules/check-types": { "version": "11.1.2", "license": "MIT" @@ -5751,11 +5762,12 @@ } }, "node_modules/color": { - "version": "3.1.3", - "license": "MIT", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "dependencies": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, "node_modules/color-convert": { @@ -5774,8 +5786,9 @@ "license": "MIT" }, "node_modules/color-string": { - "version": "1.5.4", - "license": "MIT", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -6808,9 +6821,9 @@ } }, "node_modules/didyoumean": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", - "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, "node_modules/diff-sequences": { @@ -8270,15 +8283,15 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.2.5", - "license": "MIT", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" }, "engines": { "node": ">=8" @@ -8864,8 +8877,9 @@ } }, "node_modules/glob-parent": { - "version": "5.1.1", - "license": "ISC", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": { "is-glob": "^4.0.1" }, @@ -11228,14 +11242,15 @@ "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.2", - "license": "MIT", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dependencies": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/micromatch/node_modules/braces": { @@ -12367,8 +12382,9 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "2.2.2", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "engines": { "node": ">=8.6" }, @@ -13822,9 +13838,9 @@ } }, "node_modules/purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", - "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", "dev": true, "dependencies": { "commander": "^6.0.0", @@ -13846,9 +13862,9 @@ } }, "node_modules/purgecss/node_modules/postcss": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.1.tgz", - "integrity": "sha512-9qH0MGjsSm+fjxOi3GnwViL1otfi7qkj+l/WX5gcRGmZNGsIcqc+A5fBkE6PUobEQK4APqYVaES+B3Uti98TCw==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", + "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", "dev": true, "dependencies": { "colorette": "^1.2.2", @@ -14055,6 +14071,18 @@ "react": ">=15.0.0" } }, + "node_modules/react-chartjs-2": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.0.4.tgz", + "integrity": "sha512-pcbFNpkPMTkGXXJ7k7hnukbRD0ZV01qB6JQY1ontITc2IYvhGlK6BBDy28VeydYs1Dl/c5ZpRgRVEtT5GUnxcQ==", + "dependencies": { + "lodash": "^4.17.19" + }, + "peerDependencies": { + "chart.js": "^3.1.0", + "react": "^16.8.0 || ^17.0.0" + } + }, "node_modules/react-dev-utils": { "version": "11.0.4", "license": "MIT", @@ -15929,14 +15957,16 @@ }, "node_modules/simple-swizzle": { "version": "0.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { "version": "0.3.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -16715,23 +16745,22 @@ }, "node_modules/tailwindcss": { "name": "@tailwindcss/postcss7-compat", - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz", - "integrity": "sha512-lFIBdD1D2w3RgHFg7kNB7U5LOlfbd+KXTzcLyC/RlQ9eVko6GjNCKpN/kdmfF9wiGxbSDT/3mousXeMZdOOuBg==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.6.tgz", + "integrity": "sha512-xrJqqVimKtNpyaaMapky3iFRCvEaA3PbdGG4HqHvyEdTLnfqKrRAv0znhqGeQlzFIL4P3w546cjWLYNtbvazFw==", "dev": true, "dependencies": { - "@fullhuman/postcss-purgecss": "^3.1.3", "arg": "^5.0.0", "autoprefixer": "^9", "bytes": "^3.0.0", "chalk": "^4.1.1", "chokidar": "^3.5.2", - "color": "^3.1.3", + "color": "^3.2.0", "cosmiconfig": "^7.0.0", "detective": "^5.2.0", - "didyoumean": "^1.2.1", + "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.5", + "fast-glob": "^3.2.7", "fs-extra": "^10.0.0", "glob-parent": "^6.0.0", "html-tags": "^3.1.0", @@ -16750,6 +16779,7 @@ "postcss-selector-parser": "^6.0.6", "postcss-value-parser": "^4.1.0", "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", "quick-lru": "^5.1.1", "reduce-css-calc": "^2.1.8", "resolve": "^1.20.0", @@ -20422,15 +20452,6 @@ "prop-types": "^15.7.2" } }, - "@fullhuman/postcss-purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", - "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", - "dev": true, - "requires": { - "purgecss": "^3.1.3" - } - }, "@hapi/address": { "version": "2.1.4" }, @@ -20455,6 +20476,12 @@ "@hapi/hoek": "^8.3.0" } }, + "@headlessui/react": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz", + "integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==", + "requires": {} + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "requires": { @@ -22777,6 +22804,11 @@ "char-regex": { "version": "1.0.2" }, + "chart.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz", + "integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA==" + }, "check-types": { "version": "11.1.2" }, @@ -22947,10 +22979,12 @@ } }, "color": { - "version": "3.1.3", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, "color-convert": { @@ -22968,7 +23002,9 @@ "version": "1.1.4" }, "color-string": { - "version": "1.5.4", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -23658,9 +23694,9 @@ } }, "didyoumean": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", - "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, "diff-sequences": { @@ -24626,14 +24662,15 @@ "version": "3.1.3" }, "fast-glob": { - "version": "3.2.5", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fast-json-stable-stringify": { @@ -25012,7 +25049,9 @@ } }, "glob-parent": { - "version": "5.1.1", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } @@ -26545,10 +26584,12 @@ "version": "0.1.1" }, "micromatch": { - "version": "4.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" }, "dependencies": { "braces": { @@ -27275,7 +27316,9 @@ "version": "2.1.0" }, "picomatch": { - "version": "2.2.2" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, "pify": { "version": "4.0.1" @@ -28290,9 +28333,9 @@ "version": "2.1.1" }, "purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", - "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", "dev": true, "requires": { "commander": "^6.0.0", @@ -28308,9 +28351,9 @@ "dev": true }, "postcss": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.1.tgz", - "integrity": "sha512-9qH0MGjsSm+fjxOi3GnwViL1otfi7qkj+l/WX5gcRGmZNGsIcqc+A5fBkE6PUobEQK4APqYVaES+B3Uti98TCw==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", + "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", "dev": true, "requires": { "colorette": "^1.2.2", @@ -28440,6 +28483,14 @@ "prop-types": "^15.5.10" } }, + "react-chartjs-2": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.0.4.tgz", + "integrity": "sha512-pcbFNpkPMTkGXXJ7k7hnukbRD0ZV01qB6JQY1ontITc2IYvhGlK6BBDy28VeydYs1Dl/c5ZpRgRVEtT5GUnxcQ==", + "requires": { + "lodash": "^4.17.19" + } + }, "react-dev-utils": { "version": "11.0.4", "requires": { @@ -29729,12 +29780,16 @@ }, "simple-swizzle": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", "requires": { "is-arrayish": "^0.3.1" }, "dependencies": { "is-arrayish": { - "version": "0.3.2" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" } } }, @@ -30274,23 +30329,22 @@ } }, "tailwindcss": { - "version": "npm:@tailwindcss/postcss7-compat@2.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz", - "integrity": "sha512-lFIBdD1D2w3RgHFg7kNB7U5LOlfbd+KXTzcLyC/RlQ9eVko6GjNCKpN/kdmfF9wiGxbSDT/3mousXeMZdOOuBg==", + "version": "npm:@tailwindcss/postcss7-compat@2.2.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.6.tgz", + "integrity": "sha512-xrJqqVimKtNpyaaMapky3iFRCvEaA3PbdGG4HqHvyEdTLnfqKrRAv0znhqGeQlzFIL4P3w546cjWLYNtbvazFw==", "dev": true, "requires": { - "@fullhuman/postcss-purgecss": "^3.1.3", "arg": "^5.0.0", "autoprefixer": "^9", "bytes": "^3.0.0", "chalk": "^4.1.1", "chokidar": "^3.5.2", - "color": "^3.1.3", + "color": "^3.2.0", "cosmiconfig": "^7.0.0", "detective": "^5.2.0", - "didyoumean": "^1.2.1", + "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.5", + "fast-glob": "^3.2.7", "fs-extra": "^10.0.0", "glob-parent": "^6.0.0", "html-tags": "^3.1.0", @@ -30309,6 +30363,7 @@ "postcss-selector-parser": "^6.0.6", "postcss-value-parser": "^4.1.0", "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", "quick-lru": "^5.1.1", "reduce-css-calc": "^2.1.8", "resolve": "^1.20.0", diff --git a/package.json b/package.json index f9454c1..6392c67 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", + "@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", @@ -24,10 +25,12 @@ "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.8", + "chart.js": "^3.5.0", "ethers": "^5.4.1", "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", + "react-chartjs-2": "^3.0.4", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-image": "^4.0.3", @@ -70,6 +73,6 @@ "devDependencies": { "autoprefixer": "^9.8.6", "postcss": "^7.0.36", - "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4" + "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6" } } diff --git a/src/App.tsx b/src/App.tsx index 4a4c323..4f1333d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ import React, { Suspense } from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import WarningHeader from "./WarningHeader"; import Home from "./Home"; import Search from "./Search"; import Title from "./Title"; import ConnectionErrorPanel from "./ConnectionErrorPanel"; +import London from "./special/london/London"; import Footer from "./Footer"; import { ConnectionStatus } from "./types"; import { RuntimeContext, useRuntime } from "./useRuntime"; @@ -26,6 +28,7 @@ const App = () => { ) : (
+ @@ -34,6 +37,9 @@ const App = () => { + + +
diff --git a/src/Block.tsx b/src/Block.tsx index c1578a8..a515036 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -1,92 +1,35 @@ -import React, { useEffect, useState, useMemo, useContext } from "react"; +import React, { useEffect, useMemo, useContext } from "react"; import { useParams, NavLink } from "react-router-dom"; -import { ethers, BigNumber } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faChevronLeft, - faChevronRight, -} from "@fortawesome/free-solid-svg-icons"; +import { faBurn } from "@fortawesome/free-solid-svg-icons"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; +import NavBlock from "./block/NavBlock"; import ContentFrame from "./ContentFrame"; -import NavButton from "./components/NavButton"; +import InfoRow from "./components/InfoRow"; import Timestamp from "./components/Timestamp"; import GasValue from "./components/GasValue"; +import PercentageBar from "./components/PercentageBar"; import BlockLink from "./components/BlockLink"; import DecoratedAddressLink from "./components/DecoratedAddressLink"; import TransactionValue from "./components/TransactionValue"; +import FormattedBalance from "./components/FormattedBalance"; import HexValue from "./components/HexValue"; import { RuntimeContext } from "./useRuntime"; import { useLatestBlockNumber } from "./useLatestBlock"; +import { blockTxsURL } from "./url"; +import { useBlockData } from "./useErigonHooks"; type BlockParams = { blockNumberOrHash: string; }; -interface ExtendedBlock extends ethers.providers.Block { - blockReward: BigNumber; - unclesReward: BigNumber; - feeReward: BigNumber; - size: number; - sha3Uncles: string; - stateRoot: string; - totalDifficulty: BigNumber; -} - const Block: React.FC = () => { const { provider } = useContext(RuntimeContext); const params = useParams<BlockParams>(); - const [block, setBlock] = useState<ExtendedBlock>(); - useEffect(() => { - if (!provider) { - return; - } - - const readBlock = async () => { - let blockPromise: Promise<any>; - if (ethers.utils.isHexString(params.blockNumberOrHash, 32)) { - blockPromise = provider.send("eth_getBlockByHash", [ - params.blockNumberOrHash, - false, - ]); - } else { - blockPromise = provider.send("eth_getBlockByNumber", [ - params.blockNumberOrHash, - false, - ]); - } - const [_rawBlock, _rawIssuance, _rawReceipts] = await Promise.all([ - blockPromise, - provider.send("erigon_issuance", [params.blockNumberOrHash]), - provider.send("eth_getBlockReceipts", [params.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 extBlock: ExtendedBlock = { - blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward), - unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward), - feeReward: fees, - size: provider.formatter.number(_rawBlock.size), - sha3Uncles: _rawBlock.sha3Uncles, - stateRoot: _rawBlock.stateRoot, - totalDifficulty: provider.formatter.bigNumber( - _rawBlock.totalDifficulty - ), - ..._block, - }; - setBlock(extBlock); - }; - readBlock(); - }, [provider, params.blockNumberOrHash]); - + const block = useBlockData(provider, params.blockNumberOrHash); useEffect(() => { if (block) { document.title = `Block #${block.number} | Otterscan`; @@ -101,50 +44,36 @@ const Block: React.FC = () => { console.error(err); } }, [block]); + const burntFees = + block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); + const netFeeReward = block && block.feeReward.sub(burntFees ?? 0); + const gasUsedPerc = + block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100; const latestBlockNumber = useLatestBlockNumber(provider); return ( <StandardFrame> <StandardSubtitle> - Block{" "} - <span className="text-base text-gray-500"> - #{params.blockNumberOrHash} - </span> + <div className="flex space-x-1 items-baseline"> + <span>Block</span> + <span className="text-base text-gray-500"> + #{params.blockNumberOrHash} + </span> + {block && ( + <NavBlock + blockNumber={block.number} + latestBlockNumber={latestBlockNumber} + /> + )} + </div> </StandardSubtitle> {block && ( <ContentFrame> <InfoRow title="Block Height"> - <div className="flex space-x-1 items-baseline"> - <span className="font-bold mr-1"> - {ethers.utils.commify(block.number)} - </span> - <NavButton - blockNum={block.number - 1} - disabled={block.number === 0} - > - <FontAwesomeIcon icon={faChevronLeft} /> - </NavButton> - <NavButton - blockNum={block.number + 1} - disabled={ - latestBlockNumber === undefined || - block.number >= latestBlockNumber - } - > - <FontAwesomeIcon icon={faChevronRight} /> - </NavButton> - <NavButton - blockNum={latestBlockNumber!} - disabled={ - latestBlockNumber === undefined || - block.number >= latestBlockNumber - } - > - <FontAwesomeIcon icon={faChevronRight} /> - <FontAwesomeIcon icon={faChevronRight} /> - </NavButton> - </div> + <span className="font-bold"> + {ethers.utils.commify(block.number)} + </span> </InfoRow> <InfoRow title="Timestamp"> <Timestamp value={block.timestamp} /> @@ -152,7 +81,7 @@ const Block: React.FC = () => { <InfoRow title="Transactions"> <NavLink className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded-lg px-2 py-1 text-xs" - to={`/block/${block.number}/txs`} + to={blockTxsURL(block.number)} > {block.transactions.length} transactions </NavLink>{" "} @@ -162,38 +91,77 @@ const Block: React.FC = () => { <DecoratedAddressLink address={block.miner} miner /> </InfoRow> <InfoRow title="Block Reward"> - <TransactionValue value={block.blockReward.add(block.feeReward)} /> + <TransactionValue + value={block.blockReward.add(netFeeReward ?? 0)} + /> {!block.feeReward.isZero() && ( <> {" "} (<TransactionValue value={block.blockReward} hideUnit /> +{" "} - <TransactionValue value={block.feeReward} hideUnit />) + <TransactionValue + value={netFeeReward ?? BigNumber.from(0)} + hideUnit + /> + ) </> )} </InfoRow> <InfoRow title="Uncles Reward"> <TransactionValue value={block.unclesReward} /> </InfoRow> - <InfoRow title="Difficult"> - {ethers.utils.commify(block.difficulty)} - </InfoRow> - <InfoRow title="Total Difficult"> - {ethers.utils.commify(block.totalDifficulty.toString())} - </InfoRow> <InfoRow title="Size"> {ethers.utils.commify(block.size)} bytes </InfoRow> - <InfoRow title="Gas Used"> - <GasValue value={block.gasUsed} /> - </InfoRow> - <InfoRow title="Gas Limit"> - <GasValue value={block.gasLimit} /> + {block.baseFeePerGas && ( + <InfoRow title="Base Fee"> + <span> + <FormattedBalance value={block.baseFeePerGas} decimals={9} />{" "} + Gwei ( + <FormattedBalance + value={block.baseFeePerGas} + decimals={0} + />{" "} + wei) + </span> + </InfoRow> + )} + {burntFees && ( + <InfoRow title="Burnt Fees"> + <div className="flex items-baseline space-x-1"> + <span className="flex space-x-1 text-orange-500"> + <span title="Burnt fees"> + <FontAwesomeIcon icon={faBurn} size="1x" /> + </span> + <span> + <span className="line-through"> + <FormattedBalance value={burntFees} /> + </span>{" "} + Ether + </span> + </span> + </div> + </InfoRow> + )} + <InfoRow title="Gas Used/Limit"> + <div className="flex space-x-3 items-baseline"> + <div> + <GasValue value={block.gasUsed} /> /{" "} + <GasValue value={block.gasLimit} /> + </div> + <PercentageBar perc={gasUsedPerc!} /> + </div> </InfoRow> <InfoRow title="Extra Data"> {extraStr} (Hex:{" "} <span className="font-data">{block.extraData}</span>) </InfoRow> <InfoRow title="Ether Price">N/A</InfoRow> + <InfoRow title="Difficult"> + {ethers.utils.commify(block.difficulty)} + </InfoRow> + <InfoRow title="Total Difficult"> + {ethers.utils.commify(block.totalDifficulty.toString())} + </InfoRow> <InfoRow title="Hash"> <HexValue value={block.hash} /> </InfoRow> @@ -215,15 +183,4 @@ const Block: React.FC = () => { ); }; -type InfoRowProps = { - title: string; -}; - -const InfoRow: React.FC<InfoRowProps> = ({ title, children }) => ( - <div className="grid grid-cols-4 py-4 text-sm"> - <div>{title}:</div> - <div className="col-span-3">{children}</div> - </div> -); - export default React.memo(Block); diff --git a/src/BlockTransactionHeader.tsx b/src/BlockTransactionHeader.tsx deleted file mode 100644 index 65a8288..0000000 --- a/src/BlockTransactionHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import { ethers } from "ethers"; -import StandardSubtitle from "./StandardSubtitle"; -import BlockLink from "./components/BlockLink"; - -type BlockTransactionHeaderProps = { - blockTag: ethers.providers.BlockTag; -}; - -const BlockTransactionHeader: React.FC<BlockTransactionHeaderProps> = ({ - blockTag, -}) => ( - <> - <StandardSubtitle>Transactions</StandardSubtitle> - <div className="pb-2 text-sm text-gray-500"> - For Block <BlockLink blockTag={blockTag} /> - </div> - </> -); - -export default React.memo(BlockTransactionHeader); diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index b427415..2ce687b 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -3,8 +3,8 @@ import { useParams, useLocation } from "react-router"; import { ethers } from "ethers"; import queryString from "query-string"; import StandardFrame from "./StandardFrame"; -import BlockTransactionHeader from "./BlockTransactionHeader"; -import BlockTransactionResults from "./BlockTransactionResults"; +import BlockTransactionHeader from "./block/BlockTransactionHeader"; +import BlockTransactionResults from "./block/BlockTransactionResults"; import { InternalOperation, OperationType, @@ -63,10 +63,18 @@ const BlockTransactions: React.FC = () => { to: t.to, createdContractAddress: _receipts[i].contractAddress, value: t.value, - fee: provider.formatter - .bigNumber(_receipts[i].gasUsed) - .mul(t.gasPrice!), - gasPrice: t.gasPrice!, + 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), }; diff --git a/src/Footer.tsx b/src/Footer.tsx index 61d59d7..a934b37 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -5,7 +5,13 @@ const Footer: React.FC = () => { const { provider } = useContext(RuntimeContext); return ( - <div className="w-full px-2 py-1 border-t border-t-gray-100 text-xs bg-link-blue text-gray-200 text-center"> + <div + className={`w-full px-2 py-1 border-t border-t-gray-100 text-xs ${ + provider?.network.chainId === 1 + ? "bg-link-blue text-gray-200" + : "bg-orange-400 text-white" + } text-center`} + > {provider ? ( <>Using Erigon node at {provider.connection.url}</> ) : ( diff --git a/src/Home.tsx b/src/Home.tsx index 7d74fcd..9bee0e9 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -1,10 +1,13 @@ import React, { useState, useContext } from "react"; import { NavLink, useHistory } from "react-router-dom"; import { ethers } from "ethers"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBurn } from "@fortawesome/free-solid-svg-icons"; import Logo from "./Logo"; import Timestamp from "./components/Timestamp"; import { RuntimeContext } from "./useRuntime"; import { useLatestBlock } from "./useLatestBlock"; +import { blockURL } from "./url"; const Home: React.FC = () => { const { provider } = useContext(RuntimeContext); @@ -53,10 +56,23 @@ const Home: React.FC = () => { > Search </button> + <div className="mx-auto mt-5 mb-5 text-lg text-link-blue hover:text-link-blue-hover font-bold"> + <NavLink to="/special/london"> + <div className="flex space-x-2 items-baseline text-orange-500 hover:text-orange-700 hover:underline"> + <span> + <FontAwesomeIcon icon={faBurn} /> + </span> + <span>Check out the special dashboard for EIP-1559</span> + <span> + <FontAwesomeIcon icon={faBurn} /> + </span> + </div> + </NavLink> + </div> {latestBlock && ( <NavLink className="mx-auto flex flex-col items-center space-y-1 mt-5 text-sm text-gray-500 hover:text-link-blue" - to={`/block/${latestBlock.number}`} + to={blockURL(latestBlock.number)} > <div>Latest block: {ethers.utils.commify(latestBlock.number)}</div> <Timestamp value={latestBlock.timestamp} /> diff --git a/src/Title.tsx b/src/Title.tsx index c64fdbd..46e1e5c 100644 --- a/src/Title.tsx +++ b/src/Title.tsx @@ -1,9 +1,11 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useContext } from "react"; import { Link, useHistory } from "react-router-dom"; import useKeyboardShortcut from "use-keyboard-shortcut"; import PriceBox from "./PriceBox"; +import { RuntimeContext } from "./useRuntime"; const Title: React.FC = () => { + const { provider } = useContext(RuntimeContext); const [search, setSearch] = useState<string>(); const [canSubmit, setCanSubmit] = useState<boolean>(false); const history = useHistory(); @@ -43,7 +45,7 @@ const Title: React.FC = () => { </div> </Link> <div className="flex items-baseline space-x-3"> - <PriceBox /> + {provider?.network.chainId === 1 && <PriceBox />} <form className="flex" onSubmit={handleSubmit} diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 55de7ac..ad940fe 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -92,12 +92,14 @@ const Transaction: React.FC = () => { 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!, - gasLimit: _response.gasLimit, gasUsed: _receipt.gasUsed, - gasUsedPerc: - _receipt.gasUsed.toNumber() / _response.gasLimit.toNumber(), + gasLimit: _response.gasLimit, nonce: _response.nonce, data: _response.data, logs: _receipt.logs, diff --git a/src/WarningHeader.tsx b/src/WarningHeader.tsx new file mode 100644 index 0000000..b21c249 --- /dev/null +++ b/src/WarningHeader.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from "react"; +import { RuntimeContext } from "./useRuntime"; + +const WarningHeader: React.FC = () => { + const { provider } = useContext(RuntimeContext); + const chainId = provider?.network.chainId; + if (chainId === 1) { + return <></>; + } + + let chainMsg = `ChainID: ${chainId}`; + if (chainId === 3) { + chainMsg = "Ropsten Testnet"; + } else if (chainId === 4) { + chainMsg = "Rinkeby Testnet"; + } else if (chainId === 5) { + chainMsg = "Görli Testnet"; + } else if (chainId === 42) { + chainMsg = "Kovan Testnet"; + } + return ( + <div className="w-full bg-orange-400 text-white text-center font-bold px-2 py-1"> + You are on {chainMsg} + </div> + ); +}; + +export default React.memo(WarningHeader); diff --git a/src/block/BlockTransactionHeader.tsx b/src/block/BlockTransactionHeader.tsx new file mode 100644 index 0000000..a15c463 --- /dev/null +++ b/src/block/BlockTransactionHeader.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from "react"; +import { ethers } from "ethers"; +import StandardSubtitle from "../StandardSubtitle"; +import BlockLink from "../components/BlockLink"; +import NavBlock from "./NavBlock"; +import { RuntimeContext } from "../useRuntime"; +import { useLatestBlockNumber } from "../useLatestBlock"; +import { blockTxsURL } from "../url"; + +type BlockTransactionHeaderProps = { + blockTag: ethers.providers.BlockTag; +}; + +const BlockTransactionHeader: React.FC<BlockTransactionHeaderProps> = ({ + blockTag, +}) => { + const { provider } = useContext(RuntimeContext); + const latestBlockNumber = useLatestBlockNumber(provider); + + return ( + <StandardSubtitle> + <div className="flex space-x-1 items-baseline"> + <span>Transactions</span> + <div className="flex space-x-1 text-sm text-gray-500"> + <span>For Block</span> + <BlockLink blockTag={blockTag} /> + <NavBlock + blockNumber={blockTag as number} + latestBlockNumber={latestBlockNumber} + urlBuilder={blockTxsURL} + /> + </div> + </div> + </StandardSubtitle> + ); +}; + +export default React.memo(BlockTransactionHeader); diff --git a/src/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx similarity index 76% rename from src/BlockTransactionResults.tsx rename to src/block/BlockTransactionResults.tsx index 74e2783..5937199 100644 --- a/src/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -1,15 +1,15 @@ import React, { useContext } from "react"; -import ContentFrame from "./ContentFrame"; -import PageControl from "./search/PageControl"; -import ResultHeader from "./search/ResultHeader"; -import PendingResults from "./search/PendingResults"; -import TransactionItem from "./search/TransactionItem"; -import { useFeeToggler } from "./search/useFeeToggler"; -import { RuntimeContext } from "./useRuntime"; -import { SelectionContext, useSelection } from "./useSelection"; -import { useENSCache } from "./useReverseCache"; -import { ProcessedTransaction } from "./types"; -import { PAGE_SIZE } from "./params"; +import ContentFrame from "../ContentFrame"; +import PageControl from "../search/PageControl"; +import ResultHeader from "../search/ResultHeader"; +import PendingResults from "../search/PendingResults"; +import TransactionItem from "../search/TransactionItem"; +import { useFeeToggler } from "../search/useFeeToggler"; +import { RuntimeContext } from "../useRuntime"; +import { SelectionContext, useSelection } from "../useSelection"; +import { useENSCache } from "../useReverseCache"; +import { ProcessedTransaction } from "../types"; +import { PAGE_SIZE } from "../params"; type BlockTransactionResultsProps = { page?: ProcessedTransaction[]; diff --git a/src/block/NavBlock.tsx b/src/block/NavBlock.tsx new file mode 100644 index 0000000..32a1dee --- /dev/null +++ b/src/block/NavBlock.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { ethers } from "ethers"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faChevronLeft, + faChevronRight, +} from "@fortawesome/free-solid-svg-icons"; +import NavButton from "./NavButton"; + +type NavBlockProps = { + blockNumber: number; + latestBlockNumber: number | undefined; + urlBuilder?: (blockNumber: ethers.providers.BlockTag) => string; +}; + +const NavBlock: React.FC<NavBlockProps> = ({ + blockNumber, + latestBlockNumber, + urlBuilder, +}) => ( + <div className="pl-2 self-center flex space-x-1"> + <NavButton + blockNum={blockNumber - 1} + disabled={blockNumber === 0} + urlBuilder={urlBuilder} + > + <FontAwesomeIcon icon={faChevronLeft} /> + </NavButton> + <NavButton + blockNum={blockNumber + 1} + disabled={ + latestBlockNumber === undefined || blockNumber >= latestBlockNumber + } + urlBuilder={urlBuilder} + > + <FontAwesomeIcon icon={faChevronRight} /> + </NavButton> + <NavButton + blockNum={latestBlockNumber!} + disabled={ + latestBlockNumber === undefined || blockNumber >= latestBlockNumber + } + urlBuilder={urlBuilder} + > + <FontAwesomeIcon icon={faChevronRight} /> + <FontAwesomeIcon icon={faChevronRight} /> + </NavButton> + </div> +); + +export default React.memo(NavBlock); diff --git a/src/components/NavButton.tsx b/src/block/NavButton.tsx similarity index 76% rename from src/components/NavButton.tsx rename to src/block/NavButton.tsx index a9c1a9d..edc85c4 100644 --- a/src/components/NavButton.tsx +++ b/src/block/NavButton.tsx @@ -1,13 +1,17 @@ +import { ethers } from "ethers"; import { NavLink } from "react-router-dom"; +import { blockURL } from "../url"; type NavButtonProps = { blockNum: number; disabled?: boolean; + urlBuilder?: (blockNumber: ethers.providers.BlockTag) => string; }; const NavButton: React.FC<NavButtonProps> = ({ blockNum, disabled, + urlBuilder, children, }) => { if (disabled) { @@ -21,7 +25,7 @@ const NavButton: React.FC<NavButtonProps> = ({ return ( <NavLink className="transition-colors bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded px-2 py-1 text-xs" - to={`/block/${blockNum}`} + to={urlBuilder ? urlBuilder(blockNum) : blockURL(blockNum)} > {children} </NavLink> diff --git a/src/components/BlockConfirmations.tsx b/src/components/BlockConfirmations.tsx new file mode 100644 index 0000000..4846d09 --- /dev/null +++ b/src/components/BlockConfirmations.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +type BlockConfirmationsProps = { + confirmations: number; +}; + +const BlockConfirmations: React.FC<BlockConfirmationsProps> = ({ + confirmations, +}) => ( + <span className="rounded text-xs bg-gray-100 text-gray-500 px-2 py-1"> + {confirmations} Block{" "} + {confirmations === 1 ? "Confirmation" : "Confirmations"} + </span> +); + +export default React.memo(BlockConfirmations); diff --git a/src/components/BlockLink.tsx b/src/components/BlockLink.tsx index f9a8866..155553f 100644 --- a/src/components/BlockLink.tsx +++ b/src/components/BlockLink.tsx @@ -1,6 +1,7 @@ import React from "react"; import { NavLink } from "react-router-dom"; import { ethers } from "ethers"; +import { blockURL } from "../url"; type BlockLinkProps = { blockTag: ethers.providers.BlockTag; @@ -18,7 +19,7 @@ const BlockLink: React.FC<BlockLinkProps> = ({ blockTag }) => { className={`text-link-blue hover:text-link-blue-hover ${ isNum ? "font-blocknum" : "font-hash" }`} - to={`/block/${blockTag}`} + to={blockURL(blockTag)} > {text} </NavLink> diff --git a/src/components/ExternalLink.tsx b/src/components/ExternalLink.tsx new file mode 100644 index 0000000..51769a8 --- /dev/null +++ b/src/components/ExternalLink.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; + +type ExternalLinkProps = { + href: string; +}; + +const ExternalLink: React.FC<ExternalLinkProps> = ({ href, children }) => ( + <a + className="text-link-blue hover:text-link-blue-hover" + href={href} + target="_blank" + rel="noopener noreferrer" + > + <span className="inline-flex items-baseline space-x-1"> + <span>{children}</span> + <FontAwesomeIcon icon={faExternalLinkAlt} size="sm" /> + </span> + </a> +); + +export default ExternalLink; diff --git a/src/components/InfoRow.tsx b/src/components/InfoRow.tsx index d3758b3..4651923 100644 --- a/src/components/InfoRow.tsx +++ b/src/components/InfoRow.tsx @@ -1,7 +1,7 @@ import React from "react"; type InfoRowProps = React.PropsWithChildren<{ - title: string; + title: React.ReactNode; }>; const InfoRow: React.FC<InfoRowProps> = ({ title, children }) => ( diff --git a/src/components/PercentageBar.tsx b/src/components/PercentageBar.tsx new file mode 100644 index 0000000..a868784 --- /dev/null +++ b/src/components/PercentageBar.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +type PercentageBarProps = { + perc: number; +}; + +const PercentageBar: React.FC<PercentageBarProps> = ({ perc }) => ( + <div className="self-center w-40 border rounded border-gray-200"> + <div className="w-full h-5 rounded bg-gradient-to-r from-red-400 via-yellow-300 to-green-400 relative"> + <div + className="absolute top-0 right-0 bg-white h-full rounded-r" + style={{ width: `${100 - perc}%` }} + ></div> + <div className="w-full h-full absolute flex mix-blend-multiply text-sans text-gray-600"> + <span className="m-auto">{perc}%</span> + </div> + </div> + </div> +); + +export default React.memo(PercentageBar); diff --git a/src/components/PercentageGauge.tsx b/src/components/PercentageGauge.tsx new file mode 100644 index 0000000..23af8f7 --- /dev/null +++ b/src/components/PercentageGauge.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +type PercentageGaugeProps = { + perc: number; + bgFull: string; + bgPerc: string; + textPerc: string; +}; + +const PercentageGauge: React.FC<PercentageGaugeProps> = ({ + perc, + bgFull, + bgPerc, + textPerc, +}) => ( + <div className="w-60 h-6 border-l-2 border-gray-400 relative"> + <div className="flex absolute w-full h-full"> + <div className={`my-auto h-5 rounded-r-lg w-full ${bgFull}`}></div> + </div> + <div className="flex absolute w-full h-full"> + <div + className={`my-auto h-5 rounded-r-lg ${bgPerc}`} + style={{ width: `${perc}%` }} + ></div> + </div> + <div + className={`flex absolute w-full h-full mix-blend-multiply text-sans ${textPerc}`} + > + <span className="m-auto">{perc}%</span> + </div> + </div> +); + +export default React.memo(PercentageGauge); diff --git a/src/components/TimestampAge.tsx b/src/components/TimestampAge.tsx index 7849c14..20f4784 100644 --- a/src/components/TimestampAge.tsx +++ b/src/components/TimestampAge.tsx @@ -1,11 +1,14 @@ import React from "react"; type TimestampAgeProps = { + now?: number | undefined; timestamp: number; }; -const TimestampAge: React.FC<TimestampAgeProps> = ({ timestamp }) => { - const now = Date.now() / 1000; +const TimestampAge: React.FC<TimestampAgeProps> = ({ now, timestamp }) => { + if (now === undefined) { + now = Date.now() / 1000; + } let diff = now - timestamp; let desc = ""; diff --git a/src/components/TransactionType.tsx b/src/components/TransactionType.tsx new file mode 100644 index 0000000..e7faa6c --- /dev/null +++ b/src/components/TransactionType.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import ExternalLink from "./ExternalLink"; + +type TransactionTypeProps = { + type: number; +}; + +const TransactionType: React.FC<TransactionTypeProps> = ({ type }) => { + let description: React.ReactNode; + switch (type) { + case 0: + description = "legacy"; + break; + case 1: + description = ( + <ExternalLink href="https://eips.ethereum.org/EIPS/eip-2930"> + EIP-2930 + </ExternalLink> + ); + break; + case 2: + description = ( + <ExternalLink href="https://eips.ethereum.org/EIPS/eip-1559"> + EIP-1559 + </ExternalLink> + ); + break; + default: + description = "unknown"; + } + + return ( + <span> + {type} <span className="font-bold">({description})</span> + </span> + ); +}; + +export default React.memo(TransactionType); diff --git a/src/special/london/BlockRow.tsx b/src/special/london/BlockRow.tsx new file mode 100644 index 0000000..6331cdb --- /dev/null +++ b/src/special/london/BlockRow.tsx @@ -0,0 +1,60 @@ +import { ethers } from "ethers"; +import React from "react"; +import BlockLink from "../../components/BlockLink"; +import TimestampAge from "../../components/TimestampAge"; +import { ExtendedBlock } from "../../useErigonHooks"; + +const ELASTICITY_MULTIPLIER = 2; + +type BlockRowProps = { + now: number; + block: ExtendedBlock; +}; + +const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => { + const gasTarget = block.gasLimit.div(ELASTICITY_MULTIPLIER); + const burntFees = + block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); + const netFeeReward = block && block.feeReward.sub(burntFees ?? 0); + const totalReward = block.blockReward.add(netFeeReward ?? 0); + + return ( + <div className="grid grid-cols-8 px-3 py-2 hover:bg-gray-100"> + <div> + <BlockLink blockTag={block.number} /> + </div> + <div + className={`text-right ${ + block.gasUsed.gt(gasTarget) + ? "text-green-500" + : block.gasUsed.lt(gasTarget) + ? "text-red-500" + : "" + }`} + > + {ethers.utils.commify(block.gasUsed.toString())} + </div> + <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 col-span-2"> + {ethers.utils.commify(ethers.utils.formatEther(totalReward))} Ether + </div> + <div className="text-right line-through text-orange-500"> + {ethers.utils.commify( + ethers.utils.formatUnits( + block.gasUsed.mul(block.baseFeePerGas!).toString(), + 9 + ) + )}{" "} + Gwei + </div> + <div className="text-right text-gray-400"> + <TimestampAge now={now / 1000} timestamp={block.timestamp} /> + </div> + </div> + ); +}; + +export default React.memo(BlockRow); diff --git a/src/special/london/Blocks.tsx b/src/special/london/Blocks.tsx new file mode 100644 index 0000000..7dc5f9a --- /dev/null +++ b/src/special/london/Blocks.tsx @@ -0,0 +1,208 @@ +import React, { + useState, + useEffect, + useContext, + useMemo, + useCallback, +} 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 { + faBurn, + faCoins, + faCube, + faGasPump, + faHistory, +} from "@fortawesome/free-solid-svg-icons"; +import BlockRow from "./BlockRow"; +import { ExtendedBlock, readBlock } from "../../useErigonHooks"; +import { RuntimeContext } from "../../useRuntime"; + +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; +}; + +const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => { + const { provider } = useContext(RuntimeContext); + const [blocks, setBlock] = useState<ExtendedBlock[]>([]); + const [now, setNow] = useState<number>(Date.now()); + + const addBlock = useCallback( + async (blockNumber: number) => { + if (!provider) { + return; + } + + // Skip blocks before the hard fork during the transition + if (blockNumber < targetBlockNumber) { + return; + } + + const extBlock = await readBlock(provider, blockNumber.toString()); + setNow(Date.now()); + setBlock((_blocks) => { + if (_blocks.length > 0 && blockNumber === _blocks[0].number) { + return _blocks; + } + + // Leave the last block because of transition animation + const newBlocks = [extBlock, ..._blocks].slice( + 0, + MAX_BLOCK_HISTORY + 1 + ); + + // Little hack to fix out of order block notifications + newBlocks.sort((a, b) => b.number - a.number); + return newBlocks; + }); + }, + [provider, targetBlockNumber] + ); + + useEffect(() => { + 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]); + + // On page reload, pre-populate the last N blocks + useEffect( + () => { + const addPreviousBlocks = async () => { + for ( + let i = latestBlock.number - PREV_BLOCK_COUNT; + i < latestBlock.number; + i++ + ) { + await addBlock(i); + } + }; + addPreviousBlocks(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + 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> + <div> + <Line data={data} height={100} options={options} /> + </div> + <div className="mt-5 grid grid-cols-8 px-3 py-2"> + <div className="flex space-x-1 items-baseline"> + <span className="text-gray-500"> + <FontAwesomeIcon icon={faCube} /> + </span> + <span>Block</span> + </div> + <div className="text-right flex space-x-1 justify-end items-baseline"> + <span className="text-gray-500"> + <FontAwesomeIcon icon={faGasPump} /> + </span> + <span>Gas used</span> + </div> + <div className="text-right">Gas target</div> + <div className="text-right">Base fee</div> + <div className="text-right col-span-2 flex space-x-1 justify-end items-baseline"> + <span className="text-yellow-400"> + <FontAwesomeIcon icon={faCoins} /> + </span> + <span>Rewards</span> + </div> + <div className="text-right flex space-x-1 justify-end items-baseline"> + <span className="text-orange-500"> + <FontAwesomeIcon icon={faBurn} /> + </span> + <span>Burnt fees</span> + </div> + <div className="text-right flex space-x-1 justify-end items-baseline"> + <span className="text-gray-500"> + <FontAwesomeIcon icon={faHistory} /> + </span> + <span>Age</span> + </div> + </div> + {blocks.map((b, i) => ( + <Transition + key={b.hash} + show={i < MAX_BLOCK_HISTORY} + appear + enter="transition transform ease-out duration-500" + enterFrom="opacity-0 -translate-y-10" + enterTo="opacity-100 translate-y-0" + leave="transition transform ease-out duration-1000" + leaveFrom="opacity-100 translate-y-0" + leaveTo="opacity-0 translate-y-10" + > + <BlockRow now={now} block={b} /> + </Transition> + ))} + </div> + </div> + ); +}; + +export default React.memo(Blocks); diff --git a/src/special/london/Countdown.tsx b/src/special/london/Countdown.tsx new file mode 100644 index 0000000..718aa22 --- /dev/null +++ b/src/special/london/Countdown.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from "react"; +import { ethers } from "ethers"; + +type CountdownProps = { + provider: ethers.providers.JsonRpcProvider; + currentBlock: ethers.providers.Block; + targetBlock: number; +}; + +const Countdown: React.FC<CountdownProps> = ({ + provider, + currentBlock, + targetBlock, +}) => { + const [targetTimestamp, setTargetTimestamp] = useState<number>(); + + useEffect(() => { + const calcTime = async () => { + const diff = targetBlock - currentBlock.number; + const _prevBlock = await provider.getBlock(currentBlock.number - diff); + const _targetTimestamp = + currentBlock.timestamp + + (currentBlock.timestamp - _prevBlock.timestamp); + setTargetTimestamp(_targetTimestamp); + }; + calcTime(); + }, [provider, currentBlock, targetBlock]); + + return ( + <div className="w-full h-full flex"> + <div className="m-auto text-center"> + <h1 className="text-6xl mb-10">London Network Upgrade</h1> + <h2 className="text-5xl"> + {ethers.utils.commify(targetBlock - currentBlock.number)} + </h2> + <h6 className="text-sm mb-10">Blocks remaining</h6> + <h2 className="inline-flex space-x-10 text-base mb-10"> + <div>Current block: {ethers.utils.commify(currentBlock.number)}</div> + <div>Target block: {ethers.utils.commify(targetBlock)}</div> + </h2> + {targetTimestamp && ( + <div className="text-lg"> + {new Date(targetTimestamp * 1000).toLocaleDateString()} @{" "} + {new Date(targetTimestamp * 1000).toLocaleTimeString()} (Estimative) + </div> + )} + </div> + </div> + ); +}; + +export default React.memo(Countdown); diff --git a/src/special/london/London.tsx b/src/special/london/London.tsx new file mode 100644 index 0000000..0663796 --- /dev/null +++ b/src/special/london/London.tsx @@ -0,0 +1,31 @@ +import React, { useContext } from "react"; +import { useLatestBlock } from "../../useLatestBlock"; +import { RuntimeContext } from "../../useRuntime"; +import Countdown from "./Countdown"; +import Blocks from "./Blocks"; +import { londonBlockNumber } from "./params"; + +const London: React.FC = () => { + const { provider } = useContext(RuntimeContext); + const block = useLatestBlock(provider); + if (!provider || !block) { + return <></>; + } + + // Display countdown + const targetBlockNumber = + londonBlockNumber[provider.network.chainId.toString()]; + if (block.number < targetBlockNumber) { + return ( + <Countdown + provider={provider} + currentBlock={block} + targetBlock={targetBlockNumber} + /> + ); + } + + return <Blocks latestBlock={block} targetBlockNumber={targetBlockNumber} />; +}; + +export default React.memo(London); diff --git a/src/special/london/params.ts b/src/special/london/params.ts new file mode 100644 index 0000000..4f04caf --- /dev/null +++ b/src/special/london/params.ts @@ -0,0 +1,6 @@ +export const londonBlockNumber: { [chainId: string]: number } = { + "1": 12965000, + "3": 10499401, + "4": 8897988, + "5": 5062605, +}; diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 4204908..7346b0c 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -8,16 +8,21 @@ import { import ContentFrame from "../ContentFrame"; import InfoRow from "../components/InfoRow"; import BlockLink from "../components/BlockLink"; +import BlockConfirmations from "../components/BlockConfirmations"; import AddressHighlighter from "../components/AddressHighlighter"; import DecoratedAddressLink from "../components/DecoratedAddressLink"; import Copy from "../components/Copy"; import Timestamp from "../components/Timestamp"; import InternalTransactionOperation from "../components/InternalTransactionOperation"; import MethodName from "../components/MethodName"; +import TransactionType from "../components/TransactionType"; +import RewardSplit from "./RewardSplit"; import GasValue from "../components/GasValue"; import FormattedBalance from "../components/FormattedBalance"; import TokenTransferItem from "../TokenTransferItem"; import { TransactionData, InternalOperation } from "../types"; +import PercentageBar from "../components/PercentageBar"; +import ExternalLink from "../components/ExternalLink"; type DetailsProps = { txData: TransactionData; @@ -29,146 +34,213 @@ const Details: React.FC<DetailsProps> = ({ txData, internalOps, sendsEthToMiner, -}) => ( - <ContentFrame tabs> - <InfoRow title="Transaction Hash"> - <div className="flex items-baseline space-x-2"> - <span className="font-hash">{txData.transactionHash}</span> - <Copy value={txData.transactionHash} /> - </div> - </InfoRow> - <InfoRow title="Status"> - {txData.status ? ( - <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> - <FontAwesomeIcon icon={faCheckCircle} size="1x" /> - <span>Success</span> - </span> - ) : ( - <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-red-50 text-red-500 text-xs"> - <FontAwesomeIcon icon={faTimesCircle} size="1x" /> - <span>Fail</span> - </span> - )} - </InfoRow> - <InfoRow title="Block"> - <div className="flex items-baseline space-x-2"> - <BlockLink blockTag={txData.blockNumber} /> - <span className="rounded text-xs bg-gray-100 text-gray-500 px-2 py-1"> - {txData.confirmations} Block Confirmations - </span> - </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} /> - </div> - </InfoRow> - <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}> - {txData.to ? ( - <div className="flex items-baseline space-x-2 -ml-1"> - <AddressHighlighter address={txData.to}> - <DecoratedAddressLink - address={txData.to} - miner={txData.to === txData.miner} - txTo - /> - </AddressHighlighter> - <Copy value={txData.to} /> - </div> - ) : ( - <div className="flex items-baseline space-x-2 -ml-1"> - <AddressHighlighter address={txData.createdContractAddress!}> - <DecoratedAddressLink - address={txData.createdContractAddress!} - creation - txTo - /> - </AddressHighlighter> - <Copy value={txData.createdContractAddress!} /> - </div> - )} - {internalOps && ( - <div className="mt-2 space-y-1"> - {internalOps.map((op, i) => ( - <InternalTransactionOperation - key={i} - txData={txData} - internalOp={op} - /> - ))} - </div> - )} - </InfoRow> - <InfoRow title="Transaction Action"> - <MethodName data={txData.data} /> - </InfoRow> - {txData.tokenTransfers.length > 0 && ( - <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> - <div> - {txData.tokenTransfers.map((t, i) => ( - <TokenTransferItem - key={i} - t={t} - txData={txData} - tokenMetas={txData.tokenMetas} - /> - ))} +}) => { + const hasEIP1559 = + txData.blockBaseFeePerGas !== undefined && + txData.blockBaseFeePerGas !== null; + + return ( + <ContentFrame tabs> + <InfoRow title="Transaction Hash"> + <div className="flex items-baseline space-x-2"> + <span className="font-hash">{txData.transactionHash}</span> + <Copy value={txData.transactionHash} /> </div> </InfoRow> - )} - <InfoRow title="Value"> - <span className="rounded bg-gray-100 px-2 py-1 text-xs"> - {ethers.utils.formatEther(txData.value)} Ether - </span> - </InfoRow> - <InfoRow title="Transaction Fee"> - <FormattedBalance value={txData.fee} /> Ether - </InfoRow> - <InfoRow title="Gas Price"> - <div className="flex items-baseline space-x-1"> - <span> - <FormattedBalance value={txData.gasPrice} /> Ether ( - <FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei) - </span> - {sendsEthToMiner && ( - <span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1"> - Flashbots + <InfoRow title="Status"> + {txData.status ? ( + <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> + <FontAwesomeIcon icon={faCheckCircle} size="1x" /> + <span>Success</span> + </span> + ) : ( + <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-red-50 text-red-500 text-xs"> + <FontAwesomeIcon icon={faTimesCircle} size="1x" /> + <span>Fail</span> </span> )} - </div> - </InfoRow> - <InfoRow title="Ether Price">N/A</InfoRow> - <InfoRow title="Gas Limit"> - <GasValue value={txData.gasLimit} /> - </InfoRow> - <InfoRow title="Gas Used by Transaction"> - <GasValue value={txData.gasUsed} /> ( - {(txData.gasUsedPerc * 100).toFixed(2)}%) - </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" - value={txData.data} - readOnly - /> - </InfoRow> - </ContentFrame> -); + </InfoRow> + <InfoRow title="Block"> + <div className="flex items-baseline space-x-2"> + <BlockLink blockTag={txData.blockNumber} /> + <BlockConfirmations confirmations={txData.confirmations} /> + </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} /> + </div> + </InfoRow> + <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}> + {txData.to ? ( + <div className="flex items-baseline space-x-2 -ml-1"> + <AddressHighlighter address={txData.to}> + <DecoratedAddressLink + address={txData.to} + miner={txData.to === txData.miner} + txTo + /> + </AddressHighlighter> + <Copy value={txData.to} /> + </div> + ) : ( + <div className="flex items-baseline space-x-2 -ml-1"> + <AddressHighlighter address={txData.createdContractAddress!}> + <DecoratedAddressLink + address={txData.createdContractAddress!} + creation + txTo + /> + </AddressHighlighter> + <Copy value={txData.createdContractAddress!} /> + </div> + )} + {internalOps && ( + <div className="mt-2 space-y-1"> + {internalOps.map((op, i) => ( + <InternalTransactionOperation + key={i} + txData={txData} + internalOp={op} + /> + ))} + </div> + )} + </InfoRow> + <InfoRow title="Transaction Action"> + <MethodName data={txData.data} /> + </InfoRow> + {txData.tokenTransfers.length > 0 && ( + <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> + <div> + {txData.tokenTransfers.map((t, i) => ( + <TokenTransferItem + key={i} + t={t} + txData={txData} + tokenMetas={txData.tokenMetas} + /> + ))} + </div> + </InfoRow> + )} + <InfoRow title="Value"> + <span className="rounded bg-gray-100 px-2 py-1 text-xs"> + {ethers.utils.formatEther(txData.value)} Ether + </span> + </InfoRow> + <InfoRow + title={ + <> + Type ( + <ExternalLink href="https://eips.ethereum.org/EIPS/eip-2718"> + EIP-2718 + </ExternalLink> + ) + </> + } + > + <TransactionType type={txData.type} /> + </InfoRow> + {txData.type === 2 && ( + <> + <InfoRow title="Max Priority Fee Per Gas"> + <span> + <FormattedBalance value={txData.maxPriorityFeePerGas!} /> Ether ( + <FormattedBalance + value={txData.maxPriorityFeePerGas!} + decimals={9} + />{" "} + Gwei) + </span> + </InfoRow> + <InfoRow title="Max Fee Per Gas"> + <span> + <FormattedBalance value={txData.maxFeePerGas!} /> Ether ( + <FormattedBalance + value={txData.maxFeePerGas!} + decimals={9} + />{" "} + Gwei) + </span> + </InfoRow> + </> + )} + <InfoRow title="Gas Price"> + <div className="flex items-baseline space-x-1"> + <span> + <FormattedBalance value={txData.gasPrice} /> Ether ( + <FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei) + </span> + {sendsEthToMiner && ( + <span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1"> + Flashbots + </span> + )} + </div> + </InfoRow> + <InfoRow title="Gas Used/Limit"> + <div className="flex space-x-3 items-baseline"> + <div> + <GasValue value={txData.gasUsed} /> /{" "} + <GasValue value={txData.gasLimit} /> + </div> + <PercentageBar + perc={ + Math.round( + (txData.gasUsed.toNumber() / txData.gasLimit.toNumber()) * 10000 + ) / 100 + } + /> + </div> + </InfoRow> + {hasEIP1559 && ( + <InfoRow title="Block Base Fee"> + <span> + <FormattedBalance value={txData.blockBaseFeePerGas!} decimals={9} />{" "} + Gwei ( + <FormattedBalance + value={txData.blockBaseFeePerGas!} + decimals={0} + />{" "} + wei) + </span> + </InfoRow> + )} + <InfoRow title="Transaction Fee"> + <div className="space-y-3"> + <div> + <FormattedBalance value={txData.fee} /> Ether + </div> + {hasEIP1559 && <RewardSplit txData={txData} />} + </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" + value={txData.data} + readOnly + /> + </InfoRow> + </ContentFrame> + ); +}; export default React.memo(Details); diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index f965ecc..b81fd9d 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -21,7 +21,7 @@ const Logs: React.FC<LogsProps> = ({ txData }) => ( <div className="w-full space-y-2"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="font-bold text-right">Address</div> - <div className="col-span-11"> + <div className="col-span-11 mr-auto"> <DecoratedAddressLink address={l.address} miner={l.address === txData.miner} diff --git a/src/transaction/RewardSplit.tsx b/src/transaction/RewardSplit.tsx new file mode 100644 index 0000000..9005d12 --- /dev/null +++ b/src/transaction/RewardSplit.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBurn, faCoins } from "@fortawesome/free-solid-svg-icons"; +import FormattedBalance from "../components/FormattedBalance"; +import { TransactionData } from "../types"; +import PercentageGauge from "../components/PercentageGauge"; + +type RewardSplitProps = { + txData: TransactionData; +}; + +const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => { + const burntFees = txData.blockBaseFeePerGas!.mul(txData.gasUsed); + const minerReward = txData.gasPrice.mul(txData.gasUsed).sub(burntFees); + const burntPerc = + burntFees.mul(10000).div(txData.gasPrice.mul(txData.gasUsed)).toNumber() / + 100; + + return ( + <div className="inline-block"> + <div className="grid grid-cols-2 gap-x-2 gap-y-1 items-center text-sm"> + <PercentageGauge + perc={burntPerc} + bgFull="bg-orange-100" + bgPerc="bg-orange-500" + textPerc="text-orange-800" + /> + <div className="flex items-baseline space-x-1"> + <span className="flex space-x-1 text-orange-500"> + <span title="Burnt fees"> + <FontAwesomeIcon icon={faBurn} size="1x" /> + </span> + <span> + <span className="line-through"> + <FormattedBalance value={burntFees} /> + </span>{" "} + Ether + </span> + </span> + </div> + <PercentageGauge + perc={100 - burntPerc} + bgFull="bg-yellow-100" + bgPerc="bg-yellow-300" + textPerc="text-yellow-700" + /> + <div className="flex items-baseline space-x-1"> + <span className="flex space-x-1"> + <span className="text-yellow-300" title="Miner fees"> + <FontAwesomeIcon icon={faCoins} size="1x" /> + </span> + <span> + <FormattedBalance value={minerReward} /> Ether + </span> + </span> + </div> + </div> + </div> + ); +}; + +export default React.memo(RewardSplit); diff --git a/src/types.ts b/src/types.ts index ac53edd..482828e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,11 +49,14 @@ export type TransactionData = { value: BigNumber; tokenTransfers: TokenTransfer[]; tokenMetas: TokenMetas; + type: number; + maxFeePerGas?: BigNumber | undefined; + maxPriorityFeePerGas?: BigNumber | undefined; fee: BigNumber; + blockBaseFeePerGas?: BigNumber | undefined | null; gasPrice: BigNumber; - gasLimit: BigNumber; gasUsed: BigNumber; - gasUsedPerc: number; + gasLimit: BigNumber; nonce: number; data: string; logs: ethers.providers.Log[]; diff --git a/src/url.ts b/src/url.ts index 0586dbe..3807d69 100644 --- a/src/url.ts +++ b/src/url.ts @@ -1,3 +1,5 @@ +import { ethers } from "ethers"; + export const fourBytesURL = ( assetsURLPrefix: string, fourBytes: string @@ -7,3 +9,9 @@ export const tokenLogoURL = ( assetsURLPrefix: string, address: string ): string => `${assetsURLPrefix}/assets/${address}/logo.png`; + +export const blockURL = (blockNum: ethers.providers.BlockTag) => + `/block/${blockNum}`; + +export const blockTxsURL = (blockNum: ethers.providers.BlockTag) => + `/block/${blockNum}/txs`; diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index 50d4bf3..d11d2f5 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -1,8 +1,81 @@ -import { ethers } from "ethers"; +import { ethers, BigNumber } from "ethers"; import { useState, useEffect } from "react"; import { getInternalOperations } from "./nodeFunctions"; import { TransactionData, InternalOperation } from "./types"; +export interface ExtendedBlock extends ethers.providers.Block { + blockReward: BigNumber; + unclesReward: BigNumber; + feeReward: BigNumber; + size: number; + sha3Uncles: string; + stateRoot: string; + totalDifficulty: BigNumber; +} + +export const readBlock = async ( + provider: ethers.providers.JsonRpcProvider, + blockNumberOrHash: string +) => { + let blockPromise: Promise<any>; + if (ethers.utils.isHexString(blockNumberOrHash, 32)) { + blockPromise = provider.send("eth_getBlockByHash", [ + blockNumberOrHash, + false, + ]); + } else { + blockPromise = provider.send("eth_getBlockByNumber", [ + blockNumberOrHash, + false, + ]); + } + 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 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), + ..._block, + }; + return extBlock; +}; + +export const useBlockData = ( + provider: ethers.providers.JsonRpcProvider | undefined, + blockNumberOrHash: string +) => { + const [block, setBlock] = useState<ExtendedBlock>(); + useEffect(() => { + if (!provider) { + return; + } + + const _readBlock = async () => { + const extBlock = await readBlock(provider, blockNumberOrHash); + setBlock(extBlock); + }; + _readBlock(); + }, [provider, blockNumberOrHash]); + + return block; +}; + export const useInternalOperations = ( provider: ethers.providers.JsonRpcProvider | undefined, txData: TransactionData | undefined diff --git a/src/useProvider.ts b/src/useProvider.ts index 9ce1445..c6a3a5c 100644 --- a/src/useProvider.ts +++ b/src/useProvider.ts @@ -33,10 +33,7 @@ export const useProvider = ( setConnStatus(ConnectionStatus.CONNECTING); const tryToConnect = async () => { - const provider = new ethers.providers.JsonRpcProvider( - erigonURL, - "mainnet" - ); + const provider = new ethers.providers.JsonRpcProvider(erigonURL); // Check if it is at least a regular ETH node let blockNumber: number = 0; diff --git a/trustwallet b/trustwallet index 011ccb8..5b803d3 160000 --- a/trustwallet +++ b/trustwallet @@ -1 +1 @@ -Subproject commit 011ccb88adc70c27b0d4fc0a10564199efd0a81f +Subproject commit 5b803d37011ff89b287630a9ce90f96f8ed3311f