Remove Solanabeach dependency from Explorer (#12463)
* remove solana beach socket dependency * remove socket.io dependency * timeout / retry button for cluster stats * update web3 version, add EpochInfo typing, handle no samples case * derive max TPS from final downsampled arrays * change block time to slot time
This commit is contained in:
parent
3b3f7341fa
commit
c7c6c28455
|
@ -3892,11 +3892,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"after": {
|
|
||||||
"version": "0.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
|
|
||||||
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
|
|
||||||
},
|
|
||||||
"aggregate-error": {
|
"aggregate-error": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
|
||||||
|
@ -4111,11 +4106,6 @@
|
||||||
"es-abstract": "^1.17.0-next.1"
|
"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": {
|
"arrify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
||||||
|
@ -4901,11 +4891,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
|
||||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
|
"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": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
@ -4974,11 +4959,6 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"base64-arraybuffer": {
|
|
||||||
"version": "0.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
|
||||||
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
|
|
||||||
},
|
|
||||||
"base64-js": {
|
"base64-js": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||||
|
@ -5016,11 +4996,6 @@
|
||||||
"file-uri-to-path": "1.0.0"
|
"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": {
|
"block-stream": {
|
||||||
"version": "0.0.9",
|
"version": "0.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
|
||||||
|
@ -5908,21 +5883,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
|
"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": {
|
"component-emitter": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
"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": {
|
"compose-function": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
|
||||||
|
@ -6978,59 +6943,6 @@
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engine.io-client": {
|
|
||||||
"version": "3.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz",
|
|
||||||
"integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==",
|
|
||||||
"requires": {
|
|
||||||
"component-emitter": "~1.3.0",
|
|
||||||
"component-inherit": "0.0.3",
|
|
||||||
"debug": "~3.1.0",
|
|
||||||
"engine.io-parser": "~2.2.0",
|
|
||||||
"has-cors": "1.1.0",
|
|
||||||
"indexof": "0.0.1",
|
|
||||||
"parseqs": "0.0.6",
|
|
||||||
"parseuri": "0.0.6",
|
|
||||||
"ws": "~6.1.0",
|
|
||||||
"xmlhttprequest-ssl": "~1.5.4",
|
|
||||||
"yeast": "0.1.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
|
||||||
},
|
|
||||||
"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.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
|
|
||||||
"requires": {
|
|
||||||
"after": "0.8.2",
|
|
||||||
"arraybuffer.slice": "~0.0.7",
|
|
||||||
"base64-arraybuffer": "0.1.4",
|
|
||||||
"blob": "0.0.5",
|
|
||||||
"has-binary2": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enhanced-resolve": {
|
"enhanced-resolve": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
|
||||||
|
@ -8616,26 +8528,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"has-flag": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
@ -9044,11 +8936,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
|
||||||
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
|
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
|
||||||
},
|
},
|
||||||
"indexof": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
|
|
||||||
},
|
|
||||||
"infer-owner": {
|
"infer-owner": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
|
||||||
|
@ -11880,16 +11767,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
|
||||||
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
|
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
|
||||||
},
|
},
|
||||||
"parseqs": {
|
|
||||||
"version": "0.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
|
|
||||||
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
|
|
||||||
},
|
|
||||||
"parseuri": {
|
|
||||||
"version": "0.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
|
|
||||||
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
|
|
||||||
},
|
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
@ -14977,69 +14854,6 @@
|
||||||
"kind-of": "^3.2.0"
|
"kind-of": "^3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"socket.io-client": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==",
|
|
||||||
"requires": {
|
|
||||||
"backo2": "1.0.2",
|
|
||||||
"component-bind": "1.0.0",
|
|
||||||
"component-emitter": "~1.3.0",
|
|
||||||
"debug": "~3.1.0",
|
|
||||||
"engine.io-client": "~3.4.0",
|
|
||||||
"has-binary2": "~1.0.2",
|
|
||||||
"indexof": "0.0.1",
|
|
||||||
"parseqs": "0.0.6",
|
|
||||||
"parseuri": "0.0.6",
|
|
||||||
"socket.io-parser": "~3.3.0",
|
|
||||||
"to-array": "0.1.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"socket.io-parser": {
|
|
||||||
"version": "3.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",
|
|
||||||
"integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==",
|
|
||||||
"requires": {
|
|
||||||
"component-emitter": "~1.3.0",
|
|
||||||
"debug": "~3.1.0",
|
|
||||||
"isarray": "2.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"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": {
|
"sockjs": {
|
||||||
"version": "0.3.20",
|
"version": "0.3.20",
|
||||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
|
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
|
||||||
|
@ -15962,11 +15776,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
||||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
|
"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": {
|
"to-arraybuffer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
|
||||||
|
@ -17189,11 +16998,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
|
"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": {
|
"xregexp": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
|
||||||
|
@ -17342,11 +17146,6 @@
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"yeast": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
|
||||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "3.4.3",
|
"react-scripts": "3.4.3",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "^3.1.0",
|
||||||
"socket.io-client": "^2.3.1",
|
|
||||||
"solana-sdk-wasm": "file:wasm/pkg",
|
"solana-sdk-wasm": "file:wasm/pkg",
|
||||||
"superstruct": "github:solana-labs/superstruct",
|
"superstruct": "github:solana-labs/superstruct",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
|
|
|
@ -4,12 +4,13 @@ import CountUp from "react-countup";
|
||||||
import {
|
import {
|
||||||
usePerformanceInfo,
|
usePerformanceInfo,
|
||||||
PERF_UPDATE_SEC,
|
PERF_UPDATE_SEC,
|
||||||
PerformanceInfo,
|
ClusterStatsStatus,
|
||||||
} from "providers/stats/solanaBeach";
|
} from "providers/stats/solanaClusterStats";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
|
||||||
import { ChartOptions, ChartTooltipModel } from "chart.js";
|
import { ChartOptions, ChartTooltipModel } from "chart.js";
|
||||||
|
import { PerformanceInfo } from "providers/stats/solanaPerformanceInfo";
|
||||||
|
import { StatsNotReady } from "pages/ClusterStatsPage";
|
||||||
|
|
||||||
export function TpsCard() {
|
export function TpsCard() {
|
||||||
return (
|
return (
|
||||||
|
@ -24,26 +25,12 @@ export function TpsCard() {
|
||||||
|
|
||||||
function TpsCardBody() {
|
function TpsCardBody() {
|
||||||
const performanceInfo = usePerformanceInfo();
|
const performanceInfo = usePerformanceInfo();
|
||||||
const { cluster } = useCluster();
|
|
||||||
|
|
||||||
const statsAvailable =
|
if (performanceInfo.status !== ClusterStatsStatus.Ready) {
|
||||||
cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet;
|
|
||||||
if (!statsAvailable) {
|
|
||||||
return (
|
return (
|
||||||
<div className="card-body text-center">
|
<StatsNotReady
|
||||||
<div className="text-muted">
|
error={performanceInfo.status === ClusterStatsStatus.Error}
|
||||||
Stats are not available for this cluster
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!performanceInfo) {
|
|
||||||
return (
|
|
||||||
<div className="card-body text-center">
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,15 +41,15 @@ type Series = "short" | "medium" | "long";
|
||||||
const SERIES: Series[] = ["short", "medium", "long"];
|
const SERIES: Series[] = ["short", "medium", "long"];
|
||||||
const SERIES_INFO = {
|
const SERIES_INFO = {
|
||||||
short: {
|
short: {
|
||||||
label: (index: number) => Math.floor(index / 4),
|
label: (index: number) => index,
|
||||||
interval: "30m",
|
interval: "30m",
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
label: (index: number) => index,
|
label: (index: number) => index * 4,
|
||||||
interval: "2h",
|
interval: "2h",
|
||||||
},
|
},
|
||||||
long: {
|
long: {
|
||||||
label: (index: number) => 3 * index,
|
label: (index: number) => index * 12,
|
||||||
interval: "6h",
|
interval: "6h",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { Slot } from "components/common/Slot";
|
import { Slot } from "components/common/Slot";
|
||||||
import {
|
import {
|
||||||
|
ClusterStatsStatus,
|
||||||
useDashboardInfo,
|
useDashboardInfo,
|
||||||
usePerformanceInfo,
|
usePerformanceInfo,
|
||||||
useSetActive,
|
useStatsProvider,
|
||||||
} from "providers/stats/solanaBeach";
|
} from "providers/stats/solanaClusterStats";
|
||||||
import { slotsToHumanString } from "utils";
|
import { slotsToHumanString } from "utils";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
import { useCluster } from "providers/cluster";
|
||||||
import { TpsCard } from "components/TpsCard";
|
import { TpsCard } from "components/TpsCard";
|
||||||
|
|
||||||
|
const CLUSTER_STATS_TIMEOUT = 10000;
|
||||||
|
|
||||||
export function ClusterStatsPage() {
|
export function ClusterStatsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
|
@ -32,44 +34,33 @@ export function ClusterStatsPage() {
|
||||||
function StatsCardBody() {
|
function StatsCardBody() {
|
||||||
const dashboardInfo = useDashboardInfo();
|
const dashboardInfo = useDashboardInfo();
|
||||||
const performanceInfo = usePerformanceInfo();
|
const performanceInfo = usePerformanceInfo();
|
||||||
const setSocketActive = useSetActive();
|
const { setActive } = useStatsProvider();
|
||||||
const { cluster } = useCluster();
|
const { cluster } = useCluster();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setSocketActive(true);
|
setActive(true);
|
||||||
return () => setSocketActive(false);
|
return () => setActive(false);
|
||||||
}, [setSocketActive, cluster]);
|
}, [setActive, cluster]);
|
||||||
|
|
||||||
const statsAvailable =
|
if (
|
||||||
cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet;
|
performanceInfo.status !== ClusterStatsStatus.Ready ||
|
||||||
if (!statsAvailable) {
|
dashboardInfo.status !== ClusterStatsStatus.Ready
|
||||||
return (
|
) {
|
||||||
<div className="card-body text-center">
|
const error =
|
||||||
<div className="text-muted">
|
performanceInfo.status === ClusterStatsStatus.Error ||
|
||||||
Stats are not available for this cluster
|
dashboardInfo.status === ClusterStatsStatus.Error;
|
||||||
</div>
|
return <StatsNotReady error={error} />;
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dashboardInfo || !performanceInfo) {
|
const { avgSlotTime_1h, avgSlotTime_1min, epochInfo } = dashboardInfo;
|
||||||
return (
|
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
||||||
<div className="card-body text-center">
|
const averageSlotTime = Math.round(1000 * avgSlotTime_1min) + "ms";
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { avgBlockTime_1h, avgBlockTime_1min, epochInfo } = dashboardInfo;
|
|
||||||
const hourlyBlockTime = Math.round(1000 * avgBlockTime_1h);
|
|
||||||
const averageBlockTime = Math.round(1000 * avgBlockTime_1min) + "ms";
|
|
||||||
const { slotIndex, slotsInEpoch } = epochInfo;
|
const { slotIndex, slotsInEpoch } = epochInfo;
|
||||||
const currentEpoch = epochInfo.epoch.toString();
|
const currentEpoch = epochInfo.epoch.toString();
|
||||||
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
||||||
const epochTimeRemaining = slotsToHumanString(
|
const epochTimeRemaining = slotsToHumanString(
|
||||||
slotsInEpoch - slotIndex,
|
slotsInEpoch - slotIndex,
|
||||||
hourlyBlockTime
|
hourlySlotTime
|
||||||
);
|
);
|
||||||
const { blockHeight, absoluteSlot } = epochInfo;
|
const { blockHeight, absoluteSlot } = epochInfo;
|
||||||
|
|
||||||
|
@ -81,15 +72,17 @@ function StatsCardBody() {
|
||||||
<Slot slot={absoluteSlot} />
|
<Slot slot={absoluteSlot} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{blockHeight !== undefined && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Block height</td>
|
<td className="w-100">Block height</td>
|
||||||
<td className="text-lg-right text-monospace">
|
<td className="text-lg-right text-monospace">
|
||||||
<Slot slot={blockHeight} />
|
<Slot slot={blockHeight} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Block time</td>
|
<td className="w-100">Slot time</td>
|
||||||
<td className="text-lg-right text-monospace">{averageBlockTime}</td>
|
<td className="text-lg-right text-monospace">{averageSlotTime}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Epoch</td>
|
<td className="w-100">Epoch</td>
|
||||||
|
@ -106,3 +99,44 @@ function StatsCardBody() {
|
||||||
</TableCardBody>
|
</TableCardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StatsNotReady({ error }: { error: boolean }) {
|
||||||
|
const { setTimedOut, retry, active } = useStatsProvider();
|
||||||
|
const { cluster } = useCluster();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let timedOut = 0;
|
||||||
|
if (!error) {
|
||||||
|
timedOut = setTimeout(setTimedOut, CLUSTER_STATS_TIMEOUT);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timedOut) {
|
||||||
|
clearTimeout(timedOut);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [setTimedOut, cluster, error]);
|
||||||
|
|
||||||
|
if (error || !active) {
|
||||||
|
return (
|
||||||
|
<div className="card-body text-center">
|
||||||
|
There was a problem loading cluster stats.{" "}
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
retry();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SolanaBeachProvider } from "./solanaBeach";
|
import { SolanaClusterStatsProvider } from "./solanaClusterStats";
|
||||||
|
|
||||||
type Props = { children: React.ReactNode };
|
type Props = { children: React.ReactNode };
|
||||||
export function StatsProvider({ children }: Props) {
|
export function StatsProvider({ children }: Props) {
|
||||||
return <SolanaBeachProvider>{children}</SolanaBeachProvider>;
|
return <SolanaClusterStatsProvider>{children}</SolanaClusterStatsProvider>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,202 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import io from "socket.io-client";
|
|
||||||
|
|
||||||
import { pick, array, nullable, number, is, StructType } from "superstruct";
|
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
|
||||||
|
|
||||||
const DashboardInfo = pick({
|
|
||||||
avgBlockTime_1h: number(),
|
|
||||||
avgBlockTime_1min: number(),
|
|
||||||
epochInfo: pick({
|
|
||||||
absoluteSlot: number(),
|
|
||||||
blockHeight: number(),
|
|
||||||
epoch: number(),
|
|
||||||
slotIndex: number(),
|
|
||||||
slotsInEpoch: number(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const RootInfo = pick({
|
|
||||||
root: number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PERF_UPDATE_SEC = 5;
|
|
||||||
|
|
||||||
const PerformanceInfo = pick({
|
|
||||||
avgTPS: number(),
|
|
||||||
perfHistory: pick({
|
|
||||||
s: array(nullable(number())),
|
|
||||||
m: array(nullable(number())),
|
|
||||||
l: array(nullable(number())),
|
|
||||||
}),
|
|
||||||
totalTransactionCount: number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
const SetActiveContext = React.createContext<
|
|
||||||
{ setActive: SetActive } | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
type RootInfo = StructType<typeof RootInfo>;
|
|
||||||
type RootState = { slot: number | undefined };
|
|
||||||
const RootContext = React.createContext<RootState | undefined>(undefined);
|
|
||||||
|
|
||||||
type DashboardInfo = StructType<typeof DashboardInfo>;
|
|
||||||
type DashboardState = { info: DashboardInfo | undefined };
|
|
||||||
const DashboardContext = React.createContext<DashboardState | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PerformanceInfo = {
|
|
||||||
avgTps: number;
|
|
||||||
historyMaxTps: number;
|
|
||||||
perfHistory: {
|
|
||||||
short: (number | null)[];
|
|
||||||
medium: (number | null)[];
|
|
||||||
long: (number | null)[];
|
|
||||||
};
|
|
||||||
transactionCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PerformanceState = { info: PerformanceInfo | undefined };
|
|
||||||
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
|
||||||
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<number>();
|
|
||||||
const [dashboardInfo, setDashboardInfo] = React.useState<DashboardInfo>();
|
|
||||||
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)) {
|
|
||||||
const trimSeries = (series: (number | null)[]) => {
|
|
||||||
return series.slice(series.length - 51, series.length - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const seriesMax = (series: (number | null)[]) => {
|
|
||||||
return series.reduce((max: number, next) => {
|
|
||||||
if (next === null) return max;
|
|
||||||
return Math.max(max, next);
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalize = (series: Array<number | null>, seconds: number) => {
|
|
||||||
return series.map((next) => {
|
|
||||||
if (next === null) return next;
|
|
||||||
return Math.round(next / seconds);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const short = normalize(trimSeries(data.perfHistory.s), 15);
|
|
||||||
const medium = normalize(trimSeries(data.perfHistory.m), 60);
|
|
||||||
const long = normalize(trimSeries(data.perfHistory.l), 180);
|
|
||||||
const historyMaxTps = Math.max(
|
|
||||||
seriesMax(short),
|
|
||||||
seriesMax(medium),
|
|
||||||
seriesMax(long)
|
|
||||||
);
|
|
||||||
|
|
||||||
setPerformanceInfo({
|
|
||||||
avgTps: data.avgTPS,
|
|
||||||
historyMaxTps,
|
|
||||||
perfHistory: { short, medium, long },
|
|
||||||
transactionCount: data.totalTransactionCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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 (
|
|
||||||
<SetActiveContext.Provider value={{ setActive }}>
|
|
||||||
<DashboardContext.Provider value={{ info: dashboardInfo }}>
|
|
||||||
<PerformanceContext.Provider value={{ info: performanceInfo }}>
|
|
||||||
<RootContext.Provider value={{ slot: root }}>
|
|
||||||
{children}
|
|
||||||
</RootContext.Provider>
|
|
||||||
</PerformanceContext.Provider>
|
|
||||||
</DashboardContext.Provider>
|
|
||||||
</SetActiveContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Connection } from "@solana/web3.js";
|
||||||
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
import {
|
||||||
|
DashboardInfo,
|
||||||
|
DashboardInfoActionType,
|
||||||
|
dashboardInfoReducer,
|
||||||
|
} from "./solanaDashboardInfo";
|
||||||
|
import {
|
||||||
|
PerformanceInfo,
|
||||||
|
PerformanceInfoActionType,
|
||||||
|
performanceInfoReducer,
|
||||||
|
} from "./solanaPerformanceInfo";
|
||||||
|
import { reportError } from "utils/sentry";
|
||||||
|
|
||||||
|
export const PERF_UPDATE_SEC = 5;
|
||||||
|
export const SAMPLE_HISTORY_HOURS = 6;
|
||||||
|
export const PERFORMANCE_SAMPLE_INTERVAL = 60000;
|
||||||
|
export const TRANSACTION_COUNT_INTERVAL = 5000;
|
||||||
|
export const EPOCH_INFO_INTERVAL = 2000;
|
||||||
|
export const LOADING_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
export enum ClusterStatsStatus {
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialPerformanceInfo: PerformanceInfo = {
|
||||||
|
status: ClusterStatsStatus.Loading,
|
||||||
|
avgTps: 0,
|
||||||
|
historyMaxTps: 0,
|
||||||
|
perfHistory: {
|
||||||
|
short: [],
|
||||||
|
medium: [],
|
||||||
|
long: [],
|
||||||
|
},
|
||||||
|
transactionCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialDashboardInfo: DashboardInfo = {
|
||||||
|
status: ClusterStatsStatus.Loading,
|
||||||
|
avgSlotTime_1h: 0,
|
||||||
|
avgSlotTime_1min: 0,
|
||||||
|
epochInfo: {
|
||||||
|
absoluteSlot: 0,
|
||||||
|
blockHeight: 0,
|
||||||
|
epoch: 0,
|
||||||
|
slotIndex: 0,
|
||||||
|
slotsInEpoch: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
const StatsProviderContext = React.createContext<
|
||||||
|
| {
|
||||||
|
setActive: SetActive;
|
||||||
|
setTimedOut: Function;
|
||||||
|
retry: Function;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
type DashboardState = { info: DashboardInfo };
|
||||||
|
const DashboardContext = React.createContext<DashboardState | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
type PerformanceState = { info: PerformanceInfo };
|
||||||
|
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = { children: React.ReactNode };
|
||||||
|
export function SolanaClusterStatsProvider({ children }: Props) {
|
||||||
|
const { cluster, url } = useCluster();
|
||||||
|
const [active, setActive] = React.useState(false);
|
||||||
|
const [dashboardInfo, dispatchDashboardInfo] = React.useReducer(
|
||||||
|
dashboardInfoReducer,
|
||||||
|
initialDashboardInfo
|
||||||
|
);
|
||||||
|
const [performanceInfo, dispatchPerformanceInfo] = React.useReducer(
|
||||||
|
performanceInfoReducer,
|
||||||
|
initialPerformanceInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!active || !url) return;
|
||||||
|
|
||||||
|
const connection = new Connection(url);
|
||||||
|
|
||||||
|
const getPerformanceSamples = async () => {
|
||||||
|
try {
|
||||||
|
const samples = await connection.getRecentPerformanceSamples(
|
||||||
|
60 * SAMPLE_HISTORY_HOURS
|
||||||
|
);
|
||||||
|
|
||||||
|
if (samples.length < 1) {
|
||||||
|
// no samples to work with (node has no history).
|
||||||
|
return; // we will allow for a timeout instead of throwing an error
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.SetPerfSamples,
|
||||||
|
data: samples,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.SetPerfSamples,
|
||||||
|
data: samples,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(error, { url });
|
||||||
|
}
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.SetError,
|
||||||
|
data: error.toString(),
|
||||||
|
});
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.SetError,
|
||||||
|
data: error.toString(),
|
||||||
|
});
|
||||||
|
setActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTransactionCount = async () => {
|
||||||
|
try {
|
||||||
|
const transactionCount = await connection.getTransactionCount();
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.SetTransactionCount,
|
||||||
|
data: transactionCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(error, { url });
|
||||||
|
}
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.SetError,
|
||||||
|
data: error.toString(),
|
||||||
|
});
|
||||||
|
setActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEpochInfo = async () => {
|
||||||
|
try {
|
||||||
|
const epochInfo = await connection.getEpochInfo();
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.SetEpochInfo,
|
||||||
|
data: epochInfo,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(error, { url });
|
||||||
|
}
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.SetError,
|
||||||
|
data: error.toString(),
|
||||||
|
});
|
||||||
|
setActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performanceInterval = setInterval(
|
||||||
|
getPerformanceSamples,
|
||||||
|
PERFORMANCE_SAMPLE_INTERVAL
|
||||||
|
);
|
||||||
|
const transactionCountInterval = setInterval(
|
||||||
|
getTransactionCount,
|
||||||
|
TRANSACTION_COUNT_INTERVAL
|
||||||
|
);
|
||||||
|
const epochInfoInterval = setInterval(getEpochInfo, EPOCH_INFO_INTERVAL);
|
||||||
|
|
||||||
|
getPerformanceSamples();
|
||||||
|
getTransactionCount();
|
||||||
|
getEpochInfo();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(performanceInterval);
|
||||||
|
clearInterval(transactionCountInterval);
|
||||||
|
clearInterval(epochInfoInterval);
|
||||||
|
};
|
||||||
|
}, [active, cluster, url]);
|
||||||
|
|
||||||
|
// Reset when cluster changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
resetData();
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
function resetData() {
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.Reset,
|
||||||
|
data: initialDashboardInfo,
|
||||||
|
});
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.Reset,
|
||||||
|
data: initialPerformanceInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTimedOut = React.useCallback(() => {
|
||||||
|
dispatchDashboardInfo({
|
||||||
|
type: DashboardInfoActionType.SetError,
|
||||||
|
data: "Cluster stats timed out",
|
||||||
|
});
|
||||||
|
dispatchPerformanceInfo({
|
||||||
|
type: PerformanceInfoActionType.SetError,
|
||||||
|
data: "Cluster stats timed out",
|
||||||
|
});
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(new Error("Cluster stats timed out"), { url });
|
||||||
|
}
|
||||||
|
setActive(false);
|
||||||
|
}, [cluster, url]);
|
||||||
|
|
||||||
|
const retry = React.useCallback(() => {
|
||||||
|
resetData();
|
||||||
|
setActive(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsProviderContext.Provider
|
||||||
|
value={{ setActive, setTimedOut, retry, active }}
|
||||||
|
>
|
||||||
|
<DashboardContext.Provider value={{ info: dashboardInfo }}>
|
||||||
|
<PerformanceContext.Provider value={{ info: performanceInfo }}>
|
||||||
|
{children}
|
||||||
|
</PerformanceContext.Provider>
|
||||||
|
</DashboardContext.Provider>
|
||||||
|
</StatsProviderContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStatsProvider() {
|
||||||
|
const context = React.useContext(StatsProviderContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useContext must be used within a StatsProvider`);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { EpochInfo, PerfSample } from "@solana/web3.js";
|
||||||
|
import { ClusterStatsStatus } from "./solanaClusterStats";
|
||||||
|
|
||||||
|
export type DashboardInfo = {
|
||||||
|
status: ClusterStatsStatus;
|
||||||
|
avgSlotTime_1h: number;
|
||||||
|
avgSlotTime_1min: number;
|
||||||
|
epochInfo: EpochInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum DashboardInfoActionType {
|
||||||
|
SetPerfSamples,
|
||||||
|
SetEpochInfo,
|
||||||
|
SetError,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardInfoActionSetPerfSamples = {
|
||||||
|
type: DashboardInfoActionType.SetPerfSamples;
|
||||||
|
data: PerfSample[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardInfoActionSetEpochInfo = {
|
||||||
|
type: DashboardInfoActionType.SetEpochInfo;
|
||||||
|
data: EpochInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardInfoActionReset = {
|
||||||
|
type: DashboardInfoActionType.Reset;
|
||||||
|
data: DashboardInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardInfoActionSetError = {
|
||||||
|
type: DashboardInfoActionType.SetError;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardInfoAction =
|
||||||
|
| DashboardInfoActionSetPerfSamples
|
||||||
|
| DashboardInfoActionSetEpochInfo
|
||||||
|
| DashboardInfoActionReset
|
||||||
|
| DashboardInfoActionSetError;
|
||||||
|
|
||||||
|
export function dashboardInfoReducer(
|
||||||
|
state: DashboardInfo,
|
||||||
|
action: DashboardInfoAction
|
||||||
|
) {
|
||||||
|
const status =
|
||||||
|
state.avgSlotTime_1h !== 0 && state.epochInfo.absoluteSlot !== 0
|
||||||
|
? ClusterStatsStatus.Ready
|
||||||
|
: ClusterStatsStatus.Loading;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case DashboardInfoActionType.SetPerfSamples:
|
||||||
|
if (action.data.length < 1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = action.data
|
||||||
|
.map((sample) => {
|
||||||
|
return sample.samplePeriodSecs / sample.numSlots;
|
||||||
|
})
|
||||||
|
.slice(0, 60);
|
||||||
|
|
||||||
|
const samplesInHour = samples.length < 60 ? samples.length : 60;
|
||||||
|
const avgSlotTime_1h =
|
||||||
|
samples.reduce((sum: number, cur: number) => {
|
||||||
|
return sum + cur;
|
||||||
|
}, 0) / samplesInHour;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
avgSlotTime_1h,
|
||||||
|
avgSlotTime_1min: samples[0],
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
case DashboardInfoActionType.SetEpochInfo:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
epochInfo: action.data,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
case DashboardInfoActionType.SetError:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
status: ClusterStatsStatus.Error,
|
||||||
|
};
|
||||||
|
case DashboardInfoActionType.Reset:
|
||||||
|
return {
|
||||||
|
...action.data,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { PerfSample } from "@solana/web3.js";
|
||||||
|
import { ClusterStatsStatus } from "./solanaClusterStats";
|
||||||
|
|
||||||
|
export type PerformanceInfo = {
|
||||||
|
status: ClusterStatsStatus;
|
||||||
|
avgTps: number;
|
||||||
|
historyMaxTps: number;
|
||||||
|
perfHistory: {
|
||||||
|
short: (number | null)[];
|
||||||
|
medium: (number | null)[];
|
||||||
|
long: (number | null)[];
|
||||||
|
};
|
||||||
|
transactionCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum PerformanceInfoActionType {
|
||||||
|
SetTransactionCount,
|
||||||
|
SetPerfSamples,
|
||||||
|
SetError,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PerformanceInfoActionSetTransactionCount = {
|
||||||
|
type: PerformanceInfoActionType.SetTransactionCount;
|
||||||
|
data: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PerformanceInfoActionSetPerfSamples = {
|
||||||
|
type: PerformanceInfoActionType.SetPerfSamples;
|
||||||
|
data: PerfSample[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PerformanceInfoActionSetError = {
|
||||||
|
type: PerformanceInfoActionType.SetError;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PerformanceInfoActionReset = {
|
||||||
|
type: PerformanceInfoActionType.Reset;
|
||||||
|
data: PerformanceInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PerformanceInfoAction =
|
||||||
|
| PerformanceInfoActionSetTransactionCount
|
||||||
|
| PerformanceInfoActionSetPerfSamples
|
||||||
|
| PerformanceInfoActionSetError
|
||||||
|
| PerformanceInfoActionReset;
|
||||||
|
|
||||||
|
export function performanceInfoReducer(
|
||||||
|
state: PerformanceInfo,
|
||||||
|
action: PerformanceInfoAction
|
||||||
|
) {
|
||||||
|
const status =
|
||||||
|
state.avgTps !== 0 && state.transactionCount !== 0
|
||||||
|
? ClusterStatsStatus.Ready
|
||||||
|
: ClusterStatsStatus.Loading;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case PerformanceInfoActionType.SetPerfSamples:
|
||||||
|
if (action.data.length < 1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
let short = action.data.map((sample) => {
|
||||||
|
return sample.numTransactions / sample.samplePeriodSecs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgTps = short[0];
|
||||||
|
const medium = downsampleByFactor(short, 4);
|
||||||
|
const long = downsampleByFactor(medium, 3);
|
||||||
|
|
||||||
|
const perfHistory = {
|
||||||
|
short: round(short.slice(0, 30)).reverse(),
|
||||||
|
medium: round(medium.slice(0, 30)).reverse(),
|
||||||
|
long: round(long.slice(0, 30)).reverse(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const historyMaxTps = Math.max(
|
||||||
|
Math.max(...perfHistory.short),
|
||||||
|
Math.max(...perfHistory.medium),
|
||||||
|
Math.max(...perfHistory.long)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
historyMaxTps,
|
||||||
|
avgTps,
|
||||||
|
perfHistory,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
case PerformanceInfoActionType.SetTransactionCount:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
transactionCount: action.data,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
case PerformanceInfoActionType.SetError:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
status: ClusterStatsStatus.Error,
|
||||||
|
};
|
||||||
|
case PerformanceInfoActionType.Reset:
|
||||||
|
return {
|
||||||
|
...action.data,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downsampleByFactor(series: number[], factor: number) {
|
||||||
|
return series.reduce((result: number[], num: number, i: number) => {
|
||||||
|
const downsampledIndex = Math.floor(i / factor);
|
||||||
|
if (result.length < downsampledIndex + 1) {
|
||||||
|
result.push(0);
|
||||||
|
}
|
||||||
|
const mean = result[downsampledIndex];
|
||||||
|
const differential = (num - mean) / ((i % factor) + 1);
|
||||||
|
result[downsampledIndex] = mean + differential;
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function round(series: number[]) {
|
||||||
|
return series.map((n) => Math.round(n));
|
||||||
|
}
|
Loading…
Reference in New Issue