Hush-flavored NOMP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1599 lines
79 KiB

6 years ago
var fs = require('fs');
var request = require('request');
var redis = require('redis');
var async = require('async');
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
5 years ago
module.exports = function(logger) {
6 years ago
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, results){
results.forEach(function(coin){
var poolOptions = poolConfigs[coin];
var processingConfig = poolOptions.paymentProcessing;
var logSystem = 'Payments';
var logComponent = coin;
logger.info(logSystem, logComponent, 'Payment processing setup with daemon ('
6 years ago
+ processingConfig.daemon.user + '@' + processingConfig.daemon.host + ':' + processingConfig.daemon.port
+ ') and redis (' + poolOptions.redis.host + ':' + poolOptions.redis.port + ')');
6 years ago
});
});
};
function SetupForPool(logger, poolOptions, setupFinished) {
6 years ago
var coin = poolOptions.coin.name;
var processingConfig = poolOptions.paymentProcessing;
var logSystem = 'Payments';
var logComponent = coin;
var displayBalances = false;
var sendingTXFeeSats = 10000;
5 years ago
var coinPrecision;
6 years ago
var opidCount = 0;
var opids = [];
5 years ago
var magnitude;
var minPaymentSatoshis;
var paymentInterval;
6 years ago
// 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.');
}
6 years ago
var maxBlocksPerPayment = Math.max(processingConfig.maxBlocksPerPayment || 3, 1);
6 years ago
// pplnt - pay per last N time shares
var pplntEnabled = processingConfig.paymentMode === "pplnt" || false;
var pplntTimeQualify = processingConfig.pplnt || 0.51; // 51%
if (typeof poolOptions.coin.privateChain !== 'undefined' && poolOptions.coin.privateChain === true) {
var privateChain = poolOptions.coin.privateChain === true;
var requireShielding = true;
} else {
var privateChain = false;
var requireShielding = poolOptions.coin.requireShielding === true;
}
6 years ago
var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004);
logger.info(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding);
logger.info(logSystem, logComponent, logComponent + ' minConf: ' + minConfShield);
logger.info(logSystem, logComponent, logComponent + ' payments txfee reserve: ' + fee);
logger.info(logSystem, logComponent, logComponent + ' maxBlocksPerPayment: ' + maxBlocksPerPayment);
logger.info(logSystem, logComponent, logComponent + ' PPLNT: ' + pplntEnabled + ', time period: '+pplntTimeQualify);
6 years ago
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){
logger[severity](logSystem, logComponent, message);
});
5 years ago
6 years ago
var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host);
// redis auth if enabled
if (poolOptions.redis.password) {
redisClient.auth(poolOptions.redis.password);
}
5 years ago
function validateAddress(callback) {
6 years ago
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);
}
5 years ago
function validateTAddress(callback) {
6 years ago
daemon.cmd('validateaddress', [poolOptions.tAddress], 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);
}
5 years ago
function validateZAddress(callback) {
6 years ago
daemon.cmd('z_validateaddress', [poolOptions.zAddress], function(result) {
if (result.error){
logger.error(logSystem, logComponent, 'Error with payment processing daemon ' + JSON.stringify(result.error));
callback(true);
5 years ago
} else if (!result.response || !result.response.ismine) {
6 years ago
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);
5 years ago
} else {
6 years ago
callback()
}
}, true);
}
5 years ago
function getBalance(callback) {
daemon.cmd('getbalance', [], function(result) {
if (result.error) {
6 years ago
return callback(true);
}
5 years ago
6 years ago
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;
5 years ago
} catch(e) {
6 years ago
logger.error(logSystem, logComponent, 'Error detecting number of satoshis in a coin, cannot do payment processing. Tried parsing: ' + result.data);
return callback(true);
}
callback();
}, true, true);
}
5 years ago
function asyncComplete(err) {
if (err) {
6 years ago
setupFinished(false);
return;
}
5 years ago
6 years ago
if (paymentInterval) {
clearInterval(paymentInterval);
}
//Make payment on startup if "paymentProcessing" : { "payOnStart": true } is set
if (typeof processingConfig.payOnStart !== 'undefined' && processingConfig.payOnStart) {
processPayments();
}
6 years ago
paymentInterval = setInterval(processPayments, paymentIntervalSecs * 1000);
//setTimeout(processPayments, 100);
setupFinished(true);
}
if (requireShielding === true) {
async.parallel([validateAddress, validateTAddress, validateZAddress, getBalance], asyncComplete);
} else {
async.parallel([validateAddress, validateTAddress, getBalance], asyncComplete);
}
/**
* Specify with first param if using normal listunspent or listunspentZ
* @param {String} type (T | Z)
* @param {*} addr
* @param {*} notAddr
* @param {*} minConf
* @param {*} displayBool
* @param {*} callback
*/
function listUnspentType(type, zaddr, notAddr, minConf, displayBool, callback) {
if (type == 'Z') {
return listUnspentZ (zaddr, minConf, displayBool, callback);
} else {
return listUnspent (null, notAddr, minConf, displayBool, callback);
}
}
6 years ago
//get t_address coinbalance
5 years ago
function listUnspent(addr, notAddr, minConf, displayBool, callback) {
6 years ago
if (addr !== null) {
var args = [minConf, 99999999, [addr]];
} else {
addr = 'Payout wallet';
var args = [minConf, 99999999];
}
5 years ago
6 years ago
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);
5 years ago
} else {
6 years ago
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
5 years ago
function listUnspentZ(addr, minConf, displayBool, callback) {
6 years ago
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);
5 years ago
} else {
6 years ago
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
5 years ago
function sendTToZ(callback, tBalance) {
if (callback === true) {
6 years ago
return;
5 years ago
}
6 years ago
if (tBalance === NaN) {
logger.error(logSystem, logComponent, 'tBalance === NaN for sendTToZ');
return;
}
5 years ago
if ((tBalance - sendingTXFeeSats) <= 0) {
6 years ago
return;
5 years ago
}
6 years ago
// 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 - sendingTXFeeSats);
var params = [poolOptions.address, poolOptions.zAddress, 0.00001, 0];
daemon.cmd('z_shieldcoinbase', params,
6 years ago
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[4];
6 years ago
opidCount++;
opids.push(opid);
logger.special(logSystem, logComponent, 'Shield balance ' + amount + ' ' + opid);
callback = function (){};
callback(null);
}
}
);
}
// send z_address balance to t_address
5 years ago
function sendZToT(callback, zBalance) {
if (privateChain) {
return;
}
5 years ago
if (callback === true) {
6 years ago
return;
5 years ago
}
6 years ago
if (zBalance === NaN) {
logger.error(logSystem, logComponent, 'zBalance === NaN for sendZToT');
return;
}
5 years ago
if ((zBalance - sendingTXFeeSats) <= 0) {
6 years ago
return;
5 years ago
}
6 years ago
// 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 - sendingTXFeeSats);
6 years ago
// unshield no more than 100 ZEC at a time
5 years ago
if (amount > 100.0) {
6 years ago
amount = 100.0;
5 years ago
}
6 years ago
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);
5 years ago
} else {
var opid = (result.response || result[0].response);
6 years ago
opidCount++;
opids.push(opid);
logger.special(logSystem, logComponent, 'Unshield funds for payout ' + amount + ' ' + opid);
callback = function (){};
callback(null);
}
}
);
}
// 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
// shielding not required for some equihash coins
if (requireShielding === true) {
var shieldInterval = setInterval(function() {
shieldIntervalState++;
switch (shieldIntervalState) {
case 1:
listUnspent(poolOptions.address, null, minConfShield, displayBalances, sendTToZ);
6 years ago
break;
default:
listUnspentZ(poolOptions.zAddress, minConfShield, displayBalances, sendZToT);
6 years ago
shieldIntervalState = 0;
break;
}
}, shielding_interval);
}
6 years ago
// check operation statuses every 57 seconds
//var opid_interval = 57 * 1000;
6 years ago
var opid_interval = (typeof poolOptions.sapling !== 'undefined' && ( poolOptions.sapling || poolOptions.sapling > 0 )) ? 15 * 1000 : 57 * 1000;
6 years ago
// shielding not required for some equihash coins
if (requireShielding === true) {
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.');
}
}
5 years ago
6 years ago
// 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);
}
if (privateChain && op.status != "executing") {
//Check to see if the opid exists in the payments datastore and a payment needs the transaction updated or we need to mark the payment as failed
redisClient.multi([ ['zrange', coin + ':payments', 0, -1] ]).exec(function(error, allpayments) {
if (!error && allpayments) {
// console.log(allpayments[0]);
paymentstoparse = allpayments[0];
for (var ap in paymentstoparse) {
apresult = JSON.parse(paymentstoparse[ap]);
if (apresult.opid == op.id) {
apresult.txid = op.result.txid;
// console.log(apresult);
logger.special(logSystem, logComponent, "Matched a OPID and TXID! OPID:" + op.id + " TXID:" + op.result.txid);
if (op.status == "failed") {
redisClient.multi([ ['zremrangebyscore', coin + ':payments', apresult.time, apresult.time] ]).exec(function(error, results) {
if (error) {
logger.error(logSystem, logComponent, "Removed failed payment failed OPID:" + op.id + " " + error);
} else {
logger.error(logSystem, logComponent, "Removed failed payment success! OPID:" + op.id + ", " + op.error.code +", " + op.error.message);
}
});
} else {
logger.special(logSystem, logComponent, 'Found payment opid ' + op.id );
redisClient.multi([
['zremrangebyscore', coin + ':payments', apresult.time, apresult.time],
['zadd', coin + ':payments', apresult.time, JSON.stringify(apresult)]
]).exec(function(error, results) {
if (error) {
logger.error(logSystem, logComponent, "Updating txid for ztransaction failed OPID:" + op.id + " " + error);
} else {
logger.special(logSystem, logComponent, "Updating txid for ztransaction success! OPID:" + op.id + " TXID:" + op.result.txid);
}
});
}
}
}
} else {
console.log('error fetching payments');
}
});
}
6 years ago
// log status to console
if (op.status == "failed") {
if (op.error) {
logger.error(logSystem, logComponent, "Shielded operation failed " + op.id + " " + op.error.code +", " + op.error.message);
6 years ago
} else {
logger.error(logSystem, logComponent, "Shielded operation failed " + op.id);
6 years ago
}
} else {
logger.special(logSystem, logComponent, 'Shielded operation success ' + op.id + ' txid: ' + op.result.txid);
6 years ago
}
} else if (op.status == "executing") {
logger.special(logSystem, logComponent, 'Shielded operation in progress ' + op.id );
//This is important to make sure we don't send payments while there is another operation in progress, maybe it should just stay in general?
if (privateChain) {
opidCount++;
}
6 years ago
}
});
/* Not sure if this is needed but call this to clear out operations (some seemed to get stuck) */
if (privateChain) {
5 years ago
logger.special(logSystem, logComponent, "Running z_operationresult for privateChain");
batchRPC.push(['z_getoperationresult']);
}
6 years ago
// if there are no completed operations
if (batchRPC.length <= 0) {
opidTimeout = setTimeout(checkOpids, opid_interval);
return;
}
5 years ago
6 years ago
// 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);
});
};
5 years ago
6 years ago
// 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);
}
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 checkForDuplicateBlockHeight(rounds, height) {
var count = 0;
for (var i = 0; i < rounds.length; i++) {
if (rounds[i].height == height)
count++;
}
return count > 1;
}
5 years ago
var validateAddress = async function(address, callback) {
let poolZAddressPrefix = poolOptions.zAddress.substring(0,2);
var isValid = false;
if (privateChain) {
daemon.cmd('z_validateaddress', [address], function (results) {
isValid = results.filter(function (r) {
if ( (poolOptions.coin.sapling || poolOptions.coin.sapling > 0) && poolZAddressPrefix == 'zs') {
return (r.response.isvalid && r.response.type == 'sapling');
} else if ( poolZAddressPrefix == 'zc') {
return (r.response.isvalid && r.response.type == 'sprout');
} else {
return r.response.isvalid;
}
}).length > 0;
//authCallback(isValid);
});
} else {
daemon.cmd('validateaddress', [address], function (results) {
isValid = results.filter(function (r) {
return r.response.isvalid
}).length > 0;
//authCallback(isValid);
});
}
if (isValid) {
return address;
} else {
return (poolOptions.invalidAddress || poolOptions.address);
}
};
var getProperAddress = function(address) {
let poolZAddressPrefix = poolOptions.zAddress.substring(0,2);
let minerAddressLength = address.replace(/[^0-9a-z]/gi, '').length;
let minerAddressPrefix = address.substring(0,2);
let checkPrefix = (poolOptions.minerPrefix || false);
5 years ago
if (typeof poolOptions.rewardsDisabled !== 'undefined' && poolOptions.rewardsDisabled.includes(address)) {
logger.warning(logSystem, logComponent, 'Rewards disabled for this adddress: ' + address + ', convert to address ' + (poolOptions.invalidAddress || poolOptions.address));
return (poolOptions.invalidAddress || poolOptions.address);
} else if (privateChain && poolZAddressPrefix == 'zs' && minerAddressLength == 78 && minerAddressPrefix == 'zs') {
5 years ago
//validate as sapling
return address;
} else if (privateChain && poolZAddressPrefix == 'zc' && minerAddressLength == 95 && minerAddressPrefix == 'zc') {
//validate as sprout
return address;
} else if (privateChain || address.length >= 40 || address.length <= 30) {
logger.warning(logSystem, logComponent, 'Invalid address ' + address + ', convert to address ' + (poolOptions.invalidAddress || poolOptions.address));
return (poolOptions.invalidAddress || poolOptions.address);
// as a final stopgap you can add "minerPrefix":"R" to pool_config
} else if (checkPrefix && address.substring(0, checkPrefix.length) != checkPrefix) {
logger.warning(logSystem, logComponent, 'Invalid address ' + address + ', convert to address ' + (poolOptions.invalidAddress || poolOptions.address));
5 years ago
return (poolOptions.invalidAddress || poolOptions.address);
} else {
return address;
}
};
6 years ago
/* 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. */
5 years ago
var processPayments = function() {
6 years ago
var startPaymentProcess = Date.now();
var poolperc = 0;
6 years ago
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 };
5 years ago
/*
Step 1 - build workers and rounds objects from redis
* removes duplicate block submissions from redis
*/
var buildWorkers = 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;
}
// build workers object from :balances
var workers = {};
for (var w in results[0]){
workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))};
}
// build rounds object from :blocksPending
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],
duplicate: false,
serialized: r
};
});
/* sort rounds by block hieght to pay in order */
rounds.sort(function(a, b) {
return a.height - b.height;
});
// find duplicate blocks by height
// this can happen when two or more solutions are submitted at the same block height
var duplicateFound = false;
for (var i = 0; i < rounds.length; i++) {
if (checkForDuplicateBlockHeight(rounds, rounds[i].height) === true) {
rounds[i].duplicate = true;
duplicateFound = true;
6 years ago
}
5 years ago
}
// handle duplicates if needed
if (duplicateFound) {
var dups = rounds.filter(function(round){ return round.duplicate; });
logger.warning(logSystem, logComponent, 'Duplicate pending blocks found: ' + JSON.stringify(dups));
// attempt to find the invalid duplicates
var rpcDupCheck = dups.map(function(r){
return ['getblock', [r.blockHash]];
6 years ago
});
5 years ago
startRPCTimer();
daemon.batchCmd(rpcDupCheck, function(error, blocks){
endRPCTimer();
if (error || !blocks) {
logger.error(logSystem, logComponent, 'Error with duplicate block check rpc call getblock ' + JSON.stringify(error));
return;
6 years ago
}
5 years ago
// look for the invalid duplicate block
var validBlocks = {}; // hashtable for unique look up
var invalidBlocks = []; // array for redis work
blocks.forEach(function(block, i) {
if (block && block.result) {
// invalid duplicate submit blocks have negative confirmations
if (block.result.confirmations < 0) {
logger.warning(logSystem, logComponent, 'Remove invalid duplicate block ' + block.result.height + ' > ' + block.result.hash);
// move from blocksPending to blocksDuplicate...
invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]);
} else {
// block must be valid, make sure it is unique
if (validBlocks.hasOwnProperty(dups[i].blockHash)) {
// not unique duplicate block
logger.warning(logSystem, logComponent, 'Remove non-unique duplicate block ' + block.result.height + ' > ' + block.result.hash);
6 years ago
// move from blocksPending to blocksDuplicate...
invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]);
} else {
5 years ago
// keep unique valid block
validBlocks[dups[i].blockHash] = dups[i].serialized;
logger.info(logSystem, logComponent, 'Keep valid duplicate block ' + block.result.height + ' > ' + block.result.hash);
6 years ago
}
}
}
});
5 years ago
// filter out all duplicates to prevent double payments
rounds = rounds.filter(function(round){ return !round.duplicate; });
// if we detected the invalid duplicates, move them
if (invalidBlocks.length > 0) {
// move invalid duplicate blocks in redis
startRedisTimer();
redisClient.multi(invalidBlocks).exec(function(error, kicked){
endRedisTimer();
if (error) {
logger.error(logSystem, logComponent, 'Error could not move invalid duplicate blocks in redis ' + JSON.stringify(error));
}
// continue payments normally
callback(null, workers, rounds);
});
} else {
// notify pool owner that we are unable to find the invalid duplicate blocks, manual intervention required...
logger.error(logSystem, logComponent, 'Unable to detect invalid duplicate blocks, duplicate block payments on hold.');
// continue payments normally
callback(null, workers, rounds);
}
});
} else {
// no duplicates, continue payments normally
callback(null, workers, rounds);
}
});
}
/*
Step 2 - check if mined block coinbase tx are ready for payment
* adds block reward to rounds object
* adds block confirmations count to rounds object
*/
var checkMined = function(workers, rounds, callback) {
// get pending block tx details
var batchRPCcommand = rounds.map(function(r){
return ['gettransaction', [r.txHash]];
});
// get account address (not implemented at this time)
// is this needed?
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
startRPCTimer();
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
endRPCTimer();
if (error || !txDetails){
logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch gettransactions ' + JSON.stringify(error));
callback(true);
return;
}
var addressAccount = "";
// check for transaction errors and generated coins
txDetails.forEach(function(tx, i){
if (i === txDetails.length - 1){
if (tx.result && tx.result.toString().length > 0) {
addressAccount = tx.result.toString();
}
return;
6 years ago
}
5 years ago
var round = rounds[i];
// update confirmations for round
if (tx && tx.result) {
round.confirmations = parseInt((tx.result.confirmations || 0));
}
// look for transaction errors
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));
6 years ago
return;
}
5 years ago
// get the coin base generation tx
var generationTx = tx.result.details.filter(function(tx){
return tx.address === poolOptions.address;
})[0];
6 years ago
5 years ago
if (!generationTx && tx.result.details.length === 1){
generationTx = tx.result.details[0];
}
5 years ago
if (!generationTx){
logger.error(logSystem, logComponent, 'Missing output details to pool address for transaction ' + round.txHash);
return;
}
5 years ago
// get transaction category for round
round.category = generationTx.category;
5 years ago
// get reward for newly generated blocks
if (round.category === 'generate' || round.category === 'immature') {
var minerperc = 1;
if (poolOptions.coin.disablecb && poolOptions.rewardRecipients.length !== 0) {
for (var r in poolOptions.rewardRecipients) {
minerperc = minerperc - (poolOptions.rewardRecipients[r]/100);
}
6 years ago
}
5 years ago
poolperc = roundTo(1 - minerperc,4);
round.reward = coinsRound(parseFloat(generationTx.amount*minerperc || generationTx.value*minerperc));
}
//console.log(round.reward);
});
6 years ago
5 years ago
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;
6 years ago
}
5 years ago
}
return true;
};
// only pay max blocks at a time
var payingBlocks = 0;
rounds = rounds.filter(function(r) {
// Don't count for payout before minConfirmations from config
if ( r.confirmations > -1 && r.confirmations < (minConfPayout*2) ) {
r.category = 'immature';
6 years ago
return true;
5 years ago
}
6 years ago
5 years ago
switch (r.category) {
case 'orphan':
case 'kicked':
r.canDeleteShares = canDeleteShares(r);
case 'immature':
return true;
5 years ago
case 'generate':
payingBlocks++;
// 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:
return false;
};
});
5 years ago
// continue to next step in waterfall
callback(null, workers, rounds, addressAccount);
});
}
6 years ago
5 years ago
/*
Step 3 - lookup shares and calculate rewards
* pull pplnt times from redis
* pull shares from redis
* calculate rewards
* pplnt share reductions if needed
*/
var calculateRewards = function(workers, rounds, addressAccount, callback) {
// pplnt times lookup
var timeLookups = rounds.map(function(r){
return ['hgetall', coin + ':shares:times' + r.height]
});
startRedisTimer();
redisClient.multi(timeLookups).exec(function(error, allWorkerTimes){
endRedisTimer();
if (error){
callback('Check finished - redis error with multi get rounds time');
return;
}
// shares lookup
var shareLookups = rounds.map(function(r){
return ['hgetall', coin + ':shares:round' + r.height];
6 years ago
});
startRedisTimer();
5 years ago
redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
6 years ago
endRedisTimer();
if (error){
5 years ago
callback('Check finished - redis error with multi get rounds share');
6 years ago
return;
}
5 years ago
// error detection
var err = null;
var performPayment = false;
6 years ago
5 years ago
var notAddr = null;
if (requireShielding === true) {
notAddr = poolOptions.address;
}
6 years ago
5 years ago
// 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;
6 years ago
}
5 years ago
}
// also include balances owed
for (var w in workers) {
var worker = workers[w];
totalOwed = totalOwed + (worker.balance||0);
}
5 years ago
//Check if private transaction already in progress
if (privateChain && opidCount > 0) {
logger.warning(logSystem, logComponent, 'Z Transaction already in progress, cannot send shares');
return callback(true);
}
// check if we have enough tAddress funds to begin payment processing
listunspenttype = privateChain ? 'Z' : 'T';
listUnspentType(listunspenttype, poolOptions.zAddress, notAddr, minConfPayout, displayBalances, function (error, tBalance) {
if (error) {
logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments.');
return callback(true);
5 years ago
} else if (tBalance < totalOwed) {
logger.error(logSystem, logComponent, 'Insufficient [' + listunspenttype + '] funds ('+satoshisToCoins(tBalance) + ') in ('+ poolOptions.zAddress + ') to process payments (' + satoshisToCoins(totalOwed)+'); possibly waiting for txs.');
5 years ago
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;
};
});
}
5 years ago
// 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;
6 years ago
}
5 years ago
var workerTimesWithPoolIds = allWorkerTimes[i];
var workerTimes = {};
var maxTime = 0;
if (pplntEnabled === true) {
for (var workerAddressWithPoolId in workerTimesWithPoolIds){
var workerWithoutPoolId = workerAddressWithPoolId.split('.')[0];
var workerTimeFloat = parseFloat(workerTimesWithPoolIds[workerAddressWithPoolId]);
if (maxTime < workerTimeFloat) {
maxTime = workerTimeFloat;
}
if (!(workerWithoutPoolId in workerTimes)) {
workerTimes[workerWithoutPoolId] = workerTimeFloat;
} else {
// add time from other instances with penalty
if (workerTimes[workerWithoutPoolId] < workerTimeFloat) {
workerTimes[workerWithoutPoolId] = workerTimes[workerWithoutPoolId] * 0.5 + workerTimeFloat;
} else {
5 years ago
workerTimes[workerWithoutPoolId] = workerTimes[workerWithoutPoolId] + workerTimeFloat * 0.5;
}
if (workerTimes[workerWithoutPoolId] > maxTime) {
workerTimes[workerWithoutPoolId] = maxTime;
}
}
6 years ago
}
5 years ago
}
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);
// 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);
6 years ago
}
}
}
5 years ago
worker.roundShares = shares;
totalShares += shares;
}
5 years ago
//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;
}
6 years ago
5 years ago
//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);
// 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);
6 years ago
}
5 years ago
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;
6 years ago
}
}
5 years ago
worker.roundShares = shares;
worker.totalShares = parseFloat(worker.totalShares || 0) + shares;
totalShares += shares;
}
5 years ago
//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;
6 years ago
}
5 years ago
// calculate workers reward for this round
var workerRewardTotal = Math.round(reward * percent);
worker.reward = (worker.reward || 0) + workerRewardTotal;
totalAmount += workerRewardTotal;
}
6 years ago
5 years ago
//console.log('----------------------------');
break;
6 years ago
}
5 years ago
});
5 years ago
// if there was no errors
if (err === null) {
callback(null, workers, rounds, addressAccount);
} else {
// some error, stop waterfall
callback(true);
}
6 years ago
5 years ago
}); // end funds check
});// end share lookup
}); // end time lookup
}
5 years ago
/*
Step 4 - Generate RPC commands to send payments
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)
*/
var sendPayments = function(workers, rounds, addressAccount, callback) {
6 years ago
5 years ago
var tries = 0;
var trySend = function (withholdPercent) {
5 years ago
var addressAmounts = {};
var balanceAmounts = {};
var shareAmounts = {};
var timePeriods = {};
var minerTotals = {};
var totalSent = 0;
var totalShares = 0;
5 years ago
// track attempts made, calls to trySend...
tries++;
5 years ago
// total up miner's balances
for (var w in workers) {
var worker = workers[w];
totalShares += (worker.totalShares || 0)
worker.balance = worker.balance || 0;
worker.reward = worker.reward || 0;
// get miner payout totals
var toSendSatoshis = Math.round((worker.balance + worker.reward) * (1 - withholdPercent));
var address = worker.address = (worker.address || getProperAddress(w.split('.')[0])).trim();
if (minerTotals[address] != null && minerTotals[address] > 0) {
minerTotals[address] += toSendSatoshis;
} else {
minerTotals[address] = toSendSatoshis;
6 years ago
}
5 years ago
}
5 years ago
// now process each workers balance, and pay the miner
for (var w in workers) {
var worker = workers[w];
worker.balance = worker.balance || 0;
worker.reward = worker.reward || 0;
var toSendSatoshis = Math.round((worker.balance + worker.reward) * (1 - withholdPercent));
var address = worker.address = (worker.address || getProperAddress(w.split('.')[0])).trim();
// if miners total is enough, go ahead and add this worker balance
if (minerTotals[address] >= minPaymentSatoshis) {
totalSent += toSendSatoshis;
// send funds
worker.sent = satoshisToCoins(toSendSatoshis);
worker.balanceChange = Math.min(worker.balance, toSendSatoshis) * -1;
if (addressAmounts[address] != null && addressAmounts[address] > 0) {
addressAmounts[address] = coinsRound(addressAmounts[address] + worker.sent);
6 years ago
} else {
5 years ago
addressAmounts[address] = worker.sent;
6 years ago
}
5 years ago
} else {
// add to balance, not enough minerals
worker.sent = 0;
worker.balanceChange = Math.max(toSendSatoshis - worker.balance, 0);
// track balance changes
if (worker.balanceChange > 0) {
if (balanceAmounts[address] != null && balanceAmounts[address] > 0) {
balanceAmounts[address] = coinsRound(balanceAmounts[address] + satoshisToCoins(worker.balanceChange));
6 years ago
} else {
5 years ago
balanceAmounts[address] = satoshisToCoins(worker.balanceChange);
6 years ago
}
}
}
5 years ago
// track share work
if (worker.totalShares > 0) {
if (shareAmounts[address] != null && shareAmounts[address] > 0) {
shareAmounts[address] += worker.totalShares;
} else {
shareAmounts[address] = worker.totalShares;
}
6 years ago
}
5 years ago
}
6 years ago
5 years ago
// if no payouts...continue to next set of callbacks
if (Object.keys(addressAmounts).length === 0){
callback(null, workers, rounds, []);
return;
}
5 years ago
// do final rounding of payments per address
// this forces amounts to be valid (0.12345678)
var totalcoinstosend = 0;
for (var a in addressAmounts) {
addressAmounts[a] = coinsRound(addressAmounts[a]);
totalcoinstosend = totalcoinstosend + addressAmounts[a];
}
5 years ago
if (poolOptions.coin.disablecb && poolOptions.rewardRecipients.length !== 0) {
var totalbr = coinsRound(totalcoinstosend*(poolperc+1));
//console.log(totalbr);
for (var r in poolOptions.rewardRecipients) {
var feetopay = coinsRound(totalbr*(poolOptions.rewardRecipients[r]/100));
addressAmounts[r] = feetopay;
}
}
5 years ago
//console.log(addressAmounts);
5 years ago
// POINT OF NO RETURN! GOOD LUCK!
// WE ARE SENDING PAYMENT CMD TO DAEMON
5 years ago
if (privateChain) {
// perform z_sendmany
//Get total z balance
listUnspentZ (poolOptions.zAddress, minConfPayout, displayBalances, function (error, getBalance) {
zBalance = getBalance;
//for now we'll reshape the addressAmounts variable here instead of hacking up where it's constructed in the rest of the code
zaddressAmounts = [];
sharetotal = 0;
for (var a in addressAmounts) {
sharetotal += addressAmounts[a];
thisaddressamount = {address: a, amount: addressAmounts[a]};
zaddressAmounts.push(thisaddressamount);
}
5 years ago
keepInWallet = satoshisToCoins(zBalance - sendingTXFeeSats - (sharetotal * 100000000));
5 years ago
zaddressAmounts.push({address: poolOptions.zAddress, amount: keepInWallet});
5 years ago
var rpccallTracking = 'z_sendmany ' + poolOptions.zAddress + ' ' + JSON.stringify(zaddressAmounts) + ' 1';
logger.warning(logSystem, logComponent, rpccallTracking);
5 years ago
daemon.cmd('z_sendmany', [poolOptions.zAddress, zaddressAmounts], function(result) {
if (result.error) {
logger.error(logSystem, logComponent, 'Error sending Z payments ' + JSON.stringify(result.error));
// payment failed, prevent updates to redis
callback(true);
return;
5 years ago
} else {
// make sure z_sendmany gives us back a opid
var opid = null;
if (result.response) {
5 years ago
opid = result.response;
6 years ago
}
5 years ago
if (opid != null) {
opidCount++;
// it worked, congrats on your pools payout ;)
logger.special(logSystem, logComponent, 'Sent ' + satoshisToCoins(totalSent)
5 years ago
+ ' to ' + Object.keys(addressAmounts).length + ' miners; opid: '+opid);
6 years ago
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');
}
6 years ago
// save payments data to redis
var paymentBlocks = rounds.filter(function(r){ return r.category == 'generate'; }).map(function(r){
return parseInt(r.height);
});
var paymentsUpdate = [];
5 years ago
let paymentTime = Date.now();
var paymentsData = {time:paymentTime, opid:opid, shares:totalShares, paid:satoshisToCoins(totalSent), miners:Object.keys(addressAmounts).length, blocks: paymentBlocks, amounts: addressAmounts, balances: balanceAmounts, work:shareAmounts};
paymentsUpdate.push(['zadd', logComponent + ':payments', paymentTime, JSON.stringify(paymentsData)]);
callback(null, workers, rounds, paymentsUpdate);
} else {
6 years ago
clearInterval(paymentInterval);
5 years ago
logger.error(logSystem, logComponent, 'Error RPC sendmany did not return opid '
+ JSON.stringify(result) + 'Disabling payment processing to prevent possible double-payouts.');
callback(true);
return;
}
6 years ago
}
}, true, true);
5 years ago
});
} else {
// perform the sendmany operation .. addressAccount
var rpccallTracking = 'sendmany "" '+JSON.stringify(addressAmounts);
//console.log(rpccallTracking);
daemon.cmd('sendmany', ["", addressAmounts], function (result) {
// check for failed payments, there are many reasons
if (result.error && result.error.code === -6) {
// check if it is because we don't have enough funds
if (result.error.message && result.error.message.includes("insufficient funds")) {
// only try up to XX times (Max, 0.5%)
if (tries < 5) {
// we thought we had enough funds to send payments, but apparently not...
// try decreasing payments by a small percent to cover unexpected tx fees?
var higherPercent = withholdPercent + 0.001; // 0.1%
logger.warning(logSystem, logComponent, 'Insufficient funds (??) for payments ('+satoshisToCoins(totalSent)+'), decreasing rewards by ' + (higherPercent * 100).toFixed(1) + '% and retrying');
trySend(higherPercent);
} else {
logger.warning(logSystem, logComponent, rpccallTracking);
logger.error(logSystem, logComponent, "Error sending payments, decreased rewards by too much!!!");
callback(true);
}
} else {
// there was some fatal payment error?
logger.warning(logSystem, logComponent, rpccallTracking);
logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error));
// payment failed, prevent updates to redis
callback(true);
}
return;
}
else if (result.error && result.error.code === -5) {
// invalid address specified in addressAmounts array
logger.warning(logSystem, logComponent, rpccallTracking);
logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error));
// payment failed, prevent updates to redis
callback(true);
return;
}
else if (result.error && result.error.message != null) {
// invalid amount, others?
logger.warning(logSystem, logComponent, rpccallTracking);
logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error));
// payment failed, prevent updates to redis
callback(true);
return;
}
else if (result.error) {
// unknown error
logger.error(logSystem, logComponent, 'Error sending payments ' + JSON.stringify(result.error));
// payment failed, prevent updates to redis
callback(true);
return;
}
else {
5 years ago
// make sure sendmany gives us back a txid
var txid = null;
if (result.response) {
txid = result.response;
}
if (txid != null) {
6 years ago
5 years ago
// it worked, congrats on your pools payout ;)
logger.special(logSystem, logComponent, 'Sent ' + satoshisToCoins(totalSent)
+ ' to ' + Object.keys(addressAmounts).length + ' miners; txid: '+txid);
6 years ago
5 years ago
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');
}
5 years ago
// save payments data to redis
var paymentBlocks = rounds.filter(function(r){ return r.category == 'generate'; }).map(function(r){
return parseInt(r.height);
});
6 years ago
5 years ago
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};
paymentsUpdate.push(['zadd', logComponent + ':payments', Date.now(), JSON.stringify(paymentsData)]);
6 years ago
5 years ago
callback(null, workers, rounds, paymentsUpdate);
5 years ago
} else {
5 years ago
clearInterval(paymentInterval);
6 years ago
5 years ago
logger.error(logSystem, logComponent, 'Error RPC sendmany did not return txid '
+ JSON.stringify(result) + 'Disabling payment processing to prevent possible double-payouts.');
callback(true);
return;
6 years ago
}
5 years ago
}
}, true, true);
}
};
// attempt to send any owed payments
trySend(0);
}
/*
Step 5 - Final redis commands
*/
var writeToRedis = function(workers, rounds, paymentsUpdate, callback) {
var totalPaid = parseFloat(0);
var immatureUpdateCommands = [];
var balanceUpdateCommands = [];
var workerPayoutsCommand = [];
// update worker paid/balance stats
for (var w in workers) {
var worker = workers[w];
// update balances
if ((worker.balanceChange || 0) !== 0){
balanceUpdateCommands.push([
'hincrbyfloat',
coin + ':balances',
w,
satoshisToCoins(worker.balanceChange)
]);
}
// update payouts
if ((worker.sent || 0) > 0){
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, coinsRound(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 roundsToDelete = [];
var orphanMergeCommands = [];
var confirmsUpdate = [];
var confirmsToDelete = [];
var moveSharesToCurrent = function(r){
var workerShares = r.workerShares;
if (workerShares != null) {
logger.warning(logSystem, logComponent, 'Moving shares from orphaned block '+r.height+' to current round.');
Object.keys(workerShares).forEach(function(worker){
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent', worker, workerShares[worker]]);
});
}
};
rounds.forEach(function(r){
switch(r.category){
case 'kicked':
case 'orphan':
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]);
if (r.canDeleteShares){
moveSharesToCurrent(r);
6 years ago
roundsToDelete.push(coin + ':shares:round' + r.height);
roundsToDelete.push(coin + ':shares:times' + r.height);
5 years ago
}
return;
case 'immature':
confirmsUpdate.push(['hset', coin + ':blocksPendingConfirms', r.blockHash, (r.confirmations || 0)]);
return;
case 'generate':
confirmsToDelete.push(['hdel', coin + ':blocksPendingConfirms', r.blockHash]);
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksConfirmed', r.serialized]);
roundsToDelete.push(coin + ':shares:round' + r.height);
roundsToDelete.push(coin + ':shares:times' + r.height);
return;
}
});
6 years ago
5 years ago
var finalRedisCommands = [];
6 years ago
5 years ago
if (movePendingCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(movePendingCommands);
6 years ago
5 years ago
if (orphanMergeCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
6 years ago
5 years ago
if (immatureUpdateCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(immatureUpdateCommands);
5 years ago
if (balanceUpdateCommands.length > 0)
finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);
6 years ago
5 years ago
if (workerPayoutsCommand.length > 0)
finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand);
6 years ago
5 years ago
if (roundsToDelete.length > 0)
finalRedisCommands.push(['del'].concat(roundsToDelete));
6 years ago
5 years ago
if (confirmsUpdate.length > 0)
finalRedisCommands = finalRedisCommands.concat(confirmsUpdate);
5 years ago
if (confirmsToDelete.length > 0)
finalRedisCommands = finalRedisCommands.concat(confirmsToDelete);
5 years ago
if (paymentsUpdate.length > 0)
finalRedisCommands = finalRedisCommands.concat(paymentsUpdate);
5 years ago
if (totalPaid !== 0)
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]);
6 years ago
5 years ago
if (finalRedisCommands.length === 0){
callback();
return;
}
6 years ago
5 years ago
startRedisTimer();
redisClient.multi(finalRedisCommands).exec(function(error, results){
endRedisTimer();
if (error) {
clearInterval(paymentInterval);
5 years ago
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');
5 years ago
fs.writeFile(coin + '_finalRedisCommands.txt', JSON.stringify(finalRedisCommands), function(err){
logger.error('Could not write finalRedisCommands.txt, you are fucked.');
});
}
callback();
});
}
6 years ago
5 years ago
async.waterfall([
buildWorkers,
checkMined,
calculateRewards,
sendPayments,
writeToRedis
6 years ago
], function(){
5 years ago
//On complete
6 years ago
var paymentProcessTime = Date.now() - startPaymentProcess;
logger.info(logSystem, logComponent, 'Finished interval - time spent: '
6 years ago
+ paymentProcessTime + 'ms total, ' + timeSpentRedis + 'ms redis, '
+ timeSpentRPC + 'ms daemon RPC');
});
};
}