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');
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
Object.keys(poolConfigs).forEach(function(coin) {
|
|
|
|
SetupForPool(logger, poolConfigs[coin]);
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function SetupForPool(logger, poolOptions){
|
|
|
|
|
2014-03-26 14:08:34 -07:00
|
|
|
if (!poolOptions.shareProcessing ||
|
2014-03-26 21:55:58 -07:00
|
|
|
!poolOptions.shareProcessing.internal ||
|
2014-03-26 14:08:34 -07:00
|
|
|
!poolOptions.shareProcessing.internal.enabled)
|
|
|
|
return;
|
|
|
|
|
2014-03-09 19:31:58 -07:00
|
|
|
|
2014-03-26 14:08:34 -07:00
|
|
|
var coin = poolOptions.coin.name;
|
2014-03-09 19:31:58 -07:00
|
|
|
var processingConfig = poolOptions.shareProcessing.internal;
|
|
|
|
|
2014-03-26 14:08:34 -07:00
|
|
|
|
2014-03-09 19:31:58 -07:00
|
|
|
|
2014-03-22 23:16:06 -07:00
|
|
|
var logSystem = 'Payments';
|
|
|
|
var logComponent = coin;
|
|
|
|
|
2014-03-09 19:31:58 -07:00
|
|
|
|
|
|
|
var daemon = new Stratum.daemon.interface([processingConfig.daemon]);
|
|
|
|
daemon.once('online', function(){
|
2014-03-22 23:16:06 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing');
|
2014-03-09 19:31:58 -07:00
|
|
|
|
|
|
|
daemon.cmd('validateaddress', [poolOptions.address], function(result){
|
2014-03-24 13:00:18 -07:00
|
|
|
if (!result[0].response || !result[0].response.ismine){
|
2014-03-26 23:56:36 -07:00
|
|
|
logger.error(logSystem, logComponent,
|
|
|
|
'Daemon does not own pool address - payment processing can not be done with this daemon');
|
2014-03-09 19:31:58 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}).once('connectionFailed', function(error){
|
2014-03-26 23:56:36 -07:00
|
|
|
logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' +
|
|
|
|
JSON.stringify(error));
|
2014-03-06 12:46:01 -08:00
|
|
|
}).on('error', function(error){
|
2014-03-29 18:59:22 -07:00
|
|
|
logger.error(logSystem, logComponent, 'Daemon error ' + JSON.stringify(error));
|
2014-03-09 19:31:58 -07:00
|
|
|
}).init();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var redisClient;
|
|
|
|
|
|
|
|
|
|
|
|
var connectToRedis = function(){
|
|
|
|
var reconnectTimeout;
|
|
|
|
redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host);
|
|
|
|
redisClient.on('ready', function(){
|
|
|
|
clearTimeout(reconnectTimeout);
|
2014-03-22 23:16:06 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
|
2014-03-27 15:29:43 -07:00
|
|
|
})/*).on('error', function(err){
|
2014-03-27 10:52:54 -07:00
|
|
|
logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err))
|
2014-03-27 15:29:43 -07:00
|
|
|
})*/.on('end', function(){
|
2014-03-22 23:16:06 -07:00
|
|
|
logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
|
|
|
|
logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
|
2014-03-09 19:31:58 -07:00
|
|
|
reconnectTimeout = setTimeout(function(){
|
|
|
|
connectToRedis();
|
|
|
|
}, 3000);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
connectToRedis();
|
|
|
|
|
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
/* Number.toFixed gives us the decimal places we want, but as a string. parseFloat turns it back into number
|
|
|
|
we don't care about trailing zeros in this case. */
|
|
|
|
var toPrecision = function(value, precision){
|
|
|
|
return parseFloat(value.toFixed(precision));
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/* 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-12 15:33:29 -07:00
|
|
|
|
2014-03-11 18:56:19 -07:00
|
|
|
var processPayments = function(){
|
2014-03-27 15:29:43 -07:00
|
|
|
|
|
|
|
|
|
|
|
var startPaymentProcess = Date.now();
|
|
|
|
|
2014-03-11 18:56:19 -07:00
|
|
|
async.waterfall([
|
|
|
|
|
2014-03-12 15:33:29 -07:00
|
|
|
/* 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){
|
2014-03-11 21:57:03 -07:00
|
|
|
|
2014-03-20 15:25:59 -07:00
|
|
|
redisClient.smembers(coin + '_blocksPending', function(error, results){
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-11 18:56:19 -07:00
|
|
|
if (error){
|
2014-03-22 23:16:06 -07:00
|
|
|
logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error));
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - redis error for getting blocks');
|
2014-03-11 18:56:19 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (results.length === 0){
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - no pending blocks in redis');
|
2014-03-11 18:56:19 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
var rounds = results.map(function(r){
|
|
|
|
var details = r.split(':');
|
2014-03-22 19:08:33 -07:00
|
|
|
return {
|
|
|
|
category: details[0].category,
|
2014-03-27 15:29:43 -07:00
|
|
|
solution: details[0],
|
|
|
|
txHash: details[1],
|
|
|
|
height: details[2],
|
|
|
|
reward: details[3],
|
2014-03-22 19:08:33 -07:00
|
|
|
serialized: r
|
|
|
|
};
|
2014-03-11 18:56:19 -07:00
|
|
|
});
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-12 15:33:29 -07:00
|
|
|
callback(null, rounds);
|
2014-03-11 18:56:19 -07:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2014-03-27 15:29:43 -07:00
|
|
|
/* First get data from all pending blocks via batch RPC call, we need the coinbase txHash. */
|
|
|
|
/*(function(rounds, callback){
|
|
|
|
var batchRPCcommand = rounds.map(function(r){
|
|
|
|
return ['getblock', [r.solution]];
|
|
|
|
});
|
|
|
|
|
|
|
|
daemon.batchCmd(batchRPCcommand, function(error, blocks) {
|
|
|
|
|
|
|
|
if (error || !blocks) {
|
|
|
|
callback('Check finished - daemon rpc error with batch gettransactions ' +
|
|
|
|
JSON.stringify(error));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
blocks.forEach(function (b, i) {
|
|
|
|
if (b.error || b.result.hash !== rounds[i].solution){
|
|
|
|
logger.error(logSystem, logComponent, "Did daemon drop a block? " + rounds[i].solution);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
rounds[i].txHash = b.result.tx[0];
|
|
|
|
});
|
|
|
|
callback(null, rounds);
|
|
|
|
});
|
|
|
|
},*/
|
2014-03-11 21:57:03 -07:00
|
|
|
|
2014-03-12 15:33:29 -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(rounds, callback){
|
2014-03-11 18:56:19 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
var batchRPCcommand = rounds.map(function(r){
|
|
|
|
return ['gettransaction', [r.txHash]];
|
|
|
|
});
|
2014-03-11 18:56:19 -07:00
|
|
|
|
|
|
|
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
|
|
|
|
|
2014-03-11 20:47:14 -07:00
|
|
|
if (error || !txDetails){
|
2014-03-26 23:56:36 -07:00
|
|
|
callback('Check finished - daemon rpc error with batch gettransactions ' +
|
|
|
|
JSON.stringify(error));
|
2014-03-11 20:47:14 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-27 11:57:56 -07:00
|
|
|
txDetails.forEach(function(tx, i){
|
2014-03-27 15:29:43 -07:00
|
|
|
var round = rounds[i];
|
|
|
|
|
2014-03-27 11:57:56 -07:00
|
|
|
if (tx.error && tx.error.code === -5){
|
|
|
|
|
2014-03-27 15:29:43 -07:00
|
|
|
/* Block was dropped from coin daemon even after it happily accepted it earlier. */
|
|
|
|
|
|
|
|
//If we find another block at the same height then this block was drop-kicked orphaned
|
|
|
|
var dropKicked = !!rounds.filter(function(r){
|
|
|
|
return r.height === round.height && r.solution !== round.solution && r.category !== 'dropkicked';
|
|
|
|
}).length;
|
|
|
|
|
|
|
|
if (dropKicked){
|
|
|
|
logger.warning(logSystem, logComponent,
|
|
|
|
'A block was drop-kicked orphaned'
|
|
|
|
+ ' - we found a better block at the same height, solution '
|
|
|
|
+ round.solution + " round " + round.height);
|
|
|
|
round.category = 'dropkicked';
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
/* We have no other blocks that match this height so convert to orphan in order for
|
|
|
|
shares from the round to be rewarded. */
|
|
|
|
round.category = 'orphan';
|
|
|
|
}
|
2014-03-27 11:57:56 -07:00
|
|
|
}
|
|
|
|
else if (tx.error || !tx.result){
|
2014-03-26 23:56:36 -07:00
|
|
|
logger.error(logSystem, logComponent,
|
2014-03-27 15:50:55 -07:00
|
|
|
'Error with requesting transaction from block daemon: ' + JSON.stringify(tx));
|
2014-03-15 17:58:28 -07:00
|
|
|
}
|
2014-03-27 15:29:43 -07:00
|
|
|
else{
|
|
|
|
round.category = tx.result.details[0].category;
|
|
|
|
if (round.category === 'generate')
|
|
|
|
round.amount = tx.result.amount;
|
|
|
|
}
|
2014-03-18 23:54:18 -07:00
|
|
|
});
|
2014-03-15 17:58:28 -07:00
|
|
|
|
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
var magnitude;
|
|
|
|
|
2014-03-26 23:56:36 -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){
|
2014-03-27 15:29:43 -07:00
|
|
|
switch (r.category) {
|
|
|
|
|
|
|
|
case 'generate':
|
|
|
|
/* Here we calculate the smallest unit in this coin's currency; the 'satoshi'.
|
|
|
|
The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the
|
|
|
|
rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore,
|
|
|
|
we simply divide the two to get the magnitude. I don't know math, there is probably
|
|
|
|
a better term than 'magnitude'. Sue me or do a pull request to fix it. */
|
|
|
|
var roundMagnitude = r.reward / r.amount;
|
|
|
|
|
|
|
|
if (!magnitude) {
|
|
|
|
magnitude = roundMagnitude;
|
|
|
|
|
|
|
|
if (roundMagnitude % 10 !== 0)
|
|
|
|
logger.error(logSystem, logComponent,
|
|
|
|
'Satosihis in coin is not divisible by 10 which is very odd');
|
|
|
|
}
|
|
|
|
else if (magnitude != roundMagnitude) {
|
|
|
|
/* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
|
|
|
|
100,000,000 satoshis in one coin unit. */
|
2014-03-26 23:56:36 -07:00
|
|
|
logger.error(logSystem, logComponent,
|
2014-03-27 15:29:43 -07:00
|
|
|
'Magnitude in a round was different than in another round. HUGE PROBLEM.');
|
|
|
|
}
|
|
|
|
return true;
|
2014-03-15 17:58:28 -07:00
|
|
|
|
2014-03-27 15:29:43 -07:00
|
|
|
case 'dropkicked':
|
|
|
|
case 'orphan':
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
2014-03-11 18:56:19 -07:00
|
|
|
});
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
|
|
|
|
if (rounds.length === 0){
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - no confirmed or orphaned blocks found');
|
2014-03-15 17:58:28 -07:00
|
|
|
}
|
|
|
|
else{
|
2014-03-22 19:08:33 -07:00
|
|
|
callback(null, rounds, magnitude);
|
2014-03-11 20:47:14 -07:00
|
|
|
}
|
2014-03-11 18:56:19 -07:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-12 15:33:29 -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. */
|
2014-03-22 19:08:33 -07:00
|
|
|
function(rounds, magnitude, callback){
|
2014-03-11 18:56:19 -07:00
|
|
|
|
2014-03-12 15:33:29 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
var shareLookups = rounds.map(function(r){
|
|
|
|
return ['hgetall', coin + '_shares:round' + r.height]
|
|
|
|
});
|
2014-03-12 15:33:29 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
|
2014-03-11 18:56:19 -07:00
|
|
|
if (error){
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - redis error with multi get rounds share')
|
2014-03-11 18:56:19 -07:00
|
|
|
return;
|
|
|
|
}
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
var orphanMergeCommands = [];
|
2014-03-12 15:33:29 -07:00
|
|
|
var workerRewards = {};
|
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
rounds.forEach(function(round, i){
|
|
|
|
var workerShares = allWorkerShares[i];
|
2014-03-12 15:33:29 -07:00
|
|
|
|
2014-03-27 15:29:43 -07:00
|
|
|
if (!workerShares){
|
|
|
|
logger.error(logSystem, logComponent, 'No worker shares for round: '
|
|
|
|
+ round.height + ' solution: ' + round.solution);
|
|
|
|
return;
|
2014-03-11 20:47:14 -07:00
|
|
|
}
|
2014-03-20 15:25:59 -07:00
|
|
|
|
2014-03-27 15:29:43 -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. */
|
|
|
|
Object.keys(workerShares).forEach(function(worker){
|
|
|
|
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
|
|
|
|
worker, workerShares[worker]]);
|
|
|
|
});
|
|
|
|
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 = round.reward * (1 - processingConfig.feePercent);
|
|
|
|
|
|
|
|
var totalShares = Object.keys(workerShares).reduce(function(p, c){
|
|
|
|
return p + parseInt(workerShares[c])
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
for (var worker in workerShares){
|
|
|
|
var percent = parseInt(workerShares[worker]) / totalShares;
|
|
|
|
var workerRewardTotal = Math.floor(reward * percent);
|
|
|
|
if (!(worker in workerRewards)) workerRewards[worker] = 0;
|
|
|
|
workerRewards[worker] += workerRewardTotal;
|
|
|
|
}
|
|
|
|
break;
|
2014-03-22 19:08:33 -07:00
|
|
|
}
|
2014-03-27 15:29:43 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
});
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
|
2014-03-11 18:56:19 -07:00
|
|
|
});
|
|
|
|
},
|
2014-03-09 19:31:58 -07:00
|
|
|
|
2014-03-11 21:01:33 -07:00
|
|
|
|
2014-03-12 15:33:29 -07:00
|
|
|
/* Does a batch call to redis to get worker existing balances from coin_balances*/
|
2014-03-22 19:08:33 -07:00
|
|
|
function(rounds, magnitude, workerRewards, orphanMergeCommands, callback){
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
var workers = Object.keys(workerRewards);
|
|
|
|
|
|
|
|
redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){
|
2014-03-22 19:08:33 -07:00
|
|
|
if (error && workers.length !== 0){
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - redis error with multi get balances ' + JSON.stringify(error));
|
2014-03-11 20:47:14 -07:00
|
|
|
return;
|
|
|
|
}
|
2014-03-12 23:37:27 -07:00
|
|
|
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
var workerBalances = {};
|
2014-03-11 20:47:14 -07:00
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
for (var i = 0; i < workers.length; i++){
|
2014-03-22 23:16:06 -07:00
|
|
|
workerBalances[workers[i]] = (parseInt(results[i]) || 0);
|
2014-03-11 20:47:14 -07:00
|
|
|
}
|
|
|
|
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances);
|
2014-03-11 20:47:14 -07:00
|
|
|
});
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-11 20:47:14 -07:00
|
|
|
},
|
|
|
|
|
2014-03-11 21:01:33 -07:00
|
|
|
|
2014-03-11 20:47:14 -07:00
|
|
|
/* Calculate if any payments are ready to be sent and trigger them sending
|
2014-03-11 21:57:03 -07:00
|
|
|
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)
|
|
|
|
*/
|
2014-03-22 19:08:33 -07:00
|
|
|
function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, callback){
|
|
|
|
|
|
|
|
//number of satoshis in a single coin unit - this can be different for coins so we calculate it :)
|
2014-03-20 15:25:59 -07:00
|
|
|
|
|
|
|
|
2014-03-26 08:43:52 -07:00
|
|
|
daemon.cmd('getbalance', [''], function(results){
|
2014-03-20 15:25:59 -07:00
|
|
|
|
|
|
|
var totalBalance = results[0].response * magnitude;
|
|
|
|
var toBePaid = 0;
|
|
|
|
var workerPayments = {};
|
2014-03-11 21:01:33 -07:00
|
|
|
|
|
|
|
|
2014-03-20 15:25:59 -07:00
|
|
|
var balanceUpdateCommands = [];
|
|
|
|
var workerPayoutsCommand = [];
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
/* Here we add up all workers' previous unpaid balances plus their current rewards as we are
|
|
|
|
about to check if they reach the payout threshold. */
|
2014-03-20 15:25:59 -07:00
|
|
|
for (var worker in workerRewards){
|
2014-03-22 19:08:33 -07:00
|
|
|
workerPayments[worker] = ((workerPayments[worker] || 0) + workerRewards[worker]);
|
2014-03-20 15:25:59 -07:00
|
|
|
}
|
|
|
|
for (var worker in workerBalances){
|
2014-03-22 19:08:33 -07:00
|
|
|
workerPayments[worker] = ((workerPayments[worker] || 0) + workerBalances[worker]);
|
2014-03-20 15:25:59 -07:00
|
|
|
}
|
2014-03-22 19:08:33 -07:00
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
/* Here we check if any of the workers reached their payout threshold, or delete them from the
|
|
|
|
pending payment ledger (the workerPayments object). */
|
2014-03-22 19:08:33 -07:00
|
|
|
if (Object.keys(workerPayments).length > 0){
|
|
|
|
var coinPrecision = magnitude.toString().length - 1;
|
|
|
|
for (var worker in workerPayments){
|
|
|
|
if (workerPayments[worker] < processingConfig.minimumPayment * magnitude){
|
2014-03-26 23:56:36 -07:00
|
|
|
/* The workers total earnings (balance + current reward) was not enough to warrant
|
|
|
|
a transaction, so we will store their balance in the database. Next time they
|
|
|
|
are rewarded it might reach the payout threshold. */
|
|
|
|
balanceUpdateCommands.push([
|
|
|
|
'hincrby',
|
|
|
|
coin + '_balances',
|
|
|
|
worker,
|
|
|
|
workerRewards[worker]
|
|
|
|
]);
|
2014-03-22 19:08:33 -07:00
|
|
|
delete workerPayments[worker];
|
|
|
|
}
|
|
|
|
else{
|
2014-03-26 23:56:36 -07:00
|
|
|
//If worker had a balance that is about to be paid out, subtract it from the database
|
2014-03-22 19:08:33 -07:00
|
|
|
if (workerBalances[worker] !== 0){
|
2014-03-26 23:56:36 -07:00
|
|
|
balanceUpdateCommands.push([
|
|
|
|
'hincrby',
|
|
|
|
coin + '_balances',
|
|
|
|
worker,
|
|
|
|
-1 * workerBalances[worker]
|
|
|
|
]);
|
2014-03-22 19:08:33 -07:00
|
|
|
}
|
|
|
|
var rewardInPrecision = (workerRewards[worker] / magnitude).toFixed(coinPrecision);
|
|
|
|
workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', worker, rewardInPrecision]);
|
|
|
|
toBePaid += workerPayments[worker];
|
|
|
|
}
|
2014-03-20 15:25:59 -07:00
|
|
|
}
|
2014-03-22 19:08:33 -07:00
|
|
|
|
2014-03-20 15:25:59 -07:00
|
|
|
}
|
|
|
|
|
2014-03-26 16:36:07 -07:00
|
|
|
// txfee included in feeAmountToBeCollected
|
2014-03-26 23:56:36 -07:00
|
|
|
var leftOver = toBePaid / (1 - processingConfig.feePercent);
|
|
|
|
var feeAmountToBeCollected = toPrecision(leftOver * processingConfig.feePercent, coinPrecision);
|
2014-03-26 16:36:07 -07:00
|
|
|
var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected;
|
2014-03-20 15:25:59 -07:00
|
|
|
var minReserveSatoshis = processingConfig.minimumReserve * magnitude;
|
|
|
|
if (balanceLeftOver < minReserveSatoshis){
|
2014-03-26 23:56:36 -07:00
|
|
|
/* TODO: Need to convert all these variables into whole coin units before displaying because
|
|
|
|
humans aren't good at reading satoshi units. */
|
|
|
|
callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' +
|
|
|
|
toBePaid + ' and collect ' + feeAmountToBeCollected + ' as fees' +
|
2014-03-20 15:25:59 -07:00
|
|
|
' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver +
|
|
|
|
', needs to be at least ' + minReserveSatoshis);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
/* Move pending blocks into either orphan for confirmed sets, and delete their no longer
|
|
|
|
required round/shares data. */
|
2014-03-20 15:25:59 -07:00
|
|
|
var movePendingCommands = [];
|
2014-03-22 19:08:33 -07:00
|
|
|
var roundsToDelete = [];
|
2014-03-20 15:25:59 -07:00
|
|
|
rounds.forEach(function(r){
|
2014-03-27 15:29:43 -07:00
|
|
|
|
|
|
|
var destinationSet = (function(){
|
|
|
|
switch(r.category){
|
|
|
|
case 'orphan': return '_blocksOrphaned';
|
|
|
|
case 'generate': return '_blocksConfirmed';
|
|
|
|
case 'dropkicked': return '_blocksDropKicked';
|
|
|
|
}
|
|
|
|
})();
|
2014-03-20 15:25:59 -07:00
|
|
|
movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
|
2014-03-22 19:08:33 -07:00
|
|
|
roundsToDelete.push(coin + '_shares:round' + r.height)
|
2014-03-20 15:25:59 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
var finalRedisCommands = [];
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
if (movePendingCommands.length > 0)
|
|
|
|
finalRedisCommands = finalRedisCommands.concat(movePendingCommands);
|
2014-03-12 17:09:12 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
if (orphanMergeCommands.length > 0)
|
|
|
|
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
|
2014-03-20 15:25:59 -07:00
|
|
|
|
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
if (balanceUpdateCommands.length > 0)
|
|
|
|
finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);
|
|
|
|
|
|
|
|
|
|
|
|
if (workerPayoutsCommand.length > 0)
|
|
|
|
finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand);
|
|
|
|
|
|
|
|
|
|
|
|
if (roundsToDelete.length > 0)
|
|
|
|
finalRedisCommands.push(['del'].concat(roundsToDelete));
|
2014-03-20 15:25:59 -07:00
|
|
|
|
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
if (toBePaid !== 0)
|
|
|
|
finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]);
|
|
|
|
|
|
|
|
|
2014-03-27 06:22:21 -07:00
|
|
|
callback(null, magnitude, workerPayments, finalRedisCommands);
|
2014-03-22 19:08:33 -07:00
|
|
|
|
2014-03-20 15:25:59 -07:00
|
|
|
|
|
|
|
});
|
2014-03-11 18:56:19 -07:00
|
|
|
},
|
2014-03-09 19:31:58 -07:00
|
|
|
|
2014-03-27 06:22:21 -07:00
|
|
|
function(magnitude, workerPayments, finalRedisCommands, callback){
|
2014-03-20 15:25:59 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
//This does the final all-or-nothing atom transaction if block deamon sent payments
|
|
|
|
var finalizeRedisTx = function(){
|
|
|
|
redisClient.multi(finalRedisCommands).exec(function(error, results){
|
|
|
|
if (error){
|
2014-03-26 23:56:36 -07:00
|
|
|
callback('Error with final redis commands for cleaning up ' + JSON.stringify(error));
|
2014-03-22 19:08:33 -07:00
|
|
|
return;
|
|
|
|
}
|
2014-03-26 17:57:29 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Payments processing performed an interval');
|
2014-03-22 19:08:33 -07:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
if (Object.keys(workerPayments).length === 0){
|
|
|
|
finalizeRedisTx();
|
2014-03-20 15:25:59 -07:00
|
|
|
}
|
2014-03-22 19:08:33 -07:00
|
|
|
else{
|
2014-03-20 15:25:59 -07:00
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
//This is how many decimal places to round a coin down to
|
2014-03-22 19:08:33 -07:00
|
|
|
var coinPrecision = magnitude.toString().length - 1;
|
|
|
|
var addressAmounts = {};
|
2014-03-22 23:16:06 -07:00
|
|
|
var totalAmountUnits = 0;
|
2014-03-22 19:08:33 -07:00
|
|
|
for (var address in workerPayments){
|
2014-03-26 23:56:36 -07:00
|
|
|
var coinUnits = toPrecision(workerPayments[address] / magnitude, coinPrecision);
|
2014-03-25 18:27:28 -07:00
|
|
|
addressAmounts[address] = coinUnits;
|
|
|
|
totalAmountUnits += coinUnits;
|
2014-03-20 15:34:44 -07:00
|
|
|
}
|
2014-03-22 12:58:51 -07:00
|
|
|
|
2014-03-26 23:56:36 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Payments to be sent to: ' + JSON.stringify(addressAmounts));
|
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
daemon.cmd('sendmany', ['', addressAmounts], function(results){
|
2014-03-26 23:56:36 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
if (results[0].error){
|
2014-03-22 23:46:59 -07:00
|
|
|
callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error));
|
2014-03-20 15:34:44 -07:00
|
|
|
return;
|
|
|
|
}
|
2014-03-26 23:56:36 -07:00
|
|
|
|
2014-03-22 19:08:33 -07:00
|
|
|
finalizeRedisTx();
|
2014-03-26 23:56:36 -07:00
|
|
|
|
2014-03-22 12:58:51 -07:00
|
|
|
var totalWorkers = Object.keys(workerPayments).length;
|
2014-03-26 16:36:07 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol +
|
2014-03-22 19:08:33 -07:00
|
|
|
' was sent to ' + totalWorkers + ' miners');
|
2014-03-27 06:22:21 -07:00
|
|
|
daemon.cmd('gettransaction', [results[0].response], function(results){
|
|
|
|
if (results[0].error){
|
|
|
|
callback('Check finished - error with gettransaction ' + JSON.stringify(results[0].error));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
|
|
|
|
var poolFees = feeAmountUnits - results[0].response.fee;
|
|
|
|
daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){
|
|
|
|
if (results[0].error){
|
|
|
|
callback('Check finished - error with move ' + JSON.stringify(results[0].error));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee');
|
2014-03-26 16:36:07 -07:00
|
|
|
});
|
2014-03-27 06:22:21 -07:00
|
|
|
});
|
2014-03-20 15:34:44 -07:00
|
|
|
});
|
2014-03-22 19:08:33 -07:00
|
|
|
}
|
2014-03-11 18:56:19 -07:00
|
|
|
}
|
|
|
|
], function(error, result){
|
2014-03-27 15:29:43 -07:00
|
|
|
|
|
|
|
|
|
|
|
var paymentProcessTime = Date.now() - startPaymentProcess;
|
|
|
|
|
2014-03-20 15:34:44 -07:00
|
|
|
if (error)
|
2014-03-27 15:29:43 -07:00
|
|
|
logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + error);
|
2014-03-20 15:34:44 -07:00
|
|
|
|
|
|
|
else{
|
2014-03-27 15:29:43 -07:00
|
|
|
logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + result);
|
2014-03-26 16:36:07 -07:00
|
|
|
// not sure if we need some time to let daemon update the wallet balance
|
|
|
|
setTimeout(withdrawalProfit, 1000);
|
2014-03-20 15:34:44 -07:00
|
|
|
}
|
2014-03-09 19:31:58 -07:00
|
|
|
});
|
2014-03-11 18:56:19 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-03-20 16:05:13 -07:00
|
|
|
var withdrawalProfit = function(){
|
2014-03-20 16:11:39 -07:00
|
|
|
|
|
|
|
if (!processingConfig.feeWithdrawalThreshold) return;
|
|
|
|
|
2014-03-26 16:49:08 -07:00
|
|
|
logger.debug(logSystem, logComponent, 'Profit withdrawal started');
|
2014-03-26 08:43:52 -07:00
|
|
|
daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){
|
2014-03-20 16:05:13 -07:00
|
|
|
|
2014-03-26 16:36:07 -07:00
|
|
|
// We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less
|
|
|
|
// then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance
|
|
|
|
// total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing.
|
2014-03-27 06:28:42 -07:00
|
|
|
var withdrawalAmount = results[0].response;
|
2014-03-26 08:43:52 -07:00
|
|
|
|
2014-03-27 06:28:42 -07:00
|
|
|
if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){
|
|
|
|
logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet');
|
|
|
|
}
|
|
|
|
else{
|
2014-03-26 08:43:52 -07:00
|
|
|
|
2014-03-27 06:28:42 -07:00
|
|
|
var withdrawal = {};
|
|
|
|
withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount;
|
2014-03-20 16:05:13 -07:00
|
|
|
|
2014-03-27 06:28:42 -07:00
|
|
|
daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){
|
|
|
|
if (results[0].error){
|
|
|
|
logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol +
|
|
|
|
' was sent to ' + processingConfig.feeReceiveAddress);
|
|
|
|
});
|
|
|
|
}
|
2014-03-20 16:05:13 -07:00
|
|
|
});
|
2014-03-20 16:11:39 -07:00
|
|
|
|
2014-03-20 16:05:13 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-03-27 15:29:43 -07:00
|
|
|
setInterval(function(){
|
|
|
|
try {
|
|
|
|
processPayments();
|
|
|
|
} catch(e){
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}, processingConfig.paymentInterval * 1000);
|
2014-03-11 20:47:14 -07:00
|
|
|
setTimeout(processPayments, 100);
|
2014-03-09 19:31:58 -07:00
|
|
|
|
|
|
|
};
|