diff --git a/explorer/package-lock.json b/explorer/package-lock.json index 8b7beab4d8..d8ccf025d9 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -2283,6 +2283,20 @@ "ieee754": "^1.1.4" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "superstruct": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz", + "integrity": "sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==", + "requires": { + "kind-of": "^6.0.2", + "tiny-invariant": "^1.0.6" + } + }, "tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -3012,6 +3026,11 @@ "@types/react-router": "*" } }, + "@types/socket.io-client": { + "version": "1.4.33", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.33.tgz", + "integrity": "sha512-m4LnxkljsI9fMsjwpW5QhRpMixo2BeeLpFmg0AE+sS4H1pzAd/cs/ftTiL60FLZgfFa8PFRPx5KsHu8O0bADKQ==" + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -3366,6 +3385,11 @@ } } }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -3580,6 +3604,11 @@ "es-abstract": "^1.17.0-next.1" } }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -4349,6 +4378,11 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -4417,6 +4451,11 @@ "safe-buffer": "^5.0.1" } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -4435,6 +4474,14 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -4454,6 +4501,11 @@ "file-uri-to-path": "1.0.0" } }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -4833,6 +4885,11 @@ "caller-callsite": "^2.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", @@ -5263,11 +5320,21 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "compose-function": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", @@ -5466,6 +5533,11 @@ } } }, + "countup.js": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-1.9.3.tgz", + "integrity": "sha1-zj5QzXFgRB5HjwfaMYle3MDxyd0=" + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -6309,6 +6381,46 @@ "once": "^1.4.0" } }, + "engine.io-client": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", + "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", @@ -7827,6 +7939,26 @@ } } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -8137,6 +8269,11 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "humanize-duration-ts": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz", + "integrity": "sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8235,6 +8372,11 @@ "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -10920,6 +11062,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -11348,6 +11495,22 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12786,6 +12949,16 @@ "semver": "^5.6.0" } }, + "react-countup": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/react-countup/-/react-countup-4.3.3.tgz", + "integrity": "sha512-pWnxpwdPNRyJFha/YKKbyc4RLAw8PzmULdgCziGIgw6vxhT1VdccrvQgj38HBSoM2qF/MoLmn4M2klvDWVIdaw==", + "requires": { + "countup.js": "^1.9.3", + "prop-types": "^15.7.2", + "warning": "^4.0.3" + } + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -14408,6 +14581,69 @@ "kind-of": "^3.2.0" } }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -14962,20 +15198,9 @@ } }, "superstruct": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz", - "integrity": "sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==", - "requires": { - "kind-of": "^6.0.2", - "tiny-invariant": "^1.0.6" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - } - } + "version": "0.10.12", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.12.tgz", + "integrity": "sha512-FiNhfegyytDI0QxrrEoeGknFM28SnoHqCBpkWewUm8jRNj74NVxLpiiePvkOo41Ze/aKMSHa/twWjNF81mKaQQ==" }, "supports-color": { "version": "5.5.0", @@ -15394,6 +15619,11 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -15831,6 +16061,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "wasm-dce": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz", @@ -16776,6 +17014,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", @@ -16924,6 +17167,11 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" } } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/explorer/package.json b/explorer/package.json index 2f186498dd..6ecd8ca509 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -14,16 +14,21 @@ "@types/react": "^16.9.43", "@types/react-dom": "^16.9.8", "@types/react-router-dom": "^5.1.5", + "@types/socket.io-client": "^1.4.33", "bootstrap": "^4.5.0", "bs58": "^4.0.1", + "humanize-duration-ts": "^2.1.1", "node-sass": "^4.14.1", "prettier": "^2.0.5", "react": "^16.13.1", "react-app-rewired": "^2.1.6", + "react-countup": "^4.3.3", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", + "socket.io-client": "^2.3.0", "solana-sdk-wasm": "file:wasm/pkg", + "superstruct": "^0.10.12", "typescript": "^3.9.7", "wasm-loader": "^1.3.0" }, diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index 608ff303a2..c200895521 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -12,6 +12,7 @@ import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts"; import TabbedPage from "components/TabbedPage"; import TopAccountsCard from "components/TopAccountsCard"; import SupplyCard from "components/SupplyCard"; +import StatsCard from "components/StatsCard"; import { pickCluster } from "utils/url"; import Banner from "components/Banner"; @@ -84,11 +85,11 @@ function App() { - ( - - )} - > + + + + + diff --git a/explorer/src/components/StatsCard.tsx b/explorer/src/components/StatsCard.tsx new file mode 100644 index 0000000000..0e901c576d --- /dev/null +++ b/explorer/src/components/StatsCard.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import CountUp from "react-countup"; + +import TableCardBody from "./common/TableCardBody"; +import { + useDashboardInfo, + usePerformanceInfo, + useRootSlot, + PERF_UPDATE_SEC, + useSetActive, +} from "providers/stats/solanaBeach"; +import { slotsToHumanString } from "utils"; +import { useCluster, Cluster } from "providers/cluster"; + +export default function StatsCard() { + return ( +
+
+
+
+

