From 885948543b79739a4db44ee3250e7c29067e0d24 Mon Sep 17 00:00:00 2001 From: hackyminer Date: Sat, 11 Aug 2018 07:44:59 +0900 Subject: [PATCH 01/17] support richlist * Account db added. * dynamic update accounts --- db.js | 12 +++ public/js/controllers/AccountsController.js | 72 ++++++++++++++ public/js/main.js | 21 ++++ public/views/accounts.html | 37 +++++++ routes/index.js | 2 + routes/richlist.js | 102 ++++++++++++++++++++ tools/sync.js | 76 ++++++++++++++- 7 files changed, 321 insertions(+), 1 deletion(-) create mode 100755 public/js/controllers/AccountsController.js create mode 100755 public/views/accounts.html create mode 100644 routes/richlist.js diff --git a/db.js b/db.js index 5496c1659..1d7a765ed 100644 --- a/db.js +++ b/db.js @@ -24,6 +24,14 @@ var Block = new Schema( "uncles": [String] }); +var Account = new Schema( +{ + "address": {type: String, index: {unique: true}}, + "balance": Number, + "blockNumber": Number, + "type": Number // address: 0x0, contract: 0x1 +}); + var Contract = new Schema( { "address": {type: String, index: {unique: true}}, @@ -70,16 +78,20 @@ var BlockStat = new Schema( Transaction.index({blockNumber:-1}); Transaction.index({from:1, blockNumber:-1}); Transaction.index({to:1, blockNumber:-1}); +Account.index({balance:-1}); +Account.index({balance:-1, blockNumber:-1}); Block.index({miner:1}); mongoose.model('BlockStat', BlockStat); mongoose.model('Block', Block); +mongoose.model('Account', Account); mongoose.model('Contract', Contract); mongoose.model('Transaction', Transaction); module.exports.BlockStat = mongoose.model('BlockStat'); module.exports.Block = mongoose.model('Block'); module.exports.Contract = mongoose.model('Contract'); module.exports.Transaction = mongoose.model('Transaction'); +module.exports.Account = mongoose.model('Account'); mongoose.Promise = global.Promise; mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/blockDB', { diff --git a/public/js/controllers/AccountsController.js b/public/js/controllers/AccountsController.js new file mode 100755 index 000000000..18fdb7a76 --- /dev/null +++ b/public/js/controllers/AccountsController.js @@ -0,0 +1,72 @@ +angular.module('BlocksApp').controller('AccountsController', function($stateParams, $rootScope, $scope, $http, $filter) { + $scope.settings = $rootScope.setup; + + // fetch accounts + var getAccounts = function() { + $("#table_accounts").DataTable({ + processing: true, + serverSide: true, + paging: true, + ajax: function(data, callback, settings) { + // get totalSupply only once. + data.totalSupply = $scope.totalSupply || -1; + data.recordsTotal = $scope.totalAccounts || 0; + $http.post('/richlist', data).then(function(resp) { + // set the totalSupply + if (resp.data.totalSupply) { + $scope.totalSupply = resp.data.totalSupply; + } + // set the number of total accounts + $scope.totalAccounts = resp.data.recordsTotal; + + // fixup data to show percentages + var newdata = resp.data.data.map(function(item) { + return [item[0], item[1], item[2], item[3], (item[3] / $scope.totalSupply) * 100, item[4]]; + }); + resp.data.data = newdata; + callback(resp.data); + }); + }, + lengthMenu: [ + [20, 50, 100, 150, -1], + [20, 50, 100, 150, "All"] // change per page values here + ], + pageLength: 20, + order: [ + [3, "desc"] + ], + language: { + lengthMenu: "_MENU_ accounts", + zeroRecords: "No accounts found", + infoEmpty: "", + infoFiltered: "(filtered from _MAX_ total accounts)" + }, + columnDefs: [ + { orderable: false, "targets": [0,1,2,4] }, + { + render: + function(data, type, row) { + return '' + data + '' + }, + targets: [1] + }, + { + render: + function(data, type, row) { + return $filter('number')(data, 8); + }, + targets: [3] + }, + { + render: + function(data, type, row) { + return $filter('number')(data, 4) + ' %'; + }, + targets: [4] + } + ] + }); + }; + + getAccounts(); +}); diff --git a/public/js/main.js b/public/js/main.js index 59d9bed2c..6ae01e19e 100755 --- a/public/js/main.js +++ b/public/js/main.js @@ -139,6 +139,27 @@ BlocksApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvide }] } }) + .state('accounts', { + url: "/accounts", + templateUrl: "views/accounts.html", + data: {pageTitle: 'Accounts'}, + controller: "AccountsController", + resolve: { + deps: ['$ocLazyLoad', function($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'BlocksApp', + insertBefore: '#ng_load_plugins_before', // load the above css files before '#ng_load_plugins_before' + files: [ + '/js/controllers/AccountsController.js', + '/plugins/datatables/datatables.min.css', + '/plugins/datatables/datatables.bootstrap.css', + '/plugins/datatables/datatables.all.min.js', + '/plugins/datatables/datatable.min.js' + ] + }); + }] + } + }) .state('block', { url: "/block/{number}", templateUrl: "views/block.html", diff --git a/public/views/accounts.html b/public/views/accounts.html new file mode 100755 index 000000000..3fd5a238a --- /dev/null +++ b/public/views/accounts.html @@ -0,0 +1,37 @@ +
+
+
+
Overview
+
+
+
+
+
+ Total supply: {{ totalSupply | number: 2 }} {{ settings.symbol }} +
+ Total {{ totalAccounts | number }} accounts +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + +
#AddressTypeBalancePercent
+
+
+
diff --git a/routes/index.js b/routes/index.js index e2b8b3680..46c230147 100644 --- a/routes/index.js +++ b/routes/index.js @@ -15,6 +15,7 @@ module.exports = function(app){ var compile = require('./compiler'); var fiat = require('./fiat'); var stats = require('./stats'); + var richList = require('./richlist'); /* Local DB: data request format @@ -22,6 +23,7 @@ module.exports = function(app){ { "tx": "0x1234blah" } { "block": "1234" } */ + app.post('/richlist', richList); app.post('/addr', getAddr); app.post('/addr_count', getAddrCounter); app.post('/tx', getTx); diff --git a/routes/richlist.js b/routes/richlist.js new file mode 100644 index 000000000..3826ab982 --- /dev/null +++ b/routes/richlist.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Endpoint for richlist + */ + +var async = require('async'); +var mongoose = require('mongoose'); + +require( '../db.js' ); +var Account = mongoose.model('Account'); + +var getAccounts = function(req, res) { + // count accounts only once + var count = req.body.recordsTotal || 0; + count = parseInt(count); + if (count < 0) { + count = 0; + } + + // get totalSupply only once + var queryTotalSupply = req.body.totalSupply || null; + + async.waterfall([ + function(callback) { + if (queryTotalSupply < 0) { + Account.aggregate([ + { $group: { _id: null, totalSupply: { $sum: '$balance' } } } + ]).exec(function(err, docs) { + if (err) { + callbck(err); + return; + } + + var totalSupply = docs[0].totalSupply; + callback(null, totalSupply); + }); + } else { + callback(null, null); + } + }, + function(totalSupply, callback) { + if (!count) { + // get the number of all accounts + Account.count({}, function(err, count) { + if (err) { + callbck(err); + return; + } + + count = parseInt(count); + callback(null, totalSupply, count); + }); + } else { + callback(null, totalSupply, count); + } + } + ], function(error, totalSupply, count) { + if (error) { + res.write(JSON.stringify({"error": true})); + res.end(); + return; + } + + // check sort order + var sortOrder = '-balance'; + if (req.body.order && req.body.order[0] && req.body.order[0].column) { + // balance column + if (req.body.order[0].column == 3) { + if (req.body.order[0].dir == 'asc') { + sortOrder = 'balance'; + } + } + } + + // set datatable params + var limit = parseInt(req.body.length); + var start = parseInt(req.body.start); + + var data = { draw: parseInt(req.body.draw), recordsFiltered: count, recordsTotal: count }; + if (totalSupply > 0) { + data.totalSupply = totalSupply; + } + + Account.find({}).lean(true).sort(sortOrder).skip(start).limit(limit) + .exec(function (err, accounts) { + if (err) { + res.write(JSON.stringify({"error": true})); + res.end(); + return; + } + + data.data = accounts.map(function(account, i) { + return [i + 1 + start, account.address, account.type == 0 ? "Account" : "Contract", account.balance, account.blockNumber]; + }); + res.write(JSON.stringify(data)); + res.end(); + } + ); + }); +} + +module.exports = getAccounts; diff --git a/tools/sync.js b/tools/sync.js index 217d5b9c2..6a19a496d 100644 --- a/tools/sync.js +++ b/tools/sync.js @@ -9,11 +9,13 @@ var etherUnits = require("../lib/etherUnits.js"); var BigNumber = require('bignumber.js'); var _ = require('lodash'); +var async = require('async'); var Web3 = require('web3'); var mongoose = require( 'mongoose' ); var Block = mongoose.model( 'Block' ); var Transaction = mongoose.model( 'Transaction' ); +var Account = mongoose.model( 'Account' ); /** //Just listen for latest blocks and sync from the start of the app. @@ -137,6 +139,13 @@ var writeTransactionsToDB = function(config, blockData, flush) { self.bulkOps = []; self.blocks = 0; } + // save miner addresses + if (!self.miners) { + self.miners = []; + } + if (blockData) { + self.miners.push({ address: blockData.miner, blockNumber: blockData.blockNumber, type: 0 }); + } if (blockData && blockData.transactions.length > 0) { for (d in blockData.transactions) { var txData = blockData.transactions[d]; @@ -152,8 +161,73 @@ var writeTransactionsToDB = function(config, blockData, flush) { var bulk = self.bulkOps; self.bulkOps = []; self.blocks = 0; - if(bulk.length == 0) return; + var miners = self.miners; + self.miners = []; + + // setup accounts + var data = {}; + bulk.forEach(function(tx) { + data[tx.from] = { address: tx.from, blockNumber: tx.blockNumber, type: 0 }; + if (tx.to) { + data[tx.to] = { address: tx.to, blockNumber: tx.blockNumber, type: 0 }; + } + }); + + // setup miners + miners.forEach(function(miner) { + data[miner.address] = miner; + }); + + var accounts = Object.keys(data); + + if (bulk.length == 0 && accounts.length == 0) return; + + // update balances + if (accounts.length > 0) + async.eachSeries(accounts, function(account, eachCallback) { + var blockNumber = data[account].blockNumber; + // get contract account type + web3.eth.getCode(account, function(err, code) { + if (err) { + console.log("ERROR: fail to getCode(" + account + ")"); + return eachCallback(err); + } + if (code.length > 2) { + data[account].type = 1; // contract type + } + + web3.eth.getBalance(account, blockNumber, function(err, balance) { + if (err) { + console.log("ERROR: fail to getBalance(" + account + ")"); + return eachCallback(err); + } + + //data[account].balance = web3.fromWei(balance, 'ether'); + let ether; + if (typeof balance === 'object') { + ether = parseFloat(balance.div(1e18).toString()); + } else { + ether /= 1e18; + } + data[account].balance = ether; + eachCallback(); + }); + }); + }, function(err) { + var n = 0; + accounts.forEach(function(account) { + n++; + if (n <= 5) { + console.log(' - upsert ' + account + ' / balance = ' + data[account].balance); + } else if (n == 6) { + console.log(' (...) total ' + accounts.length + ' accounts updated.'); + } + // upsert account + Account.collection.update({ address: account }, { $set: data[account] }, { upsert: true }); + }); + }); + if (bulk.length > 0) Transaction.collection.insert(bulk, function( err, tx ){ if ( typeof err !== 'undefined' && err ) { if (err.code == 11000) { From 59fec5fff5f12060b734631524662133f3c03925 Mon Sep 17 00:00:00 2001 From: hackyminer Date: Sat, 11 Aug 2018 16:07:56 +0900 Subject: [PATCH 02/17] tools/richlist.js added to support richlist feature using the parity --- tools/richlist.js | 184 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tools/richlist.js diff --git a/tools/richlist.js b/tools/richlist.js new file mode 100644 index 000000000..bff5c398d --- /dev/null +++ b/tools/richlist.js @@ -0,0 +1,184 @@ +#!/usr/bin/env node +/** + * Tool for calculating richlist by hackyminer + */ + +var _ = require('lodash'); +var Web3 = require('web3'); +var async = require('async'); +var BigNumber = require('bignumber.js'); +var mongoose = require('mongoose'); + +var Account = require('../db.js').Account; + +mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/blockDB'); + +function makeParityRichList(number, offset, blockNumber, updateCallback) { + var self = makeParityRichList; + if (!self.index) { + self.index = 0; + } + number = number || 100; + offset = offset || null; + + async.waterfall([ + function(callback) { + web3.parity.listAccounts(number, offset, blockNumber, function(err, result) { + callback(err, result); + }); + }, function(accounts, callback) { + if (!accounts) { + return callback({ + error: true, + message: "No accounts found. Please restart Parity with --fat-db=on option to enable FatDB." + }); + } + + if (accounts.length === 0) { + return callback({ + error: true, + message: "No more accounts found." + }); + } + + var lastAccount = accounts[accounts.length - 1]; + var data = {}; + + // Please see https://github.com/gobitfly/etherchain-light by gobitfly + async.eachSeries(accounts, function(account, eachCallback) { + web3.eth.getCode(account, function(err, code) { + if (err) { + console.log("ERROR: fail to getCode(" + account + ")"); + return eachCallback(err); + } + data[account] = {}; + data[account].address = account; + data[account].type = code.length > 2 ? 1 : 0; // 0: address, 1: contract + + web3.eth.getBalance(account, blockNumber, function(err, balance) { + if (err) { + console.log("ERROR: fail to getBalance(" + account + ")"); + return eachCallback(err); + } + + //data[account].balance = web3.fromWei(balance, 'ether'); + let ether; + if (typeof balance === 'object') { + ether = parseFloat(balance.div(1e18).toString()); + } else { + ether /= 1e18; + } + data[account].balance = ether; + eachCallback(); + }); + }); + }, function(err) { + callback(err, data, lastAccount); + }); + } + ], function(error, accounts, lastAccount) { + if (error) { + console.log(error); + process.exit(9); + return; + } + + //console.log(JSON.stringify(accounts, null, 2)); + offset = lastAccount; + let j = Object.keys(accounts).length; + self.index += j; + console.log(' * ' + j + ' / ' + self.index + ' accounts, offset = ' + offset); + if (updateCallback) { + updateCallback(accounts, blockNumber); + } + setTimeout(function() { + makeParityRichList(number, lastAccount, blockNumber, updateCallback); + }, 300); + }); +} + +/** + * Write accounts to DB + */ +var updateAccounts = function(accounts, blockNumber) { + var bulk = Object.keys(accounts).map(function(j) { + let account = accounts[j]; + account.blockNumber = blockNumber; + return account; + }); + + Account.collection.insert(bulk, function(error, data) { + if (error) { + if (error.code == 11000) { + async.eachSeries(bulk, function(item, eachCallback) { + // upsert accounts + delete item._id; // remove _id field + Account.collection.update({ "address": item.address }, { $set: item }, { upsert: true }, function(err, updated) { + if (err) { + if (!config.quiet) { + console.log('WARN: Duplicate DB key : ' + error); + console.log('ERROR: Fail to update account: ' + err); + } + return eachCallback(err); + } + eachCallback(); + }); + }, function(err) { + if (err) { + console.log('ERROR: Aborted due to error: ' + err); + process.exit(9); + return; + } + console.log('* ' + bulk.length + ' accounts successfully updated.'); + }); + } else { + console.log('Error: Aborted due to error on DB: ' + error); + process.exit(9); + } + } else { + console.log('* ' + data.insertedCount + ' accounts successfully inserted.'); + } + }); +} + +/** + * Start config for node connection and sync + */ +var config = { nodeAddr: 'localhost', 'gethPort': 8545 }; +// load the config.json file +try { + var loaded = require('../config.json'); + _.extend(config, loaded); + console.log('config.json found.'); +} catch (error) { + console.log('No config file found.'); + throw error; + process.exit(1); +} + +// temporary turn on some debug +//config.quiet = false; +//mongoose.set('debug', true); + +console.log('Connecting ' + config.nodeAddr + ':' + config.gethPort + '...'); + +var web3 = new Web3(new Web3.providers.HttpProvider('http://' + config.nodeAddr + ':' + config.gethPort.toString())); + +var useParity = false; +if (web3.version.node.split('/')[0].toLowerCase().includes('parity')) { + // load parity extension + web3 = require("../lib/trace.js")(web3); + useParity = true; +} + +var latestBlock = web3.eth.blockNumber; + +// run +console.log("* latestBlock = " + latestBlock); + +if (useParity) { + makeParityRichList(100, null, latestBlock, updateAccounts); +} else { + console.log("Sorry, currently only Parity is supported."); + process.exit(1); +} From ad6e2348dc57d596623a82f157d4d014f1045282 Mon Sep 17 00:00:00 2001 From: hackyminer Date: Thu, 28 Jun 2018 03:43:14 +0900 Subject: [PATCH 03/17] parity extensions added (web3.trace, web3.parity) --- lib/trace.js | 100 ++++++++++++++++++++++++++++++++++++++++++++ routes/web3relay.js | 5 +++ 2 files changed, 105 insertions(+) create mode 100644 lib/trace.js diff --git a/lib/trace.js b/lib/trace.js new file mode 100644 index 000000000..d5c81d516 --- /dev/null +++ b/lib/trace.js @@ -0,0 +1,100 @@ +/** + * @author Alexis Roussel + * @author Peter Pratscher + * @date 2017 + * @license LGPL + * @changelog 2018/05/19 - modified for web3.js 0.20.x using _extend() method. (by hackyminer ) + */ +module.exports = function(web3) { + /** + * @file trace.js + * @author Alexis Roussel + * @date 2017 + * @license LGPL + */ + web3._extend({ + property: 'trace', + methods: [ + new web3._extend.Method({ + name: 'call', + call: 'trace_call', + params: 3, + inputFormatter: [web3._extend.formatters.inputCallFormatter, null, web3._extend.formatters.inputDefaultBlockNumberFormatter] + }), + + new web3._extend.Method({ + name: 'rawTransaction', + call: 'trace_rawTransaction', + params: 2 + }), + + new web3._extend.Method({ + name: 'replayTransaction', + call: 'trace_replayTransaction', + params: 2 + }), + + new web3._extend.Method({ + name: 'block', + call: 'trace_block', + params: 1, + inputFormatter: [web3._extend.formatters.inputDefaultBlockNumberFormatter] + }), + + new web3._extend.Method({ + name: 'filter', + call: 'trace_filter', + params: 1 + }), + + new web3._extend.Method({ + name: 'get', + call: 'trace_get', + params: 2 + }), + + new web3._extend.Method({ + name: 'transaction', + call: 'trace_transaction', + params: 1 + }) + ] + }); + + /** + * @file parity.js + * @author Peter Pratscher + * @date 2017 + * @license LGPL + */ + web3._extend({ + property: 'parity', + methods: [ + new web3._extend.Method({ + name: 'pendingTransactions', + call: 'parity_pendingTransactions', + params: 0, + outputFormatter: web3._extend.formatters.outputTransactionFormatter + }), + + new web3._extend.Method({ + name: 'pendingTransactionsStats', + call: 'parity_pendingTransactionsStats', + params: 0 + }), + + new web3._extend.Method({ + name: 'listAccounts', + call: 'parity_listAccounts', + params: 2 + }), + + new web3._extend.Method({ + name: 'phraseToAddress', + call: 'parity_phraseToAddress', + params: 1 + }) + ] + }); + return web3; +}; diff --git a/routes/web3relay.js b/routes/web3relay.js index 1a284b158..c6fa91d30 100644 --- a/routes/web3relay.js +++ b/routes/web3relay.js @@ -46,6 +46,11 @@ if (web3.isConnected()) else throw "No connection, please specify web3host in conf.json"; +if (web3.version.node.split('/')[0].toLowerCase().includes('parity')) { + // parity extension + web3 = require("../lib/trace.js")(web3); +} + var newBlocks = web3.eth.filter("latest"); var newTxs = web3.eth.filter("pending"); From 1cda64e4d32a3efb614ec4a73f8c1559985c2408 Mon Sep 17 00:00:00 2001 From: hackyminer Date: Sat, 11 Aug 2018 16:40:39 +0900 Subject: [PATCH 04/17] Accounts menu added. --- public/tpl/header.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/tpl/header.html b/public/tpl/header.html index a20316ca3..3d7bf5390 100755 --- a/public/tpl/header.html +++ b/public/tpl/header.html @@ -44,6 +44,9 @@
  • Home
  • +
  • + Accounts +
  • Home
  • -
  • +
  • Accounts