diff --git a/package.json b/package.json index 9564c9f91..6dab30b17 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "concurrently": "^3.5.1", "ejs": "~2.5.7", "express": "^4.16.0", + "lodash": "^4.17.10", "mongoose": "^4.10.8", "morgan": "^1.9.0", "rlp": "^2.0.0", diff --git a/public/js/controllers/StatsController.js b/public/js/controllers/StatsController.js index 1c0e0b8c1..ddccecc15 100755 --- a/public/js/controllers/StatsController.js +++ b/public/js/controllers/StatsController.js @@ -22,6 +22,9 @@ angular.module('BlocksApp').controller('StatsController', function($stateParams, "miner_hashrate": { "title": "Miner Hashrate Distribution" }, + "supply": { + "title": "Total Supply" + }, "tx": { "title": "Transaction chart" }, @@ -228,6 +231,216 @@ angular.module('BlocksApp').controller('StatsController', function($stateParams, }); + } + } + } + }; +}) +.directive('totalSupply', function($http) { + return { + restrict: 'E', + template: '', + scope: true, + link: function(scope, elem, attrs) { + scope.stats = {}; + var statsURL = "/supply"; + + $http.post(statsURL) + .then(function(res) { + var dataset = []; + var total = 0; + Object.keys(res.data).forEach(function(k) { + if (k === 'totalSupply') { + total = res.data[k]; + } else if (k === 'genesisAlloc' && typeof res.data[k] === 'object') { + Object.keys(res.data[k]).forEach(function(kk) { + if (kk !== 'total') { + var d = { _id: kk, amount: res.data[k][kk] }; + dataset.push(d); + } + }); + } else if (k !== 'height') { + var d = { _id: k, amount: res.data[k] }; + dataset.push(d); + } + }); + + var data = _.sortBy(dataset, function(d) { + return d.amount * 1.0; + }); + scope.init(data, total, "#totalSupply"); + }); + + /** + * Created by chenxiangyu on 2016/8/5. + * slightly modified to show total supply pie chart. + */ + scope.init = function(dataset, total, chartid) { + var svg = d3.select(chartid) + .append("g"); + + + svg.append("g") + .attr("class", "slices"); + svg.append("g") + .attr("class", "labelName"); + svg.append("g") + .attr("class", "labelValue"); + svg.append("g") + .attr("class", "lines"); + + var width = parseInt(d3.select(chartid).style("width")); + var height = parseInt(d3.select(chartid).style("height")); + + // fix for mobile layout + var radius; + if (window.innerWidth < 800) { + radius = Math.min(width, 450) * 0.6; + } else { + radius = 450 * 0.5; + } + + var pie = d3.layout.pie() + .sort(null) + .value(function (d) { + //return d.value; + //console.log(d); + return d.amount; + }); + + var arc = d3.svg.arc() + .outerRadius(radius * 0.8) + .innerRadius(radius * 0.4); + + var outerArc = d3.svg.arc() + .innerRadius(radius * 0.9) + .outerRadius(radius * 0.9); + + var legendRectSize = (radius * 0.05); + var legendSpacing = radius * 0.02; + + var maxMiners = 23; + if (window.innerWidth < 800) { + var legendHeight = legendRectSize + legendSpacing; + var fixHeight = Math.min(maxMiners, dataset.length) * legendHeight; + fixHeight = height + parseInt(fixHeight) + 50; + d3.select(chartid).attr("height", fixHeight + 'px'); + } + + var div = d3.select("body").append("div").attr("class", "toolTip"); + + // fix for mobile layout + if (window.innerWidth < 800) { + svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); + } else { + svg.attr("transform", "translate(" + 200 + "," + 200 + ")"); + } + + var colorRange = d3.scale.category10(); + var color = d3.scale.ordinal() + .range(colorRange.range()); + + change(dataset); + + + d3.selectAll("input") + .on("change", selectDataset); + + function selectDataset() { + var value = this.value; + if (value == "total") { + change(datasetTotal); + } + } + + function change(data) { + /* ------- PIE SLICES -------*/ + var slice = svg.select(".slices").selectAll("path.slice") + .data(pie(dataset), function (d) { + //return d.data.label + //console.log(d); + return d.data._id; + }); + + slice.enter() + .insert("path") + .style("fill", function (d) { + return color(d.data._id); + }) + .attr("class", "slice"); + + slice + .transition().duration(1000) + .attrTween("d", function (d) { + this._current = this._current || d; + var interpolate = d3.interpolate(this._current, d); + this._current = interpolate(0); + return function (t) { + return arc(interpolate(t)); + }; + }) + slice + .on("mousemove", function (d) { + div.style("left", d3.event.pageX + 10 + "px"); + div.style("top", d3.event.pageY - 25 + "px"); + div.style("display", "inline-block"); + div.html((d.data._id) + "
" + (d.data.amount) + "
(" + d3.format(".2%")(d.data.amount / total) + ")"); + }); + slice + .on("mouseout", function (d) { + div.style("display", "none"); + }); + + slice.exit() + .remove(); + + //console.log(data.length); + + var legendHeight = Math.min(maxMiners, color.domain().length); + var legend = svg.selectAll('.legend') + //.data(color.domain()) + .data(data) + .enter() + .append('g') + .attr('class', 'legend') + .attr('transform', function (d, i) { + if (data.length - i >= maxMiners) { + // show maxMiners, hide remains + return 'translate(2000,0)'; + } + var height = legendRectSize + legendSpacing; + var offset = height * legendHeight / 2; + var horz = -3 * legendRectSize; + var vert = (data.length - i) * height; + var tx, ty; + if (window.innerWidth > 800) { + tx = 250; + ty = vert - offset; + } else { + tx = - radius * 0.8; + ty = vert + radius; + } + return 'translate(' + tx + ',' + ty + ')'; + }); + + legend.append('rect') + .attr('width', legendRectSize) + .attr('height', legendRectSize) + .style('fill', function (d,i) { + //console.log(i); + return color(d._id); + }); + + legend.append('text') + + .attr('x', legendRectSize + legendSpacing) + .attr('y', legendRectSize - legendSpacing) + .text(function (d) { + //console.log(d); + return d._id; + }); + + } } } diff --git a/public/tpl/header.html b/public/tpl/header.html index eb39f0870..75bcddacb 100755 --- a/public/tpl/header.html +++ b/public/tpl/header.html @@ -112,6 +112,10 @@ Transaction Chart +
  • + + Total Supply +
  • The bomb chart diff --git a/public/views/stats/index.html b/public/views/stats/index.html index 97e8ed4d2..1b7ffdf3d 100644 --- a/public/views/stats/index.html +++ b/public/views/stats/index.html @@ -8,6 +8,7 @@ + diff --git a/routes/index.js b/routes/index.js index e2b8b3680..7db11cff3 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,7 +4,22 @@ var Block = mongoose.model( 'Block' ); var Transaction = mongoose.model( 'Transaction' ); var filters = require('./filters'); +var _ = require('lodash'); var async = require('async'); +var BigNumber = require('bignumber.js'); + +var config = {}; +try { + config = require('../config.json'); +} catch(e) { + if (e.code == 'MODULE_NOT_FOUND') { + console.log('No config file found. Using default configuration... (tools/config.json)'); + config = require('../tools/config.json'); + } else { + throw e; + process.exit(1); + } +} module.exports = function(app){ var web3relay = require('./web3relay'); @@ -35,6 +50,8 @@ module.exports = function(app){ app.post('/fiat', fiat); app.post('/stats', stats); + app.post('/supply', getTotalSupply); + app.get('/supply', getTotalSupply); } var getAddr = function(req, res){ @@ -120,6 +137,152 @@ var getBlock = function(req, res) { res.end(); }); }; + +/** + * calc totalSupply + * total supply = genesis alloc + miner rewards + estimated uncle rewards + */ +var getTotalSupply = function(req, res) { + Block.findOne({}).lean(true).sort('-number').exec(function (err, latest) { + if(err || !latest) { + console.error("getTotalSupply error: " + err) + res.write(JSON.stringify({"error": true})); + res.end(); + } else { + console.log("getTotalSupply: latest block: " + latest.number); + var blockNumber = latest.number; + + var total = new BigNumber(0); + var genesisAlloc = new BigNumber(0); + var blocks = []; + + var rewards = { + enableECIP1017: true, + estimateUncle: 0.054, /* true: aggregate db // number(fractioal value): uncle rate // false: disable */ + genesisAlloc: 72009990.50, + blocks: [ + /* will be regeneragted later for ECIP1017 enabled case */ + { start: 1, reward: 5e+18, uncle: 0.90625 }, + { start: 5000001, reward: 4e+18, uncle: 0.0625 }, + { start: 10000001, reward: 4e+18, uncle: 0.0625 }, + ] + }; + + if (config.rewards) { + _.extend(rewards, config.rewards); + } + + if (rewards && rewards.blocks) { + // get genesis alloc + if (typeof rewards.genesisAlloc === "object") { + genesisAlloc = new BigNumber(rewards.genesisAlloc.total) || new BigNumber(0); + } else { + genesisAlloc = new BigNumber(rewards.genesisAlloc) || new BigNumber(0); + } + genesisAlloc = genesisAlloc.times(new BigNumber(1e+18)); + + if (rewards.enableECIP1017) { + // regenerate reward block config for ETC + // https://github.com/ethereumproject/ECIPs/blob/master/ECIPs/ECIP-1017.md + var reward = new BigNumber(5e+18); + var uncleRate = new BigNumber(1).div(32).plus(new BigNumber(7).div(8)); // 1/32(block miner) + 7/8(uncle miner) + blocks.push({start: 1, end: 5000000, reward, uncle: uncleRate}); + + reward = reward.times(0.8); // reduce 20% + uncleRate = new BigNumber(1).div(32).times(2); // 1/32(block miner) + 1/32(uncle miner) + blocks.push({start: 5000001, end: 10000000, reward, uncle: uncleRate}); + currentBlock = 10000001; + var i = 2; + var lastBlock = blockNumber; + for (; lastBlock > currentBlock; currentBlock += 5000000) { + var start = blocks[i - 1].end + 1; + var end = start + 5000000 - 1; + reward = reward.times(0.8); // reduce 20% + blocks.push({start, end, reward, uncle: blocks[i - 1].uncle}); + i++; + } + rewards.blocks = blocks; + blocks = []; + } + + // check reward blocks, calc total miner's reward + rewards.blocks.forEach(function(block, i) { + if (blockNumber > block.start) { + var startBlock = block.start; + if (startBlock < 0) { + startBlock = 0; + } + var endBlock = blockNumber; + var reward = new BigNumber(block.reward); + if (rewards.blocks[i + 1] && blockNumber > rewards.blocks[i + 1].start) { + endBlock = rewards.blocks[i + 1].start - 1; + } + blocks.push({start: startBlock, end: endBlock, reward: reward, uncle: block.uncle }); + + var blockNum = endBlock - startBlock; + total = total.plus(reward.times(new BigNumber(blockNum))); + } + }); + } + + var totalSupply = total.plus(genesisAlloc); + var ret = { "height": blockNumber, "totalSupply": totalSupply.div(1e+18), "genesisAlloc": genesisAlloc.div(1e+18), "minerRewards": total.div(1e+18) }; + if (req.method === 'POST' && typeof rewards.genesisAlloc === 'object') { + ret.genesisAlloc = rewards.genesisAlloc; + } + + // estimate uncleRewards + var uncleRewards = []; + if (typeof rewards.estimateUncle === 'boolean' && rewards.estimateUncle && blocks.length > 0) { + // aggregate uncle blocks (slow) + blocks.forEach(function(block) { + Block.aggregate([ + { $match: { number: { $gte: block.start, $lt: block.end } } }, + { $group: { _id: null, uncles: { $sum: { $size: "$uncles" } } } } + ]).exec(function(err, results) { + if (err) { + console.log(err); + } + if (results && results[0] && results[0].uncles) { + // estimate Uncle Rewards + var reward = block.reward.times(new BigNumber(results[0].uncles)).times(block.uncle); + uncleRewards.push(reward); + } + if (uncleRewards.length === blocks.length) { + var totalUncleRewards = new BigNumber(0); + uncleRewards.forEach(function(reward) { + totalUncleRewards = totalUncleRewards.plus(reward); + }); + ret.uncleRewards = totalUncleRewards.div(1e+18); + ret.totalSupply = totalSupply.plus(totalUncleRewards).div(1e+18); + res.write(JSON.stringify(ret)); + res.end(); + } + }); + }); + } else if (typeof rewards.estimateUncle === 'number' && rewards.estimateUncle > 0) { + // estimate Uncle rewards with uncle probability. (faster) + blocks.forEach(function(block) { + var blockcount = block.end - block.start; + var reward = block.reward.times(new BigNumber(blockcount).times(rewards.estimateUncle)).times(block.uncle); + uncleRewards.push(reward); + }); + var totalUncleRewards = new BigNumber(0); + uncleRewards.forEach(function(reward) { + totalUncleRewards = totalUncleRewards.plus(reward); + }); + ret.uncleRewards = totalUncleRewards.div(1e+18); + ret.totalSupply = totalSupply.plus(totalUncleRewards).div(1e+18); + res.write(JSON.stringify(ret)); + res.end(); + } else { + res.write(JSON.stringify(ret)); + res.end(); + } + } + }); +}; + var getTx = function(req, res){ var tx = req.body.tx.toLowerCase(); var txFind = Block.findOne( { "transactions.hash" : tx }, "transactions timestamp")