├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── BUG_REPORT_TEMPLATE.md │ ├── FEATURE_REQUEST_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── TROUBLESHOOTING.md ├── bin ├── ethstats-cli.js └── ethstats-daemon.js ├── gulpfile.js ├── lib ├── Cli.js ├── Config.js ├── Configurator.js ├── Logger.js ├── Register.js ├── Server.js ├── app-cli.js ├── app-daemon.js └── client │ ├── Error.js │ ├── HwInfo.js │ ├── Usage.js │ ├── index.js │ └── protocol │ ├── Abstract.js │ ├── Http.js │ └── WebSocket.js ├── package-lock.json ├── package.json ├── test └── index.js └── triggerDockerHub.sh /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10 7 | working_directory: ~/repo 8 | steps: 9 | - checkout 10 | - run: 11 | name: Install Node Modules 12 | command: npm install 13 | - run: 14 | name: Gulp Lint 15 | command: npm run gulp lint 16 | - run: 17 | name: Gulp Build 18 | command: npm run gulp prepare 19 | 20 | trigger-docker-hub: 21 | docker: 22 | - image: circleci/node:latest 23 | working_directory: ~/repo 24 | steps: 25 | - checkout 26 | - run: 27 | name: Trigger DockerHub 28 | command: ./triggerDockerHub.sh 29 | 30 | workflows: 31 | version: 2 32 | deploy: 33 | jobs: 34 | - build: 35 | filters: 36 | tags: 37 | only: /.*/ 38 | - trigger-docker-hub: 39 | requires: 40 | - build 41 | filters: 42 | tags: 43 | only: /^v[0-9.]+$/ 44 | branches: 45 | only: master 46 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .github 3 | node_modules 4 | coverage 5 | dist 6 | bin 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["xo-space"], 4 | "plugins": [ 5 | "babel" 6 | ], 7 | "env": { 8 | "mocha": true 9 | }, 10 | "rules": { 11 | "no-warning-comments": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | #### Describe the bug 7 | A clear and concise description of what the bug is. 8 | 9 | #### To Reproduce 10 | Steps to reproduce the behavior. 11 | 12 | #### Expected behavior 13 | A clear and concise description of what you expected to happen. 14 | 15 | #### Screenshots 16 | If applicable, add screenshots to help explain your problem. 17 | 18 | #### Environment (please complete the following information): 19 | * OS name: [e.g. Linux, OSX, Windows] 20 | * OS version [e.g. 22] 21 | * Ethereum node name: [e.q. Geth, Parity] 22 | * Ethereum node version: [e.q. 1.2.3] 23 | * ethstas-cli version: [e.g. 1.2.3] 24 | 25 | #### Additional context 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | #### Feature Request 7 | A clear and concise description of the feature. 8 | 9 | #### Acceptance Criteria 10 | A clear and concise description of the acceptance criteria. 11 | 12 | #### Benefits: 13 | * Benefit 1... 14 | * Benefit 2... 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Request to merge Changes / Bug fixes / New features 4 | --- 5 | 6 | #### Link to issue number: 7 | 8 | #### Summary of the issue: 9 | 10 | #### Description of how this pull request fixes the issue: 11 | 12 | #### Testing performed: 13 | 14 | #### Known issues with pull request: 15 | 16 | #### Change log entry: 17 | 18 | Section: Changes, Bug fixes, New features 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | bin/dev-cli.js 7 | bin/dev-daemon.js 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | 43 | # IntelliJ IDEA 44 | .idea/ 45 | 46 | # Dist files 47 | dist/ 48 | 49 | # Node request files 50 | .node-xmlhttprequest-* 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dist files 2 | !dist/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v6 4 | - v5 5 | - v4 6 | - '0.12' 7 | - '0.10' 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [2.5.3] - 2019-06-06 5 | - Add support for sending to the server validators/signers on "clique" POA networks 6 | 7 | ## [2.5.2] - 2019-05-23 8 | - Fixed output on the stats variables sent to the server 9 | 10 | ## [2.5.1] - 2019-05-22 11 | - Add support for "clientTimeout" messages from the server 12 | 13 | ## [2.5.0] - 2019-05-21 14 | - Add WebSocket API improvements 15 | - Add support for "stats.pendingTxs" 16 | - Remove ".net" subdomain 17 | 18 | ## [2.4.21] - 2019-04-02 19 | - Remove "sprintf" npm package due to memory leaks 20 | - Update npm dependent packages to latest versions 21 | 22 | ## [2.4.20] - 2019-03-27 23 | - Update register text and readme file 24 | 25 | ## [2.4.19] - 2019-02-28 26 | - Nethermind support for the latest blocks filter ([#9](https://github.com/Alethio/ethstats-cli/issues/9)) 27 | - Added details for the authentication error if trying to change the network/server ([#9](https://github.com/Alethio/ethstats-cli/issues/9)) 28 | - Updated Readme file with more details ([#9](https://github.com/Alethio/ethstats-cli/issues/9)) 29 | - Fixed syncing bug 30 | 31 | ## [2.4.18] - 2019-02-18 32 | - Show correct dashboard url when app starts 33 | - Improvements reconnecting with the Ethereum node 34 | 35 | ## [2.4.17] - 2019-02-17 36 | - Fixed server reconnection bug 37 | - Updated Readme file 38 | 39 | ## [2.4.16] - 2019-02-14 40 | - Fixed web3 WebSocketProvider connection issue 41 | 42 | ## [2.4.14] - 2019-02-12 43 | - Reconnect to server improvements 44 | - Reinit web3 improvements when no new block received 45 | - Updated dependencies to latest versions 46 | 47 | ## [2.4.13] - 2019-01-19 48 | - Fixed websocket bug when reconnecting to the ethstats network server 49 | 50 | ## [2.4.12] - 2019-01-15 51 | - Added CircleCI workflow to trigger docker hub build sequentially 52 | - Updated Dockerfile to use node:alpine for smaller image size 53 | 54 | ## [2.4.11] - 2018-12-20 55 | - Fixed basic auth when Web3 HttpProvider is used 56 | 57 | ## [2.4.10] - 2018-12-12 58 | - Added privacy policy on register 59 | - Bug fix - reinit web3 provider when no new blocks received for more then 1 hour 60 | - Updated README file 61 | 62 | ## [2.4.9] - 2018-11-22 63 | - Updated package.json 64 | - Test automated builds on Docker Hub 65 | 66 | ## [2.4.8] - 2018-11-22 67 | - Updated README file 68 | - Updated issue template files 69 | 70 | ## [2.4.7] - 2018-11-20 71 | - Dependency packages update 72 | 73 | ## [2.4.6] - 2018-08-30 74 | - Backwards compatibility with v.1 clients 75 | - Updated dependencies to latest versions 76 | - Fixed eslint problems 77 | 78 | ## [2.4.5] - 2018-08-30 (removed from NPM) 79 | - NPM Publish debugging 80 | 81 | ## [2.4.4] - 2018-08-30 (removed from NPM) 82 | - NPM Publish debugging 83 | 84 | ## [2.4.3] - 2018-08-30 (removed from NPM) 85 | - NPM Publish debugging 86 | 87 | ## [2.4.2] - 2018-08-23 88 | - Improved logging Errors and Warnings 89 | - Added history request for light mode server with no persistence 90 | 91 | ## [2.4.1] - 2018-06-27 92 | - Bug fix on require 'babel-polyfill' module 93 | 94 | ## [2.4.0] - 2018-06-27 95 | - On login send CPU, memory and disk information 96 | - Every 5 seconds collect and send usage information 97 | 98 | ## [2.3.11] - 2018-06-22 99 | - Updated Dockerfile 100 | - Automatically build docker image to `hub.docker.com/r/alethio/ethstats-cli` 101 | 102 | ## [2.3.10] - 2018-06-14 103 | - Improved WS communication mechanism with the server. 104 | 105 | ## [2.3.9] - 2018-06-07 106 | - Fixed bug when ensuring the app is running only one instance inside docker container. 107 | 108 | ## [2.3.8] - 2018-06-07 109 | - Added debug infos 110 | 111 | ## [2.3.7] - 2018-06-07 112 | - Ensure the app is running only one instance. 113 | 114 | ## [2.3.6] - 2018-06-07 115 | - Improvements on the chain detection mechanism. 116 | - Bug fixes. 117 | 118 | ## [2.3.5] - 2018-05-21 119 | - Added "--configurator-url" for custom configuration service, that provides application specific configs. 120 | 121 | ## [2.3.4] - 2018-05-16 122 | - Updated Readme file to include Troubleshooting, because the github repo is private. 123 | 124 | ## [2.3.3] - 2018-05-16 125 | - Updated Readme file. 126 | - Added Changelog file. 127 | - Added Troubleshooting file. 128 | 129 | ## [2.3.2] - 2018-05-09 130 | - Updated Readme file. 131 | 132 | ## [2.3.1] - 2018-05-08 133 | - Fixes bug on connection. 134 | - Handle Parity sync subscription error. 135 | - Added configurable text after the app successfully started. 136 | - Updated Readme file. 137 | 138 | ## [2.3.0] - 2018-04-27 139 | - Added support for [Web3 1.0](http://web3js.readthedocs.io/en/1.0/index.html) for performance issues, using websockets/ipc subscriptions. The app will use also the old version of Web3 (0.20.x) for the HTTP Provider which in the new version is deprecated. 140 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | RUN apk update && \ 4 | apk add --no-cache git python g++ make procps 5 | 6 | WORKDIR /ethstats-cli 7 | 8 | COPY package.json package-lock.json .babelrc ./ 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | RUN npm run gulp prepare 15 | 16 | ENTRYPOINT ["./bin/ethstats-cli.js", "-vd"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alethio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ethstats-cli [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | 3 | > EthStats - CLI Client 4 | > 5 | > 6 | > The application connects to your Ethereum node through RPC and extract data that will be sent to the `EthStats - Server` for analytics purposes. 7 | 8 | # Live deployments 9 | See active nodes or add your own on the following running deployments of the EthStats Network Monitor 10 | 11 | - Mainnet - [ethstats.io](https://ethstats.io/) 12 | - Rinkeby Testnet - [rinkeby.ethstats.io](https://rinkeby.ethstats.io/) 13 | - Görli Testnet - [goerli.ethstats.io](https://goerli.ethstats.io/) 14 | 15 | # Supported Ethereum nodes 16 | Geth, Parity, Besu, basically any Ethereum node that has RPC enabled. 17 | 18 | # Contents 19 | - [Getting Started](#getting-started) 20 | - [Prerequisites](#prerequisites) 21 | - [Install](#install) 22 | - [Update](#update) 23 | - [Running](#running) 24 | - [Register node](#register-node) 25 | - [Config file](#config-file) 26 | - [Node recovery](#node-recovery) 27 | - [CLI Options](#cli-options) 28 | - [Daemon](#daemon) 29 | - [Docker](#docker) 30 | - [Troubleshooting](https://github.com/Alethio/ethstats-cli/blob/master/TROUBLESHOOTING.md) 31 | - [Changelog](https://github.com/Alethio/ethstats-cli/blob/master/CHANGELOG.md) 32 | - [License](https://github.com/Alethio/ethstats-cli/blob/master/LICENSE) 33 | 34 | # Getting started 35 | 36 | ## Prerequisites 37 | Please make sure you have the following installed and running properly 38 | - [Node.js](https://nodejs.org/en/download/) >= 8.11 39 | - [NPM](https://www.npmjs.com/get-npm) >= 5.6 (Usually NPM is distributed with Node.js) 40 | - Build Tools - To compile and install native addons from NPM you may also need to install tools like: make, gcc, g++. E.q. on ubuntu `build-essential` package has all the necessary tools. 41 | - [Yarn](https://yarnpkg.com) >= 1.5 Yarn is `optional`, being an alternative to NPM. 42 | - [Git](https://git-scm.com/downloads) - Some dependencies are downloaded through Git. 43 | - [Geth](https://geth.ethereum.org/install/) or [Parity](https://wiki.parity.io/Setup) running in one of the supported configurations **synced on the Ethereum main/foundation chain** 44 | - JSON-RPC http or websockets or ipc APIs enabled and accessible on the Ethereum node of choice (Geth/Parity) 45 | 46 | 47 | ## Install 48 | 49 | Install `ethstats-cli` globally 50 | 51 | With [npm](https://www.npmjs.com): 52 | ```sh 53 | npm install -g ethstats-cli 54 | ``` 55 | If you encounter permissions issues at install time please see [troubleshooting](https://github.com/Alethio/ethstats-cli/blob/master/TROUBLESHOOTING.md) section. 56 | 57 | Or [yarn](https://yarnpkg.com): 58 | ```sh 59 | yarn global add ethstats-cli 60 | ``` 61 | If after installing the package with yarn, the binaries are not found please see troubleshooting section: [Binaries not found](#binaries-not-found) 62 | 63 | ## Update 64 | 65 | Update `ethstats-cli` to the latest available version 66 | 67 | With [npm](https://www.npmjs.com): 68 | ```sh 69 | npm install -g ethstats-cli@latest 70 | ``` 71 | 72 | Or [yarn](https://yarnpkg.com): 73 | ```sh 74 | yarn global upgrade ethstats-cli 75 | ``` 76 | 77 | ## Running 78 | To run the app use the following command: 79 | ```sh 80 | $ ethstats-cli 81 | ``` 82 | The app is configured by default to connect to the Ethereum node on your local host (http://localhost:8545). 83 | To connect to a node running on a different host see `--client-url` under [CLI Options](#cli-options). 84 | 85 | The server that is connecting to by default is the one deployed for the Ethereum `mainnet`. For sending stats for a different network see the `--net` flag under [CLI Options](#cli-options). 86 | Changing the network requires a new registration of the node. This is required because based on the network you specify at registration time the stats will be sent to a different server. Each server has its own nodes/secretKeys. 87 | A new registration is possible if the [config file](#config-file) is deleted. 88 | 89 | IMPORTANT: To be able to extract all statistics from the Ethereum node we recommend running the app on the same host. The usage information about the node like cpu and memory load cannot be extracted if on a different host. 90 | 91 | ## Register node 92 | 93 | On the first run of the app the first thing it does is to register the Ethereum node in our platform. For this you will be asked about the network the node is running on, email address and node name. 94 | 95 | It is possible to register the node also in non interactive mode without asking the necessary infos by specifying the `--register` option like in the example bellow: 96 | ```sh 97 | $ ethstats-cli --register --account-email your@email.com --node-name your_node_name 98 | ``` 99 | 100 | For more details on these options please see [CLI Options](#cli-options). 101 | 102 | If the node is already registered and you still specify the `--register` option, it will be avoided. 103 | A new registration is possible if the [config file](#config-file) is deleted. 104 | 105 | NOTE: Every registered node will and must have its own secret key. 106 | 107 | ## Config file 108 | After the node was successfully registered, a config file is created in the following location: 109 | ```sh 110 | ~/.config/configstore/ethstats-cli.json 111 | ``` 112 | 113 | It persists the node name, the secret key received on successfully registration and the values of the following CLI options: 114 | - `--configurator-url` 115 | - `--server-url` 116 | - `--client-url` 117 | - `--client-ipc-path` 118 | - `--network` 119 | 120 | ## Node recovery 121 | 122 | IMPORTANT: This is available ONLY in interactive mode. 123 | 124 | If you lost your secret key or config file or accidentally deleted it and want to use the same node name previously registered, there is possible to recover it. 125 | To do that start `ethstats-cli` and on startup by not having a config file it will try to register by asking you: 126 | ``` 127 | ? Is your node already registered ? 128 | New node 129 | > Existing node 130 | ``` 131 | Using arrow keys select "Existing node", then you need to enter your email account which was used to register your node. 132 | 133 | ``` 134 | ? Please enter account email: 135 | ``` 136 | After typing that in, next an email will be sent to that account with a list of all nodes registered with that email account. Every node name in the list will have attached a recovery hash. 137 | Select the recovery hash of the node you want to recover and type it in at the following step. 138 | 139 | ``` 140 | ? Please enter node recovery hash: 141 | ``` 142 | This should end with a successful registration of an existing node name. 143 | Keep in mind that the list of recovery hashes sent in the email expires in 30 minutes. 144 | 145 | # CLI Options: 146 | 147 | ```sh 148 | --help, -h Show help 149 | --version, -V Show version 150 | --debug, -d Output values sent to server 151 | --verbose, -v Output more detailed information 152 | 153 | --server-url Server URL (Must include protocol and port if any) 154 | --net, -n Specify Ethereum network your node is running on (Default: mainnet) 155 | Available networks: mainnet|rinkeby|goerli 156 | If --server-url is specified, this option is ignored 157 | 158 | --client-url Client URL (Must include protocol and port if any; Default: http://localhost:8545) 159 | Based on the protocol specified in the url (http | ws) the app sets the corresponding Web3 provider 160 | If --client-ipc-path is specified, this option is ignored 161 | --client-ipc-path Client IPC path 162 | 163 | --configurator-url Configurator URL (Must include protocol and port if any). Custom configuration service to provide application specific configs. 164 | 165 | --register, -r Register node in non-interactive mode 166 | --account-email Account identification, also used in case of node/secret-key recovery 167 | It is possible to have multiple nodes under the same account-email 168 | --node-name Name of the node. If node is already registered, a unique 5 char hash will be appended 169 | ``` 170 | 171 | # Daemon 172 | 173 | To keep the app running at all times, you can run it as a daemon using the following command: 174 | 175 | ```sh 176 | $ ethstats-daemon 177 | ``` 178 | 179 | ## Daemon options: 180 | 181 | ```sh 182 | start Start daemon 183 | stop Stop daemon 184 | restart Restart daemon. If it is already started, the process will be stopped first. 185 | status Show infos about the daemon. 186 | kill Ethstats daemon uses PM2 as a process manager. This command will kill PM2 god daemon. 187 | ``` 188 | 189 | If any CLI options are specified after the Daemon option, they will be forwarded to the forked process. 190 | The Daemon mode is implemented programmatically through the PM2 API. The API does not support the "startup" feature. To handle start on boot, check out the [PM2](#with-pm2) instructions. 191 | 192 | ## PM2 193 | 194 | For more control you can use directly [PM2](http://pm2.keymetrics.io). Here is a JSON format process file that we recommend: 195 | 196 | ```json 197 | { 198 | "apps": [{ 199 | "name": "ethstats-cli", 200 | "script": "ethstats-cli", 201 | "pid": "~/.ethstats-cli/ethstats-cli.pid", 202 | "error": "~/.ethstats-cli/ethstats-cli.log", 203 | "output": "~/.ethstats-cli/ethstats-cli.log", 204 | "args": "--verbose", 205 | "restartDelay": 1000 206 | }] 207 | } 208 | ``` 209 | 210 | To handle daemon start at boot time, please visit [PM2-Startup](http://pm2.keymetrics.io/docs/usage/startup/). 211 | 212 | # Docker 213 | 214 | ## Installing and running 215 | The following commands assume that the Ethereum node is either running locally or in docker with `--net host`. 216 | For other options you should check out [CLI options](#cli-options). 217 | 218 | Make a directory where your configuration files will be persisted. 219 | ```sh 220 | mkdir /opt/ethstats-cli 221 | ``` 222 | 223 | Then run the following command to run from `alethio/ethstats-cli` docker image: 224 | ```sh 225 | docker run -d \ 226 | --restart always \ 227 | --net host \ 228 | -v /opt/ethstats-cli/:/root/.config/configstore/ \ 229 | alethio/ethstats-cli --register --account-email your@email.com --node-name your_node_name 230 | ``` 231 | 232 | or from `node:latest` docker image: 233 | 234 | ```sh 235 | docker \ 236 | run -d \ 237 | --name ethstats-cli \ 238 | --restart always \ 239 | --net host \ 240 | -v /opt/ethstats-cli/:/root/.config/configstore/ \ 241 | node:latest \ 242 | /bin/sh -c "yarn global add ethstats-cli && ethstats-cli --register --account-email your@email.com --node-name your_node_name" 243 | ``` 244 | 245 | IMPORTANT: If you are running `ethstats-cli` through docker on a Mac OS X and the node is running on the same host, but not through docker make sure you specify the correct client url by adding `--client-url http://docker.for.mac.localhost:8545` 246 | 247 | ## Updating 248 | 249 | If you started from `alethio/ehtstats-cli` docker image: 250 | 251 | ```sh 252 | docker pull alethio/ethstats-cli 253 | docker stop ethstats-cli && docker rm ethstats-cli 254 | ``` 255 | 256 | then run it again. 257 | 258 | If you started from `node:latest` docker image, just stop and remove the `ethstats-cli` container: 259 | 260 | ```sh 261 | docker stop ethstats-cli && docker rm ethstats-cli 262 | ``` 263 | 264 | then run it again. 265 | 266 | MIT © [Alethio](https://aleth.io) 267 | 268 | [npm-image]: https://badge.fury.io/js/ethstats-cli.svg 269 | [npm-url]: https://npmjs.org/package/ethstats-cli 270 | [travis-image]: https://travis-ci.org/EthStats/ethstats-cli.svg?branch=master 271 | [travis-url]: https://travis-ci.org/EthStats/ethstats-cli 272 | [daviddm-image]: https://david-dm.org/EthStats/ethstats-cli.svg?theme=shields.io 273 | [daviddm-url]: https://david-dm.org/EthStats/ethstats-cli 274 | [coveralls-image]: https://coveralls.io/repos/EthStats/ethstats-cli/badge.svg 275 | [coveralls-url]: https://coveralls.io/r/EthStats/ethstats-cli 276 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | This document will help you check for common issues and make sure your issue has not already been reported. 3 | 4 | ## NPM global package permissions problem 5 | We recommend installing NPM global packages **without** sudo. If you encountered issues when tried to install `ethstats-cli` as a global package with or without sudo regarding permissions, we recommend using this script [npm-global-no-sudo](https://github.com/baxy/npm-global-no-sudo) to fix the issue. 6 | 7 | ## Binaries not found 8 | If you installed `ethstats-cli` as a global package with Yarn and the binaries are not found, we recommend running the following command: 9 | ``` 10 | export PATH="$PATH:`yarn global bin`" && echo 'export PATH="$PATH:`yarn global bin`"' >> ~/.profile 11 | ``` 12 | -------------------------------------------------------------------------------- /bin/ethstats-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@babel/polyfill'); 4 | require('../dist/app-cli.js'); 5 | -------------------------------------------------------------------------------- /bin/ethstats-daemon.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/app-daemon.js'); 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const gulp = require('gulp'); 3 | const plumber = require('gulp-plumber'); 4 | const babel = require('gulp-babel'); 5 | const eslint = require('gulp-eslint'); 6 | const excludeGitignore = require('gulp-exclude-gitignore'); 7 | 8 | // Initialize the babel transpiler so ES2015 files gets compiled when they're loaded 9 | require('@babel/register'); 10 | 11 | gulp.task('lint', function () { 12 | return gulp.src('**/*.js') 13 | .pipe(plumber()) 14 | .pipe(excludeGitignore()) 15 | .pipe(eslint()) 16 | .pipe(eslint.format()) 17 | .pipe(eslint.failAfterError()); 18 | }); 19 | 20 | gulp.task('watch', function () { 21 | gulp.watch(['lib/**/*.js', 'test/**'], ['test']); 22 | }); 23 | 24 | gulp.task('clean', function () { 25 | return del('dist'); 26 | }); 27 | 28 | gulp.task('babel', () => { 29 | return gulp.src('lib/**/*.js') 30 | .pipe(plumber()) 31 | .pipe(babel()) 32 | .pipe(gulp.dest('dist')); 33 | }); 34 | 35 | gulp.task('prepare', gulp.series('clean', 'babel')); 36 | 37 | gulp.task('default', gulp.series('lint')); 38 | -------------------------------------------------------------------------------- /lib/Cli.js: -------------------------------------------------------------------------------- 1 | export default class CLI { 2 | constructor(diContainer) { 3 | this.pkg = diContainer.pkg; 4 | this.chalk = diContainer.chalk; 5 | this.meow = diContainer.meow; 6 | this.boxen = diContainer.boxen; 7 | this.log = diContainer.logger; 8 | 9 | let boxOptions = { 10 | padding: 1, 11 | margin: 1, 12 | align: 'center', 13 | borderColor: 'yellow', 14 | borderStyle: 'round' 15 | }; 16 | let description = this.boxen(this.chalk.green.bold(this.pkg.description) + ' \n' + this.chalk.cyan('v' + this.pkg.version), boxOptions); 17 | 18 | let cli = this.meow(` 19 | Usage 20 | $ ethstats-cli [options] 21 | 22 | Options 23 | --help, -h Show help 24 | --version, -V Show version 25 | --debug, -d Output values sent to server 26 | --verbose, -v Output more detailed information 27 | 28 | --server-url Server URL (Must include protocol and port if any) 29 | --net, -n Specify Ethereum network your node is running on (Default: mainnet) 30 | Available networks: mainnet|rinkeby|goerli 31 | If --server-url is specified, this option is ignored 32 | 33 | --client-url Client URL (Must include protocol and port if any; Default: http://localhost:8545) 34 | Based on the protocol specified in the url (http | ws) the app sets the corresponding Web3 provider 35 | If --client-ipc-path is specified, this option is ignored 36 | --client-ipc-path Client IPC path 37 | 38 | --configurator-url Configurator URL (Must include protocol and port if any). Custom configuration service to provide application specific configs. 39 | 40 | --register, -r Register node in non-interactive mode 41 | --account-email Account identification, also used in case of node/secret-key recovery 42 | It is possible to have multiple nodes under the same account-email 43 | --node-name Name of the node. If node is already registered, a unique 5 char hash will be appended 44 | 45 | `, { 46 | description: description, 47 | flags: { 48 | help: { 49 | type: 'boolean', 50 | alias: 'h' 51 | }, 52 | version: { 53 | type: 'boolean', 54 | alias: 'V' 55 | }, 56 | debug: { 57 | type: 'boolean', 58 | alias: 'd' 59 | }, 60 | verbose: { 61 | type: 'boolean', 62 | alias: 'v' 63 | }, 64 | serverUrl: { 65 | type: 'string' 66 | }, 67 | net: { 68 | type: 'string', 69 | alias: 'n' 70 | }, 71 | clientUrl: { 72 | type: 'string' 73 | }, 74 | clientIpcPath: { 75 | type: 'string' 76 | }, 77 | configuratorUrl: { 78 | type: 'string' 79 | }, 80 | register: { 81 | type: 'boolean', 82 | alias: 'r' 83 | }, 84 | accountEmail: { 85 | type: 'string' 86 | }, 87 | nodeName: { 88 | type: 'string' 89 | } 90 | } 91 | }); 92 | 93 | diContainer.config.logger.showInfos = cli.flags.verbose; 94 | diContainer.config.logger.showDebugs = cli.flags.debug; 95 | 96 | if (diContainer.config.logger.showDebugs && !diContainer.config.logger.showInfos) { 97 | diContainer.config.logger.showInfos = true; 98 | } 99 | 100 | diContainer.logger.showInfos = diContainer.config.logger.showInfos; 101 | diContainer.logger.showDebugs = diContainer.config.logger.showDebugs; 102 | diContainer.logger.showDateTime = diContainer.config.logger.showInfos; 103 | 104 | return this.validateFlags(cli); 105 | } 106 | 107 | validateFlags(cli) { 108 | if (cli.flags.configuratorUrl === true || cli.flags.configuratorUrl === '') { 109 | this.log.error('Configurator URL is empty', false, true); 110 | } 111 | 112 | if (cli.flags.clientUrl === true || cli.flags.clientUrl === '') { 113 | this.log.error('Client URL is empty', false, true); 114 | } 115 | 116 | if (cli.flags.clientIpcPath === true || cli.flags.clientIpcPath === '') { 117 | this.log.error('Client IPC Path is empty', false, true); 118 | } 119 | 120 | if (cli.flags.serverUrl === true || cli.flags.serverUrl === '') { 121 | this.log.error('Server URL is empty', false, true); 122 | } 123 | 124 | if (cli.flags.net === true || cli.flags.net === '') { 125 | this.log.error('Network is empty', false, true); 126 | } 127 | 128 | if (cli.flags.register) { 129 | if (!cli.flags.accountEmail || cli.flags.accountEmail === true || cli.flags.accountEmail === '') { 130 | this.log.error('Account email is missing or empty', false, true); 131 | } 132 | 133 | if (!cli.flags.nodeName || cli.flags.nodeName === true || cli.flags.nodeName === '') { 134 | this.log.error('Node name is missing or empty', false, true); 135 | } 136 | } else if (cli.flags.accountEmail !== undefined || cli.flags.nodeName !== undefined) { 137 | this.log.error('Register flag is missing', false, true); 138 | } 139 | 140 | return cli; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/Config.js: -------------------------------------------------------------------------------- 1 | import Configstore from 'configstore'; 2 | 3 | const config = { 4 | configStore: new Configstore('ethstats-cli'), 5 | configurator: { 6 | url: 'https://config.ethstats.io:443' 7 | }, 8 | server: { 9 | net: 'mainnet' 10 | }, 11 | client: { 12 | url: 'http://localhost:8545' 13 | }, 14 | logger: { 15 | showErrors: true, 16 | showInfos: false, 17 | showDebugs: false 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /lib/Configurator.js: -------------------------------------------------------------------------------- 1 | import requestPromise from 'request-promise'; 2 | 3 | export default class Configurator { 4 | constructor(diContainer) { 5 | this.config = diContainer.config; 6 | this.log = diContainer.logger; 7 | this.lodash = diContainer.lodash; 8 | this.cli = diContainer.cli; 9 | this.requestPromise = requestPromise; 10 | 11 | this.url = this.config.configurator.url; 12 | 13 | let configStoreConfigurator = diContainer.config.configStore.get('configurator'); 14 | if (configStoreConfigurator) { 15 | this.url = configStoreConfigurator.url; 16 | } 17 | 18 | if (this.cli.flags.configuratorUrl) { 19 | this.url = this.cli.flags.configuratorUrl; 20 | this.config.configStore.set('configurator', {url: this.url}); 21 | } 22 | } 23 | 24 | get(params) { 25 | let requestOptions = { 26 | method: 'GET', 27 | uri: `${this.url}/configs/${params.configName}`, 28 | json: true 29 | }; 30 | 31 | if (!this.lodash.isEmpty(params.configParams)) { 32 | let configParamsValue = []; 33 | Object.keys(params.configParams).forEach(param => { 34 | configParamsValue.push(`configParams[${param}]=${params.configParams[param]}`); 35 | }); 36 | 37 | requestOptions.uri += `?${configParamsValue.join('&')}`; 38 | } 39 | 40 | this.log.debug(`Request config from server: ${JSON.stringify(requestOptions)}`); 41 | 42 | return this.requestPromise(requestOptions).then(requestResult => { 43 | let result = null; 44 | if (requestResult.body.success) { 45 | result = requestResult.body.data[0]; 46 | } else { 47 | this.log.error(`Configurator => ${requestResult.body.errors[0]}`, false, true); 48 | } 49 | 50 | return result; 51 | }).catch(error => { 52 | let errorMessage = this.lodash.isObject(error.error) ? ((error.error.body === undefined) ? error.error : error.error.body.errors[0]) : error.message; 53 | let exit = params.configName === 'serverUrl'; 54 | 55 | this.log.error(`Configurator => ${errorMessage}`, false, exit); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default class Logger { 4 | constructor(options) { 5 | options = options || {}; 6 | this.showDateTime = (options.showDateTime === undefined) ? true : options.showDateTime; 7 | this.showInfos = (options.showInfos === undefined) ? true : options.showInfos; 8 | this.showWarnings = (options.showWarnings === undefined) ? true : options.showWarnings; 9 | this.showErrors = (options.showErrors === undefined) ? true : options.showErrors; 10 | this.showDebugs = (options.showDebugs === undefined) ? true : options.showDebugs; 11 | 12 | this.chalk = chalk; 13 | } 14 | 15 | _log(type, string, beginWithNewLine = false, processExit = false) { 16 | let dateTime = new Date().toISOString().replace('T', ' ').replace('Z', ''); 17 | let newLine = beginWithNewLine ? '\n' : ''; 18 | let resultString = `${newLine}${(this.showDateTime) ? dateTime + ' - ' : ''}%LOG-TYPE%: ${string}`; 19 | 20 | switch (type) { 21 | case 'echo': 22 | console.log(string); 23 | break; 24 | case 'info': 25 | console.log(this.chalk.white(resultString.replace('%LOG-TYPE%', 'INFO'))); 26 | break; 27 | case 'debug': 28 | console.log(this.chalk.cyan(resultString.replace('%LOG-TYPE%', 'DEBUG'))); 29 | break; 30 | case 'warning': 31 | console.log(this.chalk.yellow(resultString.replace('%LOG-TYPE%', 'WARNING'))); 32 | break; 33 | case 'error': 34 | console.log(this.chalk.red(resultString.replace('%LOG-TYPE%', 'ERROR'))); 35 | break; 36 | default: 37 | console.log('Unknown log type'); 38 | break; 39 | } 40 | 41 | if (processExit) { 42 | process.exit((type === 'error' ? 1 : 0)); 43 | } 44 | } 45 | 46 | echo(string, beginWithNewLine = false, processExit = false) { 47 | if (this.showInfos) { 48 | this._log('info', string, beginWithNewLine, processExit); 49 | } else { 50 | this._log('echo', string); 51 | } 52 | } 53 | 54 | info(string, beginWithNewLine = false, processExit = false) { 55 | if (this.showInfos) { 56 | this._log('info', string, beginWithNewLine, processExit); 57 | } 58 | } 59 | 60 | debug(string, beginWithNewLine = false, processExit = false) { 61 | if (this.showDebugs) { 62 | this._log('debug', string, beginWithNewLine, processExit); 63 | } 64 | } 65 | 66 | warning(string, beginWithNewLine = false, processExit = false) { 67 | if (this.showWarnings) { 68 | this._log('warning', string, beginWithNewLine, processExit); 69 | } 70 | } 71 | 72 | error(string, beginWithNewLine = false, processExit = false) { 73 | if (this.showErrors) { 74 | this._log('error', string, beginWithNewLine, processExit); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Register.js: -------------------------------------------------------------------------------- 1 | const REGEX_EMAIL_VALIDATOR = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 2 | 3 | export default class Register { 4 | constructor(diContainer) { 5 | this.config = diContainer.config; 6 | this.cli = diContainer.cli; 7 | this.inquirer = diContainer.inquirer; 8 | this.log = diContainer.logger; 9 | this.lodash = diContainer.lodash; 10 | this.server = diContainer.server; 11 | this.configurator = diContainer.configurator; 12 | this.chalk = diContainer.chalk; 13 | } 14 | 15 | askInstallationType(askNetwork = true) { 16 | this.configurator.get({ 17 | configName: 'privacyPolicyUrl' 18 | }).then(privacyPolicyUrl => { 19 | if (privacyPolicyUrl === null) { 20 | this.log.error('Could not get privacy policy url', false, true); 21 | } else { 22 | this.log.echo(this.chalk.bgBlue('Thank you for using \'ethstats-cli\'. Your privacy is important to us.')); 23 | this.log.echo(this.chalk.bgBlue(`For this we kindly ask you to read our privacy policy here: ${this.chalk.redBright(privacyPolicyUrl)}`)); 24 | this.log.echo(this.chalk.bgBlue('By continuing to the next step you acknowledge and agree to our terms and conditions.')); 25 | 26 | this.inquirer.prompt([ 27 | { 28 | type: 'list', 29 | name: 'installationType', 30 | message: 'Is your node already registered ?', 31 | choices: [ 32 | { 33 | name: 'New node', 34 | value: 'new-node' 35 | }, 36 | { 37 | name: 'Existing node', 38 | value: 'existing-node' 39 | } 40 | ] 41 | } 42 | ]).then(answer => { 43 | if (askNetwork) { 44 | this.askNetwork(answer.installationType); 45 | } else if (answer.installationType === 'new-node') { 46 | this.askRegistration(); 47 | } else { 48 | this.askRecoveryAccountEmail(); 49 | } 50 | }); 51 | } 52 | }); 53 | } 54 | 55 | askNetwork(installationType) { 56 | let choices = []; 57 | 58 | if (this.lodash.isEmpty(this.config.serverUrls)) { 59 | this.log.error('Networks not found', false, true); 60 | } else { 61 | Object.keys(this.config.serverUrls).forEach(item => { 62 | choices.push({ 63 | name: item.charAt(0).toUpperCase() + item.slice(1), 64 | value: item 65 | }); 66 | }); 67 | } 68 | 69 | this.inquirer.prompt([ 70 | { 71 | type: 'list', 72 | name: 'networkName', 73 | message: 'Please select network ?', 74 | choices: choices 75 | } 76 | ]).then(answer => { 77 | let serverConfig = this.config.serverUrls[answer.networkName]; 78 | if (serverConfig) { 79 | this.server.url = serverConfig.url; 80 | this.server.configToSave = { 81 | net: answer.networkName 82 | }; 83 | 84 | this.server.create(); 85 | this.server.socket.on('open', () => { 86 | if (this.config.configStore.get('firstRun') !== false) { 87 | if (installationType === 'new-node') { 88 | this.askRegistration(); 89 | } else { 90 | this.askRecoveryAccountEmail(); 91 | } 92 | } 93 | }); 94 | } else { 95 | this.log.error('Server config for selected network not found', false, true); 96 | } 97 | }); 98 | } 99 | 100 | askRegistration() { 101 | this.inquirer.prompt([ 102 | { 103 | type: 'input', 104 | name: 'accountEmail', 105 | message: 'Please enter account email:', 106 | validate: input => { 107 | let result = true; 108 | 109 | if (!REGEX_EMAIL_VALIDATOR.test(input)) { 110 | result = 'Please enter a valid email address'; 111 | } 112 | 113 | return result; 114 | } 115 | }, 116 | { 117 | type: 'input', 118 | name: 'nodeName', 119 | message: 'Please enter node name:', 120 | validate: input => { 121 | return this.server.sendAndWait('checkIfNodeExists', {nodeName: input}).then(response => { 122 | let result = true; 123 | if (response.success) { 124 | let resposeData = response.data[0]; 125 | 126 | if (resposeData.exists) { 127 | result = 'Node already registered'; 128 | } 129 | } else { 130 | result = response.errors[0]; 131 | } 132 | 133 | return result; 134 | }); 135 | } 136 | } 137 | ]).then(answer => { 138 | this.server.registerNode(answer.accountEmail, answer.nodeName); 139 | }); 140 | } 141 | 142 | askRecoveryAccountEmail() { 143 | this.inquirer.prompt([ 144 | { 145 | type: 'input', 146 | name: 'accountEmail', 147 | message: 'Please enter account email:', 148 | validate: input => { 149 | return this.server.sendAndWait('checkIfEmailExists', {accountEmail: input}).then(response => { 150 | let result = true; 151 | if (response.success) { 152 | let resposeData = response.data[0]; 153 | 154 | if (!resposeData.exists) { 155 | result = 'Email does not exist'; 156 | } 157 | } else { 158 | result = response.errors[0]; 159 | } 160 | 161 | return result; 162 | }); 163 | } 164 | } 165 | ]).then(answer => { 166 | this.server.sendAndWait('sendRecoveryEmail', {accountEmail: answer.accountEmail}).then(response => { 167 | if (response.success) { 168 | this.log.echo('A message was sent to the provided address with a URL that will contain your list of nodes.'); 169 | this.log.echo('To use an existing node, type the corresponding recovery hash of your desired node.'); 170 | 171 | let responseData = response.data[0]; 172 | this.askNodeRecoveryHash(responseData.recoveryRequestId); 173 | } else { 174 | this.log.error(response.errors[0], false, true); 175 | } 176 | }); 177 | }); 178 | } 179 | 180 | askNodeRecoveryHash(recoveryRequestId) { 181 | this.inquirer.prompt([ 182 | { 183 | type: 'input', 184 | name: 'nodeRecoveryHash', 185 | message: 'Please enter node recovery hash:', 186 | validate: input => { 187 | return this.server.sendAndWait('checkIfNodeRecoveryHashExists', { 188 | recoveryRequestId: recoveryRequestId, 189 | nodeRecoveryHash: input 190 | }).then(response => { 191 | let result = true; 192 | if (response.success) { 193 | let resposeData = response.data[0]; 194 | 195 | if (!resposeData.exists) { 196 | result = 'Node recovery hash is invalid/expired or does not exist'; 197 | } 198 | } else { 199 | result = response.errors[0]; 200 | } 201 | 202 | return result; 203 | }); 204 | } 205 | } 206 | ]).then(answer => { 207 | this.server.send('recoverNode', { 208 | recoveryRequestId: recoveryRequestId, 209 | nodeRecoveryHash: answer.nodeRecoveryHash 210 | }); 211 | }); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /lib/Server.js: -------------------------------------------------------------------------------- 1 | import Primus from 'primus'; 2 | import primusResponder from 'primus-responder'; 3 | import EventEmitter from 'events'; 4 | import client from './client/index.js'; 5 | 6 | const PrimusSocket = Primus.createSocket({ 7 | transformer: 'websockets', 8 | pathname: '/api', 9 | parser: 'JSON', 10 | plugin: { 11 | responder: primusResponder 12 | } 13 | }); 14 | 15 | export default class Server { 16 | constructor(diContainer) { 17 | this.pkg = diContainer.pkg; 18 | this.config = diContainer.config; 19 | this.configurator = diContainer.configurator; 20 | this.log = diContainer.logger; 21 | this.cli = diContainer.cli; 22 | this.lodash = diContainer.lodash; 23 | 24 | this.eventEmitter = new EventEmitter(); 25 | 26 | this.isTryingToLogin = false; 27 | this.isLoggedIn = false; 28 | this.socketIsOpen = false; 29 | 30 | this.url = null; 31 | this.socket = null; 32 | this.configToSave = null; 33 | 34 | diContainer.server = this; 35 | this.client = client(diContainer); 36 | 37 | this.lastCheckedBlockNumber = null; 38 | this.lastCheckedSyncBlockNumber = null; 39 | 40 | this.CHECK_LAST_BLOCK_INTERVAL = 300; // 5 min 41 | this.checkLastBlockInterval = setInterval(() => { 42 | let lastReceivedBlockNumber = (this.client.lastBlock) ? this.client.lastBlock.number : null; 43 | let lastReceivedSyncBlockNumber = (this.client.lastSyncStatus) ? this.client.lastSyncStatus.currentBlock : null; 44 | 45 | this.log.debug(`Check if receiving new blocks => last checked block: ${this.lastCheckedBlockNumber}, last received block: ${lastReceivedBlockNumber}`); 46 | this.log.debug(`Check if receiving new sync blocks => last checked sync block: ${this.lastCheckedSyncBlockNumber}, last received block: ${lastReceivedSyncBlockNumber}`); 47 | 48 | if (this.lastCheckedBlockNumber === lastReceivedBlockNumber && this.lastCheckedSyncBlockNumber === lastReceivedSyncBlockNumber) { 49 | this.log.info(`No new blocks received for more than ${this.CHECK_LAST_BLOCK_INTERVAL} seconds.`); 50 | this.eventEmitter.emit('destroy'); 51 | } else { 52 | this.lastCheckedBlockNumber = lastReceivedBlockNumber; 53 | this.lastCheckedSyncBlockNumber = lastReceivedSyncBlockNumber; 54 | } 55 | }, this.CHECK_LAST_BLOCK_INTERVAL * 1000); 56 | 57 | return this; 58 | } 59 | 60 | setHostAndPort() { 61 | if (this.url === null) { 62 | let configStoreServer = this.config.configStore.get('server'); 63 | 64 | if (configStoreServer) { 65 | if (this.config.serverUrls && configStoreServer.net) { 66 | this.url = this.config.serverUrls[configStoreServer.net].url; 67 | } 68 | 69 | if (configStoreServer.url) { 70 | this.url = configStoreServer.url; 71 | } 72 | } else if (this.config.serverUrls) { 73 | this.url = this.config.serverUrls[this.config.server.net].url; 74 | this.configToSave = { 75 | net: this.config.server.net 76 | }; 77 | } 78 | 79 | if (this.config.serverUrls && this.cli.flags.net) { 80 | if (!this.config.serverUrls[this.cli.flags.net]) { 81 | this.log.error('Network does not exist', false, true); 82 | } 83 | 84 | this.url = this.config.serverUrls[this.cli.flags.net].url; 85 | this.configToSave = { 86 | net: this.cli.flags.net 87 | }; 88 | } 89 | 90 | if (this.cli.flags.serverUrl) { 91 | this.url = this.cli.flags.serverUrl; 92 | this.configToSave = { 93 | url: this.url 94 | }; 95 | } 96 | } 97 | } 98 | 99 | create() { 100 | this.setHostAndPort(); 101 | 102 | this.socket = new PrimusSocket(`${this.url}`, { 103 | reconnect: { 104 | min: (1 + (Math.floor(Math.random() * 10))) * 1000, // Random between 1 and 10 seconds 105 | factor: 1, 106 | retries: 8640 107 | } 108 | }); 109 | 110 | this.socket.on('open', () => { 111 | this.socketIsOpen = true; 112 | this.log.echo(`Connection established with ethstats server "${this.url}"`); 113 | 114 | if (this.isLoggedIn) { 115 | this.isLoggedIn = false; 116 | } 117 | 118 | if (this.configToSave && !this.lodash.isEqual(this.configToSave, this.config.configStore.get('server'))) { 119 | this.config.configStore.set('server', this.configToSave); 120 | } 121 | 122 | if (this.config.configStore.get('firstRun') === false) { 123 | if (this.config.configStore.get('nodeName') !== undefined && this.config.configStore.get('secretKey') !== undefined) { 124 | if (this.cli.flags.register) { 125 | this.log.warning('Client already registered'); 126 | } 127 | 128 | this.client.connect(); 129 | } else { 130 | this.config.configStore.set('firstRun', true); 131 | this.log.error('Credentials not found. Config file was reset, please try again.', false, true); 132 | } 133 | } else { 134 | let intervalId = setInterval(() => { 135 | if (this.config.configStore.get('firstRun') === false && this.config.configStore.get('nodeName') !== undefined && this.config.configStore.get('secretKey') !== undefined) { 136 | this.client.connect(); 137 | clearInterval(intervalId); 138 | } 139 | }, 1000); 140 | } 141 | }); 142 | 143 | this.socket.on('error', error => { 144 | this.log.error(`Socket error: ${error.message}`); 145 | }); 146 | 147 | this.socket.on('close', () => { 148 | this.isLoggedIn = false; 149 | this.socketIsOpen = false; 150 | this.log.warning('Connection closed with ethstats server'); 151 | }); 152 | 153 | this.socket.on('end', () => { 154 | this.isLoggedIn = false; 155 | this.socketIsOpen = false; 156 | this.log.error('Connection ended with ethstats server', false, true); 157 | }); 158 | 159 | this.socket.on('reconnect failed', () => { 160 | this.log.error('Reconnect to ethstats server failed! Maximum attempts reached. Please try again later or contact ethstats support.', false, true); 161 | }); 162 | 163 | this.socket.on('data', message => { 164 | this.log.debug(`Data received for topic: "${message.topic}"`); 165 | 166 | switch (message.topic) { 167 | case 'invalidMessage': 168 | this.resolveResponse(message.topic, message.payload); 169 | break; 170 | case 'clientTimeout': 171 | this.resolveResponse(message.topic, message.payload); 172 | break; 173 | case 'requestRateLimitReached': 174 | this.resolveResponse(message.topic, message.payload); 175 | break; 176 | case 'registerNodeResponse': 177 | this.resolveRegisterNodeResponse(message.payload); 178 | break; 179 | case 'loginResponse': 180 | this.resolveLoginResponse(message.payload); 181 | break; 182 | case 'logoutResponse': 183 | this.resolveResponse(message.topic, message.payload); 184 | break; 185 | case 'connectionResponse': 186 | this.resolveResponse(message.topic, message.payload); 187 | break; 188 | case 'syncResponse': 189 | this.resolveResponse(message.topic, message.payload); 190 | break; 191 | case 'statsResponse': 192 | this.resolveResponse(message.topic, message.payload); 193 | break; 194 | case 'usageResponse': 195 | this.resolveResponse(message.topic, message.payload); 196 | break; 197 | case 'blockResponse': 198 | this.resolveResponse(message.topic, message.payload); 199 | break; 200 | case 'ping': 201 | this.send('pong', message.payload); 202 | break; 203 | case 'pongResponse': 204 | this.resolveResponse(message.topic, message.payload); 205 | break; 206 | case 'checkChain': 207 | this.client.getBlockHashes(message.payload.blockNumber); 208 | break; 209 | case 'checkChainResponse': 210 | this.client.resolveCheckChainResponse(message.payload); 211 | break; 212 | case 'getBlocks': 213 | this.client.getBlocks(message.payload); 214 | break; 215 | case 'getBlocksResponse': 216 | this.resolveResponse(message.topic, message.payload); 217 | break; 218 | case 'validatorsResponse': 219 | this.resolveResponse(message.topic, message.payload); 220 | break; 221 | case 'getConfigResponse': 222 | this.resolveGetConfigResponse(message.payload); 223 | break; 224 | default: 225 | this.log.info(`Undefined topic: ${message.topic}`); 226 | break; 227 | } 228 | }); 229 | } 230 | 231 | destroy() { 232 | if (this.socket) { 233 | this.socket.destroy(); 234 | } 235 | 236 | clearInterval(this.checkLastBlockInterval); 237 | } 238 | 239 | send(topic, payload) { 240 | let result = false; 241 | let allowedTopicsWhenNotLoggedIn = [ 242 | {topic: 'login'}, 243 | {topic: 'registerNode'}, 244 | {topic: 'recoverNode'} 245 | ]; 246 | let isAllowedTopicWhenNotLoggedIn = Boolean(this.lodash.find(allowedTopicsWhenNotLoggedIn, {topic: topic})); 247 | 248 | if (this.socket && this.socketIsOpen && (this.isLoggedIn || isAllowedTopicWhenNotLoggedIn)) { 249 | result = this.socket.write({ 250 | topic: topic, 251 | payload: payload 252 | }); 253 | 254 | this.log.info(`Sending message on topic: "${topic}"`); 255 | 256 | if (topic === 'block') { 257 | payload = { 258 | number: payload.number, 259 | hash: payload.hash, 260 | parentHash: payload.parentHash 261 | }; 262 | } 263 | 264 | if (topic === 'getBlocksData') { 265 | let tmpPayload = []; 266 | for (let i = 0; i < payload.length; i++) { 267 | tmpPayload.push({number: payload[i].number}); 268 | } 269 | 270 | payload = tmpPayload; 271 | } 272 | 273 | this.log.debug(`Sent message on "${topic}" with payload: ${JSON.stringify(payload)}`); 274 | } 275 | 276 | return result; 277 | } 278 | 279 | sendAndWait(topic, payload) { 280 | let allowedTopicsWhenNotLoggedIn = [ 281 | {topic: 'checkIfNodeExists'}, 282 | {topic: 'checkIfEmailExists'}, 283 | {topic: 'sendRecoveryEmail'}, 284 | {topic: 'checkIfNodeRecoveryHashExists'} 285 | ]; 286 | let isAllowedTopicWhenNotLoggedIn = Boolean(this.lodash.find(allowedTopicsWhenNotLoggedIn, {topic: topic})); 287 | 288 | let topicsWhereLogInfosShouldBeginWithNewLine = [ 289 | {topic: 'checkIfNodeExists'}, 290 | {topic: 'checkIfEmailExists'}, 291 | {topic: 'checkIfNodeRecoveryHashExists'} 292 | ]; 293 | let beginWithNewLine = Boolean(this.lodash.find(topicsWhereLogInfosShouldBeginWithNewLine, {topic: topic})); 294 | 295 | return new Promise((resolve, reject) => { 296 | try { 297 | if (this.socket && this.socketIsOpen && (this.isLoggedIn || isAllowedTopicWhenNotLoggedIn)) { 298 | this.socket.writeAndWait({topic: topic, payload: payload}, response => { 299 | resolve(response); 300 | }); 301 | 302 | this.log.info(`Sending message on topic: "${topic}"`, beginWithNewLine); 303 | this.log.debug(`Sent message on "${topic}" with payload: ${JSON.stringify(payload)}`); 304 | } else { 305 | reject(new Error('Not connected to the server or not logged in')); 306 | } 307 | } catch (e) { 308 | reject(e); 309 | } 310 | }); 311 | } 312 | 313 | login(params) { 314 | let result = false; 315 | 316 | if (!this.isLoggedIn && !this.isTryingToLogin && this.socketIsOpen) { 317 | this.isTryingToLogin = true; 318 | this.log.echo(`Trying to login as "${this.config.configStore.get('nodeName')}"...`); 319 | result = this.send('login', params); 320 | } 321 | 322 | return result; 323 | } 324 | 325 | logout() { 326 | let result = false; 327 | 328 | if (this.isLoggedIn) { 329 | this.send('connection', {isConnected: false}); 330 | result = this.send('logout', {}); 331 | this.isLoggedIn = false; 332 | } 333 | 334 | return result; 335 | } 336 | 337 | resolveLoginResponse(response) { 338 | this.isLoggedIn = response.success; 339 | 340 | if (this.isLoggedIn) { 341 | this.log.echo('Successfully logged in'); 342 | this.log.echo(`${this.pkg.description} v${this.pkg.version} started and running...`); 343 | this.send('connection', {isConnected: true}); 344 | this.send('getConfig', {configName: 'NETWORK_ALGO'}); 345 | 346 | let configStoreServer = this.config.configStore.get('server'); 347 | if (configStoreServer && configStoreServer.net !== undefined) { 348 | this.configurator.get({ 349 | configName: 'dashboardUrl', 350 | configParams: { 351 | networkName: configStoreServer.net 352 | } 353 | }).then(value => { 354 | if (value) { 355 | this.log.echo(`Your node is now connected. You can now see your nodes stats/logs at: ${value.url}`); 356 | } 357 | }); 358 | } 359 | } else { 360 | let errorMessage = `Authentication error: ${JSON.stringify(response.errors)}.`; 361 | let possibleFlagErrorType = ''; 362 | 363 | if (this.cli.flags.net) { 364 | possibleFlagErrorType = 'network'; 365 | } 366 | 367 | if (this.cli.flags.serverUrl) { 368 | possibleFlagErrorType = 'server'; 369 | } 370 | 371 | if (possibleFlagErrorType !== '') { 372 | errorMessage += ` You are trying to switch the ${possibleFlagErrorType}! Make sure the node is registered for the that ${possibleFlagErrorType}!`; 373 | } 374 | 375 | this.log.error(errorMessage, false, true); 376 | } 377 | 378 | this.isTryingToLogin = false; 379 | } 380 | 381 | registerNode(accountEmail, nodeName) { 382 | return this.send('registerNode', { 383 | accountEmail: accountEmail, 384 | nodeName: nodeName 385 | }); 386 | } 387 | 388 | resolveRegisterNodeResponse(response) { 389 | let responseData = response.data[0]; 390 | let getUniqueHash = () => { 391 | let result = ''; 392 | let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 393 | 394 | for (let i = 0; i < 5; i++) { 395 | result += possible.charAt(Math.floor(Math.random() * possible.length)); 396 | } 397 | 398 | return result; 399 | }; 400 | 401 | if (response.success === false) { 402 | let nodeAlreadyRegistered = false; 403 | 404 | response.errors.forEach(error => { 405 | if (error === 'Node already registered') { 406 | nodeAlreadyRegistered = true; 407 | } 408 | 409 | if (!nodeAlreadyRegistered || (nodeAlreadyRegistered && !this.cli.flags.register)) { 410 | this.log.error(error, false, true); 411 | } else { 412 | this.log.warning(error); 413 | } 414 | }); 415 | 416 | if (this.cli.flags.register && nodeAlreadyRegistered) { 417 | let newNodeName = `${responseData.nodeName}-${getUniqueHash()}`; 418 | this.log.echo(`Trying to register with suffix: ${newNodeName}`); 419 | this.registerNode(responseData.accountEmail, newNodeName); 420 | } 421 | } else { 422 | this.log.echo(`Registered successfully node name: ${responseData.nodeName}`); 423 | this.config.configStore.set('nodeName', responseData.nodeName); 424 | this.config.configStore.set('secretKey', responseData.secretKey); 425 | this.config.configStore.set('firstRun', false); 426 | } 427 | } 428 | 429 | resolveResponse(topic, response) { 430 | if (response.errors && response.errors.length) { 431 | this.log.error(`Server response on topic: "${topic}" errors: ${JSON.stringify(response.errors)}`); 432 | } else if (response.warnings && response.warnings.length) { 433 | this.log.warning(`Server response on topic: "${topic}" warnings: ${JSON.stringify(response.warnings)}`); 434 | } 435 | } 436 | 437 | resolveGetConfigResponse(response) { 438 | this.resolveResponse('getConfig', response); 439 | if (response.success) { 440 | this.lodash.merge(this.config, response.data.shift()); 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /lib/app-cli.js: -------------------------------------------------------------------------------- 1 | import pkg from '../package.json'; 2 | 3 | import meow from 'meow'; 4 | import chalk from 'chalk'; 5 | import boxen from 'boxen'; 6 | import lodash from 'lodash'; 7 | import inquirer from 'inquirer'; 8 | import nodeCleanup from 'node-cleanup'; 9 | import updateNotifier from 'update-notifier'; 10 | import findProcess from 'find-process'; 11 | import systemInfo from 'systeminformation'; 12 | 13 | import config from './Config.js'; 14 | import Configurator from './Configurator.js'; 15 | import Logger from './Logger.js'; 16 | import CLI from './Cli.js'; 17 | import Register from './Register.js'; 18 | import Server from './Server.js'; 19 | 20 | updateNotifier({pkg}).notify(); 21 | 22 | const diContainer = { 23 | inquirer: inquirer, 24 | config: config, 25 | pkg: pkg, 26 | meow: meow, 27 | chalk: chalk, 28 | boxen: boxen, 29 | lodash: lodash, 30 | systemInfo: systemInfo 31 | }; 32 | 33 | const log = new Logger(config.logger); 34 | diContainer.logger = log; 35 | 36 | const cli = new CLI(diContainer); 37 | diContainer.cli = cli; 38 | 39 | const configurator = new Configurator(diContainer); 40 | diContainer.configurator = configurator; 41 | 42 | let server = new Server(diContainer); 43 | diContainer.server = server; 44 | 45 | const register = new Register(diContainer); 46 | diContainer.register = register; 47 | 48 | const initApp = () => { 49 | if (config.configStore.get('firstRun') !== false) { 50 | log.echo('First run detected. Please follow instructions to register your node.'); 51 | } 52 | 53 | let isServerFromConfigFile = !cli.flags.net && config.configStore.has('server') && config.configStore.get('server').url; 54 | 55 | if (isServerFromConfigFile || cli.flags.serverUrl) { 56 | server.create(); 57 | server.socket.on('open', () => { 58 | if (config.configStore.get('firstRun') !== false) { 59 | if (cli.flags.register) { 60 | server.registerNode(cli.flags.accountEmail, cli.flags.nodeName); 61 | } else { 62 | register.askInstallationType(false); 63 | } 64 | } 65 | }); 66 | } else { 67 | log.info('Get server connections'); 68 | configurator.get({ 69 | configName: 'serverUrl' 70 | }).then(configValue => { 71 | if (configValue === null) { 72 | log.error('Could not get server connections', false, true); 73 | } else { 74 | diContainer.config.serverUrls = configValue; 75 | 76 | if (config.configStore.get('firstRun') === false) { 77 | server.create(); 78 | } else if (!cli.flags.net && !cli.flags.register) { 79 | register.askInstallationType(true); 80 | } else { 81 | server.create(); 82 | server.socket.on('open', () => { 83 | if (config.configStore.get('firstRun') !== false) { 84 | if (cli.flags.register) { 85 | server.registerNode(cli.flags.accountEmail, cli.flags.nodeName); 86 | } else { 87 | register.askInstallationType(false); 88 | } 89 | } 90 | }); 91 | } 92 | } 93 | }); 94 | } 95 | 96 | server.eventEmitter.on('destroy', () => { 97 | log.info('Reinitializing app...'); 98 | 99 | server.client.stop(true); 100 | server.destroy(); 101 | 102 | server = new Server(diContainer); 103 | diContainer.server = server; 104 | 105 | initApp(); 106 | }); 107 | }; 108 | 109 | findProcess('name', 'ethstats-cli').then(list => { 110 | log.debug(`Process list: ${JSON.stringify(list)}`); 111 | 112 | let processList = []; 113 | 114 | list.forEach(proc => { 115 | if (proc.name === 'ethstats-cli') { 116 | processList.push(proc); 117 | } 118 | }); 119 | 120 | if (processList.length > 1) { 121 | log.error('Ethstats-CLI is already running', false, true); 122 | } else { 123 | initApp(); 124 | } 125 | }); 126 | 127 | nodeCleanup((exitCode, signal) => { 128 | if (server && server.socket) { 129 | server.logout(); 130 | server.destroy(); 131 | } 132 | 133 | log.info(`Exited with code: ${exitCode}, signal: ${signal}`); 134 | }); 135 | -------------------------------------------------------------------------------- /lib/app-daemon.js: -------------------------------------------------------------------------------- 1 | import config from './Config.js'; 2 | import pm2 from 'pm2'; 3 | import chalk from 'chalk'; 4 | import moment from 'moment'; 5 | 6 | if (config.configStore.get('firstRun') !== false) { 7 | console.log('Your node is not registered. Please run "ethstats-cli" first.'); 8 | process.exit(1); 9 | } 10 | 11 | const daemonOption = process.argv[2]; 12 | const daemonAvailableOptions = ['start', 'stop', 'restart', 'status', 'kill']; 13 | 14 | if (!daemonAvailableOptions.includes(daemonOption)) { 15 | console.log(` 16 | ${chalk.bold('Ethstats Daemon')} 17 | 18 | Usage 19 | $ ethstats-daemon [options] 20 | 21 | Options 22 | start Start daemon 23 | stop Stop daemon 24 | restart Restart daemon. If it is already started, the process will be stopped first. 25 | status Show infos about the daemon. 26 | kill Ethstats daemon uses PM2 as a process manager. This option will kill PM2 god daemon. 27 | 28 | If any CLI options are specified after the Daemon option, they will be forwarded to the forked process. 29 | `); 30 | process.exit(1); 31 | } 32 | 33 | if (!process.argv.includes('-v')) { 34 | process.argv.push('-v'); 35 | } 36 | 37 | const localDir = '~/.ethstats-cli'; 38 | const processOptions = { 39 | name: 'ethstats-cli', 40 | script: `${__dirname}/../bin/ethstats-cli.js`, 41 | pid: `${localDir}/ethstats-cli.pid`, 42 | error: `${localDir}/ethstats-cli.log`, 43 | output: `${localDir}/ethstats-cli.log`, 44 | args: process.argv, 45 | restartDelay: 1000 46 | }; 47 | 48 | pm2.connect(err => { 49 | if (err) { 50 | console.error(err); 51 | process.exit(1); 52 | } 53 | 54 | if (daemonOption === 'start') { 55 | pm2.start(processOptions, error => { 56 | console.log(`Ethstats daemon START ${(error) ? chalk.red(`[FAILED] ${error.message}`) : chalk.green('[OK]')}`); 57 | pm2.disconnect(); 58 | }); 59 | } 60 | 61 | if (daemonOption === 'stop') { 62 | pm2.stop(processOptions.name, error => { 63 | console.log(`Ethstats daemon STOP ${(error) ? chalk.red(`[FAILED] ${error.message}`) : chalk.green('[OK]')}`); 64 | pm2.disconnect(); 65 | }); 66 | } 67 | 68 | if (daemonOption === 'restart') { 69 | pm2.restart(processOptions.name, error => { 70 | console.log(`Ethstats daemon RESTART ${(error) ? chalk.red(`[FAILED] ${error.message}`) : chalk.green('[OK]')}`); 71 | pm2.disconnect(); 72 | }); 73 | } 74 | 75 | if (daemonOption === 'status') { 76 | pm2.describe(processOptions.name, (error, arr) => { 77 | arr.forEach(app => { 78 | let uptime = (app.pm2_env.status === 'stopped') ? 0 : moment.duration(Date.now() - app.pm2_env.created_at).humanize(); 79 | 80 | console.log(`Name: ${app.name}`); 81 | console.log(`PID: ${app.pid}`); 82 | console.log(`Status: ${app.pm2_env.status}`); 83 | console.log(`Uptime: ${uptime}`); 84 | console.log(`Autorestart: ${app.pm2_env.autorestart}`); 85 | console.log(`Restart times: ${app.pm2_env.restart_time}`); 86 | console.log(`Instances: ${app.pm2_env.instances}`); 87 | console.log(`CPU usage: ${app.monit.cpu}`); 88 | console.log(`MEM usage: ${app.monit.memory}`); 89 | }); 90 | pm2.disconnect(); 91 | }); 92 | } 93 | 94 | if (daemonOption === 'kill') { 95 | pm2.killDaemon(error => { 96 | console.log(`Ethstats daemon KILL ${(error) ? chalk.red(`[FAILED] ${error.message}`) : chalk.green('[OK]')}`); 97 | pm2.disconnect(); 98 | process.exit((error) ? 1 : 0); 99 | }); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /lib/client/Error.js: -------------------------------------------------------------------------------- 1 | export default class Error { 2 | constructor(protocol) { 3 | this.protocol = protocol; 4 | 5 | let _parityRecommendedFlags = { 6 | http: '`--jsonrpc-apis=web3,eth,net`', 7 | ws: '`--ws-apis=web3,eth,net,pubsub`', 8 | ipc: '`--ipc-apis=web3,eth,net,pubsub`' 9 | }; 10 | 11 | let _gethRecommendedFlags = { 12 | http: '`--rpc --rpcapi web3,eth,net`', 13 | ws: '`--ws --wsapi web3,eth,net,pubsub`', 14 | ipc: 'the `--ipcdisable` off' 15 | }; 16 | 17 | this.solutions = { 18 | 'Invalid JSON RPC response: (""|undefined)': { 19 | showOriginal: true, 20 | solution: '=> Possible fix: Make sure the Ethereum node is running and has the RPC apis enabled.' 21 | }, 22 | 'Method not found': { 23 | showOriginal: true, 24 | solution: `=> Possible fix: Parity must be run with ${_parityRecommendedFlags[protocol]} to enable the necessary apis.` 25 | }, 26 | 'The method (\\w+) does not exist/is not available': { 27 | showOriginal: true, 28 | solution: `=> Possible fix: Geth must be run with ${_gethRecommendedFlags[protocol]} to enable the necessary apis.` 29 | }, 30 | 'etherbase must be explicitly specified': { 31 | showOriginal: true, 32 | solution: '=> Possible fix: Geth must have at least one account created. Try running `geth account new`.' 33 | }, 34 | 'not supported': { 35 | showOriginal: true, 36 | solution: '=> Possible fix: Make sure the RPC apis are enabled. If you are running Geth in `light` mode, some methods are not supported.' 37 | }, 38 | 'Returned error: This request is not implemented yet. Please create an issue on Github repo.': { 39 | showOriginal: false, 40 | solution: '=> Parity does not support "sync" subscription if the WebSocket provider is used' 41 | } 42 | }; 43 | } 44 | 45 | getSolution(errorMessage) { 46 | let solution = ''; 47 | let solutionsRegexArray = Object.keys(this.solutions); 48 | 49 | for (let i = 0; i < solutionsRegexArray.length; i++) { 50 | if (errorMessage.match(RegExp(solutionsRegexArray[i], 'i'))) { 51 | if (this.solutions[solutionsRegexArray[i]].showOriginal) { 52 | solution = `${errorMessage} ${this.solutions[solutionsRegexArray[i]].solution}`; 53 | } else { 54 | solution = this.solutions[solutionsRegexArray[i]].solution; 55 | } 56 | 57 | break; 58 | } 59 | } 60 | 61 | if (!solution) { 62 | solution = errorMessage; 63 | } 64 | 65 | return solution; 66 | } 67 | 68 | resolve(error) { 69 | let solution = this.getSolution(error.message); 70 | return solution; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/client/HwInfo.js: -------------------------------------------------------------------------------- 1 | export default class HwInfo { 2 | constructor(diContainer) { 3 | this.log = diContainer.logger; 4 | this.systemInfo = diContainer.systemInfo; 5 | this.errorHandler = diContainer.clientErrorHandler; 6 | } 7 | 8 | getCpuInfo() { 9 | return this.systemInfo.cpu().then(data => { 10 | if (data) { 11 | return JSON.stringify({ 12 | manufacturer: data.manufacturer, 13 | brand: data.brand, 14 | speed: data.speed 15 | }); 16 | } 17 | 18 | return null; 19 | }).catch(error => { 20 | this.log.error(this.errorHandler.resolve(error)); 21 | return null; 22 | }); 23 | } 24 | 25 | getMemoryInfo() { 26 | return this.systemInfo.memLayout().then(data => { 27 | if (data && data.length > 0) { 28 | return JSON.stringify(data.map(mem => { 29 | return { 30 | size: mem.size, 31 | type: mem.type, 32 | clockSpeed: mem.clockSpeed, 33 | manufacturer: mem.manufacturer 34 | }; 35 | })); 36 | } 37 | 38 | return null; 39 | }).catch(error => { 40 | this.log.error(this.errorHandler.resolve(error)); 41 | return null; 42 | }); 43 | } 44 | 45 | getDiskInfo() { 46 | return this.systemInfo.diskLayout().then(data => { 47 | if (data && data.length > 0) { 48 | return JSON.stringify(data.map(disk => { 49 | return { 50 | size: disk.size, 51 | type: disk.type, 52 | name: disk.name, 53 | vendor: disk.vendor 54 | }; 55 | })); 56 | } 57 | 58 | return null; 59 | }).catch(error => { 60 | this.log.error(this.errorHandler.resolve(error)); 61 | return null; 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/client/Usage.js: -------------------------------------------------------------------------------- 1 | export default class Usage { 2 | constructor(diContainer) { 3 | this.log = diContainer.logger; 4 | this.systemInfo = diContainer.systemInfo; 5 | this.errorHandler = diContainer.clientErrorHandler; 6 | this.server = diContainer.server; 7 | 8 | this.nodeProcessName = null; 9 | } 10 | 11 | getCpuLoad() { 12 | let result = 0; 13 | 14 | return this.systemInfo.currentLoad().then(data => { 15 | if (data) { 16 | result = data.currentload; 17 | } 18 | 19 | return result; 20 | }).catch(error => { 21 | this.log.error(this.errorHandler.resolve(error)); 22 | return result; 23 | }); 24 | } 25 | 26 | getMemLoad() { 27 | let result = { 28 | memTotal: 0, 29 | memUsed: 0 30 | }; 31 | 32 | return this.systemInfo.mem().then(data => { 33 | if (data) { 34 | result.memTotal = data.total; 35 | result.memUsed = data.used; 36 | } 37 | 38 | return result; 39 | }).catch(error => { 40 | this.log.error(this.errorHandler.resolve(error)); 41 | return result; 42 | }); 43 | } 44 | 45 | async getNetworkStats() { 46 | let result = { 47 | rxSec: 0, 48 | txSec: 0 49 | }; 50 | let iface = await this.systemInfo.networkInterfaceDefault(); 51 | 52 | return this.systemInfo.networkStats(iface).then(data => { 53 | if (data) { 54 | result.rxSec = data[0].rx_sec; 55 | result.txSec = data[0].tx_sec; 56 | } 57 | 58 | return result; 59 | }).catch(error => { 60 | this.log.error(this.errorHandler.resolve(error)); 61 | return result; 62 | }); 63 | } 64 | 65 | getFileSystemStats() { 66 | let result = { 67 | rxSec: 0, 68 | wxSec: 0 69 | }; 70 | 71 | return this.systemInfo.fsStats().then(data => { 72 | if (data) { 73 | result.rxSec = data.rx_sec; 74 | result.wxSec = data.wx_sec; 75 | } 76 | 77 | return result; 78 | }).catch(error => { 79 | this.log.error(this.errorHandler.resolve(error)); 80 | return result; 81 | }); 82 | } 83 | 84 | getDiskIO() { 85 | let result = { 86 | rIOSec: 0, 87 | wIOSec: 0 88 | }; 89 | 90 | return this.systemInfo.disksIO().then(data => { 91 | if (data) { 92 | result.rIOSec = data.rIO_sec; 93 | result.wIOSec = data.wIO_sec; 94 | } 95 | 96 | return result; 97 | }).catch(error => { 98 | this.log.error(this.errorHandler.resolve(error)); 99 | return result; 100 | }); 101 | } 102 | 103 | getProcessLoad(processName) { 104 | let result = { 105 | cpu: 0, 106 | mem: 0 107 | }; 108 | 109 | return this.systemInfo.processLoad(processName).then(data => { 110 | if (data) { 111 | result.cpu = data.cpu; 112 | result.mem = data.mem; 113 | } 114 | 115 | return result; 116 | }).catch(error => { 117 | this.log.error(this.errorHandler.resolve(error)); 118 | return result; 119 | }); 120 | } 121 | 122 | setNodeProcessName(node) { 123 | if (node) { 124 | this.nodeProcessName = node.split('/')[0].toLowerCase(); 125 | } 126 | } 127 | 128 | async getStats() { 129 | this.log.debug('Get usage'); 130 | 131 | let result = { 132 | hostCpuLoad: await this.getCpuLoad(), 133 | hostMemTotal: 0, 134 | hostMemUsed: 0, 135 | hostNetRxSec: 0, 136 | hostNetTxSec: 0, 137 | hostFsRxSec: 0, 138 | hostFsWxSec: 0, 139 | hostDiskRIOSec: 0, 140 | hostDiskWIOSec: 0, 141 | nodeCpuLoad: 0, 142 | nodeMemLoad: 0, 143 | clientCpuLoad: 0, 144 | clientMemLoad: 0 145 | }; 146 | 147 | let memLoad = await this.getMemLoad(); 148 | result.hostMemTotal = memLoad.memTotal; 149 | result.hostMemUsed = memLoad.memUsed; 150 | 151 | let networkStats = await this.getNetworkStats(); 152 | result.hostNetRxSec = networkStats.rxSec < 0 ? 0 : networkStats.rxSec; 153 | result.hostNetTxSec = networkStats.txSec < 0 ? 0 : networkStats.txSec; 154 | 155 | let fsStats = await this.getFileSystemStats(); 156 | result.hostFsRxSec = fsStats.rxSec < 0 ? 0 : fsStats.rxSec; 157 | result.hostFsWxSec = fsStats.wxSec < 0 ? 0 : fsStats.wxSec; 158 | 159 | let diskIO = await this.getDiskIO(); 160 | result.hostDiskRIOSec = diskIO.rIOSec < 0 ? 0 : diskIO.rIOSec; 161 | result.hostDiskWIOSec = diskIO.wIOSec < 0 ? 0 : diskIO.wIOSec; 162 | 163 | if (this.nodeProcessName) { 164 | let nodeLoad = await this.getProcessLoad(this.nodeProcessName); 165 | result.nodeCpuLoad = nodeLoad.cpu; 166 | result.nodeMemLoad = nodeLoad.mem; 167 | } 168 | 169 | let clientLoad = await this.getProcessLoad('ethstats-cli'); 170 | result.clientCpuLoad = clientLoad.cpu; 171 | result.clientMemLoad = clientLoad.mem; 172 | 173 | this.server.send('usage', result); 174 | 175 | return result; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/client/index.js: -------------------------------------------------------------------------------- 1 | import Http from './protocol/Http.js'; 2 | import WebSocket from './protocol/WebSocket.js'; 3 | import Error from './Error.js'; 4 | 5 | export default diContainer => { 6 | let client = null; 7 | let getProtocol = url => { 8 | return url.split('://')[0]; 9 | }; 10 | 11 | let url = diContainer.config.client.url; 12 | let protocol = getProtocol(url); 13 | 14 | let configStoreClient = diContainer.config.configStore.get('client'); 15 | if (configStoreClient) { 16 | if (configStoreClient.url) { 17 | url = configStoreClient.url; 18 | protocol = getProtocol(url); 19 | } 20 | 21 | if (configStoreClient.ipcPath) { 22 | url = configStoreClient.ipcPath; 23 | protocol = 'ipc'; 24 | } 25 | } 26 | 27 | if (diContainer.cli.flags.clientUrl) { 28 | url = diContainer.cli.flags.clientUrl; 29 | protocol = getProtocol(url); 30 | diContainer.config.configStore.set('client', {url: url}); 31 | } 32 | 33 | if (diContainer.cli.flags.clientIpcPath) { 34 | url = diContainer.cli.flags.clientIpcPath; 35 | protocol = 'ipc'; 36 | diContainer.config.configStore.set('client', {ipcPath: url}); 37 | } 38 | 39 | protocol = (protocol === 'https') ? 'http' : protocol; 40 | protocol = (protocol === 'wss') ? 'ws' : protocol; 41 | 42 | diContainer.logger.debug(`Init "${protocol}" client protocol`); 43 | diContainer.clientErrorHandler = new Error(protocol); 44 | 45 | if (protocol === 'http') { 46 | client = new Http(diContainer); 47 | } else if (protocol === 'ws' || protocol === 'ipc') { 48 | client = new WebSocket(diContainer); 49 | } else { 50 | diContainer.logger.error(`Unknown protocol: "${protocol}"`, false, true); 51 | } 52 | 53 | client.protocol = protocol; 54 | client.url = url; 55 | 56 | return client; 57 | }; 58 | -------------------------------------------------------------------------------- /lib/client/protocol/Abstract.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import queue from 'async/queue'; 3 | import HwInfo from '../HwInfo.js'; 4 | import Usage from '../Usage.js'; 5 | 6 | export default class Abstract { 7 | constructor(diContainer) { 8 | this.CONNECTION_INTERVAL = 5000; 9 | this.STATS_INTERVAL = 10000; 10 | this.USAGE_INTERVAL = 5000; 11 | 12 | this.config = diContainer.config; 13 | this.lodash = diContainer.lodash; 14 | this.os = os; 15 | this.pkg = diContainer.pkg; 16 | this.log = diContainer.logger; 17 | this.cli = diContainer.cli; 18 | this.server = diContainer.server; 19 | this.errorHandler = diContainer.clientErrorHandler; 20 | 21 | this.hwInfo = new HwInfo(diContainer); 22 | this.usage = new Usage(diContainer); 23 | 24 | this.url = this.config.client.url; 25 | 26 | this.lastSyncStatus = { 27 | currentBlock: null 28 | }; 29 | this.lastBlock = null; 30 | this.isConnected = false; 31 | 32 | this.connectionInterval = null; 33 | this.statsInterval = null; 34 | this.usageInterval = null; 35 | 36 | this.blocksQueue = queue((hash, callback) => { 37 | this.getBlock(hash, callback); 38 | }, 1); 39 | } 40 | 41 | processBlock(block) { 42 | let lastBlockNumber = (this.lastBlock === null) ? null : parseInt(this.lastBlock.number, 10); 43 | let currentBlockNumber = parseInt(block.number, 10); 44 | 45 | if (lastBlockNumber === null || (currentBlockNumber - lastBlockNumber) === 1) { 46 | this.lastBlock = block; 47 | this.server.send('block', block); 48 | 49 | if (this.config.NETWORK_ALGO === 'clique') { 50 | this.getValidators(block); 51 | } 52 | } 53 | 54 | if (lastBlockNumber > 0 && (currentBlockNumber - lastBlockNumber) > 1 && (currentBlockNumber - lastBlockNumber) <= 1000) { 55 | this.log.info(`Missing blocks detected (last block: "${lastBlockNumber}")`); 56 | 57 | let blocksToGet = currentBlockNumber - lastBlockNumber; 58 | 59 | while (blocksToGet > 0) { 60 | let blockNumber = (currentBlockNumber - blocksToGet) + 1; 61 | this.blocksQueue.push(blockNumber); 62 | this.log.info(`Force get block "${blockNumber}"`); 63 | blocksToGet--; 64 | } 65 | } 66 | 67 | if (currentBlockNumber < lastBlockNumber) { 68 | this.log.info(`Ignoring block "${currentBlockNumber}" because a newer block has already been sent (last block: "${lastBlockNumber}")`); 69 | } 70 | 71 | if (currentBlockNumber === lastBlockNumber) { 72 | if (this.lastBlock.hash === block.hash) { 73 | this.log.info(`Block "${currentBlockNumber}" has already been sent`); 74 | } else { 75 | this.log.info(`Reorg for block "${currentBlockNumber}"`); 76 | this.server.send('block', block); 77 | 78 | if (this.config.NETWORK_ALGO === 'clique') { 79 | this.getValidators(block); 80 | } 81 | } 82 | } 83 | } 84 | 85 | syncStatus(sync) { 86 | let syncParams = { 87 | syncOperation: null, 88 | startingBlock: 0, 89 | currentBlock: 0, 90 | highestBlock: 0, 91 | progress: 0 92 | }; 93 | 94 | if (sync === true) { 95 | syncParams.syncOperation = 'start'; 96 | } else if (sync) { 97 | let startingBlock = (sync.startingBlock) ? sync.startingBlock : ((sync.status) ? sync.status.StartingBlock : 0); 98 | let currentBlock = (sync.currentBlock) ? sync.currentBlock : ((sync.status) ? sync.status.CurrentBlock : 0); 99 | let highestBlock = (sync.highestBlock) ? sync.highestBlock : ((sync.status) ? sync.status.HighestBlock : 0); 100 | 101 | let progress = currentBlock - startingBlock; 102 | let total = highestBlock - startingBlock; 103 | 104 | syncParams.progress = (progress > 0 && total > 0) ? (progress / total * 100).toFixed(2) : 0; 105 | syncParams.syncOperation = 'progress'; 106 | syncParams.startingBlock = startingBlock; 107 | syncParams.currentBlock = currentBlock; 108 | syncParams.highestBlock = highestBlock; 109 | } else { 110 | syncParams.syncOperation = 'finish'; 111 | } 112 | 113 | if (this.protocol === 'http') { 114 | if (syncParams.syncOperation === 'start' || syncParams.syncOperation === 'progress') { 115 | this.web3.reset(true); 116 | } else { 117 | this.setLatestBlocksFilter(); 118 | } 119 | } 120 | 121 | if (this.lastSyncStatus.currentBlock !== syncParams.currentBlock) { 122 | this.lastSyncStatus = syncParams; 123 | this.server.send('sync', syncParams); 124 | this.lastBlock = null; 125 | } 126 | } 127 | 128 | resolveCheckChainResponse(response) { 129 | if (response.success) { 130 | this.log.info('The node is on the correct network'); 131 | } else { 132 | this.log.error(`Server check chain response: ${JSON.stringify(response.errors)}`, false, true); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/client/protocol/Http.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import Web3 from 'web3-0.x-wrapper'; 3 | import parallel from 'async/parallel'; 4 | import Abstract from './Abstract.js'; 5 | import mapSeries from 'async/mapSeries'; 6 | 7 | export default class Http extends Abstract { 8 | constructor(diContainer) { 9 | super(diContainer); 10 | 11 | this.CHECK_LATEST_FILTER_INTERVAL = 300000; 12 | this.web3 = new Web3(); 13 | 14 | this.checkLatestBlocksFilterInterval = null; 15 | } 16 | 17 | connect() { 18 | if (!this.web3.currentProvider) { 19 | let urlObject = new url.URL(this.url); 20 | 21 | this.log.echo(`Setting Web3 provider to "${urlObject.origin}"`); 22 | this.web3.setProvider(new this.web3.providers.HttpProvider(urlObject.origin, 0, urlObject.username, urlObject.password)); 23 | } 24 | 25 | if (!this.web3.isConnected()) { 26 | this.log.warning('No connection found with the node. Waiting to connect...'); 27 | } 28 | 29 | this.checkConnection(); 30 | 31 | if (this.connectionInterval === null) { 32 | this.connectionInterval = setInterval(() => { 33 | this.checkConnection(); 34 | }, this.CONNECTION_INTERVAL); 35 | } 36 | } 37 | 38 | checkConnection() { 39 | this.log.debug('Check connection'); 40 | 41 | if (this.web3.isConnected()) { 42 | if (!this.isConnected) { 43 | this.isConnected = true; 44 | this.lastBlock = null; 45 | this.start(); 46 | this.log.echo('Connection established with the node'); 47 | } 48 | 49 | if (!this.server.isLoggedIn) { 50 | this.getLoginInfo().then(loginInfos => { 51 | this.usage.setNodeProcessName(loginInfos.node); 52 | this.server.login(loginInfos); 53 | }); 54 | } 55 | } else { 56 | if (this.isConnected) { 57 | this.isConnected = false; 58 | this.stop(); 59 | this.log.warning('Connection lost with the node. Waiting to reconnect...'); 60 | } 61 | 62 | if (this.server.isLoggedIn) { 63 | this.server.logout(); 64 | } 65 | } 66 | } 67 | 68 | async getLoginInfo() { 69 | let result = { 70 | nodeName: this.config.configStore.get('nodeName'), 71 | secretKey: this.config.configStore.get('secretKey'), 72 | coinbase: null, 73 | node: null, 74 | net: null, 75 | protocol: null, 76 | api: this.web3.version.api, 77 | os: this.os.type(), 78 | osVersion: this.os.release(), 79 | client: this.pkg.version, 80 | cpu: null, 81 | memory: null, 82 | disk: null 83 | }; 84 | 85 | try { 86 | result.coinbase = this.web3.eth.coinbase; 87 | } catch (error) { 88 | this.log.error(this.errorHandler.resolve(error)); 89 | } 90 | 91 | try { 92 | result.node = this.web3.version.node; 93 | } catch (error) { 94 | this.log.error(this.errorHandler.resolve(error)); 95 | } 96 | 97 | try { 98 | result.net = this.web3.version.network; 99 | } catch (error) { 100 | this.log.error(this.errorHandler.resolve(error)); 101 | } 102 | 103 | try { 104 | result.protocol = this.web3.version.ethereum; 105 | } catch (error) { 106 | this.log.error(this.errorHandler.resolve(error)); 107 | } 108 | 109 | result.cpu = await this.hwInfo.getCpuInfo(); 110 | result.memory = await this.hwInfo.getMemoryInfo(); 111 | result.disk = await this.hwInfo.getDiskInfo(); 112 | 113 | return result; 114 | } 115 | 116 | start() { 117 | this.log.debug('Start client'); 118 | 119 | // Set chain watchers 120 | this.setLatestBlocksFilter(); 121 | 122 | // Set isSyncing watcher 123 | this.web3.eth.isSyncing((error, data) => { 124 | if (error) { 125 | this.log.error(this.errorHandler.resolve(error)); 126 | } else { 127 | this.syncStatus(data); 128 | } 129 | }); 130 | 131 | this.statsInterval = setInterval(() => { 132 | this.getStats(); 133 | }, this.STATS_INTERVAL); 134 | 135 | this.usageInterval = setInterval(() => { 136 | this.usage.getStats(); 137 | }, this.USAGE_INTERVAL); 138 | 139 | this.checkLatestBlocksFilter(); 140 | } 141 | 142 | stop(stopConnectionInterval = false) { 143 | this.log.debug('Stop client'); 144 | 145 | try { 146 | this.web3.reset(false); 147 | } catch (error) { 148 | this.log.error(this.errorHandler.resolve(error)); 149 | } 150 | 151 | if (stopConnectionInterval) { 152 | clearInterval(this.connectionInterval); 153 | } 154 | 155 | clearInterval(this.statsInterval); 156 | clearInterval(this.usageInterval); 157 | clearInterval(this.checkLatestBlocksFilterInterval); 158 | } 159 | 160 | setLatestBlocksFilter() { 161 | try { 162 | this.web3.eth.filter('latest').watch((error, hash) => { 163 | if (!error) { 164 | hash = hash.value === undefined ? hash : hash.value; 165 | this.blocksQueue.push(hash); 166 | } 167 | }); 168 | } catch (error) { 169 | this.log.error(this.errorHandler.resolve(error)); 170 | } 171 | } 172 | 173 | checkLatestBlocksFilter() { 174 | this.checkLatestBlocksFilterInterval = setInterval(() => { 175 | if (this.isConnected) { 176 | let clientLastBlockNumber = (this.lastBlock === null) ? 0 : parseInt(this.lastBlock.number, 10); 177 | let nodeLastBlockNumber = 0; 178 | 179 | try { 180 | nodeLastBlockNumber = parseInt(this.web3.eth.blockNumber, 10); 181 | } catch (error) { 182 | this.log.error(this.errorHandler.resolve(error)); 183 | } 184 | 185 | if (clientLastBlockNumber > 0 && nodeLastBlockNumber > clientLastBlockNumber) { 186 | this.log.info(`Client last block ${clientLastBlockNumber} is behind Node last block ${nodeLastBlockNumber}, resetting filters...`); 187 | this.stop(); 188 | this.start(); 189 | } 190 | } 191 | }, this.CHECK_LATEST_FILTER_INTERVAL); 192 | } 193 | 194 | getStats() { 195 | this.log.debug('Get stats'); 196 | 197 | parallel({ 198 | peers: callback => { 199 | this.web3.net.getPeerCount((error, data) => { 200 | if (error) { 201 | this.log.error(this.errorHandler.resolve(error)); 202 | } 203 | 204 | data = (data) ? data : 0; 205 | return callback(null, data); 206 | }); 207 | }, 208 | gasPrice: callback => { 209 | this.web3.eth.getGasPrice((error, data) => { 210 | if (error) { 211 | this.log.error(this.errorHandler.resolve(error)); 212 | } 213 | 214 | data = (data) ? data.toString() : 0; 215 | return callback(null, data); 216 | }); 217 | }, 218 | mining: callback => { 219 | this.web3.eth.getMining((error, data) => { 220 | if (error) { 221 | this.log.error(this.errorHandler.resolve(error)); 222 | } 223 | 224 | data = (data) ? data : false; 225 | return callback(null, data); 226 | }); 227 | }, 228 | hashrate: callback => { 229 | this.web3.eth.getHashrate((error, data) => { 230 | if (error) { 231 | this.log.error(this.errorHandler.resolve(error)); 232 | } 233 | 234 | data = (data) ? data : 0; 235 | return callback(null, data); 236 | }); 237 | }, 238 | pendingTXs: callback => { 239 | this.web3.eth.getBlockTransactionCount('pending', (error, data) => { 240 | if (error) { 241 | this.log.error(this.errorHandler.resolve(error)); 242 | } 243 | 244 | data = (data) ? data : 0; 245 | return callback(null, data); 246 | }); 247 | } 248 | }, (error, stats) => { 249 | this.server.send('stats', stats); 250 | }); 251 | } 252 | 253 | getBlock(number, asyncCallback) { 254 | this.web3.eth.getBlock(number, false, (error, block) => { 255 | if (error) { 256 | this.log.error(this.errorHandler.resolve(error)); 257 | 258 | if (asyncCallback) { 259 | asyncCallback(); 260 | } 261 | } else { 262 | this.log.debug(`Got block: "${block.number}"`); 263 | this.processBlock(block); 264 | 265 | if (asyncCallback) { 266 | asyncCallback(); 267 | } 268 | } 269 | }); 270 | } 271 | 272 | getBlockHashes(blockNumber) { 273 | let result = { 274 | blockNumber: null, 275 | blockHash: null, 276 | blockParentHash: null 277 | }; 278 | 279 | this.web3.eth.getBlock(blockNumber, false, (error, block) => { 280 | if (error) { 281 | this.log.error(this.errorHandler.resolve(error)); 282 | } else if (block === null) { 283 | this.log.error(`Could not get block "${blockNumber}". Your node might be not fully synced.`, false, true); 284 | } else { 285 | result.blockNumber = parseInt(block.number, 10); 286 | result.blockHash = block.hash.toString(); 287 | result.blockParentHash = block.parentHash.toString(); 288 | } 289 | 290 | this.server.send('checkChainData', result); 291 | }); 292 | } 293 | 294 | getBlocks(range) { 295 | mapSeries(range, (blockNumber, callback) => { 296 | this.log.debug(`History get block: "${blockNumber}"`); 297 | this.web3.eth.getBlock(blockNumber, false, callback); 298 | }, (error, results) => { 299 | if (error) { 300 | this.log.error(`Error getting block history: ${error}`); 301 | results = []; 302 | } 303 | 304 | this.server.send('getBlocksData', results); 305 | }); 306 | } 307 | 308 | getValidators(block) { 309 | let result = { 310 | blockNumber: block.number, 311 | blockHash: block.hash, 312 | validators: [] 313 | }; 314 | 315 | this.web3._requestManager.sendAsync({method: 'clique_getSignersAtHash', params: [block.hash]}, (error, validators) => { 316 | if (error) { 317 | this.log.error(`Could not get validators for block ${block.number}::${block.hash} => ${error.message}`); 318 | } else { 319 | result.validators = validators; 320 | this.server.send('validators', result); 321 | } 322 | }); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /lib/client/protocol/WebSocket.js: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import Web3 from 'web3'; 3 | import Abstract from './Abstract.js'; 4 | 5 | export default class WebSocket extends Abstract { 6 | constructor(diContainer) { 7 | super(diContainer); 8 | 9 | if (this.protocol === 'ipc') { 10 | this.web3 = new Web3(new Web3.providers.IpcProvider(this.url, net)); 11 | } else { 12 | this.web3 = new Web3(new Web3.providers.WebsocketProvider(this.url)); 13 | } 14 | 15 | this.web3.isConnected = () => { 16 | let result = false; 17 | 18 | return this.web3.eth.getProtocolVersion().then(data => { 19 | if (data) { 20 | result = true; 21 | } 22 | 23 | return result; 24 | }).catch(error => { 25 | if (error.message !== 'connection not open') { 26 | this.log.error(this.errorHandler.resolve(error)); 27 | } 28 | 29 | return result; 30 | }); 31 | }; 32 | } 33 | 34 | setProvider() { 35 | this.log.debug(`Setting Web3 provider to "${this.url}"`); 36 | 37 | if (this.protocol === 'ipc') { 38 | this.web3.setProvider(new Web3.providers.IpcProvider(this.url, net)); 39 | } else { 40 | this.web3.setProvider(new Web3.providers.WebsocketProvider(this.url)); 41 | } 42 | } 43 | 44 | connect() { 45 | this.web3.isConnected().then(result => { 46 | if (!result) { 47 | this.log.echo(`Connecting to node "${this.url}"`); 48 | this.setProvider(); 49 | this.checkConnection(); 50 | } 51 | }); 52 | 53 | if (this.connectionInterval === null) { 54 | this.connectionInterval = setInterval(() => { 55 | this.checkConnection(); 56 | }, this.CONNECTION_INTERVAL); 57 | } 58 | } 59 | 60 | checkConnection() { 61 | this.web3.isConnected().then(isConnected => { 62 | this.log.debug(`Check node connection => ${isConnected}`); 63 | 64 | if (isConnected) { 65 | if (!this.isConnected) { 66 | this.isConnected = true; 67 | this.lastBlock = null; 68 | this.start(); 69 | this.log.echo('Connection established with the node'); 70 | } 71 | 72 | if (!this.server.isLoggedIn) { 73 | this.getLoginInfo().then(loginInfos => { 74 | this.usage.setNodeProcessName(loginInfos.node); 75 | this.server.login(loginInfos); 76 | }); 77 | } 78 | } else { 79 | if (this.isConnected) { 80 | this.isConnected = false; 81 | this.stop(); 82 | this.log.warning('Connection lost with the node. Waiting to reconnect...'); 83 | } else { 84 | this.setProvider(); 85 | } 86 | 87 | if (this.server.isLoggedIn) { 88 | this.server.logout(); 89 | } 90 | } 91 | }); 92 | } 93 | 94 | getLoginInfo() { 95 | let result = { 96 | nodeName: this.config.configStore.get('nodeName'), 97 | secretKey: this.config.configStore.get('secretKey'), 98 | coinbase: null, 99 | node: null, 100 | net: null, 101 | protocol: null, 102 | api: this.web3.version, 103 | os: this.os.type(), 104 | osVersion: this.os.release(), 105 | client: this.pkg.version, 106 | cpu: null, 107 | memory: null, 108 | disk: null 109 | }; 110 | 111 | let allPromises = []; 112 | 113 | let coinbase = this.web3.eth.getCoinbase().then(data => { 114 | if (data) { 115 | return data.toString(); 116 | } 117 | 118 | return null; 119 | }).catch(error => { 120 | this.log.error(this.errorHandler.resolve(error)); 121 | return null; 122 | }); 123 | allPromises.push(coinbase); 124 | 125 | let nodeInfo = this.web3.eth.getNodeInfo().then(data => { 126 | if (data) { 127 | return data.toString(); 128 | } 129 | 130 | return null; 131 | }).catch(error => { 132 | this.log.error(this.errorHandler.resolve(error)); 133 | return null; 134 | }); 135 | allPromises.push(nodeInfo); 136 | 137 | let networkId = this.web3.eth.net.getId().then(data => { 138 | if (data) { 139 | return data.toString(); 140 | } 141 | 142 | return null; 143 | }).catch(error => { 144 | this.log.error(this.errorHandler.resolve(error)); 145 | return null; 146 | }); 147 | allPromises.push(networkId); 148 | 149 | let protocolVersion = this.web3.eth.getProtocolVersion().then(data => { 150 | if (data) { 151 | return data.toString(); 152 | } 153 | 154 | return null; 155 | }).catch(error => { 156 | this.log.error(this.errorHandler.resolve(error)); 157 | return null; 158 | }); 159 | allPromises.push(protocolVersion); 160 | 161 | allPromises.push(this.hwInfo.getCpuInfo()); 162 | allPromises.push(this.hwInfo.getMemoryInfo()); 163 | allPromises.push(this.hwInfo.getDiskInfo()); 164 | 165 | return Promise.all(allPromises).then(promiseResults => { 166 | result.coinbase = promiseResults[0]; 167 | result.node = promiseResults[1]; 168 | result.net = promiseResults[2]; 169 | result.protocol = promiseResults[3]; 170 | result.cpu = promiseResults[4]; 171 | result.memory = promiseResults[5]; 172 | result.disk = promiseResults[6]; 173 | 174 | return result; 175 | }); 176 | } 177 | 178 | start() { 179 | this.log.debug('Start client'); 180 | 181 | this.statsInterval = setInterval(() => { 182 | this.getStats(); 183 | }, this.STATS_INTERVAL); 184 | 185 | this.usageInterval = setInterval(() => { 186 | this.usage.getStats(); 187 | }, this.USAGE_INTERVAL); 188 | 189 | this.web3.eth.subscribe('newBlockHeaders').on('data', blockHeader => { 190 | if (blockHeader) { 191 | this.log.debug(`Got block: "${blockHeader.number}"`); 192 | this.blocksQueue.push(blockHeader.hash); 193 | } 194 | }).on('error', error => { 195 | this.log.error(this.errorHandler.resolve(error)); 196 | }); 197 | 198 | this.web3.eth.subscribe('syncing').on('changed', isSyncing => { 199 | this.syncStatus(isSyncing); 200 | }).on('data', syncInfo => { 201 | if (syncInfo) { 202 | this.syncStatus(syncInfo); 203 | } 204 | }).on('error', error => { 205 | this.log.error(this.errorHandler.resolve(error)); 206 | }); 207 | } 208 | 209 | stop(stopConnectionInterval = false) { 210 | this.log.debug('Stop client'); 211 | 212 | if (stopConnectionInterval) { 213 | clearInterval(this.connectionInterval); 214 | } 215 | 216 | clearInterval(this.statsInterval); 217 | clearInterval(this.usageInterval); 218 | } 219 | 220 | getStats() { 221 | this.log.debug('Get stats'); 222 | 223 | let result = { 224 | peers: 0, 225 | gasPrice: 0, 226 | mining: false, 227 | hashrate: 0, 228 | pendingTXs: 0 229 | }; 230 | 231 | let allPromises = []; 232 | 233 | let peers = this.web3.eth.net.getPeerCount().then(data => { 234 | if (data) { 235 | return data; 236 | } 237 | 238 | return 0; 239 | }).catch(error => { 240 | this.log.error(this.errorHandler.resolve(error)); 241 | return 0; 242 | }); 243 | allPromises.push(peers); 244 | 245 | let gasPrice = this.web3.eth.getGasPrice().then(data => { 246 | if (data) { 247 | return data.toString(); 248 | } 249 | 250 | return 0; 251 | }).catch(error => { 252 | this.log.error(this.errorHandler.resolve(error)); 253 | return 0; 254 | }); 255 | allPromises.push(gasPrice); 256 | 257 | let mining = this.web3.eth.isMining().then(data => { 258 | if (data) { 259 | return data; 260 | } 261 | 262 | return false; 263 | }).catch(error => { 264 | this.log.error(this.errorHandler.resolve(error)); 265 | return false; 266 | }); 267 | allPromises.push(mining); 268 | 269 | let hashrate = this.web3.eth.getHashrate().then(data => { 270 | if (data) { 271 | return data; 272 | } 273 | 274 | return 0; 275 | }).catch(error => { 276 | this.log.error(this.errorHandler.resolve(error)); 277 | return 0; 278 | }); 279 | allPromises.push(hashrate); 280 | 281 | let pendingTXs = this.web3.eth.getBlockTransactionCount('pending').then(data => { 282 | if (data) { 283 | return data; 284 | } 285 | 286 | return 0; 287 | }).catch(error => { 288 | this.log.error(this.errorHandler.resolve(error)); 289 | return 0; 290 | }); 291 | allPromises.push(pendingTXs); 292 | 293 | return Promise.all(allPromises).then(promiseResults => { 294 | result.peers = promiseResults[0]; 295 | result.gasPrice = promiseResults[1]; 296 | result.mining = promiseResults[2]; 297 | result.hashrate = promiseResults[3]; 298 | result.pendingTXs = promiseResults[4]; 299 | 300 | this.server.send('stats', result); 301 | 302 | return result; 303 | }); 304 | } 305 | 306 | getBlock(number, asyncCallback) { 307 | this.web3.eth.getBlock(number, false).then(block => { 308 | if (block) { 309 | this.processBlock(block); 310 | } 311 | 312 | if (asyncCallback) { 313 | asyncCallback(); 314 | } 315 | }).catch(error => { 316 | this.log.error(this.errorHandler.resolve(error)); 317 | 318 | if (asyncCallback) { 319 | asyncCallback(); 320 | } 321 | }); 322 | } 323 | 324 | getBlockHashes(blockNumber) { 325 | let result = { 326 | blockNumber: null, 327 | blockHash: null, 328 | blockParentHash: null 329 | }; 330 | 331 | this.web3.eth.getBlock(blockNumber, false).then(block => { 332 | if (block === null) { 333 | this.log.error(`Could not get block "${blockNumber}". Your node might be not fully synced.`, false, true); 334 | } else { 335 | result.blockNumber = parseInt(block.number, 10); 336 | result.blockHash = block.hash.toString(); 337 | result.blockParentHash = block.parentHash.toString(); 338 | } 339 | 340 | this.server.send('checkChainData', result); 341 | }).catch(error => { 342 | this.log.error(this.errorHandler.resolve(error)); 343 | }); 344 | } 345 | 346 | getBlocks(range) { 347 | let allPromises = range.map(blockNumber => { 348 | this.log.debug(`History get block: "${blockNumber}"`); 349 | return this.web3.eth.getBlock(blockNumber, false); 350 | }); 351 | 352 | Promise.all(allPromises).then(results => { 353 | this.server.send('getBlocksData', results); 354 | }).catch(error => { 355 | this.log.error(`Error getting block history: ${error}`); 356 | this.server.send('getBlocksData', []); 357 | }); 358 | } 359 | 360 | getValidators(block) { 361 | let result = { 362 | blockNumber: block.number, 363 | blockHash: block.hash, 364 | validators: [] 365 | }; 366 | 367 | this.web3._requestManager.send({method: 'clique_getSignersAtHash', params: [block.hash]}, (error, validators) => { 368 | if (error) { 369 | this.log.error(`Could not get validators for block ${block.number}::${block.hash} => ${error.message}`); 370 | } else { 371 | result.validators = validators; 372 | this.server.send('validators', result); 373 | } 374 | }); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethstats-cli", 3 | "version": "2.5.3", 4 | "description": "EthStats - CLI Client", 5 | "homepage": "https://github.com/alethio/ethstats-cli", 6 | "author": { 7 | "name": "Adrian Sabau", 8 | "email": "adrian.sabau@consensys.net", 9 | "url": "https://github.com/baxy" 10 | }, 11 | "preferGlobal": true, 12 | "directories": { 13 | "bin": "./bin", 14 | "lib": "./lib" 15 | }, 16 | "main": "lib/app-cli.js", 17 | "bin": { 18 | "ethstats-cli": "./bin/ethstats-cli.js", 19 | "ethstats-daemon": "./bin/ethstats-daemon.js" 20 | }, 21 | "keywords": [ 22 | "alethio", 23 | "ethstats", 24 | "blockchain", 25 | "explorer", 26 | "netstats", 27 | "network", 28 | "statistics", 29 | "ethereum", 30 | "dashboard", 31 | "web3" 32 | ], 33 | "dependencies": { 34 | "@babel/polyfill": "7.4.0", 35 | "async": "2.6.2", 36 | "boxen": "3.0.0", 37 | "chalk": "2.4.2", 38 | "configstore": "4.0.0", 39 | "ws": "6.2.1", 40 | "primus": "7.3.2", 41 | "primus-responder": "1.0.4", 42 | "debug": "4.1.1", 43 | "inquirer": "6.2.2", 44 | "meow": "5.0.0", 45 | "update-notifier": "2.5.0", 46 | "web3-0.x-wrapper": "1.0.0", 47 | "web3": "1.0.0-beta.37", 48 | "node-cleanup": "2.1.2", 49 | "lodash": "4.17.11", 50 | "request": "2.88.0", 51 | "request-promise": "4.2.4", 52 | "pm2": "3.4.1", 53 | "moment": "2.24.0", 54 | "find-process": "1.4.1", 55 | "systeminformation": "4.1.4", 56 | "ajv": "6.10.0" 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "7.4.0", 60 | "@babel/preset-env": "7.4.2", 61 | "@babel/register": "7.4.0", 62 | "eslint": "5.16.0", 63 | "eslint-config-xo-space": "0.21.0", 64 | "babel-eslint": "10.0.1", 65 | "eslint-plugin-babel": "5.3.0", 66 | "gulp": "4.0.0", 67 | "gulp-babel": "8.0.0", 68 | "gulp-eslint": "5.0.0", 69 | "gulp-plumber": "1.2.1", 70 | "gulp-exclude-gitignore": "1.2.0", 71 | "del": "4.1.0" 72 | }, 73 | "scripts": { 74 | "gulp": "./node_modules/gulp/bin/gulp.js", 75 | "prepare": "gulp prepare" 76 | }, 77 | "license": "MIT", 78 | "repository": "git@github.com:alethio/ethstats-cli.git", 79 | "bugs": { 80 | "url": "https://github.com/alethio/ethstats-cli/issues" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | describe('ethstats-client', function () { 4 | it('should have unit test!', function () { 5 | assert(false, 'we expected this package author to add actual unit tests.'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /triggerDockerHub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! [ -z "$CIRCLE_BRANCH" ]; then 4 | SOURCE_TYPE='Branch'; 5 | SOURCE_NAME=$CIRCLE_BRANCH; 6 | elif ! [ -z "$CIRCLE_TAG" ]; then 7 | SOURCE_TYPE='Tag'; 8 | SOURCE_NAME=$CIRCLE_TAG; 9 | else 10 | echo 'No branch or tag specified!'; 11 | exit 1; 12 | fi 13 | 14 | DOCKER_HUB_URL="https://registry.hub.docker.com/u/alethio/ethstats-cli/trigger/$DOCKER_HUB_TRIGGER/"; 15 | HEADER="Content-Type: application/json"; 16 | DATA_TO_SEND="{\"source_type\": \"$SOURCE_TYPE\", \"source_name\": \"$SOURCE_NAME\"}"; 17 | 18 | curl -s -v -w "\n%{http_code}" -H "$HEADER" --data "$DATA_TO_SEND" -X POST "$DOCKER_HUB_URL" | { 19 | read body; 20 | read code; 21 | 22 | echo "Curl response:" 23 | echo "Http Code: $code"; 24 | echo "Body: $body"; 25 | 26 | if [[ $code -eq 200 || $code -eq 202 ]]; then 27 | exit 0; 28 | else 29 | exit 1; 30 | fi; 31 | } 32 | --------------------------------------------------------------------------------