From f6f78efbf3414dd1c7cdb9c0eea7773a3d822461 Mon Sep 17 00:00:00 2001
From: hackyminer <hackyminer@gmail.com>
Date: Mon, 9 Jul 2018 13:21:49 +0900
Subject: [PATCH] support totalSupply stats

---
 db.js                                    |   1 +
 package.json                             |   1 +
 public/js/controllers/StatsController.js | 213 +++++++++++++++++++++++
 public/tpl/header.html                   |   4 +
 public/views/stats/index.html            |   1 +
 routes/index.js                          | 167 ++++++++++++++++++
 6 files changed, 387 insertions(+)

diff --git a/db.js b/db.js
index 2dd3bab92..25e060fca 100644
--- a/db.js
+++ b/db.js
@@ -67,6 +67,7 @@ var BlockStat = new Schema(
 });
 
 // create indices
+Transaction.index({timestamp:-1});
 Transaction.index({blockNumber:-1});
 Transaction.index({from:1, blockNumber:-1});
 Transaction.index({to:1, blockNumber:-1});
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: '<svg id="totalSupply" width="100%" height="500px"></svg>',
+    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) + "<br>" + (d.data.amount) + "<br>(" + 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 @@
                                                 <a href="/stats/tx">
                                                     <i class="fa fa-line-chart"></i> Transaction Chart</a>
                                             </li>
+                                            <li>
+                                                <a href="/stats/supply">
+                                                    <i class="fa fa-line-chart"></i> Total Supply</a>
+                                            </li>
                                             <li>
                                                 <a href="/stats/The_bomb_chart">
                                                     <i class="fa fa-line-chart"></i> The bomb chart</a>
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 @@
             <blocktime-chart ng-if="chart == 'blocktime'"></blocktime-chart>
             <difficulty-chart ng-if="chart == 'difficulty'"></difficulty-chart>
             <transaction-chart ng-if="chart == 'tx'"></transaction-chart>
+            <total-supply ng-if="chart == 'supply'"></total-supply>
             <etc-the-bomb-chart ng-if="chart == 'The_bomb_chart'"></etc-the-bomb-chart>
           </div>
         </div>
diff --git a/routes/index.js b/routes/index.js
index e2b8b3680..de0e8722a 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,156 @@ 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 = {
+        enableEIP1017: true,
+        estimateUncle: 0.054, /* true: aggregate db // number(fractioal value): uncle rate // false: disable */
+        genesisAlloc: 72009990.50,
+        blocks: [
+          /* regeneragted for EIP1017 case */
+          { start:        1, reward: 5e+18, uncle: 0.90625 },
+          { start:  5000000, reward: 4e+18, uncle:  0.0625 },
+          { start: 10000000, 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.enableEIP1017) {
+          // 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)
+          console.log(uncleRate.toString());
+          blocks.push({start: 1, end: 4999999, 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)
+          console.log(uncleRate.toString());
+          blocks.push({start: 5000000, end: 9999999, reward, uncle: uncleRate});
+          currentBlock = 10000000;
+          var i = 2;
+          var lastBlock = blockNumber;
+          lastBlock = 200000000;
+          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:  0.0625});
+            i++;
+          }
+          rewards.blocks = blocks;
+          console.log(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")