* modified for my repositry
* Revert
* add lf
* Fixed bug in parsing balances for payment processing
* Added fixminer.com pool
* New pool
Another one using NOMP
* Update 21coin.json
Added peerMagic needed for p2p block notifications
* Update alphacoin.json
Added peerMagic. Needed for p2p block notifications
* Update benjamins.json
Added peerMagic for p2p block notifications
* Update mazacoin.json
Added peerMagic for p2p block notifications
* Update bottlecaps.json
* Update bottlecaps.json
* Create bunnycoin.json
* Update bunnycoin.json
* Update casinocoin.json
* Create coino.json
* Update coino.json
* Create cryptogenicbullion.json
* Update coino.json
* Update bunnycoin.json
* Update cryptogenicbullion.json
* Create cryptographicanomaly.json
* Update cryptographicanomaly.json
* Update cryptographicanomaly.json
* Fix switching port showing
Fix reading of config file to show active switching ports
* Update alphacoin.json
* Update README.md
fixed typo on line 529 node-statum-pool to node-stratum-pool
* Update README.md
* Valid json to Readme example coin conf
* Minor readme change
* Marked emark as POS type coin since auto detection fails
* created infinitecoin.conf
Adding infinitecoin.conf to the coin list.
https://github.com/infinitecoin/infinitecoin/blob/master/src/main.cpp#L2524
* fixed capitolization
Adjusted InfiniteCoin to Infinitecoin to match the naming practice of all the other coins.
* update symbol for Coino
* Wrong hashrate calculation
Hashes are not bytes:
> 1 byte = 8 bits
> 1 kilobyte = 1024 bytes
Hashes are the units, and so therefore - as `k`, `m` only mean *10^3 and *10^6
> 1 hash = 1 hash
> 1 kh = 1000 hashes
* Add Viacoin support
Support for Viacoin - viacoin.org
* Add FlutterCoin to coins
* Rename viacoin.conf to viacoin.json
* 42 coin
* Update poolWorker.js
var created above is spelled as initalPool not initialPool.
* http://poollo.com "service unavailable"
* Removed the links to dead pools
* http://poollo.com
* http://fixminer.com
* http://pool.trademybit.com/ (all wallets disabled)
* http://www.omargpools.ca/pools.html (dead link)
* http://mining.theminingpools.com (now called "ad depo", no sign of a mining pool).
* http://minr.es (dead link)
* http://onebtcplace.com (link leads nowhere)
* http://uberpools.org (Apache Test Page)
* http://miningwith.us (domain for sale)
* http://teamdoge.com (blank page)
* http://rapidhash.net (domain for sale)
* http://chunkypools.com
Left these three. Are they still nomp?
* http://miningpoolhub.com (Donations to this project are going directly to TheSerapher, the original author of this project. (https://github.com/MPOS/php-mpos) ###
* http://hashfaster.com (MPOS, sign up) ###
* http://suchpool.pw ###
* Removed " LiveChains UK offers full hosting, setup and management of NOMP pools with several different configurations. [...] LiveChains UK however does offer this feature as part of there own customised NOMP called LivePool." Paid Solution." from 'Paid Solution'. The company no longer exists and the links lead to a generic type of forum about software.
* Added some log info and fixed a typo
* Pinned some package versions - included async package
Should fix https://github.com/zone117x/node-open-mining-portal/pull/479
* Update litecoin testnet magic
Litecoin testnet has been reset
2fcf8079ef (diff-64cbe1ad5465e13bc59ee8bb6f3de2e7R207)
* fix orphan stat source
* API does not set the proper header
* Update api.js
Fix : #554
* Update package.json
Pin request npm package version
* Update README.md
* Update README.md
* Updated to new Core 0.16 Format (getaddressinfo)
Since the new Wallet release of Bitcoin Core, they introduced a new Validation format for addresses.
Validateaddress is not longer working.
* Update paymentProcessor.js
* Final change
* Added GLT Coin
* add peer magic for DGB (#592)
542 lines
22 KiB
JavaScript
542 lines
22 KiB
JavaScript
var fs = require('fs');
|
|
|
|
var redis = require('redis');
|
|
var async = require('async');
|
|
|
|
var Stratum = require('stratum-pool');
|
|
var util = require('stratum-pool/lib/util.js');
|
|
|
|
|
|
module.exports = function(logger){
|
|
|
|
var poolConfigs = JSON.parse(process.env.pools);
|
|
|
|
var enabledPools = [];
|
|
|
|
Object.keys(poolConfigs).forEach(function(coin) {
|
|
var poolOptions = poolConfigs[coin];
|
|
if (poolOptions.paymentProcessing &&
|
|
poolOptions.paymentProcessing.enabled)
|
|
enabledPools.push(coin);
|
|
});
|
|
|
|
async.filter(enabledPools, function(coin, callback){
|
|
SetupForPool(logger, poolConfigs[coin], function(setupResults){
|
|
callback(setupResults);
|
|
});
|
|
}, function(coins){
|
|
coins.forEach(function(coin){
|
|
|
|
var poolOptions = poolConfigs[coin];
|
|
var processingConfig = poolOptions.paymentProcessing;
|
|
var logSystem = 'Payments';
|
|
var logComponent = coin;
|
|
|
|
logger.debug(logSystem, logComponent, 'Payment processing setup to run every '
|
|
+ processingConfig.paymentInterval + ' second(s) with daemon ('
|
|
+ processingConfig.daemon.user + '@' + processingConfig.daemon.host + ':' + processingConfig.daemon.port
|
|
+ ') and redis (' + poolOptions.redis.host + ':' + poolOptions.redis.port + ')');
|
|
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
function SetupForPool(logger, poolOptions, setupFinished){
|
|
|
|
|
|
var coin = poolOptions.coin.name;
|
|
var processingConfig = poolOptions.paymentProcessing;
|
|
|
|
var logSystem = 'Payments';
|
|
var logComponent = coin;
|
|
|
|
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){
|
|
logger[severity](logSystem, logComponent, message);
|
|
});
|
|
var redisClient = redis.createClient(poolOptions.redis.port, poolOptions.redis.host);
|
|
|
|
var magnitude;
|
|
var minPaymentSatoshis;
|
|
var coinPrecision;
|
|
|
|
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) {
|
|
daemon.cmd('getaddressinfo', [poolOptions.address], function(result) {
|
|
if (result.error){
|
|
logger.error(logSystem, logComponent, 'Error with payment processing daemon, getaddressinfo failed ... ' + 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);
|
|
}
|
|
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. Tried parsing: ' + result.data);
|
|
callback(true);
|
|
}
|
|
|
|
}, true, true);
|
|
}
|
|
], function(err){
|
|
if (err){
|
|
setupFinished(false);
|
|
return;
|
|
}
|
|
paymentInterval = setInterval(function(){
|
|
try {
|
|
processPayments();
|
|
} catch(e){
|
|
throw e;
|
|
}
|
|
}, processingConfig.paymentInterval * 1000);
|
|
setTimeout(processPayments, 100);
|
|
setupFinished(true);
|
|
});
|
|
|
|
|
|
|
|
|
|
var satoshisToCoins = function(satoshis){
|
|
return parseFloat((satoshis / magnitude).toFixed(coinPrecision));
|
|
};
|
|
|
|
var coinsToSatoshies = function(coins){
|
|
return coins * magnitude;
|
|
};
|
|
|
|
/* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy
|
|
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
|
|
|
var processPayments = function(){
|
|
|
|
var startPaymentProcess = Date.now();
|
|
|
|
var timeSpentRPC = 0;
|
|
var timeSpentRedis = 0;
|
|
|
|
var startTimeRedis;
|
|
var startTimeRPC;
|
|
|
|
var startRedisTimer = function(){ startTimeRedis = Date.now() };
|
|
var endRedisTimer = function(){ timeSpentRedis += Date.now() - startTimeRedis };
|
|
|
|
var startRPCTimer = function(){ startTimeRPC = Date.now(); };
|
|
var endRPCTimer = function(){ timeSpentRPC += Date.now() - startTimeRedis };
|
|
|
|
async.waterfall([
|
|
|
|
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
|
|
blocks. */
|
|
function(callback){
|
|
|
|
startRedisTimer();
|
|
redisClient.multi([
|
|
['hgetall', coin + ':balances'],
|
|
['smembers', coin + ':blocksPending']
|
|
]).exec(function(error, results){
|
|
endRedisTimer();
|
|
|
|
if (error){
|
|
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
|
|
callback(true);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
var workers = {};
|
|
for (var w in results[0]){
|
|
workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))};
|
|
}
|
|
|
|
var rounds = results[1].map(function(r){
|
|
var details = r.split(':');
|
|
return {
|
|
blockHash: details[0],
|
|
txHash: details[1],
|
|
height: details[2],
|
|
serialized: r
|
|
};
|
|
});
|
|
|
|
callback(null, workers, rounds);
|
|
});
|
|
},
|
|
|
|
/* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
|
|
It also adds the block reward amount to the round object - which the daemon gives also gives us. */
|
|
function(workers, rounds, callback){
|
|
|
|
var batchRPCcommand = rounds.map(function(r){
|
|
return ['gettransaction', [r.txHash]];
|
|
});
|
|
|
|
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
|
|
|
|
startRPCTimer();
|
|
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
|
|
endRPCTimer();
|
|
|
|
if (error || !txDetails){
|
|
logger.error(logSystem, logComponent, 'Check finished - daemon rpc error with batch gettransactions '
|
|
+ JSON.stringify(error));
|
|
callback(true);
|
|
return;
|
|
}
|
|
|
|
var addressAccount;
|
|
|
|
txDetails.forEach(function(tx, i){
|
|
|
|
if (i === txDetails.length - 1){
|
|
addressAccount = tx.result;
|
|
return;
|
|
}
|
|
|
|
var round = rounds[i];
|
|
|
|
if (tx.error && tx.error.code === -5){
|
|
logger.warning(logSystem, logComponent, 'Daemon reports invalid transaction: ' + round.txHash);
|
|
round.category = 'kicked';
|
|
return;
|
|
}
|
|
else if (!tx.result.details || (tx.result.details && tx.result.details.length === 0)){
|
|
logger.warning(logSystem, logComponent, 'Daemon reports no details for transaction: ' + round.txHash);
|
|
round.category = 'kicked';
|
|
return;
|
|
}
|
|
else if (tx.error || !tx.result){
|
|
logger.error(logSystem, logComponent, 'Odd error with gettransaction ' + round.txHash + ' '
|
|
+ JSON.stringify(tx));
|
|
return;
|
|
}
|
|
|
|
var generationTx = tx.result.details.filter(function(tx){
|
|
return tx.address === poolOptions.address;
|
|
})[0];
|
|
|
|
|
|
if (!generationTx && tx.result.details.length === 1){
|
|
generationTx = tx.result.details[0];
|
|
}
|
|
|
|
if (!generationTx){
|
|
logger.error(logSystem, logComponent, 'Missing output details to pool address for transaction '
|
|
+ round.txHash);
|
|
return;
|
|
}
|
|
|
|
round.category = generationTx.category;
|
|
if (round.category === 'generate') {
|
|
round.reward = generationTx.amount || generationTx.value;
|
|
}
|
|
|
|
});
|
|
|
|
var canDeleteShares = function(r){
|
|
for (var i = 0; i < rounds.length; i++){
|
|
var compareR = rounds[i];
|
|
if ((compareR.height === r.height)
|
|
&& (compareR.category !== 'kicked')
|
|
&& (compareR.category !== 'orphan')
|
|
&& (compareR.serialized !== r.serialized)){
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
|
|
//Filter out all rounds that are immature (not confirmed or orphaned yet)
|
|
rounds = rounds.filter(function(r){
|
|
switch (r.category) {
|
|
case 'orphan':
|
|
case 'kicked':
|
|
r.canDeleteShares = canDeleteShares(r);
|
|
case 'generate':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
callback(null, workers, rounds, addressAccount);
|
|
|
|
});
|
|
},
|
|
|
|
|
|
/* Does a batch redis call to get shares contributed to each round. Then calculates the reward
|
|
amount owned to each miner for each round. */
|
|
function(workers, rounds, addressAccount, callback){
|
|
|
|
|
|
var shareLookups = rounds.map(function(r){
|
|
return ['hgetall', coin + ':shares:round' + r.height]
|
|
});
|
|
|
|
startRedisTimer();
|
|
redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
|
|
endRedisTimer();
|
|
|
|
if (error){
|
|
callback('Check finished - redis error with multi get rounds share');
|
|
return;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
switch (round.category){
|
|
case 'kicked':
|
|
case 'orphan':
|
|
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;
|
|
}
|
|
});
|
|
|
|
callback(null, workers, rounds, addressAccount);
|
|
});
|
|
},
|
|
|
|
|
|
/* Calculate if any payments are ready to be sent and trigger them sending
|
|
Get balance different for each address and pass it along as object of latest balances such as
|
|
{worker1: balance1, worker2, balance2}
|
|
when deciding the sent balance, it the difference should be -1*amount they had in db,
|
|
if not sending the balance, the differnce should be +(the amount they earned this round)
|
|
*/
|
|
function(workers, rounds, addressAccount, callback) {
|
|
|
|
var 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;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(addressAmounts).length === 0){
|
|
callback(null, workers, rounds);
|
|
return;
|
|
}
|
|
|
|
daemon.cmd('sendmany', [addressAccount || '', addressAmounts], function (result) {
|
|
//Check if payments failed because wallet doesn't have enough coins to pay for tx fees
|
|
if (result.error && result.error.code === -6) {
|
|
var higherPercent = withholdPercent + 0.01;
|
|
logger.warning(logSystem, logComponent, 'Not enough funds to cover the tx fees for sending out payments, decreasing rewards by '
|
|
+ (higherPercent * 100) + '% and retrying');
|
|
trySend(higherPercent);
|
|
}
|
|
else if (result.error) {
|
|
logger.error(logSystem, logComponent, 'Error trying to send payments with RPC 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');
|
|
}
|
|
callback(null, workers, rounds);
|
|
}
|
|
}, true, true);
|
|
};
|
|
trySend(0);
|
|
|
|
},
|
|
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([
|
|
'hincrbyfloat',
|
|
coin + ':balances',
|
|
w,
|
|
satoshisToCoins(worker.balanceChange)
|
|
]);
|
|
}
|
|
if (worker.sent !== 0){
|
|
workerPayoutsCommand.push(['hincrbyfloat', coin + ':payouts', w, worker.sent]);
|
|
totalPaid += worker.sent;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var movePendingCommands = [];
|
|
var roundsToDelete = [];
|
|
var orphanMergeCommands = [];
|
|
|
|
var moveSharesToCurrent = function(r){
|
|
var workerShares = r.workerShares;
|
|
Object.keys(workerShares).forEach(function(worker){
|
|
orphanMergeCommands.push(['hincrby', coin + ':shares:roundCurrent',
|
|
worker, workerShares[worker]]);
|
|
});
|
|
};
|
|
|
|
rounds.forEach(function(r){
|
|
|
|
switch(r.category){
|
|
case 'kicked':
|
|
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksKicked', r.serialized]);
|
|
case 'orphan':
|
|
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksOrphaned', r.serialized]);
|
|
if (r.canDeleteShares){
|
|
moveSharesToCurrent(r);
|
|
roundsToDelete.push(coin + ':shares:round' + r.height);
|
|
}
|
|
return;
|
|
case 'generate':
|
|
movePendingCommands.push(['smove', coin + ':blocksPending', coin + ':blocksConfirmed', r.serialized]);
|
|
roundsToDelete.push(coin + ':shares:round' + r.height);
|
|
return;
|
|
}
|
|
|
|
});
|
|
|
|
var finalRedisCommands = [];
|
|
|
|
if (movePendingCommands.length > 0)
|
|
finalRedisCommands = finalRedisCommands.concat(movePendingCommands);
|
|
|
|
if (orphanMergeCommands.length > 0)
|
|
finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);
|
|
|
|
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));
|
|
|
|
if (totalPaid !== 0)
|
|
finalRedisCommands.push(['hincrbyfloat', coin + ':stats', 'totalPaid', totalPaid]);
|
|
|
|
if (finalRedisCommands.length === 0){
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
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();
|
|
});
|
|
}
|
|
|
|
], function(){
|
|
|
|
var paymentProcessTime = Date.now() - startPaymentProcess;
|
|
logger.debug(logSystem, logComponent, 'Finished interval - time spent: '
|
|
+ paymentProcessTime + 'ms total, ' + timeSpentRedis + 'ms redis, '
|
|
+ timeSpentRPC + 'ms daemon RPC');
|
|
|
|
});
|
|
};
|
|
|
|
|
|
var getProperAddress = function(address){
|
|
if (address.length === 40){
|
|
return util.addressFromEx(poolOptions.address, address);
|
|
}
|
|
else return address;
|
|
};
|
|
|
|
|
|
}
|