diff --git a/config.json b/config.json index f602d64..c32b5ca 100644 --- a/config.json +++ b/config.json @@ -20,7 +20,7 @@ "enabled": true, "siteTitle": "Cryppit", "port": 80, - "statUpdateInterval": 5, + "statUpdateInterval": 1.5, "hashrateWindow": 600 }, "proxy": { diff --git a/init.js b/init.js index 972f554..2b915eb 100644 --- a/init.js +++ b/init.js @@ -2,6 +2,7 @@ var fs = require('fs'); var os = require('os'); var cluster = require('cluster'); +require('./libs/algoProperties.js'); var async = require('async'); var posix = require('posix'); @@ -183,7 +184,7 @@ var startPaymentProcessor = function(poolConfigs){ var enabledForAny = false; for (var pool in poolConfigs){ var p = poolConfigs[pool]; - var enabled = p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; + var enabled = !p.disabled && p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; if (enabled){ enabledForAny = true; break; diff --git a/libs/algoProperties.js b/libs/algoProperties.js new file mode 100644 index 0000000..13dbd58 --- /dev/null +++ b/libs/algoProperties.js @@ -0,0 +1,4 @@ +global.algos = { + +}; +//lets put all algo related properties in here as a global object. also put this in stratum module then borrow it for the portal. \ No newline at end of file diff --git a/libs/logUtil.js b/libs/logUtil.js index 294b351..7d25275 100644 --- a/libs/logUtil.js +++ b/libs/logUtil.js @@ -4,6 +4,8 @@ var colors = require('colors'); var severityToColor = function(severity, text) { switch(severity) { + case 'special': + return text.cyan.underline; case 'debug': return text.green; case 'warning': @@ -19,7 +21,8 @@ var severityToColor = function(severity, text) { var severityValues = { 'debug': 1, 'warning': 2, - 'error': 3 + 'error': 3, + 'special': 4 }; diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 315b34d..e9156e1 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -40,11 +40,13 @@ function SetupForPool(logger, poolOptions){ daemon.cmd('validateaddress', [poolOptions.address], function(result){ if (!result[0].response || !result[0].response.ismine){ - logger.error(logSystem, logComponent, 'Daemon does not own pool address - payment processing can not be done with this daemon'); + logger.error(logSystem, logComponent, + 'Daemon does not own pool address - payment processing can not be done with this daemon'); } }); }).once('connectionFailed', function(error){ - logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error)); + logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + + JSON.stringify(error)); }).on('error', function(error){ logger.error(logSystem, logComponent); }).init(); @@ -73,6 +75,15 @@ function SetupForPool(logger, poolOptions){ connectToRedis(); + /* Number.toFixed gives us the decimal places we want, but as a string. parseFloat turns it back into number + we don't care about trailing zeros in this case. */ + var toPrecision = function(value, precision){ + return parseFloat(value.toFixed(precision)); + }; + + + /* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy + when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */ var processPayments = function(){ async.waterfall([ @@ -120,13 +131,15 @@ function SetupForPool(logger, poolOptions){ daemon.batchCmd(batchRPCcommand, function(error, txDetails){ if (error || !txDetails){ - callback('Check finished - daemon rpc error with batch gettransactions ' + JSON.stringify(error)); + callback('Check finished - daemon rpc error with batch gettransactions ' + + JSON.stringify(error)); return; } txDetails = txDetails.filter(function(tx){ if (tx.error || !tx.result){ - logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(tx)); + logger.error(logSystem, logComponent, + 'error with requesting transaction from block daemon: ' + JSON.stringify(tx)); return false; } return true; @@ -135,11 +148,13 @@ function SetupForPool(logger, poolOptions){ var magnitude; + //Filter out all rounds that are immature (not confirmed or orphaned yet) rounds = rounds.filter(function(r){ var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0]; if (!tx){ - logger.error(logSystem, logComponent, 'daemon did not give us back a transaction that we asked for: ' + r.txHash); + logger.error(logSystem, logComponent, + 'daemon did not give us back a transaction that we asked for: ' + r.txHash); return; } @@ -148,15 +163,25 @@ function SetupForPool(logger, poolOptions){ if (r.category === 'generate'){ r.amount = tx.result.amount; + /* Here we calculate the smallest unit in this coin's currency; the 'satoshi'. + The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the + rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore, + we simply divide the two to get the magnitude. I don't know math, there is probably + a better term than 'magnitude'. Sue me or do a pull request to fix it. */ var roundMagnitude = r.reward / r.amount; + if (!magnitude){ magnitude = roundMagnitude; if (roundMagnitude % 10 !== 0) - logger.error(logSystem, logComponent, 'Satosihis in coin is not divisible by 10 which is very odd'); + logger.error(logSystem, logComponent, + 'Satosihis in coin is not divisible by 10 which is very odd'); } else if (magnitude != roundMagnitude){ - logger.error(logSystem, logComponent, 'Magnitude in a round was different than in another round. HUGE PROBLEM.'); + /* 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; } @@ -200,12 +225,18 @@ function SetupForPool(logger, poolOptions){ var workerShares = allWorkerShares[i]; if (round.category === 'orphan'){ + /* Each block that gets orphaned, all the shares go into the current round so that miners + still get a reward for their work. This seems unfair to those that just started mining + during this current round, but over time it balances out and rewards loyal miners. */ Object.keys(workerShares).forEach(function(worker){ - orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', worker, workerShares[worker]]); + 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 + we owe each miner based on the shares they submitted during that block round. */ var reward = round.reward * (1 - processingConfig.feePercent); var totalShares = Object.keys(workerShares).reduce(function(p, c){ @@ -272,6 +303,8 @@ function SetupForPool(logger, poolOptions){ var balanceUpdateCommands = []; var workerPayoutsCommand = []; + /* Here we add up all workers' previous unpaid balances plus their current rewards as we are + about to check if they reach the payout threshold. */ for (var worker in workerRewards){ workerPayments[worker] = ((workerPayments[worker] || 0) + workerRewards[worker]); } @@ -279,16 +312,32 @@ function SetupForPool(logger, poolOptions){ workerPayments[worker] = ((workerPayments[worker] || 0) + workerBalances[worker]); } + /* Here we check if any of the workers reached their payout threshold, or delete them from the + pending payment ledger (the workerPayments object). */ if (Object.keys(workerPayments).length > 0){ var coinPrecision = magnitude.toString().length - 1; for (var worker in workerPayments){ if (workerPayments[worker] < processingConfig.minimumPayment * magnitude){ - balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, workerRewards[worker]]); + /* The workers total earnings (balance + current reward) was not enough to warrant + a transaction, so we will store their balance in the database. Next time they + are rewarded it might reach the payout threshold. */ + balanceUpdateCommands.push([ + 'hincrby', + coin + '_balances', + worker, + workerRewards[worker] + ]); delete workerPayments[worker]; } else{ + //If worker had a balance that is about to be paid out, subtract it from the database if (workerBalances[worker] !== 0){ - balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, -1 * workerBalances[worker]]); + balanceUpdateCommands.push([ + 'hincrby', + coin + '_balances', + worker, + -1 * workerBalances[worker] + ]); } var rewardInPrecision = (workerRewards[worker] / magnitude).toFixed(coinPrecision); workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', worker, rewardInPrecision]); @@ -299,19 +348,23 @@ function SetupForPool(logger, poolOptions){ } // txfee included in feeAmountToBeCollected - var feeAmountToBeCollected = parseFloat((toBePaid / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var leftOver = toBePaid / (1 - processingConfig.feePercent); + var feeAmountToBeCollected = toPrecision(leftOver * processingConfig.feePercent, coinPrecision); var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected; var minReserveSatoshis = processingConfig.minimumReserve * magnitude; if (balanceLeftOver < minReserveSatoshis){ - - callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid + - ' and collect ' + feeAmountToBeCollected + ' as fees' + + /* TODO: Need to convert all these variables into whole coin units before displaying because + humans aren't good at reading satoshi units. */ + callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' + + toBePaid + ' and collect ' + feeAmountToBeCollected + ' as fees' + ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver + ', needs to be at least ' + minReserveSatoshis); return; } + /* Move pending blocks into either orphan for confirmed sets, and delete their no longer + required round/shares data. */ var movePendingCommands = []; var roundsToDelete = []; rounds.forEach(function(r){ @@ -357,7 +410,7 @@ function SetupForPool(logger, poolOptions){ var finalizeRedisTx = function(){ redisClient.multi(finalRedisCommands).exec(function(error, results){ if (error){ - callback('Check finished - error with final redis commands for cleaning up ' + JSON.stringify(error)); + callback('Error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } logger.debug(logSystem, logComponent, 'Payments processing performed an interval'); @@ -369,22 +422,27 @@ function SetupForPool(logger, poolOptions){ } else{ + //This is how many decimal places to round a coin down to var coinPrecision = magnitude.toString().length - 1; var addressAmounts = {}; var totalAmountUnits = 0; for (var address in workerPayments){ - var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision)); + var coinUnits = toPrecision(workerPayments[address] / magnitude, coinPrecision); addressAmounts[address] = coinUnits; totalAmountUnits += coinUnits; } - logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); + logger.debug(logSystem, logComponent, 'Payments to be sent to: ' + JSON.stringify(addressAmounts)); + daemon.cmd('sendmany', ['', addressAmounts], function(results){ + if (results[0].error){ callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error)); return; } + finalizeRedisTx(); + var totalWorkers = Object.keys(workerPayments).length; logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol + ' was sent to ' + totalWorkers + ' miners');