diff --git a/package-lock.json b/package-lock.json index 07a0037..00aa601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,12 +31,10 @@ "@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.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": "^4.0.0", @@ -44,7 +42,7 @@ "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", @@ -3053,11 +3051,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" @@ -3160,25 +3153,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", @@ -8559,14 +8533,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 +9254,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 +9270,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", @@ -11674,19 +11622,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 +14158,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": { @@ -14594,60 +14512,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 +15317,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 +16606,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 +16743,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 +17720,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", @@ -18477,11 +18333,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", @@ -21545,11 +21396,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" }, @@ -21640,25 +21486,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", @@ -25321,11 +25148,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 +25612,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 +25627,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" }, @@ -27345,15 +27147,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 +28869,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" }, @@ -29320,54 +29102,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 +29652,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 +30567,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 +30663,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 +31330,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", @@ -32004,11 +31727,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" }, diff --git a/package.json b/package.json index 4cdea99..95e2ed6 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,10 @@ "@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.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": "^4.0.0", @@ -39,7 +37,7 @@ "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", diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index a1013c8..17c8888 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 } 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,202 +14,61 @@ 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 { useSingleMetadata } from "./sourcify/useSourcify"; import { ChecksummedAddress } from "./types"; -import SourcifyLogo from "./sourcify.svg"; - -type BlockParams = { - addressOrName: string; - direction?: string; -}; - -type PageParams = { - p?: number; -}; 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 metadatas = useMultipleMetadata( - undefined, - addresses, + const addressMetadata = useSingleMetadata( + checksummedAddress, provider?.network.chainId, sourcifySource ); - const addressMetadata = - checksummedAddress !== undefined - ? metadatas[checksummedAddress] - : undefined; return ( {error ? ( - "{params.addressOrName}" is not an ETH address or ENS name. + "{addressOrName}" is not an ETH address or ENS name. ) : ( checksummedAddress && ( @@ -231,7 +87,7 @@ const AddressTransactions: React.FC = () => { {isENS && ( - ENS: {params.addressOrName} + ENS: {addressOrName} )} @@ -260,87 +116,37 @@ const AddressTransactions: React.FC = () => { ) : ( - - Sourcify logo + + )} - - - -
-
- {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 +156,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..59fffbb 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; 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..b13bbd2 --- /dev/null +++ b/src/address/AddressTransactionResults.tsx @@ -0,0 +1,191 @@ +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 { useMultipleMetadata } from "../sourcify/useSourcify"; +import { useMultipleETHUSDOracle } from "../usePriceOracle"; +import { RuntimeContext } from "../useRuntime"; +import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses"; +import { useAppConfigContext } from "../useAppConfig"; +import { useParams, useSearchParams } from "react-router-dom"; +import { ChecksummedAddress } from "../types"; + +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); + } + } + } + return _addresses; + }, [address, page]); + const { sourcifySource } = useAppConfigContext(); + const metadatas = useMultipleMetadata( + undefined, + addresses, + provider?.network.chainId, + sourcifySource + ); + + 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/block/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx index 969e4a4..f2d8d08 100644 --- a/src/block/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -13,7 +13,7 @@ import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { PAGE_SIZE } from "../params"; import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useAppConfigContext } from "../useAppConfig"; -import { useMultipleMetadata } from "../useSourcify"; +import { useMultipleMetadata } from "../sourcify/useSourcify"; type BlockTransactionResultsProps = { blockTag: BlockTag; @@ -28,9 +28,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]); 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/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/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/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..929d2cd 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; 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..25bda65 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"; @@ -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 92% rename from src/useSourcify.ts rename to src/sourcify/useSourcify.ts index 10f0c68..e4d1ab1 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; @@ -121,6 +121,16 @@ export const useSourcify = ( return rawMetadata; }; +export const useSingleMetadata = ( + address: ChecksummedAddress | undefined, + chainId: number | undefined, + source: SourcifySource +) => { + const addresses = useMemo(() => (address ? [address] : []), [address]); + const metadatas = useMultipleMetadata(undefined, addresses, chainId, source); + return address !== undefined ? metadatas[address] : undefined; +}; + export const useMultipleMetadata = ( baseMetadatas: Record<string, Metadata | null> | undefined, addresses: (ChecksummedAddress | undefined)[], diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index e2613be..994ab51 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -37,7 +37,7 @@ import { use4Bytes, useTransactionDescription, } from "../use4Bytes"; -import { DevDoc, useMultipleMetadata, UserDoc } from "../useSourcify"; +import { DevDoc, useMultipleMetadata, UserDoc } from "../sourcify/useSourcify"; import { ResolvedAddresses } from "../api/address-resolver"; import { RuntimeContext } from "../useRuntime"; import { useAppConfigContext } from "../useAppConfig"; 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..4a9cc93 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -4,7 +4,7 @@ import ContentFrame from "../ContentFrame"; import LogEntry from "./LogEntry"; import { TransactionData } from "../types"; import { useAppConfigContext } from "../useAppConfig"; -import { Metadata, useMultipleMetadata } from "../useSourcify"; +import { Metadata, useMultipleMetadata } from "../sourcify/useSourcify"; import { ResolvedAddresses } from "../api/address-resolver"; import { RuntimeContext } from "../useRuntime"; 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/useResolvedAddresses.ts b/src/useResolvedAddresses.ts index 9eadfde..dc0df3a 100644 --- a/src/useResolvedAddresses.ts +++ b/src/useResolvedAddresses.ts @@ -1,8 +1,68 @@ -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(() => { + // 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[];