bridge_ui: stats page overhaul, added custody-addresses page

This commit is contained in:
Kevin Peters 2022-03-18 20:59:15 +00:00 committed by Evan Gray
parent 76ade1b305
commit d4a4f8aab5
28 changed files with 2774 additions and 357 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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() {
</Typography>
</AppBar>
) : null}
<div className={classes.content}>
<div className={classes.headerImage} />
{["/transfer", "/nft", "/redeem"].includes(pathname) ? (
<Container maxWidth="md" style={{ paddingBottom: 24 }}>
<HeaderText
white
subtitle={
<>
<Typography>
Portal is a bridge that offers unlimited transfers across
chains for tokens and NFTs wrapped by Wormhole.
</Typography>
<Typography>
Unlike many other bridges, you avoid double wrapping and
never have to retrace your steps.
</Typography>
</>
}
>
Token Bridge
</HeaderText>
<Tabs
value={pathname}
variant="fullWidth"
onChange={handleTabChange}
indicatorColor="primary"
>
<Tab label="Tokens" value="/transfer" />
<Tab label="NFTs" value="/nft" />
<Tab label="Redeem" value="/redeem" to="/redeem" />
</Tabs>
</Container>
) : null}
<Switch>
<Route exact path="/transfer">
<Transfer />
</Route>
<Route exact path="/nft">
<NFT />
</Route>
<Route exact path="/redeem">
<Recovery />
</Route>
<Route exact path="/nft-origin-verifier">
<NFTOriginVerifier />
</Route>
<Route exact path="/token-origin-verifier">
<TokenOriginVerifier />
</Route>
<Route exact path="/register">
<Attest />
</Route>
<Route exact path="/migrate/Solana/:legacyAsset/:fromTokenAccount">
<Migration chainId={CHAIN_ID_SOLANA} />
</Route>
<Route exact path="/migrate/Ethereum/:legacyAsset/">
<Migration chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/:legacyAsset/">
<Migration chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Ethereum/">
<EvmQuickMigrate chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/">
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Solana/">
<SolanaQuickMigrate />
</Route>
<Route exact path="/stats">
<Stats />
</Route>
<Route exact path="/withdraw-tokens-terra">
<WithdrawTokensTerra />
</Route>
<Route exact path="/unwrap-native">
<UnwrapNative />
</Route>
<Route>
<Redirect to="/transfer" />
</Route>
</Switch>
</div>
<div className={classes.headerImage} />
{["/transfer", "/nft", "/redeem"].includes(pathname) ? (
<Container maxWidth="md" style={{ paddingBottom: 24 }}>
<HeaderText
white
subtitle={
<>
<Typography>
Portal is a bridge that offers unlimited transfers across
chains for tokens and NFTs wrapped by Wormhole.
</Typography>
<Typography>
Unlike many other bridges, you avoid double wrapping and never
have to retrace your steps.
</Typography>
</>
}
>
Token Bridge
</HeaderText>
<Tabs
value={pathname}
variant="fullWidth"
onChange={handleTabChange}
indicatorColor="primary"
>
<Tab label="Tokens" value="/transfer" />
<Tab label="NFTs" value="/nft" />
<Tab label="Redeem" value="/redeem" to="/redeem" />
</Tabs>
</Container>
) : null}
<Switch>
<Route exact path="/transfer">
<Transfer />
</Route>
<Route exact path="/nft">
<NFT />
</Route>
<Route exact path="/redeem">
<Recovery />
</Route>
<Route exact path="/nft-origin-verifier">
<NFTOriginVerifier />
</Route>
<Route exact path="/token-origin-verifier">
<TokenOriginVerifier />
</Route>
<Route exact path="/register">
<Attest />
</Route>
<Route exact path="/migrate/Solana/:legacyAsset/:fromTokenAccount">
<Migration chainId={CHAIN_ID_SOLANA} />
</Route>
<Route exact path="/migrate/Ethereum/:legacyAsset/">
<Migration chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/:legacyAsset/">
<Migration chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Ethereum/">
<EvmQuickMigrate chainId={CHAIN_ID_ETH} />
</Route>
<Route exact path="/migrate/BinanceSmartChain/">
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
</Route>
<Route exact path="/migrate/Solana/">
<SolanaQuickMigrate />
</Route>
<Route exact path="/stats">
<Stats />
</Route>
<Route exact path="/withdraw-tokens-terra">
<WithdrawTokensTerra />
</Route>
<Route exact path="/unwrap-native">
<UnwrapNative />
</Route>
<Route exact path="/custody-addresses">
<CustodyAddresses />
</Route>
<Route>
<Redirect to="/transfer" />
</Route>
</Switch>
<div className={classes.spacer} />
<div className={classes.gradientRight}></div>
<div className={classes.gradientRight2}></div>

