Payment processing seems to be working well
This commit is contained in:
parent
fc56d94092
commit
4a651dec89
@ -1,4 +1,4 @@
|
|||||||
# NOMP
|
# NOMP 
|
||||||
#### Node Open Mining Portal
|
#### Node Open Mining Portal
|
||||||
|
|
||||||
This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written
|
This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written
|
||||||
|
|||||||
@ -62,9 +62,9 @@ function SetupForPool(logger, poolOptions){
|
|||||||
redisClient.on('ready', function(){
|
redisClient.on('ready', function(){
|
||||||
clearTimeout(reconnectTimeout);
|
clearTimeout(reconnectTimeout);
|
||||||
logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
|
logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
|
||||||
}).on('error', function(err){
|
})/*).on('error', function(err){
|
||||||
logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err))
|
logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err))
|
||||||
}).on('end', function(){
|
})*/.on('end', function(){
|
||||||
logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
|
logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
|
||||||
logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
|
logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
|
||||||
reconnectTimeout = setTimeout(function(){
|
reconnectTimeout = setTimeout(function(){
|
||||||
@ -86,6 +86,10 @@ function SetupForPool(logger, poolOptions){
|
|||||||
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
||||||
|
|
||||||
var processPayments = function(){
|
var processPayments = function(){
|
||||||
|
|
||||||
|
|
||||||
|
var startPaymentProcess = Date.now();
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
|
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
|
||||||
@ -108,10 +112,10 @@ function SetupForPool(logger, poolOptions){
|
|||||||
var details = r.split(':');
|
var details = r.split(':');
|
||||||
return {
|
return {
|
||||||
category: details[0].category,
|
category: details[0].category,
|
||||||
txHash: details[0],
|
solution: details[0],
|
||||||
height: details[1],
|
txHash: details[1],
|
||||||
reward: details[2],
|
height: details[2],
|
||||||
solution: details[3],
|
reward: details[3],
|
||||||
serialized: r
|
serialized: r
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -120,6 +124,29 @@ function SetupForPool(logger, poolOptions){
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
});
|
||||||
|
},*/
|
||||||
|
|
||||||
/* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
|
/* 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. */
|
It also adds the block reward amount to the round object - which the daemon gives also gives us. */
|
||||||
@ -138,23 +165,39 @@ function SetupForPool(logger, poolOptions){
|
|||||||
}
|
}
|
||||||
|
|
||||||
txDetails.forEach(function(tx, i){
|
txDetails.forEach(function(tx, i){
|
||||||
if (tx.error && tx.error.code === -5){
|
var round = rounds[i];
|
||||||
/* Block was dropped from coin daemon even after it happily accepted it earlier.
|
|
||||||
Must be a bug in the daemon code or maybe only something that happens in testnet.
|
|
||||||
We handle this by treating it like an orphaned block. */
|
|
||||||
logger.error(logSystem, logComponent,
|
|
||||||
'Daemon dropped a previously valid block - we are treating it as an orphaned block');
|
|
||||||
|
|
||||||
//These changes to the tx will convert it from dropped to orphan
|
if (tx.error && tx.error.code === -5){
|
||||||
tx.result = {
|
|
||||||
txid: rounds[i].txHash,
|
/* Block was dropped from coin daemon even after it happily accepted it earlier. */
|
||||||
details: [{category: 'orphan'}]
|
|
||||||
};
|
//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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (tx.error || !tx.result){
|
else if (tx.error || !tx.result){
|
||||||
logger.error(logSystem, logComponent,
|
logger.error(logSystem, logComponent,
|
||||||
'error with requesting transaction from block daemon: ' + JSON.stringify(tx));
|
'error with requesting transaction from block daemon: ' + JSON.stringify(tx));
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
round.category = tx.result.details[0].category;
|
||||||
|
if (round.category === 'generate')
|
||||||
|
round.amount = tx.result.amount;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -162,44 +205,37 @@ function SetupForPool(logger, poolOptions){
|
|||||||
|
|
||||||
//Filter out all rounds that are immature (not confirmed or orphaned yet)
|
//Filter out all rounds that are immature (not confirmed or orphaned yet)
|
||||||
rounds = rounds.filter(function(r){
|
rounds = rounds.filter(function(r){
|
||||||
var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0];
|
switch (r.category) {
|
||||||
|
|
||||||
if (!tx){
|
case 'generate':
|
||||||
logger.error(logSystem, logComponent,
|
/* Here we calculate the smallest unit in this coin's currency; the 'satoshi'.
|
||||||
'daemon did not give us back a transaction that we asked for: ' + r.txHash);
|
The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the
|
||||||
return;
|
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;
|
||||||
|
|
||||||
r.category = tx.result.details[0].category;
|
if (!magnitude) {
|
||||||
|
magnitude = roundMagnitude;
|
||||||
|
|
||||||
if (r.category === 'generate'){
|
if (roundMagnitude % 10 !== 0)
|
||||||
r.amount = tx.result.amount;
|
logger.error(logSystem, logComponent,
|
||||||
|
'Satosihis in coin is not divisible by 10 which is very odd');
|
||||||
/* 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
|
else if (magnitude != roundMagnitude) {
|
||||||
rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore,
|
/* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
|
||||||
we simply divide the two to get the magnitude. I don't know math, there is probably
|
100,000,000 satoshis in one coin unit. */
|
||||||
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,
|
logger.error(logSystem, logComponent,
|
||||||
'Satosihis in coin is not divisible by 10 which is very odd');
|
'Magnitude in a round was different than in another round. HUGE PROBLEM.');
|
||||||
}
|
}
|
||||||
else if (magnitude != roundMagnitude){
|
return true;
|
||||||
/* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
|
|
||||||
100,000,000 satoshis in one coin unit. */
|
|
||||||
logger.error(logSystem, logComponent,
|
|
||||||
'Magnitude in a round was different than in another round. HUGE PROBLEM.');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (r.category === 'orphan')
|
|
||||||
return true;
|
|
||||||
|
|
||||||
|
case 'dropkicked':
|
||||||
|
case 'orphan':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -236,32 +272,42 @@ function SetupForPool(logger, poolOptions){
|
|||||||
rounds.forEach(function(round, i){
|
rounds.forEach(function(round, i){
|
||||||
var workerShares = allWorkerShares[i];
|
var workerShares = allWorkerShares[i];
|
||||||
|
|
||||||
if (round.category === 'orphan'){
|
if (!workerShares){
|
||||||
/* Each block that gets orphaned, all the shares go into the current round so that miners
|
logger.error(logSystem, logComponent, 'No worker shares for round: '
|
||||||
still get a reward for their work. This seems unfair to those that just started mining
|
+ round.height + ' solution: ' + round.solution);
|
||||||
during this current round, but over time it balances out and rewards loyal miners. */
|
return;
|
||||||
Object.keys(workerShares).forEach(function(worker){
|
|
||||||
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
|
|
||||||
worker, workerShares[worker]]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if (round.category === 'generate'){
|
|
||||||
|
|
||||||
/* We found a confirmed block! Now get the reward for it and calculate how much
|
switch (round.category){
|
||||||
we owe each miner based on the shares they submitted during that block round. */
|
case 'orphan':
|
||||||
var reward = round.reward * (1 - processingConfig.feePercent);
|
/* 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;
|
||||||
|
|
||||||
var totalShares = Object.keys(workerShares).reduce(function(p, c){
|
case 'generate':
|
||||||
return p + parseInt(workerShares[c])
|
/* We found a confirmed block! Now get the reward for it and calculate how much
|
||||||
}, 0);
|
we owe each miner based on the shares they submitted during that block round. */
|
||||||
|
var reward = round.reward * (1 - processingConfig.feePercent);
|
||||||
|
|
||||||
for (var worker in workerShares){
|
var totalShares = Object.keys(workerShares).reduce(function(p, c){
|
||||||
var percent = parseInt(workerShares[worker]) / totalShares;
|
return p + parseInt(workerShares[c])
|
||||||
var workerRewardTotal = Math.floor(reward * percent);
|
}, 0);
|
||||||
if (!(worker in workerRewards)) workerRewards[worker] = 0;
|
|
||||||
workerRewards[worker] += workerRewardTotal;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
|
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
|
||||||
@ -380,7 +426,14 @@ function SetupForPool(logger, poolOptions){
|
|||||||
var movePendingCommands = [];
|
var movePendingCommands = [];
|
||||||
var roundsToDelete = [];
|
var roundsToDelete = [];
|
||||||
rounds.forEach(function(r){
|
rounds.forEach(function(r){
|
||||||
var destinationSet = r.category === 'orphan' ? '_blocksOrphaned' : '_blocksConfirmed';
|
|
||||||
|
var destinationSet = (function(){
|
||||||
|
switch(r.category){
|
||||||
|
case 'orphan': return '_blocksOrphaned';
|
||||||
|
case 'generate': return '_blocksConfirmed';
|
||||||
|
case 'dropkicked': return '_blocksDropKicked';
|
||||||
|
}
|
||||||
|
})();
|
||||||
movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
|
movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
|
||||||
roundsToDelete.push(coin + '_shares:round' + r.height)
|
roundsToDelete.push(coin + '_shares:round' + r.height)
|
||||||
});
|
});
|
||||||
@ -477,11 +530,15 @@ function SetupForPool(logger, poolOptions){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
], function(error, result){
|
], function(error, result){
|
||||||
|
|
||||||
|
|
||||||
|
var paymentProcessTime = Date.now() - startPaymentProcess;
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
logger.debug(logSystem, logComponent, error);
|
logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + error);
|
||||||
|
|
||||||
else{
|
else{
|
||||||
logger.debug(logSystem, logComponent, result);
|
logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + result);
|
||||||
// not sure if we need some time to let daemon update the wallet balance
|
// not sure if we need some time to let daemon update the wallet balance
|
||||||
setTimeout(withdrawalProfit, 1000);
|
setTimeout(withdrawalProfit, 1000);
|
||||||
}
|
}
|
||||||
@ -523,7 +580,13 @@ function SetupForPool(logger, poolOptions){
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
setInterval(processPayments, processingConfig.paymentInterval * 1000);
|
setInterval(function(){
|
||||||
|
try {
|
||||||
|
processPayments();
|
||||||
|
} catch(e){
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, processingConfig.paymentInterval * 1000);
|
||||||
setTimeout(processPayments, 100);
|
setTimeout(processPayments, 100);
|
||||||
|
|
||||||
};
|
};
|
||||||
@ -69,7 +69,7 @@ module.exports = function(logger, poolConfig){
|
|||||||
|
|
||||||
if (isValidBlock){
|
if (isValidBlock){
|
||||||
redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]);
|
redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]);
|
||||||
redisCommands.push(['sadd', coin + '_blocksPending', shareData.tx + ':' + shareData.height + ':' + shareData.reward + ':' + shareData.solution]);
|
redisCommands.push(['sadd', coin + '_blocksPending', [shareData.solution, shareData.tx, shareData.height, shareData.reward].join(':')]);
|
||||||
redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]);
|
redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]);
|
||||||
}
|
}
|
||||||
else if (shareData.solution){
|
else if (shareData.solution){
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon.png"/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css">
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css">
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/0.2.0/Chart.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/0.2.0/Chart.min.js"></script>
|
||||||
|
|||||||
BIN
website/static/favicon.png
Normal file
BIN
website/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 413 B |
Loading…
Reference in New Issue
Block a user