diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index f457ebb19..1bcdbf361 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -31,6 +31,7 @@ "clsx": "^1.1.1", "ethers": "^5.4.1", "js-base64": "^3.6.1", + "luxon": "^2.3.1", "notistack": "^1.0.10", "numeral": "^2.0.6", "react": "^17.0.2", @@ -39,11 +40,13 @@ "react-router-dom": "^5.2.0", "react-scripts": "5.0.0", "react-table": "^7.7.0", + "recharts": "^2.1.9", "redux": "^3.7.2", "use-debounce": "^7.0.0" }, "devDependencies": { "@truffle/hdwallet-provider": "^1.4.1", + "@types/luxon": "^2.3.1", "@types/node": "^16.6.1", "@types/numeral": "^2.0.2", "@types/react-router-dom": "^5.1.8", @@ -8474,6 +8477,45 @@ "dev": true, "optional": true }, + "node_modules/@types/d3-color": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz", + "integrity": "sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w==" + }, + "node_modules/@types/d3-interpolate": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz", + "integrity": "sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw==", + "dependencies": { + "@types/d3-color": "^2" + } + }, + "node_modules/@types/d3-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", + "integrity": "sha512-3YHpvDw9LzONaJzejXLOwZ3LqwwkoXb9LI2YN7Hbd6pkGo5nIlJ09ul4bQhBN4hQZJKmUpX8HkVqbzgUKY48cg==" + }, + "node_modules/@types/d3-scale": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz", + "integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==", + "dependencies": { + "@types/d3-time": "^2" + } + }, + "node_modules/@types/d3-shape": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz", + "integrity": "sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ==", + "dependencies": { + "@types/d3-path": "^2" + } + }, + "node_modules/@types/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==" + }, "node_modules/@types/ed2curve": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@types/ed2curve/-/ed2curve-0.2.2.tgz", @@ -8670,6 +8712,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, + "node_modules/@types/luxon": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.1.tgz", + "integrity": "sha512-nAPUltOT28fal2eDZz8yyzNhBjHw1NEymFBP7Q9iCShqpflWPybxHbD7pw/46jQmT+HXOy1QN5hNTms8MOTlOQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -8793,6 +8841,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, + "node_modules/@types/resize-observer-browser": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz", + "integrity": "sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg==" + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -13120,6 +13173,11 @@ "node": ">=0.10.0" } }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "node_modules/clean-css": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", @@ -14376,6 +14434,11 @@ "node": ">=0.10.0" } }, + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, "node_modules/css-vendor": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", @@ -14574,6 +14637,73 @@ "type": "^1.0.1" } }, + "node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" + }, + "node_modules/d3-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", + "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" + }, + "node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "dependencies": { + "d3-color": "1 - 2" + } + }, + "node_modules/d3-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", + "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + }, + "node_modules/d3-scale": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", + "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==", + "dependencies": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "^2.1.1", + "d3-time-format": "2 - 3" + } + }, + "node_modules/d3-shape": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", + "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "dependencies": { + "d3-path": "1 - 2" + } + }, + "node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "dependencies": { + "d3-time": "1 - 2" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -14679,6 +14809,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -18588,6 +18723,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-equals": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz", + "integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==" + }, "node_modules/fast-fifo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.0.0.tgz", @@ -22627,6 +22767,11 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -28256,6 +28401,14 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=", "dev": true }, + "node_modules/luxon": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.3.1.tgz", + "integrity": "sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -34818,6 +34971,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-redux": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz", @@ -34855,6 +35013,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-resize-detector": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz", + "integrity": "sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw==", + "dependencies": { + "@types/resize-observer-browser": "^0.1.6", + "lodash": "^4.17.21", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0", + "react-dom": "^16.0.0 || ^17.0.0" + } + }, "node_modules/react-router": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", @@ -36133,6 +36305,44 @@ "node": ">=10" } }, + "node_modules/react-smooth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz", + "integrity": "sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw==", + "dependencies": { + "fast-equals": "^2.0.0", + "raf": "^3.4.0", + "react-transition-group": "2.9.0" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-smooth/node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/react-smooth/node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, "node_modules/react-table": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz", @@ -36194,6 +36404,47 @@ "ms": "^2.1.1" } }, + "node_modules/recharts": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.9.tgz", + "integrity": "sha512-VozH5uznUvGqD7n224FGj7cmMAenlS0HPCs+7r2HeeHiQK6un6z0CTZfWVAB860xbcr4m+BN/EGMPZmYWd34Rg==", + "dependencies": { + "@types/d3-interpolate": "^2.0.0", + "@types/d3-scale": "^3.0.0", + "@types/d3-shape": "^2.0.0", + "classnames": "^2.2.5", + "d3-interpolate": "^2.0.0", + "d3-scale": "^3.0.0", + "d3-shape": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-resize-detector": "^6.6.3", + "react-smooth": "^2.0.0", + "recharts-scale": "^0.4.4", + "reduce-css-calc": "^2.1.8" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0", + "react-dom": "^16.0.0 || ^17.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/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/recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -36205,6 +36456,20 @@ "node": ">=0.10.0" } }, + "node_modules/reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dependencies": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, "node_modules/redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", @@ -36782,6 +37047,11 @@ "node": ">= 0.8.0" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -51036,6 +51306,45 @@ "dev": true, "optional": true }, + "@types/d3-color": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz", + "integrity": "sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w==" + }, + "@types/d3-interpolate": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz", + "integrity": "sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw==", + "requires": { + "@types/d3-color": "^2" + } + }, + "@types/d3-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", + "integrity": "sha512-3YHpvDw9LzONaJzejXLOwZ3LqwwkoXb9LI2YN7Hbd6pkGo5nIlJ09ul4bQhBN4hQZJKmUpX8HkVqbzgUKY48cg==" + }, + "@types/d3-scale": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz", + "integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==", + "requires": { + "@types/d3-time": "^2" + } + }, + "@types/d3-shape": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz", + "integrity": "sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ==", + "requires": { + "@types/d3-path": "^2" + } + }, + "@types/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==" + }, "@types/ed2curve": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@types/ed2curve/-/ed2curve-0.2.2.tgz", @@ -51232,6 +51541,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, + "@types/luxon": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.1.tgz", + "integrity": "sha512-nAPUltOT28fal2eDZz8yyzNhBjHw1NEymFBP7Q9iCShqpflWPybxHbD7pw/46jQmT+HXOy1QN5hNTms8MOTlOQ==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -51359,6 +51674,11 @@ "@types/react": "*" } }, + "@types/resize-observer-browser": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz", + "integrity": "sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg==" + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -54895,6 +55215,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-css": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", @@ -55890,6 +56215,11 @@ } } }, + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, "css-vendor": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", @@ -56037,6 +56367,73 @@ "type": "^1.0.1" } }, + "d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "requires": { + "internmap": "^1.0.0" + } + }, + "d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" + }, + "d3-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", + "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" + }, + "d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "requires": { + "d3-color": "1 - 2" + } + }, + "d3-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", + "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + }, + "d3-scale": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", + "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "^2.1.1", + "d3-time-format": "2 - 3" + } + }, + "d3-shape": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", + "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "requires": { + "d3-path": "1 - 2" + } + }, + "d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "requires": { + "d3-array": "2" + } + }, + "d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "requires": { + "d3-time": "1 - 2" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -56124,6 +56521,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" }, + "decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -59371,6 +59773,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-equals": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz", + "integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==" + }, "fast-fifo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.0.0.tgz", @@ -62587,6 +62994,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -67046,6 +67458,11 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=", "dev": true }, + "luxon": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.3.1.tgz", + "integrity": "sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==" + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -72241,6 +72658,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-redux": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz", @@ -72266,6 +72688,16 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, + "react-resize-detector": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz", + "integrity": "sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw==", + "requires": { + "@types/resize-observer-browser": "^0.1.6", + "lodash": "^4.17.21", + "resize-observer-polyfill": "^1.5.1" + } + }, "react-router": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", @@ -73235,6 +73667,37 @@ } } }, + "react-smooth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz", + "integrity": "sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw==", + "requires": { + "fast-equals": "^2.0.0", + "raf": "^3.4.0", + "react-transition-group": "2.9.0" + }, + "dependencies": { + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + } + } + }, "react-table": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz", @@ -73280,6 +73743,42 @@ "ms": "^2.1.1" } }, + "recharts": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.9.tgz", + "integrity": "sha512-VozH5uznUvGqD7n224FGj7cmMAenlS0HPCs+7r2HeeHiQK6un6z0CTZfWVAB860xbcr4m+BN/EGMPZmYWd34Rg==", + "requires": { + "@types/d3-interpolate": "^2.0.0", + "@types/d3-scale": "^3.0.0", + "@types/d3-shape": "^2.0.0", + "classnames": "^2.2.5", + "d3-interpolate": "^2.0.0", + "d3-scale": "^3.0.0", + "d3-shape": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-resize-detector": "^6.6.3", + "react-smooth": "^2.0.0", + "recharts-scale": "^0.4.4", + "reduce-css-calc": "^2.1.8" + }, + "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==" + } + } + }, + "recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "requires": { + "decimal.js-light": "^2.4.1" + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -73288,6 +73787,22 @@ "minimatch": "3.0.4" } }, + "reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", @@ -73772,6 +74287,11 @@ "dev": true, "optional": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index 5278aa60a..709c34a71 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -26,6 +26,7 @@ "clsx": "^1.1.1", "ethers": "^5.4.1", "js-base64": "^3.6.1", + "luxon": "^2.3.1", "notistack": "^1.0.10", "numeral": "^2.0.6", "react": "^17.0.2", @@ -34,6 +35,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "5.0.0", "react-table": "^7.7.0", + "recharts": "^2.1.9", "redux": "^3.7.2", "use-debounce": "^7.0.0" }, @@ -63,6 +65,7 @@ }, "devDependencies": { "@truffle/hdwallet-provider": "^1.4.1", + "@types/luxon": "^2.3.1", "@types/node": "^16.6.1", "@types/numeral": "^2.0.2", "@types/react-router-dom": "^5.1.8", diff --git a/bridge_ui/src/App.js b/bridge_ui/src/App.js index 142328466..5eb3cb0eb 100644 --- a/bridge_ui/src/App.js +++ b/bridge_ui/src/App.js @@ -36,6 +36,7 @@ import NFT from "./components/NFT"; import NFTOriginVerifier from "./components/NFTOriginVerifier"; import Recovery from "./components/Recovery"; import Stats from "./components/Stats"; +import CustodyAddresses from "./components/Stats/CustodyAddresses"; import TokenOriginVerifier from "./components/TokenOriginVerifier"; import Transfer from "./components/Transfer"; import UnwrapNative from "./components/UnwrapNative"; @@ -85,19 +86,13 @@ const useStyles = makeStyles((theme) => ({ position: "relative", overflow: "hidden", }, - content: { - margin: theme.spacing(2, 0), - [theme.breakpoints.up("md")]: { - margin: theme.spacing(4, 0), - }, - }, headerImage: { position: "absolute", zIndex: -1, top: 0, background: `url(${Header})`, backgroundRepeat: "no-repeat", - backgroundPosition: "top -500px center", + backgroundPosition: "top -750px center", backgroundSize: "2070px 1155px", width: "100%", height: 1155, @@ -291,90 +286,91 @@ function App() { ) : null} -
-
- {["/transfer", "/nft", "/redeem"].includes(pathname) ? ( - - - - Portal is a bridge that offers unlimited transfers across - chains for tokens and NFTs wrapped by Wormhole. - - - Unlike many other bridges, you avoid double wrapping and - never have to retrace your steps. - - - } - > - Token Bridge - - - - - - - - ) : null} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ {["/transfer", "/nft", "/redeem"].includes(pathname) ? ( + + + + Portal is a bridge that offers unlimited transfers across + chains for tokens and NFTs wrapped by Wormhole. + + + Unlike many other bridges, you avoid double wrapping and never + have to retrace your steps. + + + } + > + Token Bridge + + + + + + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/bridge_ui/src/components/Footer.tsx b/bridge_ui/src/components/Footer.tsx index d3c87d6c3..37a8ef3df 100644 --- a/bridge_ui/src/components/Footer.tsx +++ b/bridge_ui/src/components/Footer.tsx @@ -189,6 +189,16 @@ export default function Footer() { > Wormhole + + Custody +
diff --git a/bridge_ui/src/components/HeaderText.tsx b/bridge_ui/src/components/HeaderText.tsx index 248836a12..58739f56d 100644 --- a/bridge_ui/src/components/HeaderText.tsx +++ b/bridge_ui/src/components/HeaderText.tsx @@ -5,9 +5,7 @@ import { COLORS } from "../muiTheme"; const useStyles = makeStyles((theme) => ({ centeredContainer: { - marginTop: theme.spacing(14), - marginBottom: theme.spacing(26), - minHeight: 208, + marginBottom: theme.spacing(24), textAlign: "center", width: "100%", }, @@ -19,6 +17,9 @@ const useStyles = makeStyles((theme) => ({ MozBackgroundClip: "text", MozTextFillColor: "transparent", }, + subtitle: { + marginTop: theme.spacing(2), + }, })); export default function HeaderText({ @@ -39,11 +40,14 @@ export default function HeaderText({ variant={small ? "h2" : "h1"} component="h1" className={clsx({ [classes.linearGradient]: !white })} - gutterBottom={!!subtitle} > {children} - {subtitle ? {subtitle} : null} + {subtitle ? ( + + {subtitle} + + ) : null}
); } diff --git a/bridge_ui/src/components/Stats/Charts/CustomTooltip.tsx b/bridge_ui/src/components/Stats/Charts/CustomTooltip.tsx new file mode 100644 index 000000000..fb6c037f6 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/CustomTooltip.tsx @@ -0,0 +1,46 @@ +import { makeStyles, Typography } from "@material-ui/core"; +import { formatDate } from "./utils"; + +const useStyles = makeStyles(() => ({ + container: { + padding: "16px", + minWidth: "214px", + background: "rgba(255, 255, 255, 0.95)", + borderRadius: "4px", + }, + titleText: { + color: "#21227E", + fontSize: "24px", + fontWeight: 500, + }, + ruler: { + height: "3px", + backgroundImage: "linear-gradient(90deg, #F44B1B 0%, #EEB430 100%)", + }, + valueText: { + color: "#404040", + fontSize: "18px", + fontWeight: 500, + }, +})); + +const CustomTooltip = ({ active, payload, title, valueFormatter }: any) => { + const classes = useStyles(); + if (active && payload && payload.length) { + return ( +
+ {title} +
+ + {valueFormatter(payload[0].value)} + + + {formatDate(payload[0].payload.date)} + +
+ ); + } + return null; +}; + +export default CustomTooltip; diff --git a/bridge_ui/src/components/Stats/Charts/MultiChainTooltip.tsx b/bridge_ui/src/components/Stats/Charts/MultiChainTooltip.tsx new file mode 100644 index 000000000..f86e470fb --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/MultiChainTooltip.tsx @@ -0,0 +1,123 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { makeStyles, Grid, Typography } from "@material-ui/core"; +import { + getChainShortName, + CHAINS_BY_ID, + COLOR_BY_CHAIN_ID, +} from "../../../utils/consts"; +import { formatDate } from "./utils"; + +const useStyles = makeStyles(() => ({ + container: { + padding: "16px", + minWidth: "214px", + background: "rgba(255, 255, 255, 0.95)", + borderRadius: "4px", + }, + titleText: { + color: "#21227E", + fontSize: "24px", + fontWeight: 500, + }, + row: { + display: "flex", + alignItems: "center", + marginBottom: "8px", + }, + ruler: { + height: "3px", + backgroundColor: "#374B92", + }, + valueText: { + color: "#404040", + fontSize: "18px", + fontWeight: 500, + }, + icon: { + width: "24px", + height: "24px", + }, +})); + +const MultiChainTooltip = ({ active, payload, title, valueFormatter }: any) => { + const classes = useStyles(); + if (active && payload && payload.length) { + if (payload.length === 1) { + const chainId = +payload[0].dataKey.split(".")[1] as ChainId; + const chainShortName = getChainShortName(chainId); + const data = payload.find((data: any) => data.name === chainShortName); + if (data) { + return ( +
+ + {chainShortName} + + {chainShortName} + + +
+ + {valueFormatter(data.value)} + + + {formatDate(data.payload.date)} + +
+ ); + } + } else { + return ( +
+ + {title} + + + {formatDate(payload[0].payload.date)} + +
+ {payload.map((data: any) => { + return ( +
+
+ + {data.name} + + + {valueFormatter(data.value)} + +
+ ); + })} +
+ ); + } + } + return null; +}; + +export default MultiChainTooltip; diff --git a/bridge_ui/src/components/Stats/Charts/TVLAreaChart.tsx b/bridge_ui/src/components/Stats/Charts/TVLAreaChart.tsx new file mode 100644 index 000000000..b2d3c9cce --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TVLAreaChart.tsx @@ -0,0 +1,63 @@ +import { + AreaChart, + Area, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { formatTVL, createCumulativeTVLChartData } from "./utils"; +import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL"; +import { useMemo } from "react"; +import { TimeFrame } from "./TimeFrame"; +import CustomTooltip from "./CustomTooltip"; +import { useTheme, useMediaQuery } from "@material-ui/core"; + +const TVLAreaChart = ({ + cumulativeTVL, + timeFrame, +}: { + cumulativeTVL: NotionalTVLCumulative; + timeFrame: TimeFrame; +}) => { + const data = useMemo(() => { + return createCumulativeTVLChartData(cumulativeTVL, timeFrame); + }, [cumulativeTVL, timeFrame]); + + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + } + /> + + + + + + + + + + ); +}; + +export default TVLAreaChart; diff --git a/bridge_ui/src/components/Stats/Charts/TVLBarChart.tsx b/bridge_ui/src/components/Stats/Charts/TVLBarChart.tsx new file mode 100644 index 000000000..0885e8237 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TVLBarChart.tsx @@ -0,0 +1,116 @@ +import { ChainId, CHAIN_ID_ETH } from "@certusone/wormhole-sdk"; +import { + Button, + makeStyles, + Typography, + useMediaQuery, + useTheme, +} from "@material-ui/core"; +import { ArrowForward } from "@material-ui/icons"; +import { useCallback, useMemo, useState } from "react"; +import { NotionalTVL } from "../../../hooks/useTVL"; +import { ChainInfo, getChainShortName } from "../../../utils/consts"; +import { createChainTVLChartData, formatTVL } from "./utils"; + +const useStyles = makeStyles(() => ({ + table: { + borderSpacing: "16px", + overflowX: "auto", + display: "block", + }, + button: { + height: "30px", + textTransform: "none", + width: "150px", + fontSize: "12px", + }, +})); + +const TVLBarChart = ({ + tvl, + onChainSelected, +}: { + tvl: NotionalTVL; + onChainSelected: (chainInfo: ChainInfo) => void; +}) => { + const classes = useStyles(); + + const [mouseOverChainId, setMouseOverChainId] = + useState(CHAIN_ID_ETH); + + const chainTVLs = useMemo(() => { + return createChainTVLChartData(tvl); + }, [tvl]); + + const handleClick = useCallback( + (chainInfo: ChainInfo) => { + onChainSelected(chainInfo); + }, + [onChainSelected] + ); + + const handleMouseOver = useCallback((chainId: ChainId) => { + setMouseOverChainId(chainId); + }, []); + + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); + + return ( + + + {chainTVLs.map((chainTVL) => ( + handleMouseOver(chainTVL.chainInfo.id)} + > + + + + + + + ))} + +
+ + {getChainShortName(chainTVL.chainInfo.id)} + + + {""} + +
+
+ + {formatTVL(chainTVL.tvl)} + + + {isSmall || mouseOverChainId === chainTVL.chainInfo.id ? ( + + ) : ( +
+ )} +
+ ); +}; + +export default TVLBarChart; diff --git a/bridge_ui/src/components/Stats/Charts/TVLLineChart.tsx b/bridge_ui/src/components/Stats/Charts/TVLLineChart.tsx new file mode 100644 index 000000000..ff6357d96 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TVLLineChart.tsx @@ -0,0 +1,86 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { useTheme, useMediaQuery } from "@material-ui/core"; +import { useMemo } from "react"; +import { + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL"; +import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts"; +import MultiChainTooltip from "./MultiChainTooltip"; +import { TimeFrame } from "./TimeFrame"; +import { + formatTVL, + createCumulativeTVLChartData, + renderLegendText, +} from "./utils"; + +const TVLLineChart = ({ + cumulativeTVL, + timeFrame, + selectedChains, +}: { + cumulativeTVL: NotionalTVLCumulative; + timeFrame: TimeFrame; + selectedChains: ChainId[]; +}) => { + const data = useMemo(() => { + return createCumulativeTVLChartData(cumulativeTVL, timeFrame); + }, [cumulativeTVL, timeFrame]); + + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + + } + /> + {selectedChains.map((chainId) => ( + + ))} + + + + ); +}; + +export default TVLLineChart; diff --git a/bridge_ui/src/components/Stats/Charts/TVLTable.tsx b/bridge_ui/src/components/Stats/Charts/TVLTable.tsx new file mode 100644 index 000000000..17a49998a --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TVLTable.tsx @@ -0,0 +1,141 @@ +import { makeStyles } from "@material-ui/core"; +import numeral from "numeral"; +import { useMemo } from "react"; +import { createTVLArray, NotionalTVL } from "../../../hooks/useTVL"; +import { ChainInfo } from "../../../utils/consts"; +import SmartAddress from "../../SmartAddress"; +import MuiReactTable from "../tableComponents/MuiReactTable"; +import { formatTVL } from "./utils"; + +const useStyles = makeStyles((theme) => ({ + logoPositioner: { + height: "30px", + width: "30px", + maxWidth: "30px", + marginRight: theme.spacing(1), + display: "flex", + alignItems: "center", + }, + logo: { + maxHeight: "100%", + maxWidth: "100%", + }, + tokenContainer: { + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + }, +})); + +const TVLTable = ({ + chainInfo, + tvl, +}: { + chainInfo: ChainInfo; + tvl: NotionalTVL; +}) => { + const classes = useStyles(); + const chainTVL = useMemo(() => { + return createTVLArray(tvl).filter((x) => x.originChainId === chainInfo.id); + }, [chainInfo, tvl]); + + const sortTokens = useMemo(() => { + return (rowA: any, rowB: any) => { + if (rowA.isGrouped && rowB.isGrouped) { + return rowA.values.assetAddress > rowB.values.assetAddress ? 1 : -1; + } else if (rowA.isGrouped && !rowB.isGrouped) { + return 1; + } else if (!rowA.isGrouped && rowB.isGrouped) { + return -1; + } else if (rowA.original.symbol && !rowB.original.symbol) { + return 1; + } else if (rowB.original.symbol && !rowA.original.symbol) { + return -1; + } else if (rowA.original.symbol && rowB.original.symbol) { + return rowA.original.symbol > rowB.original.symbol ? 1 : -1; + } else { + return rowA.original.assetAddress > rowB.original.assetAddress ? 1 : -1; + } + }; + }, []); + const tvlColumns = useMemo(() => { + return [ + { + Header: "Token", + id: "assetAddress", + sortType: sortTokens, + disableGroupBy: true, + accessor: (value: any) => ({ + chainId: value.originChainId, + symbol: value.symbol, + name: value.name, + logo: value.logo, + assetAddress: value.assetAddress, + }), + Cell: (value: any) => ( +
+
+ {value.row?.original?.logo ? ( + + ) : null} +
+ +
+ ), + }, + { + Header: "Quantity", + accessor: "amount", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.amount !== undefined + ? numeral(value.row?.original?.amount).format("0,0.00") + : "", + }, + { + Header: "Unit Price", + accessor: "quotePrice", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.quotePrice !== undefined + ? numeral(value.row?.original?.quotePrice).format("0,0.00") + : "", + }, + { + Header: "Value (USD)", + id: "totalValue", + accessor: "totalValue", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.totalValue !== undefined + ? formatTVL(value.row?.original?.totalValue) + : "", + }, + ]; + }, [ + classes.logo, + classes.tokenContainer, + classes.logoPositioner, + sortTokens, + ]); + + return ( + + ); +}; + +export default TVLTable; diff --git a/bridge_ui/src/components/Stats/Charts/TimeFrame.ts b/bridge_ui/src/components/Stats/Charts/TimeFrame.ts new file mode 100644 index 000000000..e81109548 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TimeFrame.ts @@ -0,0 +1,34 @@ +import { DurationLike } from "luxon"; +import { formatTickDay, formatTickMonth } from "./utils"; + +export interface TimeFrame { + interval?: number; + duration?: DurationLike; + tickFormatter: (value: any, index: number) => string; +} + +export const TIME_FRAMES: { [key: string]: TimeFrame } = { + "7 days": { + duration: { days: 7 }, + tickFormatter: formatTickDay, + }, + "30 days": { + duration: { days: 30 }, + tickFormatter: formatTickDay, + }, + "3 months": { + duration: { months: 3 }, + tickFormatter: formatTickDay, + }, + "6 months": { + duration: { months: 6 }, + interval: 30, + tickFormatter: formatTickMonth, + }, + "1 year": { + duration: { years: 1 }, + interval: 30, + tickFormatter: formatTickMonth, + }, + "All time": { interval: 30, tickFormatter: formatTickMonth }, +}; diff --git a/bridge_ui/src/components/Stats/Charts/TransactionsAreaChart.tsx b/bridge_ui/src/components/Stats/Charts/TransactionsAreaChart.tsx new file mode 100644 index 000000000..3c6994039 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TransactionsAreaChart.tsx @@ -0,0 +1,68 @@ +import { useTheme, useMediaQuery } from "@material-ui/core"; +import { useCallback } from "react"; +import { + Area, + AreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import CustomTooltip from "./CustomTooltip"; +import { TimeFrame } from "./TimeFrame"; +import { formatTransactionCount, TransactionData } from "./utils"; + +const TransactionsAreaChart = ({ + transactionData, + timeFrame, +}: { + transactionData: TransactionData[]; + timeFrame: TimeFrame; +}) => { + const formatValue = useCallback((value: number) => { + return `${formatTransactionCount(value)} transactions`; + }, []); + + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + + } + /> + + + + + + + + + + ); +}; + +export default TransactionsAreaChart; diff --git a/bridge_ui/src/components/Stats/Charts/TransactionsLineChart.tsx b/bridge_ui/src/components/Stats/Charts/TransactionsLineChart.tsx new file mode 100644 index 000000000..1ac928149 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/TransactionsLineChart.tsx @@ -0,0 +1,85 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { useTheme, useMediaQuery } from "@material-ui/core"; +import { useCallback } from "react"; +import { + ResponsiveContainer, + LineChart, + XAxis, + YAxis, + Line, + Legend, + Tooltip, +} from "recharts"; +import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts"; +import MultiChainTooltip from "./MultiChainTooltip"; +import { TimeFrame } from "./TimeFrame"; +import { + formatTransactionCount, + renderLegendText, + TransactionData, +} from "./utils"; + +const TransactionsLineChart = ({ + transactionData, + timeFrame, + chains, +}: { + transactionData: TransactionData[]; + timeFrame: TimeFrame; + chains: ChainId[]; +}) => { + const formatValue = useCallback((value: number) => { + return `${formatTransactionCount(value)} transactions`; + }, []); + + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + + } + /> + {chains.map((chainId) => ( + + ))} + + + + ); +}; + +export default TransactionsLineChart; diff --git a/bridge_ui/src/components/Stats/Charts/VolumeAreaChart.tsx b/bridge_ui/src/components/Stats/Charts/VolumeAreaChart.tsx new file mode 100644 index 000000000..0dadd11f1 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/VolumeAreaChart.tsx @@ -0,0 +1,63 @@ +import { useTheme, useMediaQuery } from "@material-ui/core"; +import { + Area, + AreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import CustomTooltip from "./CustomTooltip"; +import { TimeFrame } from "./TimeFrame"; +import { TransferChartData, formatTVL } from "./utils"; + +const VolumeAreaChart = ({ + transferData, + timeFrame, +}: { + transferData: TransferChartData[]; + timeFrame: TimeFrame; +}) => { + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + + } + /> + + + + + + + + + + ); +}; + +export default VolumeAreaChart; diff --git a/bridge_ui/src/components/Stats/Charts/VolumeLineChart.tsx b/bridge_ui/src/components/Stats/Charts/VolumeLineChart.tsx new file mode 100644 index 000000000..06b30949f --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/VolumeLineChart.tsx @@ -0,0 +1,76 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { useTheme, useMediaQuery } from "@material-ui/core"; +import { + Legend, + ResponsiveContainer, + LineChart, + XAxis, + YAxis, + Line, + Tooltip, +} from "recharts"; +import { COLOR_BY_CHAIN_ID, getChainShortName } from "../../../utils/consts"; +import MultiChainTooltip from "./MultiChainTooltip"; +import { TimeFrame } from "./TimeFrame"; +import { formatTVL, renderLegendText, TransferChartData } from "./utils"; + +const VolumeLineChart = ({ + transferData, + timeFrame, + chains, +}: { + transferData: TransferChartData[]; + timeFrame: TimeFrame; + chains: ChainId[]; +}) => { + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + + + } + /> + {chains.map((chainId) => ( + + ))} + + + + ); +}; + +export default VolumeLineChart; diff --git a/bridge_ui/src/components/Stats/Charts/VolumeStackedBarChart.tsx b/bridge_ui/src/components/Stats/Charts/VolumeStackedBarChart.tsx new file mode 100644 index 000000000..9f42ffe54 --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/VolumeStackedBarChart.tsx @@ -0,0 +1,196 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { + Typography, + makeStyles, + Grid, + useMediaQuery, + useTheme, +} from "@material-ui/core"; +import { useMemo, useState } from "react"; +import { + ResponsiveContainer, + BarChart, + XAxis, + YAxis, + Tooltip, + Bar, + Legend, +} from "recharts"; +import { + CHAINS_BY_ID, + COLOR_BY_CHAIN_ID, + getChainShortName, +} from "../../../utils/consts"; +import { TimeFrame } from "./TimeFrame"; +import { formatDate, TransferChartData, formatTVL, renderLegendText } from "./utils"; + +const useStyles = makeStyles(() => ({ + tooltipContainer: { + padding: "16px", + minWidth: "214px", + background: "rgba(255, 255, 255, 0.95)", + borderRadius: "4px", + }, + tooltipTitleText: { + color: "#21227E", + fontSize: "24px", + fontWeight: 500, + marginLeft: "8px", + }, + tooltipRuler: { + height: "3px", + }, + tooltipValueText: { + color: "#404040", + fontSize: "18px", + fontWeight: 500, + }, + tooltipIcon: { + width: "24px", + height: "24px", + }, +})); + +interface BarData { + date: Date; + volume: { + [chainId: string]: number; + }; + volumePercent: { + [chainId: string]: number; + }; +} + +const createBarData = ( + transferData: TransferChartData[], + selectedChains: ChainId[] +) => { + return transferData.reduce((barData, transfer) => { + const data: BarData = { + date: transfer.date, + volume: {}, + volumePercent: {}, + }; + const totalVolume = Object.entries(transfer.transferredByChain).reduce( + (totalVolume, [chainId, volume]) => { + if (selectedChains.indexOf(+chainId as ChainId) > -1) { + data.volume[chainId] = volume; + return totalVolume + volume; + } + return totalVolume; + }, + 0 + ); + if (totalVolume > 0) { + Object.keys(data.volume).forEach((chainId) => { + data.volumePercent[chainId] = + (data.volume[chainId] / totalVolume) * 100; + }); + } + barData.push(data); + return barData; + }, []); +}; + +const CustomTooltip = ({ active, payload, chainId }: any) => { + const classes = useStyles(); + if (active && payload && payload.length && chainId) { + const chainShortName = getChainShortName(chainId); + const data = payload.find((data: any) => data.name === chainShortName); + if (data) { + return ( +
+ + {chainShortName} + + {chainShortName} + + +
+ {`${data.value.toFixed(1)}%`} + + {formatTVL(data.payload.volume[chainId])} + + + {formatDate(data.payload.date)} + +
+ ); + } + } + return null; +}; + +const VolumeStackedBarChart = ({ + transferData, + timeFrame, + selectedChains, +}: { + transferData: TransferChartData[]; + timeFrame: TimeFrame; + selectedChains: ChainId[]; +}) => { + const [hoverChainId, setHoverChainId] = useState(null); + + const barData = useMemo(() => { + return createBarData(transferData, selectedChains); + }, [transferData, selectedChains]); + + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down("xs")); + + return ( + + + + `${tick}%`} + ticks={[0, 25, 50, 75, 100]} + domain={[0, 100]} + tick={{ fill: "white" }} + axisLine={false} + tickLine={false} + /> + } + cursor={{ fill: "transparent" }} + /> + {selectedChains.map((chainId) => ( + setHoverChainId(chainId)} + /> + ))} + + + + ); +}; + +export default VolumeStackedBarChart; diff --git a/bridge_ui/src/components/Stats/Charts/utils.tsx b/bridge_ui/src/components/Stats/Charts/utils.tsx new file mode 100644 index 000000000..d1e047fed --- /dev/null +++ b/bridge_ui/src/components/Stats/Charts/utils.tsx @@ -0,0 +1,212 @@ +import { NotionalTVLCumulative } from "../../../hooks/useCumulativeTVL"; +import { NotionalTransferredFrom } from "../../../hooks/useNotionalTransferred"; +import { TimeFrame } from "./TimeFrame"; +import { DateTime } from "luxon"; +import { Totals } from "../../../hooks/useTransactionTotals"; +import { + ChainInfo, + CHAINS_BY_ID, + VAA_EMITTER_ADDRESSES, +} from "../../../utils/consts"; +import { NotionalTVL } from "../../../hooks/useTVL"; +import { ChainId } from "@certusone/wormhole-sdk"; + +export const formatTVL = (tvl: number) => { + const [divisor, unit, fractionDigits] = + tvl < 1e3 + ? [1, "", 0] + : tvl < 1e6 + ? [1e3, "K", 0] + : tvl < 1e9 + ? [1e6, "M", 0] + : [1e9, "B", 2]; + return `$${(tvl / divisor).toFixed(fractionDigits)} ${unit}`; +}; + +export const formatDate = (date: Date) => { + return date.toLocaleString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + timeZone: "UTC", + }); +}; + +export const formatTickDay = (date: Date) => { + return date.toLocaleString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "UTC", + }); +}; + +export const formatTickMonth = (date: Date) => { + return date.toLocaleString("en-US", { + month: "short", + year: "numeric", + timeZone: "UTC", + }); +}; + +export const formatTransactionCount = (transactionCount: number) => { + return transactionCount.toLocaleString("en-US"); +}; + +export const renderLegendText = (value: any) => { + return {value}; +}; + +export const getStartDate = (timeFrame: TimeFrame) => { + return timeFrame.duration + ? DateTime.now().toUTC().minus(timeFrame.duration).toJSDate() + : undefined; +}; + +export interface CumulativeTVLChartData { + date: Date; + totalTVL: number; + tvlByChain: { + [chainId: string]: number; + }; +} + +export const createCumulativeTVLChartData = ( + cumulativeTVL: NotionalTVLCumulative, + timeFrame: TimeFrame +) => { + const startDate = getStartDate(timeFrame); + return Object.entries(cumulativeTVL.DailyLocked) + .reduce( + (chartData, [dateString, chainsAssets]) => { + const date = new Date(dateString); + if (!startDate || date >= startDate) { + const data: CumulativeTVLChartData = { + date: date, + totalTVL: 0, + tvlByChain: {}, + }; + Object.entries(chainsAssets).forEach(([chainId, lockedAssets]) => { + const notional = lockedAssets["*"].Notional; + if (chainId === "*") { + data.totalTVL = notional; + } else { + data.tvlByChain[chainId] = notional; + } + }); + chartData.push(data); + } + return chartData; + }, + [] + ) + .sort((a, z) => a.date.getTime() - z.date.getTime()); +}; + +export interface TransferChartData { + date: Date; + totalTransferred: number; + transferredByChain: { + [chainId: string]: number; + }; +} + +export const createCumulativeTransferChartData = ( + notionalTransferredFrom: NotionalTransferredFrom, + timeFrame: TimeFrame +) => { + const startDate = getStartDate(timeFrame); + return Object.keys(notionalTransferredFrom.Daily) + .sort() + .reduce((chartData, dateString) => { + const transferFromData = notionalTransferredFrom.Daily[dateString]; + const data: TransferChartData = { + date: new Date(dateString), + totalTransferred: 0, + transferredByChain: {}, + }; + Object.entries(transferFromData).forEach(([chainId, amount]) => { + if (chainId === "*") { + data.totalTransferred = + (chartData[chartData.length - 1]?.totalTransferred || 0) + amount; + } else { + data.transferredByChain[chainId] = + (chartData[chartData.length - 1]?.transferredByChain[chainId] || + 0) + amount; + } + }); + chartData.push(data); + return chartData; + }, []) + .filter((value) => !startDate || startDate <= value.date); +}; + +export interface TransactionData { + date: Date; + totalTransactions: number; + transactionsByChain: { + [chainId: string]: number; + }; +} + +export const createCumulativeTransactionData = ( + totals: Totals, + timeFrame: TimeFrame +) => { + const startDate = getStartDate(timeFrame); + return Object.keys(totals.DailyTotals) + .sort() + .reduce((chartData, dateString) => { + const groupByKeys = totals.DailyTotals[dateString]; + const prevData = chartData[chartData.length - 1]; + const data: TransactionData = { + date: new Date(dateString), + totalTransactions: prevData?.totalTransactions || 0, + transactionsByChain: {}, + }; + VAA_EMITTER_ADDRESSES.forEach((address) => { + const count = groupByKeys[address] || 0; + data.totalTransactions += count; + const chainId = address.slice(0, address.indexOf(":")); + if (data.transactionsByChain[chainId] === undefined) { + data.transactionsByChain[chainId] = + prevData?.transactionsByChain[chainId] || 0; + } + data.transactionsByChain[chainId] += count; + }); + chartData.push(data); + return chartData; + }, []) + .filter((value) => !startDate || startDate <= value.date); +}; + +export interface ChainTVLChartData { + chainInfo: ChainInfo; + tvl: number; + tvlRatio: number; +} + +export const createChainTVLChartData = (tvl: NotionalTVL) => { + let maxTVL = 0; + const chainTVLs = Object.entries(tvl.AllTime) + .reduce((chartData, [chainId, assets]) => { + const chainInfo = CHAINS_BY_ID[+chainId as ChainId]; + if (chainInfo !== undefined) { + const tvl = assets["*"].Notional; + chartData.push({ + chainInfo: chainInfo, + tvl: tvl, + tvlRatio: 0, + }); + maxTVL = Math.max(maxTVL, tvl); + } + return chartData; + }, []) + .sort((a, z) => z.tvl - a.tvl); + if (maxTVL > 0) { + chainTVLs.forEach((chainTVL) => { + chainTVL.tvlRatio = (chainTVL.tvl / maxTVL) * 100; + }); + } + return chainTVLs; +}; diff --git a/bridge_ui/src/components/Stats/CustodyAddresses.tsx b/bridge_ui/src/components/Stats/CustodyAddresses.tsx index 2dec03a85..111981782 100644 --- a/bridge_ui/src/components/Stats/CustodyAddresses.tsx +++ b/bridge_ui/src/components/Stats/CustodyAddresses.tsx @@ -1,4 +1,5 @@ import { + CHAIN_ID_AURORA, CHAIN_ID_AVAX, CHAIN_ID_BSC, CHAIN_ID_ETH, @@ -8,7 +9,7 @@ import { CHAIN_ID_SOLANA, CHAIN_ID_TERRA, } from "@certusone/wormhole-sdk"; -import { makeStyles, Paper, Typography } from "@material-ui/core"; +import { Container, makeStyles, Paper, Typography } from "@material-ui/core"; import { useMemo } from "react"; import { COLORS } from "../../muiTheme"; import { @@ -17,6 +18,7 @@ import { SOL_CUSTODY_ADDRESS, SOL_NFT_CUSTODY_ADDRESS, } from "../../utils/consts"; +import HeaderText from "../HeaderText"; import SmartAddress from "../SmartAddress"; import MuiReactTable from "./tableComponents/MuiReactTable"; @@ -97,6 +99,12 @@ const CustodyAddresses: React.FC = () => { tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_FANTOM), nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_FANTOM), }, + { + chainName: "Aurora", + chainId: CHAIN_ID_AURORA, + tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_AURORA), + nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_AURORA), + }, ]; }, []); @@ -159,10 +167,13 @@ const CustodyAddresses: React.FC = () => { ); return ( - <> + + + Custody + {header} {table} - + ); }; diff --git a/bridge_ui/src/components/Stats/TVLStats.tsx b/bridge_ui/src/components/Stats/TVLStats.tsx new file mode 100644 index 000000000..7b80e7237 --- /dev/null +++ b/bridge_ui/src/components/Stats/TVLStats.tsx @@ -0,0 +1,298 @@ +import { + Button, + Checkbox, + CircularProgress, + FormControl, + ListItemText, + makeStyles, + MenuItem, + Paper, + Select, + TextField, + Tooltip, + Typography, + withStyles, +} from "@material-ui/core"; +import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab"; +import { useCallback, useMemo, useState } from "react"; +import TVLAreaChart from "./Charts/TVLAreaChart"; +import useCumulativeTVL from "../../hooks/useCumulativeTVL"; +import { TIME_FRAMES } from "./Charts/TimeFrame"; +import TVLLineChart from "./Charts/TVLLineChart"; +import { ChainInfo, CHAINS_BY_ID } from "../../utils/consts"; +import { ChainId } from "@certusone/wormhole-sdk"; +import { COLORS } from "../../muiTheme"; +import TVLBarChart from "./Charts/TVLBarChart"; +import TVLTable from "./Charts/TVLTable"; +import useTVL from "../../hooks/useTVL"; +import { ArrowBack, InfoOutlined } from "@material-ui/icons"; + +const useStyles = makeStyles((theme) => ({ + description: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + marginBottom: "16px", + [theme.breakpoints.down("xs")]: { + flexDirection: "column", + }, + }, + displayBy: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", + marginBottom: "16px", + [theme.breakpoints.down("xs")]: { + justifyContent: "center", + columnGap: 8, + rowGap: 8, + }, + }, + mainPaper: { + display: "flex", + flexDirection: "column", + backgroundColor: COLORS.whiteWithTransparency, + padding: "2rem", + marginBottom: theme.spacing(8), + borderRadius: 8, + }, + toggleButton: { + textTransform: "none", + }, + tooltip: { + margin: 8, + }, + alignCenter: { + margin: "0 auto", + display: "block", + }, +})); + +const tooltipStyles = { + tooltip: { + minWidth: "max-content", + borderRadius: "4px", + backgroundColor: "#5EA1EC", + color: "#0F0C48", + fontSize: "14px", + }, +}; + +const StyledTooltip = withStyles(tooltipStyles)(Tooltip); + +const DISPLAY_BY_VALUES = ["Time", "Chain"]; + +const TVLStats = () => { + const classes = useStyles(); + + const [displayBy, setDisplayBy] = useState(DISPLAY_BY_VALUES[0]); + const [timeFrame, setTimeFrame] = useState("All time"); + + const [selectedChains, setSelectedChains] = useState([]); + + const [selectedChainDetail, setSelectedChainDetail] = + useState(null); + + const cumulativeTVL = useCumulativeTVL(); + const tvl = useTVL(); + + const tvlAllTime = useMemo(() => { + return tvl.data + ? new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format( + tvl.data.AllTime[selectedChainDetail?.id || "*"]["*"].Notional || 0 + ) + : ""; + }, [selectedChainDetail, tvl]); + + const availableChains = useMemo(() => { + const chainIds = cumulativeTVL.data + ? Object.keys( + Object.values(cumulativeTVL.data.DailyLocked)[0] || {} + ).reduce((chainIds, key) => { + if (key !== "*") { + const chainId = parseInt(key) as ChainId; + if (CHAINS_BY_ID[chainId]) { + chainIds.push(chainId); + } + } + return chainIds; + }, []) + : []; + setSelectedChains(chainIds); + return chainIds; + }, [cumulativeTVL]); + + const handleDisplayByChange = useCallback((event, nextValue) => { + if (nextValue) { + setDisplayBy(nextValue); + } + }, []); + + const handleTimeFrameChange = useCallback( + (event) => setTimeFrame(event.target.value), + [] + ); + + const handleSelectedChainsChange = useCallback( + (event) => { + const value = event.target.value; + if (value[value.length - 1] === "all") { + setSelectedChains((prevValue) => + prevValue.length === availableChains.length ? [] : availableChains + ); + } else { + setSelectedChains(value); + } + }, + [availableChains] + ); + + const handleChainDetailSelected = useCallback((chainInfo: ChainInfo) => { + setSelectedChainDetail(chainInfo); + }, []); + + const allChainsSelected = selectedChains.length === availableChains.length; + const tvlText = + "Total Value Locked" + + (selectedChainDetail ? ` on ${selectedChainDetail?.name}` : ""); + const tooltipText = selectedChainDetail + ? `Total Value Locked on ${selectedChainDetail?.name}` + : "USD equivalent value of all assets locked in Portal"; + + return ( + <> +
+ + {tvlText} + + + + + {tvlAllTime} +
+
+ {!selectedChainDetail ? ( +
+ + Display by + + + {DISPLAY_BY_VALUES.map((value) => ( + + {value} + + ))} + +
+ ) : null} + {displayBy === "Time" && !selectedChainDetail ? ( +
+ + + + + {Object.keys(TIME_FRAMES).map((timeFrame) => ( + + {timeFrame} + + ))} + +
+ ) : selectedChainDetail ? ( + + ) : null} +
+ + {displayBy === "Time" ? ( + cumulativeTVL.data ? ( + allChainsSelected ? ( + + ) : ( + + ) + ) : ( + + ) + ) : tvl.data ? ( + selectedChainDetail ? ( + + ) : ( + + ) + ) : ( + + )} + + + ); +}; + +export default TVLStats; diff --git a/bridge_ui/src/components/Stats/VolumeStats.tsx b/bridge_ui/src/components/Stats/VolumeStats.tsx new file mode 100644 index 000000000..be2399851 --- /dev/null +++ b/bridge_ui/src/components/Stats/VolumeStats.tsx @@ -0,0 +1,325 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { + Checkbox, + CircularProgress, + FormControl, + ListItemText, + makeStyles, + MenuItem, + Paper, + Select, + TextField, + Tooltip, + Typography, + withStyles, +} from "@material-ui/core"; +import { InfoOutlined } from "@material-ui/icons"; +import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab"; +import { useCallback, useMemo, useState } from "react"; +import useNotionalTransferred from "../../hooks/useNotionalTransferred"; +import { COLORS } from "../../muiTheme"; +import { CHAINS_BY_ID } from "../../utils/consts"; +import { TIME_FRAMES } from "./Charts/TimeFrame"; +import { + createCumulativeTransferChartData, + createCumulativeTransactionData, + formatTransactionCount, +} from "./Charts/utils"; +import VolumeAreaChart from "./Charts/VolumeAreaChart"; +import VolumeStackedBarChart from "./Charts/VolumeStackedBarChart"; +import VolumeLineChart from "./Charts/VolumeLineChart"; +import TransactionsAreaChart from "./Charts/TransactionsAreaChart"; +import TransactionsLineChart from "./Charts/TransactionsLineChart"; +import useTransactionTotals from "../../hooks/useTransactionTotals"; + +const DISPLAY_BY_VALUES = ["Dollar", "Percent", "Transactions"]; + +const useStyles = makeStyles((theme) => ({ + description: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + marginBottom: "16px", + [theme.breakpoints.down("xs")]: { + flexDirection: "column", + }, + }, + displayBy: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", + marginBottom: "16px", + [theme.breakpoints.down("xs")]: { + justifyContent: "center", + columnGap: 8, + rowGap: 8, + }, + }, + mainPaper: { + display: "flex", + flexDirection: "column", + backgroundColor: COLORS.whiteWithTransparency, + padding: "2rem", + marginBottom: theme.spacing(8), + borderRadius: 8, + }, + toggleButton: { + textTransform: "none", + }, + tooltip: { + margin: 8, + }, + alignCenter: { + margin: "0 auto", + display: "block", + }, +})); + +const tooltipStyles = { + tooltip: { + minWidth: "max-content", + borderRadius: "4px", + backgroundColor: "#5EA1EC", + color: "#0F0C48", + fontSize: "14px", + }, +}; + +const StyledTooltip = withStyles(tooltipStyles)(Tooltip); + +const VolumeStats = () => { + const classes = useStyles(); + + const [displayBy, setDisplayBy] = useState(DISPLAY_BY_VALUES[0]); + const [timeFrame, setTimeFrame] = useState("All time"); + + const [selectedChains, setSelectedChains] = useState([]); + + const notionalTransferred = useNotionalTransferred(); + + const transferData = useMemo(() => { + return notionalTransferred.data + ? createCumulativeTransferChartData( + notionalTransferred.data, + TIME_FRAMES[timeFrame] + ) + : []; + }, [notionalTransferred, timeFrame]); + + const transferredAllTime = useMemo(() => { + return notionalTransferred.data + ? new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(notionalTransferred.data.Total || 0) + : ""; + }, [notionalTransferred]); + + const transactionTotals = useTransactionTotals(); + + const transactionData = useMemo(() => { + return transactionTotals.data + ? createCumulativeTransactionData( + transactionTotals.data, + TIME_FRAMES[timeFrame] + ) + : []; + }, [transactionTotals, timeFrame]); + + const transactionsAllTime = useMemo(() => { + const totalTransactions = + transactionData[transactionData.length - 1]?.totalTransactions; + return totalTransactions !== undefined + ? formatTransactionCount(totalTransactions) + : ""; + }, [transactionData]); + + const availableChains = useMemo(() => { + const chainIds = notionalTransferred.data + ? Object.keys( + Object.values(notionalTransferred.data.Daily)[0] || {} + ).reduce((chainIds, key) => { + if (key !== "*") { + const chainId = parseInt(key) as ChainId; + if (CHAINS_BY_ID[chainId] !== undefined) { + chainIds.push(chainId); + } + } + return chainIds; + }, []) + : []; + setSelectedChains(chainIds); + return chainIds; + }, [notionalTransferred]); + + const handleDisplayByChange = useCallback((event, nextValue) => { + if (nextValue !== null) { + setDisplayBy(nextValue); + } + }, []); + + const handleTimeFrameChange = useCallback( + (event) => setTimeFrame(event.target.value), + [] + ); + + const handleSelectedChainsChange = useCallback( + (event) => { + const value = event.target.value; + if (value[value.length - 1] === "all") { + setSelectedChains((prevValue) => + prevValue.length === availableChains.length ? [] : availableChains + ); + } else { + setSelectedChains(value); + } + }, + [availableChains] + ); + + const allChainsSelected = selectedChains.length === availableChains.length; + + return ( + <> +
+ + {displayBy === "Transactions" + ? "Transaction Count" + : "Outbound Volume"} + + + + + + {displayBy === "Transactions" + ? transactionsAllTime + : transferredAllTime} + +
+
+
+ + Display by + + + {DISPLAY_BY_VALUES.map((value) => ( + + {value} + + ))} + +
+
+ + + + + {Object.keys(TIME_FRAMES).map((timeFrame) => ( + + {timeFrame} + + ))} + +
+
+ + {displayBy === "Dollar" ? ( + notionalTransferred.data ? ( + allChainsSelected ? ( + + ) : ( + + ) + ) : ( + + ) + ) : displayBy === "Percent" ? ( + + ) : transactionTotals.data ? ( + allChainsSelected ? ( + + ) : ( + + ) + ) : ( + + )} + + + ); +}; + +export default VolumeStats; diff --git a/bridge_ui/src/components/Stats/index.tsx b/bridge_ui/src/components/Stats/index.tsx index 768b805d6..dfea1748e 100644 --- a/bridge_ui/src/components/Stats/index.tsx +++ b/bridge_ui/src/components/Stats/index.tsx @@ -1,259 +1,16 @@ -import { BigNumber } from "@ethersproject/bignumber"; -import { formatUnits, parseUnits } from "@ethersproject/units"; -import { - CircularProgress, - Container, - makeStyles, - Paper, - Typography, -} from "@material-ui/core"; -import clsx from "clsx"; -import numeral from "numeral"; -import { useMemo } from "react"; -import useTVL from "../../hooks/useTVL"; -import { COLORS } from "../../muiTheme"; +import { Container } from "@material-ui/core"; import HeaderText from "../HeaderText"; -import SmartAddress from "../SmartAddress"; -import { balancePretty } from "../TokenSelectors/TokenPicker"; -import CustodyAddresses from "./CustodyAddresses"; -import NFTStats from "./NFTStats"; -import MuiReactTable from "./tableComponents/MuiReactTable"; -import TransactionMetrics from "./TransactionMetrics"; - -const useStyles = makeStyles((theme) => ({ - logoPositioner: { - height: "30px", - width: "30px", - maxWidth: "30px", - marginRight: theme.spacing(1), - display: "flex", - alignItems: "center", - }, - logo: { - maxHeight: "100%", - maxWidth: "100%", - }, - tokenContainer: { - display: "flex", - justifyContent: "flex-start", - alignItems: "center", - }, - mainPaper: { - backgroundColor: COLORS.whiteWithTransparency, - padding: "2rem", - "& > h, & > p ": { - margin: ".5rem", - }, - marginBottom: theme.spacing(8), - }, - flexBox: { - display: "flex", - alignItems: "flex-end", - marginBottom: theme.spacing(4), - textAlign: "left", - [theme.breakpoints.down("sm")]: { - flexDirection: "column", - alignItems: "unset", - }, - }, - grower: { - flexGrow: 1, - }, - explainerContainer: {}, - totalContainer: { - display: "flex", - alignItems: "flex-end", - paddingBottom: 1, // line up with left text bottom - [theme.breakpoints.down("sm")]: { - marginTop: theme.spacing(1), - }, - }, - totalValue: { - marginLeft: theme.spacing(0.5), - marginBottom: "-.125em", // line up number with label - }, - alignCenter: { - margin: "0 auto", - display: "block", - }, -})); - -const StatsRoot: React.FC = () => { - const classes = useStyles(); - const tvl = useTVL(); - - const sortTokens = useMemo(() => { - return (rowA: any, rowB: any) => { - if (rowA.isGrouped && rowB.isGrouped) { - return rowA.values.assetAddress > rowB.values.assetAddress ? 1 : -1; - } else if (rowA.isGrouped && !rowB.isGrouped) { - return 1; - } else if (!rowA.isGrouped && rowB.isGrouped) { - return -1; - } else if (rowA.original.symbol && !rowB.original.symbol) { - return 1; - } else if (rowB.original.symbol && !rowA.original.symbol) { - return -1; - } else if (rowA.original.symbol && rowB.original.symbol) { - return rowA.original.symbol > rowB.original.symbol ? 1 : -1; - } else { - return rowA.original.assetAddress > rowB.original.assetAddress ? 1 : -1; - } - }; - }, []); - const tvlColumns = useMemo(() => { - return [ - { - Header: "Token", - id: "assetAddress", - sortType: sortTokens, - disableGroupBy: true, - accessor: (value: any) => ({ - chainId: value.originChainId, - symbol: value.symbol, - name: value.name, - logo: value.logo, - assetAddress: value.assetAddress, - }), - aggregate: (leafValues: any) => leafValues.length, - Aggregated: ({ value }: { value: any }) => - `${value} Token${value === 1 ? "" : "s"}`, - Cell: (value: any) => ( -
-
- {value.row?.original?.logo ? ( - - ) : null} -
- -
- ), - }, - { Header: "Chain", accessor: "originChain" }, - { - Header: "Amount", - accessor: "amount", - align: "right", - disableGroupBy: true, - Cell: (value: any) => - value.row?.original?.amount !== undefined - ? numeral(value.row?.original?.amount).format("0,0.00") - : "", - }, - { - Header: "Total Value (USD)", - id: "totalValue", - accessor: "totalValue", - align: "right", - disableGroupBy: true, - aggregate: (leafValues: any) => - balancePretty( - formatUnits( - leafValues.reduce( - (p: BigNumber, v: number | null | undefined) => - v ? p.add(parseUnits(v.toFixed(18).toString(), 18)) : p, - BigNumber.from(0) - ), - 18 - ) - ), - Aggregated: ({ value }: { value: any }) => value, - Cell: (value: any) => - value.row?.original?.totalValue !== undefined - ? numeral(value.row?.original?.totalValue).format("0.0 a") - : "", - }, - { - Header: "Unit Price (USD)", - accessor: "quotePrice", - align: "right", - disableGroupBy: true, - Cell: (value: any) => - value.row?.original?.quotePrice !== undefined - ? numeral(value.row?.original?.quotePrice).format("0,0.00") - : "", - }, - ]; - }, [ - classes.logo, - classes.tokenContainer, - classes.logoPositioner, - sortTokens, - ]); - const tvlString = useMemo(() => { - if (!tvl.data) { - return ""; - } else { - let sum = 0; - tvl.data.forEach((val) => { - if (val.totalValue) sum += val.totalValue; - }); - return numeral(sum) - .format(sum >= 1000000000 ? "0.000 a" : "0 a") - .toUpperCase(); - } - }, [tvl.data]); +import TVLStats from "./TVLStats"; +import VolumeStats from "./VolumeStats"; +const StatsRoot = () => { return ( - Rock Hard Stats + Stats -
-
- Total Value Locked - - These assets are currently locked by the Token Bridge contracts. - -
-
- {!tvl.isFetching ? ( -
- - {"Total (USD)"} - - - {tvlString} - -
- ) : null} -
- - {!tvl.isFetching ? ( - - ) : ( - - )} - - - - + + ); }; diff --git a/bridge_ui/src/hooks/useCumulativeTVL.ts b/bridge_ui/src/hooks/useCumulativeTVL.ts new file mode 100644 index 000000000..47093c599 --- /dev/null +++ b/bridge_ui/src/hooks/useCumulativeTVL.ts @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import { + DataWrapper, + errorDataWrapper, + fetchDataWrapper, + receiveDataWrapper, +} from "../store/helpers"; +import { TVL_CUMULATIVE_URL } from "../utils/consts"; + +export interface LockedAsset { + Symbol: string; + Name: string; + Address: string; + CoinGeckoId: string; + Amount: number; + Notional: number; + TokenPrice: number; +} + +export interface LockedAssets { + [tokenAddress: string]: LockedAsset; +} + +export interface ChainsAssets { + [chainId: string]: LockedAssets; +} + +export interface NotionalTVLCumulative { + DailyLocked: { + [date: string]: ChainsAssets; + }; +} + +const useCumulativeTVL = () => { + const [cumulativeTVL, setCumulativeTVL] = useState< + DataWrapper + >(fetchDataWrapper()); + + useEffect(() => { + let cancelled = false; + axios + .get(TVL_CUMULATIVE_URL) + .then((response) => { + if (!cancelled) { + setCumulativeTVL(receiveDataWrapper(response.data)); + } + }) + .catch((error) => { + if (!cancelled) { + setCumulativeTVL(errorDataWrapper(error)); + } + console.log(error); + }); + return () => { + cancelled = true; + }; + }, []); + + return cumulativeTVL; +}; + +export default useCumulativeTVL; diff --git a/bridge_ui/src/hooks/useNotionalTransferred.ts b/bridge_ui/src/hooks/useNotionalTransferred.ts new file mode 100644 index 000000000..9b0f80c4a --- /dev/null +++ b/bridge_ui/src/hooks/useNotionalTransferred.ts @@ -0,0 +1,50 @@ +import axios from "axios"; +import { useEffect, useState } from "react"; +import { + DataWrapper, + errorDataWrapper, + fetchDataWrapper, + receiveDataWrapper, +} from "../store/helpers"; +import { NOTIONAL_TRANSFERRED_URL } from "../utils/consts"; + +export interface TransferFromData { + [leavingChainId: string]: number; +} + +export interface NotionalTransferredFrom { + Total: number; + Daily: { + [date: string]: TransferFromData; + }; +} + +const useNotionalTransferred = () => { + const [notionalTransferred, setNotionalTransferred] = useState< + DataWrapper + >(fetchDataWrapper()); + + useEffect(() => { + let cancelled = false; + axios + .get(NOTIONAL_TRANSFERRED_URL) + .then((response) => { + if (!cancelled) { + setNotionalTransferred(receiveDataWrapper(response.data)); + } + }) + .catch((error) => { + if (!cancelled) { + setNotionalTransferred(errorDataWrapper(error)); + console.error(error); + } + }); + return () => { + cancelled = true; + }; + }, []); + + return notionalTransferred; +}; + +export default useNotionalTransferred; diff --git a/bridge_ui/src/hooks/useTVL.ts b/bridge_ui/src/hooks/useTVL.ts index 8fa79124b..3bdec6795 100644 --- a/bridge_ui/src/hooks/useTVL.ts +++ b/bridge_ui/src/hooks/useTVL.ts @@ -41,12 +41,12 @@ interface ChainsAssets { [chainId: string]: LockedAssets; } -interface NotionalTvl { +export interface NotionalTVL { Last24HoursChange: ChainsAssets; AllTime: ChainsAssets; } -const createTVLArray = (notionalTvl: NotionalTvl) => { +export const createTVLArray = (notionalTvl: NotionalTVL) => { const tvl: TVL[] = []; for (const [chainId, chainAssets] of Object.entries(notionalTvl.AllTime)) { if (chainId === "*") continue; @@ -71,16 +71,16 @@ const createTVLArray = (notionalTvl: NotionalTvl) => { return tvl; }; -const useTVL = () => { - const [tvl, setTvl] = useState>(fetchDataWrapper()); +export const useTVL = () => { + const [tvl, setTvl] = useState>(fetchDataWrapper()); useEffect(() => { let cancelled = false; axios - .get(TVL_URL) + .get(TVL_URL) .then((response) => { if (!cancelled) { - setTvl(receiveDataWrapper(createTVLArray(response.data))); + setTvl(receiveDataWrapper(response.data)); } }) .catch((error) => { diff --git a/bridge_ui/src/hooks/useTotalTransactedAmount.ts b/bridge_ui/src/hooks/useTotalTransactedAmount.ts index 3dffbdb73..b77b94d62 100644 --- a/bridge_ui/src/hooks/useTotalTransactedAmount.ts +++ b/bridge_ui/src/hooks/useTotalTransactedAmount.ts @@ -6,7 +6,7 @@ import { formatUnits } from "@ethersproject/units"; import axios from "axios"; import { useEffect, useMemo, useState } from "react"; import { DataWrapper } from "../store/helpers"; -import useTVL from "./useTVL"; +import { createTVLArray, useTVL } from "./useTVL"; function convertbase64ToBinary(base64: string) { var raw = window.atob(base64); @@ -24,6 +24,9 @@ function convertbase64ToBinary(base64: string) { //Don't actually mount this hook, it's way to expensive for the prod site. const useTotalTransactedAmount = (): DataWrapper => { const tvl = useTVL(); + const tvlArray = useMemo(() => { + return tvl.data ? createTVLArray(tvl.data) : []; + }, [tvl]); const [everyVaaPayloadInHistory, setEveryVaaPayloadInHistory] = useState< { EmitterChain: string; EmitterAddress: string; Payload: string }[] | null >(null); @@ -84,7 +87,7 @@ const useTotalTransactedAmount = (): DataWrapper => { const assetAddress = hexToNativeString(payload.originAddress, payload.originChain) || ""; - const tvlItem = tvl.data?.find((item) => { + const tvlItem = tvlArray.find((item) => { return ( assetAddress && item.assetAddress.toLowerCase() === assetAddress.toLowerCase() diff --git a/bridge_ui/src/hooks/useTransactionTotals.ts b/bridge_ui/src/hooks/useTransactionTotals.ts new file mode 100644 index 000000000..5e81769c3 --- /dev/null +++ b/bridge_ui/src/hooks/useTransactionTotals.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import { + DataWrapper, + errorDataWrapper, + fetchDataWrapper, + receiveDataWrapper, +} from "../store/helpers"; +import { TOTAL_TRANSACTIONS_WORMHOLE } from "../utils/consts"; + +export interface Totals { + TotalCount: { [chainId: string]: number }; + DailyTotals: { + // "2021-08-22": { "*": 0 }, + [date: string]: { [groupByKey: string]: number }; + }; +} + +const useTransactionTotals = () => { + const [totals, setTotals] = useState>(fetchDataWrapper()); + + useEffect(() => { + let cancelled = false; + axios + .get(TOTAL_TRANSACTIONS_WORMHOLE) + .then((response) => { + if (!cancelled) { + setTotals(receiveDataWrapper(response.data)); + } + }) + .catch((error) => { + if (!cancelled) { + setTotals(errorDataWrapper(error)); + console.log(error); + } + }); + return () => { + cancelled = true; + }; + }, []); + + return totals; +}; + +export default useTransactionTotals; diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index ccf104fb5..d3e106579 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -705,6 +705,8 @@ export const COVALENT_GET_TOKENS_URL = ( }; export const TVL_URL = "https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-notionaltvl"; +export const TVL_CUMULATIVE_URL = + "https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-notionaltvlcumulative?totalsOnly=true"; export const TERRA_SWAPRATE_URL = "https://fcd.terra.dev/v1/market/swaprate/uusd"; @@ -1043,6 +1045,9 @@ export const TOTAL_TRANSACTIONS_WORMHOLE = `https://europe-west3-wormhole-315720 export const RECENT_TRANSACTIONS_WORMHOLE = `https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-recent?groupBy=address&numRows=2`; +export const NOTIONAL_TRANSFERRED_URL = + "https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-notionaltransferredfrom"; + export const VAA_EMITTER_ADDRESSES = [ `${CHAIN_ID_SOLANA}:ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5`, //SOLANA TOKEN `${CHAIN_ID_SOLANA}:0def15a24423e1edd1a5ab16f557b9060303ddbab8c803d2ee48f4b78a1cfd6b`, //SOLAN NFT @@ -1193,3 +1198,21 @@ export const ACALA_RELAYER_URL = export const ACALA_RELAY_URL = `${ACALA_RELAYER_URL}/relay`; export const ACALA_SHOULD_RELAY_URL = `${ACALA_RELAYER_URL}/shouldRelay`; + +export const getChainShortName = (chainId: ChainId) => { + return chainId === CHAIN_ID_BSC ? "BSC" : CHAINS_BY_ID[chainId]?.name; +}; + +export const COLOR_BY_CHAIN_ID: { [key in ChainId]?: string } = { + [CHAIN_ID_SOLANA]: "#31D7BB", + [CHAIN_ID_ETH]: "#8A92B2", + [CHAIN_ID_TERRA]: "#5493F7", + [CHAIN_ID_BSC]: "#F0B90B", + [CHAIN_ID_POLYGON]: "#8247E5", + [CHAIN_ID_AVAX]: "#E84142", + [CHAIN_ID_OASIS]: "#0092F6", + [CHAIN_ID_AURORA]: "#23685A", + [CHAIN_ID_FANTOM]: "#1969FF", + [CHAIN_ID_KARURA]: "#FF4B3B", + [CHAIN_ID_ACALA]: "#E00F51", +};