Merge pull request #195 from hellcatz/immature
Immature Balance Tracking
This commit is contained in:
commit
ff0e1f6f32
|
@ -22,6 +22,13 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
res.header('Content-Type', 'application/json');
|
res.header('Content-Type', 'application/json');
|
||||||
res.end(JSON.stringify(portalStats.statPoolHistory));
|
res.end(JSON.stringify(portalStats.statPoolHistory));
|
||||||
return;
|
return;
|
||||||
|
case 'blocks':
|
||||||
|
case 'getblocksstats':
|
||||||
|
portalStats.getBlocks(function(data){
|
||||||
|
res.header('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'payments':
|
case 'payments':
|
||||||
var poolBlocks = [];
|
var poolBlocks = [];
|
||||||
for(var pool in portalStats.stats.pools) {
|
for(var pool in portalStats.stats.pools) {
|
||||||
|
@ -82,7 +89,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.end(JSON.stringify({miner: address, totalHash: totalHash, totalShares: totalShares, networkSols: networkSols, balance: balances.totalHeld, paid: balances.totalPaid, workers: workers, history: history}));
|
res.end(JSON.stringify({miner: address, totalHash: totalHash, totalShares: totalShares, networkSols: networkSols, immature: balances.totalImmature, balance: balances.totalHeld, paid: balances.totalPaid, workers: workers, history: history}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -54,14 +54,14 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
// zcash team recommends 10 confirmations for safety from orphaned blocks
|
// zcash team recommends 10 confirmations for safety from orphaned blocks
|
||||||
var minConfShield = Math.max((processingConfig.minConf || 10), 1); // Don't allow 0 conf transactions.
|
var minConfShield = Math.max((processingConfig.minConf || 10), 1); // Don't allow 0 conf transactions.
|
||||||
var minConfPayout = Math.max((processingConfig.minConf || 10), 1);
|
var minConfPayout = Math.max((processingConfig.minConf || 10), 1);
|
||||||
if (minConfPayout < 10) {
|
if (minConfPayout < 3) {
|
||||||
logger.warning(logSystem, logComponent, logComponent + 'minConf of 10 is recommended to reduce chances of payments being orphaned.');
|
logger.warning(logSystem, logComponent, logComponent + ' minConf of 3 is recommended.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimum paymentInterval of 60 seconds
|
// minimum paymentInterval of 60 seconds
|
||||||
var paymentIntervalSecs = Math.max((processingConfig.paymentInterval || 180), 60);
|
var paymentIntervalSecs = Math.max((processingConfig.paymentInterval || 120), 30);
|
||||||
if (parseInt(processingConfig.paymentInterval) < 180) {
|
if (parseInt(processingConfig.paymentInterval) < 120) {
|
||||||
logger.warning(logSystem, logComponent, 'paymentInterval of 180 seconds recommended to reduce the RPC work queue.');
|
logger.warning(logSystem, logComponent, ' minimum paymentInterval of 120 seconds recommended.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxBlocksPerPayment = Math.max(processingConfig.maxBlocksPerPayment || 3, 1);
|
var maxBlocksPerPayment = Math.max(processingConfig.maxBlocksPerPayment || 3, 1);
|
||||||
|
@ -84,8 +84,10 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
logger[severity](logSystem, logComponent, message);
|
logger[severity](logSystem, logComponent, message);
|
||||||
});
|
});
|
||||||
var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host);
|
var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host);
|
||||||
// redis auth if enabled
|
// redis auth if enabled
|
||||||
|
if (poolOptions.redis.password) {
|
||||||
redisClient.auth(poolOptions.redis.password);
|
redisClient.auth(poolOptions.redis.password);
|
||||||
|
}
|
||||||
|
|
||||||
var magnitude;
|
var magnitude;
|
||||||
var minPaymentSatoshis;
|
var minPaymentSatoshis;
|
||||||
|
@ -425,17 +427,22 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
}, shielding_interval);
|
}, shielding_interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// stats caching every 58 seconds
|
// network stats caching every 58 seconds
|
||||||
var stats_interval = 58 * 1000;
|
var stats_interval = 58 * 1000;
|
||||||
var statsInterval = setInterval(function() {
|
var statsInterval = setInterval(function() {
|
||||||
// update network stats using coin daemon
|
// update network stats using coin daemon
|
||||||
cacheNetworkStats();
|
cacheNetworkStats();
|
||||||
// update market stats using coinmarketcap
|
|
||||||
if (getMarketStats === true) {
|
|
||||||
cacheMarketStats();
|
|
||||||
}
|
|
||||||
}, stats_interval);
|
}, stats_interval);
|
||||||
|
|
||||||
|
// market stats caching every 5 minutes
|
||||||
|
if (getMarketStats === true) {
|
||||||
|
var market_stats_interval = 300 * 1000;
|
||||||
|
var marketStatsInterval = setInterval(function() {
|
||||||
|
// update market stats using coinmarketcap
|
||||||
|
cacheMarketStats();
|
||||||
|
}, market_stats_interval);
|
||||||
|
}
|
||||||
|
|
||||||
// check operation statuses every 57 seconds
|
// check operation statuses every 57 seconds
|
||||||
var opid_interval = 57 * 1000;
|
var opid_interval = 57 * 1000;
|
||||||
// shielding not required for some equihash coins
|
// shielding not required for some equihash coins
|
||||||
|
@ -454,6 +461,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
logger.warning(logSystem, logComponent, 'Clearing operation ids due to empty result set.');
|
logger.warning(logSystem, logComponent, 'Clearing operation ids due to empty result set.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// loop through op-ids checking their status
|
||||||
ops.forEach(function(op, i){
|
ops.forEach(function(op, i){
|
||||||
// check operation id status
|
// check operation id status
|
||||||
if (op.status == "success" || op.status == "failed") {
|
if (op.status == "success" || op.status == "failed") {
|
||||||
|
@ -606,6 +614,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
txHash: details[1],
|
txHash: details[1],
|
||||||
height: details[2],
|
height: details[2],
|
||||||
minedby: details[3],
|
minedby: details[3],
|
||||||
|
time: details[4],
|
||||||
duplicate: false,
|
duplicate: false,
|
||||||
serialized: r
|
serialized: r
|
||||||
};
|
};
|
||||||
|
@ -696,7 +705,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
Step 2 - check if mined block coinbase tx are ready for payment
|
Step 2 - check if mined block coinbase tx are ready for payment
|
||||||
* adds block reward to rounds object
|
* adds block reward to rounds object
|
||||||
* adds block confirmations count to rounds object
|
* adds block confirmations count to rounds object
|
||||||
* updates confirmation counts in redis
|
|
||||||
*/
|
*/
|
||||||
function(workers, rounds, callback){
|
function(workers, rounds, callback){
|
||||||
// get pending block tx details
|
// get pending block tx details
|
||||||
|
@ -715,7 +723,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var confirmsUpdate = [];
|
|
||||||
var addressAccount = "";
|
var addressAccount = "";
|
||||||
|
|
||||||
// check for transaction errors and generated coins
|
// check for transaction errors and generated coins
|
||||||
|
@ -727,6 +734,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var round = rounds[i];
|
var round = rounds[i];
|
||||||
|
// update confirmations for round
|
||||||
|
round.confirmations = parseInt((tx.result.confirmations || 0));
|
||||||
// look for transaction errors
|
// look for transaction errors
|
||||||
if (tx.error && tx.error.code === -5){
|
if (tx.error && tx.error.code === -5){
|
||||||
logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash);
|
logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash);
|
||||||
|
@ -755,13 +764,10 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
}
|
}
|
||||||
// get transaction category for round
|
// get transaction category for round
|
||||||
round.category = generationTx.category;
|
round.category = generationTx.category;
|
||||||
round.confirmations = parseInt((tx.result.confirmations || 0));
|
|
||||||
// get reward for newly generated blocks
|
// get reward for newly generated blocks
|
||||||
if (round.category === 'generate') {
|
if (round.category === 'generate' || round.category === 'immature') {
|
||||||
round.reward = coinsRound(parseFloat(generationTx.amount || generationTx.value));
|
round.reward = coinsRound(parseFloat(generationTx.amount || generationTx.value));
|
||||||
}
|
}
|
||||||
// update confirmations in redis
|
|
||||||
confirmsUpdate.push(['hset', coin + ':blocksPendingConfirms', round.blockHash, round.confirmations]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var canDeleteShares = function(r){
|
var canDeleteShares = function(r){
|
||||||
|
@ -784,66 +790,33 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
case 'orphan':
|
case 'orphan':
|
||||||
case 'kicked':
|
case 'kicked':
|
||||||
r.canDeleteShares = canDeleteShares(r);
|
r.canDeleteShares = canDeleteShares(r);
|
||||||
|
case 'immature':
|
||||||
return true;
|
return true;
|
||||||
case 'generate':
|
case 'generate':
|
||||||
payingBlocks++;
|
payingBlocks++;
|
||||||
return (payingBlocks <= maxBlocksPerPayment);
|
// if over maxBlocksPerPayment...
|
||||||
|
// change category to immature to prevent payment
|
||||||
|
// and to keep track of confirmations/immature balances
|
||||||
|
if (payingBlocks > maxBlocksPerPayment)
|
||||||
|
r.category = 'immature';
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: make tx fees dynamic
|
// continue to next step in waterfall
|
||||||
var feeSatoshi = coinsToSatoshies(fee);
|
callback(null, workers, rounds, addressAccount);
|
||||||
// calculate what the pool owes its miners
|
});
|
||||||
var totalOwed = parseInt(0);
|
|
||||||
for (var i = 0; i < rounds.length; i++) {
|
|
||||||
// only pay generated blocks, not orphaned or kicked
|
|
||||||
if (rounds[i].category == 'generate') {
|
|
||||||
totalOwed = totalOwed + coinsToSatoshies(rounds[i].reward) - feeSatoshi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var notAddr = null;
|
|
||||||
if (requireShielding === true) {
|
|
||||||
notAddr = poolOptions.address;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update confirmations for pending blocks in redis
|
|
||||||
if (confirmsUpdate.length > 0) {
|
|
||||||
startRedisTimer();
|
|
||||||
redisClient.multi(confirmsUpdate).exec(function(error, result){
|
|
||||||
endRedisTimer();
|
|
||||||
if (error) {
|
|
||||||
logger.error(logSystem, logComponent, 'Error could not update confirmations for pending blocks in redis ' + JSON.stringify(error));
|
|
||||||
return callback(true);
|
|
||||||
}
|
|
||||||
// check if we have enough tAddress funds to begin payment processing
|
|
||||||
listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){
|
|
||||||
if (error) {
|
|
||||||
logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments.');
|
|
||||||
return callback(true);
|
|
||||||
} else if (tBalance < totalOwed) {
|
|
||||||
logger.error(logSystem, logComponent, 'Insufficient funds ('+satoshisToCoins(tBalance) + ') to process payments (' + satoshisToCoins(totalOwed)+') for ' + payingBlocks + ' blocks; possibly waiting for txs.');
|
|
||||||
return callback(true);
|
|
||||||
}
|
|
||||||
// account feature not implemented at this time
|
|
||||||
addressAccount = "";
|
|
||||||
// begin payments for generated coins
|
|
||||||
callback(null, workers, rounds, addressAccount);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// no pending blocks, need to find a block!
|
|
||||||
return callback(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Step 3 - lookup shares in redis and calculate rewards
|
Step 3 - lookup shares and calculate rewards
|
||||||
|
* pull pplnt times from redis
|
||||||
|
* pull shares from redis
|
||||||
|
* calculate rewards
|
||||||
|
* pplnt share reductions if needed
|
||||||
*/
|
*/
|
||||||
function(workers, rounds, addressAccount, callback){
|
function(workers, rounds, addressAccount, callback){
|
||||||
// pplnt times lookup
|
// pplnt times lookup
|
||||||
|
@ -857,6 +830,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
callback('Check finished - redis error with multi get rounds time');
|
callback('Check finished - redis error with multi get rounds time');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// shares lookup
|
||||||
var shareLookups = rounds.map(function(r){
|
var shareLookups = rounds.map(function(r){
|
||||||
return ['hgetall', coin + ':shares:round' + r.height];
|
return ['hgetall', coin + ':shares:round' + r.height];
|
||||||
});
|
});
|
||||||
|
@ -870,106 +844,218 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
// error detection
|
// error detection
|
||||||
var err = null;
|
var err = null;
|
||||||
|
var performPayment = false;
|
||||||
|
|
||||||
// total shares
|
var notAddr = null;
|
||||||
rounds.forEach(function(round, i){
|
if (requireShielding === true) {
|
||||||
var workerShares = allWorkerShares[i];
|
notAddr = poolOptions.address;
|
||||||
if (!workerShares){
|
|
||||||
err = true;
|
|
||||||
logger.error(logSystem, logComponent, 'No worker shares for round: ' + round.height + ' blockHash: ' + round.blockHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var workerTimes = allWorkerTimes[i];
|
|
||||||
switch (round.category){
|
|
||||||
case 'kicked':
|
|
||||||
case 'orphan':
|
|
||||||
round.workerShares = workerShares;
|
|
||||||
break;
|
|
||||||
case 'generate':
|
|
||||||
// TODO: make tx fees dynamic
|
|
||||||
var feeSatoshi = coinsToSatoshies(fee);
|
|
||||||
var reward = coinsToSatoshies(round.reward) - feeSatoshi;
|
|
||||||
var totalShares = parseFloat(0);
|
|
||||||
var sharesLost = parseFloat(0);
|
|
||||||
// find most time spent in this round by single worker
|
|
||||||
maxTime = 0;
|
|
||||||
for (var workerAddress in workerTimes){
|
|
||||||
if (maxTime < parseFloat(workerTimes[workerAddress]))
|
|
||||||
maxTime = parseFloat(workerTimes[workerAddress]);
|
|
||||||
}
|
|
||||||
// total up shares for round
|
|
||||||
for (var workerAddress in workerShares){
|
|
||||||
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
|
||||||
var shares = parseFloat((workerShares[workerAddress] || 0));
|
|
||||||
// if pplnt mode
|
|
||||||
if (pplntEnabled === true && maxTime > 0) {
|
|
||||||
var tshares = shares;
|
|
||||||
var lost = parseFloat(0);
|
|
||||||
var address = workerAddress.split('.')[0];
|
|
||||||
if (workerTimes[address] != null && parseFloat(workerTimes[address]) > 0) {
|
|
||||||
var timePeriod = roundTo(parseFloat(workerTimes[address] || 1) / maxTime , 2);
|
|
||||||
if (timePeriod > 0 && timePeriod < pplntTimeQualify) {
|
|
||||||
var lost = shares - (shares * timePeriod);
|
|
||||||
sharesLost += lost;
|
|
||||||
shares = Math.max(shares - lost, 0);
|
|
||||||
logger.warning(logSystem, logComponent, 'PPLNT: Reduced shares for '+workerAddress+' round:' + round.height + ' maxTime:'+maxTime+'sec timePeriod:'+roundTo(timePeriod,6)+' shares:'+tshares+' lost:'+lost+' new:'+shares);
|
|
||||||
}
|
|
||||||
if (timePeriod > 1.0) {
|
|
||||||
err = true;
|
|
||||||
logger.error(logSystem, logComponent, 'Time share period is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
worker.timePeriod = timePeriod;
|
|
||||||
} else {
|
|
||||||
logger.warning(logSystem, logComponent, 'PPLNT: Missing time share period for '+workerAddress+', miner shares qualified in round ' + round.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
worker.roundShares = shares;
|
|
||||||
worker.totalShares = parseFloat(worker.totalShares || 0) + shares;
|
|
||||||
totalShares += shares;
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log('--REWARD DEBUG--------------');
|
|
||||||
// calculate rewards for round
|
|
||||||
var totalAmount = 0;
|
|
||||||
for (var workerAddress in workerShares){
|
|
||||||
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
|
||||||
var percent = parseFloat(worker.roundShares) / totalShares;
|
|
||||||
if (percent > 1.0) {
|
|
||||||
err = true;
|
|
||||||
logger.error(logSystem, logComponent, 'Share percent is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// calculate workers reward for this round
|
|
||||||
var workerRewardTotal = Math.round(reward * percent);
|
|
||||||
// add to total reward for worker
|
|
||||||
worker.reward = (worker.reward || 0) + workerRewardTotal;
|
|
||||||
// add to total amount sent to all workers
|
|
||||||
totalAmount += worker.reward;
|
|
||||||
//console.log('rewardAmount: '+workerAddress+' '+workerRewardTotal);
|
|
||||||
//console.log('totalAmount: '+workerAddress+' '+worker.reward);
|
|
||||||
}
|
|
||||||
//console.log('totalAmount: '+totalAmount);
|
|
||||||
//console.log('blockHeight: '+round.height);
|
|
||||||
//console.log('blockReward: '+reward);
|
|
||||||
//console.log('totalShares: '+totalShares);
|
|
||||||
//console.log('sharesLost: '+sharesLost);
|
|
||||||
//console.log('----------------------------');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// if there was no errors
|
|
||||||
if (err === null) {
|
|
||||||
// continue payments
|
|
||||||
callback(null, workers, rounds, addressAccount);
|
|
||||||
} else {
|
|
||||||
// stop waterfall flow, do not process payments
|
|
||||||
callback(true);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
});
|
// calculate what the pool owes its miners
|
||||||
|
var feeSatoshi = coinsToSatoshies(fee);
|
||||||
|
var totalOwed = parseInt(0);
|
||||||
|
for (var i = 0; i < rounds.length; i++) {
|
||||||
|
// only pay generated blocks, not orphaned, kicked, immature
|
||||||
|
if (rounds[i].category == 'generate') {
|
||||||
|
totalOwed = totalOwed + coinsToSatoshies(rounds[i].reward) - feeSatoshi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// also include balances owed
|
||||||
|
for (var w in workers) {
|
||||||
|
var worker = workers[w];
|
||||||
|
totalOwed = totalOwed + (worker.balance||0);
|
||||||
|
}
|
||||||
|
// check if we have enough tAddress funds to begin payment processing
|
||||||
|
listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){
|
||||||
|
if (error) {
|
||||||
|
logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments.');
|
||||||
|
return callback(true);
|
||||||
|
} else if (tBalance < totalOwed) {
|
||||||
|
logger.error(logSystem, logComponent, 'Insufficient funds ('+satoshisToCoins(tBalance) + ') to process payments (' + satoshisToCoins(totalOwed)+'); possibly waiting for txs.');
|
||||||
|
performPayment = false;
|
||||||
|
} else if (tBalance > totalOwed) {
|
||||||
|
performPayment = true;
|
||||||
|
}
|
||||||
|
// just in case...
|
||||||
|
if (totalOwed <= 0) {
|
||||||
|
performPayment = false;
|
||||||
|
}
|
||||||
|
// if we can not perform payment
|
||||||
|
if (performPayment === false) {
|
||||||
|
// convert category generate to immature
|
||||||
|
rounds = rounds.filter(function(r){
|
||||||
|
switch (r.category) {
|
||||||
|
case 'orphan':
|
||||||
|
case 'kicked':
|
||||||
|
case 'immature':
|
||||||
|
return true;
|
||||||
|
case 'generate':
|
||||||
|
r.category = 'immature';
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle rounds
|
||||||
|
rounds.forEach(function(round, i){
|
||||||
|
var workerShares = allWorkerShares[i];
|
||||||
|
if (!workerShares){
|
||||||
|
err = true;
|
||||||
|
logger.error(logSystem, logComponent, 'No worker shares for round: ' + round.height + ' blockHash: ' + round.blockHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var workerTimes = allWorkerTimes[i];
|
||||||
|
|
||||||
|
switch (round.category){
|
||||||
|
case 'kicked':
|
||||||
|
case 'orphan':
|
||||||
|
round.workerShares = workerShares;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* calculate immature balances */
|
||||||
|
case 'immature':
|
||||||
|
var feeSatoshi = coinsToSatoshies(fee);
|
||||||
|
var immature = coinsToSatoshies(round.reward);
|
||||||
|
var totalShares = parseFloat(0);
|
||||||
|
var sharesLost = parseFloat(0);
|
||||||
|
|
||||||
|
// adjust block immature .. tx fees
|
||||||
|
immature = Math.round(immature - feeSatoshi);
|
||||||
|
|
||||||
|
// find most time spent in this round by single worker
|
||||||
|
maxTime = 0;
|
||||||
|
for (var workerAddress in workerTimes){
|
||||||
|
if (maxTime < parseFloat(workerTimes[workerAddress]))
|
||||||
|
maxTime = parseFloat(workerTimes[workerAddress]);
|
||||||
|
}
|
||||||
|
// total up shares for round
|
||||||
|
for (var workerAddress in workerShares){
|
||||||
|
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
||||||
|
var shares = parseFloat((workerShares[workerAddress] || 0));
|
||||||
|
// if pplnt mode
|
||||||
|
if (pplntEnabled === true && maxTime > 0) {
|
||||||
|
var tshares = shares;
|
||||||
|
var lost = parseFloat(0);
|
||||||
|
var address = workerAddress.split('.')[0];
|
||||||
|
if (workerTimes[address] != null && parseFloat(workerTimes[address]) > 0) {
|
||||||
|
var timePeriod = roundTo(parseFloat(workerTimes[address] || 1) / maxTime , 2);
|
||||||
|
if (timePeriod > 0 && timePeriod < pplntTimeQualify) {
|
||||||
|
var lost = shares - (shares * timePeriod);
|
||||||
|
sharesLost += lost;
|
||||||
|
shares = Math.max(shares - lost, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worker.roundShares = shares;
|
||||||
|
totalShares += shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('--IMMATURE DEBUG--------------');
|
||||||
|
//console.log('performPayment: '+performPayment);
|
||||||
|
//console.log('blockHeight: '+round.height);
|
||||||
|
//console.log('blockReward: '+Math.round(immature));
|
||||||
|
//console.log('blockConfirmations: '+round.confirmations);
|
||||||
|
|
||||||
|
// calculate rewards for round
|
||||||
|
var totalAmount = 0;
|
||||||
|
for (var workerAddress in workerShares){
|
||||||
|
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
||||||
|
var percent = parseFloat(worker.roundShares) / totalShares;
|
||||||
|
// calculate workers immature for this round
|
||||||
|
var workerImmatureTotal = Math.round(immature * percent);
|
||||||
|
worker.immature = (worker.immature || 0) + workerImmatureTotal;
|
||||||
|
totalAmount += workerImmatureTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('----------------------------');
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* calculate reward balances */
|
||||||
|
case 'generate':
|
||||||
|
var feeSatoshi = coinsToSatoshies(fee);
|
||||||
|
var reward = coinsToSatoshies(round.reward);
|
||||||
|
var totalShares = parseFloat(0);
|
||||||
|
var sharesLost = parseFloat(0);
|
||||||
|
|
||||||
|
// adjust block reward .. tx fees
|
||||||
|
reward = Math.round(reward - feeSatoshi);
|
||||||
|
|
||||||
|
// find most time spent in this round by single worker
|
||||||
|
maxTime = 0;
|
||||||
|
for (var workerAddress in workerTimes){
|
||||||
|
if (maxTime < parseFloat(workerTimes[workerAddress]))
|
||||||
|
maxTime = parseFloat(workerTimes[workerAddress]);
|
||||||
|
}
|
||||||
|
// total up shares for round
|
||||||
|
for (var workerAddress in workerShares){
|
||||||
|
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
||||||
|
var shares = parseFloat((workerShares[workerAddress] || 0));
|
||||||
|
// if pplnt mode
|
||||||
|
if (pplntEnabled === true && maxTime > 0) {
|
||||||
|
var tshares = shares;
|
||||||
|
var lost = parseFloat(0);
|
||||||
|
var address = workerAddress.split('.')[0];
|
||||||
|
if (workerTimes[address] != null && parseFloat(workerTimes[address]) > 0) {
|
||||||
|
var timePeriod = roundTo(parseFloat(workerTimes[address] || 1) / maxTime , 2);
|
||||||
|
if (timePeriod > 0 && timePeriod < pplntTimeQualify) {
|
||||||
|
var lost = shares - (shares * timePeriod);
|
||||||
|
sharesLost += lost;
|
||||||
|
shares = Math.max(shares - lost, 0);
|
||||||
|
logger.warning(logSystem, logComponent, 'PPLNT: Reduced shares for '+workerAddress+' round:' + round.height + ' maxTime:'+maxTime+'sec timePeriod:'+roundTo(timePeriod,6)+' shares:'+tshares+' lost:'+lost+' new:'+shares);
|
||||||
|
}
|
||||||
|
if (timePeriod > 1.0) {
|
||||||
|
err = true;
|
||||||
|
logger.error(logSystem, logComponent, 'Time share period is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
worker.timePeriod = timePeriod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worker.roundShares = shares;
|
||||||
|
worker.totalShares = parseFloat(worker.totalShares || 0) + shares;
|
||||||
|
totalShares += shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('--REWARD DEBUG--------------');
|
||||||
|
//console.log('performPayment: '+performPayment);
|
||||||
|
//console.log('blockHeight: '+round.height);
|
||||||
|
//console.log('blockReward: ' + Math.round(reward));
|
||||||
|
//console.log('blockConfirmations: '+round.confirmations);
|
||||||
|
|
||||||
|
// calculate rewards for round
|
||||||
|
var totalAmount = 0;
|
||||||
|
for (var workerAddress in workerShares){
|
||||||
|
var worker = workers[workerAddress] = (workers[workerAddress] || {});
|
||||||
|
var percent = parseFloat(worker.roundShares) / totalShares;
|
||||||
|
if (percent > 1.0) {
|
||||||
|
err = true;
|
||||||
|
logger.error(logSystem, logComponent, 'Share percent is greater than 1.0 for '+workerAddress+' round:' + round.height + ' blockHash:' + round.blockHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// calculate workers reward for this round
|
||||||
|
var workerRewardTotal = Math.round(reward * percent);
|
||||||
|
worker.reward = (worker.reward || 0) + workerRewardTotal;
|
||||||
|
totalAmount += workerRewardTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('----------------------------');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if there was no errors
|
||||||
|
if (err === null) {
|
||||||
|
callback(null, workers, rounds, addressAccount);
|
||||||
|
} else {
|
||||||
|
// some error, stop waterfall
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}); // end funds check
|
||||||
|
});// end share lookup
|
||||||
|
}); // end time lookup
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -983,6 +1069,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
var tries = 0;
|
var tries = 0;
|
||||||
var trySend = function (withholdPercent) {
|
var trySend = function (withholdPercent) {
|
||||||
|
|
||||||
var addressAmounts = {};
|
var addressAmounts = {};
|
||||||
var balanceAmounts = {};
|
var balanceAmounts = {};
|
||||||
var shareAmounts = {};
|
var shareAmounts = {};
|
||||||
|
@ -1022,7 +1109,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
// send funds
|
// send funds
|
||||||
worker.sent = satoshisToCoins(toSendSatoshis);
|
worker.sent = satoshisToCoins(toSendSatoshis);
|
||||||
worker.balanceChange = Math.min(worker.balance, toSendSatoshis) * -1;
|
worker.balanceChange = Math.min(worker.balance, toSendSatoshis) * -1;
|
||||||
// multiple workers may have same address, add them up
|
|
||||||
if (addressAmounts[address] != null && addressAmounts[address] > 0) {
|
if (addressAmounts[address] != null && addressAmounts[address] > 0) {
|
||||||
addressAmounts[address] = coinsRound(addressAmounts[address] + worker.sent);
|
addressAmounts[address] = coinsRound(addressAmounts[address] + worker.sent);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1053,7 +1139,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
// if no payouts...continue to next set of callbacks
|
// if no payouts...continue to next set of callbacks
|
||||||
if (Object.keys(addressAmounts).length === 0){
|
if (Object.keys(addressAmounts).length === 0){
|
||||||
callback(null, workers, rounds);
|
callback(null, workers, rounds, []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1068,6 +1154,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
// perform the sendmany operation .. addressAccount
|
// perform the sendmany operation .. addressAccount
|
||||||
var rpccallTracking = 'sendmany "" '+JSON.stringify(addressAmounts);
|
var rpccallTracking = 'sendmany "" '+JSON.stringify(addressAmounts);
|
||||||
|
//console.log(rpccallTracking);
|
||||||
|
|
||||||
daemon.cmd('sendmany', ["", addressAmounts], function (result) {
|
daemon.cmd('sendmany', ["", addressAmounts], function (result) {
|
||||||
// check for failed payments, there are many reasons
|
// check for failed payments, there are many reasons
|
||||||
if (result.error && result.error.code === -6) {
|
if (result.error && result.error.code === -6) {
|
||||||
|
@ -1144,15 +1232,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
var paymentsUpdate = [];
|
var paymentsUpdate = [];
|
||||||
var paymentsData = {time:Date.now(), txid:txid, shares:totalShares, paid:satoshisToCoins(totalSent), miners:Object.keys(addressAmounts).length, blocks: paymentBlocks, amounts: addressAmounts, balances: balanceAmounts, work:shareAmounts};
|
var paymentsData = {time:Date.now(), txid:txid, shares:totalShares, paid:satoshisToCoins(totalSent), miners:Object.keys(addressAmounts).length, blocks: paymentBlocks, amounts: addressAmounts, balances: balanceAmounts, work:shareAmounts};
|
||||||
paymentsUpdate.push(['zadd', logComponent + ':payments', Date.now(), JSON.stringify(paymentsData)]);
|
paymentsUpdate.push(['zadd', logComponent + ':payments', Date.now(), JSON.stringify(paymentsData)]);
|
||||||
startRedisTimer();
|
|
||||||
redisClient.multi(paymentsUpdate).exec(function(error, payments){
|
callback(null, workers, rounds, paymentsUpdate);
|
||||||
endRedisTimer();
|
|
||||||
if (error){
|
|
||||||
logger.error(logSystem, logComponent, 'Error redis save payments data ' + JSON.stringify(payments));
|
|
||||||
}
|
|
||||||
// perform final redis updates
|
|
||||||
callback(null, workers, rounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
@ -1168,25 +1249,27 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
}, true, true);
|
}, true, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// attempt to send any owed payments
|
||||||
trySend(0);
|
trySend(0);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Step 5 - Final redis commands
|
Step 5 - Final redis commands
|
||||||
*/
|
*/
|
||||||
function(workers, rounds, callback){
|
function(workers, rounds, paymentsUpdate, callback){
|
||||||
|
|
||||||
var totalPaid = parseFloat(0);
|
var totalPaid = parseFloat(0);
|
||||||
|
|
||||||
|
var immatureUpdateCommands = [];
|
||||||
var balanceUpdateCommands = [];
|
var balanceUpdateCommands = [];
|
||||||
var workerPayoutsCommand = [];
|
var workerPayoutsCommand = [];
|
||||||
|
|
||||||
// update worker paid/balance stats
|
// update worker paid/balance stats
|
||||||
for (var w in workers) {
|
for (var w in workers) {
|
||||||
var worker = workers[w];
|
var worker = workers[w];
|
||||||
if (worker.balanceChange !== 0){
|
// update balances
|
||||||
|
if ((worker.balanceChange || 0) !== 0){
|
||||||
balanceUpdateCommands.push([
|
balanceUpdateCommands.push([
|
||||||
'hincrbyfloat',
|
'hincrbyfloat',
|
||||||
coin + ':balances',
|
coin + ':balances',
|
||||||
|
@ -1194,41 +1277,51 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
satoshisToCoins(worker.balanceChange)
|
satoshisToCoins(worker.balanceChange)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (worker.sent !== 0){
|
// update payouts
|
||||||
|
if ((worker.sent || 0) > 0){
|
||||||
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, coinsRound(worker.sent)]);
|
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, coinsRound(worker.sent)]);
|
||||||
totalPaid = coinsRound(totalPaid + worker.sent);
|
totalPaid = coinsRound(totalPaid + worker.sent);
|
||||||
}
|
}
|
||||||
|
// update immature balances
|
||||||
|
if ((worker.immature || 0) > 0) {
|
||||||
|
immatureUpdateCommands.push(['hset', coin + ':immature', w, worker.immature]);
|
||||||
|
} else {
|
||||||
|
immatureUpdateCommands.push(['hset', coin + ':immature', w, 0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var movePendingCommands = [];
|
var movePendingCommands = [];
|
||||||
var roundsToDelete = [];
|
var roundsToDelete = [];
|
||||||
var orphanMergeCommands = [];
|
var orphanMergeCommands = [];
|
||||||
|
|
||||||
|
var confirmsUpdate = [];
|
||||||
var confirmsToDelete = [];
|
var confirmsToDelete = [];
|
||||||
|
|
||||||
var moveSharesToCurrent = function(r){
|
var moveSharesToCurrent = function(r){
|
||||||
var workerShares = r.workerShares;
|
var workerShares = r.workerShares;
|
||||||
if (workerShares != null) {
|
if (workerShares != null) {
|
||||||
|
logger.warning(logSystem, logComponent, 'Moving shares from orphaned block '+r.height+' to current round.');
|
||||||
Object.keys(workerShares).forEach(function(worker){
|
Object.keys(workerShares).forEach(function(worker){
|
||||||
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent', worker, workerShares[worker]]);
|
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent', worker, workerShares[worker]]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// handle the round
|
|
||||||
rounds.forEach(function(r){
|
rounds.forEach(function(r){
|
||||||
switch(r.category){
|
switch(r.category){
|
||||||
case 'kicked':
|
case 'kicked':
|
||||||
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
|
|
||||||
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]);
|
|
||||||
case 'orphan':
|
case 'orphan':
|
||||||
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
|
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
|
||||||
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksOrphaned', r.serialized]);
|
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]);
|
||||||
if (r.canDeleteShares){
|
if (r.canDeleteShares){
|
||||||
moveSharesToCurrent(r);
|
moveSharesToCurrent(r);
|
||||||
roundsToDelete.push(coin + ':shares:round' + r.height);
|
roundsToDelete.push(coin + ':shares:round' + r.height);
|
||||||
roundsToDelete.push(coin + ':shares:times' + r.height);
|
roundsToDelete.push(coin + ':shares:times' + r.height);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 'immature':
|
||||||
|
confirmsUpdate.push(['hset', coin + ':blocksPendingConfirms', r.blockHash, (r.confirmations || 0)]);
|
||||||
|
return;
|
||||||
case 'generate':
|
case 'generate':
|
||||||
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
|
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
|
||||||
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksConfirmed', r.serialized]);
|
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksConfirmed', r.serialized]);
|
||||||
|
@ -1246,6 +1339,9 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
if (orphanMergeCommands.length > 0)
|
if (orphanMergeCommands.length > 0)
|
||||||
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
|
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
|
||||||
|
|
||||||
|
if (immatureUpdateCommands.length > 0)
|
||||||
|
finalRedisCommands = finalRedisCommands.concat(immatureUpdateCommands);
|
||||||
|
|
||||||
if (balanceUpdateCommands.length > 0)
|
if (balanceUpdateCommands.length > 0)
|
||||||
finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);
|
finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);
|
||||||
|
|
||||||
|
@ -1255,9 +1351,15 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
if (roundsToDelete.length > 0)
|
if (roundsToDelete.length > 0)
|
||||||
finalRedisCommands.push(['del'].concat(roundsToDelete));
|
finalRedisCommands.push(['del'].concat(roundsToDelete));
|
||||||
|
|
||||||
|
if (confirmsUpdate.length > 0)
|
||||||
|
finalRedisCommands = finalRedisCommands.concat(confirmsUpdate);
|
||||||
|
|
||||||
if (confirmsToDelete.length > 0)
|
if (confirmsToDelete.length > 0)
|
||||||
finalRedisCommands = finalRedisCommands.concat(confirmsToDelete);
|
finalRedisCommands = finalRedisCommands.concat(confirmsToDelete);
|
||||||
|
|
||||||
|
if (paymentsUpdate.length > 0)
|
||||||
|
finalRedisCommands = finalRedisCommands.concat(paymentsUpdate);
|
||||||
|
|
||||||
if (totalPaid !== 0)
|
if (totalPaid !== 0)
|
||||||
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]);
|
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]);
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ module.exports = function(logger){
|
||||||
var proxySwitch = {};
|
var proxySwitch = {};
|
||||||
|
|
||||||
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
|
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
|
||||||
// redis auth if enabled
|
if (portalConfig.redis.password) {
|
||||||
redisClient.auth(portalConfig.redis.password);
|
redisClient.auth(portalConfig.redis.password);
|
||||||
|
}
|
||||||
//Handle messages from master process sent via IPC
|
//Handle messages from master process sent via IPC
|
||||||
process.on('message', function(message) {
|
process.on('message', function(message) {
|
||||||
switch(message.type){
|
switch(message.type){
|
||||||
|
@ -143,7 +143,6 @@ module.exports = function(logger){
|
||||||
authCallback(isValid);
|
authCallback(isValid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handlers.share = function(isValidShare, isValidBlock, data){
|
handlers.share = function(isValidShare, isValidBlock, data){
|
||||||
|
|
|
@ -27,9 +27,9 @@ module.exports = function(logger, poolConfig){
|
||||||
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
|
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
|
||||||
|
|
||||||
var connection = redis.createClient(redisConfig.port, redisConfig.host);
|
var connection = redis.createClient(redisConfig.port, redisConfig.host);
|
||||||
// redis auth if needed
|
if (redisConfig.password) {
|
||||||
connection.auth(redisConfig.password);
|
connection.auth(redisConfig.password);
|
||||||
|
}
|
||||||
connection.on('ready', function(){
|
connection.on('ready', function(){
|
||||||
logger.debug(logSystem, logComponent, logSubCat, 'Share processing setup with redis (' + redisConfig.host +
|
logger.debug(logSystem, logComponent, logSubCat, 'Share processing setup with redis (' + redisConfig.host +
|
||||||
':' + redisConfig.port + ')');
|
':' + redisConfig.port + ')');
|
||||||
|
|
151
libs/stats.js
151
libs/stats.js
|
@ -10,13 +10,12 @@ var algos = require('stratum-pool/lib/algoProperties.js');
|
||||||
// redis callback Ready check failed bypass trick
|
// redis callback Ready check failed bypass trick
|
||||||
function rediscreateClient(port, host, pass) {
|
function rediscreateClient(port, host, pass) {
|
||||||
var client = redis.createClient(port, host);
|
var client = redis.createClient(port, host);
|
||||||
client.auth(pass);
|
if (pass) {
|
||||||
|
client.auth(pass);
|
||||||
|
}
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
function balanceRound(number) {
|
|
||||||
return parseFloat((Math.round(number * 100000000) / 100000000).toFixed(8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort object properties (only own properties will be sorted).
|
* Sort object properties (only own properties will be sorted).
|
||||||
|
@ -98,6 +97,24 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.getBlocks = function (cback) {
|
||||||
|
var allBlocks = {};
|
||||||
|
async.each(_this.stats.pools, function(pool, pcb) {
|
||||||
|
|
||||||
|
if (_this.stats.pools[pool.name].pending && _this.stats.pools[pool.name].pending.blocks)
|
||||||
|
for (var i=0; i<_this.stats.pools[pool.name].pending.blocks.length; i++)
|
||||||
|
allBlocks[pool.name+"-"+_this.stats.pools[pool.name].pending.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].pending.blocks[i];
|
||||||
|
|
||||||
|
if (_this.stats.pools[pool.name].confirmed && _this.stats.pools[pool.name].confirmed.blocks)
|
||||||
|
for (var i=0; i<_this.stats.pools[pool.name].confirmed.blocks.length; i++)
|
||||||
|
allBlocks[pool.name+"-"+_this.stats.pools[pool.name].confirmed.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].confirmed.blocks[i];
|
||||||
|
|
||||||
|
pcb();
|
||||||
|
}, function(err) {
|
||||||
|
cback(allBlocks);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function gatherStatHistory(){
|
function gatherStatHistory(){
|
||||||
var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString();
|
var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString();
|
||||||
redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){
|
redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){
|
||||||
|
@ -160,6 +177,31 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
_this.statPoolHistory.push(data);
|
_this.statPoolHistory.push(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var magnitude = 100000000;
|
||||||
|
var coinPrecision = magnitude.toString().length - 1;
|
||||||
|
|
||||||
|
function roundTo(n, digits) {
|
||||||
|
if (digits === undefined) {
|
||||||
|
digits = 0;
|
||||||
|
}
|
||||||
|
var multiplicator = Math.pow(10, digits);
|
||||||
|
n = parseFloat((n * multiplicator).toFixed(11));
|
||||||
|
var test =(Math.round(n) / multiplicator);
|
||||||
|
return +(test.toFixed(digits));
|
||||||
|
}
|
||||||
|
|
||||||
|
var satoshisToCoins = function(satoshis){
|
||||||
|
return roundTo((satoshis / magnitude), coinPrecision);
|
||||||
|
};
|
||||||
|
|
||||||
|
var coinsToSatoshies = function(coins){
|
||||||
|
return Math.round(coins * magnitude);
|
||||||
|
};
|
||||||
|
|
||||||
|
function coinsRound(number) {
|
||||||
|
return roundTo(number, coinPrecision);
|
||||||
|
}
|
||||||
|
|
||||||
function readableSeconds(t) {
|
function readableSeconds(t) {
|
||||||
var seconds = Math.round(t);
|
var seconds = Math.round(t);
|
||||||
var minutes = Math.floor(seconds/60);
|
var minutes = Math.floor(seconds/60);
|
||||||
|
@ -187,7 +229,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
], function(err, total){
|
], function(err, total){
|
||||||
cback(balanceRound(total).toFixed(8));
|
cback(coinsRound(total).toFixed(8));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -202,7 +244,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
async.each(_this.stats.pools, function(pool, pcb) {
|
async.each(_this.stats.pools, function(pool, pcb) {
|
||||||
pindex++;
|
pindex++;
|
||||||
var coin = String(_this.stats.pools[pool.name].name);
|
var coin = String(_this.stats.pools[pool.name].name);
|
||||||
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", function(error, result) {
|
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) {
|
||||||
if (error) {
|
if (error) {
|
||||||
pcb(error);
|
pcb(error);
|
||||||
return;
|
return;
|
||||||
|
@ -243,53 +285,68 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
|
|
||||||
var totalHeld = parseFloat(0);
|
var totalHeld = parseFloat(0);
|
||||||
var totalPaid = parseFloat(0);
|
var totalPaid = parseFloat(0);
|
||||||
|
var totalImmature = parseFloat(0);
|
||||||
|
|
||||||
async.each(_this.stats.pools, function(pool, pcb) {
|
async.each(_this.stats.pools, function(pool, pcb) {
|
||||||
var coin = String(_this.stats.pools[pool.name].name);
|
var coin = String(_this.stats.pools[pool.name].name);
|
||||||
// get all balances from address
|
// get all immature balances from address
|
||||||
client.hscan(coin + ':balances', 0, "match", a+"*", "count", 1000, function(error, bals) {
|
client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) {
|
||||||
// get all payouts from address
|
// get all balances from address
|
||||||
client.hscan(coin + ':payouts', 0, "match", a+"*", "count", 1000, function(error, pays) {
|
client.hscan(coin + ':balances', 0, "match", a+"*", "count", 10000, function(error, bals) {
|
||||||
|
// get all payouts from address
|
||||||
|
client.hscan(coin + ':payouts', 0, "match", a+"*", "count", 10000, function(error, pays) {
|
||||||
|
|
||||||
var workerName = "";
|
var workerName = "";
|
||||||
var balName = "";
|
var balAmount = 0;
|
||||||
var balAmount = 0;
|
var paidAmount = 0;
|
||||||
var paidAmount = 0;
|
var pendingAmount = 0;
|
||||||
|
|
||||||
var workers = {};
|
var workers = {};
|
||||||
|
|
||||||
for (var i in pays[1]) {
|
for (var i in pays[1]) {
|
||||||
if (Math.abs(i % 2) != 1) {
|
if (Math.abs(i % 2) != 1) {
|
||||||
workerName = String(pays[1][i]);
|
workerName = String(pays[1][i]);
|
||||||
workers[workerName] = (workers[workerName] || {});
|
workers[workerName] = (workers[workerName] || {});
|
||||||
} else {
|
} else {
|
||||||
paidAmount = parseFloat(pays[1][i]);
|
paidAmount = parseFloat(pays[1][i]);
|
||||||
workers[workerName].paid = balanceRound(paidAmount);
|
workers[workerName].paid = coinsRound(paidAmount);
|
||||||
totalPaid += paidAmount;
|
totalPaid += paidAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var b in bals[1]) {
|
for (var b in bals[1]) {
|
||||||
if (Math.abs(b % 2) != 1) {
|
if (Math.abs(b % 2) != 1) {
|
||||||
balName = String(bals[1][b]);
|
workerName = String(bals[1][b]);
|
||||||
workers[balName] = (workers[balName] || {});
|
workers[workerName] = (workers[workerName] || {});
|
||||||
} else {
|
} else {
|
||||||
balAmount = parseFloat(bals[1][b]);
|
balAmount = parseFloat(bals[1][b]);
|
||||||
workers[balName].balance = balanceRound(balAmount);
|
workers[workerName].balance = coinsRound(balAmount);
|
||||||
totalHeld += balAmount;
|
totalHeld += balAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var b in pends[1]) {
|
||||||
|
if (Math.abs(b % 2) != 1) {
|
||||||
|
workerName = String(pends[1][b]);
|
||||||
|
workers[workerName] = (workers[workerName] || {});
|
||||||
|
} else {
|
||||||
|
pendingAmount = parseFloat(pends[1][b]);
|
||||||
|
workers[workerName].immature = coinsRound(pendingAmount);
|
||||||
|
totalImmature += pendingAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (var w in workers) {
|
for (var w in workers) {
|
||||||
balances.push({
|
balances.push({
|
||||||
worker:String(w),
|
worker:String(w),
|
||||||
balance:workers[w].balance,
|
balance:workers[w].balance,
|
||||||
paid:workers[w].paid
|
paid:workers[w].paid,
|
||||||
});
|
immature:workers[w].immature
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pcb();
|
pcb();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback("There was an error getting balances");
|
callback("There was an error getting balances");
|
||||||
|
@ -299,7 +356,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
_this.stats.balances = balances;
|
_this.stats.balances = balances;
|
||||||
_this.stats.address = address;
|
_this.stats.address = address;
|
||||||
|
|
||||||
cback({totalHeld:balanceRound(totalHeld), totalPaid:balanceRound(totalPaid), balances});
|
cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -382,9 +439,9 @@ module.exports = function(logger, portalConfig, poolConfigs){
|
||||||
blocks: replies[i + 6].sort(sortBlocks),
|
blocks: replies[i + 6].sort(sortBlocks),
|
||||||
confirms: (replies[i + 9] || {})
|
confirms: (replies[i + 9] || {})
|
||||||
},
|
},
|
||||||
/* show last 5 found blocks */
|
/* show last 50 found blocks */
|
||||||
confirmed: {
|
confirmed: {
|
||||||
blocks: replies[i + 7].sort(sortBlocks).slice(0,5)
|
blocks: replies[i + 7].sort(sortBlocks).slice(0,50)
|
||||||
},
|
},
|
||||||
payments: [],
|
payments: [],
|
||||||
currentRoundShares: (replies[i + 8] || {}),
|
currentRoundShares: (replies[i + 8] || {}),
|
||||||
|
|
|
@ -134,7 +134,9 @@ module.exports = function(logger){
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function(callback){
|
function(callback){
|
||||||
var client = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
|
var client = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
|
||||||
client.auth(portalConfig.redis.password);
|
if (portalConfig.redis.password) {
|
||||||
|
client.auth(portalConfig.redis.password);
|
||||||
|
}
|
||||||
client.hgetall('coinVersionBytes', function(err, coinBytes){
|
client.hgetall('coinVersionBytes', function(err, coinBytes){
|
||||||
if (err){
|
if (err){
|
||||||
client.quit();
|
client.quit();
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/api/stats">/stats</a> global pool stats</li>
|
<li><a href="/api/stats">/stats</a> global pool stats</li>
|
||||||
|
<li><a href="/api/blocks">/stats</a> global block stats</li>
|
||||||
<li><a href="/api/pool_stats">/pool_stats</a> - historical stats</li>
|
<li><a href="/api/pool_stats">/pool_stats</a> - historical stats</li>
|
||||||
<li><a href="/api/payments">/payments</a> - payment history</li>
|
<li><a href="/api/payments">/payments</a> - payment history</li>
|
||||||
<li><a href="/api/worker_stats?taddr">/worker_stats?taddr</a> - historical time per pool json </li>
|
<li><a href="/api/worker_stats?taddr">/worker_stats?taddr</a> - historical time per pool json </li>
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
<div class="chartHolder"><svg id="workerHashrate" /></div>
|
<div class="chartHolder"><svg id="workerHashrate" /></div>
|
||||||
<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: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> Immature: <span id="statsTotalImmature">...</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> </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> </div>
|
||||||
<div style="padding-top: 9px; padding-left: 18px;"><i class="fa fa-money"></i> Paid: <span id="statsTotalPaid">...</span> </div>
|
<div style="padding-top: 9px; padding-left: 18px;"><i class="fa fa-money"></i> Paid: <span id="statsTotalPaid">...</span> </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@ var workerHistoryMax = 160;
|
||||||
|
|
||||||
var statData;
|
var statData;
|
||||||
var totalHash;
|
var totalHash;
|
||||||
|
var totalImmature;
|
||||||
var totalBal;
|
var totalBal;
|
||||||
var totalPaid;
|
var totalPaid;
|
||||||
var totalShares;
|
var totalShares;
|
||||||
|
@ -143,6 +144,7 @@ function updateStats() {
|
||||||
totalHash = statData.totalHash;
|
totalHash = statData.totalHash;
|
||||||
totalPaid = statData.paid;
|
totalPaid = statData.paid;
|
||||||
totalBal = statData.balance;
|
totalBal = statData.balance;
|
||||||
|
totalImmature = statData.immature;
|
||||||
totalShares = statData.totalShares;
|
totalShares = statData.totalShares;
|
||||||
// do some calculations
|
// do some calculations
|
||||||
var _blocktime = 250;
|
var _blocktime = 250;
|
||||||
|
@ -153,6 +155,7 @@ function updateStats() {
|
||||||
$("#statsHashrate").text(getReadableHashRateString(totalHash));
|
$("#statsHashrate").text(getReadableHashRateString(totalHash));
|
||||||
$("#statsHashrateAvg").text(getReadableHashRateString(calculateAverageHashrate(null)));
|
$("#statsHashrateAvg").text(getReadableHashRateString(calculateAverageHashrate(null)));
|
||||||
$("#statsLuckDays").text(luckDays);
|
$("#statsLuckDays").text(luckDays);
|
||||||
|
$("#statsTotalImmature").text(totalImmature);
|
||||||
$("#statsTotalBal").text(totalBal);
|
$("#statsTotalBal").text(totalBal);
|
||||||
$("#statsTotalPaid").text(totalPaid);
|
$("#statsTotalPaid").text(totalPaid);
|
||||||
$("#statsTotalShares").text(totalShares.toFixed(2));
|
$("#statsTotalShares").text(totalShares.toFixed(2));
|
||||||
|
|
Loading…
Reference in New Issue