From b4f49e953ab9e6138f33c48bac824d0ceb4eba3c Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 3 Jul 2014 18:48:01 -0600 Subject: [PATCH] [REDIS DATA BREAKING CHANGES] More efficient block storage format for redis. Limited number of blocks sent to front-end. Expanded admin page. More realistic 'luck' value. Run redisBlocksUpgrade.js script to convert block data to new format. --- README.md | 4 +- config_example.json | 1 + init.js | 19 +---- lib/api.js | 116 +++++++++++++++++-------- lib/blockUnlocker.js | 95 ++++++++++++--------- lib/configReader.js | 20 +++++ lib/pool.js | 29 +++++-- redisBlocksUpgrade.js | 191 ++++++++++++++++++++++++++++++++++++++++++ website/admin.html | 135 +++++++++++++++++++++++++---- website/index.html | 191 ++++++++++++++++++++++++++---------------- 10 files changed, 616 insertions(+), 185 deletions(-) create mode 100644 lib/configReader.js create mode 100644 redisBlocksUpgrade.js diff --git a/README.md b/README.md index 59efd9d..1a3102a 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,9 @@ Explanation for each field: "enabled": true, "hashrateWindow": 600, //how many second worth of shares used to estimate hash rate "updateInterval": 3, //gather stats and broadcast every this many seconds - "port": 8117 + "port": 8117, + "blocks": 30, //amount of blocks to send at a time + "password": "test" //password required for admin stats }, /* Coin daemon connection details. */ diff --git a/config_example.json b/config_example.json index ae0ae59..df2a7aa 100644 --- a/config_example.json +++ b/config_example.json @@ -82,6 +82,7 @@ "hashrateWindow": 600, "updateInterval": 5, "port": 8117, + "blocks": 30, "password": "your_password" }, diff --git a/init.js b/init.js index 67b49a5..e13596a 100644 --- a/init.js +++ b/init.js @@ -5,24 +5,7 @@ var os = require('os'); var redis = require('redis'); -var configFile = (function(){ - for (var i = 0; i < process.argv.length; i++){ - if (process.argv[i].indexOf('-config=') === 0) - return process.argv[i].split('=')[1]; - } - return 'config.json'; -})(); - - -try { - global.config = JSON.parse(fs.readFileSync(configFile)); -} -catch(e){ - console.error('Failed to read config file ' + configFile + '\n\n' + e); - return; -} - -config.version = "v0.99.0.6"; +require('./lib/configReader.js'); require('./lib/logger.js'); diff --git a/lib/api.js b/lib/api.js index 5d17737..72c0c1d 100644 --- a/lib/api.js +++ b/lib/api.js @@ -13,13 +13,13 @@ require('./exceptionWriter.js')(logSystem); var redisCommands = [ ['zremrangebyscore', config.coin + ':hashrate', '-inf', ''], - ['zrangebyscore', config.coin + ':hashrate', '', '+inf'], - ['hgetall', config.coin + ':stats'], - ['smembers', config.coin + ':blocksPending'], - ['smembers', config.coin + ':blocksUnlocked'], - ['smembers', config.coin + ':blocksOrphaned'], + ['zrange', config.coin + ':hashrate', 0, -1], + ['hgetall', config.coin + ':stats'], + ['zrange', config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES'], + ['zrevrange', config.coin + ':blocks:matured', 0, config.api.blocks, 'WITHSCORES'], ['hgetall', config.coin + ':shares:roundCurrent'], - ['hgetall', config.coin + ':stats'] + ['hgetall', config.coin + ':stats'], + ['zcard', config.coin + ':blocks:matured'] ]; var currentStats = ""; @@ -56,11 +56,8 @@ function collectStats(){ var data = { stats: replies[2], - blocks: { - pending: replies[3], - unlocked: replies[4], - orphaned: replies[5] - } + blocks: replies[3].concat(replies[4]), + totalBlocks: parseInt(replies[7]) + replies[3].length }; var hashrates = replies[1]; @@ -86,14 +83,14 @@ function collectStats(){ data.roundHashes = 0; - if (replies[6]){ - for (var miner in replies[6]){ - data.roundHashes += parseInt(replies[6][miner]); + if (replies[5]){ + for (var miner in replies[5]){ + data.roundHashes += parseInt(replies[5][miner]); } } - if (replies[7]) { - data.lastBlockFound = replies[7].lastBlockFound; + if (replies[6]) { + data.lastBlockFound = replies[6].lastBlockFound; } callback(null, data); @@ -227,6 +224,28 @@ function formatMinerStats(redisData, address){ } +function handleGetBlocks(urlParts, response){ + redisClient.zrevrangebyscore(config.coin + ':blocks:matured', '(' + urlParts.query.height, '-inf', 'WITHSCORES', 'LIMIT', 0, config.api.blocks, function(err, result){ + + var reply; + + if (err) + reply = JSON.stringify({error: 'query failed'}); + else + reply = JSON.stringify(result); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': reply.length + }); + response.end(reply); + + }); +} + + collectStats(); function authorize(request, response){ @@ -252,21 +271,24 @@ function handleAdminStats(response){ async.waterfall([ - //Get worker keys + //Get worker keys & unlocked blocks function(callback){ - redisClient.keys(config.coin + ':workers:*', function(error, result) { + redisClient.multi([ + ['keys', config.coin + ':workers:*'], + ['zrange', config.coin + ':blocks:matured', 0, -1] + ]).exec(function(error, replies) { if (error) { - log('error', logSystem, 'Error trying to get worker balances from redis %j', [error]); + log('error', logSystem, 'Error trying to get admin data from redis %j', [error]); callback(true); return; } - callback(null, result); + callback(null, replies[0], replies[1]); }); }, //Get worker balances - function(keys, callback){ - var redisCommands = keys.map(function(k){ + function(workerKeys, blocks, callback){ + var redisCommands = workerKeys.map(function(k){ return ['hmget', k, 'balance', 'paid']; }); redisClient.multi(redisCommands).exec(function(error, replies){ @@ -276,19 +298,42 @@ function handleAdminStats(response){ return; } - var stats = { - totalOwed: 0, - totalPaid: 0 - }; - - for (var i = 0; i < replies.length; i++){ - stats.totalOwed += parseInt(replies[i][0]) || 0; - stats.totalPaid += parseInt(replies[i][1]) || 0; - } - - callback(null, stats); + callback(null, replies, blocks); }); - }], function(error, stats){ + }, + function(workerData, blocks, callback){ + var stats = { + totalOwed: 0, + totalPaid: 0, + totalRevenue: 0, + totalDiff: 0, + totalShares: 0, + blocksOrphaned: 0, + blocksUnlocked: 0, + totalWorkers: 0 + }; + + for (var i = 0; i < workerData.length; i++){ + stats.totalOwed += parseInt(workerData[i][0]) || 0; + stats.totalPaid += parseInt(workerData[i][1]) || 0; + stats.totalWorkers++; + } + + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i].split(':'); + if (block[5]) { + stats.blocksUnlocked++; + stats.totalDiff += parseInt(block[2]); + stats.totalShares += parseInt(block[3]); + stats.totalRevenue += parseInt(block[5]); + } + else{ + stats.blocksOrphaned++; + } + } + callback(null, stats); + } + ], function(error, stats){ if (error){ response.end(JSON.stringify({error: 'error collecting stats'})); return; @@ -346,6 +391,9 @@ var server = http.createServer(function(request, response){ case '/stats_address': handleMinerStats(urlParts, response); break; + case '/get_blocks': + handleGetBlocks(urlParts, response); + break; case '/admin_stats': if (!authorize(request, response)) return; diff --git a/lib/blockUnlocker.js b/lib/blockUnlocker.js index 1ea50aa..3cf37bb 100644 --- a/lib/blockUnlocker.js +++ b/lib/blockUnlocker.js @@ -32,28 +32,34 @@ var doDonations = config.blockUnlocker.devDonation > 0 && devDonationAddress[0] function runInterval(){ async.waterfall([ - //Get all pending blocks in redis + //Get all block candidates in redis function(callback){ - redisClient.smembers(config.coin + ':blocksPending', function(error, result){ + redisClient.zrange(config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES', function(error, results){ if (error){ log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); callback(true); return; } - if (result.length === 0){ - log('info', logSystem, 'No pending blocks in redis'); + if (results.length === 0){ + log('info', logSystem, 'No blocks candidates in redis'); callback(true); return; } - var blocks = result.map(function(item){ - var parts = item.split(':'); - return { - height: parseInt(parts[0]), - difficulty: parseInt(parts[1]), - hash: parts[2], - serialized: item - }; - }); + + var blocks = []; + + for (var i = 0; i < results.length; i += 2){ + var parts = results[i].split(':'); + blocks.push({ + serialized: results[i], + height: parseInt(results[i + 1]), + hash: parts[0], + time: parts[1], + difficulty: parts[2], + shares: parts[3] + }); + } + callback(null, blocks); }); }, @@ -75,7 +81,7 @@ function runInterval(){ return; } var blockHeader = result.block_header; - block.orphan = (blockHeader.hash !== block.hash); + block.orphaned = blockHeader.hash === block.hash ? 0 : 1; block.unlocked = blockHeader.depth >= config.blockUnlocker.depth; block.reward = blockHeader.reward; mapCback(block.unlocked); @@ -83,7 +89,7 @@ function runInterval(){ }, function(unlockedBlocks){ if (unlockedBlocks.length === 0){ - log('info', logSystem, 'No pending blocks are unlocked or orphaned yet (%d pending)', [blocks.length]); + log('info', logSystem, 'No pending blocks are unlocked yet (%d pending)', [blocks.length]); callback(true); return; } @@ -110,9 +116,6 @@ function runInterval(){ for (var i = 0; i < replies.length; i++){ var workerShares = replies[i]; blocks[i].workerShares = workerShares; - blocks[i].totalShares = Object.keys(workerShares).reduce(function(p, c){ - return p + parseInt(workerShares[c]) - }, 0); } callback(null, blocks); }); @@ -121,19 +124,29 @@ function runInterval(){ //Handle orphaned blocks function(blocks, callback){ var orphanCommands = []; + blocks.forEach(function(block){ - if (!block.orphan) return; - var workerShares = block.workerShares; + if (!block.orphaned) return; + orphanCommands.push(['del', config.coin + ':shares:round' + block.height]); - orphanCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); - orphanCommands.push(['sadd', config.coin + ':blocksOrphaned', block.serialized + ':' + block.totalShares]); + orphanCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); + orphanCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned + ].join(':')]); - Object.keys(workerShares).forEach(function(worker){ - orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', - worker, workerShares[worker]]); - }); + if (block.workerShares) { + var workerShares = block.workerShares; + Object.keys(workerShares).forEach(function (worker) { + orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', worker, workerShares[worker]]); + }); + } }); + if (orphanCommands.length > 0){ redisClient.multi(orphanCommands).exec(function(error, replies){ if (error){ @@ -155,13 +168,19 @@ function runInterval(){ var payments = {}; var totalBlocksUnlocked = 0; blocks.forEach(function(block){ - if (block.orphan) return; + if (block.orphaned) return; totalBlocksUnlocked++; unlockedBlocksCommands.push(['del', config.coin + ':shares:round' + block.height]); - - unlockedBlocksCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); - unlockedBlocksCommands.push(['sadd', config.coin + ':blocksUnlocked', block.serialized + ':' + block.totalShares]); + unlockedBlocksCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); + unlockedBlocksCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned, + block.reward + ].join(':')]); var feePercent = config.blockUnlocker.poolFee / 100; @@ -173,12 +192,14 @@ function runInterval(){ var reward = block.reward - (block.reward * feePercent); - var totalShares = block.totalShares; - Object.keys(block.workerShares).forEach(function(worker){ - var percent = block.workerShares[worker] / totalShares; - var workerReward = reward * percent; - payments[worker] = (payments[worker] || 0) + workerReward; - }); + if (block.workerShares) { + var totalShares = parseInt(block.shares); + Object.keys(block.workerShares).forEach(function (worker) { + var percent = block.workerShares[worker] / totalShares; + var workerReward = reward * percent; + payments[worker] = (payments[worker] || 0) + workerReward; + }); + } }); for (var worker in payments) { @@ -205,9 +226,7 @@ function runInterval(){ log('info', logSystem, 'Unlocked %d blocks and update balances for %d workers', [totalBlocksUnlocked, Object.keys(payments).length]); callback(null); }); - } - ], function(error, result){ setTimeout(runInterval, config.blockUnlocker.interval * 1000); }) diff --git a/lib/configReader.js b/lib/configReader.js new file mode 100644 index 0000000..e5187cf --- /dev/null +++ b/lib/configReader.js @@ -0,0 +1,20 @@ +var fs = require('fs'); + +var configFile = (function(){ + for (var i = 0; i < process.argv.length; i++){ + if (process.argv[i].indexOf('-config=') === 0) + return process.argv[i].split('=')[1]; + } + return 'config.json'; +})(); + + +try { + global.config = JSON.parse(fs.readFileSync(configFile)); +} +catch(e){ + console.error('Failed to read config file ' + configFile + '\n\n' + e); + return; +} + +config.version = "v0.99.1"; \ No newline at end of file diff --git a/lib/pool.js b/lib/pool.js index bba0454..dfc123e 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -350,15 +350,34 @@ function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareTy ]; if (blockCandidate){ - redisCommands.push(['sadd', config.coin + ':blocksPending', [job.height, currentBlockTemplate.difficulty, hashHex, Date.now() / 1000 | 0].join(':')]); - redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); + //redisCommands.push(['sadd', config.coin + ':blocksPending', [job.height, currentBlockTemplate.difficulty, hashHex, Date.now() / 1000 | 0].join(':')]); redisCommands.push(['hset', config.coin + ':stats', 'lastBlockFound', Date.now()]); + redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); + redisCommands.push(['hgetall', config.coin + ':shares:round' + job.height]); } redisClient.multi(redisCommands).exec(function(err, replies){ if (err){ - log('error', logSystem, 'Failed to insert share data into redis %j', [err]); + log('error', logSystem, 'Failed to insert share data into redis %j \n %j', [err, redisCommands]); + return; } + if (blockCandidate){ + var workerShares = replies[replies.length - 1]; + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + redisClient.zadd(config.coin + ':blocks:candidates', job.height, [ + hashHex, + Date.now() / 1000 | 0, + currentBlockTemplate.difficulty, + totalShares + ].join(':'), function(err, result){ + if (err){ + log('error', logSystem, 'Failed inserting block candidate %s \n %j', [hashHex, err]); + } + }); + } + }); log('info', logSystem, 'Accepted %s share at difficulty %d/%d from %s@%s', [shareType, job.difficulty, shareDiff, miner.login, miner.ip]); @@ -396,10 +415,9 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ var hashNum = bignum.fromBuffer(new Buffer(hashArray)); var hashDiff = diff1.div(hashNum); - var blockFastHash; + if (hashDiff.ge(blockTemplate.difficulty)){ - blockFastHash = cryptoNightFast(convertedBlob || cnUtil.convert_blob(shareBuffer)).toString('hex'); apiInterfaces.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function(error, result){ if (error){ @@ -407,6 +425,7 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ recordShareData(miner, job, hashDiff.toString(), false, null, shareType); } else{ + var blockFastHash = cryptoNightFast(convertedBlob || cnUtil.convert_blob(shareBuffer)).toString('hex'); log('info', logSystem, 'Block %s found at height %d by miner %s@%s - submit result: %j', [blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result] diff --git a/redisBlocksUpgrade.js b/redisBlocksUpgrade.js new file mode 100644 index 0000000..3e6a762 --- /dev/null +++ b/redisBlocksUpgrade.js @@ -0,0 +1,191 @@ + + +/* + +This script converts the block data in redis from the old format (v0.99.0.6 and earlier) to the new format +used in v0.99.1+ + +*/ + +var util = require('util'); + +var async = require('async'); + +var redis = require('redis'); + +require('./lib/configReader.js'); + +var apiInterfaces = require('./lib/apiInterfaces.js')(config.daemon, config.wallet); + + +function log(severity, system, text, data){ + + var formattedMessage = text; + + if (data) { + data.unshift(text); + formattedMessage = util.format.apply(null, data); + } + + console.log(severity + ': ' + formattedMessage); +} + + +var logSystem = 'reward script'; + +var redisClient = redis.createClient(config.redis.port, config.redis.host); + +function getTotalShares(height, callback){ + + redisClient.hgetall(config.coin + ':shares:round' + height, function(err, workerShares){ + + if (err) { + callback(err); + return; + } + + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + + callback(null, totalShares); + + }); +} + + +async.series([ + function(callback){ + redisClient.smembers(config.coin + ':blocksUnlocked', function(error, result){ + if (error){ + log('error', logSystem, 'Error trying to get unlocke blocks from redis %j', [error]); + callback(); + return; + } + if (result.length === 0){ + log('info', logSystem, 'No unlocked blocks in redis'); + callback(); + return; + } + + var blocks = result.map(function(item){ + var parts = item.split(':'); + return { + height: parseInt(parts[0]), + difficulty: parts[1], + hash: parts[2], + time: parts[3], + shares: parts[4], + orphaned: 0 + }; + }); + + async.map(blocks, function(block, mapCback){ + apiInterfaces.rpcDaemon('getblockheaderbyheight', {height: block.height}, function(error, result){ + if (error){ + log('error', logSystem, 'Error with getblockheaderbyheight RPC request for block %s - %j', [block.serialized, error]); + mapCback(null, block); + return; + } + if (!result.block_header){ + log('error', logSystem, 'Error with getblockheaderbyheight, no details returned for %s - %j', [block.serialized, result]); + mapCback(null, block); + return; + } + var blockHeader = result.block_header; + block.reward = blockHeader.reward; + mapCback(null, block); + }); + }, function(err, blocks){ + + if (blocks.length === 0){ + log('info', logSystem, 'No unlocked blocks'); + callback(); + return; + } + + var zaddCommands = [config.coin + ':blocks:matured']; + + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i]; + zaddCommands.push(block.height); + zaddCommands.push([ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned, + block.reward + ].join(':')); + } + + redisClient.zadd(zaddCommands, function(err, result){ + if (err){ + console.log('failed zadd ' + JSON.stringify(err)); + callback(); + return; + } + console.log('successfully converted unlocked blocks to matured blocks'); + callback(); + }); + + + }); + }); + }, + function(callback){ + redisClient.smembers(config.coin + ':blocksPending', function(error, result) { + if (error) { + log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); + callback(); + return; + } + if (result.length === 0) { + log('info', logSystem, 'No pending blocks in redis'); + callback(); + return; + } + + async.map(result, function(item, mapCback){ + var parts = item.split(':'); + var block = { + height: parseInt(parts[0]), + difficulty: parts[1], + hash: parts[2], + time: parts[3], + serialized: item + }; + getTotalShares(block.height, function(err, shares){ + block.shares = shares; + mapCback(null, block); + }); + }, function(err, blocks){ + + var zaddCommands = [config.coin + ':blocks:candidates']; + + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i]; + zaddCommands.push(block.height); + zaddCommands.push([ + block.hash, + block.time, + block.difficulty, + block.shares + ].join(':')); + } + + redisClient.zadd(zaddCommands, function(err, result){ + if (err){ + console.log('failed zadd ' + JSON.stringify(err)); + return; + } + console.log('successfully converted pending blocks to block candidates'); + }); + + }); + + }); + } +], function(){ + process.exit(); +}); \ No newline at end of file diff --git a/website/admin.html b/website/admin.html index 2bbbe60..c07bf1f 100644 --- a/website/admin.html +++ b/website/admin.html @@ -13,29 +13,112 @@ + + @@ -43,11 +126,29 @@ -
- Total Owed: -
-
- Total Paid: +
+ +