View File

@ -189,6 +189,16 @@ export default function Footer() {
>
Wormhole
</Link>
<Link
component={NavLink}
to={"/custody-addresses"}
color="inherit"
underline="hover"
className={classes.linkStyle}
activeClassName={classes.linkActiveStyle}
>
Custody
</Link>
</div>
</div>
<div className={classes.spacer} />

View File

@ -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}
</Typography>
{subtitle ? <Typography component="div">{subtitle}</Typography> : null}
{subtitle ? (
<Typography component="div" className={classes.subtitle}>
{subtitle}
</Typography>
) : null}
</div>
);
}

View File

@ -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 (
<div className={classes.container}>
<Typography className={classes.titleText}>{title}</Typography>
<hr className={classes.ruler}></hr>
<Typography className={classes.valueText}>
{valueFormatter(payload[0].value)}
</Typography>
<Typography className={classes.valueText}>
{formatDate(payload[0].payload.date)}
</Typography>
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@ -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 (
<div className={classes.container}>
<Grid container alignItems="center">
<img
className={classes.icon}
src={CHAINS_BY_ID[chainId]?.logo}
alt={chainShortName}
/>
<Typography
display="inline"
className={classes.titleText}
style={{ marginLeft: "8px" }}
>
{chainShortName}
</Typography>
</Grid>
<hr
className={classes.ruler}
style={{ backgroundColor: COLOR_BY_CHAIN_ID[chainId] }}
></hr>
<Typography className={classes.valueText}>
{valueFormatter(data.value)}
</Typography>
<Typography className={classes.valueText}>
{formatDate(data.payload.date)}
</Typography>
</div>
);
}
} else {
return (
<div className={classes.container}>
<Typography noWrap className={classes.titleText}>
{title}
</Typography>
<Typography className={classes.valueText}>
{formatDate(payload[0].payload.date)}
</Typography>
<hr className={classes.ruler}></hr>
{payload.map((data: any) => {
return (
<div key={data.name} className={classes.row}>
<div
style={{
width: "24px",
height: "24px",
backgroundColor: data.stroke,
}}
/>
<Typography
display="inline"
className={classes.valueText}
style={{ marginLeft: "8px", marginRight: "8px" }}
>
{data.name}
</Typography>
<Typography
display="inline"
className={classes.valueText}
style={{ marginLeft: "auto" }}
>
{valueFormatter(data.value)}
</Typography>
</div>
);
})}
</div>
);
}
}
return null;
};
export default MultiChainTooltip;

View File

@ -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 (
<ResponsiveContainer height={768}>
<AreaChart data={data}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={<CustomTooltip title="TVL" valueFormatter={formatTVL} />}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area dataKey="totalTVL" fill="url(#gradient)" stroke="#405BBC" />
</AreaChart>
</ResponsiveContainer>
);
};
export default TVLAreaChart;

View File