Live Cluster Info

+
+
+
+ +
+ ); +} + +function StatsCardBody() { + const rootSlot = useRootSlot(); + const dashboardInfo = useDashboardInfo(); + const performanceInfo = usePerformanceInfo(); + const txTrackerRef = React.useRef({ old: 0, new: 0 }); + const txTracker = txTrackerRef.current; + const setSocketActive = useSetActive(); + const { cluster } = useCluster(); + + React.useEffect(() => { + setSocketActive(true); + return () => setSocketActive(false); + }, [setSocketActive, cluster]); + + const statsAvailable = + cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet; + if (!statsAvailable) { + return ( +
+
+ Stats are not available for this cluster +
+
+ ); + } + + if (performanceInfo) { + const { totalTransactionCount: txCount, avgTPS } = performanceInfo; + + // Track last tx count to initialize count up + if (txCount !== txTracker.new) { + // If this is the first tx count value, estimate the previous one + // in order to have a starting point for our animation + txTracker.old = txTracker.new || txCount - PERF_UPDATE_SEC * avgTPS; + txTracker.new = txCount; + } + } else { + txTrackerRef.current = { old: 0, new: 0 }; + } + + if (rootSlot === undefined || !dashboardInfo || !performanceInfo) { + return ( +
+ + Loading +
+ ); + } + + const currentBlock = rootSlot.toLocaleString("en-US"); + const { avgBlockTime_1min, epochInfo } = dashboardInfo; + const averageBlockTime = Math.round(1000 * avgBlockTime_1min) + "ms"; + const { slotIndex, slotsInEpoch } = epochInfo; + const currentEpoch = epochInfo.epoch.toString(); + const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%"; + const epochTimeRemaining = slotsToHumanString(slotsInEpoch - slotIndex); + const transactionCount = ( + + ); + const averageTps = Math.round(performanceInfo.avgTPS); + + return ( + + + Block + {currentBlock} + + + Block time + {averageBlockTime} + + + Epoch + {currentEpoch} + + + Epoch progress + {epochProgress} + + + Epoch time remaining + {epochTimeRemaining} + + + Transaction count + {transactionCount} + + + Transactions per second + {averageTps} + + + ); +} diff --git a/explorer/src/components/TabbedPage.tsx b/explorer/src/components/TabbedPage.tsx index 37a16a3d08..87fbfc0827 100644 --- a/explorer/src/components/TabbedPage.tsx +++ b/explorer/src/components/TabbedPage.tsx @@ -4,7 +4,7 @@ import { useClusterModal } from "providers/cluster"; import ClusterStatusButton from "components/ClusterStatusButton"; import { pickCluster } from "utils/url"; -export type Tab = "Transactions" | "Accounts" | "Supply"; +export type Tab = "Transactions" | "Accounts" | "Supply" | "Stats"; type Props = { children: React.ReactNode; tab: Tab }; export default function TabbedPage({ children, tab }: Props) { @@ -22,6 +22,9 @@ export default function TabbedPage({ children, tab }: Props) {
    +
  • + +
  • - - - - - - - - - + + + + + + + + + + + , document.getElementById("root") diff --git a/explorer/src/providers/stats/index.tsx b/explorer/src/providers/stats/index.tsx new file mode 100644 index 0000000000..60a482d5fc --- /dev/null +++ b/explorer/src/providers/stats/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { SolanaBeachProvider } from "./solanaBeach"; + +type Props = { children: React.ReactNode }; +export function StatsProvider({ children }: Props) { + return {children}; +} diff --git a/explorer/src/providers/stats/solanaBeach.tsx b/explorer/src/providers/stats/solanaBeach.tsx new file mode 100644 index 0000000000..fab0d068c0 --- /dev/null +++ b/explorer/src/providers/stats/solanaBeach.tsx @@ -0,0 +1,187 @@ +import React from "react"; +import io from "socket.io-client"; + +import { + object, + number, + is, + StructType, + array, + nullable, + any, +} from "superstruct"; +import { useCluster, Cluster } from "providers/cluster"; + +// TODO: use `partial` when it is fixed +// https://github.com/ianstormtaylor/superstruct/issues/405 +const DashboardInfo = object({ + activatedStake: number(), + avgBlockTime_1h: number(), + avgBlockTime_1min: number(), + circulatingSupply: number(), + dailyPriceChange: number(), + dailyVolume: number(), + delinquentStake: number(), + epochInfo: object({ + absoluteEpochStartSlot: number(), + absoluteSlot: number(), + blockHeight: number(), + epoch: number(), + slotIndex: number(), + slotsInEpoch: number(), + }), + stakingYield: number(), + tokenPrice: number(), + totalDelegatedStake: number(), + totalSupply: number(), +}); + +// TODO: use `partial` when it is fixed +// https://github.com/ianstormtaylor/superstruct/issues/405 +const RootInfo = object({ + currentLeader: any(), + nextLeaders: any(), + root: number(), + servedSlots: any(), +}); + +export const PERF_UPDATE_SEC = 5; + +// TODO: use `partial` when it is fixed +// https://github.com/ianstormtaylor/superstruct/issues/405 +const PerformanceInfo = object({ + avgTPS: number(), + perfHistory: object({ + s: array(nullable(number())), + m: array(nullable(number())), + l: array(nullable(number())), + }), + totalTransactionCount: number(), +}); + +type SetActive = React.Dispatch>; +const SetActiveContext = React.createContext< + { setActive: SetActive } | undefined +>(undefined); + +type RootInfo = StructType; +type RootState = { slot: number | undefined }; +const RootContext = React.createContext(undefined); + +type DashboardInfo = StructType; +type DashboardState = { info: DashboardInfo | undefined }; +const DashboardContext = React.createContext( + undefined +); + +type PerformanceInfo = StructType; +type PerformanceState = { info: PerformanceInfo | undefined }; +const PerformanceContext = React.createContext( + undefined +); + +const MAINNET_URL = "https://api.solanabeach.io:8443/mainnet"; +const TESTNET_URL = "https://api.solanabeach.io:8443/tds"; + +type Props = { children: React.ReactNode }; +export function SolanaBeachProvider({ children }: Props) { + const { cluster } = useCluster(); + const [active, setActive] = React.useState(false); + const [root, setRoot] = React.useState(); + const [dashboardInfo, setDashboardInfo] = React.useState(); + const [performanceInfo, setPerformanceInfo] = React.useState< + PerformanceInfo + >(); + + React.useEffect(() => { + if (!active) return; + + let socket: SocketIOClient.Socket; + if (cluster === Cluster.MainnetBeta) { + socket = io(MAINNET_URL); + } else if (cluster === Cluster.Testnet) { + socket = io(TESTNET_URL); + } else { + return; + } + + socket.on("connect", () => { + socket.emit("request_dashboardInfo"); + socket.emit("request_performanceInfo"); + }); + socket.on("error", (err: any) => { + console.error(err); + }); + socket.on("dashboardInfo", (data: any) => { + if (is(data, DashboardInfo)) { + setDashboardInfo(data); + } + }); + socket.on("performanceInfo", (data: any) => { + if (is(data, PerformanceInfo)) { + setPerformanceInfo(data); + } + }); + socket.on("rootNotification", (data: any) => { + if (is(data, RootInfo)) { + setRoot(data.root); + } + }); + return () => { + socket.disconnect(); + }; + }, [active, cluster]); + + // Reset info whenever the cluster changes + React.useEffect(() => { + return () => { + setDashboardInfo(undefined); + setPerformanceInfo(undefined); + setRoot(undefined); + }; + }, [cluster]); + + return ( + + + + + {children} + + + + + ); +} + +export function useSetActive() { + const context = React.useContext(SetActiveContext); + if (!context) { + throw new Error(`useSetActive must be used within a StatsProvider`); + } + return context.setActive; +} + +export function useDashboardInfo() { + const context = React.useContext(DashboardContext); + if (!context) { + throw new Error(`useDashboardInfo must be used within a StatsProvider`); + } + return context.info; +} + +export function usePerformanceInfo() { + const context = React.useContext(PerformanceContext); + if (!context) { + throw new Error(`usePerformanceInfo must be used within a StatsProvider`); + } + return context.info; +} + +export function useRootSlot() { + const context = React.useContext(RootContext); + if (!context) { + throw new Error(`useRootSlot must be used within a StatsProvider`); + } + return context.slot; +} diff --git a/explorer/src/scss/_solana-variables.scss b/explorer/src/scss/_solana-variables.scss index bd7a29a892..7d409738b0 100644 --- a/explorer/src/scss/_solana-variables.scss +++ b/explorer/src/scss/_solana-variables.scss @@ -16,7 +16,7 @@ $path-to-fonts: "../../fonts" !default; $white: #ffffff; $gray-100: #f9fdfc; $gray-200: #f1f8f6; -$gray-300: #d9efe7; +$gray-300: #e5ebe9; $gray-400: #c6e6de; $gray-500: #abd5c6; $gray-600: #86b8b6; @@ -42,6 +42,7 @@ $danger: #43b5c5; $light: $gray-100; $dark: $gray-900; +$card-border-color: $gray-300; $text-info-muted: $info-muted; $navbar-light-active-color: $primary; $theme-colors: ( diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index ee1d4119aa..0d94df6bf5 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -1,4 +1,14 @@ import { LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { + HumanizeDuration, + HumanizeDurationLanguage, +} from "humanize-duration-ts"; + +export const NUM_TICKS_PER_SECOND = 160; +export const DEFAULT_TICKS_PER_SLOT = 64; +export const NUM_SLOTS_PER_SECOND = + NUM_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT; +export const MS_PER_SLOT = 1000 / NUM_SLOTS_PER_SECOND; export function assertUnreachable(x: never): never { throw new Error("Unreachable!"); @@ -13,3 +23,28 @@ export function lamportsToSolString( "◎" + new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol) ); } + +const HUMANIZER = new HumanizeDuration(new HumanizeDurationLanguage()); +HUMANIZER.setOptions({ + language: "short", + spacer: "", + delimiter: " ", + round: true, + units: ["d", "h", "m", "s"], + largest: 3, +}); +HUMANIZER.addLanguage("short", { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + decimal: ".", +}); + +export function slotsToHumanString(slots: number): string { + return HUMANIZER.humanize(slots * MS_PER_SLOT); +}