Admin Center

+ +
+ +

Stats

+
+
Total Owed
...
+
Total Paid
...
+
Total Mined
...
+
Profit (before tx fees)
...
+
Average Luck
...
+
Orphan Percent
...
+
Registered Addresses
...
+
+
+ +
+ +

Miner Lookup

+ +
diff --git a/website/index.html b/website/index.html index a3d213d..3891ad6 100644 --- a/website/index.html +++ b/website/index.html @@ -68,18 +68,15 @@ margin-left: 5px; } #lastHash{ - font-family: monospace; + font-family: 'Inconsolata', monospace; font-size: 0.8em; } #yourStatsInput{ - width: 820px; - max-width: 100%; - display: inline-block; - vertical-align: bottom; - font-family: monospace; + z-index: inherit; + font-family: 'Inconsolata', monospace; } #yourAddressDisplay > span { - font-family: monospace; + font-family: 'Inconsolata', monospace; } .yourStats{ display: none; @@ -90,8 +87,8 @@ overflow: hidden; text-overflow: ellipsis; vertical-align: middle; - font-family: monospace; - font-size: 0.8em; + font-family: 'Inconsolata', monospace; + font-size: 0.9em; } #addressError{ color: red; @@ -121,7 +118,7 @@ #blocks_rows > tr > td{ vertical-align: middle; font-family: 'Inconsolata', monospace; - font-size: 0.9em; + font-size: 0.95em; } .luckGood{ @@ -183,18 +180,12 @@ $(function(){ $('#siteInfo').load('info.html'); - /*$.get('info.html', function(html){ - $('#siteInfo').html(html); - }, 'html');*/ $.get(api + '/stats', function(data){ renderStats(data); updateMarkets(); }); - - - function fetchLiveStats() { $.ajax({ url: api + '/live_stats', @@ -338,10 +329,16 @@ setInterval(updateMarkets, 300000); //poll market data every 5 minutes + var lastStats; + function renderStats(stats){ + lastStats = stats; + $('#coinName').text(stats.config.coin); + $('#blocksTotal').text(stats.pool.totalBlocks); + $('#networkHashrate').text(getReadableHashRateString(stats.network.difficulty / 60) + '/sec'); $('#networkLastBlockFound').timeago('update', new Date(stats.network.timestamp * 1000).toISOString()); @@ -408,7 +405,11 @@ $('#blocksMaturityCount').text(stats.config.depth); - renderBlocks(stats.pool.blocks, stats.config.depth, stats.network.height, blockchainExplorer); + var blocksJson = JSON.stringify(stats.pool.blocks); + if (lastBlocksJson !== blocksJson) { + lastBlocksJson = blocksJson; + renderBlocks(stats.pool.blocks); + } $('#blocks_rows').find('tr[class=""]').each(function(){ var height = parseInt(this.children[0].innerHTML); @@ -434,27 +435,13 @@ var lastBlocksJson = ''; - function renderBlocks(blocksResults, depth, chainHeight, explorer){ - - var blocksJson = JSON.stringify(blocksResults); - if (lastBlocksJson === blocksJson) return; - lastBlocksJson = blocksJson; - - var blocks = []; - for (var status in blocksResults){ - var blockArray = blocksResults[status]; - for (var i = 0; i < blockArray.length; i++){ - var blockData = blockArray[i].split(':'); - blockData[0] = parseInt(blockData[0]); - blockData.unshift(status); - blocks.push(blockData); - } - } - blocks.sort(function(a, b){ - return b[1] - a[1]; - }); + var $blockRows = $('#blocks_rows'); + function renderBlocks(blocksResults){ + var depth = lastStats.config.depth; + var chainHeight = lastStats.network.height; + var explorer = blockchainExplorer; var blockStatusClasses = { 'pending': '', @@ -467,39 +454,78 @@ return new Date(parseInt(time) * 1000).toLocaleString(); }; - var formatLuck = function(percent){ - if (!percent) return ''; - return '' + percent + '%'; + var formatLuck = function(difficulty, shares){ + + if (difficulty > shares){ + var percent = 100 - Math.round(shares / difficulty * 100); + return ' ' + percent + '%'; + } + else{ + var percent = (100 - Math.round(difficulty / shares * 100)) * -1; + return '' + percent + '%'; + } + }; - var rows = ''; + var pendingRows = ''; - var totalLuck = 0; - var totalLuckBlocks = 0; + for (var i = 0; i < blocksResults.length; i += 2){ + var parts = blocksResults[i].split(':'); + var block = { + height: parseInt(blocksResults[i + 1]), + hash: parts[0], + time: parts[1], + difficulty: parseInt(parts[2]), + shares: parseInt(parts[3]), + orphaned: parts[4], + reward: parts[5] + }; - for (var i = 0; i < blocks.length; i++){ - var block = blocks[i]; - var blockLuck = null; - if (block[5]){ - blockLuck = Math.round(parseInt(block[2]) / parseInt(block[5]) * 100); - totalLuck += blockLuck; - totalLuckBlocks++; + switch (block.orphaned){ + case '0': + block.status = 'unlocked'; + break; + case '1': + block.status = 'orphaned'; + break; + default: + block.status = 'pending'; + break; + } + + var row = '' + + '' + block.height + '' + + '' + (block.status === 'pending' ? getMaturity(depth, chainHeight, block.height) : '') + '' + + '' + block.difficulty + '' + + '' + block.hash + '' + + '' + formatDate(block.time) + '' + + '' + formatLuck(block.difficulty, block.shares) + '' + + ''; + + if (block.status === 'pending'){ + pendingRows += row; + } + else if (!$blockRows.find('#blockMatured' + block.height).length){ + var inserted = false; + $blockRows.children().each(function(){ + var bHeight = parseInt(this.getAttribute('data-height')); + if (bHeight < block.height){ + $(this).after(row); + inserted = true; + return false; + } + }); + if (!inserted){ + $blockRows.append(row); + } } - rows += ('' + - '' + block[1] + '' + - '' + (block[0] === 'pending' ? getMaturity(depth, chainHeight, blocks[1]) : '') + '' + - '' + block[2] + '' + - '' + block[3] + '' + - '' + formatDate(block[4]) + '' + - '' + formatLuck(blockLuck) + '' + - ''); } - $('#blocks_rows').empty().append(rows); - $('#averageBlockLuck').html(formatLuck(Math.round(totalLuck / totalLuckBlocks))); - $('#blocksCountPending').text(Object.keys(blocksResults['pending']).length); - $('#blocksCountUnlocked').text(Object.keys(blocksResults['unlocked']).length); - $('#blocksCountOrphaned').text(Object.keys(blocksResults['orphaned']).length); + if (pendingRows) { + $blockRows.children('[title="pending"]').remove(); + $blockRows.prepend(pendingRows); + } } @@ -607,11 +633,27 @@ $('#lookUp').click(); }); + var getBlocksAjax; + + $('#loadMoreBlocks').click(function(){ + if (getBlocksAjax) + getBlocksAjax.abort(); + getBlocksAjax = $.ajax({ + url: api + '/get_blocks', + data: { + height: $blockRows.children().last().data('height') + }, + dataType: 'json', + cache: 'false', + success: function(data){ + renderBlocks(data); + } + }) + }); + }); - -