@ -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<ChainId>(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 (
<table className={classes.table}>
<tbody>
{chainTVLs.map((chainTVL) => (
<tr
key={chainTVL.chainInfo.id}
onMouseOver={() => handleMouseOver(chainTVL.chainInfo.id)}
>
<td style={{ textAlign: "right" }}>
<Typography noWrap display="inline">
{getChainShortName(chainTVL.chainInfo.id)}
</Typography>
</td>
<td>
<img
src={chainTVL.chainInfo.logo}
alt={""}
width={24}
height={24}
/>
</td>
<td width="100%">
<div
style={{
height: 30,
width: `${chainTVL.tvlRatio}%`,
backgroundImage:
"linear-gradient(90deg, #F44B1B 0%, #EEB430 100%)",
}}
></div>
</td>
<td>
<Typography noWrap display="inline">
{formatTVL(chainTVL.tvl)}
</Typography>
</td>
<td>
{isSmall || mouseOverChainId === chainTVL.chainInfo.id ? (
<Button
variant="outlined"
endIcon={<ArrowForward />}
onClick={() => handleClick(chainTVL.chainInfo)}
className={classes.button}
>
View assets
</Button>
) : (
<div style={{ width: 150 }} />
)}
</td>
</tr>
))}
</tbody>
</table>
);
};
export default TVLBarChart;

View File

