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}
+
+
+
+
+ {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 ? (
+ }
+ onClick={() => handleClick(chainTVL.chainInfo)}
+ className={classes.button}
+ >
+ View assets
+
+ ) : (
+
+ )}
+ |
+
+ ))}
+
+
+ );
+};
+
+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}
+
+
+
+
{`${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) => (
+
+ ))}
+
+
+ ) : selectedChainDetail ? (
+
}
+ onClick={() => {
+ setSelectedChainDetail(null);
+ }}
+ >
+ Back to all chains
+
+ ) : 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) => (
+
+ ))}
+
+
+
+
+ {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",
+};