[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.

pull/3/head
Matthew Little 2014-07-03 18:48:01 -06:00
parent b3f0af041b
commit b4f49e953a
10 changed files with 616 additions and 185 deletions

View File

@ -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. */

View File

@ -82,6 +82,7 @@
"hashrateWindow": 600,
"updateInterval": 5,
"port": 8117,
"blocks": 30,
"password": "your_password"
},

19
init.js
View File

@ -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');

View File

@ -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;

View File

@ -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);
})

View File

@ -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";

View File

@ -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]

View File

@ -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();
});

View File

@ -13,29 +13,112 @@
<link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<style>
#statsHolder{
margin-bottom: 0;
}
.luckGood{
color: darkgreen;
}
.luckBad{
color: darkred;
}
</style>
<script src="config.js"></script>
<script>
var coinDecimals = coinUnits.toString().length - 1;
function getReadableCoins(coins){
return (parseInt(coins || 0) / coinUnits).toFixed(coinDecimals);
}
$(function(){
var password = prompt('Admin password:');
$.get(api + '/admin_stats', {password: password}, function(data){
renderData(data);
});
getStats();
});
var docCookies = {
getItem: function (sKey) {
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
return true;
},
removeItem: function (sKey, sPath, sDomain) {
if (!sKey || !this.hasItem(sKey)) { return false; }
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : "");
return true;
},
hasItem: function (sKey) {
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
}
};
function getReadableCoins(coins){
return (parseInt(coins || 0) / coinUnits).toFixed(coinUnits.toString().length - 1);
}
function getStats(promptPassword){
var password = docCookies.getItem('password');
if (!password || promptPassword)
password = prompt('Enter admin password');
$('#loading').show();
$.ajax({
url: api + '/admin_stats',
data: {password: password},
success: function(data){
docCookies.setItem('password', password, Infinity);
$('#loading').hide();
renderData(data);
},
error: function(e){
docCookies.removeItem('password');
getStats(true);
}
});
}
var formatLuck = function(difficulty, shares){
if (difficulty > shares){
var percent = 100 - Math.round(shares / difficulty * 100);
return '<span class="luckGood">' + percent + '%</span>';
}
else{
var percent = (100 - Math.round(difficulty / shares * 100)) * -1;
return '<span class="luckBad">' + percent + '%</span>';
}
};
function renderData(data){
$('#totalOwed').text(getReadableCoins(data.totalOwed));
$('#totalPaid').text(getReadableCoins(data.totalPaid));
$('#totalMined').text(getReadableCoins(data.totalRevenue));
$('#profit').text(getReadableCoins(data.totalRevenue - data.totalOwed - data.totalPaid));
$('#averageLuck').html(formatLuck(data.totalDiff, data.totalShares));
$('#orphanPercent').text((data.blocksOrphaned / data.blocksUnlocked * 100).toFixed(2));
$('#registeredAddresses').text(data.totalWorkers);
}
</script>
@ -43,11 +126,29 @@
</head>
<body>
<div>
<span>Total Owed: </span><span id="totalOwed"></span>
</div>
<div>
<span>Total Paid: </span><span id="totalPaid"></span>
<div class="container">
<h3>Admin Center <i id="loading" class="fa fa-circle-o-notch fa-spin"></i></h3>
<hr>
<h4>Stats</h4>
<dl class="dl-horizontal" id="statsHolder">
<dt>Total Owed</dt><dd id="totalOwed">...</dd>
<dt>Total Paid</dt><dd id="totalPaid">...</dd>
<dt>Total Mined</dt><dd id="totalMined">...</dd>
<dt>Profit (before tx fees)</dt><dd id="profit">...</dd>
<dt>Average Luck</dt><dd id="averageLuck">...</dd>
<dt>Orphan Percent</dt><dd id="orphanPercent">...</dd>
<dt>Registered Addresses</dt><dd id="registeredAddresses">...</dd>
</dl>
<br>
<hr>
<h4>Miner Lookup</h4>
</div>
</body>

View File

@ -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 '<span class="' + (percent < 100 ? 'luckBad' : 'luckGood') + '">' + percent + '%</span>';
var formatLuck = function(difficulty, shares){
if (difficulty > shares){
var percent = 100 - Math.round(shares / difficulty * 100);
return '<span class="luckGood">&nbsp;' + percent + '%</span>';
}
else{
var percent = (100 - Math.round(difficulty / shares * 100)) * -1;
return '<span class="luckBad">' + percent + '%</span>';
}
};
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 = '<tr data-height="' + block.height + '" id="blockMatured' + block.height + '" title="' + block.status
+ '" class="' + blockStatusClasses[block.status] + '">' +
'<td>' + block.height + '</td>' +
'<td>' + (block.status === 'pending' ? getMaturity(depth, chainHeight, block.height) : '') + '</td>' +
'<td>' + block.difficulty + '</td>' +
'<td><a target="_blank" href="' + explorer + block.hash + '">' + block.hash + '</a></td>' +
'<td>' + formatDate(block.time) + '</td>' +
'<td>' + formatLuck(block.difficulty, block.shares) + '</td>' +
'</tr>';
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 += ('<tr title="' + block[0] + '" class="' + blockStatusClasses[block[0]] + '">' +
'<td>' + block[1] + '</td>' +
'<td>' + (block[0] === 'pending' ? getMaturity(depth, chainHeight, blocks[1]) : '') + '</td>' +
'<td>' + block[2] + '</td>' +
'<td><a target="_blank" href="' + explorer + block[3] + '">' + block[3] + '</a></td>' +
'<td>' + formatDate(block[4]) + '</td>' +
'<td>' + formatLuck(blockLuck) + '</td>' +
'</tr>');
}
$('#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);
}
})
});
});
</script>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
@ -674,9 +716,13 @@
<div class="stats">
<h3>Your Stats</h3>
<label for="yourStatsInput"><i class="fa fa-key"></i> Address</label><br>
<input class="form-control" id="yourStatsInput" type="text">
<button class="btn btn-default" id="lookUp"><i class="fa fa-search"></i> Lookup</button>
<div class="input-group">
<label class="input-group-addon" for="yourStatsInput"><i class="fa fa-key"></i> Address</label>
<input class="form-control" id="yourStatsInput" type="text">
<span class="input-group-btn"><button class="btn btn-default" type="button" id="lookUp"><i class="fa fa-search"></i> Lookup</button></span>
</div>
<div id="addressError"></div>
<div class="yourStats"><i class="fa fa-key"></i> Address: <span id="yourAddressDisplay"></span></div>
<div class="yourStats"><i class="fa fa-bank"></i> Pending Balance: <span id="yourPendingBalance"></span></div>
@ -690,12 +736,8 @@
<div class="page" id="page_pool_blocks">
<div class="blocksStatHolder">
<h4>Block Candidates</h4>
<span><span id="blocksCountPending"></span> Maturing</span>
<span class="bg-success"><span id="blocksCountUnlocked"></span> Unlocked</span>
<span class="bg-danger"><span id="blocksCountOrphaned"></span> Orphaned</span>
<span class="bg-primary"><span id="blocksTotal"></span> Total Blocks Mined</span>
<span class="bg-info">Maturity requires <span id="blocksMaturityCount"></span> blocks</span>
<span><span id="averageBlockLuck"></span> Luck Average</span>
</div>
<hr>
<div class="table-responsive">
@ -714,6 +756,11 @@
</tbody>
</table>
<p class="text-center">
<button type="button" class="btn btn-default" id="loadMoreBlocks">Load More</button>
</p>
</div>
</div>