[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.
parent
b3f0af041b
commit
b4f49e953a
|
@ -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. */
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"hashrateWindow": 600,
|
||||
"updateInterval": 5,
|
||||
"port": 8117,
|
||||
"blocks": 30,
|
||||
"password": "your_password"
|
||||
},
|
||||
|
||||
|
|
19
init.js
19
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');
|
||||
|
||||
|
|
116
lib/api.js
116
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;
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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";
|
29
lib/pool.js
29
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]
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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"> ' + 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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue