diff --git a/.gitignore b/.gitignore index 4d29575..349cafa 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +/.vscode diff --git a/package-lock.json b/package-lock.json index d3f7ea8..a4e34a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@blackbox-vision/react-qr-reader": "^5.0.0", "@chainlink/contracts": "^0.2.2", - "@craco/craco": "^6.4.0", + "@craco/craco": "^6.4.2", "@fontsource/fira-code": "^4.5.2", "@fontsource/roboto": "^4.5.1", "@fontsource/roboto-mono": "^4.5.0", @@ -22,33 +22,31 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "@headlessui/react": "^1.4.2", - "@testing-library/jest-dom": "^5.15.0", + "@testing-library/jest-dom": "^5.15.1", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", "@types/node": "^14.17.5", - "@types/react": "^17.0.35", + "@types/react": "^17.0.37", "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.11", "@types/react-highlight": "^0.12.5", - "@types/react-router-dom": "^5.3.2", "@types/react-syntax-highlighter": "^13.5.2", - "chart.js": "^3.5.1", + "chart.js": "^3.6.0", "ethers": "^5.5.1", "highlightjs-solidity": "^2.0.2", - "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", - "react-chartjs-2": "^3.3.0", + "react-chartjs-2": "^4.0.0", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.4", "react-helmet-async": "^1.1.2", "react-image": "^4.0.3", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.0.2", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.4.5", "serve": "^13.0.2", - "typescript": "^4.4.4", + "typescript": "^4.5.2", "use-keyboard-shortcut": "^1.0.6", "web-vitals": "^1.0.1" }, @@ -1249,11 +1247,10 @@ } }, "node_modules/@craco/craco": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.0.tgz", - "integrity": "sha512-puLp+pSL5B2tpoIPUYlWjKd0VDBPNF16BJIKEKrwg0x/9XC/4h8XPcVGNr6pd27pj8sahiH5QUdoBxB5AE9++g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz", + "integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==", "dependencies": { - "@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2", "cosmiconfig": "^7.0.1", "cross-spawn": "^7.0.0", "lodash": "^4.17.15", @@ -1270,23 +1267,6 @@ "react-scripts": "^4.0.0" } }, - "node_modules/@craco/craco/node_modules/@endemolshinegroup/cosmiconfig-typescript-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", - "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", - "dependencies": { - "lodash.get": "^4", - "make-error": "^1", - "ts-node": "^9", - "tslib": "^2" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "cosmiconfig": ">=6" - } - }, "node_modules/@craco/craco/node_modules/cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -2887,9 +2867,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.15.0.tgz", - "integrity": "sha512-lOMuQidnL1tWHLEWIhL6UvSZC1Qt3OkNe1khvi2h6xFiqpe5O8arYs46OU0qyUGq0cSTbroQyMktYNXu3a7sAA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.15.1.tgz", + "integrity": "sha512-kmj8opVDRE1E4GXyLlESsQthCXK7An28dFWxhiMwD7ZUI7ZxA6sjdJRxLerD9Jd8cHX4BDc1jzXaaZKqzlUkvg==", "dependencies": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -3053,11 +3033,6 @@ "@types/unist": "*" } }, - "node_modules/@types/history": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", - "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==" - }, "node_modules/@types/html-minifier-terser": { "version": "5.1.1", "license": "MIT" @@ -3127,9 +3102,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz", - "integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==", + "version": "17.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", + "integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3160,25 +3135,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-router": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz", - "integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==", - "dependencies": { - "@types/history": "*", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.2.tgz", - "integrity": "sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ==", - "dependencies": { - "@types/history": "*", - "@types/react": "*", - "@types/react-router": "*" - } - }, "node_modules/@types/react-syntax-highlighter": { "version": "13.5.2", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz", @@ -5602,9 +5558,9 @@ } }, "node_modules/chart.js": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", - "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==" + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.0.tgz", + "integrity": "sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==" }, "node_modules/check-types": { "version": "11.1.2", @@ -6207,7 +6163,9 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -6945,6 +6903,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -8559,14 +8519,6 @@ "node": ">=0.10.0" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.1.2", "license": "MIT", @@ -9288,16 +9240,11 @@ "integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ==" }, "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz", + "integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==", "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "node_modules/hmac-drbg": { @@ -9309,19 +9256,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/hoopy": { "version": "0.1.4", "license": "MIT", @@ -11361,11 +11295,6 @@ "version": "3.0.0", "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "license": "MIT" @@ -11493,7 +11422,9 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "optional": true, + "peer": true }, "node_modules/makeerror": { "version": "1.0.11", @@ -11674,19 +11605,6 @@ "node": ">=4" } }, - "node_modules/mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - }, - "peerDependencies": { - "prop-types": "^15.0.0", - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/mini-css-extract-plugin": { "version": "0.11.3", "license": "MIT", @@ -14223,23 +14141,6 @@ "node": ">=0.6" } }, - "node_modules/query-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", - "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", - "dependencies": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/querystring": { "version": "0.2.0", "engines": { @@ -14388,9 +14289,9 @@ } }, "node_modules/react-chartjs-2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.3.0.tgz", - "integrity": "sha512-4Mt0SR2aiUbWi/4762odRBYSnbNKSs4HWc0o3IW43py5bMfmfpeZU95w6mbvtuLZH/M3GsPJMU8DvDc+5U9blQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.0.0.tgz", + "integrity": "sha512-0kx41EVO6wIoeU6zvdwovX9kKcdrs7O62DGTSNmwAXZeLGJ3U+n4XijO1kxcMmAi4I6PQJWGD5oRwxVixHSp6g==", "peerDependencies": { "chart.js": "^3.5.0", "react": "^16.8.0 || ^17.0.0" @@ -14594,60 +14495,29 @@ } }, "node_modules/react-router": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", - "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz", + "integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.1.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", - "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz", + "integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.1.0", + "react-router": "6.0.2" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/react-router/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/react-router/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/react-router/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-scripts": { "version": "4.0.3", "license": "MIT", @@ -15430,11 +15300,6 @@ "node": ">=4" } }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "node_modules/resolve-url": { "version": "0.2.1", "license": "MIT" @@ -16724,14 +16589,6 @@ "node": ">= 6" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "engines": { - "node": ">=6" - } - }, "node_modules/split-string": { "version": "3.1.0", "license": "MIT", @@ -16869,14 +16726,6 @@ "version": "1.0.1", "license": "MIT" }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", @@ -17854,16 +17703,6 @@ "version": "0.3.0", "license": "MIT" }, - "node_modules/tiny-invariant": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", - "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -18016,6 +17855,8 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "optional": true, + "peer": true, "dependencies": { "arg": "^4.1.0", "create-require": "^1.1.0", @@ -18040,7 +17881,9 @@ "node_modules/ts-node/node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true, + "peer": true }, "node_modules/ts-pnp": { "version": "1.2.0", @@ -18154,9 +17997,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18477,11 +18320,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -19603,6 +19441,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -20448,11 +20288,10 @@ } }, "@craco/craco": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.0.tgz", - "integrity": "sha512-puLp+pSL5B2tpoIPUYlWjKd0VDBPNF16BJIKEKrwg0x/9XC/4h8XPcVGNr6pd27pj8sahiH5QUdoBxB5AE9++g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz", + "integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==", "requires": { - "@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2", "cosmiconfig": "^7.0.1", "cross-spawn": "^7.0.0", "lodash": "^4.17.15", @@ -20460,17 +20299,6 @@ "webpack-merge": "^4.2.2" }, "dependencies": { - "@endemolshinegroup/cosmiconfig-typescript-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", - "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", - "requires": { - "lodash.get": "^4", - "make-error": "^1", - "ts-node": "^9", - "tslib": "^2" - } - }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -21420,9 +21248,9 @@ } }, "@testing-library/jest-dom": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.15.0.tgz", - "integrity": "sha512-lOMuQidnL1tWHLEWIhL6UvSZC1Qt3OkNe1khvi2h6xFiqpe5O8arYs46OU0qyUGq0cSTbroQyMktYNXu3a7sAA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.15.1.tgz", + "integrity": "sha512-kmj8opVDRE1E4GXyLlESsQthCXK7An28dFWxhiMwD7ZUI7ZxA6sjdJRxLerD9Jd8cHX4BDc1jzXaaZKqzlUkvg==", "requires": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -21545,11 +21373,6 @@ "@types/unist": "*" } }, - "@types/history": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", - "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==" - }, "@types/html-minifier-terser": { "version": "5.1.1" }, @@ -21607,9 +21430,9 @@ "version": "1.5.4" }, "@types/react": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz", - "integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==", + "version": "17.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", + "integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -21640,25 +21463,6 @@ "@types/react": "*" } }, - "@types/react-router": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz", - "integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==", - "requires": { - "@types/history": "*", - "@types/react": "*" - } - }, - "@types/react-router-dom": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.2.tgz", - "integrity": "sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ==", - "requires": { - "@types/history": "*", - "@types/react": "*", - "@types/react-router": "*" - } - }, "@types/react-syntax-highlighter": { "version": "13.5.2", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz", @@ -23331,9 +23135,9 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" }, "chart.js": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", - "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==" + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.0.tgz", + "integrity": "sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==" }, "check-types": { "version": "11.1.2" @@ -23763,7 +23567,9 @@ "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "optional": true, + "peer": true }, "cross-spawn": { "version": "7.0.3", @@ -24247,7 +24053,9 @@ "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "optional": true, + "peer": true }, "diff-sequences": { "version": "26.6.2" @@ -25321,11 +25129,6 @@ "to-regex-range": "^2.1.0" } }, - "filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=" - }, "finalhandler": { "version": "1.1.2", "requires": { @@ -25790,16 +25593,11 @@ "integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ==" }, "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz", + "integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==", "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "hmac-drbg": { @@ -25810,21 +25608,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } - } - }, "hoopy": { "version": "0.1.4" }, @@ -27143,11 +26926,6 @@ "lodash._reinterpolate": { "version": "3.0.0" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, "lodash.memoize": { "version": "4.1.2" }, @@ -27235,7 +27013,9 @@ "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "optional": true, + "peer": true }, "makeerror": { "version": "1.0.11", @@ -27345,15 +27125,6 @@ "min-indent": { "version": "1.0.1" }, - "mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "requires": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - } - }, "mini-css-extract-plugin": { "version": "0.11.3", "requires": { @@ -29076,17 +28847,6 @@ "qs": { "version": "6.7.0" }, - "query-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", - "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", - "requires": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, "querystring": { "version": "0.2.0" }, @@ -29181,9 +28941,9 @@ } }, "react-chartjs-2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.3.0.tgz", - "integrity": "sha512-4Mt0SR2aiUbWi/4762odRBYSnbNKSs4HWc0o3IW43py5bMfmfpeZU95w6mbvtuLZH/M3GsPJMU8DvDc+5U9blQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.0.0.tgz", + "integrity": "sha512-0kx41EVO6wIoeU6zvdwovX9kKcdrs7O62DGTSNmwAXZeLGJ3U+n4XijO1kxcMmAi4I6PQJWGD5oRwxVixHSp6g==", "requires": {} }, "react-dev-utils": { @@ -29320,54 +29080,20 @@ "version": "0.8.3" }, "react-router": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", - "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz", + "integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } + "history": "^5.1.0" } }, "react-router-dom": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", - "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz", + "integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.1.0", + "react-router": "6.0.2" } }, "react-scripts": { @@ -29904,11 +29630,6 @@ "resolve-from": { "version": "3.0.0" }, - "resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "resolve-url": { "version": "0.2.1" }, @@ -30824,11 +30545,6 @@ } } }, - "split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" - }, "split-string": { "version": "3.1.0", "requires": { @@ -30925,11 +30641,6 @@ "stream-shift": { "version": "1.0.1" }, - "strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" - }, "string_decoder": { "version": "1.3.0", "requires": { @@ -31597,16 +31308,6 @@ "timsort": { "version": "0.3.0" }, - "tiny-invariant": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", - "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" - }, - "tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -31708,6 +31409,8 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "optional": true, + "peer": true, "requires": { "arg": "^4.1.0", "create-require": "^1.1.0", @@ -31720,7 +31423,9 @@ "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true, + "peer": true } } }, @@ -31794,9 +31499,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==" + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4" @@ -32004,11 +31709,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "vary": { "version": "1.1.2" }, @@ -32800,7 +32500,9 @@ "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true, + "peer": true }, "yocto-queue": { "version": "0.1.0" diff --git a/package.json b/package.json index 386cded..5bce984 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@blackbox-vision/react-qr-reader": "^5.0.0", "@chainlink/contracts": "^0.2.2", - "@craco/craco": "^6.4.0", + "@craco/craco": "^6.4.2", "@fontsource/fira-code": "^4.5.2", "@fontsource/roboto": "^4.5.1", "@fontsource/roboto-mono": "^4.5.0", @@ -17,33 +17,31 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "@headlessui/react": "^1.4.2", - "@testing-library/jest-dom": "^5.15.0", + "@testing-library/jest-dom": "^5.15.1", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", "@types/node": "^14.17.5", - "@types/react": "^17.0.35", + "@types/react": "^17.0.37", "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.11", "@types/react-highlight": "^0.12.5", - "@types/react-router-dom": "^5.3.2", "@types/react-syntax-highlighter": "^13.5.2", - "chart.js": "^3.5.1", + "chart.js": "^3.6.0", "ethers": "^5.5.1", "highlightjs-solidity": "^2.0.2", - "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", - "react-chartjs-2": "^3.3.0", + "react-chartjs-2": "^4.0.0", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.4", "react-helmet-async": "^1.1.2", "react-image": "^4.0.3", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.0.2", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.4.5", "serve": "^13.0.2", - "typescript": "^4.4.4", + "typescript": "^4.5.2", "use-keyboard-shortcut": "^1.0.6", "web-vitals": "^1.0.1" }, diff --git a/public/eth-diamond-black.png b/public/eth-diamond-black.png deleted file mode 100644 index 5149878..0000000 Binary files a/public/eth-diamond-black.png and /dev/null differ diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index a1013c8..d7222c0 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -1,15 +1,12 @@ -import React, { useState, useEffect, useMemo, useContext } from "react"; +import React, { useEffect, useContext, useCallback, useMemo } from "react"; import { useParams, - useLocation, - useHistory, - Switch, + useNavigate, + Routes, Route, + useSearchParams, } from "react-router-dom"; -import { BlockTag } from "@ethersproject/abstract-provider"; -import { getAddress, isAddress } from "@ethersproject/address"; import { Tab } from "@headlessui/react"; -import queryString from "query-string"; import Blockies from "react-blockies"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; @@ -17,189 +14,62 @@ import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestion import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import Copy from "./components/Copy"; -import ContentFrame from "./ContentFrame"; import NavTab from "./components/NavTab"; +import SourcifyLogo from "./sourcify/SourcifyLogo"; +import AddressTransactionResults from "./address/AddressTransactionResults"; import Contracts from "./address/Contracts"; -import UndefinedPageControl from "./search/UndefinedPageControl"; -import ResultHeader from "./search/ResultHeader"; -import PendingResults from "./search/PendingResults"; -import TransactionItem from "./search/TransactionItem"; -import { SearchController } from "./search/search"; import { RuntimeContext } from "./useRuntime"; -import { pageCollector, useResolvedAddresses } from "./useResolvedAddresses"; -import { useFeeToggler } from "./search/useFeeToggler"; -import { SelectionContext, useSelection } from "./useSelection"; -import { useMultipleETHUSDOracle } from "./usePriceOracle"; import { useAppConfigContext } from "./useAppConfig"; -import { useMultipleMetadata } from "./useSourcify"; +import { useAddressOrENSFromURL } from "./useResolvedAddresses"; +import { useMultipleMetadata } from "./sourcify/useSourcify"; import { ChecksummedAddress } from "./types"; -import SourcifyLogo from "./sourcify.svg"; - -type BlockParams = { - addressOrName: string; - direction?: string; -}; - -type PageParams = { - p?: number; -}; +import { useAddressesWithCode } from "./useErigonHooks"; const AddressTransactions: React.FC = () => { const { provider } = useContext(RuntimeContext); - const params = useParams(); - const location = useLocation(); - const history = useHistory(); - const qs = queryString.parse(location.search); - let hash: string | undefined; - if (qs.h) { - hash = qs.h as string; + const { addressOrName, direction } = useParams(); + if (addressOrName === undefined) { + throw new Error("addressOrName couldn't be undefined here"); } - const [checksummedAddress, setChecksummedAddress] = useState(); - const [isENS, setENS] = useState(); - const [error, setError] = useState(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const urlFixer = useCallback( + (address: ChecksummedAddress) => { + navigate( + `/address/${address}${ + direction ? "/" + direction : "" + }?${searchParams.toString()}`, + { replace: true } + ); + }, + [navigate, direction, searchParams] + ); + const [checksummedAddress, isENS, error] = useAddressOrENSFromURL( + addressOrName, + urlFixer + ); - // If it looks like it is an ENS name, try to resolve it useEffect(() => { - if (isAddress(params.addressOrName)) { - setENS(false); - setError(false); - - // Normalize to checksummed address - const _checksummedAddress = getAddress(params.addressOrName); - if (_checksummedAddress !== params.addressOrName) { - // Request came with a non-checksummed address; fix the URL - history.replace( - `/address/${_checksummedAddress}${ - params.direction ? "/" + params.direction : "" - }${location.search}` - ); - } - setChecksummedAddress(_checksummedAddress); - return; + if (isENS || checksummedAddress === undefined) { + document.title = `Address ${addressOrName} | Otterscan`; + } else { + document.title = `Address ${checksummedAddress} | Otterscan`; } + }, [addressOrName, checksummedAddress, isENS]); - if (!provider) { - return; - } - const resolveName = async () => { - const resolvedAddress = await provider.resolveName(params.addressOrName); - if (resolvedAddress !== null) { - setENS(true); - setError(false); - setChecksummedAddress(resolvedAddress); - } else { - setENS(false); - setError(true); - setChecksummedAddress(undefined); - } - }; - resolveName(); - }, [ - provider, - params.addressOrName, - history, - params.direction, - location.search, - ]); - - const [controller, setController] = useState(); - useEffect(() => { - if (!provider || !checksummedAddress) { - return; - } - - const readFirstPage = async () => { - const _controller = await SearchController.firstPage( - provider, - checksummedAddress - ); - setController(_controller); - }; - const readMiddlePage = async (next: boolean) => { - const _controller = await SearchController.middlePage( - provider, - checksummedAddress, - hash!, - next - ); - setController(_controller); - }; - const readLastPage = async () => { - const _controller = await SearchController.lastPage( - provider, - checksummedAddress - ); - setController(_controller); - }; - const prevPage = async () => { - const _controller = await controller!.prevPage(provider, hash!); - setController(_controller); - }; - const nextPage = async () => { - const _controller = await controller!.nextPage(provider, hash!); - setController(_controller); - }; - - // Page load from scratch - if (params.direction === "first" || params.direction === undefined) { - if (!controller?.isFirst || controller.address !== checksummedAddress) { - readFirstPage(); - } - } else if (params.direction === "prev") { - if (controller && controller.address === checksummedAddress) { - prevPage(); - } else { - readMiddlePage(false); - } - } else if (params.direction === "next") { - if (controller && controller.address === checksummedAddress) { - nextPage(); - } else { - readMiddlePage(true); - } - } else if (params.direction === "last") { - if (!controller?.isLast || controller.address !== checksummedAddress) { - readLastPage(); - } - } - }, [provider, checksummedAddress, params.direction, hash, controller]); - - const page = useMemo(() => controller?.getPage(), [controller]); - const addrCollector = useMemo(() => pageCollector(page), [page]); - const resolvedAddresses = useResolvedAddresses(provider, addrCollector); - - const blockTags: BlockTag[] = useMemo(() => { - if (!page) { - return []; - } - return page.map((p) => p.blockNumber); - }, [page]); - const priceMap = useMultipleETHUSDOracle(provider, blockTags); - - document.title = `Address ${params.addressOrName} | Otterscan`; - - const [feeDisplay, feeDisplayToggler] = useFeeToggler(); - - const selectionCtx = useSelection(); - const addresses = useMemo(() => { - const _addresses: ChecksummedAddress[] = []; - if (checksummedAddress) { - _addresses.push(checksummedAddress); - } - if (page) { - for (const t of page) { - if (t.to) { - _addresses.push(t.to); - } - } - } - return _addresses; - }, [checksummedAddress, page]); const { sourcifySource } = useAppConfigContext(); + const checksummedAddressAsArray = useMemo( + () => (checksummedAddress !== undefined ? [checksummedAddress] : []), + [checksummedAddress] + ); + const contractAddresses = useAddressesWithCode( + provider, + checksummedAddressAsArray + ); const metadatas = useMultipleMetadata( undefined, - addresses, + contractAddresses, provider?.network.chainId, sourcifySource ); @@ -212,7 +82,7 @@ const AddressTransactions: React.FC = () => { {error ? ( - "{params.addressOrName}" is not an ETH address or ENS name. + "{addressOrName}" is not an ETH address or ENS name. ) : ( checksummedAddress && ( @@ -231,116 +101,71 @@ const AddressTransactions: React.FC = () => { {isENS && ( - ENS: {params.addressOrName} + ENS: {addressOrName} )} - - Overview - - - - Contract - {addressMetadata === undefined ? ( - - - - ) : addressMetadata === null ? ( - - - - ) : ( - - Sourcify logo - - )} - - + Overview + {(contractAddresses?.length ?? 0) > 0 && ( + + + Contract + {addressMetadata === undefined ? ( + + + + ) : addressMetadata === null ? ( + + + + ) : ( + + + + )} + + + )} - - - -
-
- {page === undefined ? ( - <>Waiting for search results... - ) : ( - <>{page.length} transactions on this page - )} -
- -
- + + } + /> + + } + /> + - {page ? ( - - {page.map((tx) => ( - - ))} -
-
- {page === undefined ? ( - <>Waiting for search results... - ) : ( - <>{page.length} transactions on this page - )} -
- -
-
- ) : ( - - )} -
-
- - - -
+ } + /> +
@@ -350,4 +175,4 @@ const AddressTransactions: React.FC = () => { ); }; -export default React.memo(AddressTransactions); +export default AddressTransactions; diff --git a/src/App.tsx b/src/App.tsx index 110d626..f31fff6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,12 @@ -import React, { Suspense, useMemo, useState } from "react"; -import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import React, { Suspense } from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import WarningHeader from "./WarningHeader"; import Home from "./Home"; -import Search from "./Search"; -import Title from "./Title"; +import Main from "./Main"; import ConnectionErrorPanel from "./ConnectionErrorPanel"; import Footer from "./Footer"; import { ConnectionStatus } from "./types"; import { RuntimeContext, useRuntime } from "./useRuntime"; -import { AppConfig, AppConfigContext } from "./useAppConfig"; -import { SourcifySource } from "./url"; const Block = React.lazy( () => import(/* webpackChunkName: "block", webpackPrefetch: true */ "./Block") @@ -39,15 +36,6 @@ const London = React.lazy( const App = () => { const runtime = useRuntime(); - const [sourcifySource, setSourcifySource] = useState( - SourcifySource.IPFS_IPNS - ); - const appConfig = useMemo((): AppConfig => { - return { - sourcifySource, - setSourcifySource, - }; - }, [sourcifySource, setSourcifySource]); return ( @@ -61,34 +49,23 @@ const App = () => {
- - - + + } /> + } /> + }> + } /> + } + /> + } /> + } + /> + } /> - - - - - - - - - - <Route path="/block/:blockNumberOrHash" exact> - <Block /> - </Route> - <Route path="/block/:blockNumber/txs" exact> - <BlockTransactions /> - </Route> - <Route path="/tx/:txhash"> - <Transaction /> - </Route> - <Route path="/address/:addressOrName/:direction?"> - <AddressTransactions /> - </Route> - </AppConfigContext.Provider> - </Route> - </Switch> + </Routes> </Router> <Footer /> </div> @@ -98,4 +75,4 @@ const App = () => { ); }; -export default React.memo(App); +export default App; diff --git a/src/Block.tsx b/src/Block.tsx index df16643..473ef75 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -26,15 +26,14 @@ import { blockTxsURL } from "./url"; import { useBlockData } from "./useErigonHooks"; import { useETHUSDOracle } from "./usePriceOracle"; -type BlockParams = { - blockNumberOrHash: string; -}; - const Block: React.FC = () => { const { provider } = useContext(RuntimeContext); - const params = useParams<BlockParams>(); + const { blockNumberOrHash } = useParams(); + if (blockNumberOrHash === undefined) { + throw new Error("blockNumberOrHash couldn't be undefined here"); + } - const block = useBlockData(provider, params.blockNumberOrHash); + const block = useBlockData(provider, blockNumberOrHash); useEffect(() => { if (block) { document.title = `Block #${block.number} | Otterscan`; @@ -63,9 +62,7 @@ const Block: React.FC = () => { <StandardSubtitle> <div className="flex space-x-1 items-baseline"> <span>Block</span> - <span className="text-base text-gray-500"> - #{params.blockNumberOrHash} - </span> + <span className="text-base text-gray-500">#{blockNumberOrHash}</span> {block && ( <NavBlock blockNumber={block.number} @@ -192,4 +189,4 @@ const Block: React.FC = () => { ); }; -export default React.memo(Block); +export default Block; diff --git a/src/BlockTransactions.tsx b/src/BlockTransactions.tsx index ed9b2c7..b1fe5af 100644 --- a/src/BlockTransactions.tsx +++ b/src/BlockTransactions.tsx @@ -1,31 +1,24 @@ import React, { useMemo, useContext } from "react"; -import { useParams, useLocation } from "react-router"; +import { useParams } from "react-router"; import { BigNumber } from "@ethersproject/bignumber"; -import queryString from "query-string"; import StandardFrame from "./StandardFrame"; import BlockTransactionHeader from "./block/BlockTransactionHeader"; import BlockTransactionResults from "./block/BlockTransactionResults"; import { PAGE_SIZE } from "./params"; import { RuntimeContext } from "./useRuntime"; import { useBlockTransactions } from "./useErigonHooks"; - -type BlockParams = { - blockNumber: string; -}; - -type PageParams = { - p?: number; -}; +import { useSearchParams } from "react-router-dom"; const BlockTransactions: React.FC = () => { const { provider } = useContext(RuntimeContext); - const params = useParams<BlockParams>(); - const location = useLocation<PageParams>(); - const qs = queryString.parse(location.search); + const params = useParams(); + + const [searchParams] = useSearchParams(); let pageNumber = 1; - if (qs.p) { + const p = searchParams.get("p"); + if (p) { try { - pageNumber = parseInt(qs.p as string); + pageNumber = parseInt(p); } catch (err) {} } @@ -56,4 +49,4 @@ const BlockTransactions: React.FC = () => { ); }; -export default React.memo(BlockTransactions); +export default BlockTransactions; diff --git a/src/Title.tsx b/src/Header.tsx similarity index 73% rename from src/Title.tsx rename to src/Header.tsx index e1e9b8e..251e584 100644 --- a/src/Title.tsx +++ b/src/Header.tsx @@ -1,40 +1,18 @@ -import React, { useState, useRef, useContext } from "react"; -import { Link, useHistory } from "react-router-dom"; +import React, { useState, useContext } from "react"; +import { Link } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode"; -import useKeyboardShortcut from "use-keyboard-shortcut"; import PriceBox from "./PriceBox"; import SourcifyMenu from "./SourcifyMenu"; import { RuntimeContext } from "./useRuntime"; +import { useGenericSearch } from "./search/search"; import Otter from "./otter.jpg"; const CameraScanner = React.lazy(() => import("./search/CameraScanner")); -const Title: React.FC = () => { +const Header: React.FC = () => { const { provider } = useContext(RuntimeContext); - const [search, setSearch] = useState<string>(); - const [canSubmit, setCanSubmit] = useState<boolean>(false); - const history = useHistory(); - - const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { - setCanSubmit(e.target.value.trim().length > 0); - setSearch(e.target.value.trim()); - }; - - const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { - e.preventDefault(); - if (!canSubmit) { - return; - } - - history.push(`/search?q=${search}`); - }; - - const searchRef = useRef<HTMLInputElement>(null); - useKeyboardShortcut(["/"], () => { - searchRef.current?.focus(); - }); - + const [searchRef, handleChange, handleSubmit] = useGenericSearch(); const [isScanning, setScanning] = useState<boolean>(false); return ( @@ -92,4 +70,4 @@ const Title: React.FC = () => { ); }; -export default React.memo(Title); +export default Header; diff --git a/src/Home.tsx b/src/Home.tsx index 103639a..d1d8b49 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -1,5 +1,5 @@ import React, { useState, useContext } from "react"; -import { NavLink, useHistory } from "react-router-dom"; +import { NavLink } from "react-router-dom"; import { commify } from "@ethersproject/units"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; @@ -9,28 +9,13 @@ import Timestamp from "./components/Timestamp"; import { RuntimeContext } from "./useRuntime"; import { useLatestBlock } from "./useLatestBlock"; import { blockURL } from "./url"; +import { useGenericSearch } from "./search/search"; const CameraScanner = React.lazy(() => import("./search/CameraScanner")); const Home: React.FC = () => { const { provider } = useContext(RuntimeContext); - const [search, setSearch] = useState<string>(); - const [canSubmit, setCanSubmit] = useState<boolean>(false); - const history = useHistory(); - - const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { - setCanSubmit(e.target.value.trim().length > 0); - setSearch(e.target.value.trim()); - }; - - const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { - e.preventDefault(); - if (!canSubmit) { - return; - } - - history.push(`/search?q=${search}`); - }; + const [searchRef, handleChange, handleSubmit] = useGenericSearch(); const latestBlock = useLatestBlock(provider); const [isScanning, setScanning] = useState<boolean>(false); @@ -56,6 +41,7 @@ const Home: React.FC = () => { size={50} placeholder="Search by address / txn hash / block number / ENS name" onChange={handleChange} + ref={searchRef} autoFocus /> <button diff --git a/src/Main.tsx b/src/Main.tsx new file mode 100644 index 0000000..1fe30e5 --- /dev/null +++ b/src/Main.tsx @@ -0,0 +1,26 @@ +import React, { useMemo, useState } from "react"; +import { Outlet } from "react-router-dom"; +import Header from "./Header"; +import { AppConfig, AppConfigContext } from "./useAppConfig"; +import { SourcifySource } from "./url"; + +const Main: React.FC = () => { + const [sourcifySource, setSourcifySource] = useState<SourcifySource>( + SourcifySource.IPFS_IPNS + ); + const appConfig = useMemo((): AppConfig => { + return { + sourcifySource, + setSourcifySource, + }; + }, [sourcifySource, setSourcifySource]); + + return ( + <AppConfigContext.Provider value={appConfig}> + <Header /> + <Outlet /> + </AppConfigContext.Provider> + ); +}; + +export default Main; diff --git a/src/Search.tsx b/src/Search.tsx deleted file mode 100644 index 78dc724..0000000 --- a/src/Search.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useLocation, useHistory } from "react-router-dom"; -import { isAddress } from "@ethersproject/address"; -import { isHexString } from "@ethersproject/bytes"; -import queryString from "query-string"; - -type SearchParams = { - q: string; -}; - -const Search: React.FC = () => { - const location = useLocation<SearchParams>(); - const history = useHistory(); - - const qs = queryString.parse(location.search); - const q = (qs.q ?? "").toString(); - if (isAddress(q)) { - history.replace(`/address/${q}`); - return <></>; - } - if (isHexString(q, 32)) { - history.replace(`/tx/${q}`); - return <></>; - } - - const blockNumber = parseInt(q); - if (!isNaN(blockNumber)) { - history.replace(`/block/${blockNumber}`); - return <></>; - } - - // Assume it is an ENS name - history.replace(`/address/${q}`); - return <></>; -}; - -export default Search; diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index c56bc26..9e49ee4 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -11,7 +11,7 @@ import { TokenTransfer, } from "./types"; import { ResolvedAddresses } from "./api/address-resolver"; -import { Metadata } from "./useSourcify"; +import { Metadata } from "./sourcify/useSourcify"; type TokenTransferItemProps = { t: TokenTransfer; @@ -31,8 +31,8 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ <span className="text-gray-500"> <FontAwesomeIcon icon={faCaretRight} size="1x" /> </span> - <div className="grid grid-cols-5 gap-x-1"> - <div className="flex space-x-1"> + <div className="grid grid-cols-7 gap-x-1 w-full"> + <div className="col-span-2 flex space-x-1"> <span className="font-bold">From</span> <TransactionAddress address={t.from} @@ -41,7 +41,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ metadata={metadatas[t.from]} /> </div> - <div className="flex space-x-1"> + <div className="col-span-2 flex space-x-1"> <span className="font-bold">To</span> <TransactionAddress address={t.to} diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 168c83f..9f9fbc6 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useContext } from "react"; -import { Route, Switch, useParams } from "react-router-dom"; +import { useParams, Routes, Route } from "react-router-dom"; import { Tab } from "@headlessui/react"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; @@ -10,7 +10,7 @@ import { SelectionContext, useSelection } from "./useSelection"; import { useInternalOperations, useTxData } from "./useErigonHooks"; import { useETHUSDOracle } from "./usePriceOracle"; import { useAppConfigContext } from "./useAppConfig"; -import { useSourcify, useTransactionDescription } from "./useSourcify"; +import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify"; import { transactionDataCollector, useResolvedAddresses, @@ -37,14 +37,12 @@ const Trace = React.lazy( ) ); -type TransactionParams = { - txhash: string; -}; - const Transaction: React.FC = () => { const { provider } = useContext(RuntimeContext); - const params = useParams<TransactionParams>(); - const { txhash } = params; + const { txhash } = useParams(); + if (txhash === undefined) { + throw new Error("txhash couldn't be undefined here"); + } const txData = useTxData(provider, txhash); const addrCollector = useMemo( @@ -97,44 +95,53 @@ const Transaction: React.FC = () => { <SelectionContext.Provider value={selectionCtx}> <Tab.Group> <Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white"> - <NavTab href={`/tx/${txhash}`}>Overview</NavTab> + <NavTab href=".">Overview</NavTab> {txData.confirmedData?.blockNumber !== undefined && ( - <NavTab href={`/tx/${txhash}/logs`}> + <NavTab href="logs"> Logs {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} </NavTab> )} - <NavTab href={`/tx/${txhash}/trace`}>Trace</NavTab> + <NavTab href="trace">Trace</NavTab> </Tab.List> </Tab.Group> <React.Suspense fallback={null}> - <Switch> - <Route path="/tx/:txhash/" exact> - <Details - txData={txData} - txDesc={txDesc} - userDoc={metadata?.output.userdoc} - devDoc={metadata?.output.devdoc} - internalOps={internalOps} - sendsEthToMiner={sendsEthToMiner} - ethUSDPrice={blockETHUSDPrice} - resolvedAddresses={resolvedAddresses} - /> - </Route> - <Route path="/tx/:txhash/logs/" exact> - <Logs - txData={txData} - metadata={metadata} - resolvedAddresses={resolvedAddresses} - /> - </Route> - <Route path="/tx/:txhash/trace" exact> - <Trace - txData={txData} - resolvedAddresses={resolvedAddresses} - /> - </Route> - </Switch> + <Routes> + <Route + index + element={ + <Details + txData={txData} + txDesc={txDesc} + userDoc={metadata?.output.userdoc} + devDoc={metadata?.output.devdoc} + internalOps={internalOps} + sendsEthToMiner={sendsEthToMiner} + ethUSDPrice={blockETHUSDPrice} + resolvedAddresses={resolvedAddresses} + /> + } + /> + <Route + path="logs" + element={ + <Logs + txData={txData} + metadata={metadata} + resolvedAddresses={resolvedAddresses} + /> + } + /> + <Route + path="trace" + element={ + <Trace + txData={txData} + resolvedAddresses={resolvedAddresses} + /> + } + /> + </Routes> </React.Suspense> </SelectionContext.Provider> )} @@ -143,4 +150,4 @@ const Transaction: React.FC = () => { ); }; -export default React.memo(Transaction); +export default Transaction; diff --git a/src/address/AddressTransactionResults.tsx b/src/address/AddressTransactionResults.tsx new file mode 100644 index 0000000..f1a373e --- /dev/null +++ b/src/address/AddressTransactionResults.tsx @@ -0,0 +1,187 @@ +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { BlockTag } from "@ethersproject/providers"; +import ContentFrame from "../ContentFrame"; +import PendingResults from "../search/PendingResults"; +import ResultHeader from "../search/ResultHeader"; +import { SearchController } from "../search/search"; +import TransactionItem from "../search/TransactionItem"; +import UndefinedPageControl from "../search/UndefinedPageControl"; +import { useFeeToggler } from "../search/useFeeToggler"; +import { SelectionContext, useSelection } from "../useSelection"; +import { useMultipleETHUSDOracle } from "../usePriceOracle"; +import { RuntimeContext } from "../useRuntime"; +import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses"; +import { useParams, useSearchParams } from "react-router-dom"; +import { ChecksummedAddress } from "../types"; +import { useContractsMetadata } from "../hooks"; + +type AddressTransactionResultsProps = { + address: ChecksummedAddress; +}; + +const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({ + address, +}) => { + const { provider } = useContext(RuntimeContext); + const selectionCtx = useSelection(); + const [feeDisplay, feeDisplayToggler] = useFeeToggler(); + + const { addressOrName, direction } = useParams(); + if (addressOrName === undefined) { + throw new Error("addressOrName couldn't be undefined here"); + } + + const [searchParams] = useSearchParams(); + const hash = searchParams.get("h"); + + const [controller, setController] = useState<SearchController>(); + useEffect(() => { + if (!provider || !address) { + return; + } + + const readFirstPage = async () => { + const _controller = await SearchController.firstPage(provider, address); + setController(_controller); + }; + const readMiddlePage = async (next: boolean) => { + const _controller = await SearchController.middlePage( + provider, + address, + hash!, + next + ); + setController(_controller); + }; + const readLastPage = async () => { + const _controller = await SearchController.lastPage(provider, address); + setController(_controller); + }; + const prevPage = async () => { + const _controller = await controller!.prevPage(provider, hash!); + setController(_controller); + }; + const nextPage = async () => { + const _controller = await controller!.nextPage(provider, hash!); + setController(_controller); + }; + + // Page load from scratch + if (direction === "first" || direction === undefined) { + if (!controller?.isFirst || controller.address !== address) { + readFirstPage(); + } + } else if (direction === "prev") { + if (controller && controller.address === address) { + prevPage(); + } else { + readMiddlePage(false); + } + } else if (direction === "next") { + if (controller && controller.address === address) { + nextPage(); + } else { + readMiddlePage(true); + } + } else if (direction === "last") { + if (!controller?.isLast || controller.address !== address) { + readLastPage(); + } + } + }, [provider, address, direction, hash, controller]); + + const page = useMemo(() => controller?.getPage(), [controller]); + + // Extract block number from all txs on current page + // TODO: dedup blockTags + const blockTags: BlockTag[] = useMemo(() => { + if (!page) { + return []; + } + return page.map((t) => t.blockNumber); + }, [page]); + const priceMap = useMultipleETHUSDOracle(provider, blockTags); + + // Resolve all addresses that appear on this page results + const addrCollector = useMemo(() => pageCollector(page), [page]); + const resolvedAddresses = useResolvedAddresses(provider, addrCollector); + + // Calculate Sourcify metadata for all addresses that appear on this page results + const addresses = useMemo(() => { + const _addresses = [address]; + if (page) { + for (const t of page) { + if (t.to) { + _addresses.push(t.to); + } + if (t.createdContractAddress) { + _addresses.push(t.createdContractAddress); + } + } + } + return _addresses; + }, [address, page]); + const metadatas = useContractsMetadata(addresses, provider); + + return ( + <ContentFrame tabs> + <div className="flex justify-between items-baseline py-3"> + <div className="text-sm text-gray-500"> + {page === undefined ? ( + <>Waiting for search results...</> + ) : ( + <>{page.length} transactions on this page</> + )} + </div> + <UndefinedPageControl + address={address} + isFirst={controller?.isFirst} + isLast={controller?.isLast} + prevHash={page ? page[0].hash : ""} + nextHash={page ? page[page.length - 1].hash : ""} + disabled={controller === undefined} + /> + </div> + <ResultHeader + feeDisplay={feeDisplay} + feeDisplayToggler={feeDisplayToggler} + /> + {page ? ( + <SelectionContext.Provider value={selectionCtx}> + {page.map((tx) => ( + <TransactionItem + key={tx.hash} + tx={tx} + resolvedAddresses={resolvedAddresses} + selectedAddress={address} + feeDisplay={feeDisplay} + priceMap={priceMap} + metadatas={metadatas} + /> + ))} + <div className="flex justify-between items-baseline py-3"> + <div className="text-sm text-gray-500"> + {page === undefined ? ( + <>Waiting for search results...</> + ) : ( + <>{page.length} transactions on this page</> + )} + </div> + <UndefinedPageControl + address={address} + isFirst={controller?.isFirst} + isLast={controller?.isLast} + prevHash={page ? page[0].hash : ""} + nextHash={page ? page[page.length - 1].hash : ""} + disabled={controller === undefined} + /> + </div> + </SelectionContext.Provider> + ) : ( + <PendingResults /> + )} + </ContentFrame> + ); +}; + +export default AddressTransactionResults; diff --git a/src/address/Contract.tsx b/src/address/Contract.tsx index 5432f51..360e8bc 100644 --- a/src/address/Contract.tsx +++ b/src/address/Contract.tsx @@ -1,6 +1,6 @@ import React from "react"; import { SyntaxHighlighter, docco } from "../highlight-init"; -import { useContract } from "../useSourcify"; +import { useContract } from "../sourcify/useSourcify"; import { useAppConfigContext } from "../useAppConfig"; type ContractProps = { diff --git a/src/address/Contracts.tsx b/src/address/Contracts.tsx index dbaaf83..c537063 100644 --- a/src/address/Contracts.tsx +++ b/src/address/Contracts.tsx @@ -7,7 +7,7 @@ import ContentFrame from "../ContentFrame"; import InfoRow from "../components/InfoRow"; import Contract from "./Contract"; import { RuntimeContext } from "../useRuntime"; -import { Metadata } from "../useSourcify"; +import { Metadata } from "../sourcify/useSourcify"; import ExternalLink from "../components/ExternalLink"; import { openInRemixURL } from "../url"; import ContractABI from "./ContractABI"; diff --git a/src/api/address-resolver/ERCTokenResolver.ts b/src/api/address-resolver/ERCTokenResolver.ts index f087ec6..49301bd 100644 --- a/src/api/address-resolver/ERCTokenResolver.ts +++ b/src/api/address-resolver/ERCTokenResolver.ts @@ -1,24 +1,31 @@ import { BaseProvider } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; +import { Interface } from "@ethersproject/abi"; import { IAddressResolver } from "./address-resolver"; import erc20 from "../../erc20.json"; import { TokenMeta } from "../../types"; +const erc20Interface = new Interface(erc20); + export class ERCTokenResolver implements IAddressResolver<TokenMeta> { async resolveAddress( provider: BaseProvider, address: string ): Promise<TokenMeta | undefined> { - const erc20Contract = new Contract(address, erc20, provider); + const erc20Contract = new Contract(address, erc20Interface, provider); try { - const [name, symbol, decimals] = (await Promise.all([ - erc20Contract.name(), + const name = (await erc20Contract.name()) as string; + if (!name.trim()) { + return undefined; + } + + const [symbol, decimals] = (await Promise.all([ erc20Contract.symbol(), erc20Contract.decimals(), - ])) as [string, string, number]; + ])) as [string, number]; // Prevent faulty tokens with empty name/symbol - if (!name.trim() || !symbol.trim()) { + if (!symbol.trim()) { return undefined; } diff --git a/src/api/address-resolver/UniswapV1Resolver.ts b/src/api/address-resolver/UniswapV1Resolver.ts new file mode 100644 index 0000000..a8cb2e0 --- /dev/null +++ b/src/api/address-resolver/UniswapV1Resolver.ts @@ -0,0 +1,60 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { Contract } from "@ethersproject/contracts"; +import { IAddressResolver } from "./address-resolver"; +import { ChecksummedAddress, TokenMeta } from "../../types"; +import { ERCTokenResolver } from "./ERCTokenResolver"; + +const UNISWAP_V1_FACTORY = "0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95"; + +const UNISWAP_V1_FACTORY_ABI = [ + "function getToken(address exchange) external view returns (address token)", +]; + +const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; + +export type UniswapV1TokenMeta = { + address: ChecksummedAddress; +} & TokenMeta; + +export type UniswapV1PairMeta = { + exchange: ChecksummedAddress; + token: UniswapV1TokenMeta; +}; + +const ercResolver = new ERCTokenResolver(); + +export class UniswapV1Resolver implements IAddressResolver<UniswapV1PairMeta> { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise<UniswapV1PairMeta | undefined> { + const factoryContract = new Contract( + UNISWAP_V1_FACTORY, + UNISWAP_V1_FACTORY_ABI, + provider + ); + + try { + // First, probe the getToken() function; if it responds with an UniswapV1 exchange + // address, it is a LP + const token = (await factoryContract.getToken(address)) as string; + if (token === NULL_ADDRESS) { + return undefined; + } + + const metadata = await ercResolver.resolveAddress(provider, token); + if (metadata === undefined) { + return undefined; + } + + return { + exchange: address, + token: { address: token, ...metadata }, + }; + } catch (err) { + // Ignore on purpose; this indicates the probe failed and the address + // is not a token + } + return undefined; + } +} diff --git a/src/api/address-resolver/UniswapV2Resolver.ts b/src/api/address-resolver/UniswapV2Resolver.ts new file mode 100644 index 0000000..761237c --- /dev/null +++ b/src/api/address-resolver/UniswapV2Resolver.ts @@ -0,0 +1,82 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { Contract } from "@ethersproject/contracts"; +import { IAddressResolver } from "./address-resolver"; +import { ChecksummedAddress, TokenMeta } from "../../types"; +import { ERCTokenResolver } from "./ERCTokenResolver"; + +const UNISWAP_V2_FACTORY = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; + +const UNISWAP_V2_FACTORY_ABI = [ + "function getPair(address tokenA, address tokenB) external view returns (address pair)", +]; + +const UNISWAP_V2_PAIR_ABI = [ + "function factory() external view returns (address)", + "function token0() external view returns (address)", + "function token1() external view returns (address)", +]; + +export type UniswapV2TokenMeta = { + address: ChecksummedAddress; +} & TokenMeta; + +export type UniswapV2PairMeta = { + pair: ChecksummedAddress; + token0: UniswapV2TokenMeta; + token1: UniswapV2TokenMeta; +}; + +const ercResolver = new ERCTokenResolver(); + +export class UniswapV2Resolver implements IAddressResolver<UniswapV2PairMeta> { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise<UniswapV2PairMeta | undefined> { + const pairContract = new Contract(address, UNISWAP_V2_PAIR_ABI, provider); + const factoryContract = new Contract( + UNISWAP_V2_FACTORY, + UNISWAP_V2_FACTORY_ABI, + provider + ); + + try { + // First, probe the factory() function; if it responds with UniswapV2 factory + // address, it may be a pair + const factoryAddress = (await pairContract.factory()) as string; + if (factoryAddress !== UNISWAP_V2_FACTORY) { + return undefined; + } + + // Probe the token0/token1 + const [token0, token1] = await Promise.all([ + pairContract.token0() as string, + pairContract.token1() as string, + ]); + + // Probe the factory to ensure it is a legit pair + const expectedPairAddress = await factoryContract.getPair(token0, token1); + if (expectedPairAddress !== address) { + return undefined; + } + + const [meta0, meta1] = await Promise.all([ + ercResolver.resolveAddress(provider, token0), + ercResolver.resolveAddress(provider, token1), + ]); + if (meta0 === undefined || meta1 === undefined) { + return undefined; + } + + return { + pair: address, + token0: { address: token0, ...meta0 }, + token1: { address: token1, ...meta1 }, + }; + } catch (err) { + // Ignore on purpose; this indicates the probe failed and the address + // is not a token + } + return undefined; + } +} diff --git a/src/api/address-resolver/UniswapV3Resolver.ts b/src/api/address-resolver/UniswapV3Resolver.ts new file mode 100644 index 0000000..835e823 --- /dev/null +++ b/src/api/address-resolver/UniswapV3Resolver.ts @@ -0,0 +1,90 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { Contract } from "@ethersproject/contracts"; +import { IAddressResolver } from "./address-resolver"; +import { ChecksummedAddress, TokenMeta } from "../../types"; +import { ERCTokenResolver } from "./ERCTokenResolver"; + +const UNISWAP_V3_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; + +const UNISWAP_V3_FACTORY_ABI = [ + "function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)", +]; + +const UNISWAP_V3_PAIR_ABI = [ + "function factory() external view returns (address)", + "function token0() external view returns (address)", + "function token1() external view returns (address)", + "function fee() external view returns (uint24)", +]; + +export type UniswapV3TokenMeta = { + address: ChecksummedAddress; +} & TokenMeta; + +export type UniswapV3PairMeta = { + pair: ChecksummedAddress; + token0: UniswapV3TokenMeta; + token1: UniswapV3TokenMeta; + fee: number; +}; + +const ercResolver = new ERCTokenResolver(); + +export class UniswapV3Resolver implements IAddressResolver<UniswapV3PairMeta> { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise<UniswapV3PairMeta | undefined> { + const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider); + const factoryContract = new Contract( + UNISWAP_V3_FACTORY, + UNISWAP_V3_FACTORY_ABI, + provider + ); + + try { + // First, probe the factory() function; if it responds with UniswapV2 factory + // address, it may be a pair + const factoryAddress = (await poolContract.factory()) as string; + if (factoryAddress !== UNISWAP_V3_FACTORY) { + return undefined; + } + + // Probe the token0/token1/fee + const [token0, token1, fee] = await Promise.all([ + poolContract.token0() as string, + poolContract.token1() as string, + poolContract.fee() as number, + ]); + + // Probe the factory to ensure it is a legit pair + const expectedPoolAddress = await factoryContract.getPool( + token0, + token1, + fee + ); + if (expectedPoolAddress !== address) { + return undefined; + } + + const [meta0, meta1] = await Promise.all([ + ercResolver.resolveAddress(provider, token0), + ercResolver.resolveAddress(provider, token1), + ]); + if (meta0 === undefined || meta1 === undefined) { + return undefined; + } + + return { + pair: address, + token0: { address: token0, ...meta0 }, + token1: { address: token1, ...meta1 }, + fee, + }; + } catch (err) { + // Ignore on purpose; this indicates the probe failed and the address + // is not a token + } + return undefined; + } +} diff --git a/src/api/address-resolver/hardcoded-addresses/1.json b/src/api/address-resolver/hardcoded-addresses/1.json index 072a2c0..0164374 100644 --- a/src/api/address-resolver/hardcoded-addresses/1.json +++ b/src/api/address-resolver/hardcoded-addresses/1.json @@ -1,13 +1,51 @@ { + "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e": "ENS: Registry", + "0xFaC7BEA255a6990f749363002136aF6556b31e04": "ENS: Old .eth Registrar", + "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85": "ENS: .eth Registrar", + "0x6109DD117AA5486605FC85e040ab00163a75c662": "ENS: Migration Contract", + "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41": "ENS: Public Resolver", + "0x00000000219ab540356cBB839Cbe05303d7705Fa": "ETH2: Deposit Contract", "0x7d655c57f71464B6f83811C55D84009Cd9f5221C": "Gitcoin: Bulk Checkout", "0xf2354570bE2fB420832Fb7Ff6ff0AE0dF80CF2c6": "Gitcoin: GR8 Matching Payout", "0x3342E3737732D879743f2682A3953a730ae4F47C": "Gitcoin: GR9 Matching Payout", "0x3ebAFfe01513164e638480404c651E885cCA0AA4": "Gitcoin: GR10 Matching Payout", "0x0EbD2E2130b73107d0C45fF2E16c93E7e2e10e3a": "Gitcoin: GR11 Matching Payout", - "0x722122dF12D4e14e13Ac3b6895a86e84145b6967": "Tornado Cash: Proxy", "0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95": "Uniswap V1: Factory", "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f": "Uniswap V2: Factory", "0xf164fC0Ec4E93095b804a4795bBe1e041497b92a": "Uniswap V2: Router 1", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D": "Uniswap V2: Router 2", - "0x1F98431c8aD98523631AE4a59f267346ea31F984": "Uniswap V3: Router" + "0x1F98431c8aD98523631AE4a59f267346ea31F984": "Uniswap V3: Factory", + "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696": "Uniswap V3: Multicall2", + "0xB753548F6E010e7e680BA186F9Ca1BdAB2E90cf2": "Uniswap V3: ProxyAdmin", + "0xbfd8137f7d1516D3ea5cA83523914859ec47F573": "Uniswap V3: TickLens", + "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6": "Uniswap V3: Quoter", + "0xE592427A0AEce92De3Edee1F18E0157C05861564": "Uniswap V3: Router", + "0x42B24A95702b9986e82d421cC3568932790A48Ec": "Uniswap V3: NFT Descriptor", + "0x91ae842A5Ffd8d12023116943e72A606179294f3": "Uniswap V3: NFT Position Descriptor", + "0xEe6A57eC80ea46401049E92587E52f5Ec1c24785": "Uniswap V3: Transparent Upgradeable Proxy", + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88": "Uniswap V3: Nonfungible Position Manager", + "0xA5644E29708357803b5A882D272c41cC0dF92B34": "Uniswap V3: V3 Migrator", + "0x722122dF12D4e14e13Ac3b6895a86e84145b6967": "Tornado Cash: Proxy", + "0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc": "Tornado Cash: 0.1 ETH", + "0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936": "Tornado Cash: 1 ETH", + "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF": "Tornado Cash: 10 ETH", + "0xA160cdAB225685dA1d56aa342Ad8841c3b53f291": "Tornado Cash: 100 ETH", + "0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3": "Tornado Cash: 100 DAI", + "0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144": "Tornado Cash: 1K DAI", + "0x07687e702b410Fa43f4cB4Af7FA097918ffD2730": "Tornado Cash: 10K DAI", + "0x23773E65ed146A459791799d01336DB287f25334": "Tornado Cash: 100K DAI", + "0x22aaA7720ddd5388A3c0A3333430953C68f1849b": "Tornado Cash: 5K cDAI", + "0x03893a7c7463AE47D46bc7f091665f1893656003": "Tornado Cash: 50K cDAI", + "0x2717c5e28cf931547B621a5dddb772Ab6A35B701": "Tornado Cash: 500K cDAI", + "0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af": "Tornado Cash: 5M cDAI", + "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307": "Tornado Cash: 100 USDC", + "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D": "Tornado Cash: 1K USDC", + "0x169AD27A470D064DEDE56a2D3ff727986b15D52B": "Tornado Cash: 100 USDT", + "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f": "Tornado Cash: 1K USDT", + "0x178169B423a011fff22B9e3F3abeA13414dDD0F1": "Tornado Cash: 0.1 WBTC", + "0x610B717796ad172B316836AC95a2ffad065CeaB4": "Tornado Cash: 1 WBTC", + "0xbB93e510BbCD0B7beb5A853875f9eC60275CF498": "Tornado Cash: 10 WBTC", + "0x94A1B5CdB22c43faab4AbEb5c74999895464Ddaf": "Tornado Cash: Old Proxy", + "0x56Eddb7aa87536c09CCc2793473599fD21A8b17F": "Binance", + "0x9696f59E4d72E237BE84fFD425DCaD154Bf96976": "Binance" } \ No newline at end of file diff --git a/src/api/address-resolver/index.ts b/src/api/address-resolver/index.ts index f1514be..c6a1687 100644 --- a/src/api/address-resolver/index.ts +++ b/src/api/address-resolver/index.ts @@ -2,12 +2,18 @@ import { BaseProvider } from "@ethersproject/providers"; import { ensRenderer } from "../../components/ENSName"; import { plainStringRenderer } from "../../components/PlainString"; import { tokenRenderer } from "../../components/TokenName"; +import { uniswapV1PairRenderer } from "../../components/UniswapV1ExchangeName"; +import { uniswapV2PairRenderer } from "../../components/UniswapV2PairName"; +import { uniswapV3PairRenderer } from "../../components/UniswapV3PoolName"; import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver"; import { CompositeAddressResolver, SelectedResolvedName, } from "./CompositeAddressResolver"; import { ENSAddressResolver } from "./ENSAddressResolver"; +import { UniswapV1Resolver } from "./UniswapV1Resolver"; +import { UniswapV2Resolver } from "./UniswapV2Resolver"; +import { UniswapV3Resolver } from "./UniswapV3Resolver"; import { ERCTokenResolver } from "./ERCTokenResolver"; import { HardcodedAddressResolver } from "./HardcodedAddressResolver"; @@ -15,11 +21,17 @@ export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>; // Create and configure the main resolver export const ensResolver = new ENSAddressResolver(); +export const uniswapV1Resolver = new UniswapV1Resolver(); +export const uniswapV2Resolver = new UniswapV2Resolver(); +export const uniswapV3Resolver = new UniswapV3Resolver(); export const ercTokenResolver = new ERCTokenResolver(); export const hardcodedResolver = new HardcodedAddressResolver(); const _mainResolver = new CompositeAddressResolver(); _mainResolver.addResolver(ensResolver); +_mainResolver.addResolver(uniswapV3Resolver); +_mainResolver.addResolver(uniswapV2Resolver); +_mainResolver.addResolver(uniswapV1Resolver); _mainResolver.addResolver(ercTokenResolver); _mainResolver.addResolver(hardcodedResolver); @@ -31,6 +43,9 @@ export const resolverRendererRegistry = new Map< ResolvedAddressRenderer<any> >(); resolverRendererRegistry.set(ensResolver, ensRenderer); +resolverRendererRegistry.set(uniswapV1Resolver, uniswapV1PairRenderer); +resolverRendererRegistry.set(uniswapV2Resolver, uniswapV2PairRenderer); +resolverRendererRegistry.set(uniswapV3Resolver, uniswapV3PairRenderer); resolverRendererRegistry.set(ercTokenResolver, tokenRenderer); resolverRendererRegistry.set(hardcodedResolver, plainStringRenderer); diff --git a/src/block/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx index 969e4a4..01d867c 100644 --- a/src/block/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -12,8 +12,7 @@ import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses"; import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { PAGE_SIZE } from "../params"; import { useMultipleETHUSDOracle } from "../usePriceOracle"; -import { useAppConfigContext } from "../useAppConfig"; -import { useMultipleMetadata } from "../useSourcify"; +import { useContractsMetadata } from "../hooks"; type BlockTransactionResultsProps = { blockTag: BlockTag; @@ -28,9 +27,9 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({ total, pageNumber, }) => { + const { provider } = useContext(RuntimeContext); const selectionCtx = useSelection(); const [feeDisplay, feeDisplayToggler] = useFeeToggler(); - const { provider } = useContext(RuntimeContext); const addrCollector = useMemo(() => pageCollector(page), [page]); const resolvedAddresses = useResolvedAddresses(provider, addrCollector); const blockTags = useMemo(() => [blockTag], [blockTag]); @@ -41,15 +40,18 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({ return []; } - return page.map((t) => t.to).filter((to): to is string => to !== undefined); + const _addresses: ChecksummedAddress[] = []; + for (const t of page) { + if (t.to) { + _addresses.push(t.to); + } + if (t.createdContractAddress) { + _addresses.push(t.createdContractAddress); + } + } + return _addresses; }, [page]); - const { sourcifySource } = useAppConfigContext(); - const metadatas = useMultipleMetadata( - undefined, - addresses, - provider?.network.chainId, - sourcifySource - ); + const metadatas = useContractsMetadata(addresses, provider); return ( <ContentFrame> diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index 333e1ff..0f13f2c 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -7,10 +7,10 @@ import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import AddressOrENSName from "./AddressOrENSName"; +import SourcifyLogo from "../sourcify/SourcifyLogo"; import { AddressContext, ZERO_ADDRESS } from "../types"; import { ResolvedAddresses } from "../api/address-resolver"; -import { Metadata } from "../useSourcify"; -import SourcifyLogo from "../sourcify.svg"; +import { Metadata } from "../sourcify/useSourcify"; type DecoratedAddressLinkProps = { address: string; @@ -80,13 +80,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({ className="self-center flex-shrink-0 flex items-center" to={`/address/${address}/contract`} > - <img - src={SourcifyLogo} - alt="Sourcify logo" - title="Verified by Sourcify" - width={16} - height={16} - /> + <SourcifyLogo /> </NavLink> )} <AddressOrENSName diff --git a/src/components/HexValue.tsx b/src/components/HexValue.tsx index b07b473..88d6e0b 100644 --- a/src/components/HexValue.tsx +++ b/src/components/HexValue.tsx @@ -4,26 +4,8 @@ type HexValueProps = { value: string; }; -const HexValue: React.FC<HexValueProps> = ({ value }) => { - const shards: string[] = [value.slice(0, 10)]; - for (let i = 10; i < value.length; i += 8) { - shards.push(value.slice(i, i + 8)); - } +const HexValue: React.FC<HexValueProps> = ({ value }) => ( + <span className="font-hash text-black">{value}</span> +); - return ( - <> - {shards.map((s, i) => ( - <span - key={i} - className={`font-hash ${ - i % 2 === 0 ? "text-black" : "text-gray-400" - }`} - > - {s} - </span> - ))} - </> - ); -}; - -export default React.memo(HexValue); +export default HexValue; diff --git a/src/components/NavTab.tsx b/src/components/NavTab.tsx index e16d193..6b3c66c 100644 --- a/src/components/NavTab.tsx +++ b/src/components/NavTab.tsx @@ -9,7 +9,7 @@ type NavTabProps = { const NavTab: React.FC<NavTabProps> = ({ href, children }) => ( <Tab as={Fragment}> <NavLink - className={(isActive) => + className={({ isActive }) => `${ isActive ? "text-link-blue border-link-blue" @@ -17,7 +17,7 @@ const NavTab: React.FC<NavTabProps> = ({ href, children }) => ( } hover:text-link-blue text-sm font-bold px-3 py-3 border-b-2` } to={href} - exact + end replace > {children} diff --git a/src/components/SelectionHighlighter.tsx b/src/components/SelectionHighlighter.tsx index 40a3431..bebd606 100644 --- a/src/components/SelectionHighlighter.tsx +++ b/src/components/SelectionHighlighter.tsx @@ -18,6 +18,7 @@ export const genericSelector = selection.content === content; export const addressSelector: ContentSelector = genericSelector("address"); +export const valueSelector: ContentSelector = genericSelector("value"); export const functionSigSelector: ContentSelector = genericSelector("functionSig"); diff --git a/src/components/TokenLogo.tsx b/src/components/TokenLogo.tsx index 8568a0b..7eaf2bd 100644 --- a/src/components/TokenLogo.tsx +++ b/src/components/TokenLogo.tsx @@ -1,32 +1,31 @@ -import React, { Suspense, useContext } from "react"; +import React, { useContext } from "react"; import { useImage } from "react-image"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import { tokenLogoURL } from "../url"; import { RuntimeContext } from "../useRuntime"; +import { ChecksummedAddress } from "../types"; type TokenLogoProps = { - address: string; + address: ChecksummedAddress; name: string; }; -const TokenLogo: React.FC<TokenLogoProps> = (props) => ( - <Suspense fallback={null}> - <InternalTokenLogo {...props} /> - </Suspense> -); - -const InternalTokenLogo: React.FC<TokenLogoProps> = ({ address, name }) => { +const TokenLogo: React.FC<TokenLogoProps> = ({ address, name }) => { const { config } = useContext(RuntimeContext); const srcList: string[] = []; if (config) { srcList.push(tokenLogoURL(config.assetsURLPrefix ?? "", address)); } - srcList.push("/eth-diamond-black.png"); - const { src } = useImage({ srcList }); + const { src, isLoading } = useImage({ srcList, useSuspense: false }); return ( - <div className="flex items-center justify-center w-5 h-5"> - <img className="max-w-full max-h-full" src={src} alt={`${name} logo`} /> + <div className="flex items-center justify-center text-gray-400 w-5 h-5"> + {src && ( + <img className="max-w-full max-h-full" src={src} alt={`${name} logo`} /> + )} + {!src && !isLoading && <FontAwesomeIcon icon={faCoins} size="1x" />} </div> ); }; diff --git a/src/components/TransactionAddress.tsx b/src/components/TransactionAddress.tsx index 28540dd..cb1edc9 100644 --- a/src/components/TransactionAddress.tsx +++ b/src/components/TransactionAddress.tsx @@ -4,7 +4,7 @@ import DecoratedAddressLink from "./DecoratedAddressLink"; import { ResolvedAddresses } from "../api/address-resolver"; import { useSelectedTransaction } from "../useSelectedTransaction"; import { AddressContext } from "../types"; -import { Metadata } from "../useSourcify"; +import { Metadata } from "../sourcify/useSourcify"; type TransactionAddressProps = { address: string; diff --git a/src/components/UniswapV1ExchangeName.tsx b/src/components/UniswapV1ExchangeName.tsx new file mode 100644 index 0000000..710c543 --- /dev/null +++ b/src/components/UniswapV1ExchangeName.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import TokenLogo from "./TokenLogo"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; +import { ChecksummedAddress } from "../types"; +import { + UniswapV1PairMeta, + UniswapV1TokenMeta, +} from "../api/address-resolver/UniswapV1Resolver"; + +type UniswapV1ExchangeNameProps = { + address: string; + token: UniswapV1TokenMeta; + linkable: boolean; + dontOverrideColors?: boolean; +}; + +const UniswapV1ExchangeName: React.FC<UniswapV1ExchangeNameProps> = ({ + address, + token, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + <NavLink + className={`flex items-baseline space-x-1 font-sans ${ + dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover" + } truncate`} + to={`/address/${address}`} + title={`Uniswap V1 LP (${token.symbol}): ${address}`} + > + <span>Uniswap V1 LP:</span> + <Content + linkable={true} + address={token.address} + name={token.name} + symbol={token.symbol} + /> + </NavLink> + ); + } + + return ( + <div + className="flex items-baseline space-x-1 font-sans text-gray-700 truncate" + title={`Uniswap V1 LP (${token.symbol}): ${address}`} + > + <span>Uniswap V1 LP:</span> + <Content + linkable={false} + address={token.address} + name={token.name} + symbol={token.symbol} + /> + </div> + ); +}; + +type ContentProps = { + linkable: boolean; + address: ChecksummedAddress; + name: string; + symbol: string; +}; + +const Content: React.FC<ContentProps> = ({ + address, + name, + symbol, + linkable, +}) => ( + <> + <div + className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} + > + <TokenLogo address={address} name={name} /> + </div> + <span>{symbol}</span> + </> +); + +export const uniswapV1PairRenderer: ResolvedAddressRenderer<UniswapV1PairMeta> = + (address, tokenMeta, linkable, dontOverrideColors) => ( + <UniswapV1ExchangeName + address={address} + token={tokenMeta.token} + linkable={linkable} + dontOverrideColors={dontOverrideColors} + /> + ); + +export default UniswapV1ExchangeName; diff --git a/src/components/UniswapV2PairName.tsx b/src/components/UniswapV2PairName.tsx new file mode 100644 index 0000000..f4520b7 --- /dev/null +++ b/src/components/UniswapV2PairName.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import TokenLogo from "./TokenLogo"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; +import { + UniswapV2PairMeta, + UniswapV2TokenMeta, +} from "../api/address-resolver/UniswapV2Resolver"; +import { ChecksummedAddress } from "../types"; + +type UniswapV2PairNameProps = { + address: string; + token0: UniswapV2TokenMeta; + token1: UniswapV2TokenMeta; + linkable: boolean; + dontOverrideColors?: boolean; +}; + +const UniswapV2PairName: React.FC<UniswapV2PairNameProps> = ({ + address, + token0, + token1, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + <NavLink + className={`flex items-baseline space-x-1 font-sans ${ + dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover" + } truncate`} + to={`/address/${address}`} + title={`Uniswap V2 LP (${token0.symbol}/${token1.symbol}): ${address}`} + > + <span>Uniswap V2 LP:</span> + <Content + linkable={true} + address={token0.address} + name={token0.name} + symbol={token0.symbol} + /> + <span>/</span> + <Content + linkable={true} + address={token1.address} + name={token1.name} + symbol={token1.symbol} + /> + </NavLink> + ); + } + + return ( + <div + className="flex items-baseline space-x-1 font-sans text-gray-700 truncate" + title={`Uniswap V2 LP (${token0.symbol}/${token1.symbol}): ${address}`} + > + <span>Uniswap V2 LP:</span> + <Content + linkable={false} + address={token0.address} + name={token0.name} + symbol={token0.symbol} + /> + <span>/</span> + <Content + linkable={false} + address={token1.address} + name={token1.name} + symbol={token1.symbol} + /> + </div> + ); +}; + +type ContentProps = { + linkable: boolean; + address: ChecksummedAddress; + name: string; + symbol: string; +}; + +const Content: React.FC<ContentProps> = ({ + address, + name, + symbol, + linkable, +}) => ( + <> + <div + className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} + > + <TokenLogo address={address} name={name} /> + </div> + <span>{symbol}</span> + </> +); + +export const uniswapV2PairRenderer: ResolvedAddressRenderer<UniswapV2PairMeta> = + (address, tokenMeta, linkable, dontOverrideColors) => ( + <UniswapV2PairName + address={address} + token0={tokenMeta.token0} + token1={tokenMeta.token1} + linkable={linkable} + dontOverrideColors={dontOverrideColors} + /> + ); + +export default UniswapV2PairName; diff --git a/src/components/UniswapV3PoolName.tsx b/src/components/UniswapV3PoolName.tsx new file mode 100644 index 0000000..a21b186 --- /dev/null +++ b/src/components/UniswapV3PoolName.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import TokenLogo from "./TokenLogo"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; +import { + UniswapV3PairMeta, + UniswapV3TokenMeta, +} from "../api/address-resolver/UniswapV3Resolver"; +import { ChecksummedAddress } from "../types"; + +type UniswapV3PoolNameProps = { + address: string; + token0: UniswapV3TokenMeta; + token1: UniswapV3TokenMeta; + fee: number; + linkable: boolean; + dontOverrideColors?: boolean; +}; + +const UniswapV3PairName: React.FC<UniswapV3PoolNameProps> = ({ + address, + token0, + token1, + fee, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + <NavLink + className={`flex items-baseline space-x-1 font-sans ${ + dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover" + } truncate`} + to={`/address/${address}`} + title={`Uniswap V3 LP (${token0.symbol}/${token1.symbol}/${ + fee / 10000 + }%): ${address}`} + > + <span>Uniswap V3 LP:</span> + <Content + linkable={true} + address={token0.address} + name={token0.name} + symbol={token0.symbol} + /> + <span>/</span> + <Content + linkable={true} + address={token1.address} + name={token1.name} + symbol={token1.symbol} + /> + <span>/ {fee / 10000}%</span> + </NavLink> + ); + } + + return ( + <div + className="flex items-baseline space-x-1 font-sans text-gray-700 truncate" + title={`Uniswap V3 LP (${token0.symbol}/${token1.symbol}/${ + fee / 10000 + }%): ${address}`} + > + <span>Uniswap V3 LP:</span> + <Content + linkable={false} + address={token0.address} + name={token0.name} + symbol={token0.symbol} + /> + <span>/</span> + <Content + linkable={false} + address={token1.address} + name={token1.name} + symbol={token1.symbol} + /> + <span>/ {fee / 10000}%</span> + </div> + ); +}; + +type ContentProps = { + linkable: boolean; + address: ChecksummedAddress; + name: string; + symbol: string; +}; + +const Content: React.FC<ContentProps> = ({ + address, + name, + symbol, + linkable, +}) => ( + <> + <div + className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`} + > + <TokenLogo address={address} name={name} /> + </div> + <span>{symbol}</span> + </> +); + +export const uniswapV3PairRenderer: ResolvedAddressRenderer<UniswapV3PairMeta> = + (address, tokenMeta, linkable, dontOverrideColors) => ( + <UniswapV3PairName + address={address} + token0={tokenMeta.token0} + token1={tokenMeta.token1} + fee={tokenMeta.fee} + linkable={linkable} + dontOverrideColors={dontOverrideColors} + /> + ); + +export default UniswapV3PairName; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..e924314 --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ChecksummedAddress } from "./types"; +import { Metadata, useMultipleMetadata } from "./sourcify/useSourcify"; +import { useAppConfigContext } from "./useAppConfig"; +import { useAddressesWithCode } from "./useErigonHooks"; + +export const useDedupedAddresses = ( + addresses: ChecksummedAddress[] +): ChecksummedAddress[] => { + return useMemo(() => { + const deduped = new Set(addresses); + return [...deduped]; + }, [addresses]); +}; + +export const useContractsMetadata = ( + addresses: ChecksummedAddress[], + provider: JsonRpcProvider | undefined, + baseMetadatas?: Record<string, Metadata | null> +) => { + const deduped = useDedupedAddresses(addresses); + const contracts = useAddressesWithCode(provider, deduped); + const { sourcifySource } = useAppConfigContext(); + const metadatas = useMultipleMetadata( + baseMetadatas, + contracts, + provider?.network.chainId, + sourcifySource + ); + + return metadatas; +}; diff --git a/src/params.ts b/src/params.ts index 5c95cfa..7ac916a 100644 --- a/src/params.ts +++ b/src/params.ts @@ -1,3 +1,3 @@ -export const MIN_API_LEVEL = 3; +export const MIN_API_LEVEL = 4; export const PAGE_SIZE = 25; diff --git a/src/search/CameraScanner.tsx b/src/search/CameraScanner.tsx index b32d707..5766814 100644 --- a/src/search/CameraScanner.tsx +++ b/src/search/CameraScanner.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { isAddress } from "@ethersproject/address"; import { QrReader } from "@blackbox-vision/react-qr-reader"; import { OnResultFunction } from "@blackbox-vision/react-qr-reader/dist-types/types"; @@ -11,7 +11,7 @@ type CameraScannerProps = { }; const CameraScanner: React.FC<CameraScannerProps> = ({ turnOffScan }) => { - const history = useHistory(); + const navigate = useNavigate(); const evaluateScan: OnResultFunction = (result, error, codeReader) => { console.log("scan"); @@ -23,7 +23,7 @@ const CameraScanner: React.FC<CameraScannerProps> = ({ turnOffScan }) => { return; } - history.push(`/search?q=${text}`); + navigate(`/search?q=${text}`); turnOffScan(); } }; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 446daf4..b6cfe1e 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -19,7 +19,7 @@ import { FeeDisplay } from "./useFeeToggler"; import { formatValue } from "../components/formatter"; import ETH2USDValue from "../components/ETH2USDValue"; import { ResolvedAddresses } from "../api/address-resolver"; -import { Metadata } from "../useSourcify"; +import { Metadata } from "../sourcify/useSourcify"; type TransactionItemProps = { tx: ProcessedTransaction; @@ -99,7 +99,10 @@ const TransactionItem: React.FC<TransactionItemProps> = ({ /> </span> </span> - <span className="col-span-2 flex items-baseline" title={tx.to}> + <span + className="col-span-2 flex items-baseline" + title={tx.to ?? tx.createdContractAddress} + > <span className="truncate"> {tx.to ? ( <AddressHighlighter address={tx.to}> diff --git a/src/search/UndefinedPageButton.tsx b/src/search/UndefinedPageButton.tsx index 9b8f644..102fc6d 100644 --- a/src/search/UndefinedPageButton.tsx +++ b/src/search/UndefinedPageButton.tsx @@ -26,7 +26,7 @@ const UndefinedPageButton: React.FC<UndefinedPageButtonProps> = ({ return ( <NavLink className="transition-colors bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white disabled:bg-link-blue disabled:text-gray-400 disabled:cursor-default rounded-lg px-3 py-2 text-xs" - to={`/address/${address}/${direction}${ + to={`/address/${address}/txs/${direction}${ direction === "prev" || direction === "next" ? `?h=${hash}` : "" }`} > diff --git a/src/search/search.ts b/src/search/search.ts index 26af5e2..57a5958 100644 --- a/src/search/search.ts +++ b/src/search/search.ts @@ -1,4 +1,15 @@ +import { + ChangeEventHandler, + FormEventHandler, + RefObject, + useRef, + useState, +} from "react"; +import { NavigateFunction, useNavigate } from "react-router"; import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; +import { isAddress } from "@ethersproject/address"; +import { isHexString } from "@ethersproject/bytes"; +import useKeyboardShortcut from "use-keyboard-shortcut"; import { PAGE_SIZE } from "../params"; import { ProcessedTransaction, TransactionChunk } from "../types"; @@ -41,7 +52,7 @@ export class SearchController { idx: _receipt.transactionIndex, hash: t.hash, from: t.from, - to: t.to, + to: t.to ?? null, createdContractAddress: _receipt.contractAddress, value: t.value, fee: _receipt.gasUsed.mul(t.gasPrice!), @@ -194,3 +205,59 @@ export class SearchController { return this; } } + +const doSearch = (q: string, navigate: NavigateFunction) => { + if (isAddress(q)) { + navigate(`/address/${q}`, { replace: true }); + return; + } + + if (isHexString(q, 32)) { + navigate(`/tx/${q}`, { replace: true }); + return; + } + + const blockNumber = parseInt(q); + if (!isNaN(blockNumber)) { + navigate(`/block/${blockNumber}`, { replace: true }); + return; + } + + // Assume it is an ENS name + navigate(`/address/${q}`); +}; + +export const useGenericSearch = (): [ + RefObject<HTMLInputElement>, + ChangeEventHandler<HTMLInputElement>, + FormEventHandler<HTMLFormElement> +] => { + const [searchString, setSearchString] = useState<string>(""); + const [canSubmit, setCanSubmit] = useState<boolean>(false); + const navigate = useNavigate(); + + const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { + const searchTerm = e.target.value.trim(); + setCanSubmit(searchTerm.length > 0); + setSearchString(searchTerm); + }; + + const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { + e.preventDefault(); + if (!canSubmit) { + return; + } + + if (searchRef.current) { + searchRef.current.value = ""; + } + doSearch(searchString, navigate); + }; + + const searchRef = useRef<HTMLInputElement>(null); + useKeyboardShortcut(["/"], () => { + searchRef.current?.focus(); + }); + + return [searchRef, handleChange, handleSubmit]; +}; diff --git a/src/sourcify/SourcifyLogo.tsx b/src/sourcify/SourcifyLogo.tsx new file mode 100644 index 0000000..0324e76 --- /dev/null +++ b/src/sourcify/SourcifyLogo.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import SourcifyIcon from "./sourcify.svg"; + +const SourcifyLogo: React.FC = () => ( + <img + src={SourcifyIcon} + alt="Sourcify logo" + title="Verified by Sourcify" + width={16} + height={16} + /> +); + +export default SourcifyLogo; diff --git a/src/sourcify.svg b/src/sourcify/sourcify.svg similarity index 100% rename from src/sourcify.svg rename to src/sourcify/sourcify.svg diff --git a/src/useSourcify.ts b/src/sourcify/useSourcify.ts similarity index 86% rename from src/useSourcify.ts rename to src/sourcify/useSourcify.ts index 10f0c68..f5fae67 100644 --- a/src/useSourcify.ts +++ b/src/sourcify/useSourcify.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useMemo } from "react"; import { Interface } from "@ethersproject/abi"; -import { ChecksummedAddress, TransactionData } from "./types"; -import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "./url"; +import { ChecksummedAddress, TransactionData } from "../types"; +import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url"; export type UserMethod = { notice?: string | undefined; @@ -123,49 +123,44 @@ export const useSourcify = ( export const useMultipleMetadata = ( baseMetadatas: Record<string, Metadata | null> | undefined, - addresses: (ChecksummedAddress | undefined)[], + addresses: ChecksummedAddress[] | undefined, chainId: number | undefined, source: SourcifySource ): Record<ChecksummedAddress, Metadata | null | undefined> => { const [rawMetadata, setRawMetadata] = useState< Record<string, Metadata | null | undefined> >({}); - useEffect(() => { - if (!addresses || chainId === undefined) { + if (addresses === undefined || chainId === undefined) { return; } setRawMetadata({}); const abortController = new AbortController(); - const fetchMetadata = async (dedupedAddresses: string[]) => { - const promises: Promise<Metadata | null>[] = []; - for (const address of dedupedAddresses) { - promises.push( + const fetchMetadata = async (_addresses: string[]) => { + const fetchers: Promise<Metadata | null>[] = []; + for (const address of _addresses) { + fetchers.push( fetchSourcifyMetadata(address, chainId, source, abortController) ); } - const results = await Promise.all(promises); + const results = await Promise.all(fetchers); if (abortController.signal.aborted) { return; } - const metadatas: Record<string, Metadata | null> = baseMetadatas - ? { ...baseMetadatas } - : {}; + let metadatas: Record<string, Metadata | null> = {}; + if (baseMetadatas) { + metadatas = { ...baseMetadatas }; + } for (let i = 0; i < results.length; i++) { - metadatas[dedupedAddresses[i]] = results[i]; + metadatas[_addresses[i]] = results[i]; } setRawMetadata(metadatas); }; - const deduped = new Set( - addresses.filter( - (a): a is ChecksummedAddress => - a !== undefined && baseMetadatas?.[a] === undefined - ) - ); - fetchMetadata(Array.from(deduped)); + const filtered = addresses.filter((a) => baseMetadatas?.[a] === undefined); + fetchMetadata(filtered); return () => { abortController.abort(); diff --git a/src/special/london/Blocks.tsx b/src/special/london/Blocks.tsx index d100717..095bbc2 100644 --- a/src/special/london/Blocks.tsx +++ b/src/special/london/Blocks.tsx @@ -8,6 +8,15 @@ import React, { import { Block } from "@ethersproject/abstract-provider"; import { FixedNumber } from "@ethersproject/bignumber"; import { Line } from "react-chartjs-2"; +import { + Chart as ChartJS, + LinearScale, + CategoryScale, + PointElement, + LineElement, + Filler, + Tooltip, +} from "chart.js"; import { Transition } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; @@ -25,6 +34,15 @@ import { gasChartData, } from "./chart"; +ChartJS.register( + LinearScale, + CategoryScale, + PointElement, + LineElement, + Filler, + Tooltip +); + const MAX_BLOCK_HISTORY = 20; const PREV_BLOCK_COUNT = 15; diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index e2613be..e4daf2f 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -37,10 +37,10 @@ import { use4Bytes, useTransactionDescription, } from "../use4Bytes"; -import { DevDoc, useMultipleMetadata, UserDoc } from "../useSourcify"; +import { DevDoc, UserDoc } from "../sourcify/useSourcify"; import { ResolvedAddresses } from "../api/address-resolver"; import { RuntimeContext } from "../useRuntime"; -import { useAppConfigContext } from "../useAppConfig"; +import { useContractsMetadata } from "../hooks"; type DetailsProps = { txData: TransactionData; @@ -95,13 +95,7 @@ const Details: React.FC<DetailsProps> = ({ } return _addresses; }, [txData]); - const { sourcifySource } = useAppConfigContext(); - const metadatas = useMultipleMetadata( - undefined, - addresses, - provider?.network.chainId, - sourcifySource - ); + const metadatas = useContractsMetadata(addresses, provider); return ( <ContentFrame tabs> diff --git a/src/transaction/LogEntry.tsx b/src/transaction/LogEntry.tsx index 109ec1e..07be33f 100644 --- a/src/transaction/LogEntry.tsx +++ b/src/transaction/LogEntry.tsx @@ -10,7 +10,7 @@ import DecodedLogSignature from "./decoder/DecodedLogSignature"; import { useTopic0 } from "../useTopic0"; import { ResolvedAddresses } from "../api/address-resolver"; import { ChecksummedAddress } from "../types"; -import { Metadata } from "../useSourcify"; +import { Metadata } from "../sourcify/useSourcify"; type LogEntryProps = { log: Log; diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index bbf0475..eba82e1 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -3,10 +3,10 @@ import { Interface } from "@ethersproject/abi"; import ContentFrame from "../ContentFrame"; import LogEntry from "./LogEntry"; import { TransactionData } from "../types"; -import { useAppConfigContext } from "../useAppConfig"; -import { Metadata, useMultipleMetadata } from "../useSourcify"; +import { Metadata } from "../sourcify/useSourcify"; import { ResolvedAddresses } from "../api/address-resolver"; import { RuntimeContext } from "../useRuntime"; +import { useContractsMetadata } from "../hooks"; type LogsProps = { txData: TransactionData; @@ -30,13 +30,8 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => { [txData] ); const { provider } = useContext(RuntimeContext); - const { sourcifySource } = useAppConfigContext(); - const metadatas = useMultipleMetadata( - baseMetadatas, - logAddresses, - provider?.network.chainId, - sourcifySource - ); + const metadatas = useContractsMetadata(logAddresses, provider, baseMetadatas); + const logDescs = useMemo(() => { if (!txData) { return undefined; diff --git a/src/transaction/decoder/DecodedParamRow.tsx b/src/transaction/decoder/DecodedParamRow.tsx index 78467d3..3ad47cc 100644 --- a/src/transaction/decoder/DecodedParamRow.tsx +++ b/src/transaction/decoder/DecodedParamRow.tsx @@ -9,6 +9,9 @@ import AddressDecoder from "./AddressDecoder"; import BooleanDecoder from "./BooleanDecoder"; import BytesDecoder from "./BytesDecoder"; import { ResolvedAddresses } from "../../api/address-resolver"; +import SelectionHighlighter, { + valueSelector, +} from "../../components/SelectionHighlighter"; type DecodedParamRowProps = { prefix?: ReactNode; @@ -68,24 +71,30 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({ {help && showHelp && <div className="mt-2 text-gray-400">{help}</div>} </td> <td className="col-span-1 text-gray-500">{paramType.type}</td> - <td className="col-span-8 pr-1 font-code break-all"> - {paramType.baseType === "uint256" ? ( - <Uint256Decoder r={r} /> - ) : paramType.baseType === "address" ? ( - <AddressDecoder - r={r.toString()} - resolvedAddresses={resolvedAddresses} - /> - ) : paramType.baseType === "bool" ? ( - <BooleanDecoder r={r} /> - ) : paramType.baseType === "bytes" ? ( - <BytesDecoder r={r} /> - ) : paramType.baseType === "tuple" || - paramType.baseType === "array" ? ( - <></> - ) : ( - r.toString() - )} + <td className="col-span-8 pr-1 font-code break-all flex"> + <SelectionHighlighter + myType="value" + myContent={r.toString()} + selector={valueSelector} + > + {paramType.baseType === "uint256" ? ( + <Uint256Decoder r={r} /> + ) : paramType.baseType === "address" ? ( + <AddressDecoder + r={r.toString()} + resolvedAddresses={resolvedAddresses} + /> + ) : paramType.baseType === "bool" ? ( + <BooleanDecoder r={r} /> + ) : paramType.baseType === "bytes" ? ( + <BytesDecoder r={r} /> + ) : paramType.baseType === "tuple" || + paramType.baseType === "array" ? ( + <></> + ) : ( + r.toString() + )} + </SelectionHighlighter> </td> </tr> {paramType.baseType === "tuple" && diff --git a/src/transaction/decoder/DecodedParamsTable.tsx b/src/transaction/decoder/DecodedParamsTable.tsx index 376eee2..78d94e6 100644 --- a/src/transaction/decoder/DecodedParamsTable.tsx +++ b/src/transaction/decoder/DecodedParamsTable.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ParamType, Result } from "@ethersproject/abi"; import DecodedParamRow from "./DecodedParamRow"; -import { DevMethod, UserMethod } from "../../useSourcify"; +import { DevMethod, UserMethod } from "../../sourcify/useSourcify"; import { ResolvedAddresses } from "../../api/address-resolver"; type DecodedParamsTableProps = { diff --git a/src/transaction/decoder/InputDecoder.tsx b/src/transaction/decoder/InputDecoder.tsx index e3d041c..0bd9835 100644 --- a/src/transaction/decoder/InputDecoder.tsx +++ b/src/transaction/decoder/InputDecoder.tsx @@ -4,7 +4,7 @@ import { toUtf8String } from "@ethersproject/strings"; import { Tab } from "@headlessui/react"; import ModeTab from "../../components/ModeTab"; import DecodedParamsTable from "./DecodedParamsTable"; -import { DevMethod, UserMethod } from "../../useSourcify"; +import { DevMethod, UserMethod } from "../../sourcify/useSourcify"; import { ResolvedAddresses } from "../../api/address-resolver"; type InputDecoderProps = { diff --git a/src/types.ts b/src/types.ts index fde36bb..adedc86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,7 @@ export type ProcessedTransaction = { idx: number; hash: string; from?: string; - to?: string; + to: string | null; createdContractAddress?: string; internalMinerInteraction?: boolean; value: BigNumber; diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index 17d629f..30effa2 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -14,6 +14,7 @@ import { InternalOperation, ProcessedTransaction, OperationType, + ChecksummedAddress, } from "./types"; import erc20 from "./erc20.json"; @@ -94,33 +95,38 @@ export const useBlockTransactions = ( const _receipts = result.receipts; const rawTxs = _block.transactions - .map( - (t, i): ProcessedTransaction => ({ + .map((t, i): ProcessedTransaction => { + const _rawReceipt = _receipts[i]; + // Empty logs on purpose because of ethers formatter requires it + _rawReceipt.logs = []; + const _receipt = provider.formatter.receipt(_rawReceipt); + + return { blockNumber: blockNumber, timestamp: _block.timestamp, miner: _block.miner, idx: i, hash: t.hash, from: t.from, - to: t.to, - createdContractAddress: _receipts[i].contractAddress, + to: t.to ?? null, + createdContractAddress: _receipt.contractAddress, value: t.value, fee: t.type !== 2 ? provider.formatter - .bigNumber(_receipts[i].gasUsed) + .bigNumber(_receipt.gasUsed) .mul(t.gasPrice!) : provider.formatter - .bigNumber(_receipts[i].gasUsed) + .bigNumber(_receipt.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), - }) - ) + status: provider.formatter.number(_receipt.status), + }; + }) .reverse(); setTxs(rawTxs); setTotalTxs(result.fullblock.transactionCount); @@ -440,3 +446,41 @@ export const useUniqueSignatures = (traces: TraceGroup[] | undefined) => { return uniqueSignatures; }; + +const hasCode = async ( + provider: JsonRpcProvider, + address: ChecksummedAddress +): Promise<boolean> => { + const result = await provider.send("ots_hasCode", [address, "latest"]); + return result as boolean; +}; + +export const useAddressesWithCode = ( + provider: JsonRpcProvider | undefined, + addresses: ChecksummedAddress[] +): ChecksummedAddress[] | undefined => { + const [results, setResults] = useState<ChecksummedAddress[] | undefined>(); + + useEffect(() => { + // Reset + setResults(undefined); + + if (provider === undefined) { + return; + } + + const readCodes = async () => { + const checkers: Promise<boolean>[] = []; + for (const a of addresses) { + checkers.push(hasCode(provider, a)); + } + + const result = await Promise.all(checkers); + const filtered = addresses.filter((_, i) => result[i]); + setResults(filtered); + }; + readCodes(); + }, [provider, addresses]); + + return results; +}; diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index f41a1be..2e49afd 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -50,7 +50,8 @@ export const useMultipleETHUSDOracle = ( const priceData = await ethFeed.latestRoundData({ blockTag }); return BigNumber.from(priceData.answer); } catch (err) { - console.error(err); + // Silently ignore on purpose; it means the network or block number does + // not contain the chainlink feed contract return undefined; } })() diff --git a/src/useResolvedAddresses.ts b/src/useResolvedAddresses.ts index 9eadfde..9250232 100644 --- a/src/useResolvedAddresses.ts +++ b/src/useResolvedAddresses.ts @@ -1,8 +1,73 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useContext } from "react"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { ProcessedTransaction, TransactionData } from "./types"; +import { getAddress, isAddress } from "@ethersproject/address"; import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; import { TraceGroup } from "./useErigonHooks"; +import { RuntimeContext } from "./useRuntime"; +import { + ChecksummedAddress, + ProcessedTransaction, + TransactionData, +} from "./types"; + +export const useAddressOrENSFromURL = ( + addressOrName: string, + urlFixer: (address: ChecksummedAddress) => void +): [ + ChecksummedAddress | undefined, + boolean | undefined, + boolean | undefined +] => { + const { provider } = useContext(RuntimeContext); + const [checksummedAddress, setChecksummedAddress] = useState< + ChecksummedAddress | undefined + >(); + const [isENS, setENS] = useState<boolean>(); + const [error, setError] = useState<boolean>(); + + // If it looks like it is an ENS name, try to resolve it + useEffect(() => { + // Reset + setENS(false); + setError(false); + setChecksummedAddress(undefined); + + // TODO: handle and offer fallback to bad checksummed addresses + if (isAddress(addressOrName)) { + // Normalize to checksummed address + const _checksummedAddress = getAddress(addressOrName); + if (_checksummedAddress !== addressOrName) { + // Request came with a non-checksummed address; fix the URL + urlFixer(_checksummedAddress); + return; + } + + setENS(false); + setError(false); + setChecksummedAddress(_checksummedAddress); + return; + } + + if (!provider) { + return; + } + const resolveName = async () => { + const resolvedAddress = await provider.resolveName(addressOrName); + if (resolvedAddress !== null) { + setENS(true); + setError(false); + setChecksummedAddress(resolvedAddress); + } else { + setENS(false); + setError(true); + setChecksummedAddress(undefined); + } + }; + resolveName(); + }, [provider, addressOrName, urlFixer]); + + return [checksummedAddress, isENS, error]; +}; export type AddressCollector = () => string[]; @@ -21,6 +86,9 @@ export const pageCollector = if (tx.to) { uniqueAddresses.add(tx.to); } + if (tx.createdContractAddress) { + uniqueAddresses.add(tx.createdContractAddress); + } } return Array.from(uniqueAddresses); diff --git a/topic0 b/topic0 index 5026a20..63794c4 160000 --- a/topic0 +++ b/topic0 @@ -1 +1 @@ -Subproject commit 5026a20b712c1cad66878821c38e1f070e4a3799 +Subproject commit 63794c46467dea47fd99ec47db745c482887367e diff --git a/trustwallet b/trustwallet index 30e4ffa..e779c7b 160000 --- a/trustwallet +++ b/trustwallet @@ -1 +1 @@ -Subproject commit 30e4ffa0153594b11421cf383b1192e4414d2f66 +Subproject commit e779c7b400fc479f8442066f13565555be5bfcf3