mirror of https://github.com/BTCPrivate/z-nomp.git
commit
86778a2ae2
71
libs/api.js
71
libs/api.js
|
@ -5,7 +5,6 @@ var stats = require('./stats.js');
|
|||
|
||||
module.exports = function(logger, portalConfig, poolConfigs){
|
||||
|
||||
|
||||
var _this = this;
|
||||
|
||||
var portalStats = this.stats = new stats(logger, portalConfig, poolConfigs);
|
||||
|
@ -13,12 +12,80 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
this.liveStatConnections = {};
|
||||
|
||||
this.handleApiRequest = function(req, res, next){
|
||||
|
||||
switch(req.params.method){
|
||||
case 'stats':
|
||||
res.end(portalStats.statsString);
|
||||
return;
|
||||
case 'pool_stats':
|
||||
res.end(JSON.stringify(portalStats.statPoolHistory));
|
||||
return;
|
||||
case 'worker_stats':
|
||||
if (req.url.indexOf("?")>0) {
|
||||
var url_parms = req.url.split("?");
|
||||
if (url_parms.length > 0) {
|
||||
var history = {};
|
||||
var workers = {};
|
||||
var address = url_parms[1] || null;
|
||||
//res.end(portalStats.getWorkerStats(address));
|
||||
if (address != null && address.length > 0 && address.startsWith('t')) {
|
||||
// make sure it is just the miners address
|
||||
address = address.split(".")[0];
|
||||
// get miners balance along with worker balances
|
||||
portalStats.getBalanceByAddress(address, function(balances) {
|
||||
// get current round share total
|
||||
portalStats.getTotalSharesByAddress(address, function(shares) {
|
||||
var totalHash = parseFloat(0.0);
|
||||
var totalHeld = parseFloat(0.0);
|
||||
var totalPaid = parseFloat(0.0);
|
||||
var totalShares = shares;
|
||||
var networkSols = 0;
|
||||
for (var h in portalStats.statHistory) {
|
||||
for(var pool in portalStats.statHistory[h].pools) {
|
||||
for(var w in portalStats.statHistory[h].pools[pool].workers){
|
||||
if (w.startsWith(address)) {
|
||||
if (history[w] == null) {
|
||||
history[w] = [];
|
||||
}
|
||||
if (portalStats.statHistory[h].pools[pool].workers[w].hashrate) {
|
||||
history[w].push({time: portalStats.statHistory[h].time, hashrate:portalStats.statHistory[h].pools[pool].workers[w].hashrate});
|
||||
}
|
||||
}
|
||||
}
|
||||
// order check...
|
||||
//console.log(portalStats.statHistory[h].time);
|
||||
}
|
||||
}
|
||||
networkSols = portalStats.statHistory[h].pools[pool].poolStats.networkSols;
|
||||
// note, h is the last record from above loop, which is latest
|
||||
for(var pool in portalStats.stats.pools) {
|
||||
for(var w in portalStats.stats.pools[pool].workers){
|
||||
if (w.startsWith(address)) {
|
||||
workers[w] = portalStats.stats.pools[pool].workers[w];
|
||||
for (var b in balances.balances) {
|
||||
if (w == balances.balances[b].worker) {
|
||||
workers[w].paid = balances.balances[b].paid;
|
||||
workers[w].balance = balances.balances[b].balance;
|
||||
}
|
||||
}
|
||||
workers[w].balance = (workers[w].balance || 0);
|
||||
workers[w].paid = (workers[w].paid || 0);
|
||||
totalHash += portalStats.statHistory[h].pools[pool].workers[w].hashrate;
|
||||
}
|
||||
}
|
||||
}
|
||||
res.end(JSON.stringify({miner: address, totalHash: totalHash, totalShares: totalShares, networkSols: networkSols, balance: balances.totalHeld, paid: balances.totalPaid, workers: workers, history: history}));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.end(JSON.stringify({result: "error"}));
|
||||
}
|
||||
} else {
|
||||
res.end(JSON.stringify({result: "error"}));
|
||||
}
|
||||
} else {
|
||||
res.end(JSON.stringify({result: "error"}));
|
||||
}
|
||||
return;
|
||||
case 'live_stats':
|
||||
res.writeHead(200, {
|
||||
|
@ -32,14 +99,12 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
req.on("close", function() {
|
||||
delete _this.liveStatConnections[uid];
|
||||
});
|
||||
|
||||
return;
|
||||
default:
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.handleAdminApiRequest = function(req, res, next){
|
||||
switch(req.params.method){
|
||||
case 'pools': {
|
||||
|
|
|
@ -41,7 +41,6 @@ module.exports = function(logger){
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
function SetupForPool(logger, poolOptions, setupFinished){
|
||||
|
||||
|
||||
|
@ -50,6 +49,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
|
||||
var logSystem = 'Payments';
|
||||
var logComponent = coin;
|
||||
var opidCount = 0;
|
||||
|
||||
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){
|
||||
logger[severity](logSystem, logComponent, message);
|
||||
|
@ -113,7 +113,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function getBalance(callback){
|
||||
daemon.cmd('getbalance', [], function(result){
|
||||
if (result.error){
|
||||
|
@ -138,6 +137,9 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
setupFinished(false);
|
||||
return;
|
||||
}
|
||||
if (paymentInterval) {
|
||||
clearInterval(paymentInterval);
|
||||
}
|
||||
paymentInterval = setInterval(function(){
|
||||
try {
|
||||
processPayments();
|
||||
|
@ -154,28 +156,30 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
//get t_address coinbalance
|
||||
function listUnspent (addr, notAddr, minConf, displayBool, callback) {
|
||||
if (addr !== null) {
|
||||
var args = [minConf, 99999999999, [addr]];
|
||||
var args = [minConf, 99999999, [addr]];
|
||||
} else {
|
||||
addr = 'Payment-ready wallet';
|
||||
var args = [minConf, 99999999999];
|
||||
addr = 'Payout wallet';
|
||||
var args = [minConf, 99999999];
|
||||
}
|
||||
daemon.cmd('listunspent', args, function (result) {
|
||||
//Check if payments failed because wallet doesn't have enough coins to pay for tx fees
|
||||
if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error trying to get coin balance with RPC listunspent.'
|
||||
logger.error(logSystem, logComponent, 'Error trying to get t-addr ['+addr+'] balance with RPC listunspent.'
|
||||
+ JSON.stringify(result.error));
|
||||
callback = function (){};
|
||||
callback(true);
|
||||
}
|
||||
else {
|
||||
var tBalance = 0;
|
||||
for (var i = 0, len = result[0].response.length; i < len; i++) {
|
||||
if (result[0].response[i].address !== notAddr) {
|
||||
tBalance = tBalance + (result[0].response[i].amount * magnitude);
|
||||
if (result[0].response != null && result[0].response.length > 0) {
|
||||
for (var i = 0, len = result[0].response.length; i < len; i++) {
|
||||
if (result[0].response[i].address !== notAddr) {
|
||||
tBalance = tBalance + (result[0].response[i].amount * magnitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayBool === true) {
|
||||
logger.special(logSystem, logComponent, addr + ' contains a balance of ' + (tBalance / magnitude).toFixed(8));
|
||||
logger.special(logSystem, logComponent, addr+' balance of ' + (tBalance / magnitude).toFixed(8));
|
||||
}
|
||||
callback(null, tBalance.toFixed(8));
|
||||
}
|
||||
|
@ -192,9 +196,12 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
callback(true);
|
||||
}
|
||||
else {
|
||||
var zBalance = result[0].response;
|
||||
var zBalance = 0;
|
||||
if (result[0].response != null) {
|
||||
zBalance = result[0].response;
|
||||
}
|
||||
if (displayBool === true) {
|
||||
logger.special(logSystem, logComponent, addr + ' contains a balance of ' + (zBalance).toFixed(8));
|
||||
logger.special(logSystem, logComponent, addr.substring(0,14) + '...' + addr.substring(addr.length - 14) + ' balance: '+(zBalance).toFixed(8));
|
||||
}
|
||||
callback(null, (zBalance * magnitude).toFixed(8));
|
||||
}
|
||||
|
@ -207,57 +214,176 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
return;
|
||||
if ((tBalance - 10000) < 0)
|
||||
return;
|
||||
daemon.cmd('z_sendmany', [poolOptions.address,
|
||||
[{'address': poolOptions.zAddress, 'amount': ((tBalance - 10000) / magnitude)}]],
|
||||
|
||||
// do not allow more than a single z_sendmany operation at a time
|
||||
if (opidCount > 0) {
|
||||
logger.warning(logSystem, logComponent, 'sendTToZ is waiting, too many z_sendmany operations already in progress.');
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = balanceRound((tBalance - 10000) / magnitude);
|
||||
var params = [poolOptions.address, [{'address': poolOptions.zAddress, 'amount': amount}]];
|
||||
daemon.cmd('z_sendmany', params,
|
||||
function (result) {
|
||||
//Check if payments failed because wallet doesn't have enough coins to pay for tx fees
|
||||
if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error trying to send t_address coin balance to z_address.' + JSON.stringify(result.error));
|
||||
logger.error(logSystem, logComponent, 'Error trying to shield mined balance ' + JSON.stringify(result.error));
|
||||
callback = function (){};
|
||||
callback(true);
|
||||
}
|
||||
else {
|
||||
logger.special(logSystem, logComponent, 'Sent tAddress balance to z_address: ' + ((tBalance - 10000) / magnitude));
|
||||
opidCount++;
|
||||
logger.special(logSystem, logComponent, 'Shield mined balance ' + amount);
|
||||
callback = function (){};
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// send z_address balance to t_address
|
||||
function sendZToT (callback, zBalance) {
|
||||
if (callback === true)
|
||||
return;
|
||||
if ((zBalance - 10000) < 0)
|
||||
return;
|
||||
daemon.cmd('z_sendmany', [poolOptions.zAddress,
|
||||
[{'address': poolOptions.tAddress, 'amount': ((zBalance - 10000) / magnitude)}]],
|
||||
|
||||
// do not allow more than a single z_sendmany operation at a time
|
||||
if (opidCount > 0) {
|
||||
logger.warning(logSystem, logComponent, 'sendZToT is waiting, too many z_sendmany operations already in progress.');
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = balanceRound((zBalance - 10000) / magnitude);
|
||||
// no more than 100 ZEC at a time
|
||||
if (amount > 100.0)
|
||||
amount = 100.0;
|
||||
|
||||
var params = [poolOptions.zAddress, [{'address': poolOptions.tAddress, 'amount': amount}]];
|
||||
daemon.cmd('z_sendmany', params,
|
||||
function (result) {
|
||||
//Check if payments failed because wallet doesn't have enough coins to pay for tx fees
|
||||
if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error trying to send z_address coin balance to t_address.'
|
||||
logger.error(logSystem, logComponent, 'Error trying to send z_address coin balance to payout t_address.'
|
||||
+ JSON.stringify(result.error));
|
||||
callback = function (){};
|
||||
callback(true);
|
||||
}
|
||||
else {
|
||||
logger.special(logSystem, logComponent, 'Sent zAddress balance to t_address: ' + ((zBalance - 10000) / magnitude));
|
||||
opidCount++;
|
||||
logger.special(logSystem, logComponent, 'Unshield funds for payout ' + amount);
|
||||
callback = function (){};
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function cacheZCashNetworkStats () {
|
||||
var params = null;
|
||||
daemon.cmd('getmininginfo', params,
|
||||
function (result) {
|
||||
if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error getting stats from zcashd'
|
||||
+ JSON.stringify(result.error));
|
||||
} else {
|
||||
logger.special(logSystem, logComponent, "Updating "+logComponent+" network stats...");
|
||||
var coin = logComponent;
|
||||
var finalRedisCommands = [];
|
||||
finalRedisCommands.push(['hset', coin + ':stats', 'networkBlocks', result[0].response.blocks]);
|
||||
finalRedisCommands.push(['hset', coin + ':stats', 'networkDiff', result[0].response.difficulty]);
|
||||
finalRedisCommands.push(['hset', coin + ':stats', 'networkSols', result[0].response.networksolps]);
|
||||
redisClient.multi(finalRedisCommands).exec(function(error, results){
|
||||
if (error){
|
||||
logger.error(logSystem, logComponent, 'Could not update zcash stats to redis ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
daemon.cmd('getinfo', params,
|
||||
function (result) {
|
||||
if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error getting stats from zcashd'
|
||||
+ JSON.stringify(result.error));
|
||||
} else {
|
||||
var coin = logComponent;
|
||||
var finalRedisCommands = [];
|
||||
finalRedisCommands.push(['hset', coin + ':stats', 'networkConnections', result[0].response.connections]);
|
||||
redisClient.multi(finalRedisCommands).exec(function(error, results){
|
||||
if (error){
|
||||
logger.error(logSystem, logComponent, 'Could not update zcash stats to redis ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// run coinbase coin transfers every x minutes
|
||||
var intervalState = 0; // do not send ZtoT and TtoZ and same time, this results in operation failed!
|
||||
var interval = poolOptions.walletInterval * 60 * 1000; // run every x minutes
|
||||
setInterval(function() {
|
||||
listUnspent(poolOptions.address, null, 1, true, sendTToZ);
|
||||
listUnspentZ(poolOptions.zAddress, 1, true, sendZToT);
|
||||
listUnspent(null, poolOptions.address, 1, true, function (){});
|
||||
intervalState++;
|
||||
switch (intervalState){
|
||||
case 1:
|
||||
listUnspent(poolOptions.address, null, 1, false, sendTToZ);
|
||||
break;
|
||||
default:
|
||||
listUnspentZ(poolOptions.zAddress, 1, false, sendZToT);
|
||||
//listUnspent(null, poolOptions.address, 1, true, function (){});
|
||||
intervalState = 0;
|
||||
break;
|
||||
}
|
||||
// update zcash stats
|
||||
cacheZCashNetworkStats();
|
||||
}, interval);
|
||||
|
||||
// check operation statuses every x seconds
|
||||
var opid_interval = poolOptions.walletInterval * 1000;
|
||||
setInterval(function(){
|
||||
var checkOpIdSuccessAndGetResult = function(ops) {
|
||||
ops.forEach(function(op, i){
|
||||
if (op.status == "success" || op.status == "failed") {
|
||||
daemon.cmd('z_getoperationresult', [[op.id]], function (result) {
|
||||
if (result.error) {
|
||||
logger.warning(logSystem, logComponent, 'Unable to get payment operation id result ' + JSON.stringify(result));
|
||||
}
|
||||
if (result.response) {
|
||||
if (opidCount > 0) {
|
||||
opidCount = 0;
|
||||
}
|
||||
if (op.status == "failed") {
|
||||
if (op.error) {
|
||||
logger.error(logSystem, logComponent, "Payment operation failed " + op.id + " " + op.error.code +", " + op.error.message);
|
||||
} else {
|
||||
logger.error(logSystem, logComponent, "Payment operation failed " + op.id);
|
||||
}
|
||||
} else {
|
||||
logger.special(logSystem, logComponent, 'Payment operation success ' + op.id + ' txid: ' + op.result.txid);
|
||||
}
|
||||
}
|
||||
}, true, true);
|
||||
} else if (op.status == "executing") {
|
||||
if (opidCount == 0) {
|
||||
opidCount++;
|
||||
logger.special(logSystem, logComponent, 'Payment operation in progress ' + op.id );
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
daemon.cmd('z_getoperationstatus', null, function (result) {
|
||||
if (result.error) {
|
||||
logger.warning(logSystem, logComponent, 'Unable to get operation ids for clearing.');
|
||||
}
|
||||
if (result.response) {
|
||||
checkOpIdSuccessAndGetResult(result.response);
|
||||
}
|
||||
}, true, true);
|
||||
}, opid_interval);
|
||||
|
||||
|
||||
var satoshisToCoins = function(satoshis){
|
||||
return parseFloat((satoshis / magnitude).toFixed(coinPrecision));
|
||||
|
@ -267,6 +393,10 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
return coins * magnitude;
|
||||
};
|
||||
|
||||
function balanceRound(number) {
|
||||
return parseFloat((Math.round(number * 100000000) / 100000000).toFixed(8));
|
||||
}
|
||||
|
||||
/* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy
|
||||
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
||||
|
||||
|
@ -290,8 +420,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
|
||||
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
|
||||
blocks. */
|
||||
function(callback){
|
||||
|
||||
function(callback){
|
||||
startRedisTimer();
|
||||
redisClient.multi([
|
||||
['hgetall', coin + ':balances'],
|
||||
|
@ -323,119 +452,217 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
callback(null, workers, rounds);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
/* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
|
||||
It also adds the block reward amount to the round object - which the daemon gives also gives us. */
|
||||
function(workers, rounds, callback){
|
||||
|
||||
var batchRPCcommand = rounds.map(function(r){
|
||||
return ['gettransaction', [r.txHash]];
|
||||
// first verify block confirmations by block hash
|
||||
var batchRPCcommand2 = rounds.map(function(r){
|
||||
return ['getblock', [r.blockHash]];
|
||||
});
|
||||
|
||||
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
|
||||
|
||||
// guarantee a response for batchRPCcommand2
|
||||
batchRPCcommand2.push(['getblockcount']);
|
||||
|
||||
startRPCTimer();
|
||||
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
|
||||
daemon.batchCmd(batchRPCcommand2, function(error, blockDetails){
|
||||
endRPCTimer();
|
||||
|
||||
if (error || !txDetails){
|
||||
logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch gettransactions '
|
||||
|
||||
// error getting block info by hash?
|
||||
if (error || !blockDetails){
|
||||
logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch getblock '
|
||||
+ JSON.stringify(error));
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var addressAccount;
|
||||
|
||||
txDetails.forEach(function(tx, i){
|
||||
|
||||
if (i === txDetails.length - 1){
|
||||
addressAccount = tx.result;
|
||||
return;
|
||||
}
|
||||
|
||||
var round = rounds[i];
|
||||
|
||||
if (tx.error && tx.error.code === -5){
|
||||
logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash);
|
||||
round.category = 'kicked';
|
||||
return;
|
||||
}
|
||||
else if (!tx.result.details || (tx.result.details && tx.result.details.length === 0)){
|
||||
logger.warning(logSystem, logComponent, 'Daemon reports no details for transaction: ' + round.txHash);
|
||||
round.category = 'kicked';
|
||||
return;
|
||||
}
|
||||
else if (tx.error || !tx.result){
|
||||
logger.error(logSystem, logComponent, 'Odd error with gettransaction ' + round.txHash + ' '
|
||||
+ JSON.stringify(tx));
|
||||
return;
|
||||
}
|
||||
|
||||
var generationTx = tx.result.details.filter(function(tx){
|
||||
return tx.address === poolOptions.address;
|
||||
})[0];
|
||||
|
||||
if (!generationTx && tx.result.details.length === 1){
|
||||
generationTx = tx.result.details[0];
|
||||
}
|
||||
|
||||
if (!generationTx){
|
||||
logger.error(logSystem, logComponent, 'Missing output details to pool address for transaction '
|
||||
+ round.txHash);
|
||||
return;
|
||||
}
|
||||
|
||||
round.category = generationTx.category;
|
||||
if (round.category === 'generate') {
|
||||
round.reward = generationTx.amount - 0.0004 || generationTx.value - 0.0004; // TODO: Adjust fees to be dynamic
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var canDeleteShares = function(r){
|
||||
for (var i = 0; i < rounds.length; i++){
|
||||
var compareR = rounds[i];
|
||||
if ((compareR.height === r.height)
|
||||
&& (compareR.category !== 'kicked')
|
||||
&& (compareR.category !== 'orphan')
|
||||
&& (compareR.serialized !== r.serialized)){
|
||||
return false;
|
||||
|
||||
// update confirmations in redis for pending blocks
|
||||
var confirmsUpdate = blockDetails.map(function(b){
|
||||
if (b.result != null && b.result.confirmations > 0) {
|
||||
if (b.result.confirmations > 100) {
|
||||
return ['hdel', logComponent + ':blocksPendingConfirms', b.result.hash];
|
||||
}
|
||||
return ['hset', logComponent + ':blocksPendingConfirms', b.result.hash, b.result.confirmations];
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
//Filter out all rounds that are immature (not confirmed or orphaned yet)
|
||||
rounds = rounds.filter(function(r){
|
||||
switch (r.category) {
|
||||
case 'orphan':
|
||||
case 'kicked':
|
||||
r.canDeleteShares = canDeleteShares(r);
|
||||
case 'generate':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// check if we have enough tAddress funds to send payments
|
||||
var totalOwed = 0;
|
||||
for (var i = 0; i < rounds.length; i++) {
|
||||
totalOwed = totalOwed + (rounds[i].reward * magnitude) - 4000; // TODO: make tx fees dynamic
|
||||
}
|
||||
listUnspent(null, poolOptions.address, 1, false, function (error, tBalance){
|
||||
if (tBalance < totalOwed) {
|
||||
logger.error(logSystem, logComponent, (tBalance / magnitude).toFixed(8) + ' is not enough payment funds to process ' + (totalOwed / magnitude).toFixed(8) + ' of payments. (Possibly due to pending txs)');
|
||||
return callback(true);
|
||||
|
||||
// filter nulls, last item is always null...
|
||||
confirmsUpdate = confirmsUpdate.filter(function(val) { return val !== null; });
|
||||
// guarantee at least one redis update
|
||||
if (confirmsUpdate.length < 1)
|
||||
confirmsUpdate.push(['hset', logComponent + ':blocksPendingConfirms', 0, 0]);
|
||||
|
||||
startRedisTimer();
|
||||
redisClient.multi(confirmsUpdate).exec(function(error, updated){
|
||||
endRedisTimer();
|
||||
|
||||
if (error){
|
||||
logger.error(logSystem, logComponent, 'failed to update pending block confirmations'
|
||||
+ JSON.stringify(error));
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
callback(null, workers, rounds, addressAccount);
|
||||
}
|
||||
})
|
||||
|
||||
// check for invalid blocks by block hash
|
||||
blockDetails.forEach(function(block, i) {
|
||||
// this is just the response from getblockcount
|
||||
if (i === blockDetails.length - 1){
|
||||
return;
|
||||
}
|
||||
// help track duplicate or invalid blocks by block hash
|
||||
if (block && block.result && block.result.hash) {
|
||||
// find the round for this block hash
|
||||
for (var k=0; k < rounds.length; k++) {
|
||||
if (rounds[k].blockHash == block.result.hash) {
|
||||
var round = rounds[k];
|
||||
var dupFound = false;
|
||||
// duplicate, invalid, kicked, orphaned blocks will have negative confirmations
|
||||
if (block.result.confirmations < 0) {
|
||||
// check if this is an invalid duplicate
|
||||
// we need to kick invalid duplicates now, as this will cause a double payout...
|
||||
for (var d=0; d < rounds.length; d++) {
|
||||
if (rounds[d].height == block.result.height && rounds[d].blockHash != block.result.hash) {
|
||||
logger.warning(logSystem, logComponent, 'Kicking invalid duplicate block ' +round.height + ' > ' + round.blockHash);
|
||||
dupFound = true;
|
||||
// kick this round now, its completely invalid!
|
||||
var kickNow = [];
|
||||
kickNow.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', round.serialized]);
|
||||
startRedisTimer();
|
||||
redisClient.multi(kickNow).exec(function(error, kicked){
|
||||
endRedisTimer();
|
||||
if (error){
|
||||
logger.error(logSystem, logComponent, 'Error could not kick invalid duplicate block ' + JSON.stringify(kicked));
|
||||
}
|
||||
});
|
||||
// filter the duplicate out now, just in case we are actually paying this time around...
|
||||
rounds = rounds.filter(function(item){ return item.txHash != round.txHash; });
|
||||
}
|
||||
}
|
||||
// unknown reason why this block failed, possible orphan or kicked soon
|
||||
// not sure if we should take any action or just wait it out...
|
||||
if (!dupFound) {
|
||||
logger.warning(logSystem, logComponent, 'Daemon reports negative confirmations '+block.result.confirmations+' for block: ' +round.height + ' > ' + round.blockHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// now check block transaction ids
|
||||
var batchRPCcommand = rounds.map(function(r){
|
||||
return ['gettransaction', [r.txHash]];
|
||||
});
|
||||
// guarantee a response for batchRPCcommand
|
||||
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
|
||||
|
||||
startRPCTimer();
|
||||
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
|
||||
endRPCTimer();
|
||||
|
||||
if (error || !txDetails){
|
||||
logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch gettransactions '
|
||||
+ JSON.stringify(error));
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var addressAccount = "";
|
||||
|
||||
// check for transaction errors and generated coins
|
||||
txDetails.forEach(function(tx, i){
|
||||
|
||||
if (i === txDetails.length - 1){
|
||||
addressAccount = tx.result;
|
||||
return;
|
||||
}
|
||||
|
||||
var round = rounds[i];
|
||||
if (tx.error && tx.error.code === -5){
|
||||
logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash);
|
||||
round.category = 'kicked';
|
||||
return;
|
||||
}
|
||||
else if (!tx.result.details || (tx.result.details && tx.result.details.length === 0)){
|
||||
logger.warning(logSystem, logComponent, 'Daemon reports no details for transaction: ' + round.txHash);
|
||||
round.category = 'kicked';
|
||||
return;
|
||||
}
|
||||
else if (tx.error || !tx.result){
|
||||
logger.error(logSystem, logComponent, 'Odd error with gettransaction ' + round.txHash + ' '
|
||||
+ JSON.stringify(tx));
|
||||
return;
|
||||
}
|
||||
|
||||
var generationTx = tx.result.details.filter(function(tx){
|
||||
return tx.address === poolOptions.address;
|
||||
})[0];
|
||||
|
||||
if (!generationTx && tx.result.details.length === 1){
|
||||
generationTx = tx.result.details[0];
|
||||
}
|
||||
|
||||
if (!generationTx){
|
||||
logger.error(logSystem, logComponent, 'Missing output details to pool address for transaction '
|
||||
+ round.txHash);
|
||||
return;
|
||||
}
|
||||
|
||||
round.category = generationTx.category;
|
||||
if (round.category === 'generate') {
|
||||
round.reward = generationTx.amount - 0.0004 || generationTx.value - 0.0004; // TODO: Adjust fees to be dynamic
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var canDeleteShares = function(r){
|
||||
for (var i = 0; i < rounds.length; i++){
|
||||
var compareR = rounds[i];
|
||||
if ((compareR.height === r.height)
|
||||
&& (compareR.category !== 'kicked')
|
||||
&& (compareR.category !== 'orphan')
|
||||
&& (compareR.serialized !== r.serialized)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
//Filter out all rounds that are immature (not confirmed or orphaned yet)
|
||||
rounds = rounds.filter(function(r){
|
||||
switch (r.category) {
|
||||
case 'orphan':
|
||||
case 'kicked':
|
||||
r.canDeleteShares = canDeleteShares(r);
|
||||
case 'generate':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// check if we have enough tAddress funds to send payments
|
||||
var totalOwed = 0;
|
||||
for (var i = 0; i < rounds.length; i++) {
|
||||
totalOwed = totalOwed + (rounds[i].reward * magnitude) - 4000; // TODO: make tx fees dynamic
|
||||
}
|
||||
|
||||
listUnspent(null, poolOptions.address, 1, false, function (error, tBalance){
|
||||
if (tBalance < totalOwed) {
|
||||
logger.error(logSystem, logComponent, (tBalance / magnitude).toFixed(8) + ' is not enough payment funds to process ' + (totalOwed / magnitude).toFixed(8) + ' of payments. (Possibly due to pending txs)');
|
||||
return callback(true);
|
||||
}
|
||||
else {
|
||||
// zcash daemon does not support account feature
|
||||
addressAccount = "";
|
||||
callback(null, workers, rounds, addressAccount);
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -444,7 +671,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
amount owned to each miner for each round. */
|
||||
function(workers, rounds, addressAccount, callback){
|
||||
|
||||
|
||||
var shareLookups = rounds.map(function(r){
|
||||
return ['hgetall', coin + ':shares:round' + r.height]
|
||||
});
|
||||
|
@ -458,7 +684,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
rounds.forEach(function(round, i){
|
||||
var workerShares = allWorkerShares[i];
|
||||
|
||||
|
@ -467,7 +692,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
+ round.height + ' blockHash: ' + round.blockHash);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
switch (round.category){
|
||||
case 'kicked':
|
||||
case 'orphan':
|
||||
|
@ -487,6 +712,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
var percent = parseFloat(workerShares[workerAddress]) / totalShares;
|
||||
var workerRewardTotal = Math.floor(reward * percent);
|
||||
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
||||
worker.totalShares = (worker.totalShares || 0) + parseFloat(workerShares[workerAddress]);
|
||||
worker.reward = (worker.reward || 0) + workerRewardTotal;
|
||||
}
|
||||
break;
|
||||
|
@ -508,55 +734,149 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
|
||||
var trySend = function (withholdPercent) {
|
||||
var addressAmounts = {};
|
||||
var minerTotals = {};
|
||||
var totalSent = 0;
|
||||
var totalShares = 0;
|
||||
// total up miner's balances
|
||||
for (var w in workers) {
|
||||
var worker = workers[w];
|
||||
totalShares += (worker.totalShares || 0)
|
||||
worker.balance = worker.balance || 0;
|
||||
worker.reward = worker.reward || 0;
|
||||
var toSend = balanceRound(satoshisToCoins(Math.floor((worker.balance + worker.reward) * (1 - withholdPercent))));
|
||||
var address = worker.address = (worker.address || getProperAddress(w.split('.')[0]));
|
||||
if (minerTotals[address] != null && minerTotals[address] > 0) {
|
||||
minerTotals[address] = balanceRound(minerTotals[address] + toSend);
|
||||
} else {
|
||||
minerTotals[address] = toSend;
|
||||
}
|
||||
}
|
||||
// now process each workers balance, and pay the miner
|
||||
for (var w in workers) {
|
||||
var worker = workers[w];
|
||||
worker.balance = worker.balance || 0;
|
||||
worker.reward = worker.reward || 0;
|
||||
var toSend = (worker.balance + worker.reward) * (1 - withholdPercent);
|
||||
if (toSend >= minPaymentSatoshis) {
|
||||
var toSend = Math.floor((worker.balance + worker.reward) * (1 - withholdPercent));
|
||||
var address = worker.address = (worker.address || getProperAddress(w.split('.')[0]));
|
||||
// if miners total is enough, go ahead and add this worker balance
|
||||
if (minerTotals[address] >= satoshisToCoins(minPaymentSatoshis)) {
|
||||
totalSent += toSend;
|
||||
var address = worker.address = (worker.address || getProperAddress(w));
|
||||
worker.sent = addressAmounts[address] = satoshisToCoins(toSend);
|
||||
worker.sent = balanceRound(satoshisToCoins(toSend));
|
||||
worker.balanceChange = Math.min(worker.balance, toSend) * -1;
|
||||
// multiple workers may have same address, add them up
|
||||
if (addressAmounts[address] != null && addressAmounts[address] > 0) {
|
||||
addressAmounts[address] = balanceRound(addressAmounts[address] + worker.sent);
|
||||
} else {
|
||||
addressAmounts[address] = worker.sent;
|
||||
}
|
||||
}
|
||||
else {
|
||||
worker.balanceChange = Math.max(toSend - worker.balance, 0);
|
||||
worker.sent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if no payouts...continue to next set of callbacks
|
||||
if (Object.keys(addressAmounts).length === 0){
|
||||
callback(null, workers, rounds);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(addressAmounts);
|
||||
daemon.cmd('sendmany', [addressAccount || '', addressAmounts], function (result) {
|
||||
//Check if payments failed because wallet doesn't have enough coins to pay for tx fees
|
||||
/*
|
||||
var undoPaymentsOnError = function(workers) {
|
||||
totalSent = 0;
|
||||
// TODO, set round.category to immature, to attempt to pay again
|
||||
// we did not send anything to any workers
|
||||
for (var w in workers) {
|
||||
var worker = workers[w];
|
||||
if (worker.sent > 0) {
|
||||
worker.balanceChange = 0;
|
||||
worker.sent = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
*/
|
||||
// perform the sendmany operation
|
||||
daemon.cmd('sendmany', ["", addressAmounts], function (result) {
|
||||
// check for failed payments, there are many reasons
|
||||
if (result.error && result.error.code === -6) {
|
||||
// not enough minerals...
|
||||
var higherPercent = withholdPercent + 0.01;
|
||||
logger.warning(logSystem, logComponent, 'Not enough funds to cover the tx fees for sending out payments, decreasing rewards by '
|
||||
+ (higherPercent * 100) + '% and retrying');
|
||||
|
||||
trySend(higherPercent);
|
||||
}
|
||||
else if (result.error) {
|
||||
logger.error(logSystem, logComponent, 'Error trying to send payments with RPC sendmany '
|
||||
+ JSON.stringify(result.error));
|
||||
else if (result.error && result.error.code === -5) {
|
||||
// invalid address specified in addressAmounts array
|
||||
logger.error(logSystem, logComponent, 'Error sending payments ' + result.error.message);
|
||||
//undoPaymentsOnError(workers);
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
else if (result.error && result.error.message != null) {
|
||||
// unknown error from daemon
|
||||
logger.error(logSystem, logComponent, 'Error sending payments ' + result.error.message);
|
||||
//undoPaymentsOnError(workers);
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
else if (result.error) {
|
||||
// some other unknown error
|
||||
logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error));
|
||||
//undoPaymentsOnError(workers);
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
logger.special(logSystem, logComponent, 'Sent out a total of ' + (totalSent / magnitude)
|
||||
+ ' to ' + Object.keys(addressAmounts).length + ' workers');
|
||||
if (withholdPercent > 0) {
|
||||
logger.warning(logSystem, logComponent, 'Had to withhold ' + (withholdPercent * 100)
|
||||
+ '% of reward from miners to cover transaction fees. '
|
||||
+ 'Fund pool wallet with coins to prevent this from happening');
|
||||
|
||||
// make sure sendmany gives us back a txid
|
||||
var txid = null;
|
||||
if (result.response) {
|
||||
txid = result.response;
|
||||
}
|
||||
if (txid != null) {
|
||||
|
||||
// it worked, congrats on your pools payout ;)
|
||||
logger.special(logSystem, logComponent, 'Sent ' + (totalSent / magnitude).toFixed(8)
|
||||
+ ' to ' + Object.keys(addressAmounts).length + ' miners; txid: '+txid);
|
||||
|
||||
if (withholdPercent > 0) {
|
||||
logger.warning(logSystem, logComponent, 'Had to withhold ' + (withholdPercent * 100)
|
||||
+ '% of reward from miners to cover transaction fees. '
|
||||
+ 'Fund pool wallet with coins to prevent this from happening');
|
||||
}
|
||||
|
||||
// save payments data to redis
|
||||
var paymentBlocks = rounds.map(function(r){
|
||||
return parseInt(r.height);
|
||||
});
|
||||
var paymentsUpdate = [];
|
||||
var paymentsData = [{txid:txid, paid:balanceRound(totalSent / magnitude), shares:totalShares, miners:Object.keys(addressAmounts).length}, {blocks: paymentBlocks}, addressAmounts];
|
||||
paymentsUpdate.push(['zadd', logComponent + ':payments', Date.now(), JSON.stringify(paymentsData)]);
|
||||
startRedisTimer();
|
||||
redisClient.multi(paymentsUpdate).exec(function(error, payments){
|
||||
endRedisTimer();
|
||||
if (error){
|
||||
logger.error(logSystem, logComponent, 'Error redis save payments data ' + JSON.stringify(payments));
|
||||
}
|
||||
callback(null, workers, rounds);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
clearInterval(paymentInterval);
|
||||
|
||||
logger.error(logSystem, logComponent, 'Error RPC sendmany did not return txid '
|
||||
+ JSON.stringify(result) + 'Disabling payment processing to prevent possible double-payouts.');
|
||||
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
callback(null, workers, rounds);
|
||||
}
|
||||
}, true, true);
|
||||
|
||||
};
|
||||
|
||||
trySend(0);
|
||||
|
||||
},
|
||||
|
@ -574,31 +894,29 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
'hincrbyfloat',
|
||||
coin + ':balances',
|
||||
w,
|
||||
satoshisToCoins(worker.balanceChange)
|
||||
balanceRound(satoshisToCoins(worker.balanceChange))
|
||||
]);
|
||||
}
|
||||
if (worker.sent !== 0){
|
||||
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, worker.sent]);
|
||||
totalPaid += worker.sent;
|
||||
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, balanceRound(worker.sent)]);
|
||||
totalPaid = balanceRound(totalPaid + worker.sent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var movePendingCommands = [];
|
||||
var roundsToDelete = [];
|
||||
var orphanMergeCommands = [];
|
||||
|
||||
var moveSharesToCurrent = function(r){
|
||||
var workerShares = r.workerShares;
|
||||
Object.keys(workerShares).forEach(function(worker){
|
||||
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent',
|
||||
worker, workerShares[worker]]);
|
||||
});
|
||||
if (workerShares != null) {
|
||||
Object.keys(workerShares).forEach(function(worker){
|
||||
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent', worker, workerShares[worker]]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
rounds.forEach(function(r){
|
||||
|
||||
switch(r.category){
|
||||
case 'kicked':
|
||||
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]);
|
||||
|
@ -614,7 +932,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
roundsToDelete.push(coin + ':shares:round' + r.height);
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var finalRedisCommands = [];
|
||||
|
@ -635,7 +952,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
|||
finalRedisCommands.push(['del'].concat(roundsToDelete));
|
||||
|
||||
if (totalPaid !== 0)
|
||||
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]);
|
||||
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', balanceRound(totalPaid)]);
|
||||
|
||||
if (finalRedisCommands.length === 0){
|
||||
callback();
|
||||
|
|
|
@ -144,7 +144,7 @@ module.exports = function(logger){
|
|||
}
|
||||
}
|
||||
else {
|
||||
pool.daemon.cmd('validateaddress', [workerName], function (results) {
|
||||
pool.daemon.cmd('validateaddress', [String(workerName).split(".")[0]], function (results) {
|
||||
var isValid = results.filter(function (r) {
|
||||
return r.response.isvalid
|
||||
}).length > 0;
|
||||
|
@ -191,7 +191,8 @@ module.exports = function(logger){
|
|||
logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000.000!');
|
||||
else if(data.shareDiff > 1000000)
|
||||
logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000!');
|
||||
logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + '/' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' );
|
||||
|
||||
//logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + '/' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' );
|
||||
|
||||
} else if (!isValidShare)
|
||||
logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData);
|
||||
|
|
404
libs/stats.js
404
libs/stats.js
|
@ -2,13 +2,50 @@ var zlib = require('zlib');
|
|||
|
||||
var redis = require('redis');
|
||||
var async = require('async');
|
||||
|
||||
//var fancyTimestamp = require('fancy-timestamp');
|
||||
|
||||
var os = require('os');
|
||||
|
||||
var algos = require('stratum-pool/lib/algoProperties.js');
|
||||
|
||||
function balanceRound(number) {
|
||||
return parseFloat((Math.round(number * 100000000) / 100000000).toFixed(8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort object properties (only own properties will be sorted).
|
||||
* @param {object} obj object to sort properties
|
||||
* @param {string|int} sortedBy 1 - sort object properties by specific value.
|
||||
* @param {bool} isNumericSort true - sort object properties as numeric value, false - sort as string value.
|
||||
* @param {bool} reverse false - reverse sorting.
|
||||
* @returns {Array} array of items in [[key,value],[key,value],...] format.
|
||||
*/
|
||||
function sortProperties(obj, sortedBy, isNumericSort, reverse) {
|
||||
sortedBy = sortedBy || 1; // by default first key
|
||||
isNumericSort = isNumericSort || false; // by default text sort
|
||||
reverse = reverse || false; // by default no reverse
|
||||
|
||||
var reversed = (reverse) ? -1 : 1;
|
||||
|
||||
var sortable = [];
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
sortable.push([key, obj[key]]);
|
||||
}
|
||||
}
|
||||
if (isNumericSort)
|
||||
sortable.sort(function (a, b) {
|
||||
return reversed * (a[1][sortedBy] - b[1][sortedBy]);
|
||||
});
|
||||
else
|
||||
sortable.sort(function (a, b) {
|
||||
var x = a[1][sortedBy].toLowerCase(),
|
||||
y = b[1][sortedBy].toLowerCase();
|
||||
return x < y ? reversed * -1 : x > y ? reversed : 0;
|
||||
});
|
||||
return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
|
||||
}
|
||||
|
||||
module.exports = function(logger, portalConfig, poolConfigs){
|
||||
|
||||
var _this = this;
|
||||
|
@ -30,11 +67,9 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
var canDoStats = true;
|
||||
|
||||
Object.keys(poolConfigs).forEach(function(coin){
|
||||
|
||||
if (!canDoStats) return;
|
||||
|
||||
var poolConfig = poolConfigs[coin];
|
||||
|
||||
var redisConfig = poolConfig.redis;
|
||||
|
||||
for (var i = 0; i < redisClients.length; i++){
|
||||
|
@ -50,7 +85,6 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
function setupStatsRedis(){
|
||||
redisStats = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
|
||||
redisStats.on('error', function(err){
|
||||
|
@ -59,9 +93,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
}
|
||||
|
||||
function gatherStatHistory(){
|
||||
|
||||
var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString();
|
||||
|
||||
redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){
|
||||
if (err) {
|
||||
logger.error(logSystem, 'Historics', 'Error when trying to grab historical stats ' + JSON.stringify(err));
|
||||
|
@ -79,6 +111,34 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
});
|
||||
}
|
||||
|
||||
function getWorkerStats(address) {
|
||||
address = address.split(".")[0];
|
||||
if (address.length > 0 && address.startsWith('t')) {
|
||||
for (var h in statHistory) {
|
||||
for(var pool in statHistory[h].pools) {
|
||||
|
||||
statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate);
|
||||
|
||||
for(var w in statHistory[h].pools[pool].workers){
|
||||
if (w.startsWith(address)) {
|
||||
if (history[w] == null) {
|
||||
history[w] = [];
|
||||
}
|
||||
if (workers[w] == null && stats.pools[pool].workers[w] != null) {
|
||||
workers[w] = stats.pools[pool].workers[w];
|
||||
}
|
||||
if (statHistory[h].pools[pool].workers[w].hashrate) {
|
||||
history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return JSON.stringify({"workers": workers, "history": history});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addStatPoolHistory(stats){
|
||||
var data = {
|
||||
time: stats.time,
|
||||
|
@ -94,8 +154,115 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
_this.statPoolHistory.push(data);
|
||||
}
|
||||
|
||||
this.getCoins = function(cback){
|
||||
_this.stats.coins = redisClients[0].coins;
|
||||
cback();
|
||||
};
|
||||
|
||||
this.getPayout = function(address, cback){
|
||||
async.waterfall([
|
||||
function(callback){
|
||||
_this.getBalanceByAddress(address, function(){
|
||||
callback(null, 'test');
|
||||
});
|
||||
}
|
||||
], function(err, total){
|
||||
cback(balanceRound(total).toFixed(8));
|
||||
});
|
||||
};
|
||||
|
||||
this.getTotalSharesByAddress = function(address, cback) {
|
||||
var a = address.split(".")[0];
|
||||
var client = redisClients[0].client,
|
||||
coins = redisClients[0].coins,
|
||||
shares = [];
|
||||
var totalShares = 0;
|
||||
async.each(_this.stats.pools, function(pool, pcb) {
|
||||
var coin = String(_this.stats.pools[pool.name].name);
|
||||
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", function(error, result) {
|
||||
var workerName = "";
|
||||
var shares = 0;
|
||||
for (var i in result[1]) {
|
||||
if (Math.abs(i % 2) != 1) {
|
||||
workerName = String(result[1][i]);
|
||||
} else {
|
||||
shares += parseFloat(result[1][i]);
|
||||
}
|
||||
}
|
||||
totalShares = shares;
|
||||
pcb();
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
cback(0);
|
||||
return;
|
||||
}
|
||||
cback(totalShares);
|
||||
});
|
||||
};
|
||||
|
||||
this.getBalanceByAddress = function(address, cback){
|
||||
|
||||
|
||||
var a = address.split(".")[0];
|
||||
|
||||
var client = redisClients[0].client,
|
||||
coins = redisClients[0].coins,
|
||||
balances = [];
|
||||
|
||||
var totalHeld = parseFloat(0);
|
||||
var totalPaid = parseFloat(0);
|
||||
|
||||
async.each(_this.stats.pools, function(pool, pcb) {
|
||||
var coin = String(_this.stats.pools[pool.name].name);
|
||||
// get all balances from address
|
||||
client.hscan(coin + ':balances', 0, "match", a+"*", function(error, bals) {
|
||||
// get all payouts from address
|
||||
client.hscan(coin + ':payouts', 0, "match", a+"*", function(error, pays) {
|
||||
|
||||
var addressFound = false;
|
||||
var workerName = "";
|
||||
var balName = "";
|
||||
var balAmount = 0;
|
||||
var paidAmount = 0;
|
||||
|
||||
for (var i in pays[1]) {
|
||||
if (Math.abs(i % 2) != 1) {
|
||||
workerName = String(pays[1][i]);
|
||||
} else {
|
||||
balAmount = 0;
|
||||
for (var b in bals[1]) {
|
||||
if (Math.abs(b % 2) != 1) {
|
||||
balName = String(bals[1][b]);
|
||||
} else if (balName == workerName) {
|
||||
balAmount = parseFloat(bals[1][b]);
|
||||
totalHeld = balanceRound(totalHeld+balAmount);
|
||||
}
|
||||
}
|
||||
paidAmount = parseFloat(pays[1][i]);
|
||||
totalPaid = balanceRound(totalPaid+paidAmount);
|
||||
balances.push({
|
||||
worker:String(workerName),
|
||||
balance:balanceRound(balAmount),
|
||||
paid:balanceRound(paidAmount)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pcb();
|
||||
});
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
callback("There was an error getting balances");
|
||||
return;
|
||||
}
|
||||
|
||||
_this.stats.balances = balances;
|
||||
_this.stats.address = address;
|
||||
|
||||
cback({totalHeld:totalHeld, totalPaid:totalPaid, balances});
|
||||
});
|
||||
};
|
||||
|
||||
this.getGlobalStats = function(callback){
|
||||
|
||||
|
@ -107,14 +274,18 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
var windowTime = (((Date.now() / 1000) - portalConfig.website.stats.hashrateWindow) | 0).toString();
|
||||
var redisCommands = [];
|
||||
|
||||
|
||||
var redisCommandTemplates = [
|
||||
['zremrangebyscore', ':hashrate', '-inf', '(' + windowTime],
|
||||
['zrangebyscore', ':hashrate', windowTime, '+inf'],
|
||||
['hgetall', ':stats'],
|
||||
['scard', ':blocksPending'],
|
||||
['scard', ':blocksConfirmed'],
|
||||
['scard', ':blocksKicked']
|
||||
['scard', ':blocksKicked'],
|
||||
['smembers', ':blocksPending'],
|
||||
['smembers', ':blocksConfirmed'],
|
||||
['hgetall', ':shares:roundCurrent'],
|
||||
['hgetall', ':blocksPendingConfirms'],
|
||||
['hgetall', ':payments']
|
||||
];
|
||||
|
||||
var commandsPerCoin = redisCommandTemplates.length;
|
||||
|
@ -127,7 +298,6 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
client.client.multi(redisCommands).exec(function(err, replies){
|
||||
if (err){
|
||||
logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err));
|
||||
|
@ -145,14 +315,37 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0,
|
||||
validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0,
|
||||
invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0,
|
||||
totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0
|
||||
totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0,
|
||||
networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0,
|
||||
networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0,
|
||||
networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0),
|
||||
networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0,
|
||||
networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0
|
||||
},
|
||||
blocks: {
|
||||
pending: replies[i + 3],
|
||||
confirmed: replies[i + 4],
|
||||
orphaned: replies[i + 5]
|
||||
}
|
||||
},
|
||||
pending: {
|
||||
blocks: replies[i + 6].sort(sortBlocks),
|
||||
confirms: replies[i + 9]
|
||||
},
|
||||
confirmed: {
|
||||
blocks: replies[i + 7].sort(sortBlocks)
|
||||
},
|
||||
payments: replies[i + 10],
|
||||
currentRoundShares: replies[i + 8]
|
||||
};
|
||||
/*
|
||||
for (var b in coinStats.confirmed.blocks) {
|
||||
var parms = coinStats.confirmed.blocks[b].split(':');
|
||||
if (parms[4] != null && parms[4] > 0) {
|
||||
console.log(fancyTimestamp(parseInt(parms[4]), true));
|
||||
}
|
||||
break;
|
||||
}
|
||||
*/
|
||||
allCoinStats[coinStats.name] = (coinStats);
|
||||
}
|
||||
callback();
|
||||
|
@ -178,37 +371,101 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
Object.keys(allCoinStats).forEach(function(coin){
|
||||
var coinStats = allCoinStats[coin];
|
||||
coinStats.workers = {};
|
||||
coinStats.miners = {};
|
||||
coinStats.shares = 0;
|
||||
coinStats.hashrates.forEach(function(ins){
|
||||
var parts = ins.split(':');
|
||||
var workerShares = parseFloat(parts[0]);
|
||||
var miner = parts[1].split('.')[0];
|
||||
var worker = parts[1];
|
||||
var diff = Math.round(parts[0] * 8192);
|
||||
if (workerShares > 0) {
|
||||
coinStats.shares += workerShares;
|
||||
if (worker in coinStats.workers)
|
||||
// build worker stats
|
||||
if (worker in coinStats.workers) {
|
||||
coinStats.workers[worker].shares += workerShares;
|
||||
else
|
||||
coinStats.workers[worker].diff = diff;
|
||||
} else {
|
||||
coinStats.workers[worker] = {
|
||||
name: worker,
|
||||
diff: diff,
|
||||
shares: workerShares,
|
||||
invalidshares: 0,
|
||||
hashrateString: null
|
||||
currRoundShares: 0,
|
||||
hashrate: null,
|
||||
hashrateString: null,
|
||||
luckDays: null,
|
||||
luckHours: null,
|
||||
paid: 0,
|
||||
balance: 0
|
||||
};
|
||||
}
|
||||
// build miner stats
|
||||
if (miner in coinStats.miners) {
|
||||
coinStats.miners[miner].shares += workerShares;
|
||||
} else {
|
||||
coinStats.miners[miner] = {
|
||||
name: miner,
|
||||
shares: workerShares,
|
||||
invalidshares: 0,
|
||||
currRoundShares: 0,
|
||||
hashrate: null,
|
||||
hashrateString: null,
|
||||
luckDays: null,
|
||||
luckHours: null
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (worker in coinStats.workers)
|
||||
// build worker stats
|
||||
if (worker in coinStats.workers) {
|
||||
coinStats.workers[worker].invalidshares -= workerShares; // workerShares is negative number!
|
||||
else
|
||||
coinStats.workers[worker].diff = diff;
|
||||
} else {
|
||||
coinStats.workers[worker] = {
|
||||
name: worker,
|
||||
diff: diff,
|
||||
shares: 0,
|
||||
invalidshares: -workerShares,
|
||||
hashrateString: null
|
||||
invalidshares: -workerShares,
|
||||
currRoundShares: 0,
|
||||
hashrate: null,
|
||||
hashrateString: null,
|
||||
luckDays: null,
|
||||
luckHours: null,
|
||||
paid: 0,
|
||||
balance: 0
|
||||
};
|
||||
}
|
||||
// build miner stats
|
||||
if (miner in coinStats.miners) {
|
||||
coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number!
|
||||
} else {
|
||||
coinStats.miners[miner] = {
|
||||
name: miner,
|
||||
shares: 0,
|
||||
invalidshares: -workerShares,
|
||||
currRoundShares: 0,
|
||||
hashrate: null,
|
||||
hashrateString: null,
|
||||
luckDays: null,
|
||||
luckHours: null
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// sort miners
|
||||
coinStats.miners = sortMinersByHashrate(coinStats.miners);
|
||||
|
||||
var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier;
|
||||
coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
|
||||
|
||||
coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate);
|
||||
var _blocktime = 250;
|
||||
var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2;
|
||||
var _myHashRate = (coinStats.hashrate / 1000000) * 2;
|
||||
coinStats.luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
|
||||
coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3);
|
||||
coinStats.minerCount = Object.keys(coinStats.miners).length;
|
||||
coinStats.workerCount = Object.keys(coinStats.workers).length;
|
||||
portalStats.global.workers += coinStats.workerCount;
|
||||
|
||||
|
@ -224,28 +481,59 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
portalStats.algos[algo].hashrate += coinStats.hashrate;
|
||||
portalStats.algos[algo].workers += Object.keys(coinStats.workers).length;
|
||||
|
||||
for (var worker in coinStats.workers) {
|
||||
coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow);
|
||||
var _shareTotal = 0;
|
||||
for (var worker in coinStats.currentRoundShares) {
|
||||
var miner = worker.split(".")[0];
|
||||
if (miner in coinStats.miners) {
|
||||
coinStats.miners[miner].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
|
||||
}
|
||||
if (worker in coinStats.workers) {
|
||||
coinStats.workers[worker].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
|
||||
}
|
||||
_shareTotal += parseFloat(coinStats.currentRoundShares[worker]);
|
||||
}
|
||||
|
||||
coinStats.shareCount = Math.round(_shareTotal * 100) / 100;
|
||||
|
||||
for (var worker in coinStats.workers) {
|
||||
var _blocktime = 250;
|
||||
var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow;
|
||||
var _wHashRate = (_workerRate / 1000000) * 2;
|
||||
coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
|
||||
coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
|
||||
coinStats.workers[worker].hashrate = _workerRate;
|
||||
coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate);
|
||||
}
|
||||
for (var miner in coinStats.miners) {
|
||||
var _blocktime = 250;
|
||||
var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow;
|
||||
var _wHashRate = (_workerRate / 1000000) * 2;
|
||||
coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
|
||||
coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
|
||||
coinStats.miners[miner].hashrate = _workerRate;
|
||||
coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate);
|
||||
}
|
||||
|
||||
// sort workers by name
|
||||
coinStats.workers = sortWorkersByName(coinStats.workers);
|
||||
|
||||
delete coinStats.hashrates;
|
||||
delete coinStats.shares;
|
||||
coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate);
|
||||
});
|
||||
|
||||
Object.keys(portalStats.algos).forEach(function(algo){
|
||||
var algoStats = portalStats.algos[algo];
|
||||
algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate);
|
||||
});
|
||||
|
||||
|
||||
// TODO, create stats object and copy elements from portalStats we want to display...
|
||||
var showStats = portalStats;
|
||||
|
||||
_this.stats = portalStats;
|
||||
_this.statsString = JSON.stringify(portalStats);
|
||||
|
||||
|
||||
|
||||
_this.statHistory.push(portalStats);
|
||||
addStatPoolHistory(portalStats);
|
||||
|
||||
|
||||
addStatPoolHistory(portalStats);
|
||||
|
||||
var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0);
|
||||
|
||||
for (var i = 0; i < _this.statHistory.length; i++){
|
||||
|
@ -270,13 +558,63 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
|||
|
||||
};
|
||||
|
||||
function sortBlocks(a, b) {
|
||||
var as = a.split(":");
|
||||
var bs = b.split(":");
|
||||
if (as[2] > bs[2]) return -1;
|
||||
if (as[2] < bs[2]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortWorkersByName(objects) {
|
||||
var newObject = {};
|
||||
var sortedArray = sortProperties(objects, 'name', false, false);
|
||||
for (var i = 0; i < sortedArray.length; i++) {
|
||||
var key = sortedArray[i][0];
|
||||
var value = sortedArray[i][1];
|
||||
newObject[key] = value;
|
||||
}
|
||||
return newObject;
|
||||
}
|
||||
|
||||
function sortMinersByHashrate(objects) {
|
||||
var newObject = {};
|
||||
var sortedArray = sortProperties(objects, 'shares', true, true);
|
||||
for (var i = 0; i < sortedArray.length; i++) {
|
||||
var key = sortedArray[i][0];
|
||||
var value = sortedArray[i][1];
|
||||
newObject[key] = value;
|
||||
}
|
||||
return newObject;
|
||||
}
|
||||
|
||||
function sortWorkersByHashrate(a, b) {
|
||||
if (a.hashrate === b.hashrate) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return (a.hashrate < b.hashrate) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.getReadableHashRateString = function(hashrate){
|
||||
if (hashrate < 1000000)
|
||||
return '0 Sol';
|
||||
hashrate = (hashrate * 2);
|
||||
if (hashrate < 1000000) {
|
||||
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
|
||||
}
|
||||
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
|
||||
hashrate = (hashrate * 2);
|
||||
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
|
||||
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
};
|
||||
|
||||
function getReadableNetworkHashRateString(hashrate) {
|
||||
hashrate = (hashrate * 1000000);
|
||||
if (hashrate < 1000000)
|
||||
return '0 Sol';
|
||||
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
|
||||
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
|
||||
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -41,7 +41,8 @@ module.exports = function(logger){
|
|||
'workers.html': 'workers',
|
||||
'api.html': 'api',
|
||||
'admin.html': 'admin',
|
||||
'mining_key.html': 'mining_key'
|
||||
'mining_key.html': 'mining_key',
|
||||
'miner_stats.html': 'miner_stats'
|
||||
};
|
||||
|
||||
var pageTemplates = {};
|
||||
|
@ -123,7 +124,6 @@ module.exports = function(logger){
|
|||
|
||||
setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000);
|
||||
|
||||
|
||||
var buildKeyScriptPage = function(){
|
||||
async.waterfall([
|
||||
function(callback){
|
||||
|
@ -215,6 +215,58 @@ module.exports = function(logger){
|
|||
}
|
||||
};
|
||||
|
||||
var minerpage = function(req, res, next){
|
||||
var address = req.params.address || null;
|
||||
if (address != null) {
|
||||
address = address.split(".")[0];
|
||||
portalStats.getBalanceByAddress(address, function(){
|
||||
processTemplates();
|
||||
res.end(indexesProcessed['miner_stats']);
|
||||
});
|
||||
}
|
||||
else
|
||||
next();
|
||||
};
|
||||
|
||||
var payout = function(req, res, next){
|
||||
var address = req.params.address || null;
|
||||
|
||||
if (address != null){
|
||||
portalStats.getPayout(address, function(data){
|
||||
res.write(data.toString());
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
else
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
var shares = function(req, res, next){
|
||||
portalStats.getCoins(function(){
|
||||
processTemplates();
|
||||
|
||||
res.end(indexesProcessed['user_shares']);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
var usershares = function(req, res, next){
|
||||
|
||||
var coin = req.params.coin || null;
|
||||
|
||||
if(coin != null){
|
||||
portalStats.getCoinTotals(coin, null, function(){
|
||||
processTemplates();
|
||||
|
||||
res.end(indexesProcessed['user_shares']);
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
next();
|
||||
};
|
||||
|
||||
var route = function(req, res, next){
|
||||
var pageId = req.params.page || '';
|
||||
if (pageId in indexesProcessed){
|
||||
|
@ -246,6 +298,11 @@ module.exports = function(logger){
|
|||
res.end(keyScriptProcessed);
|
||||
});
|
||||
|
||||
//app.get('/stats/shares/:coin', usershares);
|
||||
//app.get('/stats/shares', shares);
|
||||
//app.get('/payout/:address', payout);
|
||||
app.get('/workers/:address', minerpage);
|
||||
|
||||
app.get('/:page', route);
|
||||
app.get('/', route);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<div>
|
||||
API Docs here
|
||||
<div style="margin: 18px;">
|
||||
API - The API is work in progress and is subject to change during development.
|
||||
|
||||
<ul>
|
||||
<li>/stats - raw json statistic</li>
|
||||
<li>/pool_stats - historical time per pool json </li>
|
||||
<li>/live_stats - live stats </li>
|
||||
<li><a href="/api/stats">/stats</a> global pool stats</li>
|
||||
<li><a href="/api/pool_stats">/pool_stats</a> - historical stats</li>
|
||||
<li><a href="/api/worker_stats?taddr">/worker_stats?taddr</a> - historical time per pool json </li>
|
||||
<li><a href="/api/live_stats">/live_stats</a> - live stats </li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
<style>
|
||||
#topCharts{
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
padding-top: 18px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
#topCharts > div > div > svg{
|
||||
display: block;
|
||||
height: 280px;
|
||||
}
|
||||
.chartWrapper{
|
||||
border: solid 1px #c7c7c7;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.chartLabel{
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
.chartHolder{
|
||||
|
||||
}
|
||||
|
||||
#boxesWorkers {
|
||||
margin: 0 9px;
|
||||
}
|
||||
#boxesWorkers > div {
|
||||
display: flex;
|
||||
}
|
||||
#boxesWorkers > div > div {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 9px 18px 9px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.boxLowerHeader{
|
||||
font-size: 1.3em;
|
||||
margin: 0 0 5px 10px;
|
||||
}
|
||||
|
||||
#boxStatsLeft{
|
||||
color: black;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
#boxStatsRight{
|
||||
color: black;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
.boxStats{
|
||||
color: white;
|
||||
}
|
||||
.boxStatsList{
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-around;
|
||||
opacity: 0.77;
|
||||
margin-bottom: 5px;
|
||||
flex: 1 1 auto;
|
||||
align-content: center;
|
||||
}
|
||||
.boxStatsList i.fa{
|
||||
height: 15px;
|
||||
width: 33px;
|
||||
text-align: center;
|
||||
}
|
||||
.boxStatsList > div{
|
||||
padding: 5px 20px;
|
||||
}
|
||||
.boxStatsList > div > div{
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div id="topCharts">
|
||||
<div class="chartWrapper">
|
||||
<div class="chartLabel">
|
||||
<!--<div style="float:left; padding-right: 18px;"><i class="fa fa-users"></i><span id="statsWorkers">...</span></div>-->
|
||||
<div style="float:left; margin-right: 9px;">{{=String(it.stats.address).split(".")[0]}}</div>
|
||||
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-tachometer"></i> <span id="statsHashrateAvg">...</span> (Avg)</small></div>
|
||||
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-tachometer"></i> <span id="statsHashrate">...</span> (Now)</small></div>
|
||||
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-gavel"></i> Luck <span id="statsLuckDays">...</span> Days</small></div>
|
||||
</div>
|
||||
<div class="chartHolder"><svg id="workerHashrate" /></div>
|
||||
<div>
|
||||
<div style="float:right; padding-top: 9px; padding-right: 18px;"><i class="fa fa-cog"></i> Shares: <span id="statsTotalShares">...</span></div>
|
||||
<div style="float:left; padding-top: 9px; padding-left: 18px; padding-right: 18px;"><i class="fa fa-money"></i> Bal: <span id="statsTotalBal">...</span> ZEC </div>
|
||||
<div style="padding-top: 9px; padding-left: 18px;"><i class="fa fa-money"></i> Paid: <span id="statsTotalPaid">...</span> ZEC </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="boxesWorkers"> </div>
|
||||
|
||||
<script>
|
||||
var _miner = "{{=String(it.stats.address).split(".")[0]}}";
|
||||
var _workerCount = 0;
|
||||
window.statsSource = new EventSource("/api/live_stats");
|
||||
document.querySelector('main').appendChild(document.createElement('script')).src = '/static/miner_stats.js';
|
||||
</script>
|
|
@ -1,54 +1,208 @@
|
|||
<style>
|
||||
|
||||
#topCharts{
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
#topCharts > div > div > svg{
|
||||
display: block;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.chartWrapper{
|
||||
border: solid 1px #c7c7c7;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.chartLabel{
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chartHolder{
|
||||
|
||||
}
|
||||
|
||||
|
||||
#boxesLower {
|
||||
margin: 0 9px;
|
||||
}
|
||||
#boxesLower > div {
|
||||
display: flex;
|
||||
}
|
||||
#boxesLower > div > div {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 9px 18px 9px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.boxLowerHeader{
|
||||
font-size: 1.3em;
|
||||
margin: 0 0 5px 10px;
|
||||
}
|
||||
#boxStatsLeft{
|
||||
color: black;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
#boxStatsRight{
|
||||
color: black;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
.boxStats{
|
||||
color: white;
|
||||
}
|
||||
.boxStatsList{
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-around;
|
||||
opacity: 0.77;
|
||||
margin-bottom: 5px;
|
||||
flex: 1 1 auto;
|
||||
align-content: center;
|
||||
}
|
||||
.boxStatsList i.fa{
|
||||
height: 15px;
|
||||
width: 33px;
|
||||
text-align: center;
|
||||
}
|
||||
.boxStatsList > div{
|
||||
padding: 5px 20px;
|
||||
}
|
||||
.boxStatsList > div > div{
|
||||
padding: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="topCharts">
|
||||
|
||||
<div class="chartWrapper">
|
||||
<div class="chartLabel">Workers Per Pool</div>
|
||||
<div class="chartHolder"><svg id="poolWorkers"/></div>
|
||||
</div>
|
||||
|
||||
<div class="chartWrapper">
|
||||
<div class="chartLabel">Hashrate Per Pool</div>
|
||||
<div class="chartLabel">Pool Historical Hashrate</div>
|
||||
<div class="chartHolder"><svg id="poolHashrate"/></div>
|
||||
</div>
|
||||
|
||||
<div class="chartWrapper">
|
||||
<div class="chartLabel">Blocks Pending Per Pool</div>
|
||||
<div class="chartHolder"><svg id="poolBlocks"/></div>
|
||||
</div>
|
||||
{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }}
|
||||
{{ function readableDate(a){ return new Date(parseInt(a)).toString(); } }}
|
||||
<div class="pure-g-r" id="boxesLower">
|
||||
{{ for(var pool in it.stats.pools) { }}
|
||||
<div class="pure-u-1-2">
|
||||
<div class="boxStats" id="boxStatsLeft">
|
||||
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Pool Stats</div>
|
||||
<div class="boxStatsList">
|
||||
<div>
|
||||
<div><i class="fa fa-users"></i><span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].minerCount}}</span> Miners</div>
|
||||
<div><i class="fa fa-rocket"></i><span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Workers</div>
|
||||
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=pool}}">{{=it.stats.pools[pool].hashrateString}}</span> (Now)</div>
|
||||
<div><i class="fa fa-tachometer"></i><span id="statsHashrateAvg{{=pool}}">...</span> (Avg)</div>
|
||||
<div><i class="fa fa-gavel"></i>Luck <span id="statsLuckDays{{=pool}}">{{=it.stats.pools[pool].luckDays}}</span> Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-2">
|
||||
<div class="boxStats" id="boxStatsRight">
|
||||
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Network Stats</div>
|
||||
<div class="boxStatsList">
|
||||
<div>
|
||||
<div><i class="fa fa-bars" aria-hidden="true"></i><small>Block Height:</small> <span id="statsNetworkBlocks{{=pool}}">{{=it.stats.pools[pool].poolStats.networkBlocks}}</span></div>
|
||||
<div><i class="fa fa-tachometer"></i><small>Network Hash/s:</small> <span id="statsNetworkSols{{=pool}}">{{=it.stats.pools[pool].poolStats.networkSolsString}}</span></div>
|
||||
<div><i class="fa fa-unlock-alt" aria-hidden="true"></i><small>Difficulty:</small> <span id="statsNetworkDiff{{=pool}}">{{=it.stats.pools[pool].poolStats.networkDiff}}</span></div>
|
||||
<div><i class="fa fa-users"></i><small>Node Connections:</small> <span id="statsNetworkConnections{{=pool}}">{{=it.stats.pools[pool].poolStats.networkConnections}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ } }}
|
||||
</div>
|
||||
|
||||
|
||||
{{ for(var pool in it.stats.pools) { }}
|
||||
{{ var paidJackpots = parseFloat(it.stats.pools[pool].poolStats.validBlocks) * 1.0; }}
|
||||
<div class="pure-g-r" id="boxesLower">
|
||||
<div class="pure-u-1-1">
|
||||
<div class="boxStats" id="boxStatsRight">
|
||||
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Blocks Found
|
||||
<span style="float:right;"><small>
|
||||
<i class="fa fa-bars"></i> <span id="statsValidBlocks{{=pool}}">{{=it.stats.pools[pool].poolStats.validBlocks}}</span> Blocks
|
||||
<i class="fa fa-money"></i> Paid: <span id="statsTotalPaid{{=pool}}">{{=(parseFloat(it.stats.pools[pool].poolStats.totalPaid)).toFixed(8)}}</span> {{=it.stats.pools[pool].symbol}}</small> </span>
|
||||
</div>
|
||||
<div class="boxStatsList" style="margin-top: 9px;">
|
||||
<!--<div id="{{=it.stats.pools[pool].name}}NewBlocks"></div>-->
|
||||
{{ for(var b in it.stats.pools[pool].pending.blocks) { }}
|
||||
{{ var block = it.stats.pools[pool].pending.blocks[b].split(":"); }}
|
||||
<div style="margin-bottom: 9px; background-color: #eeeeee; min-width:600px;"><i class="fa fa-bars"></i>
|
||||
<small>Block:</small>
|
||||
{{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }}
|
||||
<a href="https://explorer.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
|
||||
{{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }}
|
||||
<a href="https://classic.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
|
||||
{{ } else { }}
|
||||
{{=block[2]}}
|
||||
{{ } }}
|
||||
{{if (block[4] != null) { }}
|
||||
<span style="padding-left: 18px;"><small>{{=readableDate(block[4])}}</small></span>
|
||||
{{ } }}
|
||||
{{if (it.stats.pools[pool].pending.confirms[block[0]]) { }}
|
||||
<span style="float:right; color: red;"><small>{{=it.stats.pools[pool].pending.confirms[block[0]]}} of 100</small></span>
|
||||
{{ } else { }}
|
||||
<span style="float:right; color: red;"><small>*PENDING*</small></span>
|
||||
{{ } }}
|
||||
<div><i class="fa fa-gavel"></i><small>Mined By:</small> <a href="/workers/{{=block[3].split('.')[0]}}">{{=block[3]}}</a></div>
|
||||
</div>
|
||||
{{ } }}
|
||||
{{ var i=0; for(var b in it.stats.pools[pool].confirmed.blocks) { }}
|
||||
{{ if (i < 8) { i++; }}
|
||||
{{ var block = it.stats.pools[pool].confirmed.blocks[b].split(":"); }}
|
||||
<div style="margin-bottom: 9px; background-color: #eeeeee; min-width:600px;"><i class="fa fa-bars"></i>
|
||||
<small>Block:</small>
|
||||
{{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }}
|
||||
<a href="https://explorer.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
|
||||
{{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }}
|
||||
<a href="https://classic.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
|
||||
{{ } else { }}
|
||||
{{=block[2]}}
|
||||
{{ } }}
|
||||
{{if (block[4] != null) { }}
|
||||
<span style="padding-left: 18px;"><small>{{=readableDate(block[4])}}</small></span>
|
||||
{{ } }}
|
||||
<span style="float:right; padding-left: 18px; color: green;"><small>*PAID*</small></span>
|
||||
<div><i class="fa fa-gavel"></i><small>Mined By:</small> <a href="/workers/{{=block[3].split('.')[0]}}">{{=block[3]}}</a></div>
|
||||
</div>
|
||||
{{ } }}
|
||||
{{ } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ } }}
|
||||
|
||||
<script>
|
||||
document.querySelector('main').appendChild(document.createElement('script')).src = '/static/stats.js';
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
window.statsSource = new EventSource("/api/live_stats");
|
||||
$(function() {
|
||||
statsSource.addEventListener('message', function (e) {
|
||||
var stats = JSON.parse(e.data);
|
||||
for (var pool in stats.pools) {
|
||||
$('#statsMiners' + pool).text(stats.pools[pool].minerCount);
|
||||
$('#statsWorkers' + pool).text(stats.pools[pool].workerCount);
|
||||
$('#statsHashrate' + pool).text(stats.pools[pool].hashrateString);
|
||||
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
|
||||
$('#statsLuckDays' + pool).text(stats.pools[pool].luckDays);
|
||||
$('#statsValidBlocks' + pool).text(stats.pools[pool].poolStats.validBlocks);
|
||||
$('#statsTotalPaid' + pool).text((parseFloat(stats.pools[pool].poolStats.totalPaid)+paidJackpots).toFixed(8));
|
||||
$('#statsNetworkBlocks' + pool).text(stats.pools[pool].poolStats.networkBlocks);
|
||||
$('#statsNetworkDiff' + pool).text(stats.pools[pool].poolStats.networkDiff);
|
||||
$('#statsNetworkSols' + pool).text(getReadableNetworkHashRateString(stats.pools[pool].poolStats.networkSols));
|
||||
$('#statsNetworkConnections' + pool).text(stats.pools[pool].poolStats.networkConnections);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getReadableNetworkHashRateString(hashrate){
|
||||
hashrate = (hashrate * 1000000);
|
||||
if (hashrate < 1000000)
|
||||
return '0 Sol';
|
||||
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
|
||||
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
|
||||
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,66 +1,83 @@
|
|||
<style>
|
||||
|
||||
#topCharts {
|
||||
padding: 18px;
|
||||
#bottomNotes {
|
||||
display: block;
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
#topCharts > div > div > svg {
|
||||
#topPool {
|
||||
padding-top: 18px;
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
#topPool > div > div > svg {
|
||||
display: block;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.chartWrapper {
|
||||
.poolWrapper {
|
||||
border: solid 1px #c7c7c7;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.chartLabel {
|
||||
.poolLabel {
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chartHolder {
|
||||
.poolMinerTable {
|
||||
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div id="topCharts">
|
||||
|
||||
{{ for(var pool in it.stats.pools) { }}
|
||||
|
||||
<div class="chartWrapper">
|
||||
<div class="chartLabel">{{=it.stats.pools[pool].name}}</div>
|
||||
<div class="chartHolder">
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
$('.btn-lg').click(function(){
|
||||
window.location = "workers/" + $('.input-lg').val();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }}
|
||||
{{ var i=0; for(var pool in it.stats.pools) { }}
|
||||
<div id="topPool">
|
||||
<div class="poolWrapper">
|
||||
<div class="poolLabel">
|
||||
<span style="float:right; margin-bottom: 8px;">
|
||||
<small>Miner Lookup:
|
||||
<input type="text" class="form-control input-lg">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default btn-lg" type="button">Lookup</button>
|
||||
</span>
|
||||
</small>
|
||||
</span>
|
||||
{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Top Miners
|
||||
<small><i class="fa fa-users"></i> <span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].minerCount}}</span> Miners
|
||||
<i class="fa fa-rocket"></i> <span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Workers
|
||||
<i class="fa fa-cog"></i> <span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].shareCount}}</span> Shares </small>
|
||||
</div>
|
||||
<div class="poolMinerTable">
|
||||
<table class="pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>Shares</th>
|
||||
<th>Invalid shares</th>
|
||||
<th>Efficiency</th>
|
||||
<th>Hashrate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ for(var worker in it.stats.pools[pool].workers) { }}
|
||||
{{var workerstat = it.stats.pools[pool].workers[worker];}}
|
||||
{{ for(var worker in it.stats.pools[pool].miners) { }}
|
||||
{{var workerstat = it.stats.pools[pool].miners[worker];}}
|
||||
<tr class="pure-table-odd">
|
||||
<td>{{=worker}}</td>
|
||||
<td>{{=Math.round(workerstat.shares * 100) / 100}}</td>
|
||||
<td>{{=Math.round(workerstat.invalidshares * 100) / 100}}</td>
|
||||
<td><a href="/workers/{{=worker.split('.')[0]}}">{{=worker}}</a></td>
|
||||
<td>{{=Math.round(workerstat.currRoundShares * 100) / 100}}</td>
|
||||
<td>{{? workerstat.shares > 0}} {{=Math.floor(10000 * workerstat.shares / (workerstat.shares + workerstat.invalidshares)) / 100}}% {{??}} 0% {{?}}</td>
|
||||
<td>{{=workerstat.hashrateString}}</td>
|
||||
</tr>
|
||||
{{ } }}
|
||||
{{ } }}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ } }}
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
var workerHashrateData;
|
||||
var workerHashrateChart;
|
||||
var workerHistoryMax = 160;
|
||||
|
||||
var statData;
|
||||
var totalHash;
|
||||
var totalBal;
|
||||
var totalPaid;
|
||||
var totalShares;
|
||||
|
||||
function getReadableHashRateString(hashrate){
|
||||
hashrate = (hashrate * 2);
|
||||
if (hashrate < 1000000) {
|
||||
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
|
||||
}
|
||||
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
|
||||
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
|
||||
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
|
||||
function timeOfDayFormat(timestamp){
|
||||
var dStr = d3.time.format('%I:%M %p')(new Date(timestamp));
|
||||
if (dStr.indexOf('0') === 0) dStr = dStr.slice(1);
|
||||
return dStr;
|
||||
}
|
||||
|
||||
function getWorkerNameFromAddress(w) {
|
||||
var worker = w;
|
||||
if (w.split(".").length > 1) {
|
||||
worker = w.split(".")[1];
|
||||
if (worker == null || worker.length < 1) {
|
||||
worker = "noname";
|
||||
}
|
||||
} else {
|
||||
worker = "noname";
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
function buildChartData(){
|
||||
var workers = {};
|
||||
for (var w in statData.history) {
|
||||
var worker = getWorkerNameFromAddress(w);
|
||||
var a = workers[worker] = (workers[worker] || {
|
||||
hashrate: []
|
||||
});
|
||||
for (var wh in statData.history[w]) {
|
||||
a.hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
|
||||
}
|
||||
if (a.hashrate.length > workerHistoryMax) {
|
||||
workerHistoryMax = a.hashrate.length;
|
||||
}
|
||||
}
|
||||
|
||||
var i=0;
|
||||
workerHashrateData = [];
|
||||
for (var worker in workers){
|
||||
workerHashrateData.push({
|
||||
key: worker,
|
||||
disabled: (i > Math.min((_workerCount-1), 3)),
|
||||
values: workers[worker].hashrate
|
||||
});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
function updateChartData(){
|
||||
var workers = {};
|
||||
for (var w in statData.history) {
|
||||
var worker = getWorkerNameFromAddress(w);
|
||||
// get a reference to lastest workerhistory
|
||||
for (var wh in statData.history[w]) { }
|
||||
//var wh = statData.history[w][statData.history[w].length - 1];
|
||||
var foundWorker = false;
|
||||
for (var i = 0; i < workerHashrateData.length; i++) {
|
||||
if (workerHashrateData[i].key === worker) {
|
||||
foundWorker = true;
|
||||
if (workerHashrateData[i].values.length >= workerHistoryMax) {
|
||||
workerHashrateData[i].values.shift();
|
||||
}
|
||||
workerHashrateData[i].values.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWorker) {
|
||||
var hashrate = [];
|
||||
hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
|
||||
workerHashrateData.push({
|
||||
key: worker,
|
||||
values: hashrate
|
||||
});
|
||||
rebuildWorkerDisplay();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
triggerChartUpdates();
|
||||
return false;
|
||||
}
|
||||
|
||||
function calculateAverageHashrate(worker) {
|
||||
var count = 0;
|
||||
var total = 1;
|
||||
var avg = 0;
|
||||
for (var i = 0; i < workerHashrateData.length; i++) {
|
||||
count = 0;
|
||||
for (var ii = 0; ii < workerHashrateData[i].values.length; ii++) {
|
||||
if (worker == null || workerHashrateData[i].key === worker) {
|
||||
count++;
|
||||
avg += parseFloat(workerHashrateData[i].values[ii][1]);
|
||||
}
|
||||
}
|
||||
if (count > total)
|
||||
total = count;
|
||||
}
|
||||
avg = avg / total;
|
||||
return avg;
|
||||
}
|
||||
|
||||
function triggerChartUpdates(){
|
||||
workerHashrateChart.update();
|
||||
}
|
||||
|
||||
function displayCharts() {
|
||||
nv.addGraph(function() {
|
||||
workerHashrateChart = nv.models.lineChart()
|
||||
.margin({left: 80, right: 30})
|
||||
.x(function(d){ return d[0] })
|
||||
.y(function(d){ return d[1] })
|
||||
.useInteractiveGuideline(true);
|
||||
|
||||
workerHashrateChart.xAxis.tickFormat(timeOfDayFormat);
|
||||
|
||||
workerHashrateChart.yAxis.tickFormat(function(d){
|
||||
return getReadableHashRateString(d);
|
||||
});
|
||||
d3.select('#workerHashrate').datum(workerHashrateData).call(workerHashrateChart);
|
||||
return workerHashrateChart;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
totalHash = statData.totalHash;
|
||||
totalPaid = statData.paid;
|
||||
totalBal = statData.balance;
|
||||
totalShares = statData.totalShares;
|
||||
// do some calculations
|
||||
var _blocktime = 250;
|
||||
var _networkHashRate = parseFloat(statData.networkSols) * 1.2;
|
||||
var _myHashRate = (totalHash / 1000000) * 2;
|
||||
var luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
|
||||
// update miner stats
|
||||
$("#statsHashrate").text(getReadableHashRateString(totalHash));
|
||||
$("#statsHashrateAvg").text(getReadableHashRateString(calculateAverageHashrate(null)));
|
||||
$("#statsLuckDays").text(luckDays);
|
||||
$("#statsTotalBal").text(totalBal);
|
||||
$("#statsTotalPaid").text(totalPaid);
|
||||
$("#statsTotalShares").text(totalShares.toFixed(2));
|
||||
}
|
||||
function updateWorkerStats() {
|
||||
// update worker stats
|
||||
var i=0;
|
||||
for (var w in statData.workers) { i++;
|
||||
var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, '');
|
||||
var saneWorkerName = getWorkerNameFromAddress(w);
|
||||
$("#statsHashrate"+htmlSafeWorkerName).text(getReadableHashRateString(statData.workers[w].hashrate));
|
||||
$("#statsHashrateAvg"+htmlSafeWorkerName).text(getReadableHashRateString(calculateAverageHashrate(saneWorkerName)));
|
||||
$("#statsLuckDays"+htmlSafeWorkerName).text(statData.workers[w].luckDays);
|
||||
$("#statsPaid"+htmlSafeWorkerName).text(statData.workers[w].paid);
|
||||
$("#statsBalance"+htmlSafeWorkerName).text(statData.workers[w].balance);
|
||||
$("#statsShares"+htmlSafeWorkerName).text(Math.round(statData.workers[w].currRoundShares * 100) / 100);
|
||||
$("#statsDiff"+htmlSafeWorkerName).text(statData.workers[w].diff);
|
||||
}
|
||||
}
|
||||
function addWorkerToDisplay(name, htmlSafeName, workerObj) {
|
||||
var htmlToAdd = "";
|
||||
htmlToAdd = '<div class="boxStats" id="boxStatsLeft" style="float:left; margin: 9px; min-width: 260px;"><div class="boxStatsList">';
|
||||
htmlToAdd+='<div class="boxLowerHeader">'+name+'</div><div>';
|
||||
htmlToAdd+='<div><i class="fa fa-tachometer"></i> <span id="statsHashrate'+htmlSafeName+'">'+getReadableHashRateString(workerObj.hashrate)+'</span> (Now)</div>';
|
||||
htmlToAdd+='<div><i class="fa fa-tachometer"></i> <span id="statsHashrateAvg'+htmlSafeName+'">'+getReadableHashRateString(calculateAverageHashrate(name))+'</span> (Avg)</div>';
|
||||
htmlToAdd+='<div><i class="fa fa-shield"></i> <small>Diff:</small> <span id="statsDiff'+htmlSafeName+'">'+workerObj.diff+'</span></div>';
|
||||
htmlToAdd+='<div><i class="fa fa-cog"></i> <small>Shares:</small> <span id="statsShares'+htmlSafeName+'">'+(Math.round(workerObj.currRoundShares * 100) / 100)+'</span></div>';
|
||||
htmlToAdd+='<div><i class="fa fa-gavel"></i> <small>Luck <span id="statsLuckDays'+htmlSafeName+'">'+workerObj.luckDays+'</span> Days</small></div>';
|
||||
htmlToAdd+='<div><i class="fa fa-money"></i> <small>Bal: <span id="statsBalance'+htmlSafeName+'">'+workerObj.balance+'</span></small></div>';
|
||||
htmlToAdd+='<div><i class="fa fa-money"></i> <small>Paid: <span id="statsPaid'+htmlSafeName+'">'+workerObj.paid+'</span></small></div>';
|
||||
htmlToAdd+='</div></div></div>';
|
||||
$("#boxesWorkers").html($("#boxesWorkers").html()+htmlToAdd);
|
||||
}
|
||||
|
||||
function rebuildWorkerDisplay() {
|
||||
$("#boxesWorkers").html("");
|
||||
var i=0;
|
||||
for (var w in statData.workers) { i++;
|
||||
var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, '');
|
||||
var saneWorkerName = getWorkerNameFromAddress(w);
|
||||
addWorkerToDisplay(saneWorkerName, htmlSafeWorkerName, statData.workers[w]);
|
||||
}
|
||||
}
|
||||
|
||||
// resize chart on window resize
|
||||
nv.utils.windowResize(triggerChartUpdates);
|
||||
|
||||
// grab initial stats
|
||||
$.getJSON('/api/worker_stats?'+_miner, function(data){
|
||||
statData = data;
|
||||
for (var w in statData.workers) { _workerCount++; }
|
||||
buildChartData();
|
||||
displayCharts();
|
||||
rebuildWorkerDisplay();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// live stat updates
|
||||
statsSource.addEventListener('message', function(e){
|
||||
// TODO, create miner_live_stats...
|
||||
// miner_live_stats will return the same josn except without the worker history
|
||||
// FOR NOW, use this to grab updated stats
|
||||
$.getJSON('/api/worker_stats?'+_miner, function(data){
|
||||
statData = data;
|
||||
// check for missing workers
|
||||
var wc = 0;
|
||||
var rebuilt = false;
|
||||
// update worker stats
|
||||
for (var w in statData.workers) { wc++; }
|
||||
// TODO, this isn't 100% fool proof!
|
||||
if (_workerCount != wc) {
|
||||
if (_workerCount > wc) {
|
||||
rebuildWorkerDisplay();
|
||||
rebuilt = true;
|
||||
}
|
||||
_workerCount = wc;
|
||||
}
|
||||
rebuilt = (rebuilt || updateChartData());
|
||||
updateStats();
|
||||
if (!rebuilt) {
|
||||
updateWorkerStats();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,16 +1,10 @@
|
|||
var poolWorkerData;
|
||||
var poolHashrateData;
|
||||
var poolBlockData;
|
||||
|
||||
var poolWorkerChart;
|
||||
var poolHashrateChart;
|
||||
var poolBlockChart;
|
||||
|
||||
var statData;
|
||||
var poolKeys;
|
||||
|
||||
function buildChartData(){
|
||||
|
||||
var pools = {};
|
||||
|
||||
poolKeys = [];
|
||||
|
@ -21,61 +15,60 @@ function buildChartData(){
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
for (var i = 0; i < statData.length; i++){
|
||||
|
||||
for (var i = 0; i < statData.length; i++) {
|
||||
var time = statData[i].time * 1000;
|
||||
|
||||
for (var f = 0; f < poolKeys.length; f++){
|
||||
for (var f = 0; f < poolKeys.length; f++){
|
||||
var pName = poolKeys[f];
|
||||
var a = pools[pName] = (pools[pName] || {
|
||||
hashrate: [],
|
||||
workers: [],
|
||||
blocks: []
|
||||
hashrate: []
|
||||
});
|
||||
if (pName in statData[i].pools){
|
||||
a.hashrate.push([time, statData[i].pools[pName].hashrate]);
|
||||
a.workers.push([time, statData[i].pools[pName].workerCount]);
|
||||
a.blocks.push([time, statData[i].pools[pName].blocks.pending])
|
||||
}
|
||||
else{
|
||||
a.hashrate.push([time, 0]);
|
||||
a.workers.push([time, 0]);
|
||||
a.blocks.push([time, 0])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
poolWorkerData = [];
|
||||
poolHashrateData = [];
|
||||
poolBlockData = [];
|
||||
|
||||
for (var pool in pools){
|
||||
poolWorkerData.push({
|
||||
key: pool,
|
||||
values: pools[pool].workers
|
||||
});
|
||||
poolHashrateData.push({
|
||||
poolHashrateData.push({
|
||||
key: pool,
|
||||
values: pools[pool].hashrate
|
||||
});
|
||||
poolBlockData.push({
|
||||
key: pool,
|
||||
values: pools[pool].blocks
|
||||
})
|
||||
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
|
||||
}
|
||||
}
|
||||
|
||||
function calculateAverageHashrate(pool) {
|
||||
var count = 0;
|
||||
var total = 1;
|
||||
var avg = 0;
|
||||
for (var i = 0; i < poolHashrateData.length; i++) {
|
||||
count = 0;
|
||||
for (var ii = 0; ii < poolHashrateData[i].values.length; ii++) {
|
||||
if (pool == null || poolHashrateData[i].key === pool) {
|
||||
count++;
|
||||
avg += parseFloat(poolHashrateData[i].values[ii][1]);
|
||||
}
|
||||
}
|
||||
if (count > total)
|
||||
total = count;
|
||||
}
|
||||
avg = avg / total;
|
||||
return avg;
|
||||
}
|
||||
|
||||
function getReadableHashRateString(hashrate){
|
||||
if (hashrate < 1000000)
|
||||
return '0 Sol';
|
||||
hashrate = (hashrate * 2);
|
||||
if (hashrate < 1000000) {
|
||||
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
|
||||
}
|
||||
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
|
||||
hashrate = (hashrate * 2);
|
||||
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
|
||||
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
|
||||
return Math.round(hashrate) + byteUnits[i];
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
|
||||
function timeOfDayFormat(timestamp){
|
||||
|
@ -85,28 +78,9 @@ function timeOfDayFormat(timestamp){
|
|||
}
|
||||
|
||||
function displayCharts(){
|
||||
|
||||
nv.addGraph(function() {
|
||||
poolWorkerChart = nv.models.stackedAreaChart()
|
||||
.margin({left: 40, right: 40})
|
||||
.x(function(d){ return d[0] })
|
||||
.y(function(d){ return d[1] })
|
||||
.useInteractiveGuideline(true)
|
||||
.clipEdge(true);
|
||||
|
||||
poolWorkerChart.xAxis.tickFormat(timeOfDayFormat);
|
||||
|
||||
poolWorkerChart.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#poolWorkers').datum(poolWorkerData).call(poolWorkerChart);
|
||||
|
||||
return poolWorkerChart;
|
||||
});
|
||||
|
||||
|
||||
nv.addGraph(function() {
|
||||
poolHashrateChart = nv.models.lineChart()
|
||||
.margin({left: 60, right: 40})
|
||||
.margin({left: 80, right: 30})
|
||||
.x(function(d){ return d[0] })
|
||||
.y(function(d){ return d[1] })
|
||||
.useInteractiveGuideline(true);
|
||||
|
@ -121,30 +95,13 @@ function displayCharts(){
|
|||
|
||||
return poolHashrateChart;
|
||||
});
|
||||
|
||||
|
||||
nv.addGraph(function() {
|
||||
poolBlockChart = nv.models.multiBarChart()
|
||||
.x(function(d){ return d[0] })
|
||||
.y(function(d){ return d[1] });
|
||||
|
||||
poolBlockChart.xAxis.tickFormat(timeOfDayFormat);
|
||||
|
||||
poolBlockChart.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#poolBlocks').datum(poolBlockData).call(poolBlockChart);
|
||||
|
||||
return poolBlockChart;
|
||||
});
|
||||
}
|
||||
|
||||
function TriggerChartUpdates(){
|
||||
poolWorkerChart.update();
|
||||
function triggerChartUpdates(){
|
||||
poolHashrateChart.update();
|
||||
poolBlockChart.update();
|
||||
}
|
||||
|
||||
nv.utils.windowResize(TriggerChartUpdates);
|
||||
nv.utils.windowResize(triggerChartUpdates);
|
||||
|
||||
$.getJSON('/api/pool_stats', function(data){
|
||||
statData = data;
|
||||
|
@ -156,7 +113,6 @@ statsSource.addEventListener('message', function(e){
|
|||
var stats = JSON.parse(e.data);
|
||||
statData.push(stats);
|
||||
|
||||
|
||||
var newPoolAdded = (function(){
|
||||
for (var p in stats.pools){
|
||||
if (poolKeys.indexOf(p) === -1)
|
||||
|
@ -173,30 +129,15 @@ statsSource.addEventListener('message', function(e){
|
|||
var time = stats.time * 1000;
|
||||
for (var f = 0; f < poolKeys.length; f++) {
|
||||
var pool = poolKeys[f];
|
||||
for (var i = 0; i < poolWorkerData.length; i++) {
|
||||
if (poolWorkerData[i].key === pool) {
|
||||
poolWorkerData[i].values.shift();
|
||||
poolWorkerData[i].values.push([time, pool in stats.pools ? stats.pools[pool].workerCount : 0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < poolHashrateData.length; i++) {
|
||||
if (poolHashrateData[i].key === pool) {
|
||||
poolHashrateData[i].values.shift();
|
||||
poolHashrateData[i].values.push([time, pool in stats.pools ? stats.pools[pool].hashrate : 0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < poolBlockData.length; i++) {
|
||||
if (poolBlockData[i].key === pool) {
|
||||
poolBlockData[i].values.shift();
|
||||
poolBlockData[i].values.push([time, pool in stats.pools ? stats.pools[pool].blocks.pending : 0]);
|
||||
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TriggerChartUpdates();
|
||||
triggerChartUpdates();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue