diff --git a/.gitignore b/.gitignore index 84c82379..52c956b7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ ws_secret.json stats.json package-lock.json +# IdeaIDE +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 787c4141..fab0c376 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ FROM node -RUN git clone https://github.com/goerli/netstats-server /netstats-server -WORKDIR /netstats-server +ADD . /celostats-server +WORKDIR /celostats-server RUN npm install RUN npm install -g grunt-cli RUN grunt EXPOSE 3000 CMD ["npm", "start"] + diff --git a/Gruntfile.js b/Gruntfile.js index b026119f..99d64f2c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -40,6 +40,20 @@ module.exports = function(grunt) { js: ['dist/js/*.*', '!dist/js/netstats.*'], css: ['dist/css/*.css', '!dist/css/netstats.*.css'] }, + watch: { + css: { + files: ['src/css/*.css'], + tasks: ['default'] + }, + js: { + files: ['src/js/*.js'], + tasks: ['default'] + }, + html: { + files: ['src/views/*.jade'], + tasks: ['default'] + } + }, jade: { build: { options: { @@ -51,17 +65,6 @@ module.exports = function(grunt) { files: { 'dist/index.html': 'src/views/index.jade' } - }, - build_pow: { - options: { - data: { - debug: false, - pretty: true - } - }, - files: { - 'dist/index.html': 'src/pow/views/index.jade' - } } }, copy: { @@ -96,37 +99,6 @@ module.exports = function(grunt) { } ] }, - build_pow: { - files: [ - { - expand: true, - cwd: 'src/fonts/', - src: ['*.*'], - dest: 'dist/fonts/', - filter: 'isFile' - }, - { - expand: true, - cwd: 'src/images/', - src: ['*.*'], - dest: 'dist/', - filter: 'isFile' - }, - { - expand: true, - cwd: 'src/pow/css/', - src: styles, - dest: 'dist/css/', - filter: 'isFile' - }, - { - expand: true, - cwd: 'src/js/lib/', - src: ['*.*'], - dest: 'dist/js/lib' - } - ] - } }, cssmin: { build: { @@ -189,6 +161,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-jade'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-uglify'); - + grunt.loadNpmTasks('grunt-contrib-watch'); grunt.registerTask('default', ['clean:build', 'clean:js', 'clean:css', 'jade:build', 'copy:build', 'cssmin:build', 'concat:vendor', 'concat:scripts', 'uglify:app', 'concat:netstats', 'concat:css', 'clean:js', 'clean:css']); }; diff --git a/README.md b/README.md index 3287f01d..bdfd4b01 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ Celo Network Stats =============================================== -[![Build Status][travis-image]][travis-url] [![dependency status][dep-image]][dep-url] This is a visual interface for tracking proof-of-work ("mainnet") and proof-of-authority ("testnet") network status. It uses WebSockets to receive stats from running nodes and output them through an angular interface. It is the front-end implementation for [ethstats-client](https://github.com/goerli/ethstats-client). @@ -15,7 +14,7 @@ This is a visual interface for tracking proof-of-work ("mainnet") and proof-of-a * npm #### Installation -Make sure you have node.js and npm installed. +Make sure you have node.js (10 or above) and npm installed. Clone the repository and install the dependencies: @@ -23,7 +22,7 @@ Clone the repository and install the dependencies: git clone https://github.com/goerli/ethstats-server cd ethstats-server npm install -sudo npm install -g grunt-cli +npm install -g grunt-cli ``` #### Build @@ -36,33 +35,13 @@ grunt To build the static files for a network other than Ethereum copy and change src/js/defaultConfig.js and run the following command. ```bash -grunt --configPath="src/js/someOtherConfig.js" +grunt --configPath="src/js/celoConfig.js" ``` #### Run -Start a node process and pass the websocket secret to it. +Start a node process and pass a trusted node to it or edit the list of trusted nodes in [the server config](/lib/utils/config.js). ```bash -WS_SECRET="asdf" npm start +TRUSTED_NODE=0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 npm start ``` Find the interface at http://localhost:3000 - -## Proof-of-Work (Legacy) - -![Screenshot](src/images/screenshot-pow.png "Screenshot POW") - -* Demo: https://ropsten-stats.parity.io/ - -Same as above, just run the `pow` build task in Grunt. - -```bash -grunt pow -WS_SECRET="asdf" npm start -``` - -:-) - -[travis-image]: https://travis-ci.org/goerli/ethstats-server.svg -[travis-url]: https://travis-ci.org/goerli/ethstats-server -[dep-image]: https://david-dm.org/goerli/ethstats-server.svg -[dep-url]: https://david-dm.org/goerli/ethstats-server diff --git a/app.js b/app.js index 0e0cdd0f..38d52a79 100644 --- a/app.js +++ b/app.js @@ -1,45 +1,24 @@ var _ = require('lodash'); +const { Keccak } = require('sha3'); +const EC = require('elliptic').ec; var logger = require('./lib/utils/logger'); var chalk = require('chalk'); var http = require('http'); -// Init WS SECRET -var WS_SECRET; - -if( !_.isUndefined(process.env.WS_SECRET) && !_.isNull(process.env.WS_SECRET) ) -{ - if( process.env.WS_SECRET.indexOf('|') > 0 ) - { - WS_SECRET = process.env.WS_SECRET.split('|'); - } - else - { - WS_SECRET = [process.env.WS_SECRET]; - } -} -else -{ - try { - var tmp_secret_json = require('./ws_secret.json'); - WS_SECRET = _.values(tmp_secret_json); - } - catch (e) - { - console.error("WS_SECRET NOT SET!!!"); - } -} +let banned = require('./lib/utils/config').banned; +let reserved = require('./lib/utils/config').reserved; +let trusted = require('./lib/utils/config').trusted -var banned = require('./lib/utils/config').banned; -var reserved = require('./lib/utils/config').reserved; +if (process.env.TRUSTED_NODE) { + trusted.push(process.env.TRUSTED_NODE) +} // Init http server -if( process.env.NODE_ENV !== 'production' ) -{ - var app = require('./lib/express'); - server = http.createServer(app); -} -else - server = http.createServer(); +if (process.env.NODE_ENV !== 'production') { + var app = require('./lib/express'); + server = http.createServer(app); +} else + server = http.createServer(); // Init socket vars var Primus = require('primus'); @@ -50,9 +29,9 @@ var server; // Init API Socket connection api = new Primus(server, { - transformer: 'websockets', - pathname: '/api', - parser: 'JSON' + transformer: 'websockets', + pathname: '/api', + parser: 'JSON' }); api.plugin('emit', require('primus-emit')); @@ -61,9 +40,9 @@ api.plugin('spark-latency', require('primus-spark-latency')); // Init Client Socket connection client = new Primus(server, { - transformer: 'websockets', - pathname: '/primus', - parser: 'JSON' + transformer: 'websockets', + pathname: '/primus', + parser: 'JSON' }); client.plugin('emit', require('primus-emit')); @@ -71,9 +50,9 @@ client.plugin('emit', require('primus-emit')); // Init external API external = new Primus(server, { - transformer: 'websockets', - pathname: '/external', - parser: 'JSON' + transformer: 'websockets', + pathname: '/external', + parser: 'JSON' }); external.plugin('emit', require('primus-emit')); @@ -82,332 +61,339 @@ external.plugin('emit', require('primus-emit')); var Collection = require('./lib/collection'); var Nodes = new Collection(external); -Nodes.setChartsCallback(function (err, charts) -{ - if(err !== null) - { - console.error('COL', 'CHR', 'Charts error:', err); - } - else - { - client.write({ - action: 'charts', - data: charts - }); - } +Nodes.setChartsCallback(function (err, charts) { + if (err !== null) { + console.error('COL', 'CHR', 'Charts error:', err); + } else { + client.write({ + action: 'charts', + data: charts + }); + } }); +const authorize = (proof, stats) => { + let isAuthorized = false + if (!_.isUndefined(proof) + && !_.isUndefined(proof.publicKey) + && !_.isUndefined(proof.signature) + && !_.isUndefined(stats)) { + const hasher = new Keccak(256) + hasher.update(JSON.stringify(stats)) + const msgHash = hasher.digest('hex') + const ec = new EC('secp256k1') + const pubkeyNoZeroX = proof.publicKey.substr(2) + let pubkey + try { + pubkey = ec.keyFromPublic(pubkeyNoZeroX, 'hex') + } catch (e) { + console.error('API', 'SIG', 'Public Key Error', e.message) + return false + } + const addressHasher = new Keccak(256) + addressHasher.update(pubkeyNoZeroX.substr(2), 'hex') + const addressHash = addressHasher.digest("hex").substr(24) + if (!(addressHash.toLowerCase() === proof.address.substr(2).toLowerCase())) { + console.error('API', 'SIG', 'Address hash did not match', addressHash, proof.address.substr(2)) + } + const signature = { + r: proof.signature.substr(2, 64), + s: proof.signature.substr(66, 64) + } + if (!(msgHash === proof.msgHash.substr(2))) { + console.error('API', 'SIG', 'Message hash did not match', msgHash, proof.msgHash.substr(2)) + return false + } + try { + isAuthorized = pubkey.verify(msgHash, signature) + } catch (e) { + console.error('API', 'SIG', 'Signature Error', e.message) + return false + } + } + if (!isAuthorized) { + console.error('API', 'SIG', 'Signature did not verify') + } + return isAuthorized +} + // Init API Socket events -api.on('connection', function (spark) -{ - console.info('API', 'CON', 'Open:', spark.address.ip); - - spark.on('hello', function (data) - { - console.info('API', 'CON', 'Hello', data['id']); - - if( _.isUndefined(data.secret) || WS_SECRET.indexOf(data.secret) === -1 || banned.indexOf(spark.address.ip) >= 0 || _.isUndefined(data.id) || reserved.indexOf(data.id) >= 0 ) - { - spark.end(undefined, { reconnect: false }); - console.error('API', 'CON', 'Closed - wrong auth', data); - - return false; - } - - if( !_.isUndefined(data.id) && !_.isUndefined(data.info) ) - { - data.ip = spark.address.ip; - data.spark = spark.id; - data.latency = spark.latency || 0; - - Nodes.add( data, function (err, info) - { - if(err !== null) - { - console.error('API', 'CON', 'Connection error:', err); - return false; - } - - if(info !== null) - { - spark.emit('ready'); - - console.success('API', 'CON', 'Connected', data.id); - - client.write({ - action: 'add', - data: info - }); - } - }); - } - }); - - - spark.on('update', function (data) - { - if( !_.isUndefined(data.id) && !_.isUndefined(data.stats) ) - { - Nodes.update(data.id, data.stats, function (err, stats) - { - if(err !== null) - { - console.error('API', 'UPD', 'Update error:', err); - } - else - { - if(stats !== null) - { - client.write({ - action: 'update', - data: stats - }); - - console.info('API', 'UPD', 'Update from:', data.id, 'for:', stats); - - Nodes.getCharts(); - } - } - }); - } - else - { - console.error('API', 'UPD', 'Update error:', data); - } - }); - - - spark.on('block', function (data) - { - if( !_.isUndefined(data.id) && !_.isUndefined(data.block) ) - { - Nodes.addBlock(data.id, data.block, function (err, stats) - { - if(err !== null) - { - console.error('API', 'BLK', 'Block error:', err); - } - else - { - if(stats !== null) - { - client.write({ - action: 'block', - data: stats - }); - - console.success('API', 'BLK', 'Block:', data.block['number'], 'td:', data.block['totalDifficulty'], 'from:', data.id, 'ip:', spark.address.ip); - - Nodes.getCharts(); - } - } - }); - } - else - { - console.error('API', 'BLK', 'Block error:', data); - } - }); - - - spark.on('pending', function (data) - { - if( !_.isUndefined(data.id) && !_.isUndefined(data.stats) ) - { - Nodes.updatePending(data.id, data.stats, function (err, stats) { - if(err !== null) - { - console.error('API', 'TXS', 'Pending error:', err); - } - - if(stats !== null) - { - client.write({ - action: 'pending', - data: stats - }); - - console.success('API', 'TXS', 'Pending:', data.stats['pending'], 'from:', data.id); - } - }); - } - else - { - console.error('API', 'TXS', 'Pending error:', data); - } - }); - - - spark.on('stats', function (data) - { - if( !_.isUndefined(data.id) && !_.isUndefined(data.stats) ) - { - - Nodes.updateStats(data.id, data.stats, function (err, stats) - { - if(err !== null) - { - console.error('API', 'STA', 'Stats error:', err); - } - else - { - if(stats !== null) - { - client.write({ - action: 'stats', - data: stats - }); - - console.success('API', 'STA', 'Stats from:', data.id); - } - } - }); - } - else - { - console.error('API', 'STA', 'Stats error:', data); - } - }); - - - spark.on('history', function (data) - { - console.success('API', 'HIS', 'Got history from:', data.id); - - var time = chalk.reset.cyan((new Date()).toJSON()) + " "; - console.time(time, 'COL', 'CHR', 'Got charts in'); - - Nodes.addHistory(data.id, data.history, function (err, history) - { - console.timeEnd(time, 'COL', 'CHR', 'Got charts in'); - - if(err !== null) - { - console.error('COL', 'CHR', 'History error:', err); - } - else - { - client.write({ - action: 'charts', - data: history - }); - } - }); - }); - - - spark.on('node-ping', function (data) - { - var start = (!_.isUndefined(data) && !_.isUndefined(data.clientTime) ? data.clientTime : null); - - spark.emit('node-pong', { - clientTime: start, - serverTime: _.now() - }); - - console.info('API', 'PIN', 'Ping from:', data['id']); - }); - - - spark.on('latency', function (data) - { - if( !_.isUndefined(data.id) ) - { - Nodes.updateLatency(data.id, data.latency, function (err, latency) - { - if(err !== null) - { - console.error('API', 'PIN', 'Latency error:', err); - } - - if(latency !== null) - { - // client.write({ - // action: 'latency', - // data: latency - // }); - - console.info('API', 'PIN', 'Latency:', latency, 'from:', data.id); - } - }); - - if( Nodes.requiresUpdate(data.id) ) - { - var range = Nodes.getHistory().getHistoryRequestRange(); - - spark.emit('history', range); - console.info('API', 'HIS', 'Asked:', data.id, 'for history:', range.min, '-', range.max); - - Nodes.askedForHistory(true); - } - } - }); - - - spark.on('end', function (data) - { - Nodes.inactive(spark.id, function (err, stats) - { - if(err !== null) - { - console.error('API', 'CON', 'Connection end error:', err); - } - else - { - client.write({ - action: 'inactive', - data: stats - }); - - console.warn('API', 'CON', 'Connection with:', spark.id, 'ended:', data); - } - }); - }); +api.on('connection', function (spark) { + console.info('API', 'CON', 'Open:', spark.address.ip); + + spark.on('hello', function (data) { + const { stats, proof } = data + console.info('API', 'CON', 'Hello', stats['id']); + if (banned.indexOf(spark.address.ip) >= 0 + || _.isUndefined(stats.id) + || reserved.indexOf(stats.id) >= 0 + || _.isUndefined(proof) + || _.isUndefined(proof.publicKey) + || trusted.map(address => address.toLowerCase()).indexOf(proof.address) < 0 + || !authorize(proof, stats)) { + + spark.end(undefined, { reconnect: false }); + console.error('API', 'CON', 'Closed - wrong auth', data); + + return false; + } + + if (!_.isUndefined(stats.id) && !_.isUndefined(stats.info)) { + stats.ip = spark.address.ip; + stats.spark = spark.id; + stats.latency = spark.latency || 0; + + Nodes.add(stats, function (err, info) { + if (err !== null) { + console.error('API', 'CON', 'Connection error:', err); + return false; + } + + if (info !== null) { + spark.emit('ready'); + + console.success('API', 'CON', 'Connected', stats.id); + + client.write({ + action: 'add', + data: info + }); + } + }); + } + }); + + + spark.on('update', function (data) { + if (!_.isUndefined(data.id) && !_.isUndefined(data.stats)) { + Nodes.update(data.id, data.stats, function (err, stats) { + if (err !== null) { + console.error('API', 'UPD', 'Update error:', err); + } else { + if (stats !== null) { + client.write({ + action: 'update', + data: stats + }); + + console.info('API', 'UPD', 'Update from:', data.id, 'for:', stats); + + Nodes.getCharts(); + } + } + }); + } else { + console.error('API', 'UPD', 'Update error:', data); + } + }); + + + spark.on('block', function (data) { + const { stats, proof } = data + if (authorize(proof, stats) + && !_.isUndefined(stats.id) + && !_.isUndefined(stats.block)) { + + if (stats.block.validators && stats.block.validators.registered) { + stats.block.validators.registered.forEach(validator => { + validator.registered = true + const node = Nodes.getNodeOrNew({ id: validator.address }, validator) + // TODO: only if new node + node.setValidatorData(validator) + return node.name + }) + } + + Nodes.addBlock(stats.id, stats.block, function (err, stats) { + if (err !== null) { + console.error('API', 'BLK', 'Block error:', err); + } else { + if (stats !== null) { + client.write({ + action: 'block', + data: stats + }); + + console.success('API', 'BLK', + 'Block:', stats.block['number'], + 'td:', stats.block['totalDifficulty'], + 'from:', stats.id, 'ip:', spark.address.ip); + + Nodes.getCharts(); + } + } + }); + } else { + console.error('API', 'BLK', 'Block error:', data); + } + }); + + + spark.on('pending', function (data) { + const { stats, proof } = data + if (authorize(proof, stats) + && !_.isUndefined(stats.id) + && !_.isUndefined(stats.stats)) { + Nodes.updatePending(stats.id, stats.stats, function (err, pending) { + if (err !== null) { + console.error('API', 'TXS', 'Pending error:', err); + } + + if (pending !== null) { + client.write({ + action: 'pending', + data: pending + }); + + console.success('API', 'TXS', 'Pending:', pending['pending'], 'from:', pending.id); + } + }); + } else { + console.error('API', 'TXS', 'Pending error:', data); + } + }); + + + spark.on('stats', function (data) { + const { stats, proof } = data + if (authorize(proof, stats) + && !_.isUndefined(stats.id) + && !_.isUndefined(stats.stats)) { + + Nodes.updateStats(stats.id, stats.stats, function (err, stats) { + if (err !== null) { + console.error('API', 'STA', 'Stats error:', err); + } else { + if (stats !== null) { + client.write({ + action: 'stats', + data: stats + }); + + console.success('API', 'STA', 'Stats from:', stats.id); + } + } + }); + } + }); + + + spark.on('history', function (data) { + const { stats, proof } = data + if (authorize(proof, stats)) { + console.success('API', 'HIS', 'Got history from:', stats.id); + + var time = chalk.reset.cyan((new Date()).toJSON()) + " "; + console.time(time, 'COL', 'CHR', 'Got charts in'); + // Nodes.addHistory(stats.id, stats.history, function (err, history) { + // console.timeEnd(time, 'COL', 'CHR', 'Got charts in'); + // if (err !== null) { + // console.error('COL', 'CHR', 'History error:', err); + // } else { + // client.write({ + // action: 'charts', + // data: history + // }); + // } + // }); + } + }); + + + spark.on('node-ping', function (data) { + const { stats, proof } = data + if (authorize(proof, stats)) { + const start = (!_.isUndefined(stats) && !_.isUndefined(stats.clientTime) ? stats.clientTime : null); + + spark.emit('node-pong', { + clientTime: start, + serverTime: _.now() + }); + + console.success('API', 'PIN', 'Ping from:', stats['id']); + } + }); + + + spark.on('latency', function (data) { + const { stats, proof } = data + if (authorize(proof, stats) + && !_.isUndefined(stats.id)) { + Nodes.updateLatency(stats.id, stats.latency, function (err, latency) { + if (err !== null) { + console.error('API', 'PIN', 'Latency error:', err); + } + + if (latency !== null) { + console.success('API', 'PIN', 'Latency:', latency, 'from:', stats.id); + } + }); + + if (Nodes.requiresUpdate(stats.id)) { + var range = Nodes.getHistory().getHistoryRequestRange(); + + spark.emit('history', range); + console.success('API', 'HIS', 'Asked:', stats.id, 'for history:', range.min, '-', range.max); + + Nodes.askedForHistory(true); + } + } + }); + + + spark.on('end', function (data) { + Nodes.inactive(spark.id, function (err, stats) { + if (err !== null) { + console.error('API', 'CON', 'Connection end error:', err); + } else { + client.write({ + action: 'inactive', + data: stats + }); + + console.warn('API', 'CON', 'Connection with:', spark.id, 'ended:', data); + } + }); + }); }); +client.on('connection', function (clientSpark) { + clientSpark.on('ready', function (data) { + clientSpark.emit('init', { nodes: Nodes.all() }); -client.on('connection', function (clientSpark) -{ - clientSpark.on('ready', function (data) - { - clientSpark.emit('init', { nodes: Nodes.all() }); - - Nodes.getCharts(); - }); + Nodes.getCharts(); + }); - clientSpark.on('client-pong', function (data) - { - var serverTime = _.get(data, "serverTime", 0); - var latency = Math.ceil( (_.now() - serverTime) / 2 ); + clientSpark.on('client-pong', function (data) { + var serverTime = _.get(data, "serverTime", 0); + var latency = Math.ceil((_.now() - serverTime) / 2); - clientSpark.emit('client-latency', { latency: latency }); - }); + clientSpark.emit('client-latency', { latency: latency }); + }); }); -var latencyTimeout = setInterval( function () -{ - client.write({ - action: 'client-ping', - data: { - serverTime: _.now() - } - }); +var latencyTimeout = setInterval(function () { + client.write({ + action: 'client-ping', + data: { + serverTime: _.now() + } + }); }, 5000); // Cleanup old inactive nodes -var nodeCleanupTimeout = setInterval( function () -{ - client.write({ - action: 'init', - data: Nodes.all() - }); +var nodeCleanupTimeout = setInterval(function () { + client.write({ + action: 'init', + data: Nodes.all() + }); - Nodes.getCharts(); + Nodes.getCharts(); -}, 1000*60*60); +}, 1000 * 60 * 60); server.listen(process.env.PORT || 3000); diff --git a/lib/collection.js b/lib/collection.js index 22973815..a4635b0a 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -2,332 +2,274 @@ var _ = require('lodash'); var Blockchain = require('./history'); var Node = require('./node'); -var Collection = function Collection(externalAPI) -{ - this._items = []; - this._blockchain = new Blockchain(); - this._askedForHistory = false; - this._askedForHistoryTime = 0; - this._debounced = null; - this._externalAPI = externalAPI; - this._highestBlock = 0; - - return this; +var Collection = function Collection(externalAPI) { + this._items = []; + this._blockchain = new Blockchain(); + this._askedForHistory = false; + this._askedForHistoryTime = 0; + this._debounced = null; + this._externalAPI = externalAPI; + this._highestBlock = 0; + + return this; } -Collection.prototype.setupSockets = function() -{ - this._externalAPI.on('connection', function (spark) - { - this._externalAPI.on('latestBlock', function (data) - { - spark.emit('latestBlock', { - number: this._highestBlock - }); - }); - }); +Collection.prototype.setupSockets = function () { + this._externalAPI.on('connection', function (spark) { + this._externalAPI.on('latestBlock', function (data) { + spark.emit('latestBlock', { + number: this._highestBlock + }); + }); + }); } -Collection.prototype.add = function(data, callback) -{ - var node = this.getNodeOrNew({ id : data.id }, data); - node.setInfo(data, callback); +Collection.prototype.add = function (data, callback) { + var node = this.getNodeOrNew({ id: data.id }, data); + node.setInfo(data, callback); } -Collection.prototype.update = function(id, stats, callback) -{ - var node = this.getNode({ id: id }); - - if (!node) - { - callback('Node not found', null); - } - else - { - // this._blockchain.clean(this.getBestBlockFromItems()); - - var block = this._blockchain.add(stats.block, id, node.trusted); - - if (!block) - { - callback('Block data wrong', null); - } - else - { - var propagationHistory = this._blockchain.getNodePropagation(id); - - stats.block.arrived = block.block.arrived; - stats.block.received = block.block.received; - stats.block.propagation = block.block.propagation; - - node.setStats(stats, propagationHistory, callback); - } - } +Collection.prototype.update = function (id, stats, callback) { + var node = this.getNode({ id: id }); + + if (!node) { + callback('Node not found', null); + } else { + // this._blockchain.clean(this.getBestBlockFromItems()); + + var block = this._blockchain.add(stats.block, id, node.trusted); + + if (!block) { + callback('Block data wrong', null); + } else { + var propagationHistory = this._blockchain.getNodePropagation(id); + + stats.block.arrived = block.block.arrived; + stats.block.received = block.block.received; + stats.block.propagation = block.block.propagation; + + node.setStats(stats, propagationHistory, callback); + } + } } -Collection.prototype.addBlock = function(id, stats, callback) -{ - var node = this.getNode({ id: id }); - - if (!node) - { - callback('Node not found', null); - } - else - { - // this._blockchain.clean(this.getBestBlockFromItems()); - - var block = this._blockchain.add(stats, id, node.trusted); - - if (!block) - { - callback('Block undefined', null); - } - else - { - var propagationHistory = this._blockchain.getNodePropagation(id); - - stats.arrived = block.block.arrived; - stats.received = block.block.received; - stats.propagation = block.block.propagation; - - if(block.block.number > this._highestBlock) - { - this._highestBlock = block.block.number; - this._externalAPI.write({ - action:"lastBlock", - number: this._highestBlock - }); - } - - node.setBlock(stats, propagationHistory, callback); - } - } +Collection.prototype.addBlock = function (id, stats, callback) { + var node = this.getNode({ id: id }); + + if (!node) { + callback('Node not found', null); + } else { + // this._blockchain.clean(this.getBestBlockFromItems()); + + var block = this._blockchain.add(stats, id, node.trusted); + + if (!block) { + callback('Block undefined', null); + } else { + var propagationHistory = this._blockchain.getNodePropagation(id); + + stats.arrived = block.block.arrived; + stats.received = block.block.received; + stats.propagation = block.block.propagation; + stats.validators = block.block.validators; + + if (block.block.number > this._highestBlock) { + this._highestBlock = block.block.number; + this._externalAPI.write({ + action: "lastBlock", + number: this._highestBlock + }); + } + + node.setBlock(stats, propagationHistory, callback); + } + } } -Collection.prototype.updatePending = function(id, stats, callback) -{ - var node = this.getNode({ id: id }); +Collection.prototype.updatePending = function (id, stats, callback) { + var node = this.getNode({ id: id }); - if (!node) - return false; + if (!node) + return false; - node.setPending(stats, callback); + node.setPending(stats, callback); } -Collection.prototype.updateStats = function(id, stats, callback) -{ - var node = this.getNode({ id: id }); - - if (!node) - { - callback('Node not found', null); - } - else - { - node.setBasicStats(stats, callback); - } +Collection.prototype.updateStats = function (id, stats, callback) { + var node = this.getNode({ id: id }); + + if (!node) { + callback('Node not found', null); + } else { + node.setBasicStats(stats, callback); + } } // TODO: Async series -Collection.prototype.addHistory = function(id, blocks, callback) -{ - var node = this.getNode({ id: id }); +Collection.prototype.addHistory = function (id, blocks, callback) { + var node = this.getNode({ id: id }); - if (!node) - { - callback('Node not found', null) - } - else - { - blocks = blocks.reverse(); + if (!node) { + callback('Node not found', null) + } else { + blocks = blocks.reverse(); - // this._blockchain.clean(this.getBestBlockFromItems()); + // this._blockchain.clean(this.getBestBlockFromItems()); - for (var i = 0; i <= blocks.length - 1; i++) - { - this._blockchain.add(blocks[i], id, node.trusted, true); - }; + for (var i = 0; i <= blocks.length - 1; i++) { + this._blockchain.add(blocks[i], id, true, true); + } - this.getCharts(); - } + this.getCharts(); + } - this.askedForHistory(false); + this.askedForHistory(false); } -Collection.prototype.updateLatency = function(id, latency, callback) -{ - var node = this.getNode({ id: id }); +Collection.prototype.updateLatency = function (id, latency, callback) { + var node = this.getNode({ id: id }); - if (!node) - return false; + if (!node) + return false; - node.setLatency(latency, callback); + node.setLatency(latency, callback); } -Collection.prototype.inactive = function(id, callback) -{ - var node = this.getNode({ spark: id }); - - if (!node) - { - callback('Node not found', null); - } - else - { - node.setState(false); - callback(null, node.getStats()); - } +Collection.prototype.inactive = function (id, callback) { + var node = this.getNode({ spark: id }); + + if (!node) { + callback('Node not found', null); + } else { + node.setState(false); + callback(null, node.getStats()); + } } -Collection.prototype.getIndex = function(search) -{ - return _.findIndex(this._items, search); +Collection.prototype.getIndex = function (search) { + return _.findIndex(this._items, search); } -Collection.prototype.getNode = function(search) -{ - var index = this.getIndex(search); +Collection.prototype.getNode = function (search) { + var index = this.getIndex(search); - if(index >= 0) - return this._items[index]; + if (index >= 0) + return this._items[index]; - return false; + return false; } -Collection.prototype.getNodeByIndex = function(index) -{ - if(this._items[index]) - return this._items[index]; +Collection.prototype.getNodeByIndex = function (index) { + if (this._items[index]) + return this._items[index]; - return false; + return false; } -Collection.prototype.getIndexOrNew = function(search, data) -{ - var index = this.getIndex(search); +Collection.prototype.getIndexOrNew = function (search, data) { + var index = this.getIndex(search); - return (index >= 0 ? index : this._items.push(new Node(data)) - 1); + return (index >= 0 ? index : this._items.push(new Node(data)) - 1); } -Collection.prototype.getNodeOrNew = function(search, data) -{ - return this.getNodeByIndex(this.getIndexOrNew(search, data)); +Collection.prototype.getNodeOrNew = function (search, data) { + return this.getNodeByIndex(this.getIndexOrNew(search, data)); } -Collection.prototype.all = function() -{ - this.removeOldNodes(); +Collection.prototype.all = function () { + this.removeOldNodes(); - return this._items; + return this._items; } -Collection.prototype.removeOldNodes = function() -{ - var deleteList = [] - - for(var i = this._items.length - 1; i >= 0; i--) - { - if( this._items[i].isInactiveAndOld() ) - { - deleteList.push(i); - } - } - - if(deleteList.length > 0) - { - for(var i = 0; i < deleteList.length; i++) - { - this._items.splice(deleteList[i], 1); - } - } +Collection.prototype.removeOldNodes = function () { + var deleteList = [] + + for (var i = this._items.length - 1; i >= 0; i--) { + if (this._items[i].isInactiveAndOld()) { + deleteList.push(i); + } + } + + if (deleteList.length > 0) { + for (var i = 0; i < deleteList.length; i++) { + this._items.splice(deleteList[i], 1); + } + } } -Collection.prototype.blockPropagationChart = function() -{ - return this._blockchain.getBlockPropagation(); +Collection.prototype.blockPropagationChart = function () { + return this._blockchain.getBlockPropagation(); } -Collection.prototype.getUncleCount = function() -{ - return this._blockchain.getUncleCount(); +Collection.prototype.getUncleCount = function () { + return this._blockchain.getUncleCount(); } -Collection.prototype.setChartsCallback = function(callback) -{ - this._blockchain.setCallback(callback); +Collection.prototype.setChartsCallback = function (callback) { + this._blockchain.setCallback(callback); } -Collection.prototype.getCharts = function() -{ - this.getChartsDebounced(); +Collection.prototype.getCharts = function () { + this.getChartsDebounced(); } -Collection.prototype.getChartsDebounced = function() -{ - var self = this; - - if( this._debounced === null) { - this._debounced = _.debounce(function(){ - self._blockchain.getCharts(); - }, 1000, { - leading: false, - maxWait: 5000, - trailing: true - }); - } - - this._debounced(); +Collection.prototype.getChartsDebounced = function () { + var self = this; + + if (this._debounced === null) { + this._debounced = _.debounce(function () { + self._blockchain.getCharts(); + }, 500, { + leading: false, + maxWait: 2000, + trailing: true + }); + } + + this._debounced(); } -Collection.prototype.getHistory = function() -{ - return this._blockchain; +Collection.prototype.getHistory = function () { + return this._blockchain; } -Collection.prototype.getBestBlockFromItems = function() -{ - return Math.max(this._blockchain.bestBlockNumber(), _.result(_.max(this._items, function(item) { - // return ( !item.trusted ? 0 : item.stats.block.number ); - return ( item.stats.block.number ); - }), 'stats.block.number', 0)); +Collection.prototype.getBestBlockFromItems = function () { + return Math.max(this._blockchain.bestBlockNumber(), _.result(_.max(this._items, function (item) { + // return ( !item.trusted ? 0 : item.stats.block.number ); + return (item.stats.block.number); + }), 'stats.block.number', 0)); } -Collection.prototype.canNodeUpdate = function(id) -{ - var node = this.getNode({id: id}); +Collection.prototype.canNodeUpdate = function (id) { + var node = this.getNode({ id: id }); - if(!node) - return false; + if (!node) + return false; - if(node.canUpdate()) - { - var diff = node.getBlockNumber() - this._blockchain.bestBlockNumber(); + if (node.canUpdate()) { + var diff = node.getBlockNumber() - this._blockchain.bestBlockNumber(); - return Boolean(diff >= 0); - } + return Boolean(diff >= 0); + } - return false; + return false; } -Collection.prototype.requiresUpdate = function(id) -{ - return ( this.canNodeUpdate(id) && this._blockchain.requiresUpdate() && (!this._askedForHistory || _.now() - this._askedForHistoryTime > 2*60*1000) ); +Collection.prototype.requiresUpdate = function (id) { + return (this.canNodeUpdate(id) && this._blockchain.requiresUpdate() && (!this._askedForHistory || _.now() - this._askedForHistoryTime > 2 * 60 * 1000)); } -Collection.prototype.askedForHistory = function(set) -{ - if( !_.isUndefined(set) ) - { - this._askedForHistory = set; +Collection.prototype.askedForHistory = function (set) { + if (!_.isUndefined(set)) { + this._askedForHistory = set; - if(set === true) - { - this._askedForHistoryTime = _.now(); - } - } + if (set === true) { + this._askedForHistoryTime = _.now(); + } + } - return (this._askedForHistory || _.now() - this._askedForHistoryTime < 2*60*1000); + return (this._askedForHistory || _.now() - this._askedForHistoryTime < 2 * 60 * 1000); } module.exports = Collection; diff --git a/lib/express.js b/lib/express.js index bd37f5fd..7647d2fc 100644 --- a/lib/express.js +++ b/lib/express.js @@ -10,33 +10,33 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, (process.env.LITE === 'true' ? '../dist-lite' : '../dist')))); -app.get('/', function(req, res) { +app.get('/', function (req, res) { res.render('index'); }); // catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error('Not Found'); - err.status = 404; - next(err); +app.use(function (req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); }); // error handlers -app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: err - }); +app.use(function (err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); }); // production error handler -app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: {} - }); +app.use(function (err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); }); module.exports = app; \ No newline at end of file diff --git a/lib/history.js b/lib/history.js index a60123e6..32764cac 100644 --- a/lib/history.js +++ b/lib/history.js @@ -11,651 +11,560 @@ var MAX_UNCLES = 1000; var MAX_UNCLES_PER_BIN = 25; var MAX_BINS = 40; -var History = function History(data) -{ - this._items = []; - this._callback = null; +var History = function History(data) { + this._items = []; + this._callback = null; } -History.prototype.add = function(block, id, trusted, addingHistory) -{ - var changed = false; - - if( !_.isUndefined(block) && !_.isUndefined(block.number) && !_.isUndefined(block.uncles) && !_.isUndefined(block.transactions) && !_.isUndefined(block.difficulty) && block.number > 0 ) - { - trusted = (process.env.LITE === 'true' ? true : trusted); - var historyBlock = this.search(block.number); - var forkIndex = -1; - - var now = _.now(); - - block.trusted = trusted; - block.arrived = now; - block.received = now; - block.propagation = 0; - block.fork = 0; - - if( historyBlock ) - { - // We already have a block with this height in collection - - // Check if node already checked this block height - var propIndex = _.findIndex( historyBlock.propagTimes, { node: id } ); - - // Check if node already check a fork with this height - forkIndex = compareForks(historyBlock, block); - - if( propIndex === -1 ) - { - // Node didn't submit this block before - if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) ) - { - // Found fork => update data - block.arrived = historyBlock.forks[forkIndex].arrived; - block.propagation = now - historyBlock.forks[forkIndex].received; - } - else - { - // No fork found => add a new one - var prevBlock = this.prevMaxBlock(block.number); - - if( prevBlock ) - { - block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); - - if(block.number < this.bestBlock().height) - block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); - } - else - { - block.time = 0; - } - - forkIndex = historyBlock.forks.push(block) - 1; - historyBlock.forks[forkIndex].fork = forkIndex; - } - - // Push propagation time - historyBlock.propagTimes.push({ - node: id, - trusted: trusted, - fork: forkIndex, - received: now, - propagation: block.propagation - }); - } - else - { - // Node submited the block before - if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) ) - { - // Matching fork found => update data - block.arrived = historyBlock.forks[forkIndex].arrived; - - if( forkIndex === historyBlock.propagTimes[propIndex].fork ) - { - // Fork index is the same - block.received = historyBlock.propagTimes[propIndex].received; - block.propagation = historyBlock.propagTimes[propIndex].propagation; - } - else - { - // Fork index is different - historyBlock.propagTimes[propIndex].fork = forkIndex; - historyBlock.propagTimes[propIndex].propagation = block.propagation = now - historyBlock.forks[forkIndex].received; - } - - } - else - { - // No matching fork found => replace old one - block.received = historyBlock.propagTimes[propIndex].received; - block.propagation = historyBlock.propagTimes[propIndex].propagation; - - var prevBlock = this.prevMaxBlock(block.number); - - if( prevBlock ) - { - block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); - - if(block.number < this.bestBlock().height) - block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); - } - else - { - block.time = 0; - } - - forkIndex = historyBlock.forks.push(block) - 1; - historyBlock.forks[forkIndex].fork = forkIndex; - } - } - - if( trusted && !compareBlocks(historyBlock.block, historyBlock.forks[forkIndex]) ) - { - // If source is trusted update the main block - historyBlock.forks[forkIndex].trusted = trusted; - historyBlock.block = historyBlock.forks[forkIndex]; - } - - block.fork = forkIndex; - - changed = true; - - } - else - { - // Couldn't find block with this height - - // Getting previous max block - var prevBlock = this.prevMaxBlock(block.number); - - if( prevBlock ) - { - block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); - - if(block.number < this.bestBlock().height) - block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); - } - else - { - block.time = 0; - } - - var item = { - height: block.number, - block: block, - forks: [block], - propagTimes: [] - } - - if( this._items.length === 0 || (this._items.length > 0 && block.number > this.worstBlockNumber()) || (this._items.length < MAX_HISTORY && block.number < this.bestBlockNumber() && addingHistory) ) - { - item.propagTimes.push({ - node: id, - trusted: trusted, - fork: 0, - received: now, - propagation: block.propagation - }); - - this._save(item); - - changed = true; - } - } - - return { - block: block, - changed: changed - }; - } - - return false; +History.prototype.add = function (block, id, trusted, addingHistory) { + var changed = false; + + if (!_.isUndefined(block) && !_.isUndefined(block.number) && !_.isUndefined(block.uncles) && !_.isUndefined(block.transactions) && !_.isUndefined(block.difficulty) && block.number > 0) { + trusted = (process.env.LITE === 'true' ? true : trusted); + var historyBlock = this.search(block.number); + var forkIndex = -1; + + var now = _.now(); + + block.trusted = trusted; + block.arrived = now; + block.received = now; + block.propagation = 0; + block.fork = 0; + + if (historyBlock) { + // We already have a block with this height in collection + + // Check if node already checked this block height + var propIndex = _.findIndex(historyBlock.propagTimes, { node: id }); + + // Check if node already check a fork with this height + forkIndex = compareForks(historyBlock, block); + + if (propIndex === -1) { + // Node didn't submit this block before + if (forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex])) { + // Found fork => update data + block.arrived = historyBlock.forks[forkIndex].arrived; + block.propagation = now - historyBlock.forks[forkIndex].received; + } else { + // No fork found => add a new one + var prevBlock = this.prevMaxBlock(block.number); + + if (prevBlock) { + block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); + + if (block.number < this.bestBlock().height) + block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); + } else { + block.time = 0; + } + + forkIndex = historyBlock.forks.push(block) - 1; + historyBlock.forks[forkIndex].fork = forkIndex; + } + + // Push propagation time + historyBlock.propagTimes.push({ + node: id, + trusted: trusted, + fork: forkIndex, + received: now, + propagation: block.propagation + }); + } else { + // Node submited the block before + if (forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex])) { + // Matching fork found => update data + block.arrived = historyBlock.forks[forkIndex].arrived; + + if (forkIndex === historyBlock.propagTimes[propIndex].fork) { + // Fork index is the same + block.received = historyBlock.propagTimes[propIndex].received; + block.propagation = historyBlock.propagTimes[propIndex].propagation; + } else { + // Fork index is different + historyBlock.propagTimes[propIndex].fork = forkIndex; + historyBlock.propagTimes[propIndex].propagation = block.propagation = now - historyBlock.forks[forkIndex].received; + } + + } else { + // No matching fork found => replace old one + block.received = historyBlock.propagTimes[propIndex].received; + block.propagation = historyBlock.propagTimes[propIndex].propagation; + + var prevBlock = this.prevMaxBlock(block.number); + + if (prevBlock) { + block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); + + if (block.number < this.bestBlock().height) + block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); + } else { + block.time = 0; + } + + forkIndex = historyBlock.forks.push(block) - 1; + historyBlock.forks[forkIndex].fork = forkIndex; + } + } + + if (trusted && !compareBlocks(historyBlock.block, historyBlock.forks[forkIndex])) { + // If source is trusted update the main block + historyBlock.forks[forkIndex].trusted = trusted; + historyBlock.block = historyBlock.forks[forkIndex]; + } + + block.fork = forkIndex; + + changed = true; + + } else { + // Couldn't find block with this height + + // Getting previous max block + var prevBlock = this.prevMaxBlock(block.number); + + if (prevBlock) { + block.time = Math.max(block.arrived - prevBlock.block.arrived, 0); + + if (block.number < this.bestBlock().height) + block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0); + } else { + block.time = 0; + } + + var item = { + height: block.number, + block: block, + forks: [block], + propagTimes: [] + } + + if ( + this._items.length === 0 + || ( + this._items.length > 0 + && block.number > this.worstBlockNumber()) + || ( + this._items.length < MAX_HISTORY + && block.number < this.bestBlockNumber() + && addingHistory)) { + item.propagTimes.push({ + node: id, + trusted: trusted, + fork: 0, + received: now, + propagation: block.propagation + }); + + this._save(item); + + changed = true; + } + } + + return { + block: block, + changed: changed + }; + } + + return false; } -function compareBlocks(block1, block2) -{ - if( block1.hash !== block2.hash || - block1.parentHash !== block2.parentHash || - block1.sha3Uncles !== block2.sha3Uncles || - block1.transactionsRoot !== block2.transactionsRoot || - block1.stateRoot !== block2.stateRoot || - block1.miner !== block2.miner || - block1.difficulty !== block2.difficulty || - block1.totalDifficulty !== block2.totalDifficulty) - return false; - - return true; +function compareBlocks(block1, block2) { + if (block1.hash !== block2.hash || + block1.parentHash !== block2.parentHash || + block1.sha3Uncles !== block2.sha3Uncles || + block1.transactionsRoot !== block2.transactionsRoot || + block1.stateRoot !== block2.stateRoot || + block1.miner !== block2.miner || + block1.difficulty !== block2.difficulty || + block1.totalDifficulty !== block2.totalDifficulty) + return false; + + return true; } -function compareForks(historyBlock, block2) -{ - if( _.isUndefined(historyBlock) ) - return -1; +function compareForks(historyBlock, block2) { + if (_.isUndefined(historyBlock)) + return -1; - if( _.isUndefined(historyBlock.forks) || historyBlock.forks.length === 0 ) - return -1; + if (_.isUndefined(historyBlock.forks) || historyBlock.forks.length === 0) + return -1; - for(var x = 0; x < historyBlock.forks.length; x++) - if(compareBlocks(historyBlock.forks[x], block2)) - return x; + for (var x = 0; x < historyBlock.forks.length; x++) + if (compareBlocks(historyBlock.forks[x], block2)) + return x; - return -1; + return -1; } -History.prototype._save = function(block) -{ - this._items.unshift(block); +History.prototype._save = function (block) { + this._items.unshift(block); - this._items = _.sortBy( this._items, 'height', false ); + this._items = _.sortBy(this._items, 'height', false).reverse(); - if(this._items.length > MAX_HISTORY) - { - this._items.pop(); - } + if (this._items.length > MAX_HISTORY) { + this._items.pop(); + } } -History.prototype.clean = function(max) -{ - if(max > 0 && this._items.length > 0 && max < this.bestBlockNumber()) - { - console.log("MAX:", max); +History.prototype.clean = function (max) { + if (max > 0 && this._items.length > 0 && max < this.bestBlockNumber()) { + console.log("MAX:", max); - console.log("History items before:", this._items.length); + console.log("History items before:", this._items.length); - this._items = _(this._items).filter(function(item) { - return (item.height <= max && item.block.trusted === false); - }).value(); + this._items = _(this._items).filter(function (item) { + return (item.height <= max && item.block.trusted === false); + }).value(); - console.log("History items after:", this._items.length); - } + console.log("History items after:", this._items.length); + } } -History.prototype.search = function(number) -{ - var index = _.findIndex( this._items, { height: number } ); +History.prototype.search = function (number) { + var index = _.findIndex(this._items, { height: number }); - if(index < 0) - return false; + if (index < 0) + return false; - return this._items[index]; + return this._items[index]; } -History.prototype.prevMaxBlock = function(number) -{ - var index = _.findIndex(this._items, function (item) { - return item.height < number; - }); +History.prototype.prevMaxBlock = function (number) { + const heights = this._items.map(item=>item.height) + const index = heights.indexOf(Math.max(...heights)) + + if (index < 0) + return false; - if(index < 0) - return false; - - return this._items[index]; + return this._items[index]; } -History.prototype.bestBlock = function() -{ - return _.maxBy(this._items, 'height'); +History.prototype.bestBlock = function () { + return _.maxBy(this._items, 'height'); } -History.prototype.bestBlockNumber = function() -{ - var best = this.bestBlock(); +History.prototype.bestBlockNumber = function () { + var best = this.bestBlock(); - if( !_.isUndefined(best) && !_.isUndefined(best.height) ) - return best.height; + if (!_.isUndefined(best) && !_.isUndefined(best.height)) + return best.height; - return 0; + return 0; } -History.prototype.worstBlock = function() -{ - return _.minBy(this._items, 'height'); +History.prototype.worstBlock = function () { + return _.minBy(this._items, 'height'); } -History.prototype.worstBlockNumber = function(trusted) -{ - var worst = this.worstBlock(); +History.prototype.worstBlockNumber = function (trusted) { + var worst = this.worstBlock(); - if( !_.isUndefined(worst) && !_.isUndefined(worst.height) ) - return worst.height; + if (!_.isUndefined(worst) && !_.isUndefined(worst.height)) + return worst.height; - return 0; + return 0; } -History.prototype.getNodePropagation = function(id) -{ - var propagation = new Array( MAX_PEER_PROPAGATION ); - var bestBlock = this.bestBlockNumber(); - var lastBlocktime = _.now(); - - _.fill(propagation, -1); - - var sorted = _( this._items ) - .sortBy( 'height', false ) - .slice( 0, MAX_PEER_PROPAGATION ) - .forEach(function (item, key) - { - var index = MAX_PEER_PROPAGATION - 1 - bestBlock + item.height; - - if(index >= 0) - { - var tmpPropagation = _.result(_.find(item.propagTimes, 'node', id), 'propagation', false); - - if (_.result(_.find(item.propagTimes, 'node', id), 'propagation', false) !== false) - { - propagation[index] = tmpPropagation; - lastBlocktime = item.block.arrived; - } - else - { - propagation[index] = Math.max(0, lastBlocktime - item.block.arrived); - } - } - }) - .reverse(); - - return propagation; +History.prototype.getNodePropagation = function (id) { + const propagation = this._items + .slice(0, MAX_PEER_PROPAGATION) + .map(item => { + matches = item.propagTimes.filter(item => item.node === id) + if (matches.length > 0) + return matches[0].propagation + return -1 + }) + return propagation; } -History.prototype.getBlockPropagation = function() -{ - var propagation = []; - var avgPropagation = 0; - - _.forEach(this._items, function (n, key) - { - _.forEach(n.propagTimes, function (p, i) - { - var prop = Math.min(MAX_PROPAGATION_RANGE, _.result(p, 'propagation', -1)); - - if(prop >= 0) - propagation.push(prop); - }); - }); - - if(propagation.length > 0) - { - var avgPropagation = Math.round( _.sum(propagation) / propagation.length ); - } - - var data = d3.layout.histogram() - .frequency( false ) - .range([ MIN_PROPAGATION_RANGE, MAX_PROPAGATION_RANGE ]) - .bins( MAX_BINS ) - ( propagation ); - - var freqCum = 0; - var histogram = data.map(function (val) { - freqCum += val.length; - var cumPercent = ( freqCum / Math.max(1, propagation.length) ); - - return { - x: val.x, - dx: val.dx, - y: val.y, - frequency: val.length, - cumulative: freqCum, - cumpercent: cumPercent - }; - }); - - return { - histogram: histogram, - avg: avgPropagation - }; +History.prototype.getBlockPropagation = function () { + var propagation = []; + var avgPropagation = 0; + + _.forEach(this._items, function (n, key) { + _.forEach(n.propagTimes, function (p, i) { + var prop = Math.min(MAX_PROPAGATION_RANGE, _.result(p, 'propagation', -1)); + + if (prop >= 0) + propagation.push(prop); + }); + }); + + if (propagation.length > 0) { + var avgPropagation = Math.round(_.sum(propagation) / propagation.length); + } + + var data = d3.layout.histogram() + .frequency(false) + .range([MIN_PROPAGATION_RANGE, MAX_PROPAGATION_RANGE]) + .bins(MAX_BINS) + (propagation); + + var freqCum = 0; + var histogram = data.map(function (val) { + freqCum += val.length; + var cumPercent = (freqCum / Math.max(1, propagation.length)); + + return { + x: val.x, + dx: val.dx, + y: val.y, + frequency: val.length, + cumulative: freqCum, + cumpercent: cumPercent + }; + }); + + return { + histogram: histogram, + avg: avgPropagation + }; } -History.prototype.getUncleCount = function() -{ - var uncles = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, MAX_UNCLES) - .map(function (item) - { - return item.block.uncles.length; - }) - .value(); - - var uncleBins = _.fill( Array(MAX_BINS), 0 ); - - var sumMapper = function (array, key) - { - uncleBins[key] = _.sum(array); - return _.sum(array); - }; - - _.map(_.chunk( uncles, MAX_UNCLES_PER_BIN ), sumMapper); - - return uncleBins; +History.prototype.getUncleCount = function () { + var uncles = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + .slice(0, MAX_UNCLES) + .map(function (item) { + return item.block.uncles.length; + }) + .value(); + + var uncleBins = _.fill(Array(MAX_BINS), 0); + + var sumMapper = function (array, key) { + uncleBins[key] = _.sum(array); + return _.sum(array); + }; + + _.map(_.chunk(uncles, MAX_UNCLES_PER_BIN), sumMapper); + + return uncleBins; } -History.prototype.getBlockTimes = function() -{ - var blockTimes = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.time / 1000; - }) - .value(); - - return blockTimes; +History.prototype.getBlockTimes = function () { + var blockTimes = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.time / 1000; + }) + .value(); + + return blockTimes; } -History.prototype.getAvgBlocktime = function() -{ - var blockTimes = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - // .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.time / 1000; - }) - .value(); - - return _.sum(blockTimes) / (blockTimes.length === 0 ? 1 : blockTimes.length); +History.prototype.getAvgBlocktime = function () { + var blockTimes = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + // .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.time / 1000; + }) + .value(); + + return _.sum(blockTimes) / (blockTimes.length === 0 ? 1 : blockTimes.length); } -History.prototype.getGasLimit = function() -{ - var gasLimitHistory = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.gasLimit; - }) - .value(); - - return gasLimitHistory; +History.prototype.getGasLimit = function () { + var gasLimitHistory = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.gasLimit; + }) + .value(); + + return gasLimitHistory; } -History.prototype.getDifficulty = function() -{ - var difficultyHistory = _( this._items ) - .sortBy( 'height', false ) - .filter(function (item) - { - return item.block.trusted; - }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.difficulty; - }) - .value(); - - return difficultyHistory; +History.prototype.getDifficulty = function () { + var difficultyHistory = _(this._items) + .sortBy('height', false) + .filter(function (item) { + return item.block.trusted; + }) + .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.difficulty; + }) + .value(); + + return difficultyHistory; } -History.prototype.getTransactionsCount = function() -{ - var txCount = _( this._items ) - .sortBy( 'height', false ) - .filter(function (item) - { - return item.block.trusted; - }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.transactions.length; - }) - .value(); - - return txCount; +History.prototype.getTransactionsCount = function () { + var txCount = _(this._items) + .sortBy('height', false) + .filter(function (item) { + return item.block.trusted; + }) + .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.transactions.length; + }) + .value(); + + return txCount; } -History.prototype.getGasSpending = function() -{ - var gasSpending = _( this._items ) - .sortBy( 'height', false ) - .filter(function (item) - { - return item.block.trusted; - }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return item.block.gasUsed; - }) - .value(); - - return gasSpending; +History.prototype.getGasSpending = function () { + var gasSpending = _(this._items) + .sortBy('height', false) + .filter(function (item) { + return item.block.trusted; + }) + .slice(0, MAX_BINS) + .reverse() + .map(function (item) { + return item.block.gasUsed; + }) + .value(); + + return gasSpending; } -History.prototype.getAvgHashrate = function() -{ - if( _.isEmpty(this._items) ) - return 0; - - var blocktimeHistory = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, 64) - .map(function (item) - { - return item.block.time; - }) - .value(); - - var avgBlocktime = (_.sum(blocktimeHistory) / blocktimeHistory.length)/1000; - - return this.bestBlock().block.difficulty / avgBlocktime; +History.prototype.getAvgHashrate = function () { + if (_.isEmpty(this._items)) + return 0; + + var blocktimeHistory = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + .slice(0, 64) + .map(function (item) { + return item.block.time; + }) + .value(); + + var avgBlocktime = (_.sum(blocktimeHistory) / blocktimeHistory.length) / 1000; + + return this.bestBlock().block.difficulty / avgBlocktime; } -History.prototype.getMinersCount = function() -{ - var miners = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, MAX_BINS) - .map(function (item) - { - return item.block.miner; - }) - .value(); - - var minerCount = []; - - _.forEach( _.countBy(miners), function (cnt, miner) - { - minerCount.push({ miner: miner, name: false, blocks: cnt }); - }); - - return _(minerCount) - .sortBy( 'blocks', false ) - .slice(0, 2) - .value(); +History.prototype.getMinersCount = function () { + var miners = _(this._items) + .sortBy('height', false) + // .filter(function (item) + // { + // return item.block.trusted; + // }) + .slice(0, MAX_BINS) + .map(function (item) { + return item.block.miner; + }) + .value(); + + var minerCount = []; + + _.forEach(_.countBy(miners), function (cnt, miner) { + minerCount.push({ miner: miner, name: false, blocks: cnt }); + }); + + return _(minerCount) + .sortBy('blocks', false) + .slice(0, 2) + .value(); } -History.prototype.setCallback = function(callback) -{ - this._callback = callback; +History.prototype.setCallback = function (callback) { + this._callback = callback; } -History.prototype.getCharts = function() -{ - if(this._callback !== null) - { - var chartHistory = _( this._items ) - .sortBy( 'height', false ) - // .filter(function (item) - // { - // return item.block.trusted; - // }) - .slice(0, MAX_BINS) - .reverse() - .map(function (item) - { - return { - height: item.height, - blocktime: item.block.time / 1000, - difficulty: item.block.difficulty, - uncles: item.block.uncles.length, - transactions: item.block.transactions ? item.block.transactions.length : 0, - gasSpending: item.block.gasUsed, - gasLimit: item.block.gasLimit, - miner: item.block.miner - }; - }) - .value(); - - this._callback(null, { - height : _.map( chartHistory, 'height' ), - blocktime : _.map( chartHistory, 'blocktime' ), - // avgBlocktime : _.sum(_.map( chartHistory, 'blocktime' )) / (chartHistory.length === 0 ? 1 : chartHistory.length), - avgBlocktime : this.getAvgBlocktime(), - difficulty : _.map( chartHistory, 'difficulty' ), - uncles : _.map( chartHistory, 'uncles' ), - transactions : _.map( chartHistory, 'transactions' ), - gasSpending : _.map( chartHistory, 'gasSpending' ), - gasLimit : _.map( chartHistory, 'gasLimit' ), - miners : this.getMinersCount(), - propagation : this.getBlockPropagation(), - uncleCount : this.getUncleCount(), - avgHashrate : this.getAvgHashrate() - }); - } +History.prototype.getCharts = function () { + if (this._callback !== null) { + var chartHistory = _(this._items) + .sortBy('height', false) + .reverse() + .slice(0, MAX_BINS) + .map(function (item) { + return { + height: item.height, + blocktime: item.block.time / 1000, + difficulty: item.block.difficulty, + uncles: item.block.uncles.length, + transactions: item.block.transactions ? item.block.transactions.length : 0, + gasSpending: item.block.gasUsed, + gasLimit: item.block.gasLimit, + miner: item.block.miner + }; + }) + .value(); + const padArray = function(arr,len,fill) { + return arr.concat(Array(len).fill(fill)).slice(0,len); + } + + this._callback(null, { + height: _.map(chartHistory, 'height'), + blocktime: padArray(_.map(chartHistory, 'blocktime'), MAX_BINS, 0), + // avgBlocktime : _.sum(_.map( chartHistory, 'blocktime' )) / (chartHistory.length === 0 ? 1 : chartHistory.length), + avgBlocktime: this.getAvgBlocktime(), + difficulty: _.map(chartHistory, 'difficulty'), + uncles: _.map(chartHistory, 'uncles'), + transactions: _.map(chartHistory, 'transactions'), + gasSpending: padArray(_.map(chartHistory, 'gasSpending'), MAX_BINS, 0), + gasLimit: padArray(_.map(chartHistory, 'gasLimit'), MAX_BINS, 0), + miners: this.getMinersCount(), + propagation: this.getBlockPropagation(), + uncleCount: this.getUncleCount(), + avgHashrate: this.getAvgHashrate() + }); + } } -History.prototype.requiresUpdate = function() -{ - // return ( this._items.length < MAX_HISTORY && !_.isEmpty(this._items) ); - return ( this._items.length < MAX_HISTORY ); +History.prototype.requiresUpdate = function () { + // return ( this._items.length < MAX_HISTORY && !_.isEmpty(this._items) ); + return (this._items.length < MAX_HISTORY); } -History.prototype.getHistoryRequestRange = function() -{ - if( this._items.length < 2 ) - return false; +History.prototype.getHistoryRequestRange = function () { + if (this._items.length < 2) + return false; - var blocks = _.map( this._items, 'height' ); - var best = _.max( blocks ); - var range = _.range( _.max([ 0, best - MAX_HISTORY ]), best + 1); + var blocks = _.map(this._items, 'height'); + var best = _.max(blocks); + var range = _.range(_.max([0, best - MAX_HISTORY]), best + 1); - var missing = _.difference( range, blocks ); + var missing = _.difference(range, blocks); - var max = _.max(missing); - var min = max - Math.min( 50, (MAX_HISTORY - this._items.length + 1) ) + 1; + var max = _.max(missing); + var min = max - Math.min(50, (MAX_HISTORY - this._items.length + 1)) + 1; - return { - max: max, - min: min, - list: _( missing ).reverse().slice(0, 50).reverse().value() - }; + return { + max: max, + min: min, + list: _(missing).reverse().slice(0, 50).reverse().value() + }; } module.exports = History; diff --git a/lib/node.js b/lib/node.js index 454fa544..f4cd0fa0 100644 --- a/lib/node.js +++ b/lib/node.js @@ -3,395 +3,370 @@ var _ = require('lodash'); var trusted = require('./utils/config').trusted; var MAX_HISTORY = 40; -var MAX_INACTIVE_TIME = 1000*60*60*4; - -var Node = function(data) -{ - this.id = null; - this.trusted = false; - this.info = {}; - this.geo = {} - this.stats = { - active: false, - mining: false, - hashrate: 0, - peers: 0, - pending: 0, - gasPrice: 0, - block: { - number: 0, - hash: '0x0000000000000000000000000000000000000000000000000000000000000000', - difficulty: 0, - totalDifficulty: 0, - gasLimit: 0, - timestamp: 0, - time: 0, - arrival: 0, - received: 0, - propagation: 0, - transactions: [], - uncles: [] - }, - syncing: false, - propagationAvg: 0, - latency: 0, - uptime: 100 - }; - - this.history = new Array(MAX_HISTORY); - - this.uptime = { - started: null, - up: 0, - down: 0, - lastStatus: null, - lastUpdate: null - }; - - this.init(data); - - return this; +var MAX_INACTIVE_TIME = 1000 * 60 * 60 * 4; + +var Node = function (data) { + this.id = null; + this.address = null; + this.validatorData = { + name: null, + url: null, + affiliation: null, + } + this.trusted = false; + this.info = {}; + this.geo = {} + this.stats = { + active: false, + mining: false, + elected: false, + hashrate: 0, + peers: 0, + pending: 0, + gasPrice: 0, + block: { + number: 0, + hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + difficulty: 0, + totalDifficulty: 0, + gasLimit: 0, + timestamp: 0, + time: 0, + arrival: 0, + received: 0, + propagation: 0, + transactions: [], + uncles: [] + }, + syncing: false, + propagationAvg: 0, + latency: 0, + uptime: 100 + }; + + this.history = new Array(MAX_HISTORY); + + this.uptime = { + started: null, + up: 0, + down: 0, + lastStatus: null, + lastUpdate: null + }; + + if (!data.registered) { + this.init(data); + } else { + this.setValidatorData(data) + } + + if (!!data.address) { + this.address = data.address + } + + return this; } -Node.prototype.init = function(data) -{ - _.fill(this.history, -1); +Node.prototype.init = function (data) { + _.fill(this.history, -1); - if( this.id === null && this.uptime.started === null ) - this.setState(true); + if (this.id === null && this.uptime.started === null) + this.setState(true); - this.id = _.result(data, 'id', this.id); + this.id = _.result(data, 'id', this.id); - if( !_.isUndefined(data.latency) ) - this.stats.latency = data.latency; + if (!_.isUndefined(data.latency)) + this.stats.latency = data.latency; - this.setInfo(data, null); + this.setInfo(data, null); } -Node.prototype.setInfo = function(data, callback) -{ - if( !_.isUndefined(data.info) ) - { - this.info = data.info; +Node.prototype.setInfo = function (data, callback) { + if (!_.isUndefined(data.info)) { + this.info = data.info; - if( !_.isUndefined(data.info.canUpdateHistory) ) - { - this.info.canUpdateHistory = _.result(data, 'info.canUpdateHistory', false); - } - } + if (!_.isUndefined(data.info.canUpdateHistory)) { + this.info.canUpdateHistory = _.result(data, 'info.canUpdateHistory', false); + } + } - if( !_.isUndefined(data.ip) ) - { - if( trusted.indexOf(data.ip) >= 0 || process.env.LITE === 'true') - { - this.trusted = true; - } + if (!_.isUndefined(data.ip)) { + if (trusted.indexOf(data.ip) >= 0 || process.env.LITE === 'true') { + this.trusted = true; + } + this.trusted = true; + this.setGeo(data.ip); + } - this.setGeo(data.ip); - } + this.spark = _.result(data, 'spark', null); - this.spark = _.result(data, 'spark', null); + this.setState(true); - this.setState(true); - - if(callback !== null) - { - callback(null, this.getInfo()); - } + if (callback !== null) { + callback(null, this.getInfo()); + } } -Node.prototype.setGeo = function(ip) -{ - if (ip.substr(0, 7) == "::ffff:") { - ip = ip.substr(7) - } - this.info.ip = ip; - this.geo = geoip.lookup(ip); +Node.prototype.setGeo = function (ip) { + if (ip.substr(0, 7) == "::ffff:") { + ip = ip.substr(7) + } + this.info.ip = ip; + this.geo = geoip.lookup(ip); } -Node.prototype.getInfo = function(callback) -{ - return { - id: this.id, - info: this.info, - stats: { - active: this.stats.active, - mining: this.stats.mining, - syncing: this.stats.syncing, - hashrate: this.stats.hashrate, - peers: this.stats.peers, - gasPrice: this.stats.gasPrice, - block: this.stats.block, - propagationAvg: this.stats.propagationAvg, - uptime: this.stats.uptime, - latency: this.stats.latency, - pending: this.stats.pending, - }, - history: this.history, - geo: this.geo - }; +Node.prototype.getInfo = function (callback) { + return { + id: this.id, + info: this.info, + stats: { + active: this.stats.active, + mining: this.stats.mining, + syncing: this.stats.syncing, + hashrate: this.stats.hashrate, + peers: this.stats.peers, + gasPrice: this.stats.gasPrice, + block: this.stats.block, + propagationAvg: this.stats.propagationAvg, + uptime: this.stats.uptime, + latency: this.stats.latency, + pending: this.stats.pending, + }, + history: this.history, + geo: this.geo + }; } -Node.prototype.setStats = function(stats, history, callback) -{ - if( !_.isUndefined(stats) ) - { - this.setBlock( _.result(stats, 'block', this.stats.block), history, function (err, block) {} ); +Node.prototype.setStats = function (stats, history, callback) { + if (!_.isUndefined(stats)) { + this.setBlock(_.result(stats, 'block', this.stats.block), history, function (err, block) { + }); - this.setBasicStats(stats, function (err, stats) {}); + this.setBasicStats(stats, function (err, stats) { + }); - this.setPending( _.result(stats, 'pending', this.stats.pending), function (err, stats) {} ); + this.setPending(_.result(stats, 'pending', this.stats.pending), function (err, stats) { + }); - callback(null, this.getStats()); - } + callback(null, this.getStats()); + } - callback('Stats undefined', null); + callback('Stats undefined', null); } -Node.prototype.setBlock = function(block, history, callback) -{ - if( !_.isUndefined(block) && !_.isUndefined(block.number) ) - { - if ( !_.isEqual(history, this.history) || !_.isEqual(block, this.stats.block) ) - { - if(block.number !== this.stats.block.number || block.hash !== this.stats.block.hash) - { - this.stats.block = block; - } - - this.setHistory(history); - - callback(null, this.getBlockStats()); - } - else - { - callback(null, null); - } - } - else - { - callback('Block undefined', null); - } +Node.prototype.setValidatorData = function (validatorData) { + if (!_.isUndefined(validatorData)) { + this.validatorData = validatorData + this.info.name = validatorData.name + this.info.contact = validatorData.url + this.stats.mining = true + this.trusted = true + this.id = validatorData.address + } +} + +Node.prototype.setBlock = function (block, history, callback) { + if (!_.isUndefined(block) && !_.isUndefined(block.number)) { + if (!_.isEqual(history, this.history) || !_.isEqual(block, this.stats.block)) { + if (block.number !== this.stats.block.number || block.hash !== this.stats.block.hash) { + if (!block.validators.registered) { + block.validators = this.stats.block.validators + } + this.stats.block = block; + } + + this.setHistory(history); + + callback(null, this.getBlockStats()); + } else { + callback(null, null); + } + } else { + callback('Block undefined', null); + } } -Node.prototype.setHistory = function(history) -{ - if( _.isEqual(history, this.history) ) - { - return false; - } +Node.prototype.setHistory = function (history) { + if (_.isEqual(history, this.history)) { + return false; + } - if( !_.isArray(history) ) - { - this.history = _.fill( new Array(MAX_HISTORY), -1 ); - this.stats.propagationAvg = 0; + if (!_.isArray(history)) { + this.history = _.fill(new Array(MAX_HISTORY), -1); + this.stats.propagationAvg = 0; - return true; - } + return true; + } - this.history = history; + this.history = history; - var positives = _.filter(history, function(p) { - return p >= 0; - }); + var positives = _.filter(history, function (p) { + return p >= 0; + }); - this.stats.propagationAvg = ( positives.length > 0 ? Math.round( _.sum(positives) / positives.length ) : 0 ); - positives = null; + this.stats.propagationAvg = (positives.length > 0 ? Math.round(_.sum(positives) / positives.length) : 0); + positives = null; - return true; + return true; } -Node.prototype.setPending = function(stats, callback) -{ - if( !_.isUndefined(stats) && !_.isUndefined(stats.pending)) - { - if(!_.isEqual(stats.pending, this.stats.pending)) - { - this.stats.pending = stats.pending; - - callback(null, { - id: this.id, - pending: this.stats.pending - }); - } - else - { - callback(null, null); - } - } - else - { - callback('Stats undefined', null); - } +Node.prototype.setPending = function (stats, callback) { + if (!_.isUndefined(stats) && !_.isUndefined(stats.pending)) { + if (!_.isEqual(stats.pending, this.stats.pending)) { + this.stats.pending = stats.pending; + + callback(null, { + id: this.id, + pending: this.stats.pending + }); + } else { + callback(null, null); + } + } else { + callback('Stats undefined', null); + } } -Node.prototype.setBasicStats = function(stats, callback) -{ - if( !_.isUndefined(stats) ) - { - if( !_.isEqual(stats, { - active: this.stats.active, - mining: this.stats.mining, - hashrate: this.stats.hashrate, - peers: this.stats.peers, - gasPrice: this.stats.gasPrice, - uptime: this.stats.uptime - }) ) - { - this.stats.active = stats.active; - this.stats.mining = stats.mining; - this.stats.syncing = (!_.isUndefined(stats.syncing) ? stats.syncing : false); - this.stats.hashrate = stats.hashrate; - this.stats.peers = stats.peers; - this.stats.gasPrice = stats.gasPrice; - this.stats.uptime = stats.uptime; - - callback(null, this.getBasicStats()); - } - else - { - callback(null, null); - } - } - else - { - callback('Stats undefined', null); - } +Node.prototype.setBasicStats = function (stats, callback) { + if (!_.isUndefined(stats)) { + if (!_.isEqual(stats, { + active: this.stats.active, + mining: this.stats.mining, + elected: this.stats.elected, + hashrate: this.stats.hashrate, + peers: this.stats.peers, + gasPrice: this.stats.gasPrice, + uptime: this.stats.uptime + })) { + this.stats.active = stats.active; + this.stats.mining = stats.mining; + this.stats.elected = stats.elected; + this.stats.syncing = (!_.isUndefined(stats.syncing) ? stats.syncing : false); + this.stats.hashrate = stats.hashrate; + this.stats.peers = stats.peers; + this.stats.gasPrice = stats.gasPrice; + this.stats.uptime = stats.uptime; + + callback(null, this.getBasicStats()); + } else { + callback(null, null); + } + } else { + callback('Stats undefined', null); + } } -Node.prototype.setLatency = function(latency, callback) -{ - if( !_.isUndefined(latency) ) - { - if( !_.isEqual(latency, this.stats.latency) ) - { - this.stats.latency = latency; - - callback(null, { - id: this.id, - latency: latency - }); - } - else - { - callback(null, null); - } - } - else - { - callback('Latency undefined', null); - } +Node.prototype.setLatency = function (latency, callback) { + if (!_.isUndefined(latency)) { + if (!_.isEqual(latency, this.stats.latency)) { + this.stats.latency = latency; + + callback(null, { + id: this.id, + latency: latency + }); + } else { + callback(null, null); + } + } else { + callback('Latency undefined', null); + } } -Node.prototype.getStats = function() -{ - return { - id: this.id, - stats: { - active: this.stats.active, - mining: this.stats.mining, - syncing: this.stats.syncing, - hashrate: this.stats.hashrate, - peers: this.stats.peers, - gasPrice: this.stats.gasPrice, - block: this.stats.block, - propagationAvg: this.stats.propagationAvg, - uptime: this.stats.uptime, - pending: this.stats.pending, - latency: this.stats.latency - }, - history: this.history - }; +Node.prototype.getStats = function () { + return { + id: this.id, + stats: { + active: this.stats.active, + mining: this.stats.mining, + syncing: this.stats.syncing, + hashrate: this.stats.hashrate, + peers: this.stats.peers, + gasPrice: this.stats.gasPrice, + block: this.stats.block, + propagationAvg: this.stats.propagationAvg, + uptime: this.stats.uptime, + pending: this.stats.pending, + latency: this.stats.latency + }, + history: this.history + }; } -Node.prototype.getBlockStats = function() -{ - return { - id: this.id, - block: this.stats.block, - propagationAvg: this.stats.propagationAvg, - history: this.history - }; +Node.prototype.getBlockStats = function () { + return { + id: this.id, + block: this.stats.block, + propagationAvg: this.stats.propagationAvg, + history: this.history + }; } -Node.prototype.getBasicStats = function() -{ - return { - id: this.id, - stats: { - active: this.stats.active, - mining: this.stats.mining, - syncing: this.stats.syncing, - hashrate: this.stats.hashrate, - peers: this.stats.peers, - gasPrice: this.stats.gasPrice, - uptime: this.stats.uptime, - latency: this.stats.latency - } - }; +Node.prototype.getBasicStats = function () { + return { + id: this.id, + stats: { + active: this.stats.active, + mining: this.stats.mining, + elected: this.stats.elected, + syncing: this.stats.syncing, + hashrate: this.stats.hashrate, + peers: this.stats.peers, + gasPrice: this.stats.gasPrice, + uptime: this.stats.uptime, + latency: this.stats.latency + } + }; } -Node.prototype.setState = function(active) -{ - var now = _.now(); - - if(this.uptime.started !== null) - { - if(this.uptime.lastStatus === active) - { - this.uptime[(active ? 'up' : 'down')] += now - this.uptime.lastUpdate; - } - else - { - this.uptime[(active ? 'down' : 'up')] += now - this.uptime.lastUpdate; - } - } - else - { - this.uptime.started = now; - } - - this.stats.active = active; - this.uptime.lastStatus = active; - this.uptime.lastUpdate = now; - - this.stats.uptime = this.calculateUptime(); - - now = undefined; +Node.prototype.setState = function (active) { + var now = _.now(); + + if (this.uptime.started !== null) { + if (this.uptime.lastStatus === active) { + this.uptime[(active ? 'up' : 'down')] += now - this.uptime.lastUpdate; + } else { + this.uptime[(active ? 'down' : 'up')] += now - this.uptime.lastUpdate; + } + } else { + this.uptime.started = now; + } + + this.stats.active = active; + this.uptime.lastStatus = active; + this.uptime.lastUpdate = now; + + this.stats.uptime = this.calculateUptime(); + + now = undefined; } -Node.prototype.calculateUptime = function() -{ - if(this.uptime.lastUpdate === this.uptime.started) - { - return 100; - } +Node.prototype.calculateUptime = function () { + if (this.uptime.lastUpdate === this.uptime.started) { + return 100; + } - return Math.round( this.uptime.up / (this.uptime.lastUpdate - this.uptime.started) * 100); + return Math.round(this.uptime.up / (this.uptime.lastUpdate - this.uptime.started) * 100); } -Node.prototype.getBlockNumber = function() -{ - return this.stats.block.number; +Node.prototype.getBlockNumber = function () { + return this.stats.block.number; } -Node.prototype.canUpdate = function() -{ - if (this.trusted) { - return true; - } - // return (this.info.canUpdateHistory && this.trusted) || false; - return (this.info.canUpdateHistory || (this.stats.syncing === false && this.stats.peers > 0)) || false; +Node.prototype.canUpdate = function () { + if (this.trusted) { + return true; + } + // return (this.info.canUpdateHistory && this.trusted) || false; + return (this.info.canUpdateHistory || (this.stats.syncing === false && this.stats.peers > 0)) || false; } -Node.prototype.isInactiveAndOld = function() -{ - if( this.uptime.lastStatus === false && this.uptime.lastUpdate !== null && (_.now() - this.uptime.lastUpdate) > MAX_INACTIVE_TIME ) - { - return true; - } +Node.prototype.isInactiveAndOld = function () { + if (this.uptime.lastStatus === false && this.uptime.lastUpdate !== null && (_.now() - this.uptime.lastUpdate) > MAX_INACTIVE_TIME) { + return true; + } - return false; + return false; } module.exports = Node; diff --git a/lib/utils/config.js b/lib/utils/config.js index 13587f22..b86fd35d 100644 --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -1,4 +1,8 @@ -var trusted = []; +var trusted = [ + "0x47e172f6cfb6c7d01c1574fa3e2be7cc73269D95", + "0xa42c9b0d1a30722aea8b81e72957134897e7a11a", + "0xa0af2e71cecc248f4a7fd606f203467b500dd53b", +] var banned = []; diff --git a/lib/utils/logger.js b/lib/utils/logger.js index 604a1e07..b8f16486 100644 --- a/lib/utils/logger.js +++ b/lib/utils/logger.js @@ -17,7 +17,8 @@ var types = [ 'TXS', 'STA', 'HIS', - 'PIN' + 'PIN', + 'SIG' ]; var typeColors = { @@ -29,6 +30,7 @@ var typeColors = { 'STA': chalk.reset.bold.red, 'HIS': chalk.reset.bold.magenta, 'PIN': chalk.reset.bold.yellow, + 'SIG': chalk.reset.bold.green, }; var verbosity = [ diff --git a/package.json b/package.json index 7d2a64d8..f58c0262 100644 --- a/package.json +++ b/package.json @@ -16,21 +16,17 @@ "chalk": "^2.4.1", "d3": "3.5.17", "debug": "^4.1.0", + "elliptic": "^6.5.1", "express": "^4.16.4", "geoip-lite": "^1.3.5", "grunt": "^1.0.3", "grunt-cli": "^1.3.2", - "grunt-contrib-clean": "^2.0.0", - "grunt-contrib-concat": "^1.0.1", - "grunt-contrib-copy": "^1.0.0", - "grunt-contrib-cssmin": "^3.0.0", - "grunt-contrib-jade": "^1.0.0", - "grunt-contrib-uglify": "^4.0.0", "jade": "^1.11.0", "lodash": "4.17.11", "primus": "^7.3.1", "primus-emit": "^1.0.0", "primus-spark-latency": "^0.1.1", + "sha3": "^2.0.7", "ws": "^6.1.2" }, "repository": { @@ -54,5 +50,13 @@ } ], "license": "LGPL-3.0", - "devDependencies": {} + "devDependencies": { + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-cssmin": "^3.0.0", + "grunt-contrib-jade": "^1.0.0", + "grunt-contrib-uglify": "^4.0.0", + "grunt-contrib-watch": "^1.1.0" + } } diff --git a/src/css/style.css b/src/css/style.css index eb02bc72..18866b5d 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -8,6 +8,7 @@ body { font-smooth: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: #2E3338; } @font-face { @@ -19,7 +20,7 @@ body { @font-face { font-family: 'Source Sans Pro'; - font-style: normal; + font-style: normal; font-weight: 300; src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(../fonts/SourceSansPro-Light.woff2) format('woff2'); } @@ -52,6 +53,10 @@ table td { -moz-osx-font-smoothing: auto; } +table tr { + border: 1px solid rgba(255,255,255, 0.1); +} + .propagationBox { position: relative; width: 8px; @@ -65,12 +70,18 @@ table td { .bg-success, .text-success .propagationBox { - background: #50fa7b; + background: #35D07F; +} +.text-success{ + color:#35D07F } .bg-info, .text-info .propagationBox { - background: #8be9fd; + background: #73DDFF; +} +.text-info { + color: #73DDFF; } .bg-highlight, @@ -80,7 +91,7 @@ table td { .bg-warning, .text-warning .propagationBox { - background: #f1fa8c; + background: #FFB765; } .bg-orange, @@ -103,7 +114,7 @@ table td { .bg-warning, .bg-orange, .bg-danger { - color: #000; + color: #2E3338; } .text-gray { @@ -120,8 +131,8 @@ table td { } .stat-holder { - background: #090909; - border: 1px solid rgba(255,255,255,0.05); + background: #2E3338; + border: 1px solid rgba(255,255,255, 0.1); } .big-info { @@ -151,7 +162,7 @@ div.small-title-miner { line-height: 20px; letter-spacing: 1px; text-transform: uppercase; - color: #aaa; + /* color: #aaa; */ } span.small-title span.small { @@ -194,8 +205,8 @@ span.small-title span.small { } .big-info.chart { - height: 120px; - -webkit-box-sizing: border-box + height: 130px; + -webkit-box-sizing: border-box; box-sizing: border-box; } @@ -297,7 +308,7 @@ table i { table th, table td { - border-color: #222 !important; + border-color: rgba(255,255,255, 0.1) !important; } table td { @@ -343,7 +354,15 @@ nodepropagchart { } .th-nodename { - width: 300px; + width: 200px; +} + +.nodeInfo { + overflow: hidden; + white-space: nowrap; +} + +.nodeInfo>span { text-overflow: ellipsis; } @@ -556,3 +575,6 @@ svg .y.axis .tick:first-child text { font-size: 10px; } } + + +.table-striped>tbody>tr:nth-child(odd){background-color:#2E3338} \ No newline at end of file diff --git a/src/js/controllers.js b/src/js/controllers.js index 33fa96f7..6db3d22c 100644 --- a/src/js/controllers.js +++ b/src/js/controllers.js @@ -1,654 +1,528 @@ - /* Controllers */ +netStatsApp.controller('StatsCtrl', function ($scope, $filter, $localStorage, socket, _, toastr) { + + var MAX_BINS = 40; + + // Main Stats init + // --------------- + + $scope.frontierHash = '0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa'; + $scope.nodesTotal = 0; + $scope.nodesActive = 0; + $scope.bestBlock = 0; + $scope.lastBlock = 0; + $scope.lastDifficulty = 0; + $scope.upTimeTotal = 0; + $scope.avgBlockTime = 0; + $scope.blockPropagationAvg = 0; + $scope.avgHashrate = 0; + $scope.uncleCount = 0; + $scope.bestStats = {}; + + $scope.lastGasLimit = _.fill(Array(MAX_BINS), 2); + $scope.lastBlocksTime = _.fill(Array(MAX_BINS), 2); + $scope.difficultyChart = _.fill(Array(MAX_BINS), 2); + $scope.transactionDensity = _.fill(Array(MAX_BINS), 2); + $scope.gasSpending = _.fill(Array(MAX_BINS), 2); + $scope.miners = []; + $scope.validators = { + 'elected': [], + 'registered': [] + }; + + + $scope.nodes = []; + $scope.map = []; + $scope.blockPropagationChart = []; + $scope.uncleCountChart = _.fill(Array(MAX_BINS), 2); + $scope.coinbases = []; + + $scope.latency = 0; + + $scope.currentApiVersion = "0.1.1"; + + $scope.predicate = $localStorage.predicate || ['-pinned', '-stats.active', '-stats.block.number', 'stats.block.propagation']; + $scope.reverse = $localStorage.reverse || false; + $scope.pinned = $localStorage.pinned || []; + + $scope.prefixPredicate = ['-pinned', '-stats.active']; + $scope.originalPredicate = ['-stats.block.number', 'stats.block.propagation']; + + $scope.orderTable = function (predicate, reverse) { + if (!_.isEqual(predicate, $scope.originalPredicate)) { + $scope.reverse = reverse; + $scope.originalPredicate = predicate; + $scope.predicate = _.union($scope.prefixPredicate, predicate); + } else { + $scope.reverse = !$scope.reverse; + + if ($scope.reverse === true) { + _.forEach(predicate, function (value, key) { + predicate[key] = (value[0] === '-' ? value.replace('-', '') : '-' + value); + }); + } + + $scope.predicate = _.union($scope.prefixPredicate, predicate); + } + + $localStorage.predicate = $scope.predicate; + $localStorage.reverse = $scope.reverse; + } + + $scope.pinNode = function (id) { + index = findIndex({ id: id }); + + if (!_.isUndefined($scope.nodes[index])) { + $scope.nodes[index].pinned = !$scope.nodes[index].pinned; + + if ($scope.nodes[index].pinned) { + $scope.pinned.push(id); + } else { + $scope.pinned.splice($scope.pinned.indexOf(id), 1); + } + } + + $localStorage.pinned = $scope.pinned; + } + + var timeout = setInterval(function () { + $scope.$apply(); + }, 300); + + $scope.getNumber = function (num) { + return new Array(num); + } + + // Socket listeners + // ---------------- + + socket.on('open', function open() { + socket.emit('ready'); + console.log('The connection has been opened.'); + }) + .on('end', function end() { + console.log('Socket connection ended.') + }) + .on('error', function error(err) { + console.log(err); + }) + .on('reconnecting', function reconnecting(opts) { + console.log('We are scheduling a reconnect operation', opts); + }) + .on('data', function incoming(data) { + $scope.$apply(socketAction(data.action, data.data)); + }); + + socket.on('init', function (data) { + $scope.$apply(socketAction("init", data.nodes)); + }); + + socket.on('client-latency', function (data) { + $scope.latency = data.latency; + }) + + function socketAction(action, data) { + // filter data + data = xssFilter(data); + + switch (action) { + case "init": + $scope.nodes = data; + _.forEach($scope.nodes, function (node, index) { + + // Init hashrate + if (_.isUndefined(node.stats.hashrate)) + node.stats.hashrate = 0; + + // Init latency + latencyFilter(node); + + // Init history + if (_.isUndefined(data.history)) { + data.history = new Array(40); + _.fill(data.history, -1); + } + + // Init or recover pin + node.pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false); + }); + + if ($scope.nodes.length > 0) { + toastr['success']("Got nodes list", "Got nodes!"); + + updateActiveNodes(); + } + + break; + + case "add": + var index = findIndex({ id: data.id }); + + // if( addNewNode(data) ) + // toastr['success']("New node "+ $scope.nodes[findIndex({id: data.id})].info.name +" connected!", "New node!"); + // else + // toastr['info']("Node "+ $scope.nodes[index].info.name +" reconnected!", "Node is back!"); + + break; + + // TODO: Remove when everybody updates api client to 0.0.12 + case "update": + var index = findIndex({ id: data.id }); + + if (index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats)) { + if (!_.isUndefined($scope.nodes[index].stats.latency)) + data.stats.latency = $scope.nodes[index].stats.latency; + + if (_.isUndefined(data.stats.hashrate)) + data.stats.hashrate = 0; + + if ($scope.nodes[index].stats.block.number < data.stats.block.number) { + var best = _.max($scope.nodes, function (node) { + return parseInt(node.stats.block.number); + }).stats.block; + + if (data.stats.block.number > best.number) { + data.stats.block.arrived = _.now(); + } else { + data.stats.block.arrived = best.arrived; + } + $scope.nodes[index].history = data.history; + } + + $scope.nodes[index].stats = data.stats; + + if (!_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency) { + $scope.nodes[index].stats.latency = data.stats.latency; + + latencyFilter($scope.nodes[index]); + } + + updateBestBlock(); + } + + break; -netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, socket, _, toastr) { - - var MAX_BINS = 40; - - // Main Stats init - // --------------- - - $scope.frontierHash = '0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa'; - $scope.nodesTotal = 0; - $scope.nodesActive = 0; - $scope.bestBlock = 0; - $scope.lastBlock = 0; - $scope.lastDifficulty = 0; - $scope.upTimeTotal = 0; - $scope.avgBlockTime = 0; - $scope.blockPropagationAvg = 0; - $scope.avgHashrate = 0; - $scope.uncleCount = 0; - $scope.bestStats = {}; - - $scope.lastGasLimit = _.fill(Array(MAX_BINS), 2); - $scope.lastBlocksTime = _.fill(Array(MAX_BINS), 2); - $scope.difficultyChart = _.fill(Array(MAX_BINS), 2); - $scope.transactionDensity = _.fill(Array(MAX_BINS), 2); - $scope.gasSpending = _.fill(Array(MAX_BINS), 2); - $scope.miners = []; - - - $scope.nodes = []; - $scope.map = []; - $scope.blockPropagationChart = []; - $scope.uncleCountChart = _.fill(Array(MAX_BINS), 2); - $scope.coinbases = []; - - $scope.latency = 0; - - $scope.currentApiVersion = "0.1.1"; - - $scope.predicate = $localStorage.predicate || ['-pinned', '-stats.active', '-stats.block.number', 'stats.block.propagation']; - $scope.reverse = $localStorage.reverse || false; - $scope.pinned = $localStorage.pinned || []; - - $scope.prefixPredicate = ['-pinned', '-stats.active']; - $scope.originalPredicate = ['-stats.block.number', 'stats.block.propagation']; - - $scope.orderTable = function(predicate, reverse) - { - if(!_.isEqual(predicate, $scope.originalPredicate)) - { - $scope.reverse = reverse; - $scope.originalPredicate = predicate; - $scope.predicate = _.union($scope.prefixPredicate, predicate); - } - else - { - $scope.reverse = !$scope.reverse; - - if($scope.reverse === true){ - _.forEach(predicate, function (value, key) { - predicate[key] = (value[0] === '-' ? value.replace('-', '') : '-' + value); - }); - } - - $scope.predicate = _.union($scope.prefixPredicate, predicate); - } - - $localStorage.predicate = $scope.predicate; - $localStorage.reverse = $scope.reverse; - } - - $scope.pinNode = function(id) - { - index = findIndex({id: id}); - - if( !_.isUndefined($scope.nodes[index]) ) - { - $scope.nodes[index].pinned = !$scope.nodes[index].pinned; - - if($scope.nodes[index].pinned) - { - $scope.pinned.push(id); - } - else - { - $scope.pinned.splice($scope.pinned.indexOf(id), 1); - } - } - - $localStorage.pinned = $scope.pinned; - } - - var timeout = setInterval(function () - { - $scope.$apply(); - }, 300); - - $scope.getNumber = function (num) { - return new Array(num); - } - - // Socket listeners - // ---------------- - - socket.on('open', function open() { - socket.emit('ready'); - console.log('The connection has been opened.'); - }) - .on('end', function end() { - console.log('Socket connection ended.') - }) - .on('error', function error(err) { - console.log(err); - }) - .on('reconnecting', function reconnecting(opts) { - console.log('We are scheduling a reconnect operation', opts); - }) - .on('data', function incoming(data) { - $scope.$apply(socketAction(data.action, data.data)); - }); - - socket.on('init', function(data) - { - $scope.$apply(socketAction("init", data.nodes)); - }); - - socket.on('client-latency', function(data) - { - $scope.latency = data.latency; - }) - - function socketAction(action, data) - { - // filter data - data = xssFilter(data); - - // console.log('Action: ', action); - // console.log('Data: ', data); - - switch(action) - { - case "init": - $scope.nodes = data; - - _.forEach($scope.nodes, function (node, index) { - - // Init hashrate - if( _.isUndefined(node.stats.hashrate) ) - node.stats.hashrate = 0; - - // Init latency - latencyFilter(node); - - // Init history - if( _.isUndefined(data.history) ) - { - data.history = new Array(40); - _.fill(data.history, -1); - } - - // Init or recover pin - node.pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false); - }); - - if( $scope.nodes.length > 0 ) - { - toastr['success']("Got nodes list", "Got nodes!"); - - updateActiveNodes(); - } - - break; - - case "add": - var index = findIndex({id: data.id}); - - // if( addNewNode(data) ) - // toastr['success']("New node "+ $scope.nodes[findIndex({id: data.id})].info.name +" connected!", "New node!"); - // else - // toastr['info']("Node "+ $scope.nodes[index].info.name +" reconnected!", "Node is back!"); - - break; - - // TODO: Remove when everybody updates api client to 0.0.12 - case "update": - var index = findIndex({id: data.id}); - - if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) ) - { - if( !_.isUndefined($scope.nodes[index].stats.latency) ) - data.stats.latency = $scope.nodes[index].stats.latency; - - if( _.isUndefined(data.stats.hashrate) ) - data.stats.hashrate = 0; - - if( $scope.nodes[index].stats.block.number < data.stats.block.number ) - { - var best = _.max($scope.nodes, function (node) { - return parseInt(node.stats.block.number); - }).stats.block; + case "block": + var index = findIndex({ id: data.id }); - if (data.stats.block.number > best.number) { - data.stats.block.arrived = _.now(); - } else { - data.stats.block.arrived = best.arrived; - } + if (index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats)) { + if ($scope.nodes[index].stats.block.number < data.block.number) { + var best = _.max($scope.nodes, function (node) { + return parseInt(node.stats.block.number); + }).stats.block; - $scope.nodes[index].history = data.history; - } + if (data.block.number > best.number) { + data.block.arrived = _.now(); + } else { + data.block.arrived = best.arrived; + } - $scope.nodes[index].stats = data.stats; + $scope.nodes[index].history = data.history; + } + $scope.nodes[index].stats.block = data.block; + $scope.nodes[index].stats.propagationAvg = data.propagationAvg; + $scope.validators = data.block.validators; + updateBestBlock(); + } - if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency ) - { - $scope.nodes[index].stats.latency = data.stats.latency; + break; - latencyFilter($scope.nodes[index]); - } + case "pending": + var index = findIndex({ id: data.id }); - updateBestBlock(); - } + if (!_.isUndefined(data.id) && index >= 0) { + var node = $scope.nodes[index]; - break; + if (!_.isUndefined(node) && !_.isUndefined(node.stats.pending) && !_.isUndefined(data.pending)) + $scope.nodes[index].stats.pending = data.pending; + } - case "block": - var index = findIndex({id: data.id}); + break; - if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) ) - { - if( $scope.nodes[index].stats.block.number < data.block.number ) - { - var best = _.max($scope.nodes, function (node) { - return parseInt(node.stats.block.number); - }).stats.block; + case "stats": + var index = findIndex({ id: data.id }); - if (data.block.number > best.number) { - data.block.arrived = _.now(); - } else { - data.block.arrived = best.arrived; - } + if (!_.isUndefined(data.id) && index >= 0) { + var node = $scope.nodes[index]; - $scope.nodes[index].history = data.history; - } + if (!_.isUndefined(node) && !_.isUndefined(node.stats)) { + $scope.nodes[index].stats.active = data.stats.active; + $scope.nodes[index].stats.mining = data.stats.mining; + $scope.nodes[index].stats.hashrate = data.stats.hashrate; + $scope.nodes[index].stats.peers = data.stats.peers; + $scope.nodes[index].stats.gasPrice = data.stats.gasPrice; + $scope.nodes[index].stats.uptime = data.stats.uptime; + $scope.nodes[index].stats.address = data.stats.address; + console.log($scope.nodes[index].stats.address) + if (!_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency) { + $scope.nodes[index].stats.latency = data.stats.latency; - $scope.nodes[index].stats.block = data.block; - $scope.nodes[index].stats.propagationAvg = data.propagationAvg; + latencyFilter($scope.nodes[index]); + } - updateBestBlock(); - } + updateActiveNodes(); + } + } - break; + break; - case "pending": - var index = findIndex({id: data.id}); + case "info": + var index = findIndex({ id: data.id }); - if( !_.isUndefined(data.id) && index >= 0 ) - { - var node = $scope.nodes[index]; + if (index >= 0) { + $scope.nodes[index].info = data.info; - if( !_.isUndefined(node) && !_.isUndefined(node.stats.pending) && !_.isUndefined(data.pending) ) - $scope.nodes[index].stats.pending = data.pending; - } + if (_.isUndefined($scope.nodes[index].pinned)) + $scope.nodes[index].pinned = false; - break; + // Init latency + latencyFilter($scope.nodes[index]); - case "stats": - var index = findIndex({id: data.id}); + updateActiveNodes(); + } - if( !_.isUndefined(data.id) && index >= 0 ) - { - var node = $scope.nodes[index]; + break; - if( !_.isUndefined(node) && !_.isUndefined(node.stats) ) - { - $scope.nodes[index].stats.active = data.stats.active; - $scope.nodes[index].stats.mining = data.stats.mining; - $scope.nodes[index].stats.hashrate = data.stats.hashrate; - $scope.nodes[index].stats.peers = data.stats.peers; - $scope.nodes[index].stats.gasPrice = data.stats.gasPrice; - $scope.nodes[index].stats.uptime = data.stats.uptime; + case "blockPropagationChart": + $scope.blockPropagationChart = data.histogram; + $scope.blockPropagationAvg = data.avg; - if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency ) - { - $scope.nodes[index].stats.latency = data.stats.latency; + break; - latencyFilter($scope.nodes[index]); - } + case "uncleCount": + $scope.uncleCount = data[0] + data[1]; + data.reverse(); + $scope.uncleCountChart = data; - updateActiveNodes(); - } - } + break; - break; + case "charts": + if (!_.isEqual($scope.avgBlockTime, data.avgBlocktime)) + $scope.avgBlockTime = data.avgBlocktime; - case "info": - var index = findIndex({id: data.id}); + if (!_.isEqual($scope.avgHashrate, data.avgHashrate)) + $scope.avgHashrate = data.avgHashrate; - if( index >= 0 ) - { - $scope.nodes[index].info = data.info; + if (!_.isEqual($scope.lastGasLimit, data.gasLimit) && data.gasLimit.length >= MAX_BINS) + $scope.lastGasLimit = data.gasLimit; + + if (!_.isEqual($scope.lastBlocksTime, data.blocktime) && data.blocktime.length >= MAX_BINS) + $scope.lastBlocksTime = data.blocktime; - if( _.isUndefined($scope.nodes[index].pinned) ) - $scope.nodes[index].pinned = false; + if (!_.isEqual($scope.difficultyChart, data.difficulty) && data.difficulty.length >= MAX_BINS) + $scope.difficultyChart = data.difficulty; - // Init latency - latencyFilter($scope.nodes[index]); + if (!_.isEqual($scope.blockPropagationChart, data.propagation.histogram)) { + $scope.blockPropagationChart = data.propagation.histogram; + $scope.blockPropagationAvg = data.propagation.avg; + } - updateActiveNodes(); - } + data.uncleCount.reverse(); - break; + if (!_.isEqual($scope.uncleCountChart, data.uncleCount) && data.uncleCount.length >= MAX_BINS) { + $scope.uncleCount = data.uncleCount[data.uncleCount.length - 2] + data.uncleCount[data.uncleCount.length - 1]; + $scope.uncleCountChart = data.uncleCount; + } - case "blockPropagationChart": - $scope.blockPropagationChart = data.histogram; - $scope.blockPropagationAvg = data.avg; + if (!_.isEqual($scope.transactionDensity, data.transactions) && data.transactions.length >= MAX_BINS) + $scope.transactionDensity = data.transactions; - break; + if (!_.isEqual($scope.gasSpending, data.gasSpending) && data.gasSpending.length >= MAX_BINS) + $scope.gasSpending = data.gasSpending; - case "uncleCount": - $scope.uncleCount = data[0] + data[1]; - data.reverse(); - $scope.uncleCountChart = data; + if (!_.isEqual($scope.miners, data.miners)) { + $scope.miners = data.miners; + getMinersNames(); + } - break; + break; - case "charts": - if( !_.isEqual($scope.avgBlockTime, data.avgBlocktime) ) - $scope.avgBlockTime = data.avgBlocktime; + case "inactive": + var index = findIndex({ id: data.id }); - if( !_.isEqual($scope.avgHashrate, data.avgHashrate) ) - $scope.avgHashrate = data.avgHashrate; + if (index >= 0) { + if (!_.isUndefined(data.stats)) + $scope.nodes[index].stats = data.stats; - if( !_.isEqual($scope.lastGasLimit, data.gasLimit) && data.gasLimit.length >= MAX_BINS ) - $scope.lastGasLimit = data.gasLimit; + // toastr['error']("Node "+ $scope.nodes[index].info.name +" went away!", "Node connection was lost!"); - if( !_.isEqual($scope.lastBlocksTime, data.blocktime) && data.blocktime.length >= MAX_BINS ) - $scope.lastBlocksTime = data.blocktime; + updateActiveNodes(); + } - if( !_.isEqual($scope.difficultyChart, data.difficulty) && data.difficulty.length >= MAX_BINS ) - $scope.difficultyChart = data.difficulty; + break; - if( !_.isEqual($scope.blockPropagationChart, data.propagation.histogram) ) { - $scope.blockPropagationChart = data.propagation.histogram; - $scope.blockPropagationAvg = data.propagation.avg; - } + case "latency": + if (!_.isUndefined(data.id) && !_.isUndefined(data.latency)) { + var index = findIndex({ id: data.id }); - data.uncleCount.reverse(); + if (index >= 0) { + var node = $scope.nodes[index]; - if( !_.isEqual($scope.uncleCountChart, data.uncleCount) && data.uncleCount.length >= MAX_BINS ) { - $scope.uncleCount = data.uncleCount[data.uncleCount.length-2] + data.uncleCount[data.uncleCount.length-1]; - $scope.uncleCountChart = data.uncleCount; - } + if (!_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) && node.stats.latency !== data.latency) { + node.stats.latency = data.latency; + latencyFilter(node); + } + } + } - if( !_.isEqual($scope.transactionDensity, data.transactions) && data.transactions.length >= MAX_BINS ) - $scope.transactionDensity = data.transactions; + break; - if( !_.isEqual($scope.gasSpending, data.gasSpending) && data.gasSpending.length >= MAX_BINS ) - $scope.gasSpending = data.gasSpending; + case "client-ping": + socket.emit('client-pong', { + serverTime: data.serverTime, + clientTime: _.now() + }); - if( !_.isEqual($scope.miners, data.miners) ) { - $scope.miners = data.miners; - getMinersNames(); - } + break; + } - break; + // $scope.$apply(); + } - case "inactive": - var index = findIndex({id: data.id}); + function findIndex(search) { + return _.findIndex($scope.nodes, search); + } - if( index >= 0 ) - { - if( !_.isUndefined(data.stats) ) - $scope.nodes[index].stats = data.stats; + function getMinersNames() { + if ($scope.miners.length > 0) { + _.forIn($scope.miners, function (value, key) { + if (value.name !== false) + return; - // toastr['error']("Node "+ $scope.nodes[index].info.name +" went away!", "Node connection was lost!"); + if (value.miner === "0x0000000000000000000000000000000000000000") + return; - updateActiveNodes(); - } + var name = _.result(_.find(_.pluck($scope.nodes, 'info'), 'coinbase', value.miner), 'name'); - break; + if (!_.isUndefined(name)) + $scope.miners[key].name = name; + }); + } + } - case "latency": - if( !_.isUndefined(data.id) && !_.isUndefined(data.latency) ) - { - var index = findIndex({id: data.id}); + function addNewNode(data) { + var index = findIndex({ id: data.id }); - if( index >= 0 ) - { - var node = $scope.nodes[index]; + if (_.isUndefined(data.history)) { + data.history = new Array(40); + _.fill(data.history, -1); + } - if( !_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) && node.stats.latency !== data.latency ) - { - node.stats.latency = data.latency; - latencyFilter(node); - } - } - } + if (index < 0) { + if (!_.isUndefined(data.stats) && _.isUndefined(data.stats.hashrate)) { + data.stats.hashrate = 0; + } - break; + data.pinned = false; - case "client-ping": - socket.emit('client-pong', { - serverTime: data.serverTime, - clientTime: _.now() - }); + $scope.nodes.push(data); - break; - } + return true; + } - // $scope.$apply(); - } + data.pinned = (!_.isUndefined($scope.nodes[index].pinned) ? $scope.nodes[index].pinned : false); - function findIndex(search) - { - return _.findIndex($scope.nodes, search); - } - - function getMinersNames() - { - if( $scope.miners.length > 0 ) - { - _.forIn($scope.miners, function (value, key) - { - if(value.name !== false) - return; - - if(value.miner === "0x0000000000000000000000000000000000000000") - return; - - var name = _.result(_.find(_.pluck($scope.nodes, 'info'), 'coinbase', value.miner), 'name'); - - if( !_.isUndefined(name) ) - $scope.miners[key].name = name; - }); - } - } - - function addNewNode(data) - { - var index = findIndex({id: data.id}); - - if( _.isUndefined(data.history) ) - { - data.history = new Array(40); - _.fill(data.history, -1); - } - - if( index < 0 ) - { - if( !_.isUndefined(data.stats) && _.isUndefined(data.stats.hashrate) ) - { - data.stats.hashrate = 0; - } - - data.pinned = false; - - $scope.nodes.push(data); - - return true; - } - - data.pinned = ( !_.isUndefined($scope.nodes[index].pinned) ? $scope.nodes[index].pinned : false); - - if( !_.isUndefined($scope.nodes[index].history) ) - { - data.history = $scope.nodes[index].history; - } - - $scope.nodes[index] = data; - - updateActiveNodes(); - - return false; - } - - function updateActiveNodes() - { - updateBestBlock(); - - $scope.nodesTotal = $scope.nodes.length; - - $scope.nodesActive = _.filter($scope.nodes, function (node) { - // forkFilter(node); - return node.stats.active == true; - }).length; - - $scope.upTimeTotal = _.reduce($scope.nodes, function (total, node) { - return total + node.stats.uptime; - }, 0) / $scope.nodes.length; - - $scope.map = _.map($scope.nodes, function (node) { - var fill = $filter('bubbleClass')(node.stats, $scope.bestBlock); - - if(node.geo != null) - return { - radius: 3, - latitude: node.geo.ll[0], - longitude: node.geo.ll[1], - nodeName: node.info.name, - fillClass: "text-" + fill, - fillKey: fill, - }; - else - return { - radius: 0, - latitude: 0, - longitude: 0 - }; - }); - } - - function updateBestBlock() - { - if( $scope.nodes.length ) - { - var chains = {}; - var maxScore = 0; - - // _($scope.nodes) - // .map(function (item) - // { - // maxScore += (item.trusted ? 50 : 1); - - // if( _.isUndefined(chains[item.stats.block.number]) ) - // chains[item.stats.block.number] = []; - - // if( _.isUndefined(chains[item.stats.block.number][item.stats.block.fork]) ) - // chains[item.stats.block.number][item.stats.block.fork] = { - // fork: item.stats.block.fork, - // count: 0, - // trusted: 0, - // score: 0 - // }; - - // if(item.stats.block.trusted) - // chains[item.stats.block.number][item.stats.block.fork].trusted++; - // else - // chains[item.stats.block.number][item.stats.block.fork].count++; - - // chains[item.stats.block.number][item.stats.block.fork].score = chains[item.stats.block.number][item.stats.block.fork].trusted * 50 + chains[item.stats.block.number][item.stats.block.fork].count; - // }) - // .value(); - - // $scope.maxScore = maxScore; - // $scope.chains = _.reduce(chains, function (result, item, key) - // { - // result[key] = _.max(item, 'score'); - // return result; - // }, {}); - - var bestBlock = _.max($scope.nodes, function (node) - { - // if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 ) - // { - return parseInt(node.stats.block.number); - // } - - // return 0; - }).stats.block.number; - - if( bestBlock !== $scope.bestBlock ) - { - $scope.bestBlock = bestBlock; - $scope.bestStats = _.max($scope.nodes, function (node) { - return parseInt(node.stats.block.number); - }).stats; - - $scope.lastBlock = $scope.bestStats.block.arrived; - $scope.lastDifficulty = $scope.bestStats.block.difficulty; - } - } - } - - // function forkFilter(node) - // { - // if( _.isUndefined(node.readable) ) - // node.readable = {}; - - // node.readable.forkClass = 'hidden'; - // node.readable.forkMessage = ''; - - // return true; - - // if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 ) - // { - // node.readable.forkClass = 'hidden'; - // node.readable.forkMessage = ''; - - // return true; - // } - - // if( $scope.chains[node.stats.block.number].fork !== node.stats.block.fork ) - // { - // node.readable.forkClass = 'text-danger'; - // node.readable.forkMessage = 'Wrong chain.
This chain is a fork.'; - - // return false; - // } - - // if( $scope.chains[node.stats.block.number].score / $scope.maxScore < 0.5) - // { - // node.readable.forkClass = 'text-warning'; - // node.readable.forkMessage = 'May not be main chain.
Waiting for more confirmations.'; - - // return false; - // } - // } - - function latencyFilter(node) - { - if( _.isUndefined(node.readable) ) - node.readable = {}; - - if( _.isUndefined(node.stats) ) { - node.readable.latencyClass = 'text-danger'; - node.readable.latency = 'offline'; - } - - if (node.stats.active === false) - { - node.readable.latencyClass = 'text-danger'; - node.readable.latency = 'offline'; - } - else - { - if (node.stats.latency <= 100) - node.readable.latencyClass = 'text-success'; - - if (node.stats.latency > 100 && node.stats.latency <= 1000) - node.readable.latencyClass = 'text-warning'; - - if (node.stats.latency > 1000) - node.readable.latencyClass = 'text-danger'; - - node.readable.latency = node.stats.latency + ' ms'; - } - } - - // very simple xss filter - function xssFilter(obj){ - if(_.isArray(obj)) { - return _.map(obj, xssFilter); - - } else if(_.isObject(obj)) { - return _.mapValues(obj, xssFilter); - - } else if(_.isString(obj)) { - return obj.replace(/\< *\/* *script *>*/gi,'').replace(/javascript/gi,''); - } else - return obj; - } + if (!_.isUndefined($scope.nodes[index].history)) { + data.history = $scope.nodes[index].history; + } + + $scope.nodes[index] = data; + + updateActiveNodes(); + + return false; + } + + function updateActiveNodes() { + updateBestBlock(); + + $scope.nodesTotal = $scope.nodes.length; + + $scope.nodesActive = _.filter($scope.nodes, function (node) { + // forkFilter(node); + return node.stats.active == true; + }).length; + + $scope.upTimeTotal = _.reduce($scope.nodes, function (total, node) { + return total + node.stats.uptime; + }, 0) / $scope.nodes.length; + + $scope.map = _.map($scope.nodes, function (node) { + var fill = $filter('bubbleClass')(node.stats, $scope.bestBlock); + + if (node.geo != null) + return { + radius: 3, + latitude: node.geo.ll[0], + longitude: node.geo.ll[1], + nodeName: node.info.name, + fillClass: "text-" + fill, + fillKey: fill, + }; + else + return { + radius: 0, + latitude: 0, + longitude: 0 + }; + }); + } + + function updateBestBlock() { + if ($scope.nodes.length) { + var bestBlock = _.max($scope.nodes, function (node) { + return parseInt(node.stats.block.number); + }).stats.block.number; + + if (bestBlock !== $scope.bestBlock) { + $scope.bestBlock = bestBlock; + $scope.bestStats = _.max($scope.nodes, function (node) { + return parseInt(node.stats.block.number); + }).stats; + + $scope.lastBlock = $scope.bestStats.block.arrived; + $scope.lastDifficulty = $scope.bestStats.block.difficulty; + } + } + } + + function latencyFilter(node) { + if (_.isUndefined(node.readable)) + node.readable = {}; + + if (_.isUndefined(node.stats)) { + node.readable.latencyClass = 'text-danger'; + node.readable.latency = 'offline'; + } + + if (node.stats.active === false) { + node.readable.latencyClass = 'text-danger'; + node.readable.latency = 'offline'; + } else { + if (node.stats.latency <= 100) + node.readable.latencyClass = 'text-success'; + + if (node.stats.latency > 100 && node.stats.latency <= 1000) + node.readable.latencyClass = 'text-warning'; + + if (node.stats.latency > 1000) + node.readable.latencyClass = 'text-danger'; + + node.readable.latency = node.stats.latency + ' ms'; + } + } + + // very simple xss filter + function xssFilter(obj) { + if (_.isArray(obj)) { + return _.map(obj, xssFilter); + + } else if (_.isObject(obj)) { + return _.mapValues(obj, xssFilter); + + } else if (_.isString(obj)) { + return obj.replace(/\< *\/* *script *>*/gi, '').replace(/javascript/gi, ''); + } else + return obj; + } }); \ No newline at end of file diff --git a/src/js/filters.js b/src/js/filters.js index fdbced8d..9ed5feda 100644 --- a/src/js/filters.js +++ b/src/js/filters.js @@ -179,7 +179,22 @@ angular.module('netStatsApp.filters', []) if(hash.substr(0,2) === '0x') hash = hash.substr(2,64); - return hash.substr(0, 8) + '...' + hash.substr(56, 8); + return hash.substr(0, 8) + '..' + hash.substr(56, 8); + } +}) +.filter('nameFilter', function() { + return function(name) { + if(typeof name === 'undefined') + return "?"; + return name.substr(0, 30) + '..'; + } +}) +.filter('addressFilter', function() { + return function(address) { + if(typeof address === 'undefined') + return "?"; + + return address.substr(0, 10) + '..'; } }) .filter('timeClass', function() { @@ -392,6 +407,11 @@ angular.module('netStatsApp.filters', []) return blockTimeClass(time); } }) +.filter('blocksInEpochClass', function() { + return function(blocks, epochSize) { + return blockTimeClass(Math.round(40*(1-blocks/epochSize))); + } +}) .filter('upTimeFilter', function() { return function(uptime) { return Math.round(uptime) + '%'; diff --git a/src/views/index.jade b/src/views/index.jade index eb9f4904..ffbfad34 100644 --- a/src/views/index.jade +++ b/src/views/index.jade @@ -30,13 +30,21 @@ block content span.big-details {{ avgBlockTime | avgTimeFilter }} div.clearfix div.col-xs-2.stat-holder - div.big-info.blockremain.text-info + div.big-info.blockremain(class="{{ bestStats.block.blockRemain | blocksInEpochClass : bestStats.block.epochSize}}") div.pull-left.icon-full-width i.icon-block div.big-details-holder span.small-title blocks until epoch span.big-details {{ bestStats.block.blockRemain | number }} div.clearfix + div.col-xs-2.stat-holder + div.big-info.blockremain.text-success + div.pull-left.icon-full-width + i.icon-mining + div.big-details-holder + span.small-title elected validators + span.big-details {{ validators.elected.length | number }} + div.clearfix div.clearfix div.row(ng-cloak) @@ -58,10 +66,15 @@ block content span.small-title gas limit span.small-value {{ bestStats.block.gasLimit }} gas div.col-xs-2.stat-holder.box - div.gasprice.text-info + div.gasprice.text-warning i.icon-block span.small-title epoch size span.small-value {{ bestStats.block.epochSize }} + div.col-xs-2.stat-holder.box + div.gasprice.text-warning + i.icon-check + span.small-title registered validators + span.small-value {{ validators.registered.length }} div.row(ng-cloak) div.col-xs-2.stat-holder @@ -89,9 +102,9 @@ block content sparkchart.big-details.spark-difficulty(data="{{lastGasLimit.join(',')}}") div.clearfix div.col-xs-2.stat-holder.xpull-right - div.big-info.chart.xdouble-chart.text-danger - //- i.icon-hashrate - span.small-title recent validators + div.big-info.chart.xdouble-chart.text-success + i.icon-mining + span.small-title recent block proposers div.blocks-holder(ng-repeat='miner in miners track by miner.miner', data-toggle="tooltip", data-placement="right", data-original-title="{{miner.blocks}}") div.block-count(class="{{miner.blocks | minerBlocksClass : 'text-'}}") {{miner.blocks}} div.small-title-miner {{miner.miner | minerNameFilter : miner.name}} @@ -107,12 +120,16 @@ block content i.icon-check-o(data-toggle="tooltip", data-placement="top", title="Pin nodes to display first", ng-click="orderTable(['-stats.block.number', 'stats.block.propagation'], false)") th.th-nodename i.icon-node(data-toggle="tooltip", data-placement="top", title="Node name", ng-click="orderTable(['info.name'], false)") - th.th-nodetype - i.icon-laptop(data-toggle="tooltip", data-placement="top", title="Node type", ng-click="orderTable(['info.node'], false)") th.th-latency i.icon-clock(data-toggle="tooltip", data-placement="top", title="Node latency", ng-click="orderTable(['stats.latency'], false)") + th.th-nodeaddress + i.icon-node(data-toggle="tooltip", data-placement="top", title="Node address", ng-click="orderTable(['address'], false)") + th.th-nodetype + i.icon-group(data-toggle="tooltip", data-placement="top", title="Validator Group", ng-click="orderTable(['validatorData.affiliation'], false)") + th + i.icon-check(data-toggle="tooltip", data-placement="top", title="Validating", ng-click="orderTable(['-stats.hashrate'], false)") th - i.icon-check(data-toggle="tooltip", data-placement="top", title="Is validating", ng-click="orderTable(['-stats.hashrate'], false)") + i.icon-mining(data-toggle="tooltip", data-placement="top", title="Elected", ng-click="orderTable(['-stats.hashrate'], false)") th i.icon-group(data-toggle="tooltip", data-placement="top", title="Peers", ng-click="orderTable(['-stats.peers'], false)") th @@ -136,21 +153,19 @@ block content td.td-nodecheck i(ng-click="pinNode(node.id)", class="{{ node.pinned | nodePinClass }}", data-toggle="tooltip", data-placement="right", data-original-title="Click to {{ node.pinned ? 'un' : '' }}pin") td.nodeInfo(rel="{{node.id}}") - span.small(data-toggle="tooltip", data-placement="top", data-html="true", data-original-title="{{node | geoTooltip}}") {{node.info.name}} - //- span.small  ({{node.info.ip}}) - a.small(href="https://github.com/ethereum/wiki/wiki/Network-Status#updating", target="_blank", data-toggle="tooltip", data-placement="top", data-html="true", data-original-title="Netstats client needs update.
Click this icon for instructions.", class="{{ node.info | nodeClientClass : currentApiVersion }}") - i.icon-warning-o - td - div.small(ng-bind-html="node.info.node | nodeVersion") + span.small(data-toggle="tooltip", data-placement="top", data-html="true", data-original-title="{{node | geoTooltip}}") {{node.info.name | nameFilter}} td(class="{{ node.readable.latencyClass }}") span.small {{ node.readable.latency }} + td.nodeInfo(rel="{{node.address}}") + span.small {{node.address | addressFilter }} + td.nodeInfo(rel="{{node.validatorData.affiliation}}") + span.small {{ node.validatorData.affiliation || "no affiliation" | nameFilter}} td(class="{{ node.stats.mining | hashrateClass : node.stats.active }}", ng-bind-html="node.stats.hashrate | stakingFilter : node.stats.mining") + td(class="{{ node.stats.elected | hashrateClass : node.stats.elected }}", ng-bind-html="node.stats.elected | stakingFilter : node.stats.elected") td(class="{{ node.stats.peers | peerClass : node.stats.active }}", style="padding-left: 11px;") {{node.stats.peers}} td(style="padding-left: 15px;") {{node.stats.pending}} td(class="{{ node.stats | blockClass : bestBlock }}") span(class="{{ node.readable.forkMessage ? node.readable.forkClass : '' }}") {{'#'}}{{ node.stats.block.number | number }} - //- a.small(data-toggle="tooltip", data-placement="top", data-html="true", data-original-title="{{ node.readable.forkMessage }}", class="{{ node.readable.forkClass }}") - i.icon-warning-o td(class="{{ node.stats | blockClass : bestBlock }}") span.small {{node.stats.block.hash | hashFilter}} td(style="padding-left: 14px;") {{node.stats.block.transactions.length || 0}}