cryptocurrency-pool-server
Version:
An extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency pool server
1,058 lines (924 loc) • 65.1 kB
JavaScript
var fs = require('fs');
var redis = require('redis');
var async = require('async');
var Stratum = require('cryptocurrency-stratum-pool');
var util = require('cryptocurrency-stratum-pool/lib/util.js');
var CreateRedisClient = require('./createRedisClient.js');
module.exports = function (logger) {
var poolConfigs = JSON.parse(process.env.pools);
var enabledPools = [];
Object.keys(poolConfigs).forEach(function (coin) {
var poolOptions = poolConfigs[coin];
if (poolOptions.paymentProcessing &&
poolOptions.paymentProcessing.enabled)
enabledPools.push(coin);
});
async.filter(enabledPools, function (coin, callback) {
SetupForPool(logger, poolConfigs[coin], function (setupResults) {
callback(null, setupResults);
});
}, function (err, coins) {
if (err) {
console.log("Error processing enabled pools in the config"); // TODO: ASYNC LIB was updated, need to report a better error
} else {
coins.forEach(function (coin) {
var poolOptions = poolConfigs[coin];
var processingConfig = poolOptions.paymentProcessing;
var logSystem = 'Payments';
var logComponent = coin;
logger.debug(logSystem, logComponent, 'Payment processing setup to run every '
+ processingConfig.paymentInterval + ' second(s) with daemon ('
+ processingConfig.daemon.user + '@' + processingConfig.daemon.host + ':' + processingConfig.daemon.port
+ ') and redis (' + ((poolOptions.redis.socket !== undefined && poolOptions.redis.socket !== '')
? poolOptions.redis.socket
: (poolOptions.redis.host + ':' + poolOptions.redis.port)) + ')');
});
}
});
};
function SetupForPool(logger, poolOptions, setupFinished) {
var coin = poolOptions.coin.name;
var processingConfig = poolOptions.paymentProcessing;
var logSystem = 'Payments';
var logComponent = coin;
var opidCount = 0;
var opids = [];
// 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 minConfPayout = Math.max((processingConfig.minConf || 10), 1);
if (minConfPayout < 3) {
logger.warning(logSystem, logComponent, logComponent + ' minConf of 3 is recommended.');
}
// minimum paymentInterval of 60 seconds
var paymentIntervalSecs = Math.max((processingConfig.paymentInterval || 120), 30);
if (parseInt(processingConfig.paymentInterval) < 120) {
logger.warning(logSystem, logComponent, ' minimum paymentInterval of 120 seconds recommended.');
}
var requireShielding = poolOptions.coin.requireShielding === true;
var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004);
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function (severity, message) {
logger[severity](logSystem, logComponent, message);
});
var redisClient = CreateRedisClient(poolOptions.redis);
if (poolOptions.redis.password) {
redisClient.auth(poolOptions.redis.password);
}
var magnitude;
var minPaymentSatoshis;
var coinPrecision;
var paymentInterval;
function validateAddress (callback){
daemon.cmd('validateaddress', [poolOptions.address], function (result) {
if (result.error) {
logger.error(logSystem, logComponent, 'Error with payment processing daemon ' + JSON.stringify(result.error));
callback(true);
}
else if (!result.response || result.response.ismine === false) {
logger.error(logSystem, logComponent,
'Daemon does not own pool address - payment processing can not be done with this daemon, '
+ JSON.stringify(result.response));
callback(true);
}
else {
callback()
}
}, true);
}
function validateTAddress (callback) {
daemon.cmd('validateaddress', [poolOptions.tAddress], function(result) {
if (result.error){
logger.error(logSystem, logComponent, 'Error with payment processing daemon (validateTAddress) ' + JSON.stringify(result.error));
callback(true);
}
else if (!result.response || !result.response.ismine) {
logger.error(logSystem, logComponent,
'Daemon does not own pool address - payment processing can not be done with this daemon, '
+ JSON.stringify(result.response));
callback(true);
}
else{
callback()
}
}, true);
}
function validateZAddress (callback) {
daemon.cmd('z_validateaddress', [poolOptions.zAddress], function(result) {
if (result.error){
logger.error(logSystem, logComponent, 'Error with payment processing daemon (validateZAddress) ' + JSON.stringify(result.error));
callback(true);
}
else if (!result.response || !result.response.ismine) {
logger.error(logSystem, logComponent,
'Daemon does not own pool address - payment processing can not be done with this daemon, '
+ JSON.stringify(result.response));
callback(true);
}
else{
callback()
}
}, true);
}
function getBalance(callback){
daemon.cmd('getbalance', [], function (result) {
var wasICaught = false;
if (result.error) {
callback(true);
return;
}
try {
var d = result.data.split('result":')[1].split(',')[0].split('.')[1];
magnitude = parseInt('10' + new Array(d.length).join('0'));
minPaymentSatoshis = parseInt(processingConfig.minimumPayment * magnitude);
coinPrecision = magnitude.toString().length - 1;
}
catch (e) {
logger.error(logSystem, logComponent, 'Error detecting number of satoshis in a coin, cannot do payment processing. Tried parsing: ' + result.data);
wasICaught = true;
}
finally {
if (wasICaught) {
callback(true);
} else {
callback();
}
}
}, true, true);
}
function asyncComplete(err){
if (err){
setupFinished(false);
return;
}
if (paymentInterval) {
clearInterval(paymentInterval);
}
paymentInterval = setInterval(function () {
try {
processPayments();
} catch (e) {
throw e;
}
}, paymentIntervalSecs * 1000);
setupFinished(true);
}
if (!poolOptions.coin.isZCashProtocol) {
async.parallel([validateAddress, getBalance], asyncComplete);
} else if (requireShielding === true) {
async.parallel([validateAddress, validateTAddress, validateZAddress, getBalance], asyncComplete);
} else {
async.parallel([validateAddress, validateTAddress, getBalance], asyncComplete);
}
//get t_address coinbalance
function listUnspent (addr, notAddr, minConf, displayBool, callback) {
if (addr !== null) {
var args = [minConf, 99999999, [addr]];
} else {
addr = 'Payout wallet';
var args = [minConf, 99999999];
}
daemon.cmd('listunspent', args, function (result) {
if (!result || result.error || result[0].error) {
logger.error(logSystem, logComponent, 'Error with RPC call listunspent '+addr+' '+JSON.stringify(result[0].error));
callback = function (){};
callback(true);
}
else {
var tBalance = parseFloat(0);
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 && result[0].response[i].address !== notAddr) {
tBalance += parseFloat(result[0].response[i].amount || 0);
}
}
tBalance = coinsRound(tBalance);
}
if (displayBool === true) {
logger.special(logSystem, logComponent, addr+' balance of ' + tBalance);
}
callback(null, coinsToSatoshies(tBalance));
}
});
}
// get z_address coinbalance
function listUnspentZ (addr, minConf, displayBool, callback) {
daemon.cmd('z_getbalance', [addr, minConf], function (result) {
if (!result || result.error || result[0].error) {
logger.error(logSystem, logComponent, 'Error with RPC call z_getbalance '+addr+' '+JSON.stringify(result[0].error));
callback = function (){};
callback(true);
}
else {
var zBalance = parseFloat(0);
if (result[0].response != null) {
zBalance = coinsRound(result[0].response);
}
if (displayBool === true) {
logger.special(logSystem, logComponent, addr.substring(0,14) + '...' + addr.substring(addr.length - 14) + ' balance: '+(zBalance).toFixed(8));
}
callback(null, coinsToSatoshies(zBalance));
}
});
}
//send t_address balance to z_address
function sendTToZ (callback, tBalance) {
if (callback === true)
return;
if (tBalance === NaN) {
logger.error(logSystem, logComponent, 'tBalance === NaN for sendTToZ');
return;
}
if ((tBalance - 10000) <= 0)
return;
// 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 = satoshisToCoins(tBalance - 10000);
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 || result.error || result[0].error || !result[0].response) {
logger.error(logSystem, logComponent, 'Error trying to shield balance '+amount+' '+JSON.stringify(result[0].error));
callback = function (){};
callback(true);
}
else {
var opid = (result.response || result[0].response);
opidCount++;
opids.push(opid);
logger.special(logSystem, logComponent, 'Shield balance ' + amount + ' ' + opid);
callback = function (){};
callback(null);
}
}
);
}
// send z_address balance to t_address
function sendZToT (callback, zBalance) {
if (callback === true)
return;
if (zBalance === NaN) {
logger.error(logSystem, logComponent, 'zBalance === NaN for sendZToT');
return;
}
if ((zBalance - 10000) <= 0)
return;
// 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 = satoshisToCoins(zBalance - 10000);
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 || result.error || result[0].error || !result[0].response) {
logger.error(logSystem, logComponent, 'Error trying to send z_address coin balance to payout t_address.'+JSON.stringify(result[0].error));
callback = function (){};
callback(true);
}
else {
var opid = (result.response || result[0].response);
opidCount++;
opids.push(opid);
logger.special(logSystem, logComponent, 'Unshield funds for payout ' + amount + ' ' + opid);
callback = function (){};
callback(null);
}
}
);
}
function cacheNetworkStats () {
var params = null;
var batchRpcCalls = [
['getdifficulty', []]
]
if (poolOptions.coin.noNetworkInfo) {
if (!poolOptions.coin.getInfo) {
batchRpcCalls.push(['getinfo', []]);
}
} else {
batchRpcCalls.push(['getnetworkinfo', []]);
}
batchRpcCalls.push(
[poolOptions.coin.getInfo ? 'getinfo' : 'getmininginfo', []],
[poolOptions.coin.getAllNetworkHashPS ? 'getallnetworkhashps' : poolOptions.coin.getNetworkGHPS ? 'getnetworkghps' : 'getnetworkhashps', []]
);
if (poolOptions.coin.getInfo) {
batchRpcCalls.push(['getinfo', []], ['getallnetworkhashps', []]);
} else {
batchRpcCalls.push(['getmininginfo', []]);
}
daemon.batchCmd(batchRpcCalls,
function (error, results) {
if (error || !results){
logger.error(logSystem, logComponent, 'Error with cacheNetworkStats batch RPC call: ' + JSON.stringify(error));
return;
}
var rpcResults = {};
for (var i = 0; i < results.length; i++){
var rpcCall = batchRpcCalls[i][0];
var r = results[i];
rpcResults[rpcCall] = r.result || r.error;
if (r.error || !r.result){
logger.error(logSystem, logComponent, 'Error with cacheNetworkResults RPC ' + rpcCall + ' - ' + JSON.stringify(r.error));
return;
}
}
if (poolOptions.coin.noNetworkInfo) {
rpcResults.getnetworkinfo = rpcResults.getinfo
}
var coin = logComponent;
var multiAlgoKey = poolOptions.coin.passAlgorithmKey ? (poolOptions.coin.passAlgorithmKey !== true ? poolOptions.coin.passAlgorithmKey : poolOptions.coin.algorithm) : (poolOptions.coin.passAlgorithm && poolOptions.coin.passAlgorithm !== true ? poolOptions.coin.passAlgorithm : poolOptions.coin.algorithm);
var multiAlgoDifficultyKey = 'difficulty_' + multiAlgoKey;
var finalRedisCommands = [];
var blocks = rpcResults[poolOptions.coin.getInfo ? 'getinfo' : 'getmininginfo'].blocks
if (blocks != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkBlocks', blocks]);
}
var difficulty = poolOptions.coin.getInfo ? (typeof rpcResults.getinfo[multiAlgoDifficultyKey] !== 'undefined' ? rpcResults.getinfo[multiAlgoDifficultyKey] : rpcResults.getinfo.difficulty) : (typeof rpcResults.getmininginfo[multiAlgoDifficultyKey] !== 'undefined' ? rpcResults.getmininginfo[multiAlgoDifficultyKey] : rpcResults.getmininginfo.difficulty);
if (difficulty != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkDiff', difficulty]);
}
var networkhashps = poolOptions.coin.getAllNetworkHashPS ? rpcResults.getallnetworkhashps[multiAlgoKey] : poolOptions.coin.getNetworkGHPS ? (rpcResults.getnetworkghps * Math.pow(1024, 3)) : rpcResults.getnetworkhashps;
if (networkhashps != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkSols', networkhashps]);
}
if (rpcResults.getnetworkinfo.connections != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkConnections', rpcResults.getnetworkinfo.connections]);
}
if (rpcResults.getnetworkinfo.version != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkVersion', rpcResults.getnetworkinfo.version]);
}
if (rpcResults.getnetworkinfo.subversion != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkSubVersion', rpcResults.getnetworkinfo.subversion]);
}
if (rpcResults.getnetworkinfo.protocolversion != null) {
finalRedisCommands.push(['hset', coin + ':stats', 'networkProtocolVersion', rpcResults.getnetworkinfo.protocolversion]);
}
if (finalRedisCommands.length <= 0)
return;
redisClient.multi(finalRedisCommands).exec(function(error, results){
if (error){
logger.error(logSystem, logComponent, 'Error with redis during call to cacheNetworkStats() ' + JSON.stringify(error));
return;
}
});
}
);
}
if (!!poolOptions.coin.isZCashProtocol) {
// run shielding process every x minutes
var shieldIntervalState = 0; // do not send ZtoT and TtoZ and same time, this results in operation failed!
var shielding_interval = Math.max(parseInt(poolOptions.walletInterval || 1), 1) * 60 * 1000; // run every x minutes
// check operation statuses every 57 seconds
var opid_interval = 57 * 1000;
// shielding not required for some equihash coins
if (requireShielding === true) {
var shieldInterval = setInterval(function() {
shieldIntervalState++;
switch (shieldIntervalState) {
case 1:
listUnspent(poolOptions.address, null, minConfShield, false, sendTToZ);
break;
default:
listUnspentZ(poolOptions.zAddress, minConfShield, false, sendZToT);
shieldIntervalState = 0;
break;
}
}, shielding_interval);
var checkOpids = function() {
clearTimeout(opidTimeout);
var checkOpIdSuccessAndGetResult = function(ops) {
var batchRPC = [];
// if there are no op-ids
if (ops.length == 0) {
// and we think there is
if (opidCount !== 0) {
// clear them!
opidCount = 0;
opids = [];
logger.warning(logSystem, logComponent, 'Clearing operation ids due to empty result set.');
}
}
// loop through op-ids checking their status
ops.forEach(function(op, i){
// check operation id status
if (op.status == "success" || op.status == "failed") {
// clear operation id result
var opid_index = opids.indexOf(op.id);
if (opid_index > -1) {
// clear operation id count
batchRPC.push(['z_getoperationresult', [[op.id]]]);
opidCount--;
opids.splice(opid_index, 1);
}
// log status to console
if (op.status == "failed") {
if (op.error) {
logger.error(logSystem, logComponent, "Shielding operation failed " + op.id + " " + op.error.code +", " + op.error.message);
} else {
logger.error(logSystem, logComponent, "Shielding operation failed " + op.id);
}
} else {
logger.special(logSystem, logComponent, 'Shielding operation success ' + op.id + ' txid: ' + op.result.txid);
}
} else if (op.status == "executing") {
logger.special(logSystem, logComponent, 'Shielding operation in progress ' + op.id );
}
});
// if there are no completed operations
if (batchRPC.length <= 0) {
opidTimeout = setTimeout(checkOpids, opid_interval);
return;
}
// clear results for completed operations
daemon.batchCmd(batchRPC, function(error, results){
if (error || !results) {
opidTimeout = setTimeout(checkOpids, opid_interval);
logger.error(logSystem, logComponent, 'Error with RPC call z_getoperationresult ' + JSON.stringify(error));
return;
}
// check result execution_secs vs pool_config
results.forEach(function(result, i) {
if (result.result[i] && parseFloat(result.result[i].execution_secs || 0) > shielding_interval) {
logger.warning(logSystem, logComponent, 'Warning, walletInverval shorter than opid execution time of '+result.result[i].execution_secs+' secs.');
}
});
// keep checking operation ids
opidTimeout = setTimeout(checkOpids, opid_interval);
});
};
// check for completed operation ids
daemon.cmd('z_getoperationstatus', null, function (result) {
var err = false;
if (result.error) {
err = true;
logger.error(logSystem, logComponent, 'Error with RPC call z_getoperationstatus ' + JSON.stringify(result.error));
} else if (result.response) {
checkOpIdSuccessAndGetResult(result.response);
} else {
err = true;
logger.error(logSystem, logComponent, 'No response from z_getoperationstatus RPC call.');
}
if (err === true) {
opidTimeout = setTimeout(checkOpids, opid_interval);
if (opidCount !== 0) {
opidCount = 0;
opids = [];
logger.warning(logSystem, logComponent, 'Clearing operation ids due to RPC call errors.');
}
}
}, true, true);
}
var opidTimeout = setTimeout(checkOpids, opid_interval);
}
}
// network stats caching every 58 seconds
var stats_interval = 58 * 1000;
var statsInterval = setInterval(function() {
// update network stats using coin daemon
cacheNetworkStats();
}, stats_interval);
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);
}
/* 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. */
var processPayments = function () {
var startPaymentProcess = Date.now();
var timeSpentRPC = 0;
var timeSpentRedis = 0;
var startTimeRedis;
var startTimeRPC;
var startRedisTimer = function () { startTimeRedis = Date.now() };
var endRedisTimer = function () { timeSpentRedis += Date.now() - startTimeRedis };
var startRPCTimer = function () { startTimeRPC = Date.now(); };
var endRPCTimer = function () { timeSpentRPC += Date.now() - startTimeRedis };
async.waterfall([
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
blocks. */
function (callback) {
startRedisTimer();
redisClient.multi([
['hgetall', coin + ':balances'],
['smembers', coin + ':blocksPending']
]).exec(function (error, results) {
endRedisTimer();
if (error) {
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
callback(true);
return;
}
var workers = {};
for (var w in results[0]) {
workers[w] = { balance: coinsToSatoshies(parseFloat(results[0][w])) };
}
var rounds = results[1].map(function (r) {
var details = r.split(':');
return {
blockHash: details[0],
txHash: details[1],
height: details[2],
minedby: details[3],
time: details[4],
soloMined: details[5] === 'SOLO',
serialized: r
};
});
rounds.sort(function(a, b) {
return a.height - b.height;
});
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]];
});
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 = '';
txDetails.forEach(function (tx, i) {
if (i === txDetails.length - 1) {
if (tx.result && tx.result.toString().length > 0) {
addressAccount = tx.result.toString();
}
return;
}
var round = rounds[i];
if (tx && tx.result) {
round.confirmations = parseInt((tx.result.confirmations || 0));
}
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.category === 'immature') {
round.reward = coinsRound(parseFloat(generationTx.amount || generationTx.value));
}
});
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;
};
rounds = rounds.filter(function (r) {
switch (r.category) {
case 'orphan':
case 'kicked':
r.canDeleteShares = canDeleteShares(r);
case 'immature':
case 'generate':
return true;
default:
return false;
}
});
callback(null, workers, rounds, addressAccount);
});
},
/* Does a batch redis call to get shares contributed to each round. Then calculates the reward
amount owned to each miner for each round. */
!poolOptions.coin.isZCashProtocol ? function (workers, rounds, addressAccount, callback) {
var soloRounds = rounds.filter(function(r) {
return r.soloMined;
});
soloRounds.forEach(function(round, i) {
var workerAddress = round.minedby;
switch (round.category) {
case 'kicked':
case 'orphan':
round.workerShares = 0;
break;
case 'immature':
/* We found an immature block. */
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var workerImmatureTotal = parseInt(round.reward * magnitude);
worker.immature = (worker.immature || 0) + workerImmatureTotal;
break;
case 'generate':
/* We found a confirmed block! Now get the reward for it and calculate how much
we owe the miner. */
var worker = workers[workerAddress] = (workers[workerAddress] || {});
worker.totalShares = parseFloat(worker.totalShares || 0);
var workerRewardTotal = parseInt(round.reward * magnitude);
worker.reward = (worker.reward || 0) + workerRewardTotal;
break;
}
});
var shareRounds = rounds.filter(function(r) {
return !r.soloMined;
});
var shareLookups = shareRounds.map(function (r) {
return ['hgetall', coin + ':shares:round' + r.height]
});
startRedisTimer();
redisClient.multi(shareLookups).exec(function (error, allWorkerShares) {
endRedisTimer();
if (error) {
callback('Check finished - redis error with multi get rounds share');
return;
}
shareRounds.forEach(function (round, i) {
var workerShares = allWorkerShares[i];
if (!workerShares) {
logger.error(logSystem, logComponent, 'No worker shares for round: '
+ round.height + ' blockHash: ' + round.blockHash);
return;
}
switch (round.category) {
case 'kicked':
case 'orphan':
round.workerShares = workerShares;
break;
case 'immature':
/* We found an immature block. */
var reward = parseInt(round.reward * magnitude);
var totalShares = Object.keys(workerShares).reduce(function (p, c) {
return p + parseFloat(workerShares[c])
}, parseFloat(0));
for (var workerAddress in workerShares) {
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var shares = parseFloat(workerShares[workerAddress] || 0);
var percent = shares / totalShares;
var workerImmatureTotal = Math.floor(reward * percent);
worker.immature = (worker.immature || 0) + workerImmatureTotal;
}
break;
case 'generate':
/* We found a confirmed block! Now get the reward for it and calculate how much
we owe each miner based on the shares they submitted during that block round. */
var reward = parseInt(round.reward * magnitude);
var totalShares = Object.keys(workerShares).reduce(function (p, c) {
return p + parseFloat(workerShares[c])
}, 0);
for (var workerAddress in workerShares) {
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var shares = parseFloat(workerShares[workerAddress] || 0);
worker.totalShares = parseFloat(worker.totalShares || 0) + shares;
var percent = shares / totalShares;
var workerRewardTotal = Math.floor(reward * percent);
worker.reward = (worker.reward || 0) + workerRewardTotal;
}
break;
}
});
callback(null, workers, rounds, addressAccount);
});
} : function(workers, rounds, addressAccount, callback) {
var shareRounds = rounds.filter(function(r) {
return !r.soloMined;
});
var shareLookups = shareRounds.map(function (r) {
return ['hgetall', coin + ':shares:round' + r.height]
});
startRedisTimer();
redisClient.multi(shareLookups).exec(function (error, allWorkerShares) {
endRedisTimer();
if (error) {
callback('Check finished - redis error with multi get rounds share');
return;
}
// error detection
var err = null;
var performPayment = false;
var notAddr = null;
if (requireShielding === true) {
notAddr = poolOptions.address;
}
// 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
var soloRounds = rounds.filter(function(r) {
return r.soloMined;
});
soloRounds.forEach(function(round, i) {
var workerAddress = round.minedby;
switch (round.category) {
case 'kicked':
case 'orphan':
round.workerShares = 0;
break;
case 'immature':
/* We found an immature block. */
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var workerImmatureTotal = Math.round(coinsToSatoshies(round.reward) - coinsToSatoshies(fee));
worker.immature = (worker.immature || 0) + workerImmatureTotal;
break;
case 'generate':
/* We found a confirmed block! Now get the reward for it and calculate how much
we owe the miner. */
var worker = workers[workerAddress] = (workers[workerAddress] || {});
worker.totalShares = parseFloat(worker.totalShares || 0);
var workerRewardTotal = Math.round(coinsToSatoshies(round.reward) - coinsToSatoshies(fee));
worker.reward = (worker.reward || 0) + workerRewardTotal;
break;
}
});
shareRounds.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;
}
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);
// adjust block immature .. tx fees
immature = Math.round(immature - feeSatoshi);
// total up shares for round
for (var workerAddress in workerShares){
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var shares = parseFloat((workerShares[workerAddress] || 0));
worker.roundShares = shares;
totalShares += shares;
}
// 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;
}
break;
/* calculate reward balances */
case 'generate':
var feeSatoshi = coinsToSatoshies(fee);
var reward = coinsToSatoshies(round.reward);
var totalShares = parseFloat(0);
// adjust block reward .. tx fees
reward = Math.round(reward - feeSatoshi);
// total up shares for round
for (var workerAddress in workerShares){
var worker = workers[workerAddress] = (workers[workerAddress] || {});
var shares = parseFloat((workerShares[workerAddress] || 0));
worker.roundShares = shares;
worker.totalShares = parseFloat(worker.totalShares || 0) + shares;
totalShares += shares;
}
// 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;
}
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
},
/* Calculate if any payments are ready to be sent and trigger them sending
Get balance different for each address and pass it along as object of latest balances such as
{worker1: balance1, worker2, balance2}
when deciding the sent balance, it the difference should be -1*amount they had in db,
if not sending the balance, the differnce should be +(the amount they earned this round)
*/
function(workers, rounds, addressAccount, callback) {
var tries = 0;
var trySend = function (withholdPercent) {
var addressAmounts = {};
var balanceAmounts = {};
var shareAmounts = {};
var minerTotals = {};
var totalSent = 0;
var totalShares = 0;
// track attempts made, calls to trySend...
tries++;
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 = Math.round((worker.balance + worker.reward) * (1 - withholdPercent));
var addre