z-nomp/libs/paymentProcessor.js

503 lines
21 KiB
JavaScript
Raw Normal View History

2014-03-09 19:31:58 -07:00
var redis = require('redis');
2014-03-11 18:56:19 -07:00
var async = require('async');
2014-03-09 19:31:58 -07:00
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
2014-03-09 19:31:58 -07:00
2014-03-11 18:56:19 -07:00
2014-03-09 19:31:58 -07:00
module.exports = function(logger){
var poolConfigs = JSON.parse(process.env.pools);
var enabledPools = [];
2014-03-09 19:31:58 -07:00
Object.keys(poolConfigs).forEach(function(coin) {
var poolOptions = poolConfigs[coin];
if (poolOptions.paymentProcessing &&
poolOptions.paymentProcessing.enabled)
enabledPools.push(coin);
2014-03-09 19:31:58 -07:00
});
async.filter(enabledPools, function(coin, callback){
SetupForPool(logger, poolConfigs[coin], function(setupResults){
callback(setupResults);
});
}, function(coins){
coins.forEach(function(coin){
2014-03-09 19:31:58 -07:00
var poolOptions = poolConfigs[coin];
var processingConfig = poolOptions.paymentProcessing;
var logSystem = 'Payments';
var logComponent = coin;
2014-03-09 19:31:58 -07:00
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.host + ':' + poolOptions.redis.port + ')');
2014-03-09 19:31:58 -07:00
});
});
};
function SetupForPool(logger, poolOptions, setupFinished){
2014-03-09 19:31:58 -07:00
var coin = poolOptions.coin.name;
var processingConfig = poolOptions.paymentProcessing;
2014-03-09 19:31:58 -07:00
var logSystem = 'Payments';
var logComponent = coin;
var daemon = new Stratum.daemon.interface([processingConfig.daemon]);
var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host);
2014-03-09 19:31:58 -07:00
var magnitude;
var minPaymentSatoshis;
var coinPrecision;
2014-03-09 19:31:58 -07:00
var paymentInterval;
async.parallel([
function(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) {
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(callback){
daemon.cmd('getbalance', [], function(result){
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;
callback();
}
catch(e){
logger.error(logSystem, logComponent, 'Error detecting number of satoshis in a coin, cannot do payment processing');
callback(true);
}
}, true, true);
}
], function(err){
if (err){
setupFinished(false);
return;
}
paymentInterval = setInterval(function(){
try {
processPayments();
} catch(e){
throw e;
2014-03-09 19:31:58 -07:00
}
}, processingConfig.paymentInterval * 1000);
setTimeout(processPayments, 100);
setupFinished(true);
});
2014-03-09 19:31:58 -07:00
var satoshisToCoins = function(satoshis){
return parseFloat((satoshis / magnitude).toFixed(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. */
2014-03-11 18:56:19 -07:00
var processPayments = function(){
var startPaymentProcess = Date.now();
var timeSpentRPC = 0;
var timeSpentRedis = 0;
2014-03-11 18:56:19 -07:00
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. */
2014-03-11 18:56:19 -07:00
function(callback){
startRedisTimer();
redisClient.multi([
['hgetall', coin + '_balances'],
['smembers', coin + '_blocksPending']
]).exec(function(error, results){
endRedisTimer();
2014-03-11 18:56:19 -07:00
if (error){
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
callback(true);
2014-03-11 18:56:19 -07:00
return;
}
var workers = {};
for (var w in results[0]){
workers[w] = {balance: parseInt(results[0][w])};
2014-03-11 18:56:19 -07:00
}
var rounds = results[1].map(function(r){
var details = r.split(':');
2014-03-22 19:08:33 -07:00
return {
blockHash: details[0],
txHash: details[1],
height: details[2],
2014-03-22 19:08:33 -07:00
serialized: r
};
2014-03-11 18:56:19 -07:00
});
callback(null, workers, rounds);
2014-03-11 18:56:19 -07:00
});
},
/* 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){
2014-03-11 18:56:19 -07:00
var batchRPCcommand = rounds.map(function(r){
return ['gettransaction', [r.txHash]];
});
2014-03-11 18:56:19 -07:00
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
startRPCTimer();
2014-03-11 18:56:19 -07:00
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
endRPCTimer();
2014-03-11 18:56:19 -07:00
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){
addressAccount = tx.result;
return;
}
var round = rounds[i];
if (tx.error && tx.error.code === -5){
logger.error(logSystem, logComponent, 'Daemon reports invalid transaction ' + round.txHash + ' '
+ JSON.stringify(tx.error));
return;
}
else if (tx.error || !tx.result){
logger.error(logSystem, logComponent, 'Odd error with gettransaction ' + round.txHash + ' '
+ JSON.stringify(tx));
return;
}
else if (round.blockHash !== tx.result.blockhash){
logger.error(logSystem, logComponent, 'Daemon reports blockhash ' + tx.result.blockhash
+ ' for tx ' + round.txHash + ' is not the one we have stored: ' + round.blockHash);
return;
}
else if (!(tx.result.details instanceof Array)){
logger.error(logSystem, logComponent, 'Details array missing from transaction '
+ round.txHash);
return;
}
var generationTx = tx.result.details.filter(function(tx){
return tx.address === poolOptions.address;
})[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;
}
});
2014-03-22 19:08:33 -07:00
//Filter out all rounds that are immature (not confirmed or orphaned yet)
2014-03-22 19:08:33 -07:00
rounds = rounds.filter(function(r){
switch (r.category) {
case 'generate':
return true;
case 'orphan':
return true;
default:
return false;
}
2014-03-11 18:56:19 -07:00
});
2014-03-22 19:08:33 -07:00
callback(null, workers, rounds, addressAccount);
2014-03-11 18:56:19 -07:00
});
},
/* Does a batch redis call to get shares contributed to each round. Then calculates the reward
amount owned to each miner for each round. */
function(workers, rounds, addressAccount, callback){
2014-03-11 18:56:19 -07:00
var shareLookups = rounds.map(function(r){
return ['hgetall', coin + '_shares:round' + r.height]
});
startRedisTimer();
redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
endRedisTimer();
2014-03-11 18:56:19 -07:00
if (error){
callback('Check finished - redis error with multi get rounds share');
2014-03-11 18:56:19 -07:00
return;
}
2014-03-22 19:08:33 -07:00
rounds.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;
}
2014-03-20 15:25:59 -07:00
switch (round.category){
case 'orphan':
/* Each block that gets orphaned, all the shares go into the current round so that
miners still get a reward for their work. This seems unfair to those that just
started mining during this current round, but over time it balances out and rewards
loyal miners. */
round.workerShares = workerShares;
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 percent = parseFloat(workerShares[workerAddress]) / totalShares;
var workerRewardTotal = Math.floor(reward * percent);
var worker = workers[workerAddress] = (workers[workerAddress] || {});
worker.reward = (worker.reward || 0) + workerRewardTotal;
}
break;
2014-03-22 19:08:33 -07:00
}
});
callback(null, workers, rounds, addressAccount);
});
},
2014-03-11 21:01:33 -07:00
/* 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) {
2014-03-11 21:01:33 -07:00
var trySend = function (withholdPercent) {
var addressAmounts = {};
var totalSent = 0;
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) {
totalSent += toSend;
var address = worker.address = (worker.address || getProperAddress(w));
worker.sent = addressAmounts[address] = satoshisToCoins(toSend);
worker.balanceChange = Math.min(worker.balance, toSend) * -1;
}
else {
worker.balanceChange = Math.max(toSend - worker.balance, 0);
worker.sent = 0;
}
2014-03-20 15:25:59 -07:00
}
if (Object.keys(addressAmounts).length === 0){
callback(null, workers, rounds);
return;
2014-03-20 15:25:59 -07:00
}
2014-03-22 19:08:33 -07:00
daemon.cmd('sendmany', [addressAccount || '', addressAmounts], function (result) {
if (result.error && result.error.code === -6) {
var higherPercent = withholdPercent + 0.01;
console.log('asdfasdfsadfasdf');
logger.warning(logSystem, logComponent, 'Not enough funds to send out payments, decreasing rewards by '
+ (higherPercent * 100) + '% and retrying');
trySend(higherPercent);
}
else if (result.error) {
logger.error(logSystem, logComponent, 'Error trying to send payments wtih RCP sendmany '
+ JSON.stringify(result.error));
callback(true);
}
else {
logger.debug(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');
2014-03-22 19:08:33 -07:00
}
callback(null, workers, rounds);
2014-03-20 15:25:59 -07:00
}
}, true, true);
};
trySend(0);
2014-03-22 19:08:33 -07:00
},
function(workers, rounds, callback){
var totalPaid = 0;
var balanceUpdateCommands = [];
var workerPayoutsCommand = [];
for (var w in workers) {
var worker = workers[w];
if (worker.balanceChange !== 0){
balanceUpdateCommands.push([
'hincrby',
coin + '_balances',
w,
worker.balanceChange
]);
2014-03-20 15:25:59 -07:00
}
if (worker.sent !== 0){
workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', w, worker.sent]);
totalPaid += worker.sent;
2014-03-20 15:25:59 -07:00
}
}
2014-03-20 15:25:59 -07:00
var movePendingCommands = [];
var roundsToDelete = [];
var orphanMergeCommands = [];
2014-03-20 15:25:59 -07:00
rounds.forEach(function(r){
switch(r.category){
case 'orphan':
movePendingCommands.push(['smove', coin + '_blocksPending', coin + '_blocksOrphaned', r.serialized]);
var workerShares = r.workerShares;
Object.keys(workerShares).forEach(function(worker){
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
worker, workerShares[worker]]);
});
break;
case 'generate':
movePendingCommands.push(['smove', coin + '_blocksPending', coin + '_blocksConfirmed', r.serialized]);
roundsToDelete.push(coin + '_shares:round' + r.height);
break;
}
});
2014-03-20 15:25:59 -07:00
var finalRedisCommands = [];
2014-03-22 19:08:33 -07:00
if (movePendingCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(movePendingCommands);
2014-03-22 19:08:33 -07:00
if (orphanMergeCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
2014-03-20 15:25:59 -07:00
if (balanceUpdateCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);
2014-03-22 19:08:33 -07:00
if (workerPayoutsCommand.length > 0)
finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand);
if (roundsToDelete.length > 0)
finalRedisCommands.push(['del'].concat(roundsToDelete));
2014-03-22 19:08:33 -07:00
if (totalPaid !== 0)
finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', totalPaid]);
2014-03-22 19:08:33 -07:00
if (finalRedisCommands.length === 0){
callback();
return;
}
2014-03-09 19:31:58 -07:00
startRedisTimer();
redisClient.multi(finalRedisCommands).exec(function(error, results){
endRedisTimer();
if (error){
clearInterval(paymentInterval);
logger.error(logSystem, logComponent,
'Payments sent but could not update redis. ' + JSON.stringify(error)
+ ' Disabling payment processing to prevent possible double-payouts. The redis commands in '
+ coin + '_finalRedisCommands.txt must be ran manually');
fs.writeFile(coin + '_finalRedisCommands.txt', JSON.stringify(finalRedisCommands), function(err){
logger.error('Could not write finalRedisCommands.txt, you are fucked.');
});
}
callback();
});
2014-03-11 18:56:19 -07:00
}
], function(){
var paymentProcessTime = Date.now() - startPaymentProcess;
logger.debug(logSystem, logComponent, 'Finished interval - time spent: '
+ paymentProcessTime + 'ms total, ' + timeSpentRedis + 'ms redis, '
+ timeSpentRPC + 'ms daemon RPC');
2014-03-09 19:31:58 -07:00
});
2014-03-11 18:56:19 -07:00
};
var getProperAddress = function(address){
if (address.length === 40){
return util.addressFromEx(poolOptions.address, address);
}
else return address;
};
}