Merge pull request #94 from hellcatz/hellcatz

Major Updates
This commit is contained in:
Procrastinator 2017-03-11 09:45:13 -05:00 committed by GitHub
commit 86778a2ae2
11 changed files with 1579 additions and 345 deletions

View File

@ -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': {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &nbsp;&nbsp;
<span style="float:right;"><small>
<i class="fa fa-bars"></i> <span id="statsValidBlocks{{=pool}}">{{=it.stats.pools[pool].poolStats.validBlocks}}</span> Blocks &nbsp;&nbsp;
<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>&nbsp;&nbsp;</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>

View File

@ -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 &nbsp;&nbsp;
<small><i class="fa fa-users"></i> <span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].minerCount}}</span> Miners &nbsp;&nbsp;
<i class="fa fa-rocket"></i> <span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Workers &nbsp;&nbsp;
<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>
{{ } }}

View File

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

View File

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