@ -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 (
<ResponsiveContainer height={768}>
<LineChart data={data}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatTVL}
/>
}
/>
{selectedChains.map((chainId) => (
<Line
dataKey={`tvlByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default TVLLineChart;

View File

@ -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) => (
<div className={classes.tokenContainer}>
<div className={classes.logoPositioner}>
{value.row?.original?.logo ? (
<img
src={value.row?.original?.logo}
alt=""
className={classes.logo}
/>
) : null}
</div>
<SmartAddress
chainId={value.row?.original?.originChainId}
address={value.row?.original?.assetAddress}
symbol={value.row?.original?.symbol}
tokenName={value.row?.original?.name}
/>
</div>
),
},
{
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 (
<MuiReactTable
columns={tvlColumns}
data={chainTVL || []}
skipPageReset={false}
initialState={{ sortBy: [{ id: "totalValue", desc: true }] }}
/>
);
};
export default TVLTable;

View File

@ -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 },
};

View File

@ -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 (
<ResponsiveContainer height={768}>
<AreaChart data={transactionData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTransactionCount}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<CustomTooltip title="All chains" valueFormatter={formatValue} />
}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area
dataKey="totalTransactions"
stroke="#405BBC"
fill="url(#gradient)"
/>
</AreaChart>
</ResponsiveContainer>
);
};
export default TransactionsAreaChart;

View File

@ -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 (
<ResponsiveContainer height={768}>
<LineChart data={transactionData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTransactionCount}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatValue}
/>
}
/>
{chains.map((chainId) => (
<Line
dataKey={`transactionsByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default TransactionsLineChart;

View File

@ -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 (
<ResponsiveContainer height={768}>
<AreaChart data={transferData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<CustomTooltip title="All chains" valueFormatter={formatTVL} />
}
/>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(100)">
<stop offset="0%" stopColor="#FF2B57" />
<stop offset="100%" stopColor="#5EA1EC" />
</linearGradient>
</defs>
<Area
dataKey="totalTransferred"
stroke="#405BBC"
fill="url(#gradient)"
/>
</AreaChart>
</ResponsiveContainer>
);
};
export default VolumeAreaChart;

View File

@ -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 (
<ResponsiveContainer height={768}>
<LineChart data={transferData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={formatTVL}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={
<MultiChainTooltip
title="Multiple Chains"
valueFormatter={formatTVL}
/>
}
/>
{chains.map((chainId) => (
<Line
dataKey={`transferredByChain.${chainId}`}
name={getChainShortName(chainId)}
stroke={COLOR_BY_CHAIN_ID[chainId]}
strokeWidth="4"
dot={false}
key={chainId}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default VolumeLineChart;

View File

@ -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[]>((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 (
<div className={classes.tooltipContainer}>
<Grid container alignItems="center">
<img
className={classes.tooltipIcon}
src={CHAINS_BY_ID[chainId as ChainId]?.logo}
alt={chainShortName}
/>
<Typography display="inline" className={classes.tooltipTitleText}>
{chainShortName}
</Typography>
</Grid>
<hr
className={classes.tooltipRuler}
style={{ backgroundColor: COLOR_BY_CHAIN_ID[chainId as ChainId] }}
></hr>
<Typography
className={classes.tooltipValueText}
>{`${data.value.toFixed(1)}%`}</Typography>
<Typography className={classes.tooltipValueText}>
{formatTVL(data.payload.volume[chainId])}
</Typography>
<Typography className={classes.tooltipValueText}>
{formatDate(data.payload.date)}
</Typography>
</div>
);
}
}
return null;
};
const VolumeStackedBarChart = ({
transferData,
timeFrame,
selectedChains,
}: {
transferData: TransferChartData[];
timeFrame: TimeFrame;
selectedChains: ChainId[];
}) => {
const [hoverChainId, setHoverChainId] = useState<ChainId | null>(null);
const barData = useMemo(() => {
return createBarData(transferData, selectedChains);
}, [transferData, selectedChains]);
const theme = useTheme();
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
return (
<ResponsiveContainer height={768}>
<BarChart data={barData}>
<XAxis
dataKey="date"
tickFormatter={timeFrame.tickFormatter}
tick={{ fill: "white" }}
interval={!isXSmall ? timeFrame.interval : undefined}
axisLine={false}
tickLine={false}
dy={16}
/>
<YAxis
tickFormatter={(tick) => `${tick}%`}
ticks={[0, 25, 50, 75, 100]}
domain={[0, 100]}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={<CustomTooltip chainId={hoverChainId} barData={barData} />}
cursor={{ fill: "transparent" }}
/>
{selectedChains.map((chainId) => (
<Bar
dataKey={`volumePercent.${chainId}`}
name={getChainShortName(chainId)}
fill={COLOR_BY_CHAIN_ID[chainId]}
key={chainId}
stackId="a"
onMouseOver={() => setHoverChainId(chainId)}
/>
))}
<Legend
iconType="square"
iconSize={32}
formatter={renderLegendText}
wrapperStyle={{ paddingTop: 24 }}
/>
</BarChart>
</ResponsiveContainer>
);
};
export default VolumeStackedBarChart;

View File

@ -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 <span style={{ color: "white", margin: "8px" }}>{value}</span>;
};
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<CumulativeTVLChartData[]>(
(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<TransferChartData[]>((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<TransactionData[]>((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<ChainTVLChartData[]>((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;
};

View File

@ -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<any> = () => {
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<any> = () => {
);
return (
<>
<Container maxWidth="lg">
<Container maxWidth="md">
<HeaderText white>Custody</HeaderText>
</Container>
{header}
<Paper className={classes.mainPaper}>{table}</Paper>
</>
</Container>
);
};

View File

@ -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<ChainId[]>([]);
const [selectedChainDetail, setSelectedChainDetail] =
useState<ChainInfo | null>(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<ChainId[]>((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 (
<>
<div className={classes.description}>
<Typography variant="h3">
{tvlText}
<StyledTooltip title={tooltipText} className={classes.tooltip}>
<InfoOutlined />
</StyledTooltip>
</Typography>
<Typography variant="h3">{tvlAllTime}</Typography>
</div>
<div className={classes.displayBy}>
{!selectedChainDetail ? (
<div>
<Typography display="inline" style={{ marginRight: "8px" }}>
Display by
</Typography>
<ToggleButtonGroup
value={displayBy}
exclusive
onChange={handleDisplayByChange}
>
{DISPLAY_BY_VALUES.map((value) => (
<ToggleButton
key={value}
value={value}
className={classes.toggleButton}
>
{value}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
) : null}
{displayBy === "Time" && !selectedChainDetail ? (
<div>
<FormControl>
<Select
multiple
variant="outlined"
value={selectedChains}
onChange={handleSelectedChainsChange}
renderValue={(selected: any) =>
selected.length === availableChains.length
? "All chains"
: selected.length > 1
? `${selected.length} chains`
: //@ts-ignore
CHAINS_BY_ID[selected[0]]?.name
}
MenuProps={{ getContentAnchorEl: null }} // hack to prevent popup menu from moving
style={{ minWidth: 128 }}
>
<MenuItem value="all">
<Checkbox
checked={availableChains.length > 0 && allChainsSelected}
indeterminate={
selectedChains.length > 0 &&
selectedChains.length < availableChains.length
}
/>
<ListItemText primary="All chains" />
</MenuItem>
{availableChains.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox checked={selectedChains.indexOf(option) > -1} />
<ListItemText primary={CHAINS_BY_ID[option]?.name} />
</MenuItem>
))}
</Select>
</FormControl>
<TextField
select
variant="outlined"
value={timeFrame}
onChange={handleTimeFrameChange}
style={{ marginLeft: 8 }}
>
{Object.keys(TIME_FRAMES).map((timeFrame) => (
<MenuItem key={timeFrame} value={timeFrame}>
{timeFrame}
</MenuItem>
))}
</TextField>
</div>
) : selectedChainDetail ? (
<Button
startIcon={<ArrowBack />}
onClick={() => {
setSelectedChainDetail(null);
}}
>
Back to all chains
</Button>
) : null}
</div>
<Paper className={classes.mainPaper}>
{displayBy === "Time" ? (
cumulativeTVL.data ? (
allChainsSelected ? (
<TVLAreaChart
cumulativeTVL={cumulativeTVL.data}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<TVLLineChart
cumulativeTVL={cumulativeTVL.data}
timeFrame={TIME_FRAMES[timeFrame]}
selectedChains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)
) : tvl.data ? (
selectedChainDetail ? (
<TVLTable chainInfo={selectedChainDetail} tvl={tvl.data} />
) : (
<TVLBarChart
tvl={tvl.data}
onChainSelected={handleChainDetailSelected}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
</>
);
};
export default TVLStats;

View File

@ -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<ChainId[]>([]);
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<ChainId[]>((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 (
<>
<div className={classes.description}>
<Typography variant="h3">
{displayBy === "Transactions"
? "Transaction Count"
: "Outbound Volume"}
<StyledTooltip
title={
displayBy === "Transactions"
? "Total number of transactions the Token Bridge has processed"
: "Amount of assets bridged through Portal in the outbound direction"
}
className={classes.tooltip}
>
<InfoOutlined />
</StyledTooltip>
</Typography>
<Typography variant="h3">
{displayBy === "Transactions"
? transactionsAllTime
: transferredAllTime}
</Typography>
</div>
<div className={classes.displayBy}>
<div>
<Typography display="inline" style={{ marginRight: "8px" }}>
Display by
</Typography>
<ToggleButtonGroup
value={displayBy}
exclusive
onChange={handleDisplayByChange}
>
{DISPLAY_BY_VALUES.map((value) => (
<ToggleButton
key={value}
value={value}
className={classes.toggleButton}
>
{value}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
<div>
<FormControl>
<Select
multiple
variant="outlined"
value={selectedChains}
onChange={handleSelectedChainsChange}
renderValue={(selected: any) =>
selected.length === availableChains.length
? "All chains"
: selected.length > 1
? `${selected.length} chains`
: //@ts-ignore
CHAINS_BY_ID[selected[0]]?.name
}
MenuProps={{ getContentAnchorEl: null }} // hack to prevent popup menu from moving
style={{ minWidth: 128 }}
>
<MenuItem value="all">
<Checkbox
checked={availableChains.length > 0 && allChainsSelected}
indeterminate={
selectedChains.length > 0 &&
selectedChains.length < availableChains.length
}
/>
<ListItemText primary="All chains" />
</MenuItem>
{availableChains.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox checked={selectedChains.indexOf(option) > -1} />
<ListItemText primary={CHAINS_BY_ID[option]?.name} />
</MenuItem>
))}
</Select>
</FormControl>
<TextField
select
variant="outlined"
value={timeFrame}
onChange={handleTimeFrameChange}
style={{ marginLeft: 8 }}
>
{Object.keys(TIME_FRAMES).map((timeFrame) => (
<MenuItem key={timeFrame} value={timeFrame}>
{timeFrame}
</MenuItem>
))}
</TextField>
</div>
</div>
<Paper className={classes.mainPaper}>
{displayBy === "Dollar" ? (
notionalTransferred.data ? (
allChainsSelected ? (
<VolumeAreaChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<VolumeLineChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)
) : displayBy === "Percent" ? (
<VolumeStackedBarChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
selectedChains={selectedChains}
/>
) : transactionTotals.data ? (
allChainsSelected ? (
<TransactionsAreaChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<TransactionsLineChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
</>
);
};
export default VolumeStats;

View File

@ -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<any> = () => {
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) => (
<div className={classes.tokenContainer}>
<div className={classes.logoPositioner}>
{value.row?.original?.logo ? (
<img
src={value.row?.original?.logo}
alt=""
className={classes.logo}
/>
) : null}
</div>
<SmartAddress
chainId={value.row?.original?.originChainId}
address={value.row?.original?.assetAddress}
symbol={value.row?.original?.symbol}
tokenName={value.row?.original?.name}
/>
</div>
),
},
{ 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 (
<Container maxWidth="lg">
<Container maxWidth="md">
<HeaderText white>Rock Hard Stats</HeaderText>
<HeaderText white>Stats</HeaderText>
</Container>
<div className={classes.flexBox}>
<div className={classes.explainerContainer}>
<Typography variant="h4">Total Value Locked</Typography>
<Typography variant="subtitle1" color="textSecondary">
These assets are currently locked by the Token Bridge contracts.
</Typography>
</div>
<div className={classes.grower} />
{!tvl.isFetching ? (
<div
className={clsx(classes.explainerContainer, classes.totalContainer)}
>
<Typography
variant="body2"
color="textSecondary"
component="div"
noWrap
>
{"Total (USD)"}
</Typography>
<Typography
variant="h3"
component="div"
noWrap
className={classes.totalValue}
>
{tvlString}
</Typography>
</div>
) : null}
</div>
<Paper className={classes.mainPaper}>
{!tvl.isFetching ? (
<MuiReactTable
columns={tvlColumns}
data={tvl.data || []}
skipPageReset={false}
initialState={{ sortBy: [{ id: "totalValue", desc: true }] }}
/>
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
<TransactionMetrics />
<CustodyAddresses />
<NFTStats />
<TVLStats />
<VolumeStats />
</Container>
);
};

View File

@ -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<NotionalTVLCumulative>
>(fetchDataWrapper());
useEffect(() => {
let cancelled = false;
axios
.get<NotionalTVLCumulative>(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;

View File

@ -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<NotionalTransferredFrom>
>(fetchDataWrapper());
useEffect(() => {
let cancelled = false;
axios
.get<NotionalTransferredFrom>(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;

View File

@ -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<DataWrapper<TVL[]>>(fetchDataWrapper());
export const useTVL = () => {
const [tvl, setTvl] = useState<DataWrapper<NotionalTVL>>(fetchDataWrapper());
useEffect(() => {
let cancelled = false;
axios
.get<NotionalTvl>(TVL_URL)
.get<NotionalTVL>(TVL_URL)
.then((response) => {
if (!cancelled) {
setTvl(receiveDataWrapper(createTVLArray(response.data)));
setTvl(receiveDataWrapper(response.data));
}
})
.catch((error) => {

View File

@ -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<number> => {
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<number> => {
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()

View File

@ -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<DataWrapper<Totals>>(fetchDataWrapper());
useEffect(() => {
let cancelled = false;
axios
.get<Totals>(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;

View File

@ -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",
};