From ceb6f0dae07ec821a748731a9e5dcf2e7d5b790e Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 22 Mar 2014 13:58:51 -0600 Subject: [PATCH 001/150] More stats added --- README.md | 2 +- libs/api.js | 1 + libs/paymentProcessor.js | 9 ++++--- libs/stats.js | 52 ++++++++++++++++++++++++++++------------ libs/website.js | 3 +++ 5 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 libs/api.js diff --git a/README.md b/README.md index d7dba05..e16f974 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR Credits ------- * [vekexasia](https://github.com/vekexasia) - co-developer & great tester -* [TheSeven](https://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful and king gentleman +* [TheSeven](https://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * Those that contributed to [node-stratum](/zone117x/node-stratum) diff --git a/libs/api.js b/libs/api.js new file mode 100644 index 0000000..84636c4 --- /dev/null +++ b/libs/api.js @@ -0,0 +1 @@ +//create the stats object in here. then let the website use this object. that way we can have a config for stats and website separate :D \ No newline at end of file diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index ab1bde7..087b02c 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -335,7 +335,7 @@ function SetupForPool(logger, poolOptions){ ); finalRedisCommands.push(deleteRoundsCommand); - finalRedisCommands.push(['hincrby', coin + '_stats', 'totalPaid', toBePaid]); + finalRedisCommands.push(['hincrby', coin + '_stats', 'totalPaid', toBePaid / magnitude]); callback(null, magnitude, workerPayments, finalRedisCommands); @@ -356,18 +356,21 @@ function SetupForPool(logger, poolOptions){ console.log(JSON.stringify(workerPayments, null, 4)); console.log(JSON.stringify(sendManyCmd, null, 4)); - //return callback('not yet...'); daemon.cmd('sendmany', sendManyCmd, function(results){ if (results[0].error){ callback('done - error with sendmany ' + JSON.stringify(results[0].error)); return; } + + //This does the final all-or-nothing atom transaction if block deamon sent payments redisClient.multi(finalRedisCommands).exec(function(error, results){ if (error){ callback('done - error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } - callback(null, 'Payments sent'); + var totalWorkers = Object.keys(workerPayments).length; + var totalAmount = Object.keys(workerPayments).reduce(function(p, c){return p + workerPayments[c]}, 0); + callback(null, 'Payments sent, a total of ' + totalAmount + ' was sent to ' + totalWorkers); }); }); diff --git a/libs/stats.js b/libs/stats.js index 0493e20..4fe73a9 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -35,26 +35,37 @@ module.exports = function(logger, portalConfig, poolConfigs){ this.stats = {}; + this.statsString = ''; this.getStats = function(callback){ - var allCoinStats = []; + var allCoinStats = {}; async.each(redisClients, function(client, callback){ var windowTime = (((Date.now() / 1000) - portalConfig.website.hashrateWindow) | 0).toString(); var redisCommands = []; - var commandsPerCoin = 4; - //Clear out old hashrate stats for each coin from redis - client.coins.forEach(function(coin){ - redisCommands.push(['zremrangebyscore', coin + '_hashrate', '-inf', '(' + windowTime]); - redisCommands.push(['zrangebyscore', coin + '_hashrate', windowTime, '+inf']); - redisCommands.push(['hgetall', coin + '_stats']); - redisCommands.push(['scard', coin + '_blocksPending']); + + var redisComamndTemplates = [ + ['zremrangebyscore', '_hashrate', '-inf', '(' + windowTime], + ['zrangebyscore', '_hashrate', windowTime, '+inf'], + ['hgetall', '_stats'], + ['scard', '_blocksPending'], + ['scard', '_blocksConfirmed'], + ['scard', '_blocksOrphaned'] + ]; + + var commandsPerCoin = redisComamndTemplates.length; + + client.coins.map(function(coin){ + redisComamndTemplates.map(function(t){ + var clonedTemplates = t.slice(0); + clonedTemplates[1] = coin + clonedTemplates [1]; + redisCommands.push(clonedTemplates); + }); }); - client.client.multi(redisCommands).exec(function(err, replies){ if (err){ console.log('error with getting hashrate stats ' + JSON.stringify(err)); @@ -62,14 +73,20 @@ module.exports = function(logger, portalConfig, poolConfigs){ } else{ for(var i = 0; i < replies.length; i += commandsPerCoin){ + var coinName = client.coins[i / commandsPerCoin | 0]; var coinStats = { - coinName: client.coins[i / commandsPerCoin | 0], + name: coinName, + symbol: poolConfigs[coinName].coin.symbol, + algorithm: poolConfigs[coinName].coin.algorithm, hashrates: replies[i + 1], poolStats: replies[i + 2], - poolPendingBlocks: replies[i + 3] + blocks: { + pending: replies[i + 3], + confirmed: replies[i + 4], + orphaned: replies[i + 5] + } }; - allCoinStats.push(coinStats) - + allCoinStats[coinStats.name] = (coinStats); } callback(); } @@ -89,7 +106,8 @@ module.exports = function(logger, portalConfig, poolConfigs){ pools: allCoinStats }; - allCoinStats.forEach(function(coinStats){ + Object.keys(allCoinStats).forEach(function(coin){ + var coinStats = allCoinStats[coin]; coinStats.workers = {}; coinStats.shares = 0; coinStats.hashrates.forEach(function(ins){ @@ -102,14 +120,18 @@ module.exports = function(logger, portalConfig, poolConfigs){ else coinStats.workers[worker] = workerShares }); - var shareMultiplier = algoMultipliers[poolConfigs[coinStats.coinName].coin.algorithm]; + var shareMultiplier = algoMultipliers[coinStats.algorithm]; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; coinStats.hashrate = hashratePre / 1e3 | 0; delete coinStats.hashrates; + delete coinStats.shares; portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; }); + _this.stats = portalStats; + _this.statsString = JSON.stringify(portalStats); + console.log(JSON.stringify(portalStats, null, 4)); callback(); }); diff --git a/libs/website.js b/libs/website.js index da3ba4a..82a6834 100644 --- a/libs/website.js +++ b/libs/website.js @@ -183,6 +183,9 @@ module.exports = function(logger){ res.end(requestedPage); return; } + case 'stats': + res.end(portalStats.statsString); + return; case 'live_stats': res.writeHead(200, { 'Content-Type': 'text/event-stream', From cb30876b33ee2ef34582c332330da3c8057e5d23 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 22 Mar 2014 20:08:33 -0600 Subject: [PATCH 002/150] More fixes for payment processing --- README.md | 32 ++---- libs/paymentProcessor.js | 243 +++++++++++++++++++++------------------ libs/stats.js | 1 - 3 files changed, 143 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index e16f974..10136e6 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ have different properties and hashing algorithms). It can be used to create a po coins at once. The pools use clustering to load balance across multiple CPU cores. * For reward/payment processing, shares are inserted into Redis (a fast NoSQL key/value store). The PROP (proportional) -reward system is used. Each and every share will be rewarded - even for rounds resulting in orphaned blocks. +reward system is used with [Redis Transactions](http://redis.io/topics/transactions) for secure and super speedy payouts. +Each and every share will be rewarded - even for rounds resulting in orphaned blocks. * This portal does not have user accounts/logins/registrations. Instead, miners simply use their coin address for stratum authentication. A minimalistic HTML5 front-end connects to the portals statistics API to display stats from from each @@ -28,23 +29,11 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. #### Planned Features -* To knock down the barrier to entry for cryptocurrency and mining for those not programmers or tech-origiented, instead -of the "help" page on the website being being confusing for non-techies (when most people see a black command prompt screen -they run away screaming), there will be a simple "Download NOMP Desktop App" to get started mining immediately for your -platform (use javascript to detect platform and default them to the correct one). I will create this app using C# + Mono -so runs with ease on all platforms, and it will have its own github repo that NOMP links to. So a pool operator does a -`git clone --recursive` on NOMP repo, it will download NOMP app executables for each platform. There will be a nomp.ini -file paired with the executable which the pool operator configures to user their NOMP pool's API url. When the NOMP portal -initiates creates a zip for each platform with the nomp.ini inside. When user's download the app, it auto-connects to the -NOMP pool API to get available coins along with the version-byte for each coin so the app can securely generate a local private -key and valid address to mine with. The app pill prompt printing the private key to paper and also enforce a -STRONG (uncrackable) password encryption on the file. -The app will scan their system to get the appropriate mining software - run in background - parse the gibberish (to a noob) output -into something that makes sense. It will also prompt them to download the coins wallet software and import their private key. -When using the app they can choose a unique username that is used -with stratum authentication like "zone117x.mfsm1ckZKTTjDz94KonZZsbZnAbm1UV4BF", so that on a NOMP mobile app, a user can -enter in the NOMP pool and their username in order to see how their mining rig is doing since the API will report stats -back for the address such as hashrate and balance. +* NOMP API - this API be used in several ways. + * The website will use the API to display stats and information about the pool(s) on the portal's front-end website. + * The NOMP Desktop app will use the API to connect to the portal to display a list of available coins to mine + * NOMP server will have to send the desktop app each coin's version-byte so that a wallet (private key & address) can be + generated securely and locally then used to mine on the pool. * To reduce variance for pools just starting out which have little to no hashing power a feature is planned which will allow your own pool to connect upstream to a larger pool server. It will request work from the larger pool then @@ -285,10 +274,13 @@ Description of options: } }, - /* Recommended to have at least two daemon instances running in case one drops out-of-sync or offline. For redundancy, all instances will be polled for block/transaction updates - and be used for submitting blocks. */ + and be used for submitting blocks. Creating a backup daemon involves spawning a daemon + using the "-datadir=/backup" argument which creates a new daemon instance with it's own + RPC config. For more info on this see: + - https://en.bitcoin.it/wiki/Data_directory + - https://en.bitcoin.it/wiki/Running_bitcoind */ "daemons": [ { //Main daemon instance "host": "localhost", diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 087b02c..feac07a 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -100,7 +100,13 @@ function SetupForPool(logger, poolOptions){ var rounds = results.map(function(r){ var details = r.split(':'); - return {txHash: details[0], height: details[1], reward: details[2], serialized: r}; + return { + category: details[0].category, + txHash: details[0], + height: details[1], + reward: details[2], + serialized: r + }; }); callback(null, rounds); @@ -125,45 +131,51 @@ function SetupForPool(logger, poolOptions){ txDetails = txDetails.filter(function(tx){ if (tx.error || !tx.result){ - console.log('error with requesting transaction from block daemon: ' + JSON.stringify(t)); + paymentLogger.error('error with requesting transaction from block daemon: ' + JSON.stringify(t)); return false; } return true; }); - var orphanedRounds = []; - var confirmedRounds = []; - //Rounds that are not confirmed yet are removed from the round array - //We also get reward amount for each block from daemon reply - rounds.forEach(function(r){ + var magnitude; + + rounds = rounds.filter(function(r){ var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0]; if (!tx){ - console.log('daemon did not give us back a transaction that we asked for: ' + r.txHash); + paymentLogger.error('system', 'daemon did not give us back a transaction that we asked for: ' + r.txHash); return; } - r.category = tx.result.details[0].category; - if (r.category === 'orphan'){ - orphanedRounds.push(r); - - } - else if (r.category === 'generate'){ + if (r.category === 'generate'){ r.amount = tx.result.amount; - r.magnitude = r.reward / r.amount; - confirmedRounds.push(r); + + var roundMagnitude = r.reward / r.amount; + if (!magnitude){ + magnitude = roundMagnitude; + + if (roundMagnitude % 10 !== 0) + paymentLogger.error('system', 'Satosihis in coin is not divisible by 10 which is very odd'); + } + else if (magnitude != roundMagnitude){ + paymentLogger.error('system', 'Magnitude in a round was different than in another round. HUGE PROBLEM.'); + } + return true; } + else if (r.category === 'orphan') + return true; }); - if (orphanedRounds.length === 0 && confirmedRounds.length === 0){ - callback('done - no confirmed or orhpaned rounds'); + + if (rounds.length === 0){ + callback('done - no confirmed or orphaned rounds'); } else{ - callback(null, confirmedRounds, orphanedRounds); + callback(null, rounds, magnitude); } }); }, @@ -171,85 +183,62 @@ function SetupForPool(logger, poolOptions){ /* 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(confirmedRounds, orphanedRounds, callback){ - - - var rounds = []; - for (var i = 0; i < orphanedRounds.length; i++) rounds.push(orphanedRounds[i]); - for (var i = 0; i < confirmedRounds.length; i++) rounds.push(confirmedRounds[i]); + function(rounds, magnitude, callback){ var shareLookups = rounds.map(function(r){ return ['hgetall', coin + '_shares:round' + r.height] }); + redisClient.multi(shareLookups).exec(function(error, allWorkerShares){ if (error){ callback('done - redis error with multi get rounds share') return; } - - // Iterate through the beginning of the share results which are for the orphaned rounds - var orphanMergeCommands = [] - for (var i = 0; i < orphanedRounds.length; i++){ - var workerShares = allWorkerShares[i]; - Object.keys(workerShares).forEach(function(worker){ - orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', worker, workerShares[worker]]); - }); - orphanMergeCommands.push([]); - } - - // Iterate through the rest of the share results which are for the worker rewards + var orphanMergeCommands = []; var workerRewards = {}; - for (var i = orphanedRounds.length; i < allWorkerShares.length; i++){ - var round = rounds[i]; + + rounds.forEach(function(round, i){ var workerShares = allWorkerShares[i]; - var reward = round.reward * (1 - processingConfig.feePercent); - - var totalShares = Object.keys(workerShares).reduce(function(p, c){ - return p + parseInt(workerShares[c]) - }, 0); - - - for (var worker in workerShares){ - var percent = parseInt(workerShares[worker]) / totalShares; - var workerRewardTotal = Math.floor(reward * percent); - if (!(worker in workerRewards)) workerRewards[worker] = 0; - workerRewards[worker] += workerRewardTotal; + if (round.category === 'orphan'){ + Object.keys(workerShares).forEach(function(worker){ + orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', worker, workerShares[worker]]); + }); } - } + else if (round.category === 'generate'){ + var reward = round.reward * (1 - processingConfig.feePercent); - //this calculates profit if you wanna see it - /* - var workerTotalRewards = Object.keys(workerRewards).reduce(function(p, c){ - return p + workerRewards[c]; - }, 0); + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); - var poolTotalRewards = rounds.reduce(function(p, c){ - return p + c.amount * c.magnitude; - }, 0); + for (var worker in workerShares){ + var percent = parseInt(workerShares[worker]) / totalShares; + var workerRewardTotal = Math.floor(reward * percent); + if (!(worker in workerRewards)) workerRewards[worker] = 0; + workerRewards[worker] += workerRewardTotal; + } + } + }); - console.log(workerRewards); - console.log('pool profit percent' + ((poolTotalRewards - workerTotalRewards) / poolTotalRewards)); - */ - - callback(null, rounds, workerRewards, orphanMergeCommands); + callback(null, rounds, magnitude, workerRewards, orphanMergeCommands); }); }, /* Does a batch call to redis to get worker existing balances from coin_balances*/ - function(rounds, workerRewards, orphanMergeCommands, callback){ + function(rounds, magnitude, workerRewards, orphanMergeCommands, callback){ var workers = Object.keys(workerRewards); redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){ - if (error){ - callback('done - redis error with multi get balances'); + if (error && workers.length !== 0){ + callback('done - redis error with multi get balances ' + JSON.stringify(error)); return; } @@ -257,11 +246,11 @@ function SetupForPool(logger, poolOptions){ var workerBalances = {}; for (var i = 0; i < workers.length; i++){ - workerBalances[workers[i]] = parseInt(results[i]) || 0; + workerBalances[workers[i]] = (parseInt(results[i]) || 0) * magnitude; } - callback(null, rounds, workerRewards, workerBalances, orphanMergeCommands); + callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances); }); }, @@ -273,9 +262,10 @@ function SetupForPool(logger, poolOptions){ 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(rounds, workerRewards, workerBalances, orphanMergeCommands, callback){ + function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, callback){ + + //number of satoshis in a single coin unit - this can be different for coins so we calculate it :) - var magnitude = rounds[0].magnitude; daemon.cmd('getbalance', [], function(results){ @@ -288,22 +278,29 @@ function SetupForPool(logger, poolOptions){ var workerPayoutsCommand = []; for (var worker in workerRewards){ - workerPayments[worker] = (workerPayments[worker] || 0) + workerRewards[worker]; + workerPayments[worker] = ((workerPayments[worker] || 0) + workerRewards[worker]); } for (var worker in workerBalances){ - workerPayments[worker] = (workerPayments[worker] || 0) + workerBalances[worker]; + workerPayments[worker] = ((workerPayments[worker] || 0) + workerBalances[worker]); } - for (var worker in workerPayments){ - if (workerPayments[worker] < processingConfig.minimumPayment * magnitude){ - balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, workerRewards[worker]]); - delete workerPayments[worker]; - } - else{ - if (workerBalances[worker] !== 0) - balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, -1 * workerBalances[worker]]); - workerPayoutsCommand.push(['hincrby', coin + '_balances', worker, workerRewards[worker]]); - toBePaid += workerPayments[worker]; + + 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]]); + delete workerPayments[worker]; + } + else{ + if (workerBalances[worker] !== 0){ + balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, -1 * workerBalances[worker]]); + } + var rewardInPrecision = (workerRewards[worker] / magnitude).toFixed(coinPrecision); + workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', worker, rewardInPrecision]); + toBePaid += workerPayments[worker]; + } } + } var balanceLeftOver = totalBalance - toBePaid; @@ -318,61 +315,83 @@ function SetupForPool(logger, poolOptions){ var movePendingCommands = []; - var deleteRoundsCommand = ['del']; + var roundsToDelete = []; rounds.forEach(function(r){ var destinationSet = r.category === 'orphan' ? '_blocksOrphaned' : '_blocksConfirmed'; movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]); - deleteRoundsCommand.push(coin + '_shares:round' + r.height) + roundsToDelete.push(coin + '_shares:round' + r.height) }); var finalRedisCommands = []; - finalRedisCommands = finalRedisCommands.concat( - movePendingCommands, - orphanMergeCommands, - balanceUpdateCommands, - workerPayoutsCommand - ); + if (movePendingCommands.length > 0) + finalRedisCommands = finalRedisCommands.concat(movePendingCommands); - finalRedisCommands.push(deleteRoundsCommand); - finalRedisCommands.push(['hincrby', coin + '_stats', 'totalPaid', toBePaid / magnitude]); + 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 (toBePaid !== 0) + finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]); callback(null, magnitude, workerPayments, finalRedisCommands); - }); }, function(magnitude, workerPayments, finalRedisCommands, callback){ - var sendManyCmd = ['', {}]; - for (var address in workerPayments){ - sendManyCmd[1][address] = workerPayments[address] / magnitude; - } - console.log(JSON.stringify(finalRedisCommands, null, 4)); - console.log(JSON.stringify(workerPayments, null, 4)); - console.log(JSON.stringify(sendManyCmd, null, 4)); - - daemon.cmd('sendmany', sendManyCmd, function(results){ - if (results[0].error){ - callback('done - error with sendmany ' + JSON.stringify(results[0].error)); - return; - } - - //This does the final all-or-nothing atom transaction if block deamon sent payments + //This does the final all-or-nothing atom transaction if block deamon sent payments + var finalizeRedisTx = function(){ redisClient.multi(finalRedisCommands).exec(function(error, results){ if (error){ callback('done - error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } + callback(null, 'Payments processing performed an interval'); + }); + }; + + if (Object.keys(workerPayments).length === 0){ + finalizeRedisTx(); + } + else{ + + + var coinPrecision = magnitude.toString().length - 1; + var addressAmounts = {}; + for (var address in workerPayments){ + addressAmounts[address] = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision)); + } + + paymentLogger.debug('system', 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); + daemon.cmd('sendmany', ['', addressAmounts], function(results){ + if (results[0].error){ + callback('done - error with sendmany ' + JSON.stringify(results[0].error)); + return; + } + finalizeRedisTx(); var totalWorkers = Object.keys(workerPayments).length; var totalAmount = Object.keys(workerPayments).reduce(function(p, c){return p + workerPayments[c]}, 0); - callback(null, 'Payments sent, a total of ' + totalAmount + ' was sent to ' + totalWorkers); + paymentLogger.debug('system', 'Payments sent, a total of ' + totalAmount + + ' was sent to ' + totalWorkers + ' miners'); }); - }); + } diff --git a/libs/stats.js b/libs/stats.js index 4fe73a9..87062e7 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -131,7 +131,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ _this.stats = portalStats; _this.statsString = JSON.stringify(portalStats); - console.log(JSON.stringify(portalStats, null, 4)); callback(); }); From ae2bc9867581520f404827bc83827fec69279a8f Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 22 Mar 2014 20:09:26 -0600 Subject: [PATCH 003/150] Readme uppdate --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10136e6..7d1f76a 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. * NOMP API - this API be used in several ways. * The website will use the API to display stats and information about the pool(s) on the portal's front-end website. - * The NOMP Desktop app will use the API to connect to the portal to display a list of available coins to mine - * NOMP server will have to send the desktop app each coin's version-byte so that a wallet (private key & address) can be - generated securely and locally then used to mine on the pool. + * The NOMP Desktop app will use the API to connect to the portal to display a list of available coins to mine and + NOMP server will have to send the desktop app each coin's version-byte so that a wallet (private key & address) can be + generated securely and locally then used to mine on the pool. * To reduce variance for pools just starting out which have little to no hashing power a feature is planned which will allow your own pool to connect upstream to a larger pool server. It will request work from the larger pool then From ad1f4ce3d0869b36ad39a3a02ec1bdcf4f2c0faa Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 23 Mar 2014 00:16:06 -0600 Subject: [PATCH 004/150] Betting logging. Payment processing fixes. --- init.js | 30 +++-- libs/logUtil.js | 123 ++++++++++----------- libs/mposCompatibility.js | 19 ++-- libs/paymentProcessor.js | 80 ++++++-------- libs/poolWorker.js | 42 +++---- libs/shareProcessor.js | 14 ++- libs/website.js | 28 +---- package.json | 3 +- pool_configs/litecoin_testnet_example.json | 2 +- 9 files changed, 146 insertions(+), 195 deletions(-) diff --git a/init.js b/init.js index f5938a2..30f13ff 100644 --- a/init.js +++ b/init.js @@ -17,19 +17,17 @@ JSON.minify = JSON.minify || require("node-json-minify"); var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); -var loggerInstance = new PoolLogger({ +var logger = new PoolLogger({ logLevel: portalConfig.logLevel }); -var logDebug = loggerInstance.logDebug; -var logWarning = loggerInstance.logWarning; -var logError = loggerInstance.logError; + try { require('newrelic'); if (cluster.isMaster) - logDebug('newrelic', 'system', 'New Relic initiated'); + logger.debug('NewRelic', 'Monitor', 'New Relic initiated'); } catch(e) {} @@ -38,7 +36,7 @@ try{ posix.setrlimit('nofile', { soft: 100000, hard: 100000 }); } catch(e){ - logWarning('posix', 'system', '(Safe to ignore) Must be ran as root to increase resource limits'); + logger.warning('POSIX', 'Connection Limit', '(Safe to ignore) Must be ran as root to increase resource limits'); } @@ -47,13 +45,13 @@ if (cluster.isWorker){ switch(process.env.workerType){ case 'pool': - new PoolWorker(loggerInstance); + new PoolWorker(logger); break; case 'paymentProcessor': - new PaymentProcessor(loggerInstance); + new PaymentProcessor(logger); break; case 'website': - new Website(loggerInstance); + new Website(logger); break; } @@ -81,7 +79,7 @@ var buildPoolConfigs = function(){ if (poolOptions.disabled) return; var coinFilePath = 'coins/' + poolOptions.coin; if (!fs.existsSync(coinFilePath)){ - logError(poolOptions.coin, 'system', 'could not find file: ' + coinFilePath); + logger.error('Master', poolOptions.coin, 'could not find file: ' + coinFilePath); return; } @@ -117,7 +115,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ portalConfig : JSON.stringify(portalConfig), }); worker.on('exit', function(code, signal){ - logError('poolWorker', 'system', 'Fork ' + forkId + ' died, spawning replacement worker...'); + logger.error('Master', 'Pool Worker', 'Fork ' + forkId + ' died, spawning replacement worker...'); setTimeout(function(){ createPoolWorker(forkId); }, 2000); @@ -132,7 +130,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ var startWorkerListener = function(poolConfigs){ - var workerListener = new WorkerListener(loggerInstance, poolConfigs); + var workerListener = new WorkerListener(logger, poolConfigs); workerListener.init(); }; @@ -142,7 +140,7 @@ var startBlockListener = function(portalConfig){ //setup block notify here and use IPC to tell appropriate pools var listener = new BlocknotifyListener(portalConfig.blockNotifyListener); listener.on('log', function(text){ - logDebug('blocknotify', 'system', text); + logger.debug('Master', 'Blocknotify', text); }); listener.on('hash', function(message){ @@ -163,7 +161,7 @@ var startRedisBlockListener = function(portalConfig){ var listener = new RedisBlocknotifyListener(portalConfig.redisBlockNotifyListener); listener.on('log', function(text){ - logDebug('blocknotify', 'system', text); + logger.debug('Master', 'blocknotify', text); }).on('hash', function (message) { var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash}; Object.keys(cluster.workers).forEach(function(id) { @@ -180,7 +178,7 @@ var startPaymentProcessor = function(poolConfigs){ pools: JSON.stringify(poolConfigs) }); worker.on('exit', function(code, signal){ - logError('paymentProcessor', 'system', 'Payment processor died, spawning replacement...'); + logger.error('Master', 'Payment Processor', 'Payment processor died, spawning replacement...'); setTimeout(function(){ startPaymentProcessor(poolConfigs); }, 2000); @@ -198,7 +196,7 @@ var startWebsite = function(portalConfig, poolConfigs){ portalConfig: JSON.stringify(portalConfig) }); worker.on('exit', function(code, signal){ - logError('website', 'system', 'Website process died, spawning replacement...'); + logger.error('Master', 'Website', 'Website process died, spawning replacement...'); setTimeout(function(){ startWebsite(portalConfig, poolConfigs); }, 2000); diff --git a/libs/logUtil.js b/libs/logUtil.js index 06803a6..294b351 100644 --- a/libs/logUtil.js +++ b/libs/logUtil.js @@ -1,78 +1,73 @@ var dateFormat = require('dateformat'); -/* -var defaultConfiguration = { - 'default': true, - 'keys': { - 'client' : 'warning', - 'system' : true, - 'submitblock' : true, +var colors = require('colors'); + + +var severityToColor = function(severity, text) { + switch(severity) { + case 'debug': + return text.green; + case 'warning': + return text.yellow; + case 'error': + return text.red; + default: + console.log("Unknown severity " + severity); + return text.italic; } }; -*/ -var severityToInt = function(severity) { - switch(severity) { - case 'debug': - return 10; - case 'warning': - return 20; - case 'error': - return 30; - default: - console.log("Unknown severity "+severity); - return 1000; - } -} -var getSeverityColor = function(severity) { - switch(severity) { - case 'debug': - return 32; - case 'warning': - return 33; - case 'error': - return 31; - default: - console.log("Unknown severity "+severity); - return 31; - } -} +var severityValues = { + 'debug': 1, + 'warning': 2, + 'error': 3 +}; + var PoolLogger = function (configuration) { - var logLevelInt = severityToInt(configuration.logLevel); - // privates - var shouldLog = function(key, severity) { - var severity = severityToInt(severity); - return severity >= logLevelInt; + var logLevelInt = severityValues[configuration.logLevel]; + + + + var log = function(severity, system, component, text, subcat) { + + if (severityValues[severity] < logLevelInt) return; + + if (subcat){ + var realText = subcat; + var realSubCat = text; + text = realText; + subcat = realSubCat; + } + + var entryDesc = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + ' [' + system + ']\t'; + entryDesc = severityToColor(severity, entryDesc); + + var logString = + entryDesc + + ('[' + component + '] ').italic; + + if (subcat) + logString += ('(' + subcat + ') ').bold.grey + + logString += text.grey; + + console.log(logString); + + }; - var log = function(severity, key, poolName, text) { - if (!shouldLog(key, severity)) - return; - - var desc = poolName ? '[' + poolName + '] ' : ''; - console.log( - '\u001b[' + getSeverityColor(severity) + 'm' + - dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + - " [" + key + "]" + '\u001b[39m: ' + "\t" + - desc + text - ); - } - // public - this.logDebug = function(poolName, logKey, text){ - log('debug', logKey, poolName, text); - } - - this.logWarning = function(poolName, logKey, text) { - log('warning', logKey, poolName, text); - } - - this.logError = function(poolName, logKey, text) { - log('error', logKey, poolName, text); - } -} + var _this = this; + Object.keys(severityValues).forEach(function(logType){ + _this[logType] = function(){ + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(logType); + log.apply(this, args); + }; + }); +}; module.exports = PoolLogger; \ No newline at end of file diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index 50d25bf..2417ca3 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -7,6 +7,8 @@ module.exports = function(logger, poolConfig){ var connection; + var logIdentify = 'MPOS'; + function connect(){ connection = mysql.createConnection({ host: mposConfig.host, @@ -17,18 +19,18 @@ module.exports = function(logger, poolConfig){ }); connection.connect(function(err){ if (err) - logger.error('mysql', 'Could not connect to mysql database: ' + JSON.stringify(err)) + logger.error(logIdentify, 'mysql', 'Could not connect to mysql database: ' + JSON.stringify(err)) else{ - logger.debug('mysql', 'Successful connection to MySQL database'); + logger.debug(logIdentify, 'mysql', 'Successful connection to MySQL database'); } }); connection.on('error', function(err){ if(err.code === 'PROTOCOL_CONNECTION_LOST') { - logger.warning('mysql', 'Lost connection to MySQL database, attempting reconnection...'); + logger.warning(logIdentify, 'mysql', 'Lost connection to MySQL database, attempting reconnection...'); connect(); } else{ - logger.error('mysql', 'Database error: ' + JSON.stringify(err)) + logger.error(logIdentify, 'mysql', 'Database error: ' + JSON.stringify(err)) } }); } @@ -41,7 +43,7 @@ module.exports = function(logger, poolConfig){ [workerName], function(err, result){ if (err){ - logger.error('mysql', 'Database error when authenticating worker: ' + + logger.error(logIdentify, 'mysql', 'Database error when authenticating worker: ' + JSON.stringify(err)); authCallback(false); } @@ -74,8 +76,9 @@ module.exports = function(logger, poolConfig){ dbData, function(err, result) { if (err) - logger.error('mysql', 'Insert error when adding share: ' + - JSON.stringify(err)); + logger.error(logIdentify, 'mysql', 'Insert error when adding share: ' + JSON.stringify(err)); + else + logger.debug(logIdentify, 'mysql', 'Share inserted'); } ); }; @@ -86,7 +89,7 @@ module.exports = function(logger, poolConfig){ 'UPDATE `pool_worker` SET `difficulty` = ' + diff + ' WHERE `username` = ' + connection.escape(workerName), function(err, result){ if (err) - logger.error('mysql', 'Error when updating worker diff: ' + + logger.error(logIdentify, 'mysql', 'Error when updating worker diff: ' + JSON.stringify(err)); else if (result.affectedRows === 0){ connection.query('INSERT INTO `pool_worker` SET ?', {username: workerName, difficulty: diff}); diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index feac07a..16bb5c7 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -25,33 +25,23 @@ function SetupForPool(logger, poolOptions){ if (!processingConfig.enabled) return; - var logIdentify = 'Payment Processor (' + coin + ')'; + var logSystem = 'Payments'; + var logComponent = coin; - var paymentLogger = { - debug: function(key, text){ - logger.logDebug(logIdentify, key, text); - }, - warning: function(key, text){ - logger.logWarning(logIdentify, key, text); - }, - error: function(key, text){ - logger.logError(logIdentify, key, text); - } - }; var daemon = new Stratum.daemon.interface([processingConfig.daemon]); daemon.once('online', function(){ - paymentLogger.debug('system', 'Connected to daemon for payment processing'); + logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing'); daemon.cmd('validateaddress', [poolOptions.address], function(result){ if (!result[0].response.ismine){ - paymentLogger.error('system', '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){ - paymentLogger.error('system', '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){ - paymentLogger.error('system', error); + logger.error(logSystem, logComponent); }).init(); @@ -64,12 +54,12 @@ function SetupForPool(logger, poolOptions){ redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host); redisClient.on('ready', function(){ clearTimeout(reconnectTimeout); - paymentLogger.debug('redis', 'Successfully connected to redis database'); + logger.debug(logSystem, logComponent, 'Successfully connected to redis database'); }).on('error', function(err){ paymentLogger.error('redis', 'Redis client had an error: ' + JSON.stringify(err)) }).on('end', function(){ - paymentLogger.error('redis', 'Connection to redis database as been ended'); - paymentLogger.warning('redis', 'Trying reconnection in 3 seconds...'); + logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); + logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...'); reconnectTimeout = setTimeout(function(){ connectToRedis(); }, 3000); @@ -89,12 +79,12 @@ function SetupForPool(logger, poolOptions){ redisClient.smembers(coin + '_blocksPending', function(error, results){ if (error){ - paymentLogger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error)); - callback('done - redis error for getting blocks'); + logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error)); + callback('check finished - redis error for getting blocks'); return; } if (results.length === 0){ - callback('done - no pending blocks in redis'); + callback('check finished - no pending blocks in redis'); return; } @@ -125,13 +115,13 @@ function SetupForPool(logger, poolOptions){ daemon.batchCmd(batchRPCcommand, function(error, txDetails){ if (error || !txDetails){ - callback('done - 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){ - paymentLogger.error('error with requesting transaction from block daemon: ' + JSON.stringify(t)); + logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(t)); return false; } return true; @@ -144,7 +134,7 @@ function SetupForPool(logger, poolOptions){ var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0]; if (!tx){ - paymentLogger.error('system', '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; } @@ -158,10 +148,10 @@ function SetupForPool(logger, poolOptions){ magnitude = roundMagnitude; if (roundMagnitude % 10 !== 0) - paymentLogger.error('system', '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){ - paymentLogger.error('system', 'Magnitude in a round was different than in another round. HUGE PROBLEM.'); + logger.error(logSystem, logComponent, 'Magnitude in a round was different than in another round. HUGE PROBLEM.'); } return true; } @@ -172,7 +162,7 @@ function SetupForPool(logger, poolOptions){ if (rounds.length === 0){ - callback('done - no confirmed or orphaned rounds'); + callback('check finished - no confirmed or orphaned blocks found'); } else{ callback(null, rounds, magnitude); @@ -193,7 +183,7 @@ function SetupForPool(logger, poolOptions){ redisClient.multi(shareLookups).exec(function(error, allWorkerShares){ if (error){ - callback('done - redis error with multi get rounds share') + callback('check finished - redis error with multi get rounds share') return; } @@ -238,7 +228,7 @@ function SetupForPool(logger, poolOptions){ redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){ if (error && workers.length !== 0){ - callback('done - redis error with multi get balances ' + JSON.stringify(error)); + callback('check finished - redis error with multi get balances ' + JSON.stringify(error)); return; } @@ -246,7 +236,7 @@ function SetupForPool(logger, poolOptions){ var workerBalances = {}; for (var i = 0; i < workers.length; i++){ - workerBalances[workers[i]] = (parseInt(results[i]) || 0) * magnitude; + workerBalances[workers[i]] = (parseInt(results[i]) || 0); } @@ -307,7 +297,7 @@ function SetupForPool(logger, poolOptions){ var minReserveSatoshis = processingConfig.minimumReserve * magnitude; if (balanceLeftOver < minReserveSatoshis){ - callback('done - payments would wipe out minimum reserve, tried to pay out ' + toBePaid + + callback('check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid + ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver + ', needs to be at least ' + minReserveSatoshis); return; @@ -355,12 +345,11 @@ function SetupForPool(logger, poolOptions){ function(magnitude, workerPayments, finalRedisCommands, callback){ - //This does the final all-or-nothing atom transaction if block deamon sent payments var finalizeRedisTx = function(){ redisClient.multi(finalRedisCommands).exec(function(error, results){ if (error){ - callback('done - error with final redis commands for cleaning up ' + JSON.stringify(error)); + callback('check finished - error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } callback(null, 'Payments processing performed an interval'); @@ -372,36 +361,35 @@ function SetupForPool(logger, poolOptions){ } else{ - var coinPrecision = magnitude.toString().length - 1; var addressAmounts = {}; + var totalAmountUnits = 0; for (var address in workerPayments){ - addressAmounts[address] = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision)); + var coiUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));; + addressAmounts[address] = coiUnits; + totalAmountUnits += coiUnits; } - paymentLogger.debug('system', 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); + logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); daemon.cmd('sendmany', ['', addressAmounts], function(results){ if (results[0].error){ - callback('done - error with sendmany ' + JSON.stringify(results[0].error)); + callback('check finished - error with sendmany ' + JSON.stringify(results[0].error)); return; } finalizeRedisTx(); var totalWorkers = Object.keys(workerPayments).length; - var totalAmount = Object.keys(workerPayments).reduce(function(p, c){return p + workerPayments[c]}, 0); - paymentLogger.debug('system', 'Payments sent, a total of ' + totalAmount + + logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' was sent to ' + totalWorkers + ' miners'); }); } - - } ], function(error, result){ if (error) - paymentLogger.debug('system', error) + logger.debug(logSystem, logComponent, error); else{ - paymentLogger.debug('system', result); + logger.debug(logSystem, logComponent, result); withdrawalProfit(); } }); @@ -420,11 +408,11 @@ function SetupForPool(logger, poolOptions){ if (leftOverBalance < processingConfig.minimumReserve || withdrawalAmount < processingConfig.feeWithdrawalThreshold){ - paymentLogger.debug('system', 'Not enough profit to withdrawal yet'); + logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); } else{ //Need to figure out how much of the balance is profit... ??? - paymentLogger.debug('system', 'Can send profit'); + logger.debug(logSystem, logComponent, 'Can send profit'); } }); diff --git a/libs/poolWorker.js b/libs/poolWorker.js index a4083a4..85e1acb 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -16,7 +16,6 @@ module.exports = function(logger){ var forkId = process.env.forkId; var pools = {}; - var varDiffsInstances = {}; // contains all the vardiffs for the profit switching pool var proxyStuff = {} //Handle messages from master process sent via IPC @@ -51,19 +50,10 @@ module.exports = function(logger){ var poolOptions = poolConfigs[coin]; - var logIdentify = 'Pool Fork ' + forkId + ' (' + coin + ')'; + var logSystem = 'Pool'; + var logComponent = coin; + var logSubCat = 'Fork ' + forkId; - var poolLogger = { - debug: function(key, text){ - logger.logDebug(logIdentify, key, text); - }, - warning: function(key, text){ - logger.logWarning(logIdentify, key, text); - }, - error: function(key, text){ - logger.logError(logIdentify, key, text); - } - }; var handlers = { auth: function(){}, @@ -75,7 +65,7 @@ module.exports = function(logger){ //Functions required for MPOS compatibility if (shareProcessing.mpos && shareProcessing.mpos.enabled){ - var mposCompat = new MposCompatibility(poolLogger, poolOptions) + var mposCompat = new MposCompatibility(logger, poolOptions) handlers.auth = function(workerName, password, authCallback){ mposCompat.handleAuth(workerName, password, authCallback); @@ -93,7 +83,7 @@ module.exports = function(logger){ //Functions required for internal payment processing else if (shareProcessing.internal && shareProcessing.internal.enabled){ - var shareProcessor = new ShareProcessor(poolLogger, poolOptions) + var shareProcessor = new ShareProcessor(logger, poolOptions) handlers.auth = function(workerName, password, authCallback){ pool.daemon.cmd('validateaddress', [workerName], function(results){ @@ -112,7 +102,7 @@ module.exports = function(logger){ var authString = authorized ? 'Authorized' : 'Unauthorized '; - poolLogger.debug('client', authString + ' [' + ip + '] ' + workerName + ':' + password); + logger.debug(logSystem, logComponent, logSubCat, authString + ' ' + workerName + ':' + password + ' [' + ip + ']'); callback({ error: null, authorized: authorized, @@ -122,20 +112,20 @@ module.exports = function(logger){ }; - var pool = Stratum.createPool(poolOptions, authorizeFN); + var pool = Stratum.createPool(poolOptions, authorizeFN, logger); pool.on('share', function(isValidShare, isValidBlock, data){ var shareData = JSON.stringify(data); if (data.solution && !isValidBlock) - poolLogger.debug('client', 'We thought a block solution was found but it was rejected by the daemon, share data: ' + shareData); + logger.debug(logSystem, logComponent, logSubCat, 'We thought a block solution was found but it was rejected by the daemon, share data: ' + shareData); else if (isValidBlock) - poolLogger.debug('client', 'Block found, solution: ' + data.solution); + logger.debug(logSystem, logComponent, logSubCat, 'Block found, solution: ' + data.solution); if (isValidShare) - poolLogger.debug('client', 'Valid share submitted, share data: ' + shareData); + logger.debug(logSystem, logComponent, logSubCat, 'Valid share submitted, share data: ' + shareData); else if (!isValidShare) - poolLogger.debug('client', 'Invalid share submitted, share data: ' + shareData) + logger.debug(logSystem, logComponent, logSubCat, 'Invalid share submitted, share data: ' + shareData) handlers.share(isValidShare, isValidBlock, data) @@ -143,14 +133,8 @@ module.exports = function(logger){ }).on('difficultyUpdate', function(workerName, diff){ handlers.diff(workerName, diff); - }).on('log', function(severity, logKey, logText) { - if (severity == 'debug') { - poolLogger.debug(logKey, logText); - } else if (severity == 'warning') { - poolLogger.warning(logKey, logText); - } else if (severity == 'error') { - poolLogger.error(logKey, logText); - } + }).on('log', function(severity, text) { + logger[severity](logSystem, logComponent, logSubCat, text); }); pool.start(); pools[poolOptions.coin.name.toLowerCase()] = pool; diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 29fb9a5..46825c5 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -20,6 +20,8 @@ module.exports = function(logger, poolConfig){ var redisConfig = internalConfig.redis; var coin = poolConfig.coin.name; + var logSystem = 'Shares'; + var connection; function connect(){ @@ -29,14 +31,14 @@ module.exports = function(logger, poolConfig){ connection = redis.createClient(redisConfig.port, redisConfig.host); connection.on('ready', function(){ clearTimeout(reconnectTimeout); - logger.debug('redis', 'Successfully connected to redis database'); + logger.debug(logSystem, 'redis', 'Successfully connected to redis database'); }); connection.on('error', function(err){ - logger.error('redis', 'Redis client had an error: ' + JSON.stringify(err)) + logger.error(logSystem, 'redis', 'Redis client had an error: ' + JSON.stringify(err)) }); connection.on('end', function(){ - logger.error('redis', 'Connection to redis database as been ended'); - logger.warning('redis', 'Trying reconnection in 3 seconds...'); + logger.error(logSystem, 'redis', 'Connection to redis database as been ended'); + logger.warning(logSystem, 'redis', 'Trying reconnection in 3 seconds...'); reconnectTimeout = setTimeout(function(){ connect(); }, 3000); @@ -75,7 +77,9 @@ module.exports = function(logger, poolConfig){ connection.multi(redisCommands).exec(function(err, replies){ if (err) - logger.error('redis', 'error with share processor multi ' + JSON.stringify(err)); + logger.error(logSystem, 'redis', 'error with share processor multi ' + JSON.stringify(err)); + else + logger.debug(logSystem, 'redis', 'share related data recorded'); }); diff --git a/libs/website.js b/libs/website.js index 82a6834..a018fcf 100644 --- a/libs/website.js +++ b/libs/website.js @@ -43,19 +43,7 @@ module.exports = function(logger){ var portalStats = new stats(logger, portalConfig, poolConfigs); - var logIdentify = 'Website'; - - var websiteLogger = { - debug: function(key, text){ - logger.logDebug(logIdentify, key, text); - }, - warning: function(key, text){ - logger.logWarning(logIdentify, key, text); - }, - error: function(key, text){ - logger.logError(logIdentify, key, text); - } - }; + var logSystem = 'Website'; var pageFiles = { @@ -89,6 +77,7 @@ module.exports = function(logger){ portalConfig: portalConfig }); } + logger.debug(logSystem, 'Stats', 'Website updated to latest stats'); }; @@ -148,17 +137,6 @@ module.exports = function(logger){ var route = function(req, res, next){ var pageId = req.params.page || ''; - /*var requestedPage = getPage(pageId); - if (requestedPage){ - var data = pageTemplates.index({ - page: requestedPage, - selected: pageId, - stats: portalStats.stats, - poolConfigs: poolConfigs, - portalConfig: portalConfig - }); - res.end(data); - }*/ if (pageId in indexesProcessed){ res.end(indexesProcessed[pageId]); } @@ -215,7 +193,7 @@ module.exports = function(logger){ }); app.listen(portalConfig.website.port, function(){ - websiteLogger.debug('system', 'Website started on port ' + portalConfig.website.port); + logger.debug(logSystem, 'Server', 'Website started on port ' + portalConfig.website.port); }); diff --git a/package.json b/package.json index cf62501..af44f45 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "mysql": "*", "async": "*", "express": "*", - "dot": "*" + "dot": "*", + "colors": "*" }, "engines": { "node": ">=0.10" diff --git a/pool_configs/litecoin_testnet_example.json b/pool_configs/litecoin_testnet_example.json index 695cd74..1c73847 100644 --- a/pool_configs/litecoin_testnet_example.json +++ b/pool_configs/litecoin_testnet_example.json @@ -7,7 +7,7 @@ "enabled": true, "validateWorkerAddress": true, "paymentInterval": 10, - "minimumPayment": 0.001, + "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", From 14cd1d00703d1d735c3c3ae71fecb87a89e965fa Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 23 Mar 2014 00:46:59 -0600 Subject: [PATCH 005/150] Added interval for spawning pool forks --- init.js | 16 +++++++++++----- libs/paymentProcessor.js | 18 +++++++++--------- libs/poolWorker.js | 4 ++-- libs/shareProcessor.js | 4 ++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/init.js b/init.js index 30f13ff..0564329 100644 --- a/init.js +++ b/init.js @@ -3,7 +3,7 @@ var os = require('os'); var cluster = require('cluster'); - +var async = require('async'); var posix = require('posix'); var PoolLogger = require('./libs/logUtil.js'); var BlocknotifyListener = require('./libs/blocknotifyListener.js'); @@ -112,19 +112,25 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ workerType : 'pool', forkId : forkId, pools : serializedConfigs, - portalConfig : JSON.stringify(portalConfig), + portalConfig : JSON.stringify(portalConfig) }); worker.on('exit', function(code, signal){ - logger.error('Master', 'Pool Worker', 'Fork ' + forkId + ' died, spawning replacement worker...'); + logger.error('Master', 'PoolSpanwer', 'Fork ' + forkId + ' died, spawning replacement worker...'); setTimeout(function(){ createPoolWorker(forkId); }, 2000); }); }; - for (var i = 0; i < numForks; i++) { + var i = 0; + var spawnInterval = setInterval(function(){ createPoolWorker(i); - } + i++; + if (i === numForks){ + clearInterval(spawnInterval); + logger.debug('Master', 'PoolSpawner', 'Spawned pools for all ' + numForks + ' configured forks'); + } + }, 250); }; diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 16bb5c7..f503d0d 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -80,11 +80,11 @@ function SetupForPool(logger, poolOptions){ if (error){ logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error)); - callback('check finished - redis error for getting blocks'); + callback('Check finished - redis error for getting blocks'); return; } if (results.length === 0){ - callback('check finished - no pending blocks in redis'); + callback('Check finished - no pending blocks in redis'); return; } @@ -115,7 +115,7 @@ 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; } @@ -162,7 +162,7 @@ function SetupForPool(logger, poolOptions){ if (rounds.length === 0){ - callback('check finished - no confirmed or orphaned blocks found'); + callback('Check finished - no confirmed or orphaned blocks found'); } else{ callback(null, rounds, magnitude); @@ -183,7 +183,7 @@ function SetupForPool(logger, poolOptions){ redisClient.multi(shareLookups).exec(function(error, allWorkerShares){ if (error){ - callback('check finished - redis error with multi get rounds share') + callback('Check finished - redis error with multi get rounds share') return; } @@ -228,7 +228,7 @@ function SetupForPool(logger, poolOptions){ redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){ if (error && workers.length !== 0){ - callback('check finished - redis error with multi get balances ' + JSON.stringify(error)); + callback('Check finished - redis error with multi get balances ' + JSON.stringify(error)); return; } @@ -297,7 +297,7 @@ function SetupForPool(logger, poolOptions){ var minReserveSatoshis = processingConfig.minimumReserve * magnitude; if (balanceLeftOver < minReserveSatoshis){ - callback('check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid + + callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid + ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver + ', needs to be at least ' + minReserveSatoshis); return; @@ -349,7 +349,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('Check finished - error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } callback(null, 'Payments processing performed an interval'); @@ -373,7 +373,7 @@ function SetupForPool(logger, poolOptions){ logger.debug(logSystem, logComponent, 'Payments about 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)); + callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error)); return; } finalizeRedisTx(); diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 85e1acb..7a5b438 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -120,10 +120,10 @@ module.exports = function(logger){ if (data.solution && !isValidBlock) logger.debug(logSystem, logComponent, logSubCat, 'We thought a block solution was found but it was rejected by the daemon, share data: ' + shareData); else if (isValidBlock) - logger.debug(logSystem, logComponent, logSubCat, 'Block found, solution: ' + data.solution); + logger.debug(logSystem, logComponent, logSubCat, 'Block solution found: ' + data.solution); if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Valid share submitted, share data: ' + shareData); + logger.debug(logSystem, logComponent, logSubCat, 'Valid share diff of ' + data.difficultiy + ' submitted by worker ' + data.worker + ' [ ' + data.ip + ']' ); else if (!isValidShare) logger.debug(logSystem, logComponent, logSubCat, 'Invalid share submitted, share data: ' + shareData) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 46825c5..b28ac20 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -77,9 +77,9 @@ module.exports = function(logger, poolConfig){ connection.multi(redisCommands).exec(function(err, replies){ if (err) - logger.error(logSystem, 'redis', 'error with share processor multi ' + JSON.stringify(err)); + logger.error(logSystem, 'redis', 'Error with share processor multi ' + JSON.stringify(err)); else - logger.debug(logSystem, 'redis', 'share related data recorded'); + logger.debug(logSystem, 'redis', 'Share data and stats recorded'); }); From f2862598a9fe40e92ff3ba16095207e8a0869770 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 23 Mar 2014 14:18:54 -0600 Subject: [PATCH 006/150] Slightly faster hashrate adding without using math.random --- libs/shareProcessor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index b28ac20..1022858 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -60,7 +60,8 @@ module.exports = function(logger, poolConfig){ /* Stores share diff, worker, and unique value with a score that is the timestamp. Unique value ensures it doesn't overwrite an existing entry, and timestamp as score lets us query shares from last X minutes to generate hashrate for each worker and pool. */ - redisCommands.push(['zadd', coin + '_hashrate', Date.now() / 1000 | 0, [shareData.difficulty, shareData.worker, Math.random()].join(':')]); + var dateNow = Date.now(); + redisCommands.push(['zadd', coin + '_hashrate', dateNow / 1000 | 0, [shareData.difficulty, shareData.worker, dateNow].join(':')]); } else{ redisCommands.push(['hincrby', coin + '_stats', 'invalidShares', 1]); From 1a00efc42abbe60b2a490fb8b508b11e196eb86d Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 24 Mar 2014 14:00:18 -0600 Subject: [PATCH 007/150] Better error message for when payment processor detects that it doesn't own the address. Updated readme with better deamon setup instructions. Removed 'reward' field from coins since we auto-detect that now. --- README.md | 32 +++++++++++++++++++------------- coins/alphacoin.json | 1 - coins/anoncoin.json | 1 - coins/auroracoin.json | 1 - coins/bitcoin.json | 1 - coins/bottlecaps.json | 1 - coins/casinocoin.json | 1 - coins/catcoin.json | 1 - coins/darkcoin.json | 1 - coins/diamondcoin.json | 1 - coins/digibyte.json | 1 - coins/dogecoin.json | 1 - coins/earthcoin.json | 1 - coins/elephantcoin.json | 1 - coins/emerald.json | 1 - coins/ezcoin.json | 1 - coins/fastcoin.json | 1 - coins/flappycoin.json | 1 - coins/florincoin.json | 1 - coins/frankocoin.json | 1 - coins/galaxycoin.json | 1 - coins/gamecoin.json | 1 - coins/hobonickels.json | 6 ++++++ coins/junkcoin.json | 1 - coins/kittehcoin.json | 1 - coins/krugercoin.json | 1 - coins/litecoin.json | 1 - coins/lottocoin.json | 1 - coins/luckycoin.json | 1 - coins/memecoin.json | 1 - coins/neocoin.json | 1 - coins/netcoin.json | 1 - coins/noirbits.json | 1 - coins/peercoin.json | 1 - coins/phoenixcoin.json | 1 - coins/quarkcoin.json | 1 - coins/reddcoin.json | 1 - coins/sexcoin.json | 1 - coins/skeincoin.json | 1 - coins/spots.json | 1 - coins/stablecoin.json | 1 - coins/xencoin.json | 1 - coins/yacoin.json | 1 - init.js | 8 ++++---- libs/paymentProcessor.js | 2 +- libs/poolWorker.js | 7 +++++-- 46 files changed, 35 insertions(+), 61 deletions(-) create mode 100644 coins/hobonickels.json diff --git a/README.md b/README.md index 7d1f76a..46b967a 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,8 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. #### Planned Features -* NOMP API - this API be used in several ways. - * The website will use the API to display stats and information about the pool(s) on the portal's front-end website. - * The NOMP Desktop app will use the API to connect to the portal to display a list of available coins to mine and - NOMP server will have to send the desktop app each coin's version-byte so that a wallet (private key & address) can be - generated securely and locally then used to mine on the pool. +* NOMP API - Used by the website to display stats and information about the pool(s) on the portal's front-end website, +and by the NOMP Desktop app to retrieve a list of available coins (and version-bytes for local wallet/address generation). * To reduce variance for pools just starting out which have little to no hashing power a feature is planned which will allow your own pool to connect upstream to a larger pool server. It will request work from the larger pool then @@ -75,6 +72,21 @@ Usage * [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart)) +#### 0) Setting up coin daemon +Follow the build/install instructions for your coin daemon. Your coin.conf file should end up looking something like this: +``` +daemon=1 +rpcuser=litecoinrpc +rpcpassword=securepassword +rpcport=19332 +``` +For redundancy, its recommended to have at least two daemon instances running in case one drops out-of-sync or offline, +all instances will be polled for block/transaction updates and be used for submitting blocks. Creating a backup daemon +involves spawning a daemon using the "-datadir=/backup" argument which creates a new daemon instance with it's own +config directory and coin.conf file. For more info on this see: + * https://en.bitcoin.it/wiki/Data_directory + * https://en.bitcoin.it/wiki/Running_bitcoind + #### 1) Downloading & Installing Clone the repository and run `npm update` for all the dependencies to be installed: @@ -135,7 +147,6 @@ Here is an example of the required fields: "name": "Litecoin", "symbol": "ltc", "algorithm": "scrypt", //or "sha256", "scrypt-jane", "quark", "x11" - "reward": "POW", //or "POS" "txMessages": false //or true } ```` @@ -274,13 +285,8 @@ Description of options: } }, - /* Recommended to have at least two daemon instances running in case one drops out-of-sync - or offline. For redundancy, all instances will be polled for block/transaction updates - and be used for submitting blocks. Creating a backup daemon involves spawning a daemon - using the "-datadir=/backup" argument which creates a new daemon instance with it's own - RPC config. For more info on this see: - - https://en.bitcoin.it/wiki/Data_directory - - https://en.bitcoin.it/wiki/Running_bitcoind */ + /* For redundancy, recommended to have at least two daemon instances running in case one + drops out-of-sync or offline. */ "daemons": [ { //Main daemon instance "host": "localhost", diff --git a/coins/alphacoin.json b/coins/alphacoin.json index 82c1213..24f3594 100644 --- a/coins/alphacoin.json +++ b/coins/alphacoin.json @@ -2,6 +2,5 @@ "name" : "Alphacoin", "symbol" : "ALF", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/anoncoin.json b/coins/anoncoin.json index be315db..bd917fc 100644 --- a/coins/anoncoin.json +++ b/coins/anoncoin.json @@ -2,6 +2,5 @@ "name" : "Anoncoin", "symbol" : "ANC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/auroracoin.json b/coins/auroracoin.json index 1427596..172acd5 100644 --- a/coins/auroracoin.json +++ b/coins/auroracoin.json @@ -2,6 +2,5 @@ "name" : "Auroracoin", "symbol" : "AUR", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/bitcoin.json b/coins/bitcoin.json index d50db3d..e3870ef 100644 --- a/coins/bitcoin.json +++ b/coins/bitcoin.json @@ -2,6 +2,5 @@ "name": "Bitcoin", "symbol": "btc", "algorithm": "sha256", - "reward": "POW", "txMessages": false } \ No newline at end of file diff --git a/coins/bottlecaps.json b/coins/bottlecaps.json index e62cdb7..da29c1b 100644 --- a/coins/bottlecaps.json +++ b/coins/bottlecaps.json @@ -2,6 +2,5 @@ "name" : "Bottlecaps", "symbol" : "CAP", "algorithm" : "scrypt", - "reward" : "POS", "txMessages" : false } \ No newline at end of file diff --git a/coins/casinocoin.json b/coins/casinocoin.json index f513819..b6b8e8b 100644 --- a/coins/casinocoin.json +++ b/coins/casinocoin.json @@ -2,6 +2,5 @@ "name" : "Casinocoin", "symbol" : "CSC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/catcoin.json b/coins/catcoin.json index cdf24b4..b03af91 100644 --- a/coins/catcoin.json +++ b/coins/catcoin.json @@ -2,6 +2,5 @@ "name" : "Catcoin", "symbol" : "CAT", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/darkcoin.json b/coins/darkcoin.json index cecede4..3120964 100644 --- a/coins/darkcoin.json +++ b/coins/darkcoin.json @@ -2,6 +2,5 @@ "name": "Darkcoin", "symbol": "drk", "algorithm": "x11", - "reward": "POW", "txMessages": false } \ No newline at end of file diff --git a/coins/diamondcoin.json b/coins/diamondcoin.json index d379cd0..838bc96 100644 --- a/coins/diamondcoin.json +++ b/coins/diamondcoin.json @@ -2,6 +2,5 @@ "name" : "Diamondcoin", "symbol" : "DMD", "algorithm" : "scrypt", - "reward" : "POS", "txMessages" : true } \ No newline at end of file diff --git a/coins/digibyte.json b/coins/digibyte.json index 218224e..ef0333d 100644 --- a/coins/digibyte.json +++ b/coins/digibyte.json @@ -2,6 +2,5 @@ "name" : "Digibyte", "symbol" : "DGB", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/dogecoin.json b/coins/dogecoin.json index 5d28a36..f0aa6c8 100644 --- a/coins/dogecoin.json +++ b/coins/dogecoin.json @@ -2,6 +2,5 @@ "name" : "Dogecoin", "symbol" : "DOGE", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/earthcoin.json b/coins/earthcoin.json index 153f48a..4aabbb3 100644 --- a/coins/earthcoin.json +++ b/coins/earthcoin.json @@ -2,6 +2,5 @@ "name" : "Earthcoin", "symbol" : "EAC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/elephantcoin.json b/coins/elephantcoin.json index f6c9620..e1c1fdd 100644 --- a/coins/elephantcoin.json +++ b/coins/elephantcoin.json @@ -2,6 +2,5 @@ "name" : "Elephantcoin", "symbol" : "ELP", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/emerald.json b/coins/emerald.json index 9f3df96..49242fa 100644 --- a/coins/emerald.json +++ b/coins/emerald.json @@ -2,6 +2,5 @@ "name" : "Emerald", "symbol" : "EMD", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/ezcoin.json b/coins/ezcoin.json index 30b06ad..8edc861 100644 --- a/coins/ezcoin.json +++ b/coins/ezcoin.json @@ -2,6 +2,5 @@ "name" : "Ezcoin", "symbol" : "EZC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/fastcoin.json b/coins/fastcoin.json index e64e528..0bf5409 100644 --- a/coins/fastcoin.json +++ b/coins/fastcoin.json @@ -2,6 +2,5 @@ "name" : "Fastcoin", "symbol" : "FST", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/flappycoin.json b/coins/flappycoin.json index 32ccf9a..f6e4a6d 100644 --- a/coins/flappycoin.json +++ b/coins/flappycoin.json @@ -2,6 +2,5 @@ "name" : "Flappycoin", "symbol" : "FLAP", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/florincoin.json b/coins/florincoin.json index 5fd8d28..0fabdcb 100644 --- a/coins/florincoin.json +++ b/coins/florincoin.json @@ -2,6 +2,5 @@ "name" : "Florincoin", "symbol" : "FLO", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : true } \ No newline at end of file diff --git a/coins/frankocoin.json b/coins/frankocoin.json index 2836f27..a649419 100644 --- a/coins/frankocoin.json +++ b/coins/frankocoin.json @@ -2,6 +2,5 @@ "name" : "Frankocoin", "symbol" : "FRK", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/galaxycoin.json b/coins/galaxycoin.json index b3c59c8..8e95913 100644 --- a/coins/galaxycoin.json +++ b/coins/galaxycoin.json @@ -2,6 +2,5 @@ "name" : "Galaxycoin", "symbol" : "GLX", "algorithm" : "scrypt", - "reward" : "POS", "txMessages" : false } \ No newline at end of file diff --git a/coins/gamecoin.json b/coins/gamecoin.json index 122c5e2..a7dc73b 100644 --- a/coins/gamecoin.json +++ b/coins/gamecoin.json @@ -2,6 +2,5 @@ "name" : "Gamecoin", "symbol" : "GME", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/hobonickels.json b/coins/hobonickels.json new file mode 100644 index 0000000..b7eb3fb --- /dev/null +++ b/coins/hobonickels.json @@ -0,0 +1,6 @@ +{ + "name" : "Hobonickels", + "symbol" : "HBN", + "algorithm" : "scrypt", + "txMessages" : false +} \ No newline at end of file diff --git a/coins/junkcoin.json b/coins/junkcoin.json index 9ea8bb7..fe4f8db 100644 --- a/coins/junkcoin.json +++ b/coins/junkcoin.json @@ -2,6 +2,5 @@ "name" : "Junkcoin", "symbol" : "JKC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/kittehcoin.json b/coins/kittehcoin.json index 8a83fff..cf4d776 100644 --- a/coins/kittehcoin.json +++ b/coins/kittehcoin.json @@ -2,6 +2,5 @@ "name" : "Kittehcoin", "symbol" : "MEOW", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/krugercoin.json b/coins/krugercoin.json index b740fc1..7d9a07b 100644 --- a/coins/krugercoin.json +++ b/coins/krugercoin.json @@ -2,6 +2,5 @@ "name" : "Krugercoin", "symbol" : "KGC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/litecoin.json b/coins/litecoin.json index 1b28960..536be58 100644 --- a/coins/litecoin.json +++ b/coins/litecoin.json @@ -2,6 +2,5 @@ "name" : "Litecoin", "symbol" : "LTC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/lottocoin.json b/coins/lottocoin.json index f815453..25d33a0 100644 --- a/coins/lottocoin.json +++ b/coins/lottocoin.json @@ -2,6 +2,5 @@ "name" : "Lottocoin", "symbol" : "LOT", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/luckycoin.json b/coins/luckycoin.json index 8c55231..213986b 100644 --- a/coins/luckycoin.json +++ b/coins/luckycoin.json @@ -2,6 +2,5 @@ "name" : "Luckycoin", "symbol" : "LKY", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/memecoin.json b/coins/memecoin.json index 7a80316..566de59 100644 --- a/coins/memecoin.json +++ b/coins/memecoin.json @@ -2,6 +2,5 @@ "name" : "Memecoin", "symbol" : "MEM", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/neocoin.json b/coins/neocoin.json index 14998c1..316a6f2 100644 --- a/coins/neocoin.json +++ b/coins/neocoin.json @@ -2,6 +2,5 @@ "name" : "Neocoin", "symbol" : "NEC", "algorithm" : "scrypt", - "reward" : "POS", "txMessages" : true } \ No newline at end of file diff --git a/coins/netcoin.json b/coins/netcoin.json index 5f580eb..4554a73 100644 --- a/coins/netcoin.json +++ b/coins/netcoin.json @@ -2,6 +2,5 @@ "name" : "Netcoin", "symbol" : "NET", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : true } \ No newline at end of file diff --git a/coins/noirbits.json b/coins/noirbits.json index 09a81cd..e540b95 100644 --- a/coins/noirbits.json +++ b/coins/noirbits.json @@ -2,6 +2,5 @@ "name" : "Noirbits", "symbol" : "NRB", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/peercoin.json b/coins/peercoin.json index 6485731..428bbad 100644 --- a/coins/peercoin.json +++ b/coins/peercoin.json @@ -2,6 +2,5 @@ "name": "Peercoin", "symbol": "ppc", "algorithm": "sha256", - "reward": "POS", "txMessages": false } \ No newline at end of file diff --git a/coins/phoenixcoin.json b/coins/phoenixcoin.json index a4bcf74..9e3695c 100644 --- a/coins/phoenixcoin.json +++ b/coins/phoenixcoin.json @@ -2,6 +2,5 @@ "name" : "Phoenixcoin", "symbol" : "PXC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/quarkcoin.json b/coins/quarkcoin.json index 660142f..597aeb3 100644 --- a/coins/quarkcoin.json +++ b/coins/quarkcoin.json @@ -2,6 +2,5 @@ "name": "Quarkcoin", "symbol": "qrk", "algorithm": "quark", - "reward": "POW", "txMessages": false } \ No newline at end of file diff --git a/coins/reddcoin.json b/coins/reddcoin.json index 402227d..29a017a 100644 --- a/coins/reddcoin.json +++ b/coins/reddcoin.json @@ -2,6 +2,5 @@ "name" : "Reddcoin", "symbol" : "REDD", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/sexcoin.json b/coins/sexcoin.json index 88d1389..a558019 100644 --- a/coins/sexcoin.json +++ b/coins/sexcoin.json @@ -2,6 +2,5 @@ "name" : "Sexcoin", "symbol" : "SXC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/skeincoin.json b/coins/skeincoin.json index cff9be6..31479be 100644 --- a/coins/skeincoin.json +++ b/coins/skeincoin.json @@ -2,6 +2,5 @@ "name": "Skeincoin", "symbol": "skc", "algorithm": "skein", - "reward": "POW", "txMessages": false } \ No newline at end of file diff --git a/coins/spots.json b/coins/spots.json index ac0eaac..8130747 100644 --- a/coins/spots.json +++ b/coins/spots.json @@ -2,6 +2,5 @@ "name" : "Spots", "symbol" : "SPT", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/stablecoin.json b/coins/stablecoin.json index 5190e7e..404e28a 100644 --- a/coins/stablecoin.json +++ b/coins/stablecoin.json @@ -2,6 +2,5 @@ "name" : "Stablecoin", "symbol" : "SBC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/xencoin.json b/coins/xencoin.json index f6b4d15..d961dd2 100644 --- a/coins/xencoin.json +++ b/coins/xencoin.json @@ -2,6 +2,5 @@ "name" : "Xencoin", "symbol" : "XNC", "algorithm" : "scrypt", - "reward" : "POW", "txMessages" : false } \ No newline at end of file diff --git a/coins/yacoin.json b/coins/yacoin.json index b4a0422..932d8ce 100644 --- a/coins/yacoin.json +++ b/coins/yacoin.json @@ -2,6 +2,5 @@ "name": "Yacoin", "symbol": "yac", "algorithm": "scrypt-jane", - "reward": "POS", "txMessages": false } \ No newline at end of file diff --git a/init.js b/init.js index 0564329..4b7870b 100644 --- a/init.js +++ b/init.js @@ -109,10 +109,10 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ var createPoolWorker = function(forkId){ var worker = cluster.fork({ - workerType : 'pool', - forkId : forkId, - pools : serializedConfigs, - portalConfig : JSON.stringify(portalConfig) + workerType: 'pool', + forkId: forkId, + pools: serializedConfigs, + portalConfig: JSON.stringify(portalConfig) }); worker.on('exit', function(code, signal){ logger.error('Master', 'PoolSpanwer', 'Fork ' + forkId + ' died, spawning replacement worker...'); diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index f503d0d..b0b391e 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -34,7 +34,7 @@ function SetupForPool(logger, poolOptions){ logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing'); daemon.cmd('validateaddress', [poolOptions.address], function(result){ - if (!result[0].response.ismine){ + 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'); } }); diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 7a5b438..dd39636 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -119,13 +119,16 @@ module.exports = function(logger){ if (data.solution && !isValidBlock) logger.debug(logSystem, logComponent, logSubCat, 'We thought a block solution was found but it was rejected by the daemon, share data: ' + shareData); + else if (isValidBlock) logger.debug(logSystem, logComponent, logSubCat, 'Block solution found: ' + data.solution); + if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Valid share diff of ' + data.difficultiy + ' submitted by worker ' + data.worker + ' [ ' + data.ip + ']' ); + logger.debug(logSystem, logComponent, logSubCat, 'Valid share of difficulty ' + data.difficulty + ' by ' + data.worker + ' [' + data.ip + ']' ); + else if (!isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Invalid share submitted, share data: ' + shareData) + logger.debug(logSystem, logComponent, logSubCat, 'Invalid share submitted, share data: ' + shareData); handlers.share(isValidShare, isValidBlock, data) From 82844687ac6a53e720fc7f3f30b835f247d09168 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 25 Mar 2014 12:52:11 -0600 Subject: [PATCH 008/150] Fixed blockNotify.js script which was using wrong indexes for arguments. --- coins/bitcoin.json | 2 +- coins/darkcoin.json | 2 +- coins/hirocoin.json | 6 ++++++ coins/peercoin.json | 2 +- coins/quarkcoin.json | 2 +- coins/skeincoin.json | 2 +- coins/yacoin.json | 2 +- libs/stats.js | 2 +- scripts/blockNotify.js | 8 ++++---- 9 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 coins/hirocoin.json diff --git a/coins/bitcoin.json b/coins/bitcoin.json index e3870ef..93b1061 100644 --- a/coins/bitcoin.json +++ b/coins/bitcoin.json @@ -1,6 +1,6 @@ { "name": "Bitcoin", - "symbol": "btc", + "symbol": "BTC", "algorithm": "sha256", "txMessages": false } \ No newline at end of file diff --git a/coins/darkcoin.json b/coins/darkcoin.json index 3120964..cb970d5 100644 --- a/coins/darkcoin.json +++ b/coins/darkcoin.json @@ -1,6 +1,6 @@ { "name": "Darkcoin", - "symbol": "drk", + "symbol": "DRK", "algorithm": "x11", "txMessages": false } \ No newline at end of file diff --git a/coins/hirocoin.json b/coins/hirocoin.json new file mode 100644 index 0000000..197d366 --- /dev/null +++ b/coins/hirocoin.json @@ -0,0 +1,6 @@ +{ + "name": "Hirocoin", + "symbol": "hic", + "algorithm": "x11", + "txMessages": false +} \ No newline at end of file diff --git a/coins/peercoin.json b/coins/peercoin.json index 428bbad..12470ed 100644 --- a/coins/peercoin.json +++ b/coins/peercoin.json @@ -1,6 +1,6 @@ { "name": "Peercoin", - "symbol": "ppc", + "symbol": "PPC", "algorithm": "sha256", "txMessages": false } \ No newline at end of file diff --git a/coins/quarkcoin.json b/coins/quarkcoin.json index 597aeb3..084ae78 100644 --- a/coins/quarkcoin.json +++ b/coins/quarkcoin.json @@ -1,6 +1,6 @@ { "name": "Quarkcoin", - "symbol": "qrk", + "symbol": "QRK", "algorithm": "quark", "txMessages": false } \ No newline at end of file diff --git a/coins/skeincoin.json b/coins/skeincoin.json index 31479be..283834d 100644 --- a/coins/skeincoin.json +++ b/coins/skeincoin.json @@ -1,6 +1,6 @@ { "name": "Skeincoin", - "symbol": "skc", + "symbol": "SKC", "algorithm": "skein", "txMessages": false } \ No newline at end of file diff --git a/coins/yacoin.json b/coins/yacoin.json index 932d8ce..8f2f03f 100644 --- a/coins/yacoin.json +++ b/coins/yacoin.json @@ -1,6 +1,6 @@ { "name": "Yacoin", - "symbol": "yac", + "symbol": "YAC", "algorithm": "scrypt-jane", "txMessages": false } \ No newline at end of file diff --git a/libs/stats.js b/libs/stats.js index 87062e7..eb42833 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -76,7 +76,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ var coinName = client.coins[i / commandsPerCoin | 0]; var coinStats = { name: coinName, - symbol: poolConfigs[coinName].coin.symbol, + symbol: poolConfigs[coinName].coin.symbol.toUpperCase(), algorithm: poolConfigs[coinName].coin.algorithm, hashrates: replies[i + 1], poolStats: replies[i + 2], diff --git a/scripts/blockNotify.js b/scripts/blockNotify.js index ea61d2c..f43dd0f 100644 --- a/scripts/blockNotify.js +++ b/scripts/blockNotify.js @@ -7,13 +7,13 @@ This script will then send the blockhash along with other information to a liste */ var net = require('net'); -var config = process.argv[1]; +var config = process.argv[2]; var parts = config.split(':'); var host = parts[0]; var port = parts[1]; -var password = process.argv[2]; -var coin = process.argv[3]; -var blockHash = process.argv[4]; +var password = process.argv[3]; +var coin = process.argv[4]; +var blockHash = process.argv[5]; var client = net.connect(port, host, function() { console.log('client connected'); From 72c26213a19451b971b1a19203cad233bed52f09 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 25 Mar 2014 16:21:17 -0600 Subject: [PATCH 009/150] Updated git info for node-stratum-pool and refactored website/api structure. --- README.md | 6 +-- libs/api.js | 40 +++++++++++++++- libs/stats.js | 4 +- libs/website.js | 53 +++++++--------------- package.json | 2 +- pool_configs/litecoin_testnet_example.json | 2 +- website/static/main.js | 2 +- 7 files changed, 64 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 46b967a..4b6995b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ front-end website. #### Features -* For the pool server it uses the highly efficient [node-stratum](https://github.com/zone117x/node-stratum) module which +* For the pool server it uses the highly efficient [node-stratum-pool](https://github.com/zone117x/node-stratum-pool) module which supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, several hashing algorithms. * The portal has an [MPOS](https://github.com/MPOS/php-mpos) compatibility mode so that the it can @@ -328,7 +328,7 @@ Description of options: You can create as many of these pool config files as you want (such as one pool per coin you which to operate). If you are creating multiple pools, ensure that they have unique stratum ports. -For more information on these configuration options see the [pool module documentation](https://github.com/zone117x/node-stratum#module-usage) +For more information on these configuration options see the [pool module documentation](https://github.com/zone117x/node-stratum-pool#module-usage) @@ -373,7 +373,7 @@ Credits ------- * [vekexasia](https://github.com/vekexasia) - co-developer & great tester * [TheSeven](https://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman -* Those that contributed to [node-stratum](/zone117x/node-stratum) +* Those that contributed to [node-stratum-pool](/zone117x/node-stratum-pool) License diff --git a/libs/api.js b/libs/api.js index 84636c4..092d9b8 100644 --- a/libs/api.js +++ b/libs/api.js @@ -1 +1,39 @@ -//create the stats object in here. then let the website use this object. that way we can have a config for stats and website separate :D \ No newline at end of file +var redis = require('redis'); +var async = require('async'); + +var stats = require('./stats.js'); + +module.exports = function(logger, portalConfig, poolConfigs){ + + + var _this = this; + + var portalStats = this.stats = new stats(logger, portalConfig, poolConfigs); + + this.liveStatConnections = {}; + + this.handleApiRequest = function(req, res, next){ + switch(req.params.method){ + case 'stats': + res.end(portalStats.statsString); + return; + case 'live_stats': + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + res.write('\n'); + var uid = Math.random().toString(); + _this.liveStatConnections[uid] = res; + req.on("close", function() { + delete _this.liveStatConnections[uid]; + }); + + return; + default: + next(); + } + }; + +}; \ No newline at end of file diff --git a/libs/stats.js b/libs/stats.js index eb42833..177c82a 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -11,7 +11,9 @@ module.exports = function(logger, portalConfig, poolConfigs){ var redisClients = []; var algoMultipliers = { + 'x11': Math.pow(2, 16), 'scrypt': Math.pow(2, 16), + 'scrypt-jane': Math.pow(2,16), 'sha256': Math.pow(2, 32) }; @@ -38,7 +40,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ this.statsString = ''; - this.getStats = function(callback){ + this.getGlobalStats = function(callback){ var allCoinStats = {}; diff --git a/libs/website.js b/libs/website.js index a018fcf..5e6cbfe 100644 --- a/libs/website.js +++ b/libs/website.js @@ -31,7 +31,7 @@ var async = require('async'); var dot = require('dot'); var express = require('express'); -var stats = require('./stats.js'); +var api = require('./api.js'); module.exports = function(logger){ @@ -41,7 +41,8 @@ module.exports = function(logger){ var websiteConfig = portalConfig.website; - var portalStats = new stats(logger, portalConfig, poolConfigs); + var portalApi = new api(logger, portalConfig, poolConfigs); + var portalStats = portalApi.stats; var logSystem = 'Website'; @@ -106,17 +107,17 @@ module.exports = function(logger){ }); - portalStats.getStats(function(){ + portalStats.getGlobalStats(function(){ readPageFiles(Object.keys(pageFiles)); }); var buildUpdatedWebsite = function(){ - portalStats.getStats(function(){ + portalStats.getGlobalStats(function(){ processTemplates(); var statData = 'data: ' + JSON.stringify(portalStats.stats) + '\n\n'; - for (var uid in liveStatConnections){ - var res = liveStatConnections[uid]; + for (var uid in portalApi.liveStatConnections){ + var res = portalApi.liveStatConnections[uid]; res.write(statData); } @@ -146,43 +147,21 @@ module.exports = function(logger){ }; - var liveStatConnections = {}; + app.get('/get_page', function(req, res, next){ + var requestedPage = getPage(req.query.id); + if (requestedPage){ + res.end(requestedPage); + return; + } + next(); + }); app.get('/:page', route); app.get('/', route); app.get('/api/:method', function(req, res, next){ - - switch(req.params.method){ - case 'get_page': - var requestedPage = getPage(req.query.id); - if (requestedPage){ - res.end(requestedPage); - return; - } - case 'stats': - res.end(portalStats.statsString); - return; - case 'live_stats': - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' - }); - res.write('\n'); - var uid = Math.random().toString(); - liveStatConnections[uid] = res; - req.on("close", function() { - delete liveStatConnections[uid]; - }); - - return; - default: - next(); - } - - //res.send('you did api method ' + req.params.method); + portalApi.handleApiRequest(req, res, next); }); app.use('/static', express.static('website/static')); diff --git a/package.json b/package.json index af44f45..c63ae63 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/zone117x/node-open-mining-portal.git" }, "dependencies": { - "stratum-pool": "https://github.com/zone117x/node-stratum/archive/master.tar.gz", + "stratum-pool": "https://github.com/zone117x/node-stratum-pool/archive/master.tar.gz", "dateformat": "*", "node-json-minify": "*", "posix": "*", diff --git a/pool_configs/litecoin_testnet_example.json b/pool_configs/litecoin_testnet_example.json index 1c73847..ba66286 100644 --- a/pool_configs/litecoin_testnet_example.json +++ b/pool_configs/litecoin_testnet_example.json @@ -60,7 +60,7 @@ } }, "3032": { - "diff": 32 + "diff": 16 }, "3256": { "diff": 256 diff --git a/website/static/main.js b/website/static/main.js index df5c7d7..a87d63e 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -5,7 +5,7 @@ $(function(){ if (pushSate) history.pushState(null, null, '/' + page); $('.selected').removeClass('selected'); $('a[href="/' + page + '"]').parent().addClass('selected') - $.get("/api/get_page", {id: page}, function(data){ + $.get("/get_page", {id: page}, function(data){ $('#page').html(data); }, 'html') }; From 572a5ea45b45bdb6267159c158e610cf850c9c27 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 25 Mar 2014 16:41:30 -0600 Subject: [PATCH 010/150] Typo with error message on tx error --- libs/paymentProcessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index b0b391e..3b5c0ac 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -121,7 +121,7 @@ function SetupForPool(logger, poolOptions){ txDetails = txDetails.filter(function(tx){ if (tx.error || !tx.result){ - logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(t)); + logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(tx)); return false; } return true; From add62039dbaaa0cfadad919c0a6572955e949796 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Wed, 26 Mar 2014 05:27:28 +0400 Subject: [PATCH 011/150] fix possible typo in paymentProcessor --- libs/paymentProcessor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 3b5c0ac..aa5d0e9 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -365,9 +365,9 @@ function SetupForPool(logger, poolOptions){ var addressAmounts = {}; var totalAmountUnits = 0; for (var address in workerPayments){ - var coiUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));; - addressAmounts[address] = coiUnits; - totalAmountUnits += coiUnits; + var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));; + addressAmounts[address] = coinUnits; + totalAmountUnits += coinUnits; } logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); From df68d339834048fde95950b871dc90b374093485 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Wed, 26 Mar 2014 19:43:52 +0400 Subject: [PATCH 012/150] add fees withdrawal --- libs/paymentProcessor.js | 37 +++++++++++++++------- pool_configs/litecoin_testnet_example.json | 1 + 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index aa5d0e9..9221cb4 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -257,7 +257,7 @@ function SetupForPool(logger, poolOptions){ //number of satoshis in a single coin unit - this can be different for coins so we calculate it :) - daemon.cmd('getbalance', [], function(results){ + daemon.cmd('getbalance', [''], function(results){ var totalBalance = results[0].response * magnitude; var toBePaid = 0; @@ -365,10 +365,11 @@ function SetupForPool(logger, poolOptions){ var addressAmounts = {}; var totalAmountUnits = 0; for (var address in workerPayments){ - var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));; + var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision)); addressAmounts[address] = coinUnits; totalAmountUnits += coinUnits; } + var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); daemon.cmd('sendmany', ['', addressAmounts], function(results){ @@ -380,9 +381,16 @@ function SetupForPool(logger, poolOptions){ var totalWorkers = Object.keys(workerPayments).length; logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' was sent to ' + totalWorkers + ' miners'); + daemon.cmd('move', ['', processingConfig.feeCollectAccount, feeAmountUnits], function(results){ + if (results[0].error){ + callback('Check finished - error with move ' + JSON.stringify(results[0].error)); + return; + } + var totalWorkers = Object.keys(workerPayments).length; + logger.debug(logSystem, logComponent, feeAmountUnits + ' collected as fee'); + }); }); } - } ], function(error, result){ if (error) @@ -400,19 +408,26 @@ function SetupForPool(logger, poolOptions){ if (!processingConfig.feeWithdrawalThreshold) return; - daemon.cmd('getbalance', [], function(results){ + daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){ - var totalBalance = results[0].response; - var withdrawalAmount = totalBalance - processingConfig.minimumReserve; - var leftOverBalance = totalBalance - withdrawalAmount; + var withdrawalAmount = results[0].response; - - if (leftOverBalance < processingConfig.minimumReserve || withdrawalAmount < processingConfig.feeWithdrawalThreshold){ + if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); } else{ - //Need to figure out how much of the balance is profit... ??? - logger.debug(logSystem, logComponent, 'Can send profit'); + + var withdrawal = {}; + withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount; + + daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ + if (results[0].error){ + logger.debug(logSystem, logComponent, 'Withdrawal profit finished - error with sendmany ' + JSON.stringify(results[0].error)); + return; + } + logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + + ' was sent to ' + processingConfig.feeReceiveAddress); + }); } }); diff --git a/pool_configs/litecoin_testnet_example.json b/pool_configs/litecoin_testnet_example.json index ba66286..6efb22f 100644 --- a/pool_configs/litecoin_testnet_example.json +++ b/pool_configs/litecoin_testnet_example.json @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", "feeWithdrawalThreshold": 5, "daemon": { From 0be59cb34ff97c2b7bb79ef26d5973f29dbfd715 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Wed, 26 Mar 2014 23:59:59 +0400 Subject: [PATCH 013/150] include tx fees and pool fees in minimumReserve check and adjust pool fees accordingly --- libs/paymentProcessor.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 9221cb4..667b1ac 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -27,6 +27,7 @@ function SetupForPool(logger, poolOptions){ var logSystem = 'Payments'; var logComponent = coin; + var paytxfee; var daemon = new Stratum.daemon.interface([processingConfig.daemon]); @@ -38,6 +39,14 @@ function SetupForPool(logger, poolOptions){ logger.error(logSystem, logComponent, 'Daemon does not own pool address - payment processing can not be done with this daemon'); } }); + daemon.cmd('getinfo', [], function(result){ + if (!result[0].response || !result[0].response.paytxfee){ + paytxfee = 0; + logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - payment processing could be broken with this daemon'); + return; + } + paytxfee = result[0].response.paytxfee; + }); }).once('connectionFailed', function(error){ logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error)); }).on('error', function(error){ @@ -293,7 +302,9 @@ function SetupForPool(logger, poolOptions){ } - var balanceLeftOver = totalBalance - toBePaid; + var feeAmountToBeCollected = parseFloat((toBePaid / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var txFee = Object.keys(workerPayments).length * paytxfee; + var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected - txFee; var minReserveSatoshis = processingConfig.minimumReserve * magnitude; if (balanceLeftOver < minReserveSatoshis){ @@ -369,7 +380,6 @@ function SetupForPool(logger, poolOptions){ addressAmounts[address] = coinUnits; totalAmountUnits += coinUnits; } - var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts)); daemon.cmd('sendmany', ['', addressAmounts], function(results){ @@ -381,12 +391,14 @@ function SetupForPool(logger, poolOptions){ var totalWorkers = Object.keys(workerPayments).length; logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' was sent to ' + totalWorkers + ' miners'); - daemon.cmd('move', ['', processingConfig.feeCollectAccount, feeAmountUnits], function(results){ + var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var txFee = totalWorkers * paytxfee; + var collectableFee = feeAmountUnits - txFee; + daemon.cmd('move', ['', processingConfig.feeCollectAccount, collectableFee], function(results){ if (results[0].error){ callback('Check finished - error with move ' + JSON.stringify(results[0].error)); return; } - var totalWorkers = Object.keys(workerPayments).length; logger.debug(logSystem, logComponent, feeAmountUnits + ' collected as fee'); }); }); @@ -410,7 +422,7 @@ function SetupForPool(logger, poolOptions){ daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){ - var withdrawalAmount = results[0].response; + var withdrawalAmount = results[0].response - paytxfee; if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); From 609eb0ae90af65b6ca91ea8a99f6b4f23d7463db Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 26 Mar 2014 15:08:34 -0600 Subject: [PATCH 014/150] Fixed errors that occurred when certain modules were not configured/enabled. --- coins/helixcoin.json | 6 ++ coins/wecoin.json | 6 ++ init.js | 14 ++++ libs/paymentProcessor.js | 9 ++- libs/poolWorker.js | 4 +- libs/stats.js | 14 ++++ pool_configs/litecoin_testnet_example.json | 92 ---------------------- 7 files changed, 49 insertions(+), 96 deletions(-) create mode 100644 coins/helixcoin.json create mode 100644 coins/wecoin.json delete mode 100644 pool_configs/litecoin_testnet_example.json diff --git a/coins/helixcoin.json b/coins/helixcoin.json new file mode 100644 index 0000000..adf4d2e --- /dev/null +++ b/coins/helixcoin.json @@ -0,0 +1,6 @@ +{ + "name" : "Helixcoin", + "symbol" : "HXC", + "algorithm" : "keccak", + "txMessages" : false +} \ No newline at end of file diff --git a/coins/wecoin.json b/coins/wecoin.json new file mode 100644 index 0000000..19e2e17 --- /dev/null +++ b/coins/wecoin.json @@ -0,0 +1,6 @@ +{ + "name" : "Wecoin", + "symbol" : "WEC", + "algorithm" : "keccak", + "txMessages" : false +} \ No newline at end of file diff --git a/init.js b/init.js index 4b7870b..972f554 100644 --- a/init.js +++ b/init.js @@ -179,6 +179,20 @@ var startRedisBlockListener = function(portalConfig){ 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; + if (enabled){ + enabledForAny = true; + break; + } + } + + if (!enabledForAny) + return; + var worker = cluster.fork({ workerType: 'paymentProcessor', pools: JSON.stringify(poolConfigs) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 3b5c0ac..38a14fa 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -19,11 +19,16 @@ module.exports = function(logger){ function SetupForPool(logger, poolOptions){ - var coin = poolOptions.coin.name; + if (!poolOptions.shareProcessing || + poolOptions.shareProcessing.internal || + !poolOptions.shareProcessing.internal.enabled) + return; + + var coin = poolOptions.coin.name; var processingConfig = poolOptions.shareProcessing.internal; - if (!processingConfig.enabled) return; + var logSystem = 'Payments'; var logComponent = coin; diff --git a/libs/poolWorker.js b/libs/poolWorker.js index dd39636..8706fe8 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -64,7 +64,7 @@ module.exports = function(logger){ var shareProcessing = poolOptions.shareProcessing; //Functions required for MPOS compatibility - if (shareProcessing.mpos && shareProcessing.mpos.enabled){ + if (shareProcessing && shareProcessing.mpos && shareProcessing.mpos.enabled){ var mposCompat = new MposCompatibility(logger, poolOptions) handlers.auth = function(workerName, password, authCallback){ @@ -81,7 +81,7 @@ module.exports = function(logger){ } //Functions required for internal payment processing - else if (shareProcessing.internal && shareProcessing.internal.enabled){ + else if (shareProcessing && shareProcessing.internal && shareProcessing.internal.enabled){ var shareProcessor = new ShareProcessor(logger, poolOptions) diff --git a/libs/stats.js b/libs/stats.js index 177c82a..c049c76 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -8,6 +8,8 @@ module.exports = function(logger, portalConfig, poolConfigs){ var _this = this; + var logSystem = 'Stats'; + var redisClients = []; var algoMultipliers = { @@ -17,8 +19,20 @@ module.exports = function(logger, portalConfig, poolConfigs){ 'sha256': Math.pow(2, 32) }; + var canDoStats = true; + Object.keys(poolConfigs).forEach(function(coin){ + + if (!canDoStats) return; + var poolConfig = poolConfigs[coin]; + + if (!poolConfig.shareProcessing || !poolConfig.shareProcessing.internal){ + logger.error(logSystem, coin, 'Cannot do stats without internal share processing setup'); + canDoStats = false; + return; + } + var internalConfig = poolConfig.shareProcessing.internal; var redisConfig = internalConfig.redis; diff --git a/pool_configs/litecoin_testnet_example.json b/pool_configs/litecoin_testnet_example.json deleted file mode 100644 index ba66286..0000000 --- a/pool_configs/litecoin_testnet_example.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "disabled": false, - "coin": "litecoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 10, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19332, - "user": "litecoinrpc", - "password": "testnet" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - - "address": "mfsm1ckZKTTjDz94KonZZsbZnAbm1UV4BF", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3008":{ - "diff": 8, - "varDiff": { - "minDiff": 8, - "maxDiff": 512, - "targetTime": 15, - "retargetTime": 90, - "variancePercent": 30 - } - }, - "3032": { - "diff": 16 - }, - "3256": { - "diff": 256 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 19332, - "user": "litecoinrpc", - "password": "testnet" - }, - { - "host": "localhost", - "port": 19344, - "user": "litecoinrpc", - "password": "testnet" - } - ], - - "p2p": { - "enabled": false, - "host": "localhost", - "port": 19333, - "protocolVersion": 70002, - "magic": "fcc1b7dc" - } -} \ No newline at end of file From aa3b960512fec3edc4470b69a0495f2e5dc9f1ed Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Mar 2014 16:35:55 -0600 Subject: [PATCH 015/150] Added more example configs --- .gitignore | 3 +- pool_configs/darkcoin_example.json | 64 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 pool_configs/darkcoin_example.json diff --git a/.gitignore b/.gitignore index ccd8331..d35bbf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules/ -.idea/ -pool_configs/ \ No newline at end of file +.idea/ \ No newline at end of file diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json new file mode 100644 index 0000000..0ab86a5 --- /dev/null +++ b/pool_configs/darkcoin_example.json @@ -0,0 +1,64 @@ +{ + "disabled": true, + "coin": "darkcoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 18342, + "user": "darkcoinrpc1", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "4073": { + "diff": 0.002 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 18342, + "user": "darkcoinrpc1", + "password": "testpass" + } + ] +} \ No newline at end of file From 20ff1262535a3f272658dc897135db37e8bc84c2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Mar 2014 16:36:26 -0600 Subject: [PATCH 016/150] Added more example configs --- pool_configs/helixcoin_example.json | 33 ++++++++++ pool_configs/hirocoin_example.json | 65 +++++++++++++++++++ pool_configs/hobonickels_example.json | 65 +++++++++++++++++++ pool_configs/litecoin_example.json | 92 +++++++++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 pool_configs/helixcoin_example.json create mode 100644 pool_configs/hirocoin_example.json create mode 100644 pool_configs/hobonickels_example.json create mode 100644 pool_configs/litecoin_example.json diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json new file mode 100644 index 0000000..9732ad5 --- /dev/null +++ b/pool_configs/helixcoin_example.json @@ -0,0 +1,33 @@ +{ + "disabled": false, + "coin": "helixcoin.json", + + "address": "H9xyrh45LzLX4uXCP6jG6ZGrWho8srUgiG", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + + "ports": { + "3737": { + "diff": 0.2 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 16385, + "user": "testuser", + "password": "testpass" + } + ], + + "p2p": { + "enabled": false, + "host": "localhost", + "port": 19333, + "protocolVersion": 70002, + "magic": "fcc1b7dc" + } +} \ No newline at end of file diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json new file mode 100644 index 0000000..295065a --- /dev/null +++ b/pool_configs/hirocoin_example.json @@ -0,0 +1,65 @@ +{ + "disabled": true, + "coin": "hirocoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19389, + "user": "hirocoin", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + + "address": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3073": { + "diff": 0.002 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 19389, + "user": "hirocoin", + "password": "testpass" + } + ] +} \ No newline at end of file diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json new file mode 100644 index 0000000..144fd24 --- /dev/null +++ b/pool_configs/hobonickels_example.json @@ -0,0 +1,65 @@ +{ + "disabled": true, + "coin": "hobonickels.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "EhA4HXF7VPWfnV8TXerAP6p12BiEpXbiYR", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19339, + "user": "hobonickelsrpc", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + + "address": "EzGiarU2S56jyRkYpS7FKssXCS6AAJChdU", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3033": { + "diff": 8 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 19339, + "user": "hobonickelsrpc", + "password": "testpass" + } + ] +} \ No newline at end of file diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json new file mode 100644 index 0000000..54e3a40 --- /dev/null +++ b/pool_configs/litecoin_example.json @@ -0,0 +1,92 @@ +{ + "disabled": true, + "coin": "litecoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19332, + "user": "litecoinrpc", + "password": "testnet" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + + "address": "mfsm1ckZKTTjDz94KonZZsbZnAbm1UV4BF", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3008":{ + "diff": 8, + "varDiff": { + "minDiff": 8, + "maxDiff": 512, + "targetTime": 15, + "retargetTime": 90, + "variancePercent": 30 + } + }, + "3032": { + "diff": 16 + }, + "3256": { + "diff": 256 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 19332, + "user": "litecoinrpc", + "password": "testnet" + }, + { + "host": "localhost", + "port": 19344, + "user": "litecoinrpc", + "password": "testnet" + } + ], + + "p2p": { + "enabled": false, + "host": "localhost", + "port": 19333, + "protocolVersion": 70002, + "magic": "fcc1b7dc" + } +} \ No newline at end of file From a3b3b17d43bb03e492c76a8b2e6acc1990282152 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 03:36:07 +0400 Subject: [PATCH 017/150] fix fee calculations and withdrawals once again --- libs/paymentProcessor.js | 96 +++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 667b1ac..07afffc 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -27,7 +27,6 @@ function SetupForPool(logger, poolOptions){ var logSystem = 'Payments'; var logComponent = coin; - var paytxfee; var daemon = new Stratum.daemon.interface([processingConfig.daemon]); @@ -39,14 +38,6 @@ function SetupForPool(logger, poolOptions){ logger.error(logSystem, logComponent, 'Daemon does not own pool address - payment processing can not be done with this daemon'); } }); - daemon.cmd('getinfo', [], function(result){ - if (!result[0].response || !result[0].response.paytxfee){ - paytxfee = 0; - logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - payment processing could be broken with this daemon'); - return; - } - paytxfee = result[0].response.paytxfee; - }); }).once('connectionFailed', function(error){ logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error)); }).on('error', function(error){ @@ -302,13 +293,14 @@ function SetupForPool(logger, poolOptions){ } + // txfee included in feeAmountToBeCollected var feeAmountToBeCollected = parseFloat((toBePaid / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); - var txFee = Object.keys(workerPayments).length * paytxfee; - var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected - txFee; + 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' + ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver + ', needs to be at least ' + minReserveSatoshis); return; @@ -348,13 +340,13 @@ function SetupForPool(logger, poolOptions){ finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]); - callback(null, magnitude, workerPayments, finalRedisCommands); + callback(null, magnitude, results[0].response, workerPayments, finalRedisCommands); }); }, - function(magnitude, workerPayments, finalRedisCommands, callback){ + function(magnitude, balanceBefore, workerPayments, finalRedisCommands, callback){ //This does the final all-or-nothing atom transaction if block deamon sent payments var finalizeRedisTx = function(){ @@ -389,18 +381,23 @@ function SetupForPool(logger, poolOptions){ } finalizeRedisTx(); var totalWorkers = Object.keys(workerPayments).length; - logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + + logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol + ' was sent to ' + totalWorkers + ' miners'); - var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); - var txFee = totalWorkers * paytxfee; - var collectableFee = feeAmountUnits - txFee; - daemon.cmd('move', ['', processingConfig.feeCollectAccount, collectableFee], function(results){ - if (results[0].error){ - callback('Check finished - error with move ' + JSON.stringify(results[0].error)); - return; - } - logger.debug(logSystem, logComponent, feeAmountUnits + ' collected as fee'); - }); + setTimeout(function() { // not sure if we need some time to let daemon update the wallet balance + daemon.cmd('getbalance', [''], function(results){ + var balanceDiff = balanceBefore - results[0].response; + var txFee = balanceDiff - totalAmountUnits; + var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var poolFees = feeAmountUnits - txFee; + daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){ + if (results[0].error){ + callback('Check finished - error with move ' + JSON.stringify(results[0].error)); + return; + } + callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee'); + }); + }); + }, 1000); }); } } @@ -410,7 +407,8 @@ function SetupForPool(logger, poolOptions){ else{ logger.debug(logSystem, logComponent, result); - withdrawalProfit(); + // not sure if we need some time to let daemon update the wallet balance + setTimeout(withdrawalProfit, 1000); } }); }; @@ -420,27 +418,43 @@ function SetupForPool(logger, poolOptions){ if (!processingConfig.feeWithdrawalThreshold) return; + logger.debug(logSystem, logComponent, 'Trying to withdrawal profit'); daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){ - var withdrawalAmount = results[0].response - paytxfee; + // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less + // then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance + // total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing. + // But to be as much accurate as we can we use getinfo command to retrieve minimum tx fee (paytxfee). + daemon.cmd('getinfo', [], function(result){ - if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ - logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); - } - else{ + var paytxfee; + if (!result[0].response || !result[0].response.paytxfee){ + logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - withdrawal processing could be broken with this daemon'); + paytxfee = 0; + } else { + paytxfee = result[0].response.paytxfee; + } - var withdrawal = {}; - withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount; + var withdrawalAmount = results[0].response - paytxfee; - daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ - if (results[0].error){ - logger.debug(logSystem, logComponent, 'Withdrawal profit finished - error with sendmany ' + JSON.stringify(results[0].error)); - return; - } - logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + - ' was sent to ' + processingConfig.feeReceiveAddress); - }); - } + if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ + logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); + } + else{ + + var withdrawal = {}; + withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount; + + daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ + if (results[0].error){ + logger.debug(logSystem, logComponent, 'Withdrawal profit finished - error with sendmany ' + JSON.stringify(results[0].error)); + return; + } + logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + + ' was sent to ' + processingConfig.feeReceiveAddress); + }); + } + }); }); From 47247aafcbb07ee8dcd2dc551b1b51aaefe808f1 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 26 Mar 2014 17:41:07 -0600 Subject: [PATCH 018/150] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b6995b..cb9736c 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ config directory and coin.conf file. For more info on this see: Clone the repository and run `npm update` for all the dependencies to be installed: ```bash -git clone https://github.com/zone117x/node-stratum-portal.git +git clone https://github.com/zone117x/node-stratum-portal.git nomp +cd nomp npm update ``` From 68bea9ea7da901e5f97951ef31236d8117db826b Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 03:49:08 +0400 Subject: [PATCH 019/150] some syntax cleaning --- libs/paymentProcessor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 07afffc..b33760f 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -418,7 +418,7 @@ function SetupForPool(logger, poolOptions){ if (!processingConfig.feeWithdrawalThreshold) return; - logger.debug(logSystem, logComponent, 'Trying to withdrawal profit'); + logger.debug(logSystem, logComponent, 'Profit withdrawal started'); daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){ // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less @@ -438,7 +438,7 @@ function SetupForPool(logger, poolOptions){ var withdrawalAmount = results[0].response - paytxfee; if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ - logger.debug(logSystem, logComponent, 'Not enough profit to withdrawal yet'); + logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet'); } else{ @@ -447,7 +447,7 @@ function SetupForPool(logger, poolOptions){ daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ if (results[0].error){ - logger.debug(logSystem, logComponent, 'Withdrawal profit finished - error with sendmany ' + JSON.stringify(results[0].error)); + logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); return; } logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + From fc5cf3b883bc2fcef0518d4c6012bde7a3efa200 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 04:57:29 +0400 Subject: [PATCH 020/150] change success callback in finalizeRedisTx to logger.debug to prevent untimely withdrawalProfit call --- libs/paymentProcessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index b33760f..4114205 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -355,7 +355,7 @@ function SetupForPool(logger, poolOptions){ callback('Check finished - error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } - callback(null, 'Payments processing performed an interval'); + logger.debug(logSystem, logComponent, 'Payments processing performed an interval'); }); }; From 631575ed7b2f52d8f36dfa907b0121e83a1aba1a Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Mar 2014 20:12:52 -0600 Subject: [PATCH 021/150] Added recursive watch so web development is easier. --- coins/galleon.json | 6 ++++++ coins/helixcoin.json | 2 +- coins/wecoin.json | 2 +- libs/website.js | 32 +++++----------------------- package.json | 3 ++- pool_configs/galleon_example.json | 33 +++++++++++++++++++++++++++++ pool_configs/helixcoin_example.json | 2 +- pool_configs/litecoin_example.json | 2 +- website/pages/getting_started.html | 2 +- 9 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 coins/galleon.json create mode 100644 pool_configs/galleon_example.json diff --git a/coins/galleon.json b/coins/galleon.json new file mode 100644 index 0000000..53c6a2b --- /dev/null +++ b/coins/galleon.json @@ -0,0 +1,6 @@ +{ + "name" : "Galleon", + "symbol" : "GLN", + "algorithm" : "keccak", + "txMessages" : false +} \ No newline at end of file diff --git a/coins/helixcoin.json b/coins/helixcoin.json index adf4d2e..2ae867b 100644 --- a/coins/helixcoin.json +++ b/coins/helixcoin.json @@ -1,6 +1,6 @@ { "name" : "Helixcoin", "symbol" : "HXC", - "algorithm" : "keccak", + "algorithm" : "max", "txMessages" : false } \ No newline at end of file diff --git a/coins/wecoin.json b/coins/wecoin.json index 19e2e17..cdac3d3 100644 --- a/coins/wecoin.json +++ b/coins/wecoin.json @@ -1,6 +1,6 @@ { "name" : "Wecoin", "symbol" : "WEC", - "algorithm" : "keccak", + "algorithm" : "max", "txMessages" : false } \ No newline at end of file diff --git a/libs/website.js b/libs/website.js index 5e6cbfe..6f39f53 100644 --- a/libs/website.js +++ b/libs/website.js @@ -1,28 +1,3 @@ -/* TODO - - -Need to condense the entire website into a single html page. Embedding the javascript and css is easy. For images, -hopefully we can only use svg which can be embedded - otherwise we can convert the image into a data-url that can -be embedded, Favicon can also be a data-url which some javascript kungfu can display in browser. I'm focusing on -this mainly to help mitigate ddos and other kinds of attacks - and to just have a badass blazing fast project. - -Don't worry about doing any of that condensing yourself - go head and keep all the resources as separate files. -I will write a script for when the server starts to read all the files in the /website folder and minify and condense -it all together into one file, saved in memory. We will have 1 persistent condensed file that servers as our "template" -file that contains things like: -
Hashrate: {{=stats.hashrate}
- -And then on some caching interval (maybe 5 seconds?) we will apply the template engine to generate the real html page -that we serve and hold in in memory - this is the file we serve to seo-bots (googlebot) and users when they first load -the page. - -Once the user loads the page we will have server-side event source connected to the portal api where it receives -updated stats on some interval (probably 5 seconds like template cache updater) and applies the changes to the already -displayed page. - -We will use fs.watch to detect changes to anything in the /website folder and update our stuff in memory. - - */ var fs = require('fs'); var path = require('path'); @@ -31,6 +6,8 @@ var async = require('async'); var dot = require('dot'); var express = require('express'); +var watch = require('node-watch'); + var api = require('./api.js'); @@ -101,8 +78,9 @@ module.exports = function(logger){ - fs.watch('website', function(event, filename){ - if (event === 'change' && filename in pageFiles) + watch('website', function(filename){ + //if (event === 'change' && filename in pageFiles) + //READ ALL THE FILEZ BLAHHH readPageFiles(); }); diff --git a/package.json b/package.json index c63ae63..5ebd0ec 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "async": "*", "express": "*", "dot": "*", - "colors": "*" + "colors": "*", + "node-watch": "*" }, "engines": { "node": ">=0.10" diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json new file mode 100644 index 0000000..08c1c6e --- /dev/null +++ b/pool_configs/galleon_example.json @@ -0,0 +1,33 @@ +{ + "disabled": false, + "coin": "galleon.json", + + "address": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + + "ports": { + "3537": { + "diff": 4 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 19632, + "user": "testuser", + "password": "testpass" + } + ], + + "p2p": { + "enabled": false, + "host": "localhost", + "port": 19333, + "protocolVersion": 70002, + "magic": "fcc1b7dc" + } +} \ No newline at end of file diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json index 9732ad5..75f412c 100644 --- a/pool_configs/helixcoin_example.json +++ b/pool_configs/helixcoin_example.json @@ -1,5 +1,5 @@ { - "disabled": false, + "disabled": true, "coin": "helixcoin.json", "address": "H9xyrh45LzLX4uXCP6jG6ZGrWho8srUgiG", diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 54e3a40..ba66286 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "disabled": false, "coin": "litecoin.json", "shareProcessing": { diff --git a/website/pages/getting_started.html b/website/pages/getting_started.html index daf9fd0..e228dc7 100644 --- a/website/pages/getting_started.html +++ b/website/pages/getting_started.html @@ -1,5 +1,5 @@
- To get started.... + To get started simply....
\ No newline at end of file From 314dacf98a7b11861a958ecc250b03bc0e52aadb Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Mar 2014 20:53:30 -0600 Subject: [PATCH 022/150] Fixed global hashrate problem --- config.json | 2 +- libs/stats.js | 4 ++-- libs/website.js | 3 ++- pool_configs/galleon_example.json | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index ec88df7..f602d64 100644 --- a/config.json +++ b/config.json @@ -20,7 +20,7 @@ "enabled": true, "siteTitle": "Cryppit", "port": 80, - "statUpdateInterval": 3, + "statUpdateInterval": 5, "hashrateWindow": 600 }, "proxy": { diff --git a/libs/stats.js b/libs/stats.js index c049c76..87277b4 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -139,10 +139,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ var shareMultiplier = algoMultipliers[coinStats.algorithm]; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; coinStats.hashrate = hashratePre / 1e3 | 0; - delete coinStats.hashrates; - delete coinStats.shares; portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; + delete coinStats.hashrates; + delete coinStats.shares; }); _this.stats = portalStats; diff --git a/libs/website.js b/libs/website.js index 6f39f53..d4ea491 100644 --- a/libs/website.js +++ b/libs/website.js @@ -55,7 +55,8 @@ module.exports = function(logger){ portalConfig: portalConfig }); } - logger.debug(logSystem, 'Stats', 'Website updated to latest stats'); + + //logger.debug(logSystem, 'Stats', 'Website updated to latest stats'); }; diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index 08c1c6e..470361a 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -7,6 +7,28 @@ "txRefreshInterval": 20000, "connectionTimeout": 600, + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19632, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + } + }, "ports": { "3537": { From b73b6a828eadba7ed7c920cd2f268c0acce1204f Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 08:55:58 +0400 Subject: [PATCH 023/150] typo in paymentProcessor --- libs/paymentProcessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 1e75c35..06104de 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -20,7 +20,7 @@ module.exports = function(logger){ function SetupForPool(logger, poolOptions){ if (!poolOptions.shareProcessing || - poolOptions.shareProcessing.internal || + !poolOptions.shareProcessing.internal || !poolOptions.shareProcessing.internal.enabled) return; From 1fc9684677173ec030253f6c02ea50fca6d1a5a6 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Mar 2014 23:16:52 -0600 Subject: [PATCH 024/150] Prettier logs --- config.json | 2 +- init.js | 1 + libs/algoProperties.js | 4 ++++ libs/logUtil.js | 5 ++++- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 libs/algoProperties.js 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..a6c99f7 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'); 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 }; From 634f6c842fdfc326005f97422e08e25b17a45197 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 00:56:36 -0600 Subject: [PATCH 025/150] Added lots more comments and tried to squeeze all comments and code into github's max page width --- libs/paymentProcessor.js | 119 ++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 06104de..8cb3dca 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,30 +422,38 @@ 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'); + + logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + + ' ' + poolOptions.coin.symbol + ' was sent to ' + totalWorkers + ' miners'); + setTimeout(function() { // not sure if we need some time to let daemon update the wallet balance daemon.cmd('getbalance', [''], function(results){ var balanceDiff = balanceBefore - results[0].response; var txFee = balanceDiff - totalAmountUnits; - var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var leftOver = totalAmountUnits / (1 - processingConfig.feePercent); + var feeAmountUnits = toPrecision(leftOver * processingConfig.feePercent, coinPrecision); var poolFees = feeAmountUnits - txFee; daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){ if (results[0].error){ @@ -426,15 +487,17 @@ function SetupForPool(logger, poolOptions){ logger.debug(logSystem, logComponent, 'Profit withdrawal started'); daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){ - // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less - // then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance - // total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing. - // But to be as much accurate as we can we use getinfo command to retrieve minimum tx fee (paytxfee). + /* We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee + is less then minimumReserve value. Because in this case even if feeCollectAccount account will have + negative balance total wallet balance will be positive and feeCollectAccount account will be refilled + during next payment processing. But to be as much accurate as we can we use getinfo command to retrieve + minimum tx fee (paytxfee). */ daemon.cmd('getinfo', [], function(result){ var paytxfee; if (!result[0].response || !result[0].response.paytxfee){ - logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - withdrawal processing could be broken with this daemon'); + logger.error(logSystem, logComponent, + 'Daemon does not have paytxfee - withdrawal processing could be broken with this daemon'); paytxfee = 0; } else { paytxfee = result[0].response.paytxfee; @@ -452,10 +515,12 @@ function SetupForPool(logger, poolOptions){ daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ if (results[0].error){ - logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); + logger.debug(logSystem, logComponent, + 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); return; } - logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + + logger.debug(logSystem, logComponent, + 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + ' was sent to ' + processingConfig.feeReceiveAddress); }); } From b5c9d0174ba6d4db92ec8612a4dcf867e4896bfb Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 17:22:21 +0400 Subject: [PATCH 026/150] get tx fee paid from transaction made by sendmany --- libs/paymentProcessor.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 06104de..08adb5e 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -345,13 +345,13 @@ function SetupForPool(logger, poolOptions){ finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]); - callback(null, magnitude, results[0].response, workerPayments, finalRedisCommands); + callback(null, magnitude, workerPayments, finalRedisCommands); }); }, - function(magnitude, balanceBefore, workerPayments, finalRedisCommands, callback){ + function(magnitude, workerPayments, finalRedisCommands, callback){ //This does the final all-or-nothing atom transaction if block deamon sent payments var finalizeRedisTx = function(){ @@ -388,21 +388,21 @@ function SetupForPool(logger, poolOptions){ var totalWorkers = Object.keys(workerPayments).length; logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol + ' was sent to ' + totalWorkers + ' miners'); - setTimeout(function() { // not sure if we need some time to let daemon update the wallet balance - daemon.cmd('getbalance', [''], function(results){ - var balanceDiff = balanceBefore - results[0].response; - var txFee = balanceDiff - totalAmountUnits; - var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); - var poolFees = feeAmountUnits - txFee; - daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){ - if (results[0].error){ - callback('Check finished - error with move ' + JSON.stringify(results[0].error)); - return; - } - callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee'); - }); + daemon.cmd('gettransaction', [results[0].response], function(results){ + if (results[0].error){ + callback('Check finished - error with gettransaction ' + JSON.stringify(results[0].error)); + return; + } + var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision)); + var poolFees = feeAmountUnits - results[0].response.fee; + daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){ + if (results[0].error){ + callback('Check finished - error with move ' + JSON.stringify(results[0].error)); + return; + } + callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee'); }); - }, 1000); + }); }); } } From d744dc2857ad5d4e377e30d712e72c5900155bc6 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 27 Mar 2014 17:28:42 +0400 Subject: [PATCH 027/150] remove paytxfee functionality - it doesnt really help --- libs/paymentProcessor.js | 45 ++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 08adb5e..315b34d 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -429,38 +429,25 @@ function SetupForPool(logger, poolOptions){ // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less // then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance // total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing. - // But to be as much accurate as we can we use getinfo command to retrieve minimum tx fee (paytxfee). - daemon.cmd('getinfo', [], function(result){ + var withdrawalAmount = results[0].response; - var paytxfee; - if (!result[0].response || !result[0].response.paytxfee){ - logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - withdrawal processing could be broken with this daemon'); - paytxfee = 0; - } else { - paytxfee = result[0].response.paytxfee; - } + if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ + logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet'); + } + else{ - var withdrawalAmount = results[0].response - paytxfee; - - if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){ - logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet'); - } - else{ - - var withdrawal = {}; - withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount; - - daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ - if (results[0].error){ - logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); - return; - } - logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + - ' was sent to ' + processingConfig.feeReceiveAddress); - }); - } - }); + var withdrawal = {}; + withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount; + daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ + if (results[0].error){ + logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); + return; + } + logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + + ' was sent to ' + processingConfig.feeReceiveAddress); + }); + } }); }; From c3c87e7dba676bbfcc28f26c95c2722146bd8d73 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 09:33:57 -0600 Subject: [PATCH 028/150] Was starting payment processor for disabled pool configs --- init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.js b/init.js index a6c99f7..2b915eb 100644 --- a/init.js +++ b/init.js @@ -184,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; From 42ae0a4231a8f77142089d5a8116408a56a9690b Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 11:52:54 -0600 Subject: [PATCH 029/150] Added block hash (solution) to redis so we can see whats going on with the invalid transactions. --- init.js | 15 ++++++++++++--- libs/algoProperties.js | 4 ---- libs/paymentProcessor.js | 3 ++- libs/shareProcessor.js | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 libs/algoProperties.js diff --git a/init.js b/init.js index 2b915eb..5ecfe89 100644 --- a/init.js +++ b/init.js @@ -2,8 +2,6 @@ var fs = require('fs'); var os = require('os'); var cluster = require('cluster'); -require('./libs/algoProperties.js'); - var async = require('async'); var posix = require('posix'); var PoolLogger = require('./libs/logUtil.js'); @@ -94,8 +92,19 @@ var buildPoolConfigs = function(){ var spawnPoolWorkers = function(portalConfig, poolConfigs){ - var serializedConfigs = JSON.stringify(poolConfigs); + Object.keys(poolConfigs).forEach(function(coin){ + var p = poolConfigs[coin]; + var internalEnabled = p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; + var mposEnabled = p.shareProcesssing && p.shareProcessing.mpos && p.shareProcessing.mpos.enabled; + + if (!internalEnabled && !mposEnabled){ + logger.error('Master', coin, 'Share processing is not configured so a pool cannot be started for this coin.'); + delete poolConfigs[coin]; + } + }); + + var serializedConfigs = JSON.stringify(poolConfigs); var numForks = (function(){ if (!portalConfig.clustering || !portalConfig.clustering.enabled) diff --git a/libs/algoProperties.js b/libs/algoProperties.js deleted file mode 100644 index 13dbd58..0000000 --- a/libs/algoProperties.js +++ /dev/null @@ -1,4 +0,0 @@ -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/paymentProcessor.js b/libs/paymentProcessor.js index 8cb3dca..b80d737 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -63,7 +63,7 @@ function SetupForPool(logger, poolOptions){ clearTimeout(reconnectTimeout); logger.debug(logSystem, logComponent, 'Successfully connected to redis database'); }).on('error', function(err){ - paymentLogger.error('redis', 'Redis client had an error: ' + JSON.stringify(err)) + logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err)) }).on('end', function(){ logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...'); @@ -111,6 +111,7 @@ function SetupForPool(logger, poolOptions){ txHash: details[0], height: details[1], reward: details[2], + solution: details[3], serialized: r }; }); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 1022858..a085a56 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -69,7 +69,7 @@ module.exports = function(logger, poolConfig){ if (isValidBlock){ redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]); - redisCommands.push(['sadd', coin + '_blocksPending', shareData.tx + ':' + shareData.height + ':' + shareData.reward]); + redisCommands.push(['sadd', coin + '_blocksPending', shareData.tx + ':' + shareData.height + ':' + shareData.reward + ':' + shareData.solution]); redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]); } else if (shareData.solution){ From a1071bd30d739a4a6b20b51c6c96a1a7fed45416 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:36:35 -0600 Subject: [PATCH 030/150] Updated credits --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb9736c..39dbd97 100644 --- a/README.md +++ b/README.md @@ -372,9 +372,11 @@ BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR Credits ------- -* [vekexasia](https://github.com/vekexasia) - co-developer & great tester -* [TheSeven](https://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman -* Those that contributed to [node-stratum-pool](/zone117x/node-stratum-pool) +* [Tony Dobbs](anthonydobbs.com) - graphical help with logo and front-end design +* [vekexasia](github.com/vekexasia) - co-developer & great tester +* [TheSeven](github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman +* [UdjinM6](github.com/UdjinM6) - helped implement fee withdrawal in payment processing +* Those that contributed to [node-stratum-pool](github.com/zone117x/node-stratum-pool) License From 6bc6c25785fc49498e7818b5bf86405777e768d1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:38:52 -0600 Subject: [PATCH 031/150] Updated credits --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39dbd97..8a78092 100644 --- a/README.md +++ b/README.md @@ -372,11 +372,11 @@ BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR Credits ------- -* [Tony Dobbs](anthonydobbs.com) - graphical help with logo and front-end design -* [vekexasia](github.com/vekexasia) - co-developer & great tester -* [TheSeven](github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman -* [UdjinM6](github.com/UdjinM6) - helped implement fee withdrawal in payment processing -* Those that contributed to [node-stratum-pool](github.com/zone117x/node-stratum-pool) +* [Tony Dobbs](//anthonydobbs.com) - graphical help with logo and front-end design +* [vekexasia](http://github.com/vekexasia) - co-developer & great tester +* [TheSeven](http://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman +* [UdjinM6](http://github.com/UdjinM6) - helped implement fee withdrawal in payment processing +* Those that contributed to [node-stratum-pool](http://github.com/zone117x/node-stratum-pool) License From 6efd18dfb35c523d9a3ae48a4c34ab232d71041e Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:40:57 -0600 Subject: [PATCH 032/150] Updated readme --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8a78092..21da2a6 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ front-end website. #### Features -* For the pool server it uses the highly efficient [node-stratum-pool](https://github.com/zone117x/node-stratum-pool) module which +* For the pool server it uses the highly efficient [node-stratum-pool](//github.com/zone117x/node-stratum-pool) module which supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, several hashing algorithms. -* The portal has an [MPOS](https://github.com/MPOS/php-mpos) compatibility mode so that the it can -function as a drop-in-replacement for [python-stratum-mining](https://github.com/Crypto-Expert/stratum-mining). This +* The portal has an [MPOS](//github.com/MPOS/php-mpos) compatibility mode so that the it can +function as a drop-in-replacement for [python-stratum-mining](//github.com/Crypto-Expert/stratum-mining). This mode can be enabled in the configuration and will insert shares into a MySQL database in the format which MPOS expects. * Multi-pool ability - this software was built from the ground up to run with multiple coins simultaneously (which can @@ -372,11 +372,11 @@ BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR Credits ------- -* [Tony Dobbs](//anthonydobbs.com) - graphical help with logo and front-end design -* [vekexasia](http://github.com/vekexasia) - co-developer & great tester -* [TheSeven](http://github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman -* [UdjinM6](http://github.com/UdjinM6) - helped implement fee withdrawal in payment processing -* Those that contributed to [node-stratum-pool](http://github.com/zone117x/node-stratum-pool) +* [Tony Dobbs](http://anthonydobbs.com) - graphical help with logo and front-end design +* [vekexasia](//github.com/vekexasia) - co-developer & great tester +* [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman +* [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing +* Those that contributed to [node-stratum-pool](//github.com/zone117x/node-stratum-pool) License From c71e9d71c3805bbfe33c56c60057095b9fe53be3 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:43:45 -0600 Subject: [PATCH 033/150] Updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21da2a6..898b602 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,8 @@ rpcport=19332 ``` For redundancy, its recommended to have at least two daemon instances running in case one drops out-of-sync or offline, all instances will be polled for block/transaction updates and be used for submitting blocks. Creating a backup daemon -involves spawning a daemon using the "-datadir=/backup" argument which creates a new daemon instance with it's own -config directory and coin.conf file. For more info on this see: +involves spawning a daemon using the `-datadir=/backup` command-line argument which creates a new daemon instance with +it's own config directory and coin.conf file. For more info on this see: * https://en.bitcoin.it/wiki/Data_directory * https://en.bitcoin.it/wiki/Running_bitcoind From d3e2057ec9d47aa94dda138b4e2a964b4248da5c Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:57:56 -0600 Subject: [PATCH 034/150] Daemons will rarely arbitrarily drop blocks that it previously considered valid. This is a bug in the daemon or maybe only in testnet. This fix treats those dropped blocks as orhpans. --- libs/paymentProcessor.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 7d6a83e..ead7e79 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -137,13 +137,24 @@ function SetupForPool(logger, poolOptions){ return; } - txDetails = txDetails.filter(function(tx){ - if (tx.error || !tx.result){ + txDetails.forEach(function(tx, i){ + if (tx.error && tx.error.code === -5){ + /* 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 + tx.result = { + txid: rounds[i].txHash, + details: [{category: 'orphan'}] + }; + } + else if (tx.error || !tx.result){ logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(tx)); - return false; } - return true; }); From f60f9e41e90cb509ff8cd13986371fc64092d46a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 12:59:35 -0600 Subject: [PATCH 035/150] Pushed logo --- website/static/logo.svg | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 website/static/logo.svg diff --git a/website/static/logo.svg b/website/static/logo.svg new file mode 100644 index 0000000..0e92037 --- /dev/null +++ b/website/static/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From fc56d940925855ed505df900af8b9e2ecbdc2343 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 13:11:52 -0600 Subject: [PATCH 036/150] Added example config files for different types of coins (hashes and POS) --- pool_configs/galleon_example.json | 2 +- pool_configs/helixcoin_example.json | 24 ++++++++++++++++++++++++ pool_configs/hobonickels_example.json | 2 +- pool_configs/litecoin_example.json | 8 ++++---- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index 470361a..b195a66 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -1,5 +1,5 @@ { - "disabled": false, + "disabled": true, "coin": "galleon.json", "address": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json index 75f412c..6eb6e76 100644 --- a/pool_configs/helixcoin_example.json +++ b/pool_configs/helixcoin_example.json @@ -2,6 +2,30 @@ "disabled": true, "coin": "helixcoin.json", + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 70, + "minimumReserve": 10, + "feePercent": 0.05, + "feeCollectAccount": "feesCollected", + "feeReceiveAddress": "mppaGeNaSbG1Q7S6V3gL5uJztMhucgL9Vh", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19332, + "user": "litecoinrpc", + "password": "testnet" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + } + }, + "address": "H9xyrh45LzLX4uXCP6jG6ZGrWho8srUgiG", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json index 144fd24..3e57c63 100644 --- a/pool_configs/hobonickels_example.json +++ b/pool_configs/hobonickels_example.json @@ -4,7 +4,7 @@ "shareProcessing": { "internal": { - "enabled": true, + "enabled": false, "validateWorkerAddress": true, "paymentInterval": 10, "minimumPayment": 100.001, diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 6efb22f..27e68fc 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -7,11 +7,11 @@ "enabled": true, "validateWorkerAddress": true, "paymentInterval": 10, - "minimumPayment": 100.001, + "minimumPayment": 70, "minimumReserve": 10, - "feePercent": 0.02, + "feePercent": 0.05, "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", + "feeReceiveAddress": "mppaGeNaSbG1Q7S6V3gL5uJztMhucgL9Vh", "feeWithdrawalThreshold": 5, "daemon": { "host": "localhost", @@ -36,7 +36,7 @@ }, - "address": "mfsm1ckZKTTjDz94KonZZsbZnAbm1UV4BF", + "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, "connectionTimeout": 600, From 4a651dec89017515add2b7712bc295079b3395a7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2014 16:29:43 -0600 Subject: [PATCH 037/150] Payment processing seems to be working well --- README.md | 2 +- libs/paymentProcessor.js | 213 ++++++++++++++++++++++++------------- libs/shareProcessor.js | 2 +- website/index.html | 2 + website/static/favicon.png | Bin 0 -> 413 bytes 5 files changed, 142 insertions(+), 77 deletions(-) create mode 100644 website/static/favicon.png diff --git a/README.md b/README.md index 898b602..6660c1b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NOMP +# NOMP ![NOMP Logo](http://zone117x.github.io/node-open-mining-portal/logo.svg "NOMP Logo") #### Node Open Mining Portal This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index ead7e79..d805771 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -62,9 +62,9 @@ function SetupForPool(logger, poolOptions){ redisClient.on('ready', function(){ clearTimeout(reconnectTimeout); 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)) - }).on('end', function(){ + })*/.on('end', function(){ logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...'); 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. */ var processPayments = function(){ + + + var startPaymentProcess = Date.now(); + async.waterfall([ /* 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(':'); return { category: details[0].category, - txHash: details[0], - height: details[1], - reward: details[2], - solution: details[3], + solution: details[0], + txHash: details[1], + height: details[2], + reward: details[3], 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. 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){ - if (tx.error && tx.error.code === -5){ - /* 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'); + var round = rounds[i]; - //These changes to the tx will convert it from dropped to orphan - tx.result = { - txid: rounds[i].txHash, - details: [{category: 'orphan'}] - }; + if (tx.error && tx.error.code === -5){ + + /* Block was dropped from coin daemon even after it happily accepted it earlier. */ + + //If we find another block at the same height then this block was drop-kicked orphaned + var dropKicked = !!rounds.filter(function(r){ + return r.height === round.height && r.solution !== round.solution && r.category !== 'dropkicked'; + }).length; + + if (dropKicked){ + logger.warning(logSystem, logComponent, + 'A block was drop-kicked orphaned' + + ' - we found a better block at the same height, solution ' + + round.solution + " round " + round.height); + round.category = 'dropkicked'; + } + else{ + /* We have no other blocks that match this height so convert to orphan in order for + shares from the round to be rewarded. */ + round.category = 'orphan'; + } } else if (tx.error || !tx.result){ logger.error(logSystem, logComponent, '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) rounds = rounds.filter(function(r){ - var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0]; + switch (r.category) { - if (!tx){ - logger.error(logSystem, logComponent, - 'daemon did not give us back a transaction that we asked for: ' + r.txHash); - return; - } + case 'generate': + /* Here we calculate the smallest unit in this coin's currency; the 'satoshi'. + The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the + rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore, + we simply divide the two to get the magnitude. I don't know math, there is probably + a better term than 'magnitude'. Sue me or do a pull request to fix it. */ + var roundMagnitude = r.reward / r.amount; - r.category = tx.result.details[0].category; + if (!magnitude) { + magnitude = roundMagnitude; - 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) + if (roundMagnitude % 10 !== 0) + logger.error(logSystem, logComponent, + 'Satosihis in coin is not divisible by 10 which is very odd'); + } + else if (magnitude != roundMagnitude) { + /* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are + 100,000,000 satoshis in one coin unit. */ logger.error(logSystem, logComponent, - 'Satosihis in coin is not divisible by 10 which is very odd'); - } - else if (magnitude != roundMagnitude){ - /* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are - 100,000,000 satoshis in one coin unit. */ - 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; + 'Magnitude in a round was different than in another round. HUGE PROBLEM.'); + } + return true; + case 'dropkicked': + case 'orphan': + return true; + default: + return false; + } }); @@ -236,32 +272,42 @@ function SetupForPool(logger, poolOptions){ rounds.forEach(function(round, i){ 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]]); - }); + if (!workerShares){ + logger.error(logSystem, logComponent, 'No worker shares for round: ' + + round.height + ' solution: ' + round.solution); + return; } - 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); + switch (round.category){ + case 'orphan': + /* Each block that gets orphaned, all the shares go into the current round so that + miners still get a reward for their work. This seems unfair to those that just + started mining during this current round, but over time it balances out and rewards + loyal miners. */ + Object.keys(workerShares).forEach(function(worker){ + orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', + worker, workerShares[worker]]); + }); + break; - var totalShares = Object.keys(workerShares).reduce(function(p, c){ - return p + parseInt(workerShares[c]) - }, 0); + case 'generate': + /* We found a confirmed block! Now get the reward for it and calculate how much + we owe each miner based on the shares they submitted during that block round. */ + var reward = round.reward * (1 - processingConfig.feePercent); - 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; - } + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + + for (var worker in workerShares){ + var percent = parseInt(workerShares[worker]) / totalShares; + var workerRewardTotal = Math.floor(reward * percent); + if (!(worker in workerRewards)) workerRewards[worker] = 0; + workerRewards[worker] += workerRewardTotal; + } + break; } + }); callback(null, rounds, magnitude, workerRewards, orphanMergeCommands); @@ -380,7 +426,14 @@ function SetupForPool(logger, poolOptions){ var movePendingCommands = []; var roundsToDelete = []; 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]); roundsToDelete.push(coin + '_shares:round' + r.height) }); @@ -477,11 +530,15 @@ function SetupForPool(logger, poolOptions){ } } ], function(error, result){ + + + var paymentProcessTime = Date.now() - startPaymentProcess; + if (error) - logger.debug(logSystem, logComponent, error); + logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + error); 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 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); }; \ No newline at end of file diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index a085a56..9534189 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -69,7 +69,7 @@ module.exports = function(logger, poolConfig){ if (isValidBlock){ 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]); } else if (shareData.solution){ diff --git a/website/index.html b/website/index.html index 1371b95..35ac208 100644 --- a/website/index.html +++ b/website/index.html @@ -4,6 +4,8 @@ + + diff --git a/website/static/favicon.png b/website/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..738dbba65b9f7213a43c81781056b3d7119f89a4 GIT binary patch literal 413 zcmV;O0b>4%P)%~E8%pO%)-N3!TX5cdOd z0GfjWXk~dn)PnceEP+Oc0=46}0V#pOe0UxzRsd2%O&=;i4aFC~xbS*t+7KYu;`wA) ziVeSqhK>NSmIpvJ{iiYpg`xo=%N=N7F?why;B&xz8an`YnSjmre;%Cu4+8%_Jp2D` zb}Z4Bd|y@e|KIn|V7G2mF0;3q>y$H4eBc51bD8 zb#euL9RR{0OF;k>6<>Q?h;{%dxIzRzeqhvoFET2!PU1$Jp9Xa4>8P7*j z1H-tQ?gdC{Nb98FNf!+ygdX`F6=-Pe$ASZJI2M~)yuJnimf-2lJ1 Date: Fri, 28 Mar 2014 02:50:55 +0400 Subject: [PATCH 038/150] merging fixes --- libs/paymentProcessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index d805771..953cc6c 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -191,7 +191,7 @@ function SetupForPool(logger, poolOptions){ } else if (tx.error || !tx.result){ 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; From c5e650cd6ddc643f2593ba5196bd3407c48753ed Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Fri, 28 Mar 2014 16:42:55 +0400 Subject: [PATCH 039/150] enabled:true seems more natural then disabled:false in poolOptions --- init.js | 4 ++-- pool_configs/darkcoin_example.json | 2 +- pool_configs/galleon_example.json | 2 +- pool_configs/helixcoin_example.json | 2 +- pool_configs/hirocoin_example.json | 2 +- pool_configs/hobonickels_example.json | 2 +- pool_configs/litecoin_example.json | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/init.js b/init.js index 5ecfe89..4d477cf 100644 --- a/init.js +++ b/init.js @@ -75,7 +75,7 @@ var buildPoolConfigs = function(){ var configs = {}; fs.readdirSync('pool_configs').forEach(function(file){ var poolOptions = JSON.parse(JSON.minify(fs.readFileSync('pool_configs/' + file, {encoding: 'utf8'}))); - if (poolOptions.disabled) return; + if (!poolOptions.enabled) return; var coinFilePath = 'coins/' + poolOptions.coin; if (!fs.existsSync(coinFilePath)){ logger.error('Master', poolOptions.coin, 'could not find file: ' + coinFilePath); @@ -193,7 +193,7 @@ var startPaymentProcessor = function(poolConfigs){ var enabledForAny = false; for (var pool in poolConfigs){ var p = poolConfigs[pool]; - var enabled = !p.disabled && p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; + var enabled = p.enabled && p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; if (enabled){ enabledForAny = true; break; diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json index 0ab86a5..7b8487a 100644 --- a/pool_configs/darkcoin_example.json +++ b/pool_configs/darkcoin_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "enabled": false, "coin": "darkcoin.json", "shareProcessing": { diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index b195a66..e149565 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "enabled": false, "coin": "galleon.json", "address": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json index 6eb6e76..31292a4 100644 --- a/pool_configs/helixcoin_example.json +++ b/pool_configs/helixcoin_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "enabled": false, "coin": "helixcoin.json", "shareProcessing": { diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json index 295065a..9c49962 100644 --- a/pool_configs/hirocoin_example.json +++ b/pool_configs/hirocoin_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "enabled": false, "coin": "hirocoin.json", "shareProcessing": { diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json index 3e57c63..bc3e4eb 100644 --- a/pool_configs/hobonickels_example.json +++ b/pool_configs/hobonickels_example.json @@ -1,5 +1,5 @@ { - "disabled": true, + "enabled": false, "coin": "hobonickels.json", "shareProcessing": { diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 27e68fc..ce54056 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -1,5 +1,5 @@ { - "disabled": false, + "enabled": false, "coin": "litecoin.json", "shareProcessing": { From 514d04d6adc12bf92fc6b009368c8d5518f58332 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 28 Mar 2014 10:19:10 -0600 Subject: [PATCH 040/150] Allow shares of float difficulties to be inserted into redis instead of just integers. --- libs/shareProcessor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 9534189..56229a2 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -54,7 +54,7 @@ module.exports = function(logger, poolConfig){ var redisCommands = []; if (isValidShare){ - redisCommands.push(['hincrby', coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty]); + redisCommands.push(['hincrbyfloat', coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty]); redisCommands.push(['hincrby', coin + '_stats', 'validShares', 1]); /* Stores share diff, worker, and unique value with a score that is the timestamp. Unique value ensures it From b6af2444441b971e62aa6c823effe69ef0f485c2 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 28 Mar 2014 11:38:59 -0600 Subject: [PATCH 041/150] Added more info on daemon readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6660c1b..d8f0b22 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,12 @@ rpcport=19332 For redundancy, its recommended to have at least two daemon instances running in case one drops out-of-sync or offline, all instances will be polled for block/transaction updates and be used for submitting blocks. Creating a backup daemon involves spawning a daemon using the `-datadir=/backup` command-line argument which creates a new daemon instance with -it's own config directory and coin.conf file. For more info on this see: - * https://en.bitcoin.it/wiki/Data_directory +it's own config directory and coin.conf file. Learn about the daemon, how to use it and how it works if you want to be +a good pool operator. For starters be sure to read: * https://en.bitcoin.it/wiki/Running_bitcoind + * https://en.bitcoin.it/wiki/Data_directory + * https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list + * https://en.bitcoin.it/wiki/Difficulty #### 1) Downloading & Installing From 3ec38e5523ef8fa7ca6402198abd18da729f55ca Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 28 Mar 2014 18:05:15 -0600 Subject: [PATCH 042/150] Fixed typo preventing mpos mode --- init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.js b/init.js index 4d477cf..458b9b5 100644 --- a/init.js +++ b/init.js @@ -96,7 +96,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ Object.keys(poolConfigs).forEach(function(coin){ var p = poolConfigs[coin]; var internalEnabled = p.shareProcessing && p.shareProcessing.internal && p.shareProcessing.internal.enabled; - var mposEnabled = p.shareProcesssing && p.shareProcessing.mpos && p.shareProcessing.mpos.enabled; + var mposEnabled = p.shareProcessing && p.shareProcessing.mpos && p.shareProcessing.mpos.enabled; if (!internalEnabled && !mposEnabled){ logger.error('Master', coin, 'Share processing is not configured so a pool cannot be started for this coin.'); From c699e66a5d4e9cdd7e113f0edb7eb6710d1b58da Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 29 Mar 2014 19:59:22 -0600 Subject: [PATCH 043/150] Better mpos/mysql logging. Added maxcoin config. --- coins/maxcoin.json | 6 ++++ libs/mposCompatibility.js | 19 +++++++------ libs/paymentProcessor.js | 2 +- pool_configs/maxcoin_example.json | 47 +++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 coins/maxcoin.json create mode 100644 pool_configs/maxcoin_example.json diff --git a/coins/maxcoin.json b/coins/maxcoin.json new file mode 100644 index 0000000..bd90799 --- /dev/null +++ b/coins/maxcoin.json @@ -0,0 +1,6 @@ +{ + "name" : "Maxcoin", + "symbol" : "MAX", + "algorithm" : "keccak", + "txMessages" : false +} \ No newline at end of file diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index 2417ca3..4b75adb 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -7,7 +7,8 @@ module.exports = function(logger, poolConfig){ var connection; - var logIdentify = 'MPOS'; + var logIdentify = 'MySQL'; + var logComponent = coin; function connect(){ connection = mysql.createConnection({ @@ -19,18 +20,18 @@ module.exports = function(logger, poolConfig){ }); connection.connect(function(err){ if (err) - logger.error(logIdentify, 'mysql', 'Could not connect to mysql database: ' + JSON.stringify(err)) + logger.error(logIdentify, logComponent, 'Could not connect to mysql database: ' + JSON.stringify(err)) else{ - logger.debug(logIdentify, 'mysql', 'Successful connection to MySQL database'); + logger.debug(logIdentify, logComponent, 'Successful connection to MySQL database'); } }); connection.on('error', function(err){ if(err.code === 'PROTOCOL_CONNECTION_LOST') { - logger.warning(logIdentify, 'mysql', 'Lost connection to MySQL database, attempting reconnection...'); + logger.warning(logIdentify, logComponent, 'Lost connection to MySQL database, attempting reconnection...'); connect(); } else{ - logger.error(logIdentify, 'mysql', 'Database error: ' + JSON.stringify(err)) + logger.error(logIdentify, logComponent, 'Database error: ' + JSON.stringify(err)) } }); } @@ -43,7 +44,7 @@ module.exports = function(logger, poolConfig){ [workerName], function(err, result){ if (err){ - logger.error(logIdentify, 'mysql', 'Database error when authenticating worker: ' + + logger.error(logIdentify, logComponent, 'Database error when authenticating worker: ' + JSON.stringify(err)); authCallback(false); } @@ -76,9 +77,9 @@ module.exports = function(logger, poolConfig){ dbData, function(err, result) { if (err) - logger.error(logIdentify, 'mysql', 'Insert error when adding share: ' + JSON.stringify(err)); + logger.error(logIdentify, logComponent, 'Insert error when adding share: ' + JSON.stringify(err)); else - logger.debug(logIdentify, 'mysql', 'Share inserted'); + logger.debug(logIdentify, logComponent, 'Share inserted'); } ); }; @@ -89,7 +90,7 @@ module.exports = function(logger, poolConfig){ 'UPDATE `pool_worker` SET `difficulty` = ' + diff + ' WHERE `username` = ' + connection.escape(workerName), function(err, result){ if (err) - logger.error(logIdentify, 'mysql', 'Error when updating worker diff: ' + + logger.error(logIdentify, logComponent, 'Error when updating worker diff: ' + JSON.stringify(err)); else if (result.affectedRows === 0){ connection.query('INSERT INTO `pool_worker` SET ?', {username: workerName, difficulty: diff}); diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 953cc6c..2f2f25c 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -48,7 +48,7 @@ function SetupForPool(logger, poolOptions){ logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error)); }).on('error', function(error){ - logger.error(logSystem, logComponent); + logger.error(logSystem, logComponent, 'Daemon error ' + JSON.stringify(error)); }).init(); diff --git a/pool_configs/maxcoin_example.json b/pool_configs/maxcoin_example.json new file mode 100644 index 0000000..9319189 --- /dev/null +++ b/pool_configs/maxcoin_example.json @@ -0,0 +1,47 @@ +{ + "enabled": false, + "coin": "maxcoin.json", + + "address": "tKWkadAkT2vqK6v2PHLJmV1RTVvF7XZEgN", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 10, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "tQ2TKXXk2jh7EX2RJQ4ZxpZYEdRx7fdiaw", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 27932, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + } + }, + + "ports": { + "5547": { + "diff": 2 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 27932, + "user": "testuser", + "password": "testpass" + } + ] +} From da0bbd0b441a2aad15d8e9ddd16335ebf60da011 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 30 Mar 2014 03:08:19 -0600 Subject: [PATCH 044/150] Added ultracoin and chainStartTime to scrypt-jane coins. Added warning for if no pools are configured/enabled. --- coins/ultracoin.json | 7 +++++++ coins/yacoin.json | 1 + init.js | 5 +++++ 3 files changed, 13 insertions(+) create mode 100644 coins/ultracoin.json diff --git a/coins/ultracoin.json b/coins/ultracoin.json new file mode 100644 index 0000000..90aaae1 --- /dev/null +++ b/coins/ultracoin.json @@ -0,0 +1,7 @@ +{ + "name": "Ultracoin", + "symbol": "UTC", + "algorithm": "scrypt-jane", + "chainStartTime": 1388361600, + "txMessages": false +} \ No newline at end of file diff --git a/coins/yacoin.json b/coins/yacoin.json index 8f2f03f..4d697ad 100644 --- a/coins/yacoin.json +++ b/coins/yacoin.json @@ -2,5 +2,6 @@ "name": "Yacoin", "symbol": "YAC", "algorithm": "scrypt-jane", + "chainStartTime": 1367991200, "txMessages": false } \ No newline at end of file diff --git a/init.js b/init.js index 458b9b5..dcd7b6b 100644 --- a/init.js +++ b/init.js @@ -104,6 +104,11 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ } }); + if (Object.keys(poolConfigs).length === 0){ + logger.warning('Master', 'PoolSpawner', 'No pool configs exists or are enabled in pool_configs folder. No pools spawned.'); + return; + } + var serializedConfigs = JSON.stringify(poolConfigs); var numForks = (function(){ From 9b3d5766df7e644306e981ac3c0a679caea08407 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 30 Mar 2014 04:16:02 -0600 Subject: [PATCH 045/150] Stats now use algo data from stratum-pool module --- config.json | 2 +- libs/stats.js | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/config.json b/config.json index c32b5ca..b0fa531 100644 --- a/config.json +++ b/config.json @@ -21,7 +21,7 @@ "siteTitle": "Cryppit", "port": 80, "statUpdateInterval": 1.5, - "hashrateWindow": 600 + "hashrateWindow": 300 }, "proxy": { "enabled": false, diff --git a/libs/stats.js b/libs/stats.js index 87277b4..8e00f58 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -3,6 +3,8 @@ var async = require('async'); var os = require('os'); +var algos = require('stratum-pool/lib/algoProperties.js'); + module.exports = function(logger, portalConfig, poolConfigs){ @@ -12,12 +14,12 @@ module.exports = function(logger, portalConfig, poolConfigs){ var redisClients = []; - var algoMultipliers = { + /*var algoMultipliers = { 'x11': Math.pow(2, 16), 'scrypt': Math.pow(2, 16), 'scrypt-jane': Math.pow(2,16), 'sha256': Math.pow(2, 32) - }; + };*/ var canDoStats = true; @@ -128,7 +130,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ coinStats.shares = 0; coinStats.hashrates.forEach(function(ins){ var parts = ins.split(':'); - var workerShares = parseInt(parts[0]); + var workerShares = parseFloat(parts[0]); coinStats.shares += workerShares; var worker = parts[1]; if (worker in coinStats.workers) @@ -136,13 +138,14 @@ module.exports = function(logger, portalConfig, poolConfigs){ else coinStats.workers[worker] = workerShares }); - var shareMultiplier = algoMultipliers[coinStats.algorithm]; + var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; + console.log([hashratePre, shareMultiplier, coinStats.shares, portalConfig.website.hashrateWindow]); coinStats.hashrate = hashratePre / 1e3 | 0; portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; - delete coinStats.hashrates; - delete coinStats.shares; + coinStats.hashrates; + coinStats.shares; }); _this.stats = portalStats; From b7e7916736d16d8a3076e04ab8b0e517b5df8cfa Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 30 Mar 2014 17:04:54 -0600 Subject: [PATCH 046/150] Better logging for various components, changed example payment intervals from 10 seconds to 60 seconds. Added more coins and example pool configs. --- coins/copperbars.json | 7 + init.js | 2 +- libs/paymentProcessor.js | 176 ++++++++++++++------------ libs/poolWorker.js | 9 +- libs/shareProcessor.js | 42 +++--- libs/stats.js | 5 +- pool_configs/bitcoin_example.json | 64 ++++++++++ pool_configs/darkcoin_example.json | 2 +- pool_configs/galleon_example.json | 2 +- pool_configs/helixcoin_example.json | 2 +- pool_configs/hirocoin_example.json | 2 +- pool_configs/hobonickels_example.json | 2 +- pool_configs/litecoin_example.json | 2 +- pool_configs/maxcoin_example.json | 2 +- pool_configs/ultracoin_example.json | 64 ++++++++++ 15 files changed, 263 insertions(+), 120 deletions(-) create mode 100644 coins/copperbars.json create mode 100644 pool_configs/bitcoin_example.json create mode 100644 pool_configs/ultracoin_example.json diff --git a/coins/copperbars.json b/coins/copperbars.json new file mode 100644 index 0000000..597043d --- /dev/null +++ b/coins/copperbars.json @@ -0,0 +1,7 @@ +{ + "name": "Copperbars", + "symbol": "CPR", + "algorithm": "scrypt-jane", + "chainStartTime": 1376184687, + "txMessages": false +} \ No newline at end of file diff --git a/init.js b/init.js index dcd7b6b..c2cdebf 100644 --- a/init.js +++ b/init.js @@ -143,7 +143,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ i++; if (i === numForks){ clearInterval(spawnInterval); - logger.debug('Master', 'PoolSpawner', 'Spawned pools for all ' + numForks + ' configured forks'); + logger.debug('Master', 'PoolSpawner', 'Spawned ' + Object.keys(poolConfigs).length + ' pool(s) on ' + numForks + ' thread(s)'); } }, 250); diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 2f2f25c..70b80aa 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -9,70 +9,120 @@ module.exports = function(logger){ var poolConfigs = JSON.parse(process.env.pools); + var enabledPools = []; Object.keys(poolConfigs).forEach(function(coin) { - SetupForPool(logger, poolConfigs[coin]); + var poolOptions = poolConfigs[coin]; + if (poolOptions.shareProcessing && + poolOptions.shareProcessing.internal && + poolOptions.shareProcessing.internal.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.shareProcessing.internal; + 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 (' + processingConfig.redis.host + ':' + processingConfig.redis.port + ')'); + + }); + }); + + }; -function SetupForPool(logger, poolOptions){ - - if (!poolOptions.shareProcessing || - !poolOptions.shareProcessing.internal || - !poolOptions.shareProcessing.internal.enabled) - return; +function SetupForPool(logger, poolOptions, setupFinished){ var coin = poolOptions.coin.name; var processingConfig = poolOptions.shareProcessing.internal; - - var logSystem = 'Payments'; var logComponent = coin; - var daemon = new Stratum.daemon.interface([processingConfig.daemon]); - daemon.once('online', function(){ - logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing'); - - 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'); - } - }); - }).once('connectionFailed', function(error){ - logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + - JSON.stringify(error)); - }).on('error', function(error){ - logger.error(logSystem, logComponent, 'Daemon error ' + JSON.stringify(error)); - }).init(); - - - + var daemon; var redisClient; + async.parallel([ + + function(callback){ + daemon = new Stratum.daemon.interface([processingConfig.daemon]); + daemon.once('online', function(){ + 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'); + return; + } + callback() + }); + }).once('connectionFailed', function(error){ + logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: config ' + + JSON.stringify(processingConfig.daemon) + ', error: ' + + JSON.stringify(error)); + callback('Error connecting to deamon'); + }).on('error', function(error){ + logger.error(logSystem, logComponent, 'Daemon error ' + JSON.stringify(error)); + }).init(); + }, + function(callback){ + + redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host); + redisClient.on('ready', function(){ + if (callback) { + callback(); + callback = null; + return; + } + logger.debug(logSystem, logComponent, 'Connected to redis at ' + + processingConfig.redis.host + ':' + processingConfig.redis.port + ' for payment processing'); + }).on('end', function(){ + logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); + }).once('error', function(){ + if (callback) { + logger.error(logSystem, logComponent, 'Failed to connect to redis at ' + + processingConfig.redis.host + ':' + processingConfig.redis.port + ' for payment processing'); + callback('Error connecting to redis'); + callback = null; + } + }); + + } + ], function(err){ + if (err){ + setupFinished(false); + return; + } + setInterval(function(){ + try { + processPayments(); + } catch(e){ + throw e; + } + }, processingConfig.paymentInterval * 1000); + setTimeout(processPayments, 100); + setupFinished(true); + }); + + + + + + - var connectToRedis = function(){ - var reconnectTimeout; - redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host); - redisClient.on('ready', function(){ - clearTimeout(reconnectTimeout); - logger.debug(logSystem, logComponent, 'Successfully connected to redis database'); - })/*).on('error', function(err){ - logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err)) - })*/.on('end', function(){ - logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); - logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...'); - reconnectTimeout = setTimeout(function(){ - connectToRedis(); - }, 3000); - }); - }; - connectToRedis(); /* Number.toFixed gives us the decimal places we want, but as a string. parseFloat turns it back into number @@ -124,30 +174,6 @@ 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. It also adds the block reward amount to the round object - which the daemon gives also gives us. */ function(rounds, callback){ @@ -535,7 +561,7 @@ function SetupForPool(logger, poolOptions){ var paymentProcessTime = Date.now() - startPaymentProcess; if (error) - logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + error); + logger.debug(logSystem, logComponent, '[Took ' + paymentProcessTime + 'ms] ' + error); else{ logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + result); @@ -579,14 +605,4 @@ function SetupForPool(logger, poolOptions){ }; - - setInterval(function(){ - try { - processPayments(); - } catch(e){ - throw e; - } - }, processingConfig.paymentInterval * 1000); - setTimeout(processPayments, 100); - }; \ No newline at end of file diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 8706fe8..fdcde2f 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -13,11 +13,12 @@ module.exports = function(logger){ var poolConfigs = JSON.parse(process.env.pools); var portalConfig = JSON.parse(process.env.portalConfig); - var forkId = process.env.forkId; + var forkId = process.env.forkId; - var pools = {}; + var pools = {}; + + var proxyStuff = {}; - var proxyStuff = {} //Handle messages from master process sent via IPC process.on('message', function(message) { switch(message.type){ @@ -52,7 +53,7 @@ module.exports = function(logger){ var logSystem = 'Pool'; var logComponent = coin; - var logSubCat = 'Fork ' + forkId; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); var handlers = { diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 56229a2..938bd48 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -20,31 +20,23 @@ module.exports = function(logger, poolConfig){ var redisConfig = internalConfig.redis; var coin = poolConfig.coin.name; - var logSystem = 'Shares'; + var forkId = process.env.forkId; + var logSystem = 'Pool'; + var logComponent = coin; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); - var connection; + var connection = redis.createClient(redisConfig.port, redisConfig.host); - function connect(){ - - var reconnectTimeout; - - connection = redis.createClient(redisConfig.port, redisConfig.host); - connection.on('ready', function(){ - clearTimeout(reconnectTimeout); - logger.debug(logSystem, 'redis', 'Successfully connected to redis database'); - }); - connection.on('error', function(err){ - logger.error(logSystem, 'redis', 'Redis client had an error: ' + JSON.stringify(err)) - }); - connection.on('end', function(){ - logger.error(logSystem, 'redis', 'Connection to redis database as been ended'); - logger.warning(logSystem, 'redis', 'Trying reconnection in 3 seconds...'); - reconnectTimeout = setTimeout(function(){ - connect(); - }, 3000); - }); - } - connect(); + connection.on('ready', function(){ + logger.debug(logSystem, logComponent, logSubCat, 'Share processing setup with redis (' + redisConfig.host + + ':' + redisConfig.port + ')'); + }); + connection.on('error', function(err){ + logger.error(logSystem, logComponent, logSubCat, 'Redis client had an error: ' + JSON.stringify(err)) + }); + connection.on('end', function(){ + logger.error(logSystem, logComponent, logSubCat, 'Connection to redis database as been ended'); + }); @@ -78,9 +70,9 @@ module.exports = function(logger, poolConfig){ connection.multi(redisCommands).exec(function(err, replies){ if (err) - logger.error(logSystem, 'redis', 'Error with share processor multi ' + JSON.stringify(err)); + logger.error(logSystem, logComponent, logSubCat, 'Error with share processor multi ' + JSON.stringify(err)); else - logger.debug(logSystem, 'redis', 'Share data and stats recorded'); + logger.debug(logSystem, logComponent, logSubCat, 'Share data and stats recorded'); }); diff --git a/libs/stats.js b/libs/stats.js index 8e00f58..9977256 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -140,12 +140,11 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; - console.log([hashratePre, shareMultiplier, coinStats.shares, portalConfig.website.hashrateWindow]); coinStats.hashrate = hashratePre / 1e3 | 0; portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; - coinStats.hashrates; - coinStats.shares; + delete coinStats.hashrates; + delete coinStats.shares; }); _this.stats = portalStats; diff --git a/pool_configs/bitcoin_example.json b/pool_configs/bitcoin_example.json new file mode 100644 index 0000000..fb92dcd --- /dev/null +++ b/pool_configs/bitcoin_example.json @@ -0,0 +1,64 @@ +{ + "enabled": false, + "coin": "bitcoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "msjLr1XfpB6aAL1wi8e2CDnDSNhF4WrJ5n", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 18332, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "mtCiLWzBy9EpuxzkLwizPYiPFDy69HTd4b", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "6774": { + "diff": 1 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 18332, + "user": "testuser", + "password": "testpass" + } + ] +} \ No newline at end of file diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json index 7b8487a..7f8fda4 100644 --- a/pool_configs/darkcoin_example.json +++ b/pool_configs/darkcoin_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index e149565..3506975 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -11,7 +11,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json index 31292a4..3608317 100644 --- a/pool_configs/helixcoin_example.json +++ b/pool_configs/helixcoin_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 70, "minimumReserve": 10, "feePercent": 0.05, diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json index 9c49962..a963c4b 100644 --- a/pool_configs/hirocoin_example.json +++ b/pool_configs/hirocoin_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json index bc3e4eb..438bae6 100644 --- a/pool_configs/hobonickels_example.json +++ b/pool_configs/hobonickels_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": false, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index ce54056..acd9856 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 70, "minimumReserve": 10, "feePercent": 0.05, diff --git a/pool_configs/maxcoin_example.json b/pool_configs/maxcoin_example.json index 9319189..03c8189 100644 --- a/pool_configs/maxcoin_example.json +++ b/pool_configs/maxcoin_example.json @@ -11,7 +11,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 10, + "paymentInterval": 60, "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, diff --git a/pool_configs/ultracoin_example.json b/pool_configs/ultracoin_example.json new file mode 100644 index 0000000..2ad473c --- /dev/null +++ b/pool_configs/ultracoin_example.json @@ -0,0 +1,64 @@ +{ + "enabled": false, + "coin": "ultracoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "UZ3cz7EKjC4eRKbhiN9tA6xcaDGSVoESc8", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 18365, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "UhyKVr4m516TPsrLwm91pYL4f99VGrJ1oC", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "6856": { + "diff": 8 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 18365, + "user": "testuser", + "password": "testpass" + } + ] +} \ No newline at end of file From ba74b107d5cb8b051d817114a31412fb09855ad4 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 11:29:36 -0600 Subject: [PATCH 047/150] 1) Added link to supported hashing algos, 2) added vertcoin (scrypt-n) example --- README.md | 2 +- coins/vertcoin.json | 6 ++++ libs/paymentProcessor.js | 3 +- pool_configs/vertcoin_example.json | 57 ++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 coins/vertcoin.json create mode 100644 pool_configs/vertcoin_example.json diff --git a/README.md b/README.md index d8f0b22..ccd6a28 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ front-end website. #### Features * For the pool server it uses the highly efficient [node-stratum-pool](//github.com/zone117x/node-stratum-pool) module which -supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, several hashing algorithms. +supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, [several hashing algorithms](//github.com/zone117x/node-stratum-pool#hashing-algorithms-supported). * The portal has an [MPOS](//github.com/MPOS/php-mpos) compatibility mode so that the it can function as a drop-in-replacement for [python-stratum-mining](//github.com/Crypto-Expert/stratum-mining). This diff --git a/coins/vertcoin.json b/coins/vertcoin.json new file mode 100644 index 0000000..280beb4 --- /dev/null +++ b/coins/vertcoin.json @@ -0,0 +1,6 @@ +{ + "name": "Vertcoin", + "symbol": "VTC", + "algorithm": "scrypt-n", + "txMessages": false +} \ No newline at end of file diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 70b80aa..d01271b 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -64,7 +64,8 @@ function SetupForPool(logger, poolOptions, setupFinished){ 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'); + 'Daemon does not own pool address - payment processing can not be done with this daemon, ' + + JSON.stringify(result[0].response)); return; } callback() diff --git a/pool_configs/vertcoin_example.json b/pool_configs/vertcoin_example.json new file mode 100644 index 0000000..f16a917 --- /dev/null +++ b/pool_configs/vertcoin_example.json @@ -0,0 +1,57 @@ +{ + "enabled": true, + "coin": "vertcoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeReceiveAddress": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 13295, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + + "ports": { + "6381": { + "diff": 8 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 13295, + "user": "testuser", + "password": "testpass" + } + ] +} \ No newline at end of file From 83db1aab81586e693eb2bb2bba277ec6db5c3fa1 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 13:36:31 -0600 Subject: [PATCH 048/150] Added check in reading pool configs so only .json files are read. --- init.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/init.js b/init.js index c2cdebf..982b358 100644 --- a/init.js +++ b/init.js @@ -1,4 +1,5 @@ var fs = require('fs'); +var path = require('path'); var os = require('os'); var cluster = require('cluster'); @@ -73,8 +74,10 @@ if (cluster.isWorker){ //Read all pool configs from pool_configs and join them with their coin profile var buildPoolConfigs = function(){ var configs = {}; - fs.readdirSync('pool_configs').forEach(function(file){ - var poolOptions = JSON.parse(JSON.minify(fs.readFileSync('pool_configs/' + file, {encoding: 'utf8'}))); + var configDir = 'pool_configs/'; + fs.readdirSync(configDir).forEach(function(file){ + if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return; + var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'}))); if (!poolOptions.enabled) return; var coinFilePath = 'coins/' + poolOptions.coin; if (!fs.existsSync(coinFilePath)){ From 34b51def19a919ec31bd89e7b56a7ddf3989f198 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Mon, 31 Mar 2014 22:55:16 +0000 Subject: [PATCH 049/150] set enabled flag back to false --- pool_configs/vertcoin_example.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pool_configs/vertcoin_example.json b/pool_configs/vertcoin_example.json index f16a917..c554778 100644 --- a/pool_configs/vertcoin_example.json +++ b/pool_configs/vertcoin_example.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "coin": "vertcoin.json", "shareProcessing": { @@ -54,4 +54,4 @@ "password": "testpass" } ] -} \ No newline at end of file +} From 7a806b9b8c280113e35fbb78009a1459589bed92 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:14:11 -0600 Subject: [PATCH 050/150] Removed txMessages: false because its already false by default. Fixed ultracoin timestamp --- coins/alphacoin.json | 7 +++---- coins/anoncoin.json | 7 +++---- coins/auroracoin.json | 7 +++---- coins/bitcoin.json | 3 +-- coins/bottlecaps.json | 7 +++---- coins/casinocoin.json | 7 +++---- coins/catcoin.json | 7 +++---- coins/copperbars.json | 3 +-- coins/darkcoin.json | 3 +-- coins/diamondcoin.json | 8 ++++---- coins/digibyte.json | 7 +++---- coins/dogecoin.json | 7 +++---- coins/earthcoin.json | 7 +++---- coins/elephantcoin.json | 7 +++---- coins/emerald.json | 7 +++---- coins/execoin.json | 16 ++++++++++++++++ coins/ezcoin.json | 7 +++---- coins/fastcoin.json | 7 +++---- coins/flappycoin.json | 7 +++---- coins/florincoin.json | 8 ++++---- coins/frankocoin.json | 7 +++---- coins/galaxycoin.json | 7 +++---- coins/galleon.json | 7 +++---- coins/gamecoin.json | 7 +++---- coins/helixcoin.json | 7 +++---- coins/hirocoin.json | 3 +-- coins/hobonickels.json | 7 +++---- coins/junkcoin.json | 7 +++---- coins/kittehcoin.json | 7 +++---- coins/krugercoin.json | 7 +++---- coins/litecoin.json | 7 +++---- coins/lottocoin.json | 7 +++---- coins/luckycoin.json | 7 +++---- coins/maxcoin.json | 7 +++---- coins/memecoin.json | 7 +++---- coins/neocoin.json | 8 ++++---- coins/netcoin.json | 8 ++++---- coins/noirbits.json | 7 +++---- coins/peercoin.json | 3 +-- coins/phoenixcoin.json | 3 +-- coins/quarkcoin.json | 3 +-- coins/reddcoin.json | 7 +++---- coins/sexcoin.json | 7 +++---- coins/skeincoin.json | 3 +-- coins/spots.json | 7 +++---- coins/stablecoin.json | 7 +++---- coins/ultracoin.json | 3 +-- coins/vertcoin.json | 3 +-- coins/wecoin.json | 7 +++---- coins/xencoin.json | 7 +++---- coins/yacoin.json | 3 +-- 51 files changed, 148 insertions(+), 178 deletions(-) create mode 100644 coins/execoin.json diff --git a/coins/alphacoin.json b/coins/alphacoin.json index 24f3594..2d25d0e 100644 --- a/coins/alphacoin.json +++ b/coins/alphacoin.json @@ -1,6 +1,5 @@ { - "name" : "Alphacoin", - "symbol" : "ALF", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Alphacoin", + "symbol": "ALF", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/anoncoin.json b/coins/anoncoin.json index bd917fc..fcc48d5 100644 --- a/coins/anoncoin.json +++ b/coins/anoncoin.json @@ -1,6 +1,5 @@ { - "name" : "Anoncoin", - "symbol" : "ANC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Anoncoin", + "symbol": "ANC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/auroracoin.json b/coins/auroracoin.json index 172acd5..712ad07 100644 --- a/coins/auroracoin.json +++ b/coins/auroracoin.json @@ -1,6 +1,5 @@ { - "name" : "Auroracoin", - "symbol" : "AUR", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Auroracoin", + "symbol": "AUR", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/bitcoin.json b/coins/bitcoin.json index 93b1061..f0413e7 100644 --- a/coins/bitcoin.json +++ b/coins/bitcoin.json @@ -1,6 +1,5 @@ { "name": "Bitcoin", "symbol": "BTC", - "algorithm": "sha256", - "txMessages": false + "algorithm": "sha256" } \ No newline at end of file diff --git a/coins/bottlecaps.json b/coins/bottlecaps.json index da29c1b..1dcccb9 100644 --- a/coins/bottlecaps.json +++ b/coins/bottlecaps.json @@ -1,6 +1,5 @@ { - "name" : "Bottlecaps", - "symbol" : "CAP", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Bottlecaps", + "symbol": "CAP", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/casinocoin.json b/coins/casinocoin.json index b6b8e8b..d6b396f 100644 --- a/coins/casinocoin.json +++ b/coins/casinocoin.json @@ -1,6 +1,5 @@ { - "name" : "Casinocoin", - "symbol" : "CSC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Casinocoin", + "symbol": "CSC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/catcoin.json b/coins/catcoin.json index b03af91..6c951e3 100644 --- a/coins/catcoin.json +++ b/coins/catcoin.json @@ -1,6 +1,5 @@ { - "name" : "Catcoin", - "symbol" : "CAT", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Catcoin", + "symbol": "CAT", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/copperbars.json b/coins/copperbars.json index 597043d..b1ce0e5 100644 --- a/coins/copperbars.json +++ b/coins/copperbars.json @@ -2,6 +2,5 @@ "name": "Copperbars", "symbol": "CPR", "algorithm": "scrypt-jane", - "chainStartTime": 1376184687, - "txMessages": false + "chainStartTime": 1376184687 } \ No newline at end of file diff --git a/coins/darkcoin.json b/coins/darkcoin.json index cb970d5..ed863c3 100644 --- a/coins/darkcoin.json +++ b/coins/darkcoin.json @@ -1,6 +1,5 @@ { "name": "Darkcoin", "symbol": "DRK", - "algorithm": "x11", - "txMessages": false + "algorithm": "x11" } \ No newline at end of file diff --git a/coins/diamondcoin.json b/coins/diamondcoin.json index 838bc96..21a21d7 100644 --- a/coins/diamondcoin.json +++ b/coins/diamondcoin.json @@ -1,6 +1,6 @@ { - "name" : "Diamondcoin", - "symbol" : "DMD", - "algorithm" : "scrypt", - "txMessages" : true + "name": "Diamondcoin", + "symbol": "DMD", + "algorithm": "scrypt", + "txMessages": true } \ No newline at end of file diff --git a/coins/digibyte.json b/coins/digibyte.json index ef0333d..ed65fed 100644 --- a/coins/digibyte.json +++ b/coins/digibyte.json @@ -1,6 +1,5 @@ { - "name" : "Digibyte", - "symbol" : "DGB", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Digibyte", + "symbol": "DGB", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/dogecoin.json b/coins/dogecoin.json index f0aa6c8..88e28dc 100644 --- a/coins/dogecoin.json +++ b/coins/dogecoin.json @@ -1,6 +1,5 @@ { - "name" : "Dogecoin", - "symbol" : "DOGE", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Dogecoin", + "symbol": "DOGE", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/earthcoin.json b/coins/earthcoin.json index 4aabbb3..272ebda 100644 --- a/coins/earthcoin.json +++ b/coins/earthcoin.json @@ -1,6 +1,5 @@ { - "name" : "Earthcoin", - "symbol" : "EAC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Earthcoin", + "symbol": "EAC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/elephantcoin.json b/coins/elephantcoin.json index e1c1fdd..71ceaf8 100644 --- a/coins/elephantcoin.json +++ b/coins/elephantcoin.json @@ -1,6 +1,5 @@ { - "name" : "Elephantcoin", - "symbol" : "ELP", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Elephantcoin", + "symbol": "ELP", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/emerald.json b/coins/emerald.json index 49242fa..c717f7a 100644 --- a/coins/emerald.json +++ b/coins/emerald.json @@ -1,6 +1,5 @@ { - "name" : "Emerald", - "symbol" : "EMD", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Emerald", + "symbol": "EMD", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/execoin.json b/coins/execoin.json new file mode 100644 index 0000000..d877c57 --- /dev/null +++ b/coins/execoin.json @@ -0,0 +1,16 @@ +{ + "name": "Execoin", + "symbol": "EXE", + "algorithm": "scrypt-n", + "timeTable": { + "2048": 1390959880, + "4096": 1438295269, + "8192": 1485630658, + "16384": 1532966047, + "32768": 1580301436, + "65536": 1627636825, + "131072": 1674972214, + "262144": 1722307603 + } + +} \ No newline at end of file diff --git a/coins/ezcoin.json b/coins/ezcoin.json index 8edc861..378cd9e 100644 --- a/coins/ezcoin.json +++ b/coins/ezcoin.json @@ -1,6 +1,5 @@ { - "name" : "Ezcoin", - "symbol" : "EZC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Ezcoin", + "symbol": "EZC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/fastcoin.json b/coins/fastcoin.json index 0bf5409..077f767 100644 --- a/coins/fastcoin.json +++ b/coins/fastcoin.json @@ -1,6 +1,5 @@ { - "name" : "Fastcoin", - "symbol" : "FST", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Fastcoin", + "symbol": "FST", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/flappycoin.json b/coins/flappycoin.json index f6e4a6d..cbbcb99 100644 --- a/coins/flappycoin.json +++ b/coins/flappycoin.json @@ -1,6 +1,5 @@ { - "name" : "Flappycoin", - "symbol" : "FLAP", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Flappycoin", + "symbol": "FLAP", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/florincoin.json b/coins/florincoin.json index 0fabdcb..d6c81f0 100644 --- a/coins/florincoin.json +++ b/coins/florincoin.json @@ -1,6 +1,6 @@ { - "name" : "Florincoin", - "symbol" : "FLO", - "algorithm" : "scrypt", - "txMessages" : true + "name": "Florincoin", + "symbol": "FLO", + "algorithm": "scrypt", + "txMessages": true } \ No newline at end of file diff --git a/coins/frankocoin.json b/coins/frankocoin.json index a649419..4254146 100644 --- a/coins/frankocoin.json +++ b/coins/frankocoin.json @@ -1,6 +1,5 @@ { - "name" : "Frankocoin", - "symbol" : "FRK", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Frankocoin", + "symbol": "FRK", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/galaxycoin.json b/coins/galaxycoin.json index 8e95913..fa4e62e 100644 --- a/coins/galaxycoin.json +++ b/coins/galaxycoin.json @@ -1,6 +1,5 @@ { - "name" : "Galaxycoin", - "symbol" : "GLX", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Galaxycoin", + "symbol": "GLX", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/galleon.json b/coins/galleon.json index 53c6a2b..7e98f85 100644 --- a/coins/galleon.json +++ b/coins/galleon.json @@ -1,6 +1,5 @@ { - "name" : "Galleon", - "symbol" : "GLN", - "algorithm" : "keccak", - "txMessages" : false + "name": "Galleon", + "symbol": "GLN", + "algorithm": "keccak" } \ No newline at end of file diff --git a/coins/gamecoin.json b/coins/gamecoin.json index a7dc73b..8675565 100644 --- a/coins/gamecoin.json +++ b/coins/gamecoin.json @@ -1,6 +1,5 @@ { - "name" : "Gamecoin", - "symbol" : "GME", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Gamecoin", + "symbol": "GME", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/helixcoin.json b/coins/helixcoin.json index 2ae867b..9fc6f85 100644 --- a/coins/helixcoin.json +++ b/coins/helixcoin.json @@ -1,6 +1,5 @@ { - "name" : "Helixcoin", - "symbol" : "HXC", - "algorithm" : "max", - "txMessages" : false + "name": "Helixcoin", + "symbol": "HXC", + "algorithm": "max" } \ No newline at end of file diff --git a/coins/hirocoin.json b/coins/hirocoin.json index 197d366..3b21d84 100644 --- a/coins/hirocoin.json +++ b/coins/hirocoin.json @@ -1,6 +1,5 @@ { "name": "Hirocoin", "symbol": "hic", - "algorithm": "x11", - "txMessages": false + "algorithm": "x11" } \ No newline at end of file diff --git a/coins/hobonickels.json b/coins/hobonickels.json index b7eb3fb..6ff0fc9 100644 --- a/coins/hobonickels.json +++ b/coins/hobonickels.json @@ -1,6 +1,5 @@ { - "name" : "Hobonickels", - "symbol" : "HBN", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Hobonickels", + "symbol": "HBN", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/junkcoin.json b/coins/junkcoin.json index fe4f8db..f6bad9e 100644 --- a/coins/junkcoin.json +++ b/coins/junkcoin.json @@ -1,6 +1,5 @@ { - "name" : "Junkcoin", - "symbol" : "JKC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Junkcoin", + "symbol": "JKC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/kittehcoin.json b/coins/kittehcoin.json index cf4d776..c9293c2 100644 --- a/coins/kittehcoin.json +++ b/coins/kittehcoin.json @@ -1,6 +1,5 @@ { - "name" : "Kittehcoin", - "symbol" : "MEOW", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Kittehcoin", + "symbol": "MEOW", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/krugercoin.json b/coins/krugercoin.json index 7d9a07b..2f0c44e 100644 --- a/coins/krugercoin.json +++ b/coins/krugercoin.json @@ -1,6 +1,5 @@ { - "name" : "Krugercoin", - "symbol" : "KGC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Krugercoin", + "symbol": "KGC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/litecoin.json b/coins/litecoin.json index 536be58..73307fb 100644 --- a/coins/litecoin.json +++ b/coins/litecoin.json @@ -1,6 +1,5 @@ { - "name" : "Litecoin", - "symbol" : "LTC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Litecoin", + "symbol": "LTC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/lottocoin.json b/coins/lottocoin.json index 25d33a0..188238a 100644 --- a/coins/lottocoin.json +++ b/coins/lottocoin.json @@ -1,6 +1,5 @@ { - "name" : "Lottocoin", - "symbol" : "LOT", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Lottocoin", + "symbol": "LOT", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/luckycoin.json b/coins/luckycoin.json index 213986b..9d28b0b 100644 --- a/coins/luckycoin.json +++ b/coins/luckycoin.json @@ -1,6 +1,5 @@ { - "name" : "Luckycoin", - "symbol" : "LKY", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Luckycoin", + "symbol": "LKY", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/maxcoin.json b/coins/maxcoin.json index bd90799..33c286e 100644 --- a/coins/maxcoin.json +++ b/coins/maxcoin.json @@ -1,6 +1,5 @@ { - "name" : "Maxcoin", - "symbol" : "MAX", - "algorithm" : "keccak", - "txMessages" : false + "name": "Maxcoin", + "symbol": "MAX", + "algorithm": "keccak" } \ No newline at end of file diff --git a/coins/memecoin.json b/coins/memecoin.json index 566de59..bb982dc 100644 --- a/coins/memecoin.json +++ b/coins/memecoin.json @@ -1,6 +1,5 @@ { - "name" : "Memecoin", - "symbol" : "MEM", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Memecoin", + "symbol": "MEM", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/neocoin.json b/coins/neocoin.json index 316a6f2..61cd33e 100644 --- a/coins/neocoin.json +++ b/coins/neocoin.json @@ -1,6 +1,6 @@ { - "name" : "Neocoin", - "symbol" : "NEC", - "algorithm" : "scrypt", - "txMessages" : true + "name": "Neocoin", + "symbol": "NEC", + "algorithm": "scrypt", + "txMessages": true } \ No newline at end of file diff --git a/coins/netcoin.json b/coins/netcoin.json index 4554a73..8b6586a 100644 --- a/coins/netcoin.json +++ b/coins/netcoin.json @@ -1,6 +1,6 @@ { - "name" : "Netcoin", - "symbol" : "NET", - "algorithm" : "scrypt", - "txMessages" : true + "name": "Netcoin", + "symbol": "NET", + "algorithm": "scrypt", + "txMessages": true } \ No newline at end of file diff --git a/coins/noirbits.json b/coins/noirbits.json index e540b95..b2b73a8 100644 --- a/coins/noirbits.json +++ b/coins/noirbits.json @@ -1,6 +1,5 @@ { - "name" : "Noirbits", - "symbol" : "NRB", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Noirbits", + "symbol": "NRB", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/peercoin.json b/coins/peercoin.json index 12470ed..400df2d 100644 --- a/coins/peercoin.json +++ b/coins/peercoin.json @@ -1,6 +1,5 @@ { "name": "Peercoin", "symbol": "PPC", - "algorithm": "sha256", - "txMessages": false + "algorithm": "sha256" } \ No newline at end of file diff --git a/coins/phoenixcoin.json b/coins/phoenixcoin.json index 9e3695c..c7707dd 100644 --- a/coins/phoenixcoin.json +++ b/coins/phoenixcoin.json @@ -1,6 +1,5 @@ { "name" : "Phoenixcoin", "symbol" : "PXC", - "algorithm" : "scrypt", - "txMessages" : false + "algorithm" : "scrypt" } \ No newline at end of file diff --git a/coins/quarkcoin.json b/coins/quarkcoin.json index 084ae78..797e7cd 100644 --- a/coins/quarkcoin.json +++ b/coins/quarkcoin.json @@ -1,6 +1,5 @@ { "name": "Quarkcoin", "symbol": "QRK", - "algorithm": "quark", - "txMessages": false + "algorithm": "quark" } \ No newline at end of file diff --git a/coins/reddcoin.json b/coins/reddcoin.json index 29a017a..8e9f20d 100644 --- a/coins/reddcoin.json +++ b/coins/reddcoin.json @@ -1,6 +1,5 @@ { - "name" : "Reddcoin", - "symbol" : "REDD", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Reddcoin", + "symbol": "REDD", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/sexcoin.json b/coins/sexcoin.json index a558019..9bb4c24 100644 --- a/coins/sexcoin.json +++ b/coins/sexcoin.json @@ -1,6 +1,5 @@ { - "name" : "Sexcoin", - "symbol" : "SXC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Sexcoin", + "symbol": "SXC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/skeincoin.json b/coins/skeincoin.json index 283834d..b1636c6 100644 --- a/coins/skeincoin.json +++ b/coins/skeincoin.json @@ -1,6 +1,5 @@ { "name": "Skeincoin", "symbol": "SKC", - "algorithm": "skein", - "txMessages": false + "algorithm": "skein" } \ No newline at end of file diff --git a/coins/spots.json b/coins/spots.json index 8130747..89f9315 100644 --- a/coins/spots.json +++ b/coins/spots.json @@ -1,6 +1,5 @@ { - "name" : "Spots", - "symbol" : "SPT", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Spots", + "symbol": "SPT", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/stablecoin.json b/coins/stablecoin.json index 404e28a..ccb22f1 100644 --- a/coins/stablecoin.json +++ b/coins/stablecoin.json @@ -1,6 +1,5 @@ { - "name" : "Stablecoin", - "symbol" : "SBC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Stablecoin", + "symbol": "SBC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/ultracoin.json b/coins/ultracoin.json index 90aaae1..964b939 100644 --- a/coins/ultracoin.json +++ b/coins/ultracoin.json @@ -2,6 +2,5 @@ "name": "Ultracoin", "symbol": "UTC", "algorithm": "scrypt-jane", - "chainStartTime": 1388361600, - "txMessages": false + "chainStartTime": 1388361600 } \ No newline at end of file diff --git a/coins/vertcoin.json b/coins/vertcoin.json index 280beb4..5e74691 100644 --- a/coins/vertcoin.json +++ b/coins/vertcoin.json @@ -1,6 +1,5 @@ { "name": "Vertcoin", "symbol": "VTC", - "algorithm": "scrypt-n", - "txMessages": false + "algorithm": "scrypt-n" } \ No newline at end of file diff --git a/coins/wecoin.json b/coins/wecoin.json index cdac3d3..794acda 100644 --- a/coins/wecoin.json +++ b/coins/wecoin.json @@ -1,6 +1,5 @@ { - "name" : "Wecoin", - "symbol" : "WEC", - "algorithm" : "max", - "txMessages" : false + "name": "Wecoin", + "symbol": "WEC", + "algorithm": "max" } \ No newline at end of file diff --git a/coins/xencoin.json b/coins/xencoin.json index d961dd2..01b0045 100644 --- a/coins/xencoin.json +++ b/coins/xencoin.json @@ -1,6 +1,5 @@ { - "name" : "Xencoin", - "symbol" : "XNC", - "algorithm" : "scrypt", - "txMessages" : false + "name": "Xencoin", + "symbol": "XNC", + "algorithm": "scrypt" } \ No newline at end of file diff --git a/coins/yacoin.json b/coins/yacoin.json index 4d697ad..1f6d1eb 100644 --- a/coins/yacoin.json +++ b/coins/yacoin.json @@ -2,6 +2,5 @@ "name": "Yacoin", "symbol": "YAC", "algorithm": "scrypt-jane", - "chainStartTime": 1367991200, - "txMessages": false + "chainStartTime": 1367991200 } \ No newline at end of file From 5344471f465650563bcb4539bd0addbf2d883d53 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:44:05 -0600 Subject: [PATCH 051/150] Added link to coin configuration documentation, and added lots of scrypt-jane coins with their proper config --- README.md | 7 +++++-- coins/applecoin.json | 6 ++++++ coins/cachecoin.json | 6 ++++++ coins/freecoin.json | 8 ++++++++ coins/goldpressedlatinum.json | 6 ++++++ coins/internetcoin.json | 6 ++++++ coins/microcoin.json | 8 ++++++++ coins/onecoin.json | 7 +++++++ coins/radioactivecoin.json | 6 ++++++ coins/velocitycoin.json | 6 ++++++ coins/ybcoin.json | 6 ++++++ coins/zzcoin.json | 7 +++++++ 12 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 coins/applecoin.json create mode 100644 coins/cachecoin.json create mode 100644 coins/freecoin.json create mode 100644 coins/goldpressedlatinum.json create mode 100644 coins/internetcoin.json create mode 100644 coins/microcoin.json create mode 100644 coins/onecoin.json create mode 100644 coins/radioactivecoin.json create mode 100644 coins/velocitycoin.json create mode 100644 coins/ybcoin.json create mode 100644 coins/zzcoin.json diff --git a/README.md b/README.md index ccd6a28..d2a98fd 100644 --- a/README.md +++ b/README.md @@ -150,11 +150,14 @@ Here is an example of the required fields: { "name": "Litecoin", "symbol": "ltc", - "algorithm": "scrypt", //or "sha256", "scrypt-jane", "quark", "x11" - "txMessages": false //or true + "algorithm": "scrypt", //or "sha256", "scrypt-jane", "scrypt-n", "quark", "x11" + "txMessages": false, //or true (not required, defaults to false) } ```` +For additional documentation how to configure coins __(especially important for scrypt-n and scrypt-jane coins)__ +see these instructions: https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage + ##### Pool config Take a look at the example json file inside the `pool_configs` directory. Rename it to `yourcoin.json` and change the diff --git a/coins/applecoin.json b/coins/applecoin.json new file mode 100644 index 0000000..624320c --- /dev/null +++ b/coins/applecoin.json @@ -0,0 +1,6 @@ +{ + "name": "Applecoin", + "symbol": "APC", + "algorithm": "scrypt-jane", + "chainStartTime": 1384720832 +} \ No newline at end of file diff --git a/coins/cachecoin.json b/coins/cachecoin.json new file mode 100644 index 0000000..61d6f2e --- /dev/null +++ b/coins/cachecoin.json @@ -0,0 +1,6 @@ +{ + "name": "Cachecoin", + "symbol": "CACH", + "algorithm": "scrypt-jane", + "chainStartTime": 1388949883 +} \ No newline at end of file diff --git a/coins/freecoin.json b/coins/freecoin.json new file mode 100644 index 0000000..b58ac36 --- /dev/null +++ b/coins/freecoin.json @@ -0,0 +1,8 @@ +{ + "name": "Freecoin", + "symbol": "FEC", + "algorithm": "scrypt-jane", + "chainStartTime": 1375801200, + "nMin": 6, + "nMax": 32 +} \ No newline at end of file diff --git a/coins/goldpressedlatinum.json b/coins/goldpressedlatinum.json new file mode 100644 index 0000000..d61f262 --- /dev/null +++ b/coins/goldpressedlatinum.json @@ -0,0 +1,6 @@ +{ + "name": "GoldPressedLatinum", + "symbol": "GPL", + "algorithm": "scrypt-jane", + "chainStartTime": 1377557832 +} \ No newline at end of file diff --git a/coins/internetcoin.json b/coins/internetcoin.json new file mode 100644 index 0000000..bdccfa3 --- /dev/null +++ b/coins/internetcoin.json @@ -0,0 +1,6 @@ +{ + "name": "Internetcoin", + "symbol": "ITC", + "algorithm": "scrypt-jane", + "chainStartTime": 1388385602 +} \ No newline at end of file diff --git a/coins/microcoin.json b/coins/microcoin.json new file mode 100644 index 0000000..1d3792f --- /dev/null +++ b/coins/microcoin.json @@ -0,0 +1,8 @@ +{ + "name": "Microcoin", + "symbol": "MCR", + "algorithm": "scrypt-jane", + "chainStartTime": 1389028879, + "nMin": 6, + "nMax": 32 +} \ No newline at end of file diff --git a/coins/onecoin.json b/coins/onecoin.json new file mode 100644 index 0000000..b37345f --- /dev/null +++ b/coins/onecoin.json @@ -0,0 +1,7 @@ +{ + "name": "Onecoin", + "symbol": "ONC", + "algorithm": "scrypt-jane", + "chainStartTime": 1371119462, + "nMin": 6 +} \ No newline at end of file diff --git a/coins/radioactivecoin.json b/coins/radioactivecoin.json new file mode 100644 index 0000000..c7fbd18 --- /dev/null +++ b/coins/radioactivecoin.json @@ -0,0 +1,6 @@ +{ + "name": "Radioactivecoin", + "symbol": "RAD", + "algorithm": "scrypt-jane", + "chainStartTime": 1389196388 +} \ No newline at end of file diff --git a/coins/velocitycoin.json b/coins/velocitycoin.json new file mode 100644 index 0000000..24bad9c --- /dev/null +++ b/coins/velocitycoin.json @@ -0,0 +1,6 @@ +{ + "name": "Velocitycoin", + "symbol": "VEL", + "algorithm": "scrypt-jane", + "chainStartTime": 1387769316 +} \ No newline at end of file diff --git a/coins/ybcoin.json b/coins/ybcoin.json new file mode 100644 index 0000000..982007b --- /dev/null +++ b/coins/ybcoin.json @@ -0,0 +1,6 @@ +{ + "name": "YBcoin", + "symbol": "YBC", + "algorithm": "scrypt-jane", + "chainStartTime": 1372386273 +} \ No newline at end of file diff --git a/coins/zzcoin.json b/coins/zzcoin.json new file mode 100644 index 0000000..44dbf60 --- /dev/null +++ b/coins/zzcoin.json @@ -0,0 +1,7 @@ +{ + "name": "ZZcoin", + "symbol": "ZZC", + "algorithm": "scrypt-jane", + "chainStartTime": 1375817223, + "nMin": 12 +} \ No newline at end of file From 6a9f092034c6c74721bc3be3fb681d75d48fe364 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:45:08 -0600 Subject: [PATCH 052/150] minor readme change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2a98fd..80d2eb4 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Here is an example of the required fields: } ```` -For additional documentation how to configure coins __(especially important for scrypt-n and scrypt-jane coins)__ +For additional documentation how to configure coins **(especially important for scrypt-n and scrypt-jane coins)** see these instructions: https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage From d4e272bbc1112ca90aad0546481832f2ba7ccc74 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:46:14 -0600 Subject: [PATCH 053/150] minor readme change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80d2eb4..249917e 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Here is an example of the required fields: } ```` -For additional documentation how to configure coins **(especially important for scrypt-n and scrypt-jane coins)** +For additional documentation how to configure coins *(especially important for scrypt-n and scrypt-jane coins)* see these instructions: https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage From 043bce4ef58f531fc948679b75eb5e53cb6846d5 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:48:44 -0600 Subject: [PATCH 054/150] Minor readme change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 249917e..293cf94 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Here is an example of the required fields: ```` For additional documentation how to configure coins *(especially important for scrypt-n and scrypt-jane coins)* -see these instructions: https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage +see [these instructions](https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage). ##### Pool config From dac1908da1f8a6b96d0640452a6623b80f8f9cdd Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:56:46 -0600 Subject: [PATCH 055/150] Added feeCollectAccount which was missing from most examples. --- pool_configs/bitcoin_example.json | 1 + pool_configs/darkcoin_example.json | 1 + pool_configs/execoin_example.json | 58 +++++++++++++++++++++++++++++ pool_configs/galleon_example.json | 1 + pool_configs/hirocoin_example.json | 1 + pool_configs/maxcoin_example.json | 1 + pool_configs/ultracoin_example.json | 1 + pool_configs/vertcoin_example.json | 3 +- 8 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 pool_configs/execoin_example.json diff --git a/pool_configs/bitcoin_example.json b/pool_configs/bitcoin_example.json index fb92dcd..c6cd29b 100644 --- a/pool_configs/bitcoin_example.json +++ b/pool_configs/bitcoin_example.json @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "msjLr1XfpB6aAL1wi8e2CDnDSNhF4WrJ5n", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json index 7f8fda4..3ad716e 100644 --- a/pool_configs/darkcoin_example.json +++ b/pool_configs/darkcoin_example.json @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/execoin_example.json b/pool_configs/execoin_example.json new file mode 100644 index 0000000..afc7b20 --- /dev/null +++ b/pool_configs/execoin_example.json @@ -0,0 +1,58 @@ +{ + "enabled": false, + "coin": "execoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100.001, + "minimumReserve": 10, + "feePercent": 0.02, + "feeCollectAccount": "feesCollected", + "feeReceiveAddress": "ESfYVyfh1yZx1AkqJnu6rJje5nWE3f2C3r", + "feeWithdrawalThreshold": 5, + "daemon": { + "host": "localhost", + "port": 19847, + "user": "testuser", + "password": "testpass" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "EabPhjVKcmus3LKFViAcwq3dfo8kzqpMkQ", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + + "ports": { + "6368": { + "diff": 8 + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 19847, + "user": "testuser", + "password": "testpass" + } + ] +} \ No newline at end of file diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index 3506975..d257684 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -15,6 +15,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json index a963c4b..68bca63 100644 --- a/pool_configs/hirocoin_example.json +++ b/pool_configs/hirocoin_example.json @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/maxcoin_example.json b/pool_configs/maxcoin_example.json index 03c8189..a5a852c 100644 --- a/pool_configs/maxcoin_example.json +++ b/pool_configs/maxcoin_example.json @@ -15,6 +15,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "tQ2TKXXk2jh7EX2RJQ4ZxpZYEdRx7fdiaw", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/ultracoin_example.json b/pool_configs/ultracoin_example.json index 2ad473c..7b8ffa7 100644 --- a/pool_configs/ultracoin_example.json +++ b/pool_configs/ultracoin_example.json @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "UZ3cz7EKjC4eRKbhiN9tA6xcaDGSVoESc8", "feeWithdrawalThreshold": 5, "daemon": { diff --git a/pool_configs/vertcoin_example.json b/pool_configs/vertcoin_example.json index f16a917..2e191e4 100644 --- a/pool_configs/vertcoin_example.json +++ b/pool_configs/vertcoin_example.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "coin": "vertcoin.json", "shareProcessing": { @@ -10,6 +10,7 @@ "minimumPayment": 100.001, "minimumReserve": 10, "feePercent": 0.02, + "feeCollectAccount": "feesCollected", "feeReceiveAddress": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", "feeWithdrawalThreshold": 5, "daemon": { From 2ab598cd9badca591030b41c6693dbf62cdc9b67 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 31 Mar 2014 17:57:56 -0600 Subject: [PATCH 056/150] Updated the pool config disable option in readme --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 293cf94..841ea7f 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Description of options: ````javascript { - "disabled": false, //Set this to true and a pool will not be created from this config file + "enabled": true, //Set this to false and a pool will not be created from this config file "coin": "litecoin.json", //Reference to coin config file in 'coins' directory @@ -204,8 +204,11 @@ Description of options: /* (2% default) What percent fee your pool takes from the block reward. */ "feePercent": 0.02, - /* (Not implemented yet) Your address that receives pool revenue from fees */ - //"feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", + /* Name of the account to use when moving coin profit within daemon wallet. */ + "feeCollectAccount": "feesCollected", + + /* Your address that receives pool revenue from fees. */ + "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", /* (Not implemented yet) How many coins from fee revenue must accumulate on top of the minimum reserve amount in order to trigger withdrawal to fee address. The higher From 77ec4eee7d38c99f755ca65149be2253f0447671 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Tue, 1 Apr 2014 00:39:08 +0000 Subject: [PATCH 057/150] Added infinitecoin,emoticoin and carpediemcoin configs --- coins/carpediemcoin.json | 6 +++ coins/emoticoin.json | 6 +++ coins/infinitecoin.json | 6 +++ pool_configs/carpediemcoin_example.json | 70 +++++++++++++++++++++++++ pool_configs/emoticoin_example.json | 70 +++++++++++++++++++++++++ pool_configs/infinitecoin_example.json | 70 +++++++++++++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 coins/carpediemcoin.json create mode 100644 coins/emoticoin.json create mode 100644 coins/infinitecoin.json create mode 100644 pool_configs/carpediemcoin_example.json create mode 100644 pool_configs/emoticoin_example.json create mode 100644 pool_configs/infinitecoin_example.json diff --git a/coins/carpediemcoin.json b/coins/carpediemcoin.json new file mode 100644 index 0000000..966a013 --- /dev/null +++ b/coins/carpediemcoin.json @@ -0,0 +1,6 @@ +{ + "name": "Carpediemcoin", + "symbol": "DIEM", + "algorithm": "sha256", + "txMessages": false +} diff --git a/coins/emoticoin.json b/coins/emoticoin.json new file mode 100644 index 0000000..6e25e27 --- /dev/null +++ b/coins/emoticoin.json @@ -0,0 +1,6 @@ +{ + "name" : "Emoticoin", + "symbol" : "EMO", + "algorithm" : "scrypt", + "txMessages" : false +} diff --git a/coins/infinitecoin.json b/coins/infinitecoin.json new file mode 100644 index 0000000..aa4561d --- /dev/null +++ b/coins/infinitecoin.json @@ -0,0 +1,6 @@ +{ + "name" : "Infinitecoin", + "symbol" : "IFC", + "algorithm" : "scrypt", + "txMessages" : false +} diff --git a/pool_configs/carpediemcoin_example.json b/pool_configs/carpediemcoin_example.json new file mode 100644 index 0000000..beb2066 --- /dev/null +++ b/pool_configs/carpediemcoin_example.json @@ -0,0 +1,70 @@ +{ + "enabled": false, + "coin": "carpediemcoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 10000, + "minimumReserve": 10000, + "feePercent": 0.01, + "feeReceiveAddress": "13vMVvzddHCwsDAqSJW3bQUu7ZEQbopBty", + "feeWithdrawalThreshold": 10, + "daemon": { + "host": "localhost", + "port": 20000, + "user": "carpediemcoinrpc", + "password": "KB57jSBEbX2pCsfh2UPTfG4ZBvzB7qnqhNF5VNt4FfE" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "19tMKU5c8BkPyf2f2KnGngfsEh4RHonEqW", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3000": { + "varDiff": { + "minDiff": 4, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 20000, + "user": "carpediemcoinrpc", + "password": "KB57jSBEbX2pCsfh2UPTfG4ZBvzB7qnqhNF5VNt4FfE" + } + ] +} diff --git a/pool_configs/emoticoin_example.json b/pool_configs/emoticoin_example.json new file mode 100644 index 0000000..6587749 --- /dev/null +++ b/pool_configs/emoticoin_example.json @@ -0,0 +1,70 @@ +{ + "enabled": false, + "coin": "emoticoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100000, + "minimumReserve": 100000, + "feePercent": 0.01, + "feeReceiveAddress": "6Uivs1TiGHk9CbVGBPWgbgm4TPNTGocSHT", + "feeWithdrawalThreshold": 10, + "daemon": { + "host": "localhost", + "port": 20002, + "user": "emoticoinrpc", + "password": "8rTX7zSJB5szN3oLekP9W1DcitpcPfX2XXbv4Tmi3Wc4" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "6cS9GpkZeTxifJ9aiQRRRehiAepP1GDvYE", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3002": { + "varDiff": { + "minDiff": 4, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 20002, + "user": "emoticoinrpc", + "password": "8rTX7zSJB5szN3oLekP9W1DcitpcPfX2XXbv4Tmi3Wc4" + } + ] +} diff --git a/pool_configs/infinitecoin_example.json b/pool_configs/infinitecoin_example.json new file mode 100644 index 0000000..e5c8f42 --- /dev/null +++ b/pool_configs/infinitecoin_example.json @@ -0,0 +1,70 @@ +{ + "enabled": false, + "coin": "infinitecoin.json", + + "shareProcessing": { + "internal": { + "enabled": true, + "validateWorkerAddress": true, + "paymentInterval": 60, + "minimumPayment": 100000, + "minimumReserve": 100000, + "feePercent": 0.01, + "feeReceiveAddress": "i6yDb6Fa4wLvS11pniGixUVM3WupE5Bego", + "feeWithdrawalThreshold": 10, + "daemon": { + "host": "localhost", + "port": 20001, + "user": "infinitecoinrpc", + "password": "7g9HDJVH2S6iDWvYyMuzuKCuuHj6NfTk6cYvk5eurPme" + }, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "mpos": { + "enabled": false, + "host": "localhost", + "port": 3306, + "user": "me", + "password": "mypass", + "database": "ltc", + "stratumAuth": "password" + } + }, + + "address": "i82jdH89PuxHUmXKCRckea9cey5CJsQjpJ", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "connectionTimeout": 600, + + "banning": { + "enabled": true, + "time": 600, + "invalidPercent": 50, + "checkThreshold": 500, + "purgeInterval": 300 + }, + + "ports": { + "3001": { + "varDiff": { + "minDiff": 4, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + } + }, + + "daemons": [ + { + "host": "localhost", + "port": 20001, + "user": "infinitecoinrpc", + "password": "7g9HDJVH2S6iDWvYyMuzuKCuuHj6NfTk6cYvk5eurPme" + } + ] +} From 5624ddcd7449873ef6f9f3f5c62ad500e82694b5 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Apr 2014 09:41:13 -0600 Subject: [PATCH 058/150] Fixed incorrect link for coin config in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 841ea7f..ee91f6e 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Here is an example of the required fields: ```` For additional documentation how to configure coins *(especially important for scrypt-n and scrypt-jane coins)* -see [these instructions](https://github.com/zone117x/node-stratum-pool/edit/master/README.md#module-usage). +see [these instructions](//github.com/zone117x/node-stratum-pool#module-usage). ##### Pool config From 8ca0253eb6cf53a116357f4bd1a11a91d8ee9283 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Apr 2014 09:43:18 -0600 Subject: [PATCH 059/150] Clarified that payment processing is completed and working in readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee91f6e..df4369a 100644 --- a/README.md +++ b/README.md @@ -204,16 +204,16 @@ Description of options: /* (2% default) What percent fee your pool takes from the block reward. */ "feePercent": 0.02, - /* Name of the account to use when moving coin profit within daemon wallet. */ + /* Name of the daemon account to use when moving coin profit within daemon wallet. */ "feeCollectAccount": "feesCollected", /* Your address that receives pool revenue from fees. */ "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", - /* (Not implemented yet) How many coins from fee revenue must accumulate on top of the + /* How many coins from fee revenue must accumulate on top of the minimum reserve amount in order to trigger withdrawal to fee address. The higher this threshold, the less of your profit goes to transactions fees. */ - //"feeWithdrawalThreshold": 5, + "feeWithdrawalThreshold": 5, /* This daemon is used to send out payments. It MUST be for the daemon that owns the configured 'address' that receives the block rewards, otherwise the daemon will not From 9938681fbc71f87a8831369f94543eeec9a769dc Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Apr 2014 10:10:01 -0600 Subject: [PATCH 060/150] Added jobRebroadcastTimeout configuration --- README.md | 4 ++++ pool_configs/bitcoin_example.json | 1 + pool_configs/darkcoin_example.json | 1 + pool_configs/execoin_example.json | 1 + pool_configs/galleon_example.json | 1 + pool_configs/helixcoin_example.json | 1 + pool_configs/hirocoin_example.json | 1 + pool_configs/hobonickels_example.json | 1 + pool_configs/litecoin_example.json | 1 + pool_configs/maxcoin_example.json | 3 ++- pool_configs/ultracoin_example.json | 1 + pool_configs/vertcoin_example.json | 1 + 12 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df4369a..66f9f49 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,10 @@ Description of options: job broadcast. */ "txRefreshInterval": 20000, + /* Some miner software is bugged and will consider the pool offline if it doesn't receive + anything for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast + in this many seconds unless we find a new job. Set to zero or remove to disable this. */ + "jobRebroadcastTimeout": 55, //instanceId: 37, //Recommend not using this because a crypto-random one will be generated diff --git a/pool_configs/bitcoin_example.json b/pool_configs/bitcoin_example.json index c6cd29b..a18e6bf 100644 --- a/pool_configs/bitcoin_example.json +++ b/pool_configs/bitcoin_example.json @@ -38,6 +38,7 @@ "address": "mtCiLWzBy9EpuxzkLwizPYiPFDy69HTd4b", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json index 3ad716e..291cd46 100644 --- a/pool_configs/darkcoin_example.json +++ b/pool_configs/darkcoin_example.json @@ -38,6 +38,7 @@ "address": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/execoin_example.json b/pool_configs/execoin_example.json index afc7b20..6625326 100644 --- a/pool_configs/execoin_example.json +++ b/pool_configs/execoin_example.json @@ -38,6 +38,7 @@ "address": "EabPhjVKcmus3LKFViAcwq3dfo8kzqpMkQ", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json index d257684..32e3a41 100644 --- a/pool_configs/galleon_example.json +++ b/pool_configs/galleon_example.json @@ -5,6 +5,7 @@ "address": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "shareProcessing": { diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json index 3608317..9949a88 100644 --- a/pool_configs/helixcoin_example.json +++ b/pool_configs/helixcoin_example.json @@ -29,6 +29,7 @@ "address": "H9xyrh45LzLX4uXCP6jG6ZGrWho8srUgiG", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json index 68bca63..d0b27f7 100644 --- a/pool_configs/hirocoin_example.json +++ b/pool_configs/hirocoin_example.json @@ -39,6 +39,7 @@ "address": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json index 438bae6..275ad3f 100644 --- a/pool_configs/hobonickels_example.json +++ b/pool_configs/hobonickels_example.json @@ -38,6 +38,7 @@ "address": "EzGiarU2S56jyRkYpS7FKssXCS6AAJChdU", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index acd9856..223b33c 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -39,6 +39,7 @@ "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/maxcoin_example.json b/pool_configs/maxcoin_example.json index a5a852c..7907165 100644 --- a/pool_configs/maxcoin_example.json +++ b/pool_configs/maxcoin_example.json @@ -5,6 +5,7 @@ "address": "tKWkadAkT2vqK6v2PHLJmV1RTVvF7XZEgN", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "shareProcessing": { @@ -33,7 +34,7 @@ "ports": { "5547": { - "diff": 2 + "diff": 6 } }, diff --git a/pool_configs/ultracoin_example.json b/pool_configs/ultracoin_example.json index 7b8ffa7..cd6158d 100644 --- a/pool_configs/ultracoin_example.json +++ b/pool_configs/ultracoin_example.json @@ -38,6 +38,7 @@ "address": "UhyKVr4m516TPsrLwm91pYL4f99VGrJ1oC", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, "banning": { diff --git a/pool_configs/vertcoin_example.json b/pool_configs/vertcoin_example.json index 2f053ff..1cf3685 100644 --- a/pool_configs/vertcoin_example.json +++ b/pool_configs/vertcoin_example.json @@ -38,6 +38,7 @@ "address": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, "connectionTimeout": 600, From dafb0a7a55b522ec7a1d456639651654fa904664 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Apr 2014 16:39:27 -0600 Subject: [PATCH 061/150] Added check for dropkicked (self orphaned) blocks that have the same txHash --- libs/paymentProcessor.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index d01271b..a9edd14 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -123,9 +123,6 @@ function SetupForPool(logger, poolOptions, setupFinished){ - - - /* 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){ @@ -194,14 +191,14 @@ function SetupForPool(logger, poolOptions, setupFinished){ txDetails.forEach(function(tx, i){ var round = rounds[i]; - if (tx.error && tx.error.code === -5){ + if (tx.error && tx.error.code === -5 || round.solution !== tx.result.blockhash){ /* Block was dropped from coin daemon even after it happily accepted it earlier. */ //If we find another block at the same height then this block was drop-kicked orphaned - var dropKicked = !!rounds.filter(function(r){ + var dropKicked = rounds.filter(function(r){ return r.height === round.height && r.solution !== round.solution && r.category !== 'dropkicked'; - }).length; + }).length > 0; if (dropKicked){ logger.warning(logSystem, logComponent, @@ -536,8 +533,10 @@ function SetupForPool(logger, poolOptions, setupFinished){ 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'); + + logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + + ' ' + poolOptions.coin.symbol + ' was sent to ' + totalWorkers + ' miners'); + daemon.cmd('gettransaction', [results[0].response], function(results){ if (results[0].error){ callback('Check finished - error with gettransaction ' + JSON.stringify(results[0].error)); @@ -595,11 +594,12 @@ function SetupForPool(logger, poolOptions, setupFinished){ daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){ if (results[0].error){ - logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error)); + logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + + JSON.stringify(results[0].error)); return; } - logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol + - ' was sent to ' + processingConfig.feeReceiveAddress); + logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + + ' ' + poolOptions.coin.symbol + ' was sent to ' + processingConfig.feeReceiveAddress); }); } }); From 6aae77a15994137a2657a44edfb5ee8ad555c346 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Apr 2014 16:41:03 -0600 Subject: [PATCH 062/150] Removed all but one example config. --- pool_configs/bitcoin_example.json | 66 -------------------------- pool_configs/darkcoin_example.json | 66 -------------------------- pool_configs/execoin_example.json | 59 ----------------------- pool_configs/galleon_example.json | 57 ----------------------- pool_configs/helixcoin_example.json | 58 ----------------------- pool_configs/hirocoin_example.json | 67 --------------------------- pool_configs/hobonickels_example.json | 66 -------------------------- pool_configs/litecoin_example.json | 5 +- pool_configs/maxcoin_example.json | 49 -------------------- pool_configs/ultracoin_example.json | 66 -------------------------- pool_configs/vertcoin_example.json | 59 ----------------------- 11 files changed, 2 insertions(+), 616 deletions(-) delete mode 100644 pool_configs/bitcoin_example.json delete mode 100644 pool_configs/darkcoin_example.json delete mode 100644 pool_configs/execoin_example.json delete mode 100644 pool_configs/galleon_example.json delete mode 100644 pool_configs/helixcoin_example.json delete mode 100644 pool_configs/hirocoin_example.json delete mode 100644 pool_configs/hobonickels_example.json delete mode 100644 pool_configs/maxcoin_example.json delete mode 100644 pool_configs/ultracoin_example.json delete mode 100644 pool_configs/vertcoin_example.json diff --git a/pool_configs/bitcoin_example.json b/pool_configs/bitcoin_example.json deleted file mode 100644 index a18e6bf..0000000 --- a/pool_configs/bitcoin_example.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "enabled": false, - "coin": "bitcoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "msjLr1XfpB6aAL1wi8e2CDnDSNhF4WrJ5n", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 18332, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "mtCiLWzBy9EpuxzkLwizPYiPFDy69HTd4b", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "6774": { - "diff": 1 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 18332, - "user": "testuser", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/darkcoin_example.json b/pool_configs/darkcoin_example.json deleted file mode 100644 index 291cd46..0000000 --- a/pool_configs/darkcoin_example.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "enabled": false, - "coin": "darkcoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 18342, - "user": "darkcoinrpc1", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "XfkoYutJ8KYtLxZ4TgnMqC6SCmxuc3LEDY", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "4073": { - "diff": 0.002 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 18342, - "user": "darkcoinrpc1", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/execoin_example.json b/pool_configs/execoin_example.json deleted file mode 100644 index 6625326..0000000 --- a/pool_configs/execoin_example.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "enabled": false, - "coin": "execoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "ESfYVyfh1yZx1AkqJnu6rJje5nWE3f2C3r", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19847, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "EabPhjVKcmus3LKFViAcwq3dfo8kzqpMkQ", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - - "ports": { - "6368": { - "diff": 8 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 19847, - "user": "testuser", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/galleon_example.json b/pool_configs/galleon_example.json deleted file mode 100644 index 32e3a41..0000000 --- a/pool_configs/galleon_example.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "enabled": false, - "coin": "galleon.json", - - "address": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "GRAiuGCWLrL8Psdr6pkhLpxrQGHdYfrSEz", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19632, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - } - }, - - "ports": { - "3537": { - "diff": 4 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 19632, - "user": "testuser", - "password": "testpass" - } - ], - - "p2p": { - "enabled": false, - "host": "localhost", - "port": 19333, - "protocolVersion": 70002, - "magic": "fcc1b7dc" - } -} \ No newline at end of file diff --git a/pool_configs/helixcoin_example.json b/pool_configs/helixcoin_example.json deleted file mode 100644 index 9949a88..0000000 --- a/pool_configs/helixcoin_example.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "enabled": false, - "coin": "helixcoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 70, - "minimumReserve": 10, - "feePercent": 0.05, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "mppaGeNaSbG1Q7S6V3gL5uJztMhucgL9Vh", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19332, - "user": "litecoinrpc", - "password": "testnet" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - } - }, - - "address": "H9xyrh45LzLX4uXCP6jG6ZGrWho8srUgiG", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - - "ports": { - "3737": { - "diff": 0.2 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 16385, - "user": "testuser", - "password": "testpass" - } - ], - - "p2p": { - "enabled": false, - "host": "localhost", - "port": 19333, - "protocolVersion": 70002, - "magic": "fcc1b7dc" - } -} \ No newline at end of file diff --git a/pool_configs/hirocoin_example.json b/pool_configs/hirocoin_example.json deleted file mode 100644 index d0b27f7..0000000 --- a/pool_configs/hirocoin_example.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "enabled": false, - "coin": "hirocoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19389, - "user": "hirocoin", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - - "address": "HR6WNSR19kvaXSbwKymrba6uNx36BBjinS", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3073": { - "diff": 0.002 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 19389, - "user": "hirocoin", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/hobonickels_example.json b/pool_configs/hobonickels_example.json deleted file mode 100644 index 275ad3f..0000000 --- a/pool_configs/hobonickels_example.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "enabled": false, - "coin": "hobonickels.json", - - "shareProcessing": { - "internal": { - "enabled": false, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeReceiveAddress": "EhA4HXF7VPWfnV8TXerAP6p12BiEpXbiYR", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 19339, - "user": "hobonickelsrpc", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - - "address": "EzGiarU2S56jyRkYpS7FKssXCS6AAJChdU", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3033": { - "diff": 8 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 19339, - "user": "hobonickelsrpc", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 223b33c..044a1e9 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -6,7 +6,7 @@ "internal": { "enabled": true, "validateWorkerAddress": true, - "paymentInterval": 60, + "paymentInterval": 20, "minimumPayment": 70, "minimumReserve": 10, "feePercent": 0.05, @@ -35,7 +35,6 @@ } }, - "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", "blockRefreshInterval": 1000, "txRefreshInterval": 20000, @@ -51,7 +50,7 @@ }, "ports": { - "3008":{ + "3008": { "diff": 8, "varDiff": { "minDiff": 8, diff --git a/pool_configs/maxcoin_example.json b/pool_configs/maxcoin_example.json deleted file mode 100644 index 7907165..0000000 --- a/pool_configs/maxcoin_example.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "enabled": false, - "coin": "maxcoin.json", - - "address": "tKWkadAkT2vqK6v2PHLJmV1RTVvF7XZEgN", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "tQ2TKXXk2jh7EX2RJQ4ZxpZYEdRx7fdiaw", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 27932, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - } - }, - - "ports": { - "5547": { - "diff": 6 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 27932, - "user": "testuser", - "password": "testpass" - } - ] -} diff --git a/pool_configs/ultracoin_example.json b/pool_configs/ultracoin_example.json deleted file mode 100644 index cd6158d..0000000 --- a/pool_configs/ultracoin_example.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "enabled": false, - "coin": "ultracoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "UZ3cz7EKjC4eRKbhiN9tA6xcaDGSVoESc8", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 18365, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "UhyKVr4m516TPsrLwm91pYL4f99VGrJ1oC", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "6856": { - "diff": 8 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 18365, - "user": "testuser", - "password": "testpass" - } - ] -} \ No newline at end of file diff --git a/pool_configs/vertcoin_example.json b/pool_configs/vertcoin_example.json deleted file mode 100644 index 1cf3685..0000000 --- a/pool_configs/vertcoin_example.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "enabled": false, - "coin": "vertcoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100.001, - "minimumReserve": 10, - "feePercent": 0.02, - "feeCollectAccount": "feesCollected", - "feeReceiveAddress": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", - "feeWithdrawalThreshold": 5, - "daemon": { - "host": "localhost", - "port": 13295, - "user": "testuser", - "password": "testpass" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "VrcunuCcNUULMBoiw66v9SAdQq2m1FpP3C", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - - - "ports": { - "6381": { - "diff": 8 - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 13295, - "user": "testuser", - "password": "testpass" - } - ] -} From 7fe215679a6d7f774c73919776ca0985a3b32a9d Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Wed, 2 Apr 2014 14:56:13 +0400 Subject: [PATCH 063/150] fix payment redis final cleanout issue - possible double spending - optimized --- libs/paymentProcessor.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index a9edd14..af67f6a 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -493,6 +493,33 @@ function SetupForPool(logger, poolOptions, setupFinished){ }); }, + /* Call redis to check if previous sendmany and/or redis cleanout commands completed successfully. + If sendmany worked fine but redis commands failed you HAVE TO run redis commands again + (manually) to prevent double payments. If sendmany failed too you can safely delete + coin + '_finalRedisCommands' string from redis to let pool calculate payments again. */ + function(magnitude, workerPayments, finalRedisCommands, callback) { + redisClient.get(coin + '_finalRedisCommands', function(error, reply) { + if (error){ + callback('Check finished - error with redis getting finalRedisCommands' + JSON.stringify(error)); + return; + } + if (reply) { + callback('Check finished - previous sendmany and/or redis cleanout commands failed - ' + reply); + return; + } else { + /* There was no error in previous sendmany and/or redis cleanout commands + so we can safely continue */ + redisClient.set(coin + '_finalRedisCommands', JSON.stringify(finalRedisCommands), function(error, reply) { + if (error){ + callback('Check finished - error with saving finalRedisCommands' + JSON.stringify(error)); + return; + } + callback(null, magnitude, workerPayments, finalRedisCommands); + }); + } + }); + }, + function(magnitude, workerPayments, finalRedisCommands, callback){ //This does the final all-or-nothing atom transaction if block deamon sent payments From b08468ba8d25ad31bd1bc3c90f1ba3a267022062 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Apr 2014 13:01:05 -0600 Subject: [PATCH 064/150] Added "emitInvalidBlocksHahes" option for those in MPOS mode that require it. --- .gitignore | 3 +- README.md | 3 ++ coins/365coin.json | 5 ++++ config_example.json | 61 +++++++++++++++++++++++++++++++++++++++ libs/mposCompatibility.js | 2 +- libs/paymentProcessor.js | 13 +++++---- libs/poolWorker.js | 6 ++-- libs/shareProcessor.js | 4 +-- 8 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 coins/365coin.json create mode 100644 config_example.json diff --git a/.gitignore b/.gitignore index d35bbf7..88d9a09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.idea/ \ No newline at end of file +.idea/ +config.json \ No newline at end of file diff --git a/README.md b/README.md index 66f9f49..de2b6cd 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,9 @@ Description of options: detects those and disconnects them. */ "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds + /* Sometimes you want the block hashes even for shares that aren't block candidates. */ + "emitInvalidBlockHashes": false, + /* If a worker is submitting a high threshold of invalid shares we can temporarily ban them to reduce system/network load. Also useful to fight against flooding attacks. */ "banning": { diff --git a/coins/365coin.json b/coins/365coin.json new file mode 100644 index 0000000..c208e44 --- /dev/null +++ b/coins/365coin.json @@ -0,0 +1,5 @@ +{ + "name": "365coin", + "symbol": "365", + "algorithm": "keccak" +} \ No newline at end of file diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..b0fa531 --- /dev/null +++ b/config_example.json @@ -0,0 +1,61 @@ +{ + "logLevel": "debug", + "clustering": { + "enabled": true, + "forks": "auto" + }, + "blockNotifyListener": { + "enabled": false, + "port": 8117, + "password": "test" + }, + + "redisBlockNotifyListener": { + "enabled" : false, + "redisPort" : 6379, + "redisHost" : "hostname", + "psubscribeKey" : "newblocks:*" + }, + "website": { + "enabled": true, + "siteTitle": "Cryppit", + "port": 80, + "statUpdateInterval": 1.5, + "hashrateWindow": 300 + }, + "proxy": { + "enabled": false, + "ports": { + "80": { + "diff": 32, + "varDiff": { + "minDiff" : 8, + "maxDiff" : 512, + "targetTime" : 15, + "retargetTime" : 90, + "variancePercent" : 30 + } + }, + "6000": { + "diff": 32, + "varDiff": { + "minDiff" : 8, + "maxDiff" : 512, + "targetTime" : 15, + "retargetTime" : 90, + "variancePercent" : 30 + } + }, + "8080": { + "diff": 32, + "varDiff": { + "minDiff" : 8, + "maxDiff" : 512, + "targetTime" : 15, + "retargetTime" : 90, + "variancePercent" : 30 + } + } + } + } +} \ No newline at end of file diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index 4b75adb..26b30a2 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -70,7 +70,7 @@ module.exports = function(logger, poolConfig){ isValidBlock ? 'Y' : 'N', shareData.difficulty, typeof(shareData.error) === 'undefined' ? null : shareData.error, - typeof(shareData.solution) === 'undefined' ? '' : shareData.solution + shareData.blockHash ? shareData.blockHash : (shareData.blockHashInvalid ? shareData.blockHashInvalid : '') ]; connection.query( 'INSERT INTO `shares` SET time = NOW(), rem_host = ?, username = ?, our_result = ?, upstream_result = ?, difficulty = ?, reason = ?, solution = ?', diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index a9edd14..11ddc7f 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -160,7 +160,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ var details = r.split(':'); return { category: details[0].category, - solution: details[0], + blockHash: details[0], txHash: details[1], height: details[2], reward: details[3], @@ -191,20 +191,20 @@ function SetupForPool(logger, poolOptions, setupFinished){ txDetails.forEach(function(tx, i){ var round = rounds[i]; - if (tx.error && tx.error.code === -5 || round.solution !== tx.result.blockhash){ + if (tx.error && tx.error.code === -5 || round.blockHash !== tx.result.blockhash){ /* Block was dropped from coin daemon even after it happily accepted it earlier. */ //If we find another block at the same height then this block was drop-kicked orphaned var dropKicked = rounds.filter(function(r){ - return r.height === round.height && r.solution !== round.solution && r.category !== 'dropkicked'; + return r.height === round.height && r.blockHash !== round.blockHash && r.category !== 'dropkicked'; }).length > 0; 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); + + ' - we found a better block at the same height, blockHash ' + + round.blockHash + " round " + round.height); round.category = 'dropkicked'; } else{ @@ -298,7 +298,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ if (!workerShares){ logger.error(logSystem, logComponent, 'No worker shares for round: ' - + round.height + ' solution: ' + round.solution); + + round.height + ' blockHash: ' + round.blockHash); return; } @@ -486,6 +486,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ if (toBePaid !== 0) finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]); + finalRedisCommands.push(['bgsave']); callback(null, magnitude, workerPayments, finalRedisCommands); diff --git a/libs/poolWorker.js b/libs/poolWorker.js index fdcde2f..453ea08 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -118,11 +118,11 @@ module.exports = function(logger){ var shareData = JSON.stringify(data); - if (data.solution && !isValidBlock) - logger.debug(logSystem, logComponent, logSubCat, 'We thought a block solution was found but it was rejected by the daemon, share data: ' + shareData); + if (data.blockHash && !isValidBlock) + logger.debug(logSystem, logComponent, logSubCat, 'We thought a block was found but it was rejected by the daemon, share data: ' + shareData); else if (isValidBlock) - logger.debug(logSystem, logComponent, logSubCat, 'Block solution found: ' + data.solution); + logger.debug(logSystem, logComponent, logSubCat, 'Block found: ' + data.blockHash); if (isValidShare) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 938bd48..57df98e 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -61,10 +61,10 @@ module.exports = function(logger, poolConfig){ if (isValidBlock){ redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]); - redisCommands.push(['sadd', coin + '_blocksPending', [shareData.solution, shareData.tx, shareData.height, shareData.reward].join(':')]); + redisCommands.push(['sadd', coin + '_blocksPending', [shareData.blockHash, shareData.txHash, shareData.height, shareData.reward].join(':')]); redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]); } - else if (shareData.solution){ + else if (shareData.blockHash){ redisCommands.push(['hincrby', coin + '_stats', 'invalidBlocks', 1]); } From 4904f050f5e6e56087acf6df91eba05916a3f9ab Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 2 Apr 2014 13:02:11 -0600 Subject: [PATCH 065/150] Delete config.json --- config.json | 61 ----------------------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 config.json diff --git a/config.json b/config.json deleted file mode 100644 index b0fa531..0000000 --- a/config.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "logLevel": "debug", - "clustering": { - "enabled": true, - "forks": "auto" - }, - "blockNotifyListener": { - "enabled": false, - "port": 8117, - "password": "test" - }, - - "redisBlockNotifyListener": { - "enabled" : false, - "redisPort" : 6379, - "redisHost" : "hostname", - "psubscribeKey" : "newblocks:*" - }, - "website": { - "enabled": true, - "siteTitle": "Cryppit", - "port": 80, - "statUpdateInterval": 1.5, - "hashrateWindow": 300 - }, - "proxy": { - "enabled": false, - "ports": { - "80": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } - }, - "6000": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } - }, - "8080": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } - } - } - } -} \ No newline at end of file From 53283f502289f23c597b995342ff5c842932688a Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 2 Apr 2014 13:06:06 -0600 Subject: [PATCH 066/150] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de2b6cd..75c9990 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ npm update #### 2) Configuration ##### Portal config -Inside the `config.json` file, ensure the default configuration will work for your environment. +Inside the `config_example.json` file, ensure the default configuration will work for your environment, then copy the file to `config.json`. Explanation for each field: ````javascript @@ -232,7 +232,9 @@ Description of options: } }, - "mpos": { //Enabled this and shares will be inserted into share table in a MySQL database + /* Enabled mpos and shares will be inserted into share table in a MySQL database. You may + also want to use the "emitInvalidBlockHashes" option below if you require it. */ + "mpos": { "enabled": false, "host": "localhost", //MySQL db host "port": 3306, //MySQL db port From db99f49ec05857e3c814d63f7988216ca0753b80 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Apr 2014 14:09:57 -0600 Subject: [PATCH 067/150] Added unsupported algorithm error and fixed algo for helixcoin --- coins/helixcoin.json | 2 +- init.js | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/coins/helixcoin.json b/coins/helixcoin.json index 9fc6f85..65302ec 100644 --- a/coins/helixcoin.json +++ b/coins/helixcoin.json @@ -1,5 +1,5 @@ { "name": "Helixcoin", "symbol": "HXC", - "algorithm": "max" + "algorithm": "keccak" } \ No newline at end of file diff --git a/init.js b/init.js index 982b358..decf2cb 100644 --- a/init.js +++ b/init.js @@ -3,15 +3,18 @@ var path = require('path'); var os = require('os'); var cluster = require('cluster'); -var async = require('async'); -var posix = require('posix'); -var PoolLogger = require('./libs/logUtil.js'); -var BlocknotifyListener = require('./libs/blocknotifyListener.js'); +var async = require('async'); +var posix = require('posix'); +var PoolLogger = require('./libs/logUtil.js'); +var BlocknotifyListener = require('./libs/blocknotifyListener.js'); var RedisBlocknotifyListener = require('./libs/redisblocknotifyListener.js'); -var WorkerListener = require('./libs/workerListener.js'); -var PoolWorker = require('./libs/poolWorker.js'); -var PaymentProcessor = require('./libs/paymentProcessor.js'); -var Website = require('./libs/website.js'); +var WorkerListener = require('./libs/workerListener.js'); +var PoolWorker = require('./libs/poolWorker.js'); +var PaymentProcessor = require('./libs/paymentProcessor.js'); +var Website = require('./libs/website.js'); + +var algos = require('stratum-pool/lib/algoProperties.js'); + JSON.minify = JSON.minify || require("node-json-minify"); var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); @@ -88,6 +91,12 @@ var buildPoolConfigs = function(){ var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'}))); poolOptions.coin = coinProfile; configs[poolOptions.coin.name] = poolOptions; + + if (!(coinProfile.algorithm in algos)){ + logger.error('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"'); + delete configs[poolOptions.coin.name]; + } + }); return configs; }; From a9132319cc1c573882c819c31e5749d71ae7b0c4 Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Thu, 3 Apr 2014 00:43:58 +0400 Subject: [PATCH 068/150] fix payment redis final cleanout issue - possible double spending - optimized - fixed --- libs/paymentProcessor.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 3f1f2cb..91bfeb4 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -470,27 +470,24 @@ function SetupForPool(logger, poolOptions, setupFinished){ 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 (toBePaid !== 0) finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]); + finalRedisCommands.push(['del', coin + '_finalRedisCommands']); + finalRedisCommands.push(['bgsave']); callback(null, magnitude, workerPayments, finalRedisCommands); - }); }, From ba1836b6053899834f29dfc79562fe43d54d9ffd Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Wed, 2 Apr 2014 21:42:50 +0000 Subject: [PATCH 069/150] Added stats grouped by algorithm --- libs/stats.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/libs/stats.js b/libs/stats.js index 9977256..d0c16d5 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -14,13 +14,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ var redisClients = []; - /*var algoMultipliers = { - 'x11': Math.pow(2, 16), - 'scrypt': Math.pow(2, 16), - 'scrypt-jane': Math.pow(2,16), - 'sha256': Math.pow(2, 32) - };*/ - var canDoStats = true; Object.keys(poolConfigs).forEach(function(coin){ @@ -55,7 +48,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ this.stats = {}; this.statsString = ''; - this.getGlobalStats = function(callback){ var allCoinStats = {}; @@ -76,6 +68,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ var commandsPerCoin = redisComamndTemplates.length; + client.coins.map(function(coin){ redisComamndTemplates.map(function(t){ var clonedTemplates = t.slice(0); @@ -84,6 +77,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); }); + client.client.multi(redisCommands).exec(function(err, replies){ if (err){ console.log('error with getting hashrate stats ' + JSON.stringify(err)); @@ -121,6 +115,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ workers: 0, hashrate: 0 }, + algos: {}, pools: allCoinStats }; @@ -143,6 +138,18 @@ module.exports = function(logger, portalConfig, poolConfigs){ coinStats.hashrate = hashratePre / 1e3 | 0; portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; + + /* algorithm specific global stats */ + var algo = coinStats.algorithm; + if (!portalStats.algos.hasOwnProperty(algo)){ + portalStats.algos[algo] = { + workers: 0, + hashrate: 0 + }; + } + portalStats.algos[algo].hashrate += coinStats.hashrate; + portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; + delete coinStats.hashrates; delete coinStats.shares; }); From c0dae1daa7f0f0769442d61c4f4ead55d32ad291 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Wed, 2 Apr 2014 21:46:52 +0000 Subject: [PATCH 070/150] Revert "Added infinitecoin,emoticoin and carpediemcoin configs" This reverts commit 77ec4eee7d38c99f755ca65149be2253f0447671. --- coins/carpediemcoin.json | 6 --- coins/emoticoin.json | 6 --- coins/infinitecoin.json | 6 --- pool_configs/carpediemcoin_example.json | 70 ------------------------- pool_configs/emoticoin_example.json | 70 ------------------------- pool_configs/infinitecoin_example.json | 70 ------------------------- 6 files changed, 228 deletions(-) delete mode 100644 coins/carpediemcoin.json delete mode 100644 coins/emoticoin.json delete mode 100644 coins/infinitecoin.json delete mode 100644 pool_configs/carpediemcoin_example.json delete mode 100644 pool_configs/emoticoin_example.json delete mode 100644 pool_configs/infinitecoin_example.json diff --git a/coins/carpediemcoin.json b/coins/carpediemcoin.json deleted file mode 100644 index 966a013..0000000 --- a/coins/carpediemcoin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Carpediemcoin", - "symbol": "DIEM", - "algorithm": "sha256", - "txMessages": false -} diff --git a/coins/emoticoin.json b/coins/emoticoin.json deleted file mode 100644 index 6e25e27..0000000 --- a/coins/emoticoin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name" : "Emoticoin", - "symbol" : "EMO", - "algorithm" : "scrypt", - "txMessages" : false -} diff --git a/coins/infinitecoin.json b/coins/infinitecoin.json deleted file mode 100644 index aa4561d..0000000 --- a/coins/infinitecoin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name" : "Infinitecoin", - "symbol" : "IFC", - "algorithm" : "scrypt", - "txMessages" : false -} diff --git a/pool_configs/carpediemcoin_example.json b/pool_configs/carpediemcoin_example.json deleted file mode 100644 index beb2066..0000000 --- a/pool_configs/carpediemcoin_example.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "enabled": false, - "coin": "carpediemcoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 10000, - "minimumReserve": 10000, - "feePercent": 0.01, - "feeReceiveAddress": "13vMVvzddHCwsDAqSJW3bQUu7ZEQbopBty", - "feeWithdrawalThreshold": 10, - "daemon": { - "host": "localhost", - "port": 20000, - "user": "carpediemcoinrpc", - "password": "KB57jSBEbX2pCsfh2UPTfG4ZBvzB7qnqhNF5VNt4FfE" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "19tMKU5c8BkPyf2f2KnGngfsEh4RHonEqW", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3000": { - "varDiff": { - "minDiff": 4, //Minimum difficulty - "maxDiff": 512, //Network difficulty will be used if it is lower than this - "targetTime": 15, //Try to get 1 share per this many seconds - "retargetTime": 90, //Check to see if we should retarget every this many seconds - "variancePercent": 30 //Allow time to very this % from target without retargeting - } - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 20000, - "user": "carpediemcoinrpc", - "password": "KB57jSBEbX2pCsfh2UPTfG4ZBvzB7qnqhNF5VNt4FfE" - } - ] -} diff --git a/pool_configs/emoticoin_example.json b/pool_configs/emoticoin_example.json deleted file mode 100644 index 6587749..0000000 --- a/pool_configs/emoticoin_example.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "enabled": false, - "coin": "emoticoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100000, - "minimumReserve": 100000, - "feePercent": 0.01, - "feeReceiveAddress": "6Uivs1TiGHk9CbVGBPWgbgm4TPNTGocSHT", - "feeWithdrawalThreshold": 10, - "daemon": { - "host": "localhost", - "port": 20002, - "user": "emoticoinrpc", - "password": "8rTX7zSJB5szN3oLekP9W1DcitpcPfX2XXbv4Tmi3Wc4" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "6cS9GpkZeTxifJ9aiQRRRehiAepP1GDvYE", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3002": { - "varDiff": { - "minDiff": 4, //Minimum difficulty - "maxDiff": 512, //Network difficulty will be used if it is lower than this - "targetTime": 15, //Try to get 1 share per this many seconds - "retargetTime": 90, //Check to see if we should retarget every this many seconds - "variancePercent": 30 //Allow time to very this % from target without retargeting - } - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 20002, - "user": "emoticoinrpc", - "password": "8rTX7zSJB5szN3oLekP9W1DcitpcPfX2XXbv4Tmi3Wc4" - } - ] -} diff --git a/pool_configs/infinitecoin_example.json b/pool_configs/infinitecoin_example.json deleted file mode 100644 index e5c8f42..0000000 --- a/pool_configs/infinitecoin_example.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "enabled": false, - "coin": "infinitecoin.json", - - "shareProcessing": { - "internal": { - "enabled": true, - "validateWorkerAddress": true, - "paymentInterval": 60, - "minimumPayment": 100000, - "minimumReserve": 100000, - "feePercent": 0.01, - "feeReceiveAddress": "i6yDb6Fa4wLvS11pniGixUVM3WupE5Bego", - "feeWithdrawalThreshold": 10, - "daemon": { - "host": "localhost", - "port": 20001, - "user": "infinitecoinrpc", - "password": "7g9HDJVH2S6iDWvYyMuzuKCuuHj6NfTk6cYvk5eurPme" - }, - "redis": { - "host": "localhost", - "port": 6379 - } - }, - "mpos": { - "enabled": false, - "host": "localhost", - "port": 3306, - "user": "me", - "password": "mypass", - "database": "ltc", - "stratumAuth": "password" - } - }, - - "address": "i82jdH89PuxHUmXKCRckea9cey5CJsQjpJ", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "connectionTimeout": 600, - - "banning": { - "enabled": true, - "time": 600, - "invalidPercent": 50, - "checkThreshold": 500, - "purgeInterval": 300 - }, - - "ports": { - "3001": { - "varDiff": { - "minDiff": 4, //Minimum difficulty - "maxDiff": 512, //Network difficulty will be used if it is lower than this - "targetTime": 15, //Try to get 1 share per this many seconds - "retargetTime": 90, //Check to see if we should retarget every this many seconds - "variancePercent": 30 //Allow time to very this % from target without retargeting - } - } - }, - - "daemons": [ - { - "host": "localhost", - "port": 20001, - "user": "infinitecoinrpc", - "password": "7g9HDJVH2S6iDWvYyMuzuKCuuHj6NfTk6cYvk5eurPme" - } - ] -} From abcf97d34f951d8e1f76c6ca5778549e342a1c86 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Apr 2014 16:17:57 -0600 Subject: [PATCH 071/150] Blocknotify listener errors are logged and don't crash the system. --- libs/blocknotifyListener.js | 39 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/libs/blocknotifyListener.js b/libs/blocknotifyListener.js index 1f283b1..921e1f4 100644 --- a/libs/blocknotifyListener.js +++ b/libs/blocknotifyListener.js @@ -17,27 +17,34 @@ var listener = module.exports = function listener(options){ } var blockNotifyServer = net.createServer(function(c) { + emitLog('Block listener has incoming connection'); var data = ''; - c.on('data', function(d){ - emitLog('Block listener received blocknotify data'); - data += d; - if (data.slice(-1) === '\n'){ - c.end(); - } - }); - c.on('end', function() { + try { + c.on('data', function (d) { + emitLog('Block listener received blocknotify data'); + data += d; + if (data.slice(-1) === '\n') { + c.end(); + } + }); + c.on('end', function () { - emitLog('Block listener connection ended'); + emitLog('Block listener connection ended'); - var message = JSON.parse(data); - if (message.password === options.password){ - _this.emit('hash', message); - } - else - emitLog('Block listener received notification with incorrect password'); + var message = JSON.parse(data); + if (message.password === options.password) { + _this.emit('hash', message); + } + else + emitLog('Block listener received notification with incorrect password'); + + }); + } + catch(e){ + emitLog('Block listener failed to parse message ' + data); + } - }); }); blockNotifyServer.listen(options.port, function() { emitLog('Block notify listener server started on port ' + options.port) From 5918baac2f577c1ea20648f9f33b4ecccc5621d4 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Wed, 2 Apr 2014 23:36:36 +0000 Subject: [PATCH 072/150] added human readable auto-scaling hash rate strings to stats --- libs/stats.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/libs/stats.js b/libs/stats.js index d0c16d5..10e4af0 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -115,7 +115,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ workers: 0, hashrate: 0 }, - algos: {}, + algos: {}, pools: allCoinStats }; @@ -139,19 +139,25 @@ module.exports = function(logger, portalConfig, poolConfigs){ portalStats.global.hashrate += coinStats.hashrate; portalStats.global.workers += Object.keys(coinStats.workers).length; - /* algorithm specific global stats */ - var algo = coinStats.algorithm; - if (!portalStats.algos.hasOwnProperty(algo)){ - portalStats.algos[algo] = { - workers: 0, - hashrate: 0 - }; - } + /* algorithm specific global stats */ + var algo = coinStats.algorithm; + if (!portalStats.algos.hasOwnProperty(algo)){ + portalStats.algos[algo] = { + workers: 0, + hashrate: 0, + hashrateString: null + }; + } portalStats.algos[algo].hashrate += coinStats.hashrate; portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; delete coinStats.hashrates; delete coinStats.shares; + coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate); + }); + + Object.keys(portalStats.algos).forEach(function(algo){ + algo.hashrateString = _this.getReadableHashRateString(algo.hashrate); }); _this.stats = portalStats; @@ -160,5 +166,16 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); }; + + this.getReadableHashRateString = function(hashrate){ + var i = -1; + var byteUnits = [' KH', ' MH', ' GH', ' TH', 'PH' ]; + do { + hashrate = hashrate / 1024; + i++; + } while (hashrate > 1024); + return hashrate.toFixed(2) + byteUnits[i]; + }; + }; From 8849d0ade483f0c7614be48ce918c1527b2a629a Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Apr 2014 18:30:09 -0600 Subject: [PATCH 073/150] Added donation addresses --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 75c9990..b174d4c 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,14 @@ To support development of this project feel free to donate :) BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR +BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR +LTC: LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE +VTC: VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN +MAX: mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT +QRK: QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC +DRK: XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ +DOGE: DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW +Cryptsy Trade Key: 254ca13444be14937b36c44ba29160bd8f02ff76 Credits ------- From 24f4f22aa046957827cbd96834bdea89b3a25ae7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Apr 2014 18:31:03 -0600 Subject: [PATCH 074/150] Added donation addresses --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b174d4c..cfb8e7c 100644 --- a/README.md +++ b/README.md @@ -385,16 +385,14 @@ Donations --------- To support development of this project feel free to donate :) -BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR - -BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR -LTC: LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE -VTC: VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN -MAX: mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT -QRK: QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC -DRK: XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ -DOGE: DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW -Cryptsy Trade Key: 254ca13444be14937b36c44ba29160bd8f02ff76 +* BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR +* LTC: LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE +* VTC: VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN +* MAX: mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT +* QRK: QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC +* DRK: XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ +* DOGE: DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW +* Cryptsy Trade Key: 254ca13444be14937b36c44ba29160bd8f02ff76 Credits ------- From 795b93e254499027a751ada5b49861b231fcec94 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 3 Apr 2014 02:04:39 +0000 Subject: [PATCH 075/150] fixed bug in collection of per algo hash rate string --- libs/stats.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/stats.js b/libs/stats.js index 10e4af0..5c31ccb 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -157,7 +157,8 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); Object.keys(portalStats.algos).forEach(function(algo){ - algo.hashrateString = _this.getReadableHashRateString(algo.hashrate); + var algoStats = portalStats.algos[algo]; + algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate); }); _this.stats = portalStats; @@ -169,10 +170,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ this.getReadableHashRateString = function(hashrate){ var i = -1; - var byteUnits = [' KH', ' MH', ' GH', ' TH', 'PH' ]; + var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ]; do { hashrate = hashrate / 1024; - i++; + i++; } while (hashrate > 1024); return hashrate.toFixed(2) + byteUnits[i]; }; From 361641019f744ad3efe91e8113215dfe7aeacb8d Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 3 Apr 2014 12:33:10 -0600 Subject: [PATCH 076/150] Better logging for shares --- libs/poolWorker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 453ea08..97d2283 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -126,10 +126,10 @@ module.exports = function(logger){ if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Valid share of difficulty ' + data.difficulty + ' by ' + data.worker + ' [' + data.ip + ']' ); + logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at ' + data.difficulty + ' with diff ' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); else if (!isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Invalid share submitted, share data: ' + shareData); + logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData); handlers.share(isValidShare, isValidBlock, data) From 257010f6a4f92e9ce18f41810b6eaa082a96ff80 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 3 Apr 2014 14:37:55 -0600 Subject: [PATCH 077/150] Added low share diff tolerance. --- README.md | 51 ++++++++++++++++-------------- libs/poolWorker.js | 2 +- pool_configs/litecoin_example.json | 16 ++++++---- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cfb8e7c..fbafa84 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,34 @@ Description of options: "enabled": true, //Set this to false and a pool will not be created from this config file "coin": "litecoin.json", //Reference to coin config file in 'coins' directory + "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given + + "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds + + /* How many milliseconds should have passed before new block transactions will trigger a new + job broadcast. */ + "txRefreshInterval": 20000, + + /* Some miner software is bugged and will consider the pool offline if it doesn't receive + anything for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast + in this many seconds unless we find a new job. Set to zero or remove to disable this. */ + "jobRebroadcastTimeout": 55, + + //instanceId: 37, //Recommend not using this because a crypto-random one will be generated + + /* Some attackers will create thousands of workers that use up all available socket connections, + usually the workers are zombies and don't submit shares after connecting. This feature + detects those and disconnects them. */ + "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds + + /* Sometimes you want the block hashes even for shares that aren't block candidates. */ + "emitInvalidBlockHashes": false, + + /* We use proper maximum algorithm difficulties found in the coin daemon source code. Most + miners/pools that deal with scrypt use a guesstimated one that is about 5.86% off from the + actual one. So here we can set a tolerable threshold for if a share is slightly too low + due to mining apps using incorrect max diffs and this pool using correct max diffs. */ + "shareVariancePercent": 10, /* This determines what to do with submitted shares (and stratum worker authentication). You have two options: @@ -249,29 +277,6 @@ Description of options: } }, - "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given - - "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds - - /* How many milliseconds should have passed before new block transactions will trigger a new - job broadcast. */ - "txRefreshInterval": 20000, - - /* Some miner software is bugged and will consider the pool offline if it doesn't receive - anything for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast - in this many seconds unless we find a new job. Set to zero or remove to disable this. */ - "jobRebroadcastTimeout": 55, - - //instanceId: 37, //Recommend not using this because a crypto-random one will be generated - - /* Some attackers will create thousands of workers that use up all available socket connections, - usually the workers are zombies and don't submit shares after connecting. This feature - detects those and disconnects them. */ - "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds - - /* Sometimes you want the block hashes even for shares that aren't block candidates. */ - "emitInvalidBlockHashes": false, - /* If a worker is submitting a high threshold of invalid shares we can temporarily ban them to reduce system/network load. Also useful to fight against flooding attacks. */ "banning": { diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 97d2283..d288375 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -126,7 +126,7 @@ module.exports = function(logger){ if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at ' + data.difficulty + ' with diff ' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); + logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + ' with diff ' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); else if (!isValidShare) logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData); diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 044a1e9..e11b945 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -2,6 +2,14 @@ "enabled": false, "coin": "litecoin.json", + "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", + "blockRefreshInterval": 1000, + "txRefreshInterval": 20000, + "jobRebroadcastTimeout": 55, + "connectionTimeout": 600, + "emitInvalidBlockHashes": false, + "shareVariancePercent": 15, + "shareProcessing": { "internal": { "enabled": true, @@ -35,12 +43,6 @@ } }, - "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", - "blockRefreshInterval": 1000, - "txRefreshInterval": 20000, - "jobRebroadcastTimeout": 55, - "connectionTimeout": 600, - "banning": { "enabled": true, "time": 600, @@ -61,7 +63,7 @@ } }, "3032": { - "diff": 16 + "diff": 8 }, "3256": { "diff": 256 From 71024f18dc240941c1adda9836c490ffc30ca6f2 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 4 Apr 2014 02:55:35 +0000 Subject: [PATCH 078/150] Fix hashrate bug and add stats by algorithm to index and stats pages --- libs/stats.js | 3 +-- website/index.html | 12 +++++++----- website/pages/stats.html | 30 ++++++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/libs/stats.js b/libs/stats.js index 5c31ccb..05be656 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -135,8 +135,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; - coinStats.hashrate = hashratePre / 1e3 | 0; - portalStats.global.hashrate += coinStats.hashrate; + coinStats.hashrate = hashratePre | 0; portalStats.global.workers += Object.keys(coinStats.workers).length; /* algorithm specific global stats */ diff --git a/website/index.html b/website/index.html index 35ac208..e68fdb4 100644 --- a/website/index.html +++ b/website/index.html @@ -50,10 +50,12 @@ -
-
 {{=it.stats.global.workers}} Miners
-
 {{=it.stats.global.hashrate}} KH/s
-
+ {{ for(var algo in it.stats.algos) { }} + {{=algo}}:
+
 {{=it.stats.algos[algo].workers}} Miners
+
 {{=it.stats.algos[algo].hashrateString}}
+
  + {{ } }} @@ -86,4 +88,4 @@ - \ No newline at end of file + diff --git a/website/pages/stats.html b/website/pages/stats.html index ccab08e..7a71d0e 100644 --- a/website/pages/stats.html +++ b/website/pages/stats.html @@ -1,5 +1,27 @@
- fancy graphs here -
- {{=JSON.stringify(it.stats)}} -
\ No newline at end of file + + + + + + + + + + + + + + {{ for(var pool in it.stats.pools) { }} + + + + + + + + + {{ } }} +
PoolAlgoWorkersValid SharesInvalid SharesBlocksHashrate
{{=it.stats.pools[pool].name}}{{=it.stats.pools[pool].algorithm}}{{=Object.keys(it.stats.pools[pool].workers).length}}{{=it.stats.pools[pool].poolStats.validShares}}{{=it.stats.pools[pool].poolStats.invalidShares}}{{=it.stats.pools[pool].poolStats.validBlocks}}{{=it.stats.pools[pool].hashrateString}} +
+ From 4b49b70e131619af5cad480720b3d2aa5fbd0878 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 4 Apr 2014 02:56:53 +0000 Subject: [PATCH 079/150] Added stat updater per algorithm --- website/static/main.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/static/main.js b/website/static/main.js index a87d63e..6c94769 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -1,6 +1,5 @@ $(function(){ - var hotSwap = function(page, pushSate){ if (pushSate) history.pushState(null, null, '/' + page); $('.selected').removeClass('selected'); @@ -29,8 +28,10 @@ $(function(){ var statsSource = new EventSource("/api/live_stats"); statsSource.addEventListener('message', function(e){ var stats = JSON.parse(e.data); - $('#statsMiners').text(stats.global.workers); - $('#statsHashrate').text(stats.global.hashrate); + for (algo in algos) { + $('#statsMiners'+algo).text(stats.algos[algo].workers); + $('#statsHashrate'+algo).text(stats.algos[algo].hashrateString); + } }); -}); \ No newline at end of file +}); From ac61d50feb96964a6a8e55a276d4a5166340ec0d Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Fri, 4 Apr 2014 14:10:29 -0600 Subject: [PATCH 080/150] Minor readame change --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fbafa84..fe10989 100644 --- a/README.md +++ b/README.md @@ -390,14 +390,14 @@ Donations --------- To support development of this project feel free to donate :) -* BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR -* LTC: LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE -* VTC: VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN -* MAX: mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT -* QRK: QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC -* DRK: XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ -* DOGE: DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW -* Cryptsy Trade Key: 254ca13444be14937b36c44ba29160bd8f02ff76 +* BTC: `1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR` +* LTC: `LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE` +* VTC: `VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN` +* MAX: `mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT` +* QRK: `QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC` +* DRK: `XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ` +* DOGE: `DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW` +* Cryptsy Trade Key: `254ca13444be14937b36c44ba29160bd8f02ff76` Credits ------- From 086ace75d738d037779f2c5d4eed69cfef76aabf Mon Sep 17 00:00:00 2001 From: "Eugene@ubuntu" Date: Sat, 5 Apr 2014 05:02:52 +0400 Subject: [PATCH 081/150] fix payment redis final cleanout issue - possible double spending - optimized.v2 --- libs/paymentProcessor.js | 68 ++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 91bfeb4..3d3482a 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -52,6 +52,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ var logSystem = 'Payments'; var logComponent = coin; + var processingPayments = true; var daemon; var redisClient; @@ -119,8 +120,28 @@ function SetupForPool(logger, poolOptions, setupFinished){ }); - - + /* Call redis to check if previous sendmany and/or redis cleanout commands completed successfully. + If sendmany worked fine but redis commands failed you HAVE TO run redis commands again + (manually) to prevent double payments. If sendmany failed too you can safely delete + coin + '_finalRedisCommands' string from redis to let pool calculate payments again. */ + function checkPreviousPaymentsStatus(callback) { + redisClient.get(coin + '_finalRedisCommands', function(error, reply) { + if (error){ + callback('Could not get finalRedisCommands - ' + JSON.stringify(error)); + return; + } + if (reply) { + callback('Payments stopped because of the critical error - failed commands saved in ' + + coin + '_finalRedisCommands redis set:\n' + reply); + return; + } else { + /* There was no error in previous sendmany and/or redis cleanout commands + so we can safely continue */ + processingPayments = false; + callback(); + } + }); + } /* Number.toFixed gives us the decimal places we want, but as a string. parseFloat turns it back into number @@ -140,6 +161,21 @@ function SetupForPool(logger, poolOptions, setupFinished){ async.waterfall([ + function(callback) { + if (processingPayments) { + checkPreviousPaymentsStatus(function(error){ + if (error) { + logger.error(logSystem, logComponent, error); + callback('Check finished - previous payments processing error'); + return; + } + callback(); + }); + return; + } + callback(); + }, + /* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted blocks. */ function(callback){ @@ -147,7 +183,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ redisClient.smembers(coin + '_blocksPending', function(error, results){ if (error){ - logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error)); + logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error)); callback('Check finished - redis error for getting blocks'); return; } @@ -491,30 +527,14 @@ function SetupForPool(logger, poolOptions, setupFinished){ }); }, - /* Call redis to check if previous sendmany and/or redis cleanout commands completed successfully. - If sendmany worked fine but redis commands failed you HAVE TO run redis commands again - (manually) to prevent double payments. If sendmany failed too you can safely delete - coin + '_finalRedisCommands' string from redis to let pool calculate payments again. */ function(magnitude, workerPayments, finalRedisCommands, callback) { - redisClient.get(coin + '_finalRedisCommands', function(error, reply) { + /* Save final redis cleanout commands in case something goes wrong during payments */ + redisClient.set(coin + '_finalRedisCommands', JSON.stringify(finalRedisCommands), function(error, reply) { if (error){ - callback('Check finished - error with redis getting finalRedisCommands' + JSON.stringify(error)); + callback('Check finished - error with saving finalRedisCommands' + JSON.stringify(error)); return; } - if (reply) { - callback('Check finished - previous sendmany and/or redis cleanout commands failed - ' + reply); - return; - } else { - /* There was no error in previous sendmany and/or redis cleanout commands - so we can safely continue */ - redisClient.set(coin + '_finalRedisCommands', JSON.stringify(finalRedisCommands), function(error, reply) { - if (error){ - callback('Check finished - error with saving finalRedisCommands' + JSON.stringify(error)); - return; - } - callback(null, magnitude, workerPayments, finalRedisCommands); - }); - } + callback(null, magnitude, workerPayments, finalRedisCommands); }); }, @@ -527,6 +547,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ callback('Error with final redis commands for cleaning up ' + JSON.stringify(error)); return; } + processingPayments = false; logger.debug(logSystem, logComponent, 'Payments processing performed an interval'); }); }; @@ -548,6 +569,7 @@ function SetupForPool(logger, poolOptions, setupFinished){ logger.debug(logSystem, logComponent, 'Payments to be sent to: ' + JSON.stringify(addressAmounts)); + processingPayments = true; daemon.cmd('sendmany', ['', addressAmounts], function(results){ if (results[0].error){ From d9269f483ce70ba7cb2a77039c9c2fc324606929 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 14:19:02 -0600 Subject: [PATCH 082/150] Changed npm dependency to use git uri --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ebd0ec..a6abc57 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/zone117x/node-open-mining-portal.git" }, "dependencies": { - "stratum-pool": "https://github.com/zone117x/node-stratum-pool/archive/master.tar.gz", + "stratum-pool": "git://github.com/zone117x/node-stratum-pool.git", "dateformat": "*", "node-json-minify": "*", "posix": "*", From 27289ba37faffa34d6177394051e05f4890910d5 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 14:27:05 -0600 Subject: [PATCH 083/150] Added check for config.json file existence --- init.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/init.js b/init.js index decf2cb..1a7a51b 100644 --- a/init.js +++ b/init.js @@ -17,6 +17,11 @@ var algos = require('stratum-pool/lib/algoProperties.js'); JSON.minify = JSON.minify || require("node-json-minify"); +if (!fs.existsSync('config.json')){ + console.log('config.json file does not exist. Read the installation/setup instructions.'); + return; +} + var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); From 323c8b669df2427108c120438447eadc613ba031 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 15:47:00 -0600 Subject: [PATCH 084/150] Removed posix module from being installed by default --- init.js | 13 ++++++++++--- package.json | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/init.js b/init.js index 1a7a51b..9cd58b2 100644 --- a/init.js +++ b/init.js @@ -4,7 +4,6 @@ var os = require('os'); var cluster = require('cluster'); var async = require('async'); -var posix = require('posix'); var PoolLogger = require('./libs/logUtil.js'); var BlocknotifyListener = require('./libs/blocknotifyListener.js'); var RedisBlocknotifyListener = require('./libs/redisblocknotifyListener.js'); @@ -41,10 +40,18 @@ try { //Try to give process ability to handle 100k concurrent connections try{ - posix.setrlimit('nofile', { soft: 100000, hard: 100000 }); + var posix = require('posix'); + try { + posix.setrlimit('nofile', { soft: 100000, hard: 100000 }); + } + catch(e){ + if (cluster.isMaster) + logger.warning('POSIX', 'Connection Limit', '(Safe to ignore) Must be ran as root to increase resource limits'); + } } catch(e){ - logger.warning('POSIX', 'Connection Limit', '(Safe to ignore) Must be ran as root to increase resource limits'); + if (cluster.isMaster) + logger.debug('POSIX', 'Connection Limit', '(Safe to ignore) POSIX module not installed and resource (connection) limit was not raised'); } diff --git a/package.json b/package.json index a6abc57..24e73b1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "stratum-pool": "git://github.com/zone117x/node-stratum-pool.git", "dateformat": "*", "node-json-minify": "*", - "posix": "*", "redis": "*", "mysql": "*", "async": "*", From 575158f8586bc538703056ec29d11ff0ebb47dcd Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 16:29:24 -0600 Subject: [PATCH 085/150] More error handling for blocknotify --- libs/blocknotifyListener.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/blocknotifyListener.js b/libs/blocknotifyListener.js index 921e1f4..4691c13 100644 --- a/libs/blocknotifyListener.js +++ b/libs/blocknotifyListener.js @@ -32,7 +32,16 @@ var listener = module.exports = function listener(options){ emitLog('Block listener connection ended'); - var message = JSON.parse(data); + var message; + + try{ + message = JSON.parse(data); + } + catch(e){ + emitLog('Block listener failed to parse message ' + data); + return; + } + if (message.password === options.password) { _this.emit('hash', message); } @@ -42,7 +51,7 @@ var listener = module.exports = function listener(options){ }); } catch(e){ - emitLog('Block listener failed to parse message ' + data); + emitLog('Block listener had an error: ' + e); } }); From 634cfb7bff44aaba8ed6254724ef76748f1d8bb1 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sat, 5 Apr 2014 23:40:50 +0000 Subject: [PATCH 086/150] Ensure configuration value for validation of pool workers is honored --- libs/poolWorker.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index d288375..fb18fea 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -87,10 +87,14 @@ module.exports = function(logger){ var shareProcessor = new ShareProcessor(logger, poolOptions) handlers.auth = function(workerName, password, authCallback){ - pool.daemon.cmd('validateaddress', [workerName], function(results){ - var isValid = results.filter(function(r){return r.response.isvalid}).length > 0; - authCallback(isValid); - }); + if (shareProcessing.internal.validateWorkerAddress !== true) + authCallback(true); + else { + pool.daemon.cmd('validateaddress', [workerName], function(results){ + var isValid = results.filter(function(r){return r.response.isvalid}).length > 0; + authCallback(isValid); + }); + } }; handlers.share = function(isValidShare, isValidBlock, data){ @@ -171,4 +175,4 @@ module.exports = function(logger){ } -}; \ No newline at end of file +}; From dc3dcd01a7aef20b2f2d7a2270e85680db302e29 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sat, 5 Apr 2014 23:43:02 +0000 Subject: [PATCH 087/150] Ensure configuration value for validation of pool workers is honored --- libs/poolWorker.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index d288375..fb18fea 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -87,10 +87,14 @@ module.exports = function(logger){ var shareProcessor = new ShareProcessor(logger, poolOptions) handlers.auth = function(workerName, password, authCallback){ - pool.daemon.cmd('validateaddress', [workerName], function(results){ - var isValid = results.filter(function(r){return r.response.isvalid}).length > 0; - authCallback(isValid); - }); + if (shareProcessing.internal.validateWorkerAddress !== true) + authCallback(true); + else { + pool.daemon.cmd('validateaddress', [workerName], function(results){ + var isValid = results.filter(function(r){return r.response.isvalid}).length > 0; + authCallback(isValid); + }); + } }; handlers.share = function(isValidShare, isValidBlock, data){ @@ -171,4 +175,4 @@ module.exports = function(logger){ } -}; \ No newline at end of file +}; From a0a8c0cd75fe2b4b6ab0082757aab404b4a34af5 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 6 Apr 2014 00:12:00 +0000 Subject: [PATCH 088/150] fix issue #40 --- stats.js | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 stats.js diff --git a/stats.js b/stats.js new file mode 100644 index 0000000..7f2e126 --- /dev/null +++ b/stats.js @@ -0,0 +1,180 @@ +var redis = require('redis'); +var async = require('async'); + +var os = require('os'); + +var algos = require('stratum-pool/lib/algoProperties.js'); + + +module.exports = function(logger, portalConfig, poolConfigs){ + + var _this = this; + + var logSystem = 'Stats'; + + var redisClients = []; + + var canDoStats = true; + + Object.keys(poolConfigs).forEach(function(coin){ + + if (!canDoStats) return; + + var poolConfig = poolConfigs[coin]; + + if (!poolConfig.shareProcessing || !poolConfig.shareProcessing.internal){ + logger.error(logSystem, coin, 'Cannot do stats without internal share processing setup'); + canDoStats = false; + return; + } + + var internalConfig = poolConfig.shareProcessing.internal; + var redisConfig = internalConfig.redis; + + for (var i = 0; i < redisClients.length; i++){ + var client = redisClients[i]; + if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){ + client.coins.push(coin); + return; + } + } + redisClients.push({ + coins: [coin], + client: redis.createClient(redisConfig.port, redisConfig.host) + }); + }); + + + this.stats = {}; + this.statsString = ''; + + this.getGlobalStats = function(callback){ + + var allCoinStats = {}; + + async.each(redisClients, function(client, callback){ + var windowTime = (((Date.now() / 1000) - portalConfig.website.hashrateWindow) | 0).toString(); + var redisCommands = []; + + + var redisComamndTemplates = [ + ['zremrangebyscore', '_hashrate', '-inf', '(' + windowTime], + ['zrangebyscore', '_hashrate', windowTime, '+inf'], + ['hgetall', '_stats'], + ['scard', '_blocksPending'], + ['scard', '_blocksConfirmed'], + ['scard', '_blocksOrphaned'] + ]; + + var commandsPerCoin = redisComamndTemplates.length; + + + client.coins.map(function(coin){ + redisComamndTemplates.map(function(t){ + var clonedTemplates = t.slice(0); + clonedTemplates[1] = coin + clonedTemplates [1]; + redisCommands.push(clonedTemplates); + }); + }); + + + client.client.multi(redisCommands).exec(function(err, replies){ + if (err){ + console.log('error with getting hashrate stats ' + JSON.stringify(err)); + callback(err); + } + else{ + for(var i = 0; i < replies.length; i += commandsPerCoin){ + var coinName = client.coins[i / commandsPerCoin | 0]; + var coinStats = { + name: coinName, + symbol: poolConfigs[coinName].coin.symbol.toUpperCase(), + algorithm: poolConfigs[coinName].coin.algorithm, + hashrates: replies[i + 1], + poolStats: replies[i + 2] != null ? replies[i + 2] : { validShares: 0, validBlocks: 0, invalidShares: 0 }, + blocks: { + pending: replies[i + 3], + confirmed: replies[i + 4], + orphaned: replies[i + 5] + } + }; + allCoinStats[coinStats.name] = (coinStats); + } + callback(); + } + }); + }, function(err){ + if (err){ + console.log('error getting all stats' + JSON.stringify(err)); + callback(); + return; + } + + var portalStats = { + global:{ + workers: 0, + hashrate: 0 + }, + algos: {}, + pools: allCoinStats + }; + + Object.keys(allCoinStats).forEach(function(coin){ + var coinStats = allCoinStats[coin]; + coinStats.workers = {}; + coinStats.shares = 0; + coinStats.hashrates.forEach(function(ins){ + var parts = ins.split(':'); + var workerShares = parseFloat(parts[0]); + coinStats.shares += workerShares; + var worker = parts[1]; + if (worker in coinStats.workers) + coinStats.workers[worker] += workerShares + else + coinStats.workers[worker] = workerShares + }); + var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; + var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; + coinStats.hashrate = hashratePre | 0; + portalStats.global.workers += Object.keys(coinStats.workers).length; + + /* algorithm specific global stats */ + var algo = coinStats.algorithm; + if (!portalStats.algos.hasOwnProperty(algo)){ + portalStats.algos[algo] = { + workers: 0, + hashrate: 0, + hashrateString: null + }; + } + portalStats.algos[algo].hashrate += coinStats.hashrate; + portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; + + delete coinStats.hashrates; + delete coinStats.shares; + coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate); + }); + + Object.keys(portalStats.algos).forEach(function(algo){ + var algoStats = portalStats.algos[algo]; + algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate); + }); + + _this.stats = portalStats; + _this.statsString = JSON.stringify(portalStats); + callback(); + }); + + }; + + this.getReadableHashRateString = function(hashrate){ + var i = -1; + var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ]; + do { + hashrate = hashrate / 1024; + i++; + } while (hashrate > 1024); + return hashrate.toFixed(2) + byteUnits[i]; + }; + +}; From a9737393fbdd6b0ee7a8a8a251c88f8e9f07f306 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 6 Apr 2014 00:13:31 +0000 Subject: [PATCH 089/150] Revert "fix issue #40" This reverts commit a0a8c0cd75fe2b4b6ab0082757aab404b4a34af5. --- stats.js | 180 ------------------------------------------------------- 1 file changed, 180 deletions(-) delete mode 100644 stats.js diff --git a/stats.js b/stats.js deleted file mode 100644 index 7f2e126..0000000 --- a/stats.js +++ /dev/null @@ -1,180 +0,0 @@ -var redis = require('redis'); -var async = require('async'); - -var os = require('os'); - -var algos = require('stratum-pool/lib/algoProperties.js'); - - -module.exports = function(logger, portalConfig, poolConfigs){ - - var _this = this; - - var logSystem = 'Stats'; - - var redisClients = []; - - var canDoStats = true; - - Object.keys(poolConfigs).forEach(function(coin){ - - if (!canDoStats) return; - - var poolConfig = poolConfigs[coin]; - - if (!poolConfig.shareProcessing || !poolConfig.shareProcessing.internal){ - logger.error(logSystem, coin, 'Cannot do stats without internal share processing setup'); - canDoStats = false; - return; - } - - var internalConfig = poolConfig.shareProcessing.internal; - var redisConfig = internalConfig.redis; - - for (var i = 0; i < redisClients.length; i++){ - var client = redisClients[i]; - if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){ - client.coins.push(coin); - return; - } - } - redisClients.push({ - coins: [coin], - client: redis.createClient(redisConfig.port, redisConfig.host) - }); - }); - - - this.stats = {}; - this.statsString = ''; - - this.getGlobalStats = function(callback){ - - var allCoinStats = {}; - - async.each(redisClients, function(client, callback){ - var windowTime = (((Date.now() / 1000) - portalConfig.website.hashrateWindow) | 0).toString(); - var redisCommands = []; - - - var redisComamndTemplates = [ - ['zremrangebyscore', '_hashrate', '-inf', '(' + windowTime], - ['zrangebyscore', '_hashrate', windowTime, '+inf'], - ['hgetall', '_stats'], - ['scard', '_blocksPending'], - ['scard', '_blocksConfirmed'], - ['scard', '_blocksOrphaned'] - ]; - - var commandsPerCoin = redisComamndTemplates.length; - - - client.coins.map(function(coin){ - redisComamndTemplates.map(function(t){ - var clonedTemplates = t.slice(0); - clonedTemplates[1] = coin + clonedTemplates [1]; - redisCommands.push(clonedTemplates); - }); - }); - - - client.client.multi(redisCommands).exec(function(err, replies){ - if (err){ - console.log('error with getting hashrate stats ' + JSON.stringify(err)); - callback(err); - } - else{ - for(var i = 0; i < replies.length; i += commandsPerCoin){ - var coinName = client.coins[i / commandsPerCoin | 0]; - var coinStats = { - name: coinName, - symbol: poolConfigs[coinName].coin.symbol.toUpperCase(), - algorithm: poolConfigs[coinName].coin.algorithm, - hashrates: replies[i + 1], - poolStats: replies[i + 2] != null ? replies[i + 2] : { validShares: 0, validBlocks: 0, invalidShares: 0 }, - blocks: { - pending: replies[i + 3], - confirmed: replies[i + 4], - orphaned: replies[i + 5] - } - }; - allCoinStats[coinStats.name] = (coinStats); - } - callback(); - } - }); - }, function(err){ - if (err){ - console.log('error getting all stats' + JSON.stringify(err)); - callback(); - return; - } - - var portalStats = { - global:{ - workers: 0, - hashrate: 0 - }, - algos: {}, - pools: allCoinStats - }; - - Object.keys(allCoinStats).forEach(function(coin){ - var coinStats = allCoinStats[coin]; - coinStats.workers = {}; - coinStats.shares = 0; - coinStats.hashrates.forEach(function(ins){ - var parts = ins.split(':'); - var workerShares = parseFloat(parts[0]); - coinStats.shares += workerShares; - var worker = parts[1]; - if (worker in coinStats.workers) - coinStats.workers[worker] += workerShares - else - coinStats.workers[worker] = workerShares - }); - var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; - var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; - coinStats.hashrate = hashratePre | 0; - portalStats.global.workers += Object.keys(coinStats.workers).length; - - /* algorithm specific global stats */ - var algo = coinStats.algorithm; - if (!portalStats.algos.hasOwnProperty(algo)){ - portalStats.algos[algo] = { - workers: 0, - hashrate: 0, - hashrateString: null - }; - } - portalStats.algos[algo].hashrate += coinStats.hashrate; - portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; - - delete coinStats.hashrates; - delete coinStats.shares; - coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate); - }); - - Object.keys(portalStats.algos).forEach(function(algo){ - var algoStats = portalStats.algos[algo]; - algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate); - }); - - _this.stats = portalStats; - _this.statsString = JSON.stringify(portalStats); - callback(); - }); - - }; - - this.getReadableHashRateString = function(hashrate){ - var i = -1; - var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ]; - do { - hashrate = hashrate / 1024; - i++; - } while (hashrate > 1024); - return hashrate.toFixed(2) + byteUnits[i]; - }; - -}; From 22697fed80f590a33149eb280d24dc8951ac177f Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 6 Apr 2014 00:14:07 +0000 Subject: [PATCH 090/150] fix issue #40 --- libs/stats.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/stats.js b/libs/stats.js index 05be656..7f2e126 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -91,7 +91,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ symbol: poolConfigs[coinName].coin.symbol.toUpperCase(), algorithm: poolConfigs[coinName].coin.algorithm, hashrates: replies[i + 1], - poolStats: replies[i + 2], + poolStats: replies[i + 2] != null ? replies[i + 2] : { validShares: 0, validBlocks: 0, invalidShares: 0 }, blocks: { pending: replies[i + 3], confirmed: replies[i + 4], @@ -178,4 +178,3 @@ module.exports = function(logger, portalConfig, poolConfigs){ }; }; - From 210f6e862771e8589c046ce04586d284b44ccd1c Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:33:45 -0600 Subject: [PATCH 091/150] Added attack-mitigation features to readme --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fe10989..fb66044 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,16 @@ Each and every share will be rewarded - even for rounds resulting in orphaned bl authentication. A minimalistic HTML5 front-end connects to the portals statistics API to display stats from from each pool such as connected miners, network/pool difficulty/hash rate, etc. +* Attack mitigation features: + * Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). + * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). + * Detects and thwarts invalid share attacks: + * NOMP dynamically generates the max difficulty for each hashing algorithm based on values founds in coin source + code so that the low difficulty share exploits happening to other pool servers won't work on this one. + * IP banning feature which on a configurable threshold will ban an IP for a configurable amount of time if the miner + submits over a configurable threshold of invalid shares. + * NOMP is written in Node.js which uses a single thread (async) to handle connections rather than the overhead of one + thread per connection, and clustering is also implemented so all CPU cores are taken advantage of. #### Planned Features From 3942e75dcdeed44ee1e84d959fed138833f70513 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:35:28 -0600 Subject: [PATCH 092/150] Miner readme update --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fb66044..40d8d7d 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,17 @@ Each and every share will be rewarded - even for rounds resulting in orphaned bl authentication. A minimalistic HTML5 front-end connects to the portals statistics API to display stats from from each pool such as connected miners, network/pool difficulty/hash rate, etc. -* Attack mitigation features: - * Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). - * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). - * Detects and thwarts invalid share attacks: - * NOMP dynamically generates the max difficulty for each hashing algorithm based on values founds in coin source - code so that the low difficulty share exploits happening to other pool servers won't work on this one. - * IP banning feature which on a configurable threshold will ban an IP for a configurable amount of time if the miner - submits over a configurable threshold of invalid shares. - * NOMP is written in Node.js which uses a single thread (async) to handle connections rather than the overhead of one - thread per connection, and clustering is also implemented so all CPU cores are taken advantage of. +#### Attack Mitigation +* Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). +* Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). +* Detects and thwarts invalid share attacks: + * NOMP dynamically generates the max difficulty for each hashing algorithm based on values founds in coin source + code so that the low difficulty share exploits happening to other pool servers won't work on this one. + * IP banning feature which on a configurable threshold will ban an IP for a configurable amount of time if the miner + submits over a configurable threshold of invalid shares. +* NOMP is written in Node.js which uses a single thread (async) to handle connections rather than the overhead of one +thread per connection, and clustering is also implemented so all CPU cores are taken advantage of. + #### Planned Features From 4d2940099eb9f27b7e626d341b8603e8b1581e77 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:36:24 -0600 Subject: [PATCH 093/150] Minor readme update --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 40d8d7d..e4a1e1e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. thread per connection, and clustering is also implemented so all CPU cores are taken advantage of. +#### Security +NOMP has some implicit security advantages for pool operators and miners: +* Without a registration/login system, non-security-oriented miners reusing passwords across pools is no longer a concern. +* Automated payouts by default and pool profits are sent to another address so pool wallets aren't plump with coins - +giving hackers little reward and keeping your pool from being a target. +* Miners can notice lack of automated payments as a possible early warning sign that an operator is about to run off with their coins. + + #### Planned Features * NOMP API - Used by the website to display stats and information about the pool(s) on the portal's front-end website, @@ -52,14 +60,6 @@ of this software. The switching can be controlled using a coin profitability API (or calculated locally using daemon-reported network difficulties and exchange APIs). -#### Security -NOMP has some implicit security advantages for pool operators and miners: -* Without a registration/login system, non-security-oriented miners reusing passwords across pools is no longer a concern. -* Automated payouts by default and pool profits are sent to another address so pool wallets aren't plump with coins - -giving hackers little reward and keeping your pool from being a target. -* Miners can notice lack of automated payments as a possible early warning sign that an operator is about to run off with their coins. - - #### Community / Support IRC * Support / general discussion join #nomp: https://webchat.freenode.net/?channels=#nomp From 2f0346a2cb8e4bc2b4e86772f592a8252b341de8 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:40:52 -0600 Subject: [PATCH 094/150] Minor readme update --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e4a1e1e..a5be4f6 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. * Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). * Detects and thwarts invalid share attacks: - * NOMP dynamically generates the max difficulty for each hashing algorithm based on values founds in coin source - code so that the low difficulty share exploits happening to other pool servers won't work on this one. + * Other pool server software guesstimate and hardcode the max difficulties for new hashing algorithms. NOMP + dynamically generates the max difficulty for each algorithm based on values founds in coin source + code - NOMP is not vulnerable to the low difficulty share exploits happening to other pool servers. * IP banning feature which on a configurable threshold will ban an IP for a configurable amount of time if the miner submits over a configurable threshold of invalid shares. * NOMP is written in Node.js which uses a single thread (async) to handle connections rather than the overhead of one From 41d5e847a9fc4165b9188a1a6a2de1748814e26e Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:42:48 -0600 Subject: [PATCH 095/150] Minor readme update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a5be4f6..a8c0b59 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ pool such as connected miners, network/pool difficulty/hash rate, etc. * Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). * Detects and thwarts invalid share attacks: - * Other pool server software guesstimate and hardcode the max difficulties for new hashing algorithms. NOMP - dynamically generates the max difficulty for each algorithm based on values founds in coin source - code - NOMP is not vulnerable to the low difficulty share exploits happening to other pool servers. + * NOMP is not vulnerable to the low difficulty share exploits happening to other pool servers. Other pool server + software has hardcoded guesstimated max difficulties for new hashing algorithms while NOMP dynamically generates the + max difficulty for each algorithm based on values founds in coin source code. * IP banning feature which on a configurable threshold will ban an IP for a configurable amount of time if the miner submits over a configurable threshold of invalid shares. * NOMP is written in Node.js which uses a single thread (async) to handle connections rather than the overhead of one From f276554bd5455bca66ffa1f4f8e295eca7405977 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2014 18:45:05 -0600 Subject: [PATCH 096/150] Minor readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8c0b59..1dd0e67 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ authentication. A minimalistic HTML5 front-end connects to the portals statistic pool such as connected miners, network/pool difficulty/hash rate, etc. #### Attack Mitigation -* Detects and thwarts socket flooding (invalid data sent over socket in order to consume system resources). +* Detects and thwarts socket flooding (garbage data sent over socket in order to consume system resources). * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). * Detects and thwarts invalid share attacks: * NOMP is not vulnerable to the low difficulty share exploits happening to other pool servers. Other pool server From f827d2749d791a6b7bbb9db7d2ca0ba043210c13 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sun, 6 Apr 2014 10:38:43 -0600 Subject: [PATCH 097/150] Update README.md Updated name of repo in setup instructions - it was set to the old one --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dd0e67..e526a52 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ a good pool operator. For starters be sure to read: Clone the repository and run `npm update` for all the dependencies to be installed: ```bash -git clone https://github.com/zone117x/node-stratum-portal.git nomp +git clone https://github.com/zone117x/node-open-mining-portal.git nomp cd nomp npm update ``` From 6d2672105afe300826ffc0c5f1a7eadc395bf51d Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sun, 6 Apr 2014 10:55:47 -0600 Subject: [PATCH 098/150] Added detailed upgrade/updating instructions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e526a52..0043982 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,13 @@ output from NOMP. * Use [New Relic](http://newrelic.com/) to monitor your NOMP instance and server performance. +#### Upgrading NOMP +When updating NOMP to the latest code its important to not only `git pull` the latest from this repo, but to also update the `node-statum-pool` module and any config files that may have been changed. +* Inside your NOMP directory (where the init.js script is) do `git pull` to get the latest NOMP code. +* Remove the dependenices by deleting the `node_modules` directory with `rm -r node_modules`. +* Run `npm update` to force updating/reinstalling of the dependencies. +* Compare your `config.json` and `pool_configs/coin.json` configurations to the lateset example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes. + Donations --------- To support development of this project feel free to donate :) From 27a8de62cbe75b93887c5898b8b99fe930bbd4bf Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Mon, 7 Apr 2014 03:11:53 +0000 Subject: [PATCH 099/150] proxy switching added and working --- config_example.json | 70 ++++++++------- init.js | 71 +++++++++++---- libs/coinswitchListener.js | 56 ++++++++++++ libs/poolWorker.js | 175 ++++++++++++++++++++++++++++--------- libs/shareProcessor.js | 4 +- scripts/coinSwitch.js | 38 ++++++++ 6 files changed, 326 insertions(+), 88 deletions(-) create mode 100644 libs/coinswitchListener.js create mode 100644 scripts/coinSwitch.js diff --git a/config_example.json b/config_example.json index b0fa531..b1c744e 100644 --- a/config_example.json +++ b/config_example.json @@ -23,39 +23,45 @@ "statUpdateInterval": 1.5, "hashrateWindow": 300 }, + /* + In a proxy configuration, you can setup ports that accept miners for work based on + a specific algorithm instead of a specific coin. Miners that connect to these ports + are automatically switched a coin determined by the server. + + The default coin is the first configured pool for each algorithm and coin switching + can be triggered using the coinSwitch.js script in the scripts folder. + + Please note miner address authentication must be disabled when using NOMP in a proxy + configuration and that payout processing is left up to the server administrator. + */ "proxy": { - "enabled": false, - "ports": { - "80": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } - }, - "6000": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } - }, - "8080": { - "diff": 32, - "varDiff": { - "minDiff" : 8, - "maxDiff" : 512, - "targetTime" : 15, - "retargetTime" : 90, - "variancePercent" : 30 - } + "sha256": { + "enabled": false, + "port": "3333", + "diff": 10, + "varDiff": { + "minDiff": 16, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting } + }, + "scrypt": { + "enabled": false, + "port": "4444", + "diff": 10, + "varDiff": { + "minDiff": 16, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + }, + "scrypt-n": { + "enabled": false, + "port": "5555" } } -} \ No newline at end of file +} diff --git a/init.js b/init.js index 9cd58b2..8ca546f 100644 --- a/init.js +++ b/init.js @@ -6,6 +6,7 @@ var cluster = require('cluster'); var async = require('async'); var PoolLogger = require('./libs/logUtil.js'); var BlocknotifyListener = require('./libs/blocknotifyListener.js'); +var CoinswitchListener = require('./libs/coinswitchListener.js'); var RedisBlocknotifyListener = require('./libs/redisblocknotifyListener.js'); var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); @@ -32,9 +33,11 @@ var logger = new PoolLogger({ try { +/* require('newrelic'); if (cluster.isMaster) logger.debug('NewRelic', 'Monitor', 'New Relic initiated'); +*/ } catch(e) {} @@ -71,19 +74,7 @@ if (cluster.isWorker){ } return; -} /* else { - var coinNames = ['alphacoin','frankocoin','emerald','kittehcoin']; - var curIndex = 0; - setInterval(function () { - var newCoinName = coinNames[++curIndex % coinNames.length]; - console.log("SWITCHING to "+newCoinName); - var ipcMessage = {type:'switch', coin: newCoinName}; - Object.keys(cluster.workers).forEach(function(id) { - cluster.workers[id].send(ipcMessage); - }); - }, 20000); -} */ - +} //Read all pool configs from pool_configs and join them with their coin profile @@ -102,11 +93,11 @@ var buildPoolConfigs = function(){ var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'}))); poolOptions.coin = coinProfile; - configs[poolOptions.coin.name] = poolOptions; + configs[poolOptions.coin.name.toLowerCase()] = poolOptions; if (!(coinProfile.algorithm in algos)){ logger.error('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"'); - delete configs[poolOptions.coin.name]; + delete configs[poolOptions.coin.name.toLowerCase()]; } }); @@ -198,6 +189,54 @@ var startBlockListener = function(portalConfig){ listener.start(); }; + +// +// Receives authenticated events from coin switch listener and triggers proxy +// to swtich to a new coin. +// +var startCoinswitchListener = function(portalConfig){ + var listener = new CoinswitchListener(portalConfig.coinSwitchListener); + listener.on('log', function(text){ + logger.debug('Master', 'Coinswitch', text); + }); + listener.on('switchcoin', function(message){ + + var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash}; + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send(ipcMessage); + }); + var ipcMessage = { + type:'switch', + coin: message.coin.toLowerCase() + }; + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send(ipcMessage); + }); + + }); + listener.start(); + +/* +if !cluster.isWorker +else { + var coinNames = ['Emoticoin','Infinitecoin']; + var curIndex = 0; + setInterval(function () { + var newCoinName = coinNames[++curIndex % coinNames.length]; + console.log("SWITCHING to " + newCoinName); + var ipcMessage = { + type:'switch', + coin: newCoinName + }; + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send(ipcMessage); + }); + }, 30000); +} +*/ + +}; + var startRedisBlockListener = function(portalConfig){ //block notify options //setup block notify here and use IPC to tell appropriate pools @@ -273,6 +312,8 @@ var startWebsite = function(portalConfig, poolConfigs){ startBlockListener(portalConfig); + startCoinswitchListener(portalConfig); + startRedisBlockListener(portalConfig); startWorkerListener(poolConfigs); diff --git a/libs/coinswitchListener.js b/libs/coinswitchListener.js new file mode 100644 index 0000000..fad0d2f --- /dev/null +++ b/libs/coinswitchListener.js @@ -0,0 +1,56 @@ +var events = require('events'); +var net = require('net'); + +var listener = module.exports = function listener(options){ + + var _this = this; + + var emitLog = function(text){ + _this.emit('log', text); + }; + + + this.start = function(){ + if (!options || !options.enabled){ + emitLog('Coinswitch listener disabled'); + return; + } + + var coinswitchServer = net.createServer(function(c) { + + emitLog('Coinswitch listener has incoming connection'); + var data = ''; + try { + c.on('data', function (d) { + emitLog('Coinswitch listener received switch request'); + data += d; + if (data.slice(-1) === '\n') { + c.end(); + } + }); + c.on('end', function () { + + var message = JSON.parse(data); + if (message.password === options.password) { + _this.emit('switchcoin', message); + } + else + emitLog('Coinswitch listener received notification with incorrect password'); + + }); + } + catch(e){ + emitLog('Coinswitch listener failed to parse message ' + data); + } + + }); + coinswitchServer.listen(options.port, function() { + emitLog('Coinswitch notify listener server started on port ' + options.port) + }); + + emitLog("Coinswitch listener is enabled, starting server on port " + options.port); + } + +}; + +listener.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/libs/poolWorker.js b/libs/poolWorker.js index fb18fea..d765865 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -1,14 +1,14 @@ var Stratum = require('stratum-pool'); var Vardiff = require('stratum-pool/lib/varDiff.js'); +var redis = require('redis'); var net = require('net'); - - var MposCompatibility = require('./mposCompatibility.js'); var ShareProcessor = require('./shareProcessor.js'); module.exports = function(logger){ + var _this = this; var poolConfigs = JSON.parse(process.env.pools); var portalConfig = JSON.parse(process.env.portalConfig); @@ -17,30 +17,54 @@ module.exports = function(logger){ var pools = {}; - var proxyStuff = {}; + var proxySwitch = {}; //Handle messages from master process sent via IPC process.on('message', function(message) { switch(message.type){ + case 'blocknotify': var pool = pools[message.coin.toLowerCase()] if (pool) pool.processBlockNotify(message.hash) break; + + // IPC message for pool switching case 'switch': - var newCoinPool = pools[message.coin.toLowerCase()]; - if (newCoinPool) { - var oldPool = pools[proxyStuff.curActivePool]; + var logSystem = 'Proxy'; + var logComponent = 'Switch'; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var newCoin = message.coin.toLowerCase(); + if (!poolConfigs.hasOwnProperty(newCoin)) { + logger.debug(logSystem, logComponent, logSubCat, 'Switch message to coin that is not recognized: ' + newCoin); + break; + } + + var algo = poolConfigs[newCoin].coin.algorithm; + var newPool = pools[newCoin]; + var oldCoin = proxySwitch[algo].currentPool; + var oldPool = pools[oldCoin]; + var proxyPort = proxySwitch[algo].port; + + if (newCoin == oldCoin) { + logger.debug(logSystem, logComponent, logSubCat, 'Switch message would have no effect - ignoring ' + newCoin); + break; + } + + logger.debug(logSystem, logComponent, logSubCat, 'Proxy message for ' + algo + ' from ' + oldCoin + ' to ' + newCoin); + + if (newPool) { oldPool.relinquishMiners( function (miner, cback) { // relinquish miners that are attached to one of the "Auto-switch" ports and leave the others there. - cback(typeof(portalConfig.proxy.ports[miner.client.socket.localPort]) !== 'undefined') + cback(miner.client.socket.localPort == proxyPort) }, function (clients) { - newCoinPool.attachMiners(clients); - proxyStuff.curActivePool = message.coin.toLowerCase(); + newPool.attachMiners(clients); } - ) - + ); + proxySwitch[algo].currentPool = newCoin; + //TODO write new pool to REDIS } break; } @@ -55,7 +79,6 @@ module.exports = function(logger){ var logComponent = coin; var logSubCat = 'Thread ' + (parseInt(forkId) + 1); - var handlers = { auth: function(){}, share: function(){}, @@ -128,51 +151,125 @@ module.exports = function(logger){ else if (isValidBlock) logger.debug(logSystem, logComponent, logSubCat, 'Block found: ' + data.blockHash); - if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + ' with diff ' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); + logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + ' by ' + data.worker + ' [' + data.ip + ']' ); else if (!isValidShare) logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData); - handlers.share(isValidShare, isValidBlock, data) }).on('difficultyUpdate', function(workerName, diff){ + logger.debug(logSystem, logComponent, logSubCat, 'Difficulty update to diff ' + diff + ' workerName=' + JSON.stringify(workerName)); handlers.diff(workerName, diff); }).on('log', function(severity, text) { logger[severity](logSystem, logComponent, logSubCat, text); }); + pool.start(); pools[poolOptions.coin.name.toLowerCase()] = pool; }); - - if (typeof(portalConfig.proxy) !== 'undefined' && portalConfig.proxy.enabled === true) { - proxyStuff.curActivePool = Object.keys(pools)[0]; - proxyStuff.proxys = {}; - proxyStuff.varDiffs = {}; - Object.keys(portalConfig.proxy.ports).forEach(function(port) { - proxyStuff.varDiffs[port] = new Vardiff(port, portalConfig.proxy.ports[port].varDiff); - }); - Object.keys(pools).forEach(function (coinName) { - var p = pools[coinName]; - Object.keys(proxyStuff.varDiffs).forEach(function(port) { - p.setVarDiff(port, proxyStuff.varDiffs[port]); + + if (typeof(portalConfig.proxy) !== 'undefined') { + + var logSystem = 'Proxy'; + var logComponent = 'Setup'; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + var proxyState = {}; + + // + // Load proxy state for each algorithm from redis which allows NOMP to resume operation + // on the last pool it was using when reloaded or restarted + // + logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis'); + var redisClient = redis.createClient(6379, "localhost") //TODO figure out where redis config will come from for such things + redisClient.on('ready', function(){ + redisClient.hgetall("proxyState", function(error, obj) { + if (error) { + logger.debug(logSystem, logComponent, logSubCat, 'No last proxy state found in redis'); + } + else { + proxyState = obj; + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); + } + + // + // Setup proxySwitch object to control proxy operations from configuration and any restored + // state. Each algorithm has a listening port, current coin name, and an active pool to + // which traffic is directed when activated in the config. + // + // In addition, the proxy config also takes diff and varDiff parmeters the override the + // defaults for the standard config of the coin. + // + Object.keys(portalConfig.proxy).forEach(function(algorithm) { + + if (portalConfig.proxy[algorithm].enabled === true) { + var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm].currentPool : _this.getFirstPoolForAlgorithm(algorithm); + proxySwitch[algorithm] = { + port: portalConfig.proxy[algorithm].port, + currentPool: initalPool, + proxy: {} + } + + + // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up + // + // Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was + // routed into instead. + // + if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { + proxySwitch[algorithm].varDiff = new Vardiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff); + proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff; + } + Object.keys(pools).forEach(function (coinName) { + var a = poolConfigs[coinName].coin.algorithm; + var p = pools[coinName]; + if (a == algorithm) { + p.setVarDiff(proxySwitch[algorithm].port, proxySwitch[algorithm].varDiff); + + // Set diff for proxy port by mimicking coin port config and setting it in the pool + // the diff wasn't being picked up by the stratum server for proxy workers and was always using the default of 8 + //p.options.ports[proxySwitch[algorithm].port] = {}; + //p.options.ports[proxySwitch[algorithm].port].proxy = true; + //p.options.ports[proxySwitch[algorithm].port].diff = proxySwitch[algorithm].diff; + } + }); + + proxySwitch[algorithm].proxy = net.createServer({allowHalfOpen: true}, function(socket) { + var currentPool = proxySwitch[algorithm].currentPool; + var logSubCat = 'Thread ' + (parseInt(forkId) + 1); + + logger.debug(logSystem, 'Connect', logSubCat, 'Proxy connect from ' + socket.remoteAddress + ' on ' + proxySwitch[algorithm].port + + ' routing to ' + currentPool); + pools[currentPool].getStratumServer().handleNewClient(socket); + + }).listen(parseInt(proxySwitch[algorithm].port), function() { + logger.debug(logSystem, logComponent, logSubCat, 'Proxy listening for ' + algorithm + ' on port ' + proxySwitch[algorithm].port + + ' into ' + proxySwitch[algorithm].currentPool); + }); + } + else { + logger.debug(logSystem, logComponent, logSubCat, 'Proxy pool for ' + algorithm + ' disabled.'); + } + }); }); + }).on('error', function(err){ + logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err); }); - - Object.keys(portalConfig.proxy.ports).forEach(function (port) { - proxyStuff.proxys[port] = net .createServer({allowHalfOpen: true}, function(socket) { - console.log(proxyStuff.curActivePool); - pools[proxyStuff.curActivePool].getStratumServer().handleNewClient(socket); - }).listen(parseInt(port), function(){ - console.log("Proxy listening on " + port); - }); - }); - - - + redisClient.quit(); } + + this.getFirstPoolForAlgorithm = function(algorithm) { + var foundCoin = ""; + Object.keys(poolConfigs).forEach(function(coinName) { + if (poolConfigs[coinName].coin.algorithm == algorithm) { + if (foundCoin === "") + foundCoin = coinName; + } + }); + return foundCoin; + }; }; diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 57df98e..82d7808 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -18,7 +18,7 @@ module.exports = function(logger, poolConfig){ var internalConfig = poolConfig.shareProcessing.internal; var redisConfig = internalConfig.redis; - var coin = poolConfig.coin.name; + var coin = poolConfig.coin.name.toLowerCase(); var forkId = process.env.forkId; var logSystem = 'Pool'; @@ -78,4 +78,4 @@ module.exports = function(logger, poolConfig){ }; -}; \ No newline at end of file +}; diff --git a/scripts/coinSwitch.js b/scripts/coinSwitch.js new file mode 100644 index 0000000..a039ae1 --- /dev/null +++ b/scripts/coinSwitch.js @@ -0,0 +1,38 @@ +/* +This script demonstrates sending a coin switch request and can be invoked from the command line +with: + + "node coinSwitch.js localhost:8118 password %s" + +where <%s> is the name of the coin proxy miners will be switched onto. + +If the coin name is not configured, disabled or matches the existing proxy setting, no action +will be taken by NOMP on receipt of the message. +*/ + +var net = require('net'); +var config = process.argv[2]; +var parts = config.split(':'); +var host = parts[0]; +var port = parts[1]; +var password = process.argv[3]; +var coin = process.argv[4]; +var blockHash = process.argv[5]; + +var client = net.connect(port, host, function() { + console.log('client connected'); + client.write(JSON.stringify({ + password: password, + coin: coin, + }) + '\n'); +}); + +client.on('data', function(data) { + console.log(data.toString()); + //client.end(); +}); + +client.on('end', function() { + console.log('client disconnected'); + //process.exit(); +}); From fa03295afbf516d37004ee188ad41dd01354b4ff Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Mon, 7 Apr 2014 03:14:21 +0000 Subject: [PATCH 100/150] reverted comments --- init.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/init.js b/init.js index 8ca546f..0809f5d 100644 --- a/init.js +++ b/init.js @@ -33,11 +33,9 @@ var logger = new PoolLogger({ try { -/* require('newrelic'); if (cluster.isMaster) logger.debug('NewRelic', 'Monitor', 'New Relic initiated'); -*/ } catch(e) {} From b98dd31d05cc5fcbd90598c0809d7eca7b0575e4 Mon Sep 17 00:00:00 2001 From: GMFlash Date: Mon, 7 Apr 2014 12:57:42 -0400 Subject: [PATCH 101/150] Fix ReferenceError: Can't find variable: algos --- website/static/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/static/main.js b/website/static/main.js index 6c94769..ebb149c 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -28,9 +28,9 @@ $(function(){ var statsSource = new EventSource("/api/live_stats"); statsSource.addEventListener('message', function(e){ var stats = JSON.parse(e.data); - for (algo in algos) { - $('#statsMiners'+algo).text(stats.algos[algo].workers); - $('#statsHashrate'+algo).text(stats.algos[algo].hashrateString); + for (algo in stats.algos) { + $('#statsMiners'+algo).text(stats.algos[algo].workers); + $('#statsHashrate'+algo).text(stats.algos[algo].hashrateString); } }); From c1caaf94e1049a8f9f414b1d01d924ce933870e8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 11:38:19 -0600 Subject: [PATCH 102/150] Added check for missing daemon config --- init.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/init.js b/init.js index 0809f5d..1694313 100644 --- a/init.js +++ b/init.js @@ -115,6 +115,11 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ logger.error('Master', coin, 'Share processing is not configured so a pool cannot be started for this coin.'); delete poolConfigs[coin]; } + + if (p.daemons.length < 1){ + logger.error('Master', coin, 'No daemons configured so a pool cannot be started for this coin.'); + delete poolConfigs[coin]; + } }); if (Object.keys(poolConfigs).length === 0){ @@ -214,24 +219,6 @@ var startCoinswitchListener = function(portalConfig){ }); listener.start(); -/* -if !cluster.isWorker -else { - var coinNames = ['Emoticoin','Infinitecoin']; - var curIndex = 0; - setInterval(function () { - var newCoinName = coinNames[++curIndex % coinNames.length]; - console.log("SWITCHING to " + newCoinName); - var ipcMessage = { - type:'switch', - coin: newCoinName - }; - Object.keys(cluster.workers).forEach(function(id) { - cluster.workers[id].send(ipcMessage); - }); - }, 30000); -} -*/ }; From 36e09e497fad5e31c1f32581b2e3ac79eaf2379b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 11:41:40 -0600 Subject: [PATCH 103/150] Added check for if daemons config is not array --- init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.js b/init.js index 1694313..9929c20 100644 --- a/init.js +++ b/init.js @@ -116,7 +116,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ delete poolConfigs[coin]; } - if (p.daemons.length < 1){ + if (!Array.isArray(p.daemons) || p.daemons.length < 1){ logger.error('Master', coin, 'No daemons configured so a pool cannot be started for this coin.'); delete poolConfigs[coin]; } From c3ba819f242b46a172e0d1533e9f16fde3e9b8a1 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 12:23:48 -0600 Subject: [PATCH 104/150] Set coin switching feature to not require lower case. Added documentation for proxy and coin switching --- README.md | 73 +++++++++++++++++++++++++----- config_example.json | 65 +++++++++++++------------- init.js | 6 +-- libs/poolWorker.js | 46 ++++++++++--------- libs/shareProcessor.js | 2 +- pool_configs/litecoin_example.json | 2 +- scripts/blockNotify.js | 31 ++++++------- scripts/coinSwitch.js | 35 +++++++------- 8 files changed, 155 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 0043982..f40de6e 100644 --- a/README.md +++ b/README.md @@ -132,24 +132,73 @@ Explanation for each field: "enabled": true, "forks": "auto" }, - - /* With this enabled, the master process will start listening on the configured port for - messages from the 'scripts/blockNotify.js' script which your coin daemons can be configured - to run when a new block is available. When a blocknotify message is received, the master - process uses IPC (inter-process communication) to notify each worker process about the - message. Each worker process then sends the message to the appropriate coin pool. See - "Setting up blocknotify" below to set up your daemon to use this feature. */ + + /* This is the front-end. Its not finished. When it is finished, this comment will say so. */ + "website": { + "enabled": true, + "port": 80, + "liveStats": true + }, + + /* With this enabled, the master process listen on the configured port for messages from the + 'scripts/blockNotify.js' script which your coin daemons can be configured to run when a + new block is available. When a blocknotify message is received, the master process uses + IPC (inter-process communication) to notify each thread about the message. Each thread + then sends the message to the appropriate coin pool. See "Setting up blocknotify" below to + set up your daemon to use this feature. */ "blockNotifyListener": { "enabled": true, "port": 8117, "password": "test" }, - /* This is the front-end. Its not finished. When it is finished, this comment will say so. */ - "website": { - "enabled": true, - "port": 80, - "liveStats": true + /* With this enabled, the master process will listen on the configured port for messages from + the 'scripts/coinSwitch.js' script which will trigger your proxy pools to switch to the + specified coin (non-case-sensitive). This setting is used in conjuction with the proxy + feature below. */ + "coinSwitchListener": { + "enabled": false, + "port": 8118, + "password": "test" + }, + + /* In a proxy configuration, you can setup ports that accept miners for work based on a + specific algorithm instead of a specific coin. Miners that connect to these ports are + automatically switched a coin determined by the server. The default coin is the first + configured pool for each algorithm and coin switching can be triggered using the + coinSwitch.js script in the scripts folder. + + Please note miner address authentication must be disabled when using NOMP in a proxy + configuration and that payout processing is left up to the server administrator. */ + "proxy": { + "sha256": { + "enabled": false, + "port": "3333", + "diff": 10, + "varDiff": { + "minDiff": 16, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + }, + "scrypt": { + "enabled": false, + "port": "4444", + "diff": 10, + "varDiff": { + "minDiff": 16, //Minimum difficulty + "maxDiff": 512, //Network difficulty will be used if it is lower than this + "targetTime": 15, //Try to get 1 share per this many seconds + "retargetTime": 90, //Check to see if we should retarget every this many seconds + "variancePercent": 30 //Allow time to very this % from target without retargeting + } + }, + "scrypt-n": { + "enabled": false, + "port": "5555" + } } } ```` diff --git a/config_example.json b/config_example.json index b1c744e..af3774b 100644 --- a/config_example.json +++ b/config_example.json @@ -1,21 +1,11 @@ { "logLevel": "debug", + "clustering": { "enabled": true, "forks": "auto" }, - "blockNotifyListener": { - "enabled": false, - "port": 8117, - "password": "test" - }, - "redisBlockNotifyListener": { - "enabled" : false, - "redisPort" : 6379, - "redisHost" : "hostname", - "psubscribeKey" : "newblocks:*" - }, "website": { "enabled": true, "siteTitle": "Cryppit", @@ -23,45 +13,54 @@ "statUpdateInterval": 1.5, "hashrateWindow": 300 }, - /* - In a proxy configuration, you can setup ports that accept miners for work based on - a specific algorithm instead of a specific coin. Miners that connect to these ports - are automatically switched a coin determined by the server. - The default coin is the first configured pool for each algorithm and coin switching - can be triggered using the coinSwitch.js script in the scripts folder. + "blockNotifyListener": { + "enabled": false, + "port": 8117, + "password": "test" + }, + + "coinSwitchListener": { + "enabled": false, + "port": 8118, + "password": "test" + }, - Please note miner address authentication must be disabled when using NOMP in a proxy - configuration and that payout processing is left up to the server administrator. - */ "proxy": { "sha256": { "enabled": false, "port": "3333", - "diff": 10, + "diff": 10, "varDiff": { - "minDiff": 16, //Minimum difficulty - "maxDiff": 512, //Network difficulty will be used if it is lower than this - "targetTime": 15, //Try to get 1 share per this many seconds - "retargetTime": 90, //Check to see if we should retarget every this many seconds - "variancePercent": 30 //Allow time to very this % from target without retargeting + "minDiff": 16, + "maxDiff": 512, + "targetTime": 15, + "retargetTime": 90, + "variancePercent": 30 } }, "scrypt": { "enabled": false, "port": "4444", - "diff": 10, + "diff": 10, "varDiff": { - "minDiff": 16, //Minimum difficulty - "maxDiff": 512, //Network difficulty will be used if it is lower than this - "targetTime": 15, //Try to get 1 share per this many seconds - "retargetTime": 90, //Check to see if we should retarget every this many seconds - "variancePercent": 30 //Allow time to very this % from target without retargeting + "minDiff": 16, + "maxDiff": 512, + "targetTime": 15, + "retargetTime": 90, + "variancePercent": 30 } }, "scrypt-n": { "enabled": false, "port": "5555" } + }, + + "redisBlockNotifyListener": { + "enabled": false, + "redisPort": 6379, + "redisHost": "hostname", + "psubscribeKey": "newblocks:*" } -} +} \ No newline at end of file diff --git a/init.js b/init.js index 9929c20..0daf3fd 100644 --- a/init.js +++ b/init.js @@ -91,11 +91,11 @@ var buildPoolConfigs = function(){ var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'}))); poolOptions.coin = coinProfile; - configs[poolOptions.coin.name.toLowerCase()] = poolOptions; + configs[poolOptions.coin.name] = poolOptions; if (!(coinProfile.algorithm in algos)){ logger.error('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"'); - delete configs[poolOptions.coin.name.toLowerCase()]; + delete configs[poolOptions.coin.name]; } }); @@ -210,7 +210,7 @@ var startCoinswitchListener = function(portalConfig){ }); var ipcMessage = { type:'switch', - coin: message.coin.toLowerCase() + coin: message.coin }; Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send(ipcMessage); diff --git a/libs/poolWorker.js b/libs/poolWorker.js index d765865..2464755 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -1,5 +1,4 @@ var Stratum = require('stratum-pool'); -var Vardiff = require('stratum-pool/lib/varDiff.js'); var redis = require('redis'); var net = require('net'); @@ -24,8 +23,15 @@ module.exports = function(logger){ switch(message.type){ case 'blocknotify': - var pool = pools[message.coin.toLowerCase()] - if (pool) pool.processBlockNotify(message.hash) + + var messageCoin = message.coin.toLowerCase(); + var poolTarget = Object.keys(pools).filter(function(p){ + return p.toLowerCase() === messageCoin; + })[0]; + + if (poolTarget) + pools[poolTarget].processBlockNotify(message.hash); + break; // IPC message for pool switching @@ -34,13 +40,17 @@ module.exports = function(logger){ var logComponent = 'Switch'; var logSubCat = 'Thread ' + (parseInt(forkId) + 1); - var newCoin = message.coin.toLowerCase(); - if (!poolConfigs.hasOwnProperty(newCoin)) { - logger.debug(logSystem, logComponent, logSubCat, 'Switch message to coin that is not recognized: ' + newCoin); - break; - } + var messageCoin = message.coin.toLowerCase(); + var newCoin = Object.keys(pools).filter(function(p){ + return p.toLowerCase() === messageCoin; + })[0]; - var algo = poolConfigs[newCoin].coin.algorithm; + if (!newCoin){ + logger.debug(logSystem, logComponent, logSubCat, 'Switch message to coin that is not recognized: ' + messageCoin); + break; + } + + var algo = poolConfigs[newCoin].coin.algorithm; var newPool = pools[newCoin]; var oldCoin = proxySwitch[algo].currentPool; var oldPool = pools[oldCoin]; @@ -168,7 +178,7 @@ module.exports = function(logger){ }); pool.start(); - pools[poolOptions.coin.name.toLowerCase()] = pool; + pools[poolOptions.coin.name] = pool; }); @@ -212,7 +222,7 @@ module.exports = function(logger){ port: portalConfig.proxy[algorithm].port, currentPool: initalPool, proxy: {} - } + }; // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up @@ -221,24 +231,18 @@ module.exports = function(logger){ // routed into instead. // if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { - proxySwitch[algorithm].varDiff = new Vardiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff); - proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff; + proxySwitch[algorithm].varDiff = new Stratum.varDiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff); + proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff; } Object.keys(pools).forEach(function (coinName) { var a = poolConfigs[coinName].coin.algorithm; var p = pools[coinName]; - if (a == algorithm) { + if (a === algorithm) { p.setVarDiff(proxySwitch[algorithm].port, proxySwitch[algorithm].varDiff); - - // Set diff for proxy port by mimicking coin port config and setting it in the pool - // the diff wasn't being picked up by the stratum server for proxy workers and was always using the default of 8 - //p.options.ports[proxySwitch[algorithm].port] = {}; - //p.options.ports[proxySwitch[algorithm].port].proxy = true; - //p.options.ports[proxySwitch[algorithm].port].diff = proxySwitch[algorithm].diff; } }); - proxySwitch[algorithm].proxy = net.createServer({allowHalfOpen: true}, function(socket) { + proxySwitch[algorithm].proxy = net.createServer(function(socket) { var currentPool = proxySwitch[algorithm].currentPool; var logSubCat = 'Thread ' + (parseInt(forkId) + 1); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 82d7808..815eb1a 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -18,7 +18,7 @@ module.exports = function(logger, poolConfig){ var internalConfig = poolConfig.shareProcessing.internal; var redisConfig = internalConfig.redis; - var coin = poolConfig.coin.name.toLowerCase(); + var coin = poolConfig.coin.name; var forkId = process.env.forkId; var logSystem = 'Pool'; diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index e11b945..06601a5 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -1,5 +1,5 @@ { - "enabled": false, + "enabled": true, "coin": "litecoin.json", "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", diff --git a/scripts/blockNotify.js b/scripts/blockNotify.js index f43dd0f..67ed235 100644 --- a/scripts/blockNotify.js +++ b/scripts/blockNotify.js @@ -1,21 +1,20 @@ - /* -This script should be hooked to the coin daemon as follow: -litecoind -blocknotify="node /path/to/this/script/blockNotify.js localhost:8117 password litecoin %s" -The above will send tell litecoin to launch this script with those parameters every time a block is found. -This script will then send the blockhash along with other information to a listening tcp socket -*/ + This script should be hooked to the coin daemon as follow: + litecoind -blocknotify="node /path/to/this/script/blockNotify.js localhost:8117 password litecoin %s" + The above will send tell litecoin to launch this script with those parameters every time a block is found. + This script will then send the blockhash along with other information to a listening tcp socket + */ -var net = require('net'); -var config = process.argv[2]; -var parts = config.split(':'); -var host = parts[0]; -var port = parts[1]; -var password = process.argv[3]; -var coin = process.argv[4]; +var net = require('net'); +var config = process.argv[2]; +var parts = config.split(':'); +var host = parts[0]; +var port = parts[1]; +var password = process.argv[3]; +var coin = process.argv[4]; var blockHash = process.argv[5]; -var client = net.connect(port, host, function() { +var client = net.connect(port, host, function () { console.log('client connected'); client.write(JSON.stringify({ password: password, @@ -24,12 +23,12 @@ var client = net.connect(port, host, function() { }) + '\n'); }); -client.on('data', function(data) { +client.on('data', function (data) { console.log(data.toString()); //client.end(); }); -client.on('end', function() { +client.on('end', function () { console.log('client disconnected'); //process.exit(); }); \ No newline at end of file diff --git a/scripts/coinSwitch.js b/scripts/coinSwitch.js index a039ae1..18dc1c8 100644 --- a/scripts/coinSwitch.js +++ b/scripts/coinSwitch.js @@ -1,38 +1,37 @@ /* -This script demonstrates sending a coin switch request and can be invoked from the command line -with: + This script demonstrates sending a coin switch request and can be invoked from the command line + with: "node coinSwitch.js localhost:8118 password %s" -where <%s> is the name of the coin proxy miners will be switched onto. + where <%s> is the name of the coin proxy miners will be switched onto. -If the coin name is not configured, disabled or matches the existing proxy setting, no action -will be taken by NOMP on receipt of the message. -*/ + If the coin name is not configured, disabled or matches the existing proxy setting, no action + will be taken by NOMP on receipt of the message. + */ -var net = require('net'); -var config = process.argv[2]; -var parts = config.split(':'); -var host = parts[0]; -var port = parts[1]; -var password = process.argv[3]; -var coin = process.argv[4]; -var blockHash = process.argv[5]; +var net = require('net'); +var config = process.argv[2]; +var parts = config.split(':'); +var host = parts[0]; +var port = parts[1]; +var password = process.argv[3]; +var coin = process.argv[4]; -var client = net.connect(port, host, function() { +var client = net.connect(port, host, function () { console.log('client connected'); client.write(JSON.stringify({ password: password, - coin: coin, + coin: coin }) + '\n'); }); -client.on('data', function(data) { +client.on('data', function (data) { console.log(data.toString()); //client.end(); }); -client.on('end', function() { +client.on('end', function () { console.log('client disconnected'); //process.exit(); }); From afae2060b1fb3211e195d7905a94e40271ec05fb Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 12:33:13 -0600 Subject: [PATCH 105/150] Moved coin switching into included features in readme --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f40de6e..fc86580 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ Each and every share will be rewarded - even for rounds resulting in orphaned bl authentication. A minimalistic HTML5 front-end connects to the portals statistics API to display stats from from each pool such as connected miners, network/pool difficulty/hash rate, etc. +* Automated switching of connected miners to different pools/coins is also easily done due to the multi-pool architecture +of this software. To use this feature the switching must be controlled by your own script, such as one that calculates +coin profitability via an API such as CoinChoose.com or CoinWarz.com (or calculated locally using daemon-reported network +difficulties and exchange APIs). NOMP's regular payment processing and miner authentication which using coin address as stratum +username will obviously not work with this coin switching feature - so you must control those with your own script as well. + + #### Attack Mitigation * Detects and thwarts socket flooding (garbage data sent over socket in order to consume system resources). * Detects and thwarts zombie miners (botnet infected computers connecting to your server to use up sockets but not sending any shares). @@ -56,10 +63,6 @@ and by the NOMP Desktop app to retrieve a list of available coins (and version-b allow your own pool to connect upstream to a larger pool server. It will request work from the larger pool then redistribute the work to our own connected miners. -* Automated switching of connected miners to different pools/coins is also easily done due to the multi-pool architecture -of this software. The switching can be controlled using a coin profitability API such as CoinChoose.com or CoinWarz.com -(or calculated locally using daemon-reported network difficulties and exchange APIs). - #### Community / Support IRC From 35d4cb0ae1a2202da93ca83003f3032ed8869236 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 13:00:54 -0600 Subject: [PATCH 106/150] Added minty to credits for his work on coin switching --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fc86580..761a5db 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,7 @@ To support development of this project feel free to donate :) Credits ------- +* [Jerry Brady / mintyfresh68](https://github.com/bluecircle) - got coin-switching fully working and developed proxy-per-algo feature * [Tony Dobbs](http://anthonydobbs.com) - graphical help with logo and front-end design * [vekexasia](//github.com/vekexasia) - co-developer & great tester * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman From 202ab9e9baa11d82406c9e21e7425f616eb44730 Mon Sep 17 00:00:00 2001 From: Alex Petrov Date: Tue, 8 Apr 2014 15:50:29 -0400 Subject: [PATCH 107/150] simple and portable block notifycation added. sysman@sysman.net same usage as node notify, but now simple blocknotify="/bin/blocknotify localhost:8117 mySuperSecurePassword dogecoin %s" --- scripts/blocknotify.c | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts/blocknotify.c diff --git a/scripts/blocknotify.c b/scripts/blocknotify.c new file mode 100644 index 0000000..6519eb5 --- /dev/null +++ b/scripts/blocknotify.c @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include + +/* + part of NOMP project + optimal block change pool notify in pure c. + (may also work as coin switch) + + Simple lightweight & fast. + +# gcc blocknotify.c -o blocknotify +# +# Platforms : Linux,BSD,Solaris (mostly OS independent) + + Alex Petrov aka SysMan at sysman.net + + +// {"password":"notepas","coin":"Xcoin","hash":"d2191a8b644c9cd903439edf1d89ee060e196b3e116e0d48a3f11e5e3987a03b"} +// simplest connect + send json string to server + +# $Id: blocknotify.c,v 0.1 2014/04/07 22:38:09 sysman Exp $ +*/ +int main(int argc, char **argv) +{ + int sockfd,n; + struct sockaddr_in servaddr,cliaddr; + char sendline[1000]; + char recvline[1000]; + char host[200]; + char *p,*arg,*errptr; + int port; + + if (argc < 4) + { + // print help + printf("NOMP pool block notify\n usage: \n"); + exit(1); + } + + strncpy(host,argv[1],(sizeof(host)-1)); + p=host; + +if ( (arg=strchr(p,':')) ) + { *arg='\0'; + + errno=0; // reset errno + port=strtol(++arg,&errptr,10); + +if ( (errno != 0) || (errptr == arg) ) { fprintf(stderr, "port number fail [%s]\n",errptr); } +// if(strlen(arg) > (errptr-arg) ) also fail, but we ignore it for now +// printf("host %s:%d\n",host,port); +} + +// printf("pass: %s coin: %s block:[%s]\n",argv[2],argv[3],argv[4]); +snprintf(sendline,sizeof(sendline)-1, + "{\"password\":\"%s\",\"coin\":\"%s\",\"hash\":\"%s\"}\n", + argv[2], argv[3], argv[4]); + +// printf("sendline:[%s]",sendline); + + sockfd=socket(AF_INET,SOCK_STREAM,0); + + bzero(&servaddr,sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr=inet_addr(host); + servaddr.sin_port=htons(port); + + connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); + + sendto(sockfd,sendline,strlen(sendline),0, + (struct sockaddr *)&servaddr,sizeof(servaddr)); +exit(0); +} From 3c4eaaf5ce263b42acb3998ee96c0a0e27b371b2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 8 Apr 2014 14:26:45 -0600 Subject: [PATCH 108/150] Added blocknotify C script to readme --- README.md | 4 +++- scripts/blocknotify.c | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 761a5db..9d2b727 100644 --- a/README.md +++ b/README.md @@ -431,7 +431,8 @@ Example: inside `dogecoin.conf` add the line blocknotify="node scripts/blockNotify.js localhost:8117 mySuperSecurePassword dogecoin %s" ``` - +Alternatively, you can use a more efficient block notify script written in pure C. Build and usage instructions +are commented in [scripts/blocknotify.c](scripts/blocknotify.c). #### 3) Start the portal @@ -477,6 +478,7 @@ Credits * [vekexasia](//github.com/vekexasia) - co-developer & great tester * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing +* [Alex Petrov / sysmanalex](https://github.com/sysmanalex) - contributed the pure C block notify script * Those that contributed to [node-stratum-pool](//github.com/zone117x/node-stratum-pool) diff --git a/scripts/blocknotify.c b/scripts/blocknotify.c index 6519eb5..74c455a 100644 --- a/scripts/blocknotify.c +++ b/scripts/blocknotify.c @@ -7,17 +7,22 @@ #include /* - part of NOMP project - optimal block change pool notify in pure c. - (may also work as coin switch) - Simple lightweight & fast. +Contributed by Alex Petrov aka SysMan at sysman.net -# gcc blocknotify.c -o blocknotify -# -# Platforms : Linux,BSD,Solaris (mostly OS independent) +Part of NOMP project +Simple lightweight & fast - a more efficient block notify script in pure C. - Alex Petrov aka SysMan at sysman.net +(may also work as coin switch) + +Platforms : Linux,BSD,Solaris (mostly OS independent) + +Build with: + gcc blocknotify.c -o blocknotify + + +Usage in daemon coin.conf + blocknotify="/bin/blocknotify localhost:8117 mySuperSecurePassword dogecoin %s" // {"password":"notepas","coin":"Xcoin","hash":"d2191a8b644c9cd903439edf1d89ee060e196b3e116e0d48a3f11e5e3987a03b"} @@ -25,6 +30,8 @@ # $Id: blocknotify.c,v 0.1 2014/04/07 22:38:09 sysman Exp $ */ + + int main(int argc, char **argv) { int sockfd,n; From 2f8620a454c3d96f7db7cd0199286f6c95a95caf Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Wed, 9 Apr 2014 01:33:46 +0000 Subject: [PATCH 109/150] store proxy state in redis and restore on restart; disable example pool --- libs/poolWorker.js | 18 ++++++++++++++---- pool_configs/litecoin_example.json | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 2464755..4855507 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -74,7 +74,18 @@ module.exports = function(logger){ } ); proxySwitch[algo].currentPool = newCoin; - //TODO write new pool to REDIS + + var redisClient = redis.createClient(6379, "localhost") + redisClient.on('ready', function(){ + redisClient.hset('proxyState', algo, newCoin, function(error, obj) { + if (error) { + logger.error(logSystem, logComponent, logSubCat, 'Redis error writing proxy config: ' + JSON.stringify(err)) + } + else { + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo); + } + }); + }); } break; } @@ -195,7 +206,7 @@ module.exports = function(logger){ // on the last pool it was using when reloaded or restarted // logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis'); - var redisClient = redis.createClient(6379, "localhost") //TODO figure out where redis config will come from for such things + var redisClient = redis.createClient(6379, "localhost") redisClient.on('ready', function(){ redisClient.hgetall("proxyState", function(error, obj) { if (error) { @@ -217,7 +228,7 @@ module.exports = function(logger){ Object.keys(portalConfig.proxy).forEach(function(algorithm) { if (portalConfig.proxy[algorithm].enabled === true) { - var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm].currentPool : _this.getFirstPoolForAlgorithm(algorithm); + var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm] : _this.getFirstPoolForAlgorithm(algorithm); proxySwitch[algorithm] = { port: portalConfig.proxy[algorithm].port, currentPool: initalPool, @@ -263,7 +274,6 @@ module.exports = function(logger){ }).on('error', function(err){ logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err); }); - redisClient.quit(); } this.getFirstPoolForAlgorithm = function(algorithm) { diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 06601a5..0e7536e 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "coin": "litecoin.json", "address": "n4jSe18kZMCdGcZqaYprShXW6EH1wivUK1", @@ -92,4 +92,4 @@ "protocolVersion": 70002, "magic": "fcc1b7dc" } -} \ No newline at end of file +} From c487972f6b0a5491bb61a0863a2dc3dd0f3c5412 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Apr 2014 22:53:35 -0600 Subject: [PATCH 110/150] Added some sites using nomp to the readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 9d2b727..de00abb 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,13 @@ didn't follow the instructions in this README. Please __read the usage instructi If your pool uses NOMP let us know and we will list your website here. +##### Some pools using NOMP or node-stratum-module: +* http://chunkypools.com +* http://clevermining.com +* http://rapidhash.net +* http://hashfaster.com +* http://kryptochaos.com + Usage ===== From 201d02d10bdfb0460b5787cc1a49cab378553d1a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 10 Apr 2014 19:33:41 -0600 Subject: [PATCH 111/150] Refactored website. Added historical stats. --- README.md | 26 ++- config_example.json | 17 +- libs/api.js | 16 ++ libs/stats.js | 118 ++++++++++-- libs/website.js | 48 +++-- package.json | 2 + pool_configs/litecoin_example.json | 8 +- website/index.html | 46 ++--- website/pages/admin.html | 50 ++++++ website/pages/getting_started.html | 279 ++++++++++++++++++++++++++++- website/pages/home.html | 120 ++++++++++++- website/pages/stats.html | 69 ++++--- website/static/admin.js | 100 +++++++++++ website/static/main.js | 9 +- website/static/nvd3.css | 1 + website/static/nvd3.js | 6 + website/static/stats.js | 150 ++++++++++++++++ website/static/style.css | 120 ++++--------- 18 files changed, 1003 insertions(+), 182 deletions(-) create mode 100644 website/pages/admin.html create mode 100644 website/static/admin.js create mode 100644 website/static/nvd3.css create mode 100644 website/static/nvd3.js create mode 100644 website/static/stats.js diff --git a/README.md b/README.md index de00abb..baa047b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,27 @@ Explanation for each field: "website": { "enabled": true, "port": 80, - "liveStats": true + /* Used for displaying stratum connection data on the Getting Started page. */ + "stratumHost": "cryppit.com", + "stats": { + /* Gather stats to broadcast to page viewers and store in redis for historical stats + every this many seconds. */ + "updateInterval": 15, + /* How many seconds to hold onto historical stats. Currently set to 24 hours. */ + "historicalRetention": 43200, + /* How many seconds worth of shares should be gathered to generate hashrate. */ + "hashrateWindow": 300, + /* Redis instance of where to store historical stats. */ + "redis": { + "host": "localhost", + "port": 6379 + } + }, + /* Not done yet. */ + "adminCenter": { + "enabled": true, + "password": "password" + } }, /* With this enabled, the master process listen on the configured port for messages from the @@ -463,7 +483,7 @@ When updating NOMP to the latest code its important to not only `git pull` the l * Inside your NOMP directory (where the init.js script is) do `git pull` to get the latest NOMP code. * Remove the dependenices by deleting the `node_modules` directory with `rm -r node_modules`. * Run `npm update` to force updating/reinstalling of the dependencies. -* Compare your `config.json` and `pool_configs/coin.json` configurations to the lateset example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes. +* Compare your `config.json` and `pool_configs/coin.json` configurations to the latest example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes. Donations --------- @@ -481,7 +501,7 @@ To support development of this project feel free to donate :) Credits ------- * [Jerry Brady / mintyfresh68](https://github.com/bluecircle) - got coin-switching fully working and developed proxy-per-algo feature -* [Tony Dobbs](http://anthonydobbs.com) - graphical help with logo and front-end design +* [Tony Dobbs](http://anthonydobbs.com) - designs for front-end and created the NOMP logo * [vekexasia](//github.com/vekexasia) - co-developer & great tester * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing diff --git a/config_example.json b/config_example.json index af3774b..8759fcf 100644 --- a/config_example.json +++ b/config_example.json @@ -8,10 +8,21 @@ "website": { "enabled": true, - "siteTitle": "Cryppit", "port": 80, - "statUpdateInterval": 1.5, - "hashrateWindow": 300 + "stratumHost": "cryppit.com", + "stats": { + "updateInterval": 15, + "historicalRetention": 43200, + "hashrateWindow": 300, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "adminCenter": { + "enabled": true, + "password": "password" + } }, "blockNotifyListener": { diff --git a/libs/api.js b/libs/api.js index 092d9b8..53ebd70 100644 --- a/libs/api.js +++ b/libs/api.js @@ -17,6 +17,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ case 'stats': res.end(portalStats.statsString); return; + case 'pool_stats': + res.writeHead(200, {'content-encoding': 'gzip'}); + res.end(portalStats.statPoolHistoryBuffer); + return; case 'live_stats': res.writeHead(200, { 'Content-Type': 'text/event-stream', @@ -36,4 +40,16 @@ module.exports = function(logger, portalConfig, poolConfigs){ } }; + + this.handleAdminApiRequest = function(req, res, next){ + switch(req.params.method){ + case 'pools': { + res.end(JSON.stringify({result: poolConfigs})); + return; + } + default: + next(); + } + }; + }; \ No newline at end of file diff --git a/libs/stats.js b/libs/stats.js index 7f2e126..459ff14 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -1,6 +1,9 @@ +var zlib = require('zlib'); + var redis = require('redis'); var async = require('async'); + var os = require('os'); var algos = require('stratum-pool/lib/algoProperties.js'); @@ -13,6 +16,17 @@ module.exports = function(logger, portalConfig, poolConfigs){ var logSystem = 'Stats'; var redisClients = []; + var redisStats; + + this.statHistory = []; + this.statPoolHistory = []; + this.statPoolHistoryBuffer; + + this.stats = {}; + this.statsString = ''; + + setupStatsRedis(); + gatherStatHistory(); var canDoStats = true; @@ -45,15 +59,65 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); - this.stats = {}; - this.statsString = ''; + function setupStatsRedis(){ + redisStats = redis.createClient(portalConfig.website.stats.redis.port, portalConfig.website.stats.redis.host); + redisStats.on('error', function(err){ + logger.error(logSystem, 'Historics', 'Redis for stats had an error ' + JSON.stringify(err)); + }); + } + + function gatherStatHistory(){ + + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString(); + + redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){ + if (err) { + logger.error(logSystem, 'Historics', 'Error when trying to grab historical stats ' + JSON.stringify(err)); + return; + } + for (var i = 0; i < replies.length; i++){ + _this.statHistory.push(JSON.parse(replies[i])); + } + _this.statHistory = _this.statHistory.sort(function(a, b){ + return a.time - b.time; + }); + _this.statHistory.forEach(function(stats){ + addStatPoolHistory(stats); + }); + deflateStatPoolHistory(); + }); + } + + function addStatPoolHistory(stats){ + var data = { + time: stats.time, + pools: {} + }; + for (var pool in stats.pools){ + data.pools[pool] = { + hashrate: stats.pools[pool].hashrate, + workers: stats.pools[pool].workerCount, + blocks: stats.pools[pool].blocks + } + } + _this.statPoolHistory.push(data); + } + + + function deflateStatPoolHistory(){ + zlib.gzip(JSON.stringify(_this.statPoolHistory), function(err, buffer){ + _this.statPoolHistoryBuffer = buffer; + }); + } this.getGlobalStats = function(callback){ + var statGatherTime = Date.now() / 1000 | 0; + var allCoinStats = {}; async.each(redisClients, function(client, callback){ - var windowTime = (((Date.now() / 1000) - portalConfig.website.hashrateWindow) | 0).toString(); + var windowTime = (((Date.now() / 1000) - portalConfig.website.stats.hashrateWindow) | 0).toString(); var redisCommands = []; @@ -68,11 +132,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ var commandsPerCoin = redisComamndTemplates.length; - client.coins.map(function(coin){ redisComamndTemplates.map(function(t){ var clonedTemplates = t.slice(0); - clonedTemplates[1] = coin + clonedTemplates [1]; + clonedTemplates[1] = coin + clonedTemplates[1]; redisCommands.push(clonedTemplates); }); }); @@ -80,7 +143,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ client.client.multi(redisCommands).exec(function(err, replies){ if (err){ - console.log('error with getting hashrate stats ' + JSON.stringify(err)); + logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err)); callback(err); } else{ @@ -105,12 +168,13 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); }, function(err){ if (err){ - console.log('error getting all stats' + JSON.stringify(err)); + logger.error(logSystem, 'Global', 'error getting all stats' + JSON.stringify(err)); callback(); return; } var portalStats = { + time: statGatherTime, global:{ workers: 0, hashrate: 0 @@ -129,24 +193,25 @@ module.exports = function(logger, portalConfig, poolConfigs){ coinStats.shares += workerShares; var worker = parts[1]; if (worker in coinStats.workers) - coinStats.workers[worker] += workerShares + coinStats.workers[worker] += workerShares; else - coinStats.workers[worker] = workerShares + coinStats.workers[worker] = workerShares; }); var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; - var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; + var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow; coinStats.hashrate = hashratePre | 0; - portalStats.global.workers += Object.keys(coinStats.workers).length; + coinStats.workerCount = Object.keys(coinStats.workers).length; + portalStats.global.workers += coinStats.workerCount; /* algorithm specific global stats */ - var algo = coinStats.algorithm; + var algo = coinStats.algorithm; if (!portalStats.algos.hasOwnProperty(algo)){ portalStats.algos[algo] = { workers: 0, hashrate: 0, hashrateString: null }; - } + } portalStats.algos[algo].hashrate += coinStats.hashrate; portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; @@ -162,6 +227,33 @@ module.exports = function(logger, portalConfig, poolConfigs){ _this.stats = portalStats; _this.statsString = JSON.stringify(portalStats); + + + + _this.statHistory.push(portalStats); + addStatPoolHistory(portalStats); + + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0); + + for (var i = 0; i < _this.statHistory.length; i++){ + if (retentionTime < _this.statHistory[i].time){ + if (i > 0) { + _this.statHistory = _this.statHistory.slice(i); + _this.statPoolHistory = _this.statPoolHistory.slice(i); + } + break; + } + } + + deflateStatPoolHistory(); + + redisStats.multi([ + ['zadd', 'statHistory', statGatherTime, _this.statsString], + ['zremrangebyscore', 'statHistory', '-inf', '(' + retentionTime] + ]).exec(function(err, replies){ + if (err) + logger.error(logSystem, 'Historics', 'Error adding stats to historics ' + JSON.stringify(err)); + }); callback(); }); diff --git a/libs/website.js b/libs/website.js index d4ea491..44d9b55 100644 --- a/libs/website.js +++ b/libs/website.js @@ -5,6 +5,8 @@ var path = require('path'); var async = require('async'); var dot = require('dot'); var express = require('express'); +var bodyParser = require('body-parser'); +var compress = require('compression'); var watch = require('node-watch'); @@ -29,7 +31,8 @@ module.exports = function(logger){ 'home.html': '', 'getting_started.html': 'getting_started', 'stats.html': 'stats', - 'api.html': 'api' + 'api.html': 'api', + 'admin.html': 'admin' }; var pageTemplates = {}; @@ -60,8 +63,9 @@ module.exports = function(logger){ }; - var readPageFiles = function(){ - async.each(Object.keys(pageFiles), function(fileName, callback){ + + var readPageFiles = function(files){ + async.each(files, function(fileName, callback){ var filePath = 'website/' + (fileName === 'index.html' ? '' : 'pages/') + fileName; fs.readFile(filePath, 'utf8', function(err, data){ var pTemp = dot.template(data); @@ -78,12 +82,14 @@ module.exports = function(logger){ }; - + //If an html file was changed reload it watch('website', function(filename){ - //if (event === 'change' && filename in pageFiles) - //READ ALL THE FILEZ BLAHHH - readPageFiles(); - + var basename = path.basename(filename); + if (basename in pageFiles){ + console.log(filename); + readPageFiles([basename]); + logger.debug(logSystem, 'Server', 'Reloaded file ' + basename); + } }); portalStats.getGlobalStats(function(){ @@ -103,10 +109,9 @@ module.exports = function(logger){ }); }; - setInterval(buildUpdatedWebsite, websiteConfig.statUpdateInterval * 1000); + setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000); - var app = express(); var getPage = function(pageId){ if (pageId in pageProcessed){ @@ -127,6 +132,11 @@ module.exports = function(logger){ + var app = express(); + + + app.use(bodyParser.json()); + app.get('/get_page', function(req, res, next){ var requestedPage = getPage(req.query.id); if (requestedPage){ @@ -143,11 +153,27 @@ module.exports = function(logger){ portalApi.handleApiRequest(req, res, next); }); + app.post('/api/admin/:method', function(req, res, next){ + if (portalConfig.website + && portalConfig.website.adminCenter + && portalConfig.website.adminCenter.enabled){ + if (portalConfig.website.adminCenter.password === req.body.password) + portalApi.handleAdminApiRequest(req, res, next); + else + res.send(401, JSON.stringify({error: 'Incorrect Password'})); + + } + else + next(); + + }); + + app.use(compress()); app.use('/static', express.static('website/static')); app.use(function(err, req, res, next){ console.error(err.stack); - res.end(500, 'Something broke!'); + res.send(500, 'Something broke!'); }); app.listen(portalConfig.website.port, function(){ diff --git a/package.json b/package.json index 24e73b1..c1a28a3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "mysql": "*", "async": "*", "express": "*", + "body-parser": "*", + "compression": "*", "dot": "*", "colors": "*", "node-watch": "*" diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 0e7536e..a753c31 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -53,7 +53,10 @@ "ports": { "3008": { - "diff": 8, + "diff": 8 + }, + "3032": { + "diff": 32, "varDiff": { "minDiff": 8, "maxDiff": 512, @@ -62,9 +65,6 @@ "variancePercent": 30 } }, - "3032": { - "diff": 8 - }, "3256": { "diff": 256 } diff --git a/website/index.html b/website/index.html index e68fdb4..e613e65 100644 --- a/website/index.html +++ b/website/index.html @@ -6,64 +6,57 @@ + + + - + - + + + + + - - {{=it.portalConfig.website.siteTitle}} - + NOMP -
-
+
+
+ +  NOMP + - - {{ for(var algo in it.stats.algos) { }} - {{=algo}}:
-
 {{=it.stats.algos[algo].workers}} Miners
-
 {{=it.stats.algos[algo].hashrateString}}
-
  - {{ } }} -
-
- {{=it.page}} -
+ {{=it.page}}
@@ -86,6 +79,5 @@ - diff --git a/website/pages/admin.html b/website/pages/admin.html new file mode 100644 index 0000000..b752a0c --- /dev/null +++ b/website/pages/admin.html @@ -0,0 +1,50 @@ +
+ + + +
+
+ Password + + + + + + +
+
+ +
+ +
+ Administration + +
+ +
+ +
+ + + + +
\ No newline at end of file diff --git a/website/pages/getting_started.html b/website/pages/getting_started.html index e228dc7..37e8dad 100644 --- a/website/pages/getting_started.html +++ b/website/pages/getting_started.html @@ -1,5 +1,278 @@ -
+ + +
+ + + + +
+ + + + + + \ No newline at end of file diff --git a/website/pages/home.html b/website/pages/home.html index 53d5751..75dee3d 100644 --- a/website/pages/home.html +++ b/website/pages/home.html @@ -1 +1,119 @@ -
Welcome to {{=it.portalConfig.website.siteTitle}}! - The first multi-pool running on NOMP!
\ No newline at end of file + + + +
+
+ +
+
+
Welcome to the future of mining
+
    +
  • Low fees
  • +
  • High performance Node.js backend
  • +
  • User friendly mining client
  • +
  • Multi-coin / multi-pool
  • +
+
+
+ +
+ +
+
+
Global Stats
+
+ {{ for(var algo in it.stats.algos) { }} +
+
{{=algo}}
+
{{=it.stats.algos[algo].workers}} Miners
+
{{=it.stats.algos[algo].hashrateString}}
+
+ {{ } }} +
+
+
+ +
+
+
Pools / Coins
+
+ {{ for(var pool in it.stats.pools) { }} +
+
{{=pool}}
+
{{=Object.keys(it.stats.pools[pool].workers).length}} Miners
+
{{=it.stats.pools[pool].hashrateString}}
+
+ {{ } }} +
+
+
+ +
\ No newline at end of file diff --git a/website/pages/stats.html b/website/pages/stats.html index 7a71d0e..d7193b3 100644 --- a/website/pages/stats.html +++ b/website/pages/stats.html @@ -1,27 +1,44 @@ -
- - - - - - - - - - - - - - {{ for(var pool in it.stats.pools) { }} - - - - - - - - - {{ } }} -
PoolAlgoWorkersValid SharesInvalid SharesBlocksHashrate
{{=it.stats.pools[pool].name}}{{=it.stats.pools[pool].algorithm}}{{=Object.keys(it.stats.pools[pool].workers).length}}{{=it.stats.pools[pool].poolStats.validShares}}{{=it.stats.pools[pool].poolStats.invalidShares}}{{=it.stats.pools[pool].poolStats.validBlocks}}{{=it.stats.pools[pool].hashrateString}} -
+ + +
+ +
Workers Per Pool
+
+ +
Hashrate Per Pool
+
+ +
Blocks Pending Per Pool
+
+
+ + + + \ No newline at end of file diff --git a/website/static/admin.js b/website/static/admin.js new file mode 100644 index 0000000..82c3666 --- /dev/null +++ b/website/static/admin.js @@ -0,0 +1,100 @@ +var docCookies = { + getItem: function (sKey) { + return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; + }, + setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) { + if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; } + var sExpires = ""; + if (vEnd) { + switch (vEnd.constructor) { + case Number: + sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; + break; + case String: + sExpires = "; expires=" + vEnd; + break; + case Date: + sExpires = "; expires=" + vEnd.toUTCString(); + break; + } + } + document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); + return true; + }, + removeItem: function (sKey, sPath, sDomain) { + if (!sKey || !this.hasItem(sKey)) { return false; } + document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : ""); + return true; + }, + hasItem: function (sKey) { + return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); + } +}; + +var password = docCookies.getItem('password'); + + +function showLogin(){ + $('#adminCenter').hide(); + $('#passwordForm').show(); +} + +function showAdminCenter(){ + $('#passwordForm').hide(); + $('#adminCenter').show(); +} + +function tryLogin(){ + apiRequest('pools', {}, function(response){ + showAdminCenter(); + displayMenu(response.result) + }); +} + +function displayMenu(pools){ + $('#poolList').after(Object.keys(pools).map(function(poolName){ + return '
  • ' + poolName + '
  • '; + }).join('')); +} + +function apiRequest(func, data, callback){ + var httpRequest = new XMLHttpRequest(); + httpRequest.onreadystatechange = function(){ + if (httpRequest.readyState === 4 && httpRequest.responseText){ + if (httpRequest.status === 401){ + docCookies.removeItem('password'); + $('#password').val(''); + showLogin(); + alert('Incorrect Password'); + } + else{ + var response = JSON.parse(httpRequest.responseText); + callback(response); + } + } + }; + httpRequest.open('POST', '/api/admin/' + func); + data.password = password; + httpRequest.setRequestHeader('Content-Type', 'application/json'); + httpRequest.send(JSON.stringify(data)); +} + +if (password){ + tryLogin(); +} +else{ + showLogin(); +} + +$('#passwordForm').submit(function(event){ + event.preventDefault(); + password = $('#password').val(); + if (password){ + if ($('#remember').is(':checked')) + docCookies.setItem('password', password, Infinity); + else + docCookies.setItem('password', password); + tryLogin(); + } + return false; +}); diff --git a/website/static/main.js b/website/static/main.js index ebb149c..23a88ef 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -1,11 +1,12 @@ + $(function(){ var hotSwap = function(page, pushSate){ if (pushSate) history.pushState(null, null, '/' + page); - $('.selected').removeClass('selected'); - $('a[href="/' + page + '"]').parent().addClass('selected') + $('.pure-menu-selected').removeClass('pure-menu-selected'); + $('a[href="/' + page + '"]').parent().addClass('pure-menu-selected'); $.get("/get_page", {id: page}, function(data){ - $('#page').html(data); + $('main').html(data); }, 'html') }; @@ -25,7 +26,7 @@ $(function(){ }, 0); }); - var statsSource = new EventSource("/api/live_stats"); + window.statsSource = new EventSource("/api/live_stats"); statsSource.addEventListener('message', function(e){ var stats = JSON.parse(e.data); for (algo in stats.algos) { diff --git a/website/static/nvd3.css b/website/static/nvd3.css new file mode 100644 index 0000000..d46f7eb --- /dev/null +++ b/website/static/nvd3.css @@ -0,0 +1 @@ +.chartWrap{margin:0;padding:0;overflow:hidden}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;font-family:Arial;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 250ms linear;-moz-transition:opacity 250ms linear;-webkit-transition:opacity 250ms linear;transition-delay:250ms;-moz-transition-delay:250ms;-webkit-transition-delay:250ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:400;background-color:rgba(247,247,247,.75);text-align:center;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:400}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{position:absolute;pointer-events:none}svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}svg text{font:400 12px Arial}svg .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .disabled circle{fill-opacity:0}.nvd3 .nv-axis{pointer-events:none}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-bars .negative rect{zfill:brown}.nvd3 .nv-bars rect{zfill:#4682b4;fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie .hover path{fill-opacity:.7}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups path.nv-line{fill:none;stroke-width:1.5px}.nvd3 .nv-groups path.nv-line.nv-thin-line{stroke-width:1px}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3 .nv-line.hover path{stroke-width:6px}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}.nvd3 .nv-distribution{pointer-events:none}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:4px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel{font-weight:700}.nvd3.nv-historicalStockChart .nv-dragTarget{fill-opacity:0;stroke:none;cursor:move}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}.nvd3.nv-indentedtree .name{margin-left:5px}.nvd3.nv-indentedtree .clickable{color:#08C;cursor:pointer}.nvd3.nv-indentedtree span.clickable:hover{color:#005580;text-decoration:underline}.nvd3.nv-indentedtree .nv-childrenCount{display:inline-block;margin-left:5px}.nvd3.nv-indentedtree .nv-treeicon{cursor:pointer}.nvd3.nv-indentedtree .nv-treeicon.nv-folded{cursor:pointer}.nvd3 .background path{fill:none;stroke:#ccc;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke:#4682b4;stroke-opacity:.7}.nvd3 .brush .extent{fill-opacity:.3;stroke:#fff;shape-rendering:crispEdges}.nvd3 .axis line,.axis path{fill:none;stroke:#000;shape-rendering:crispEdges}.nvd3 .axis text{text-shadow:0 1px 0 #fff}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc} \ No newline at end of file diff --git a/website/static/nvd3.js b/website/static/nvd3.js new file mode 100644 index 0000000..bddd4ae --- /dev/null +++ b/website/static/nvd3.js @@ -0,0 +1,6 @@ +(function(){function t(e,t){return(new Date(t,e+1,0)).getDate()}function n(e,t,n){return function(r,i,s){var o=e(r),u=[];o1)while(op||r>d||d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined||a){if(l&&d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined&&d3.event.relatedTarget.className.match(t.nvPointerEventsClass))return;u.elementMouseout({mouseX:n,mouseY:r}),c.renderGuideLine(null);return}var f=s.invert(n);u.elementMousemove({mouseX:n,mouseY:r,pointXValue:f}),d3.event.type==="dblclick"&&u.elementDblclick({mouseX:n,mouseY:r,pointXValue:f})}var h=d3.select(this),p=n||960,d=r||400,v=h.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([o]),m=v.enter().append("g").attr("class"," nv-wrap nv-interactiveLineLayer");m.append("g").attr("class","nv-interactiveGuideLine");if(!f)return;f.on("mousemove",g,!0).on("mouseout",g,!0).on("dblclick",g),c.renderGuideLine=function(t){if(!a)return;var n=v.select(".nv-interactiveGuideLine").selectAll("line").data(t!=null?[e.utils.NaNtoZero(t)]:[],String);n.enter().append("line").attr("class","nv-guideline").attr("x1",function(e){return e}).attr("x2",function(e){return e}).attr("y1",d).attr("y2",0),n.exit().remove()}})}var t=e.models.tooltip(),n=null,r=null,i={left:0,top:0},s=d3.scale.linear(),o=d3.scale.linear(),u=d3.dispatch("elementMousemove","elementMouseout","elementDblclick"),a=!0,f=null,l=navigator.userAgent.indexOf("MSIE")!==-1;return c.dispatch=u,c.tooltip=t,c.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.left=typeof e.left!="undefined"?e.left:i.left,c):i},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.xScale=function(e){return arguments.length?(s=e,c):s},c.showGuideLine=function(e){return arguments.length?(a=e,c):a},c.svgContainer=function(e){return arguments.length?(f=e,c):f},c},e.interactiveBisect=function(e,t,n){"use strict";if(!e instanceof Array)return null;typeof n!="function"&&(n=function(e,t){return e.x});var r=d3.bisector(n).left,i=d3.max([0,r(e,t)-1]),s=n(e[i],i);typeof s=="undefined"&&(s=i);if(s===t)return i;var o=d3.min([i+1,e.length-1]),u=n(e[o],o);return typeof u=="undefined"&&(u=o),Math.abs(u-t)>=Math.abs(s-t)?i:o},e.nearestValueIndex=function(e,t,n){"use strict";var r=Infinity,i=null;return e.forEach(function(e,s){var o=Math.abs(t-e);o<=r&&oT.height?0:x}v.top=Math.abs(x-S.top),v.left=Math.abs(E.left-S.left)}t+=a.offsetLeft+v.left-2*a.scrollLeft,u+=a.offsetTop+v.top-2*a.scrollTop}return s&&s>0&&(u=Math.floor(u/s)*s),e.tooltip.calcTooltipPosition([t,u],r,i,h),w}var t=null,n=null,r="w",i=50,s=25,o=null,u=null,a=null,f=null,l={left:null,top:null},c=!0,h="nvtooltip-"+Math.floor(Math.random()*1e5),p="nv-pointer-events-none",d=function(e,t){return e},v=function(e){return e},m=function(e){if(t!=null)return t;if(e==null)return"";var n=d3.select(document.createElement("table")),r=n.selectAll("thead").data([e]).enter().append("thead");r.append("tr").append("td").attr("colspan",3).append("strong").classed("x-value",!0).html(v(e.value));var i=n.selectAll("tbody").data([e]).enter().append("tbody"),s=i.selectAll("tr").data(function(e){return e.series}).enter().append("tr").classed("highlight",function(e){return e.highlight});s.append("td").classed("legend-color-guide",!0).append("div").style("background-color",function(e){return e.color}),s.append("td").classed("key",!0).html(function(e){return e.key}),s.append("td").classed("value",!0).html(function(e,t){return d(e.value,t)}),s.selectAll("td").each(function(e){if(e.highlight){var t=d3.scale.linear().domain([0,1]).range(["#fff",e.color]),n=.6;d3.select(this).style("border-bottom-color",t(n)).style("border-top-color",t(n))}});var o=n.node().outerHTML;return e.footer!==undefined&&(o+=""),o},g=function(e){return e&&e.series&&e.series.length>0?!0:!1};return w.nvPointerEventsClass=p,w.content=function(e){return arguments.length?(t=e,w):t},w.tooltipElem=function(){return f},w.contentGenerator=function(e){return arguments.length?(typeof e=="function"&&(m=e),w):m},w.data=function(e){return arguments.length?(n=e,w):n},w.gravity=function(e){return arguments.length?(r=e,w):r},w.distance=function(e){return arguments.length?(i=e,w):i},w.snapDistance=function(e){return arguments.length?(s=e,w):s},w.classes=function(e){return arguments.length?(u=e,w):u},w.chartContainer=function(e){return arguments.length?(a=e,w):a},w.position=function(e){return arguments.length?(l.left=typeof e.left!="undefined"?e.left:l.left,l.top=typeof e.top!="undefined"?e.top:l.top,w):l},w.fixedTop=function(e){return arguments.length?(o=e,w):o},w.enabled=function(e){return arguments.length?(c=e,w):c},w.valueFormatter=function(e){return arguments.length?(typeof e=="function"&&(d=e),w):d},w.headerFormatter=function(e){return arguments.length?(typeof e=="function"&&(v=e),w):v},w.id=function(){return h},w},e.tooltip.show=function(t,n,r,i,s,o){var u=document.createElement("div");u.className="nvtooltip "+(o?o:"xy-tooltip");var a=s;if(!s||s.tagName.match(/g|svg/i))a=document.getElementsByTagName("body")[0];u.style.left=0,u.style.top=0,u.style.opacity=0,u.innerHTML=n,a.appendChild(u),s&&(t[0]=t[0]-s.scrollLeft,t[1]=t[1]-s.scrollTop),e.tooltip.calcTooltipPosition(t,r,i,u)},e.tooltip.findFirstNonSVGParent=function(e){while(e.tagName.match(/^g|svg$/i)!==null)e=e.parentNode;return e},e.tooltip.findTotalOffsetTop=function(e,t){var n=t;do isNaN(e.offsetTop)||(n+=e.offsetTop);while(e=e.offsetParent);return n},e.tooltip.findTotalOffsetLeft=function(e,t){var n=t;do isNaN(e.offsetLeft)||(n+=e.offsetLeft);while(e=e.offsetParent);return n},e.tooltip.calcTooltipPosition=function(t,n,r,i){var s=parseInt(i.offsetHeight),o=parseInt(i.offsetWidth),u=e.utils.windowSize().width,a=e.utils.windowSize().height,f=window.pageYOffset,l=window.pageXOffset,c,h;a=window.innerWidth>=document.body.scrollWidth?a:a-16,u=window.innerHeight>=document.body.scrollHeight?u:u-16,n=n||"s",r=r||20;var p=function(t){return e.tooltip.findTotalOffsetTop(t,h)},d=function(t){return e.tooltip.findTotalOffsetLeft(t,c)};switch(n){case"e":c=t[0]-o-r,h=t[1]-s/2;var v=d(i),m=p(i);vl?t[0]+r:l-v+c),mf+a&&(h=f+a-m+h-s);break;case"w":c=t[0]+r,h=t[1]-s/2;var v=d(i),m=p(i);v+o>u&&(c=t[0]-o-r),mf+a&&(h=f+a-m+h-s);break;case"n":c=t[0]-o/2-5,h=t[1]+r;var v=d(i),m=p(i);vu&&(c=c-o/2+5),m+s>f+a&&(h=f+a-m+h-s);break;case"s":c=t[0]-o/2,h=t[1]-s-r;var v=d(i),m=p(i);vu&&(c=c-o/2+5),f>m&&(h=f);break;case"none":c=t[0],h=t[1]-r;var v=d(i),m=p(i)}return i.style.left=c+"px",i.style.top=h+"px",i.style.opacity=1,i.style.position="absolute",i},e.tooltip.cleanup=function(){var e=document.getElementsByClassName("nvtooltip"),t=[];while(e.length)t.push(e[0]),e[0].style.transitionDelay="0 !important",e[0].style.opacity=0,e[0].className="nvtooltip-pending-removal";setTimeout(function(){while(t.length){var e=t.pop();e.parentNode.removeChild(e)}},500)}}(),e.utils.windowSize=function(){var e={width:640,height:480};return document.body&&document.body.offsetWidth&&(e.width=document.body.offsetWidth,e.height=document.body.offsetHeight),document.compatMode=="CSS1Compat"&&document.documentElement&&document.documentElement.offsetWidth&&(e.width=document.documentElement.offsetWidth,e.height=document.documentElement.offsetHeight),window.innerWidth&&window.innerHeight&&(e.width=window.innerWidth,e.height=window.innerHeight),e},e.utils.windowResize=function(e){if(e===undefined)return;var t=window.onresize;window.onresize=function(n){typeof t=="function"&&t(n),e(n)}},e.utils.getColor=function(t){return arguments.length?Object.prototype.toString.call(t)==="[object Array]"?function(e,n){return e.color||t[n%t.length]}:t:e.utils.defaultColor()},e.utils.defaultColor=function(){var e=d3.scale.category20().range();return function(t,n){return t.color||e[n%e.length]}},e.utils.customTheme=function(e,t,n){t=t||function(e){return e.key},n=n||d3.scale.category20().range();var r=n.length;return function(i,s){var o=t(i);return r||(r=n.length),typeof e[o]!="undefined"?typeof e[o]=="function"?e[o]():e[o]:n[--r]}},e.utils.pjax=function(t,n){function r(r){d3.html(r,function(r){var i=d3.select(n).node();i.parentNode.replaceChild(d3.select(r).select(n).node(),i),e.utils.pjax(t,n)})}d3.selectAll(t).on("click",function(){history.pushState(this.href,this.textContent,this.href),r(this.href),d3.event.preventDefault()}),d3.select(window).on("popstate",function(){d3.event.state&&r(d3.event.state)})},e.utils.calcApproxTextWidth=function(e){if(typeof e.style=="function"&&typeof e.text=="function"){var t=parseInt(e.style("font-size").replace("px","")),n=e.text().length;return n*t*.5}return 0},e.utils.NaNtoZero=function(e){return typeof e!="number"||isNaN(e)||e===null||e===Infinity?0:e},e.utils.optionsFunc=function(e){return e&&d3.map(e).forEach(function(e,t){typeof this[e]=="function"&&this[e](t)}.bind(this)),this},e.models.axis=function(){"use strict";function m(e){return e.each(function(e){var i=d3.select(this),m=i.selectAll("g.nv-wrap.nv-axis").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-axis"),y=g.append("g"),b=m.select("g");p!==null?t.ticks(p):(t.orient()=="top"||t.orient()=="bottom")&&t.ticks(Math.abs(s.range()[1]-s.range()[0])/100),b.transition().call(t),v=v||t.scale();var w=t.tickFormat();w==null&&(w=v.tickFormat());var E=b.selectAll("text.nv-axislabel").data([o||null]);E.exit().remove();switch(t.orient()){case"top":E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",0).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+s(e)+",0)"}).select("text").attr("dy","-0.5em").attr("y",-t.tickPadding()).attr("text-anchor","middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+s.range()[t]+",0)"})}break;case"bottom":var T=36,N=30,C=b.selectAll("g").select("text");if(f%360){C.each(function(e,t){var n=this.getBBox().width;n>N&&(N=n)});var k=Math.abs(Math.sin(f*Math.PI/180)),T=(k?k*N:N)+30;C.attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f%360>0?"start":"end")}E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",T).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data([s.domain()[0],s.domain()[s.domain().length-1]]);x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"}).select("text").attr("dy",".71em").attr("y",t.tickPadding()).attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f?f%360>0?"start":"end":"middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"})}c&&C.attr("transform",function(e,t){return"translate(0,"+(t%2==0?"0":"12")+")"});break;case"right":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"begin").attr("transform",l?"rotate(90)":"").attr("y",l?-Math.max(n.right,r)+12:-10).attr("x",l?s.range()[0]/2:t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+s(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",t.tickPadding()).style("text-anchor","start").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}break;case"left":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"end").attr("transform",l?"rotate(-90)":"").attr("y",l?-Math.max(n.left,r)+d:-10).attr("x",l?-s.range()[0]/2:-t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+v(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",-t.tickPadding()).attr("text-anchor","end").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}}E.text(function(e){return e}),u&&(t.orient()==="left"||t.orient()==="right")&&(b.selectAll("g").each(function(e,t){d3.select(this).select("text").attr("opacity",1);if(s(e)s.range()[0]-10)(e>1e-10||e<-1e-10)&&d3.select(this).attr("opacity",0),d3.select(this).select("text").attr("opacity",0)}),s.domain()[0]==s.domain()[1]&&s.domain()[0]==0&&m.selectAll("g.nv-axisMaxMin").style("opacity",function(e,t){return t?0:1}));if(u&&(t.orient()==="top"||t.orient()==="bottom")){var L=[];m.selectAll("g.nv-axisMaxMin").each(function(e,t){try{t?L.push(s(e)-this.getBBox().width-4):L.push(s(e)+this.getBBox().width+4)}catch(n){t?L.push(s(e)-4):L.push(s(e)+4)}}),b.selectAll("g").each(function(e,t){if(s(e)L[1])e>1e-10||e<-1e-10?d3.select(this).remove():d3.select(this).select("text").remove()})}a&&b.selectAll(".tick").filter(function(e){return!parseFloat(Math.round(e.__data__*1e5)/1e6)&&e.__data__!==undefined}).classed("zero",!0),v=s.copy()}),m}var t=d3.svg.axis(),n={top:0,right:0,bottom:0,left:0},r=75,i=60,s=d3.scale.linear(),o=null,u=!0,a=!0,f=0,l=!0,c=!1,h=!1,p=null,d=12;t.scale(s).orient("bottom").tickFormat(function(e){return e});var v;return m.axis=t,d3.rebind(m,t,"orient","tickValues","tickSubdivide","tickSize","tickPadding","tickFormat"),d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.ticks=function(e){return arguments.length?(p=e,m):p},m.height=function(e){return arguments.length?(i=e,m):i},m.axisLabel=function(e){return arguments.length?(o=e,m):o},m.showMaxMin=function(e){return arguments.length?(u=e,m):u},m.highlightZero=function(e){return arguments.length?(a=e,m):a},m.scale=function(e){return arguments.length?(s=e,t.scale(s),h=typeof s.rangeBands=="function",d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m):s},m.rotateYLabel=function(e){return arguments.length?(l=e,m):l},m.rotateLabels=function(e){return arguments.length?(f=e,m):f},m.staggerLabels=function(e){return arguments.length?(c=e,m):c},m.axisLabelDistance=function(e){return arguments.length?(d=e,m):d},m},e.models.bullet=function(){"use strict";function m(e){return e.each(function(e,n){var p=c-t.left-t.right,m=h-t.top-t.bottom,g=d3.select(this),y=i.call(this,e,n).slice().sort(d3.descending),b=s.call(this,e,n).slice().sort(d3.descending),w=o.call(this,e,n).slice().sort(d3.descending),E=u.call(this,e,n).slice(),S=a.call(this,e,n).slice(),x=f.call(this,e,n).slice(),T=d3.scale.linear().domain(d3.extent(d3.merge([l,y]))).range(r?[p,0]:[0,p]),N=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(T.range());this.__chart__=T;var C=d3.min(y),k=d3.max(y),L=y[1],A=g.selectAll("g.nv-wrap.nv-bullet").data([e]),O=A.enter().append("g").attr("class","nvd3 nv-wrap nv-bullet"),M=O.append("g"),_=A.select("g");M.append("rect").attr("class","nv-range nv-rangeMax"),M.append("rect").attr("class","nv-range nv-rangeAvg"),M.append("rect").attr("class","nv-range nv-rangeMin"),M.append("rect").attr("class","nv-measure"),M.append("path").attr("class","nv-markerTriangle"),A.attr("transform","translate("+t.left+","+t.top+")");var D=function(e){return Math.abs(N(e)-N(0))},P=function(e){return Math.abs(T(e)-T(0))},H=function(e){return e<0?N(e):N(0)},B=function(e){return e<0?T(e):T(0)};_.select("rect.nv-rangeMax").attr("height",m).attr("width",P(k>0?k:C)).attr("x",B(k>0?k:C)).datum(k>0?k:C),_.select("rect.nv-rangeAvg").attr("height",m).attr("width",P(L)).attr("x",B(L)).datum(L),_.select("rect.nv-rangeMin").attr("height",m).attr("width",P(k)).attr("x",B(k)).attr("width",P(k>0?C:k)).attr("x",B(k>0?C:k)).datum(k>0?C:k),_.select("rect.nv-measure").style("fill",d).attr("height",m/3).attr("y",m/3).attr("width",w<0?T(0)-T(w[0]):T(w[0])-T(0)).attr("x",B(w)).on("mouseover",function(){v.elementMouseover({value:w[0],label:x[0]||"Current",pos:[T(w[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:w[0],label:x[0]||"Current"})});var j=m/6;b[0]?_.selectAll("path.nv-markerTriangle").attr("transform",function(e){return"translate("+T(b[0])+","+m/2+")"}).attr("d","M0,"+j+"L"+j+","+ -j+" "+ -j+","+ -j+"Z").on("mouseover",function(){v.elementMouseover({value:b[0],label:S[0]||"Previous",pos:[T(b[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:b[0],label:S[0]||"Previous"})}):_.selectAll("path.nv-markerTriangle").remove(),A.selectAll(".nv-range").on("mouseover",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseover({value:e,label:n,pos:[T(e),m/2]})}).on("mouseout",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseout({value:e,label:n})})}),m}var t={top:0,right:0,bottom:0,left:0},n="left",r=!1,i=function(e){return e.ranges},s=function(e){return e.markers},o=function(e){return e.measures},u=function(e){return e.rangeLabels?e.rangeLabels:[]},a=function(e){return e.markerLabels?e.markerLabels:[]},f=function(e){return e.measureLabels?e.measureLabels:[]},l=[0],c=380,h=30,p=null,d=e.utils.getColor(["#1f77b4"]),v=d3.dispatch("elementMouseover","elementMouseout");return m.dispatch=v,m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(i=e,m):i},m.markers=function(e){return arguments.length?(s=e,m):s},m.measures=function(e){return arguments.length?(o=e,m):o},m.forceX=function(e){return arguments.length?(l=e,m):l},m.width=function(e){return arguments.length?(c=e,m):c},m.height=function(e){return arguments.length?(h=e,m):h},m.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,m):t},m.tickFormat=function(e){return arguments.length?(p=e,m):p},m.color=function(t){return arguments.length?(d=e.utils.getColor(t),m):d},m},e.models.bulletChart=function(){"use strict";function m(e){return e.each(function(n,h){var g=d3.select(this),y=(a||parseInt(g.style("width"))||960)-i.left-i.right,b=f-i.top-i.bottom,w=this;m.update=function(){m(e)},m.container=this;if(!n||!s.call(this,n,h)){var E=g.selectAll(".nv-noData").data([p]);return E.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),E.attr("x",i.left+y/2).attr("y",18+i.top+b/2).text(function(e){return e}),m}g.selectAll(".nv-noData").remove();var S=s.call(this,n,h).slice().sort(d3.descending),x=o.call(this,n,h).slice().sort(d3.descending),T=u.call(this,n,h).slice().sort(d3.descending),N=g.selectAll("g.nv-wrap.nv-bulletChart").data([n]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-bulletChart"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-bulletWrap"),k.append("g").attr("class","nv-titles"),N.attr("transform","translate("+i.left+","+i.top+")");var A=d3.scale.linear().domain([0,Math.max(S[0],x[0],T[0])]).range(r?[y,0]:[0,y]),O=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(A.range());this.__chart__=A;var M=function(e){return Math.abs(O(e)-O(0))},_=function(e){return Math.abs(A(e)-A(0))},D=k.select(".nv-titles").append("g").attr("text-anchor","end").attr("transform","translate(-6,"+(f-i.top-i.bottom)/2+")");D.append("text").attr("class","nv-title").text(function(e){return e.title}),D.append("text").attr("class","nv-subtitle").attr("dy","1em").text(function(e){return e.subtitle}),t.width(y).height(b);var P=L.select(".nv-bulletWrap");d3.transition(P).call(t);var H=l||A.tickFormat(y/100),B=L.selectAll("g.nv-tick").data(A.ticks(y/50),function(e){return this.textContent||H(e)}),j=B.enter().append("g").attr("class","nv-tick").attr("transform",function(e){return"translate("+O(e)+",0)"}).style("opacity",1e-6);j.append("line").attr("y1",b).attr("y2",b*7/6),j.append("text").attr("text-anchor","middle").attr("dy","1em").attr("y",b*7/6).text(H);var F=d3.transition(B).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1);F.select("line").attr("y1",b).attr("y2",b*7/6),F.select("text").attr("y",b*7/6),d3.transition(B.exit()).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1e-6).remove(),d.on("tooltipShow",function(e){e.key=n.title,c&&v(e,w.parentNode)})}),d3.timer.flush(),m}var t=e.models.bullet(),n="left",r=!1,i={top:5,right:40,bottom:20,left:120},s=function(e){return e.ranges},o=function(e){return e.markers},u=function(e){return e.measures},a=null,f=55,l=null,c=!0,h=function(e,t,n,r,i){return"

    "+t+"

    "+"

    "+n+"

    "},p="No Data Available.",d=d3.dispatch("tooltipShow","tooltipHide"),v=function(t,n){var r=t.pos[0]+(n.offsetLeft||0)+i.left,s=t.pos[1]+(n.offsetTop||0)+i.top,o=h(t.key,t.label,t.value,t,m);e.tooltip.show([r,s],o,t.value<0?"e":"w",null,n)};return t.dispatch.on("elementMouseover.tooltip",function(e){d.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){d.tooltipHide(e)}),d.on("tooltipHide",function(){c&&e.tooltip.cleanup()}),m.dispatch=d,m.bullet=t,d3.rebind(m,t,"color"),m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(s=e,m):s},m.markers=function(e){return arguments.length?(o=e,m):o},m.measures=function(e){return arguments.length?(u=e,m):u},m.width=function(e){return arguments.length?(a=e,m):a},m.height=function(e){return arguments.length?(f=e,m):f},m.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,m):i},m.tickFormat=function(e){return arguments.length?(l=e,m):l},m.tooltips=function(e){return arguments.length?(c=e,m):c},m.tooltipContent=function(e){return arguments.length?(h=e,m):h},m.noData=function(e){return arguments.length?(p=e,m):p},m},e.models.cumulativeLineChart=function(){"use strict";function D(b){return b.each(function(b){function q(e,t){d3.select(D.container).style("cursor","ew-resize")}function R(e,t){M.x=d3.event.x,M.i=Math.round(O.invert(M.x)),rt()}function U(e,t){d3.select(D.container).style("cursor","auto"),x.index=M.i,k.stateChange(x)}function rt(){nt.data([M]);var e=D.transitionDuration();D.transitionDuration(0),D.update(),D.transitionDuration(e)}var A=d3.select(this).classed("nv-chart-"+S,!0),H=this,B=(f||parseInt(A.style("width"))||960)-u.left-u.right,j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom;D.update=function(){A.transition().duration(L).call(D)},D.container=this,x.disabled=b.map(function(e){return!!e.disabled});if(!T){var F;T={};for(F in x)x[F]instanceof Array?T[F]=x[F].slice(0):T[F]=x[F]}var I=d3.behavior.drag().on("dragstart",q).on("drag",R).on("dragend",U);if(!b||!b.length||!b.filter(function(e){return e.values.length}).length){var z=A.selectAll(".nv-noData").data([N]);return z.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),z.attr("x",u.left+B/2).attr("y",u.top+j/2).text(function(e){return e}),D}A.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();if(!y){var W=b.filter(function(e){return!e.disabled}).map(function(e,n){var r=d3.extent(e.values,t.y());return r[0]<-0.95&&(r[0]=-0.95),[(r[0]-r[1])/(1+r[1]),(r[1]-r[0])/(1+r[0])]}),X=[d3.min(W,function(e){return e[0]}),d3.max(W,function(e){return e[1]})];t.yDomain(X)}else t.yDomain(null);O.domain([0,b[0].values.length-1]).range([0,B]).clamp(!0);var b=P(M.i,b),V=g?"none":"all",$=A.selectAll("g.nv-wrap.nv-cumulativeLine").data([b]),J=$.enter().append("g").attr("class","nvd3 nv-wrap nv-cumulativeLine").append("g"),K=$.select("g");J.append("g").attr("class","nv-interactive"),J.append("g").attr("class","nv-x nv-axis").style("pointer-events","none"),J.append("g").attr("class","nv-y nv-axis"),J.append("g").attr("class","nv-background"),J.append("g").attr("class","nv-linesWrap").style("pointer-events",V),J.append("g").attr("class","nv-avgLinesWrap").style("pointer-events","none"),J.append("g").attr("class","nv-legendWrap"),J.append("g").attr("class","nv-controlsWrap"),c&&(i.width(B),K.select(".nv-legendWrap").datum(b).call(i),u.top!=i.height()&&(u.top=i.height(),j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom),K.select(".nv-legendWrap").attr("transform","translate(0,"+ -u.top+")"));if(m){var Q=[{key:"Re-scale y-axis",disabled:!y}];s.width(140).color(["#444","#444","#444"]).rightAlign(!1).margin({top:5,right:0,bottom:5,left:20}),K.select(".nv-controlsWrap").datum(Q).attr("transform","translate(0,"+ -u.top+")").call(s)}$.attr("transform","translate("+u.left+","+u.top+")"),d&&K.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)");var G=b.filter(function(e){return e.tempDisabled});$.select(".tempDisabled").remove(),G.length&&$.append("text").attr("class","tempDisabled").attr("x",B/2).attr("y","-.71em").style("text-anchor","end").text(G.map(function(e){return e.key}).join(", ")+" values cannot be calculated for this time period."),g&&(o.width(B).height(j).margin({left:u.left,top:u.top}).svgContainer(A).xScale(w),$.select(".nv-interactive").call(o)),J.select(".nv-background").append("rect"),K.select(".nv-background rect").attr("width",B).attr("height",j),t.y(function(e){return e.display.y}).width(B).height(j).color(b.map(function(e,t){return e.color||a(e,t)}).filter(function(e,t){return!b[t].disabled&&!b[t].tempDisabled}));var Y=K.select(".nv-linesWrap").datum(b.filter(function(e){return!e.disabled&&!e.tempDisabled}));Y.call(t),b.forEach(function(e,t){e.seriesIndex=t});var Z=b.filter(function(e){return!e.disabled&&!!C(e)}),et=K.select(".nv-avgLinesWrap").selectAll("line").data(Z,function(e){return e.key}),tt=function(e){var t=E(C(e));return t<0?0:t>j?j:t};et.enter().append("line").style("stroke-width",2).style("stroke-dasharray","10,10").style("stroke",function(e,n){return t.color()(e,e.seriesIndex)}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.style("stroke-opacity",function(e){var t=E(C(e));return t<0||t>j?0:1}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.exit().remove();var nt=Y.selectAll(".nv-indexLine").data([M]);nt.enter().append("rect").attr("class","nv-indexLine").attr("width",3).attr("x",-2).attr("fill","red").attr("fill-opacity",.5).style("pointer-events","all").call(I),nt.attr("transform",function(e){return"translate("+O(e.i)+",0)"}).attr("height",j),h&&(n.scale(w).ticks(Math.min(b[0].values.length,B/70)).tickSize(-j,0),K.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),d3.transition(K.select(".nv-x.nv-axis")).call(n)),p&&(r.scale(E).ticks(j/36).tickSize(-B,0),d3.transition(K.select(".nv-y.nv-axis")).call(r)),K.select(".nv-background rect").on("click",function(){M.x=d3.mouse(this)[0],M.i=Math.round(O.invert(M.x)),x.index=M.i,k.stateChange(x),rt()}),t.dispatch.on("elementClick",function(e){M.i=e.pointIndex,M.x=O(M.i),x.index=M.i,k.stateChange(x),rt()}),s.dispatch.on("legendClick",function(e,t){e.disabled=!e.disabled,y=!e.disabled,x.rescaleY=y,k.stateChange(x),D.update()}),i.dispatch.on("stateChange",function(e){x.disabled=e.disabled,k.stateChange(x),D.update()}),o.dispatch.on("elementMousemove",function(i){t.clearHighlights();var s,f,l,c=[];b.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,D.x()),t.highlightPoint(r,f,!0);var o=n.values[f];if(typeof o=="undefined")return;typeof s=="undefined"&&(s=o),typeof l=="undefined"&&(l=D.xScale()(D.x()(o,f))),c.push({key:n.key,value:D.y()(o,f),color:a(n,n.seriesIndex)})});if(c.length>2){var h=D.yScale().invert(i.mouseY),p=Math.abs(D.yScale().domain()[0]-D.yScale().domain()[1]),d=.03*p,m=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);m!==null&&(c[m].highlight=!0)}var g=n.tickFormat()(D.x()(s,f),f);o.tooltip.position({left:l+u.left,top:i.mouseY+u.top}).chartContainer(H.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:g,series:c})(),o.renderGuideLine(l)}),o.dispatch.on("elementMouseout",function(e){k.tooltipHide(),t.clearHighlights()}),k.on("tooltipShow",function(e){v&&_(e,H.parentNode)}),k.on("changeState",function(e){typeof e.disabled!="undefined"&&(b.forEach(function(t,n){t.disabled=e.disabled[n]}),x.disabled=e.disabled),typeof e.index!="undefined"&&(M.i=e.index,M.x=O(M.i),x.index=e.index,nt.data([M])),typeof e.rescaleY!="undefined"&&(y=e.rescaleY),D.update()})}),D}function P(e,n){return n.map(function(n,r){if(!n.values)return n;var i=t.y()(n.values[e],e);return i<-0.95&&!A?(n.tempDisabled=!0,n):(n.tempDisabled=!1,n.values= +n.values.map(function(e,n){return e.display={y:(t.y()(e,n)-i)/(1+i)},e}),n)})}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:30,bottom:50,left:60},a=e.utils.defaultColor(),f=null,l=null,c=!0,h=!0,p=!0,d=!1,v=!0,m=!0,g=!1,y=!0,b=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},w,E,S=t.id(),x={index:0,rescaleY:y},T=null,N="No Data Available.",C=function(e){return e.average},k=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),L=250,A=!1;n.orient("bottom").tickPadding(7),r.orient(d?"right":"left"),s.updateState(!1);var O=d3.scale.linear(),M={i:0,x:0},_=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,D);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],k.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){k.tooltipHide(e)}),k.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),D.dispatch=k,D.lines=t,D.legend=i,D.xAxis=n,D.yAxis=r,D.interactiveLayer=o,d3.rebind(D,t,"defined","isArea","x","y","xScale","yScale","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id"),D.options=e.utils.optionsFunc.bind(D),D.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,D):u},D.width=function(e){return arguments.length?(f=e,D):f},D.height=function(e){return arguments.length?(l=e,D):l},D.color=function(t){return arguments.length?(a=e.utils.getColor(t),i.color(a),D):a},D.rescaleY=function(e){return arguments.length?(y=e,D):y},D.showControls=function(e){return arguments.length?(m=e,D):m},D.useInteractiveGuideline=function(e){return arguments.length?(g=e,e===!0&&(D.interactive(!1),D.useVoronoi(!1)),D):g},D.showLegend=function(e){return arguments.length?(c=e,D):c},D.showXAxis=function(e){return arguments.length?(h=e,D):h},D.showYAxis=function(e){return arguments.length?(p=e,D):p},D.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),D):d},D.tooltips=function(e){return arguments.length?(v=e,D):v},D.tooltipContent=function(e){return arguments.length?(b=e,D):b},D.state=function(e){return arguments.length?(x=e,D):x},D.defaultState=function(e){return arguments.length?(T=e,D):T},D.noData=function(e){return arguments.length?(N=e,D):N},D.average=function(e){return arguments.length?(C=e,D):C},D.transitionDuration=function(e){return arguments.length?(L=e,D):L},D.noErrorCheck=function(e){return arguments.length?(A=e,D):A},D},e.models.discreteBar=function(){"use strict";function E(e){return e.each(function(e){var i=n-t.left-t.right,E=r-t.top-t.bottom,S=d3.select(this);e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var T=p&&d?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0}})});s.domain(p||d3.merge(T).map(function(e){return e.x})).rangeBands(v||[0,i],.1),o.domain(d||d3.extent(d3.merge(T).map(function(e){return e.y}).concat(f))),c?o.range(m||[E-(o.domain()[0]<0?12:0),o.domain()[1]>0?12:0]):o.range(m||[E,0]),b=b||s,w=w||o.copy().range([o(0),o(0)]);var N=S.selectAll("g.nv-wrap.nv-discretebar").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discretebar"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-groups"),N.attr("transform","translate("+t.left+","+t.top+")");var A=N.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});A.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),A.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),A.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),A.transition().style("stroke-opacity",1).style("fill-opacity",.75);var O=A.selectAll("g.nv-bar").data(function(e){return e.values});O.exit().remove();var M=O.enter().append("g").attr("transform",function(e,t,n){return"translate("+(s(u(e,t))+s.rangeBand()*.05)+", "+o(0)+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),g.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),g.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){g.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){g.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()});M.append("rect").attr("height",0).attr("width",s.rangeBand()*.9/e.length),c?(M.append("text").attr("text-anchor","middle"),O.select("text").text(function(e,t){return h(a(e,t))}).transition().attr("x",s.rangeBand()*.9/2).attr("y",function(e,t){return a(e,t)<0?o(a(e,t))-o(0)+12:-4})):O.selectAll("text").remove(),O.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).style("fill",function(e,t){return e.color||l(e,t)}).style("stroke",function(e,t){return e.color||l(e,t)}).select("rect").attr("class",y).transition().attr("width",s.rangeBand()*.9/e.length),O.transition().attr("transform",function(e,t){var n=s(u(e,t))+s.rangeBand()*.05,r=a(e,t)<0?o(0):o(0)-o(a(e,t))<1?o(0)-1:o(a(e,t));return"translate("+n+", "+r+")"}).select("rect").attr("height",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(d&&d[0]||0))||1)}),b=s.copy(),w=o.copy()}),E}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=!1,h=d3.format(",.2f"),p,d,v,m,g=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),y="discreteBar",b,w;return E.dispatch=g,E.options=e.utils.optionsFunc.bind(E),E.x=function(e){return arguments.length?(u=e,E):u},E.y=function(e){return arguments.length?(a=e,E):a},E.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,E):t},E.width=function(e){return arguments.length?(n=e,E):n},E.height=function(e){return arguments.length?(r=e,E):r},E.xScale=function(e){return arguments.length?(s=e,E):s},E.yScale=function(e){return arguments.length?(o=e,E):o},E.xDomain=function(e){return arguments.length?(p=e,E):p},E.yDomain=function(e){return arguments.length?(d=e,E):d},E.xRange=function(e){return arguments.length?(v=e,E):v},E.yRange=function(e){return arguments.length?(m=e,E):m},E.forceY=function(e){return arguments.length?(f=e,E):f},E.color=function(t){return arguments.length?(l=e.utils.getColor(t),E):l},E.id=function(e){return arguments.length?(i=e,E):i},E.showValues=function(e){return arguments.length?(c=e,E):c},E.valueFormat=function(e){return arguments.length?(h=e,E):h},E.rectClass=function(e){return arguments.length?(y=e,E):y},E},e.models.discreteBarChart=function(){"use strict";function w(e){return e.each(function(e){var u=d3.select(this),p=this,E=(s||parseInt(u.style("width"))||960)-i.left-i.right,S=(o||parseInt(u.style("height"))||400)-i.top-i.bottom;w.update=function(){g.beforeUpdate(),u.transition().duration(y).call(w)},w.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var T=u.selectAll(".nv-noData").data([m]);return T.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),T.attr("x",i.left+E/2).attr("y",i.top+S/2).text(function(e){return e}),w}u.selectAll(".nv-noData").remove(),d=t.xScale(),v=t.yScale().clamp(!0);var N=u.selectAll("g.nv-wrap.nv-discreteBarWithAxes").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discreteBarWithAxes").append("g"),k=C.append("defs"),L=N.select("g");C.append("g").attr("class","nv-x nv-axis"),C.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),C.append("g").attr("class","nv-barsWrap"),L.attr("transform","translate("+i.left+","+i.top+")"),l&&L.select(".nv-y.nv-axis").attr("transform","translate("+E+",0)"),t.width(E).height(S);var A=L.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));A.transition().call(t),k.append("clipPath").attr("id","nv-x-label-clip-"+t.id()).append("rect"),L.select("#nv-x-label-clip-"+t.id()+" rect").attr("width",d.rangeBand()*(c?2:1)).attr("height",16).attr("x",-d.rangeBand()/(c?1:2));if(a){n.scale(d).ticks(E/100).tickSize(-S,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+(v.range()[0]+(t.showValues()&&v.domain()[0]<0?16:0))+")"),L.select(".nv-x.nv-axis").transition().call(n);var O=L.select(".nv-x.nv-axis").selectAll("g");c&&O.selectAll("text").attr("transform",function(e,t,n){return"translate(0,"+(n%2==0?"5":"17")+")"})}f&&(r.scale(v).ticks(S/36).tickSize(-E,0),L.select(".nv-y.nv-axis").transition().call(r)),L.select(".nv-zeroLine line").attr("x1",0).attr("x2",E).attr("y1",v(0)).attr("y2",v(0)),g.on("tooltipShow",function(e){h&&b(e,p.parentNode)})}),w}var t=e.models.discreteBar(),n=e.models.axis(),r=e.models.axis(),i={top:15,right:10,bottom:50,left:60},s=null,o=null,u=e.utils.getColor(),a=!0,f=!0,l=!1,c=!1,h=!0,p=function(e,t,n,r,i){return"

    "+t+"

    "+"

    "+n+"

    "},d,v,m="No Data Available.",g=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate"),y=250;n.orient("bottom").highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(l?"right":"left").tickFormat(d3.format(",.1f"));var b=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=p(i.series.key,a,f,i,w);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+i.left,e.pos[1]+i.top],g.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){g.tooltipHide(e)}),g.on("tooltipHide",function(){h&&e.tooltip.cleanup()}),w.dispatch=g,w.discretebar=t,w.xAxis=n,w.yAxis=r,d3.rebind(w,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","id","showValues","valueFormat"),w.options=e.utils.optionsFunc.bind(w),w.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,w):i},w.width=function(e){return arguments.length?(s=e,w):s},w.height=function(e){return arguments.length?(o=e,w):o},w.color=function(n){return arguments.length?(u=e.utils.getColor(n),t.color(u),w):u},w.showXAxis=function(e){return arguments.length?(a=e,w):a},w.showYAxis=function(e){return arguments.length?(f=e,w):f},w.rightAlignYAxis=function(e){return arguments.length?(l=e,r.orient(e?"right":"left"),w):l},w.staggerLabels=function(e){return arguments.length?(c=e,w):c},w.tooltips=function(e){return arguments.length?(h=e,w):h},w.tooltipContent=function(e){return arguments.length?(p=e,w):p},w.noData=function(e){return arguments.length?(m=e,w):m},w.transitionDuration=function(e){return arguments.length?(y=e,w):y},w},e.models.distribution=function(){"use strict";function l(e){return e.each(function(e){var a=n-(i==="x"?t.left+t.right:t.top+t.bottom),l=i=="x"?"y":"x",c=d3.select(this);f=f||u;var h=c.selectAll("g.nv-distribution").data([e]),p=h.enter().append("g").attr("class","nvd3 nv-distribution"),d=p.append("g"),v=h.select("g");h.attr("transform","translate("+t.left+","+t.top+")");var m=v.selectAll("g.nv-dist").data(function(e){return e},function(e){return e.key});m.enter().append("g"),m.attr("class",function(e,t){return"nv-dist nv-series-"+t}).style("stroke",function(e,t){return o(e,t)});var g=m.selectAll("line.nv-dist"+i).data(function(e){return e.values});g.enter().append("line").attr(i+"1",function(e,t){return f(s(e,t))}).attr(i+"2",function(e,t){return f(s(e,t))}),m.exit().selectAll("line.nv-dist"+i).transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}).style("stroke-opacity",0).remove(),g.attr("class",function(e,t){return"nv-dist"+i+" nv-dist"+i+"-"+t}).attr(l+"1",0).attr(l+"2",r),g.transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}),f=u.copy()}),l}var t={top:0,right:0,bottom:0,left:0},n=400,r=8,i="x",s=function(e){return e[i]},o=e.utils.defaultColor(),u=d3.scale.linear(),a,f;return l.options=e.utils.optionsFunc.bind(l),l.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,l):t},l.width=function(e){return arguments.length?(n=e,l):n},l.axis=function(e){return arguments.length?(i=e,l):i},l.size=function(e){return arguments.length?(r=e,l):r},l.getData=function(e){return arguments.length?(s=d3.functor(e),l):s},l.scale=function(e){return arguments.length?(u=e,l):u},l.color=function(t){return arguments.length?(o=e.utils.getColor(t),l):o},l},e.models.historicalBar=function(){"use strict";function w(E){return E.each(function(w){var E=n-t.left-t.right,S=r-t.top-t.bottom,T=d3.select(this);s.domain(d||d3.extent(w[0].values.map(u).concat(f))),c?s.range(m||[E*.5/w[0].values.length,E*(w[0].values.length-.5)/w[0].values.length]):s.range(m||[0,E]),o.domain(v||d3.extent(w[0].values.map(a).concat(l))).range(g||[S,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=T.selectAll("g.nv-wrap.nv-historicalBar-"+i).data([w[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBar-"+i),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-bars"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){y.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",E).attr("height",S),A.attr("clip-path",h?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-bars").selectAll(".nv-bar").data(function(e){return e},function(e,t){return u(e,t)});O.exit().remove();var M=O.enter().append("rect").attr("x",0).attr("y",function(t,n){return e.utils.NaNtoZero(o(Math.max(0,a(t,n))))}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.abs(o(a(t,n))-o(0)))}).attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).on("mouseover",function(e,t){if(!b)return;d3.select(this).classed("hover",!0),y.elementMouseover({point:e,series:w[0],pos:[s(u(e,t)),o(a(e,t))],pointIndex:t,seriesIndex:0,e:d3.event})}).on("mouseout",function(e,t){if(!b)return;d3.select(this).classed("hover",!1),y.elementMouseout({point:e,series:w[0],pointIndex:t,seriesIndex:0,e:d3.event})}).on("click",function(e,t){if(!b)return;y.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){if(!b)return;y.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("fill",function(e,t){return p(e,t)}).attr("class",function(e,t,n){return(a(e,t)<0?"nv-bar negative":"nv-bar positive")+" nv-bar-"+n+"-"+t}).transition().attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).attr("width",E/w[0].values.length*.9),O.transition().attr("y",function(t,n){var r=a(t,n)<0?o(0):o(0)-o(a(t,n))<1?o(0)-1:o(a(t,n));return e.utils.NaNtoZero(r)}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.max(Math.abs(o(a(t,n))-o(0)),1))})}),w}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[],l=[0],c=!1,h=!0,p=e.utils.defaultColor(),d,v,m,g,y=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),b=!0;return w.highlightPoint=function(e,t){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar-0-"+e).classed("hover",t)},w.clearHighlights=function(){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar.hover").classed("hover",!1)},w.dispatch=y,w.options=e.utils.optionsFunc.bind(w),w.x=function(e){return arguments.length?(u=e,w):u},w.y=function(e){return arguments.length?(a=e,w):a},w.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,w):t},w.width=function(e){return arguments.length?(n=e,w):n},w.height=function(e){return arguments.length?(r=e,w):r},w.xScale=function(e){return arguments.length?(s=e,w):s},w.yScale=function(e){return arguments.length?(o=e,w):o},w.xDomain=function(e){return arguments.length?(d=e,w):d},w.yDomain=function(e){return arguments.length?(v=e,w):v},w.xRange=function(e){return arguments.length?(m=e,w):m},w.yRange=function(e){return arguments.length?(g=e,w):g},w.forceX=function(e){return arguments.length?(f=e,w):f},w.forceY=function(e){return arguments.length?(l=e,w):l},w.padData=function(e){return arguments.length?(c=e,w):c},w.clipEdge=function(e){return arguments.length?(h=e,w):h},w.color=function(t){return arguments.length?(p=e.utils.getColor(t),w):p},w.id=function(e){return arguments.length?(i=e,w):i},w.interactive=function(e){return arguments.length?(b=!1,w):b},w},e.models.historicalBarChart=function(){"use strict";function x(e){return e.each(function(d){var T=d3.select(this),N=this,C=(u||parseInt(T.style("width"))||960)-s.left-s.right,k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom;x.update=function(){T.transition().duration(E).call(x)},x.container=this,g.disabled=d.map(function(e){return!!e.disabled});if(!y){var L;y={};for(L in g)g[L]instanceof Array?y[L]=g[L].slice(0):y[L]=g[L]}if(!d||!d.length||!d.filter(function(e){return e.values.length}).length){var A=T.selectAll(".nv-noData").data([b]);return A.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),A.attr("x",s.left+C/2).attr("y",s.top+k/2).text(function(e){return e}),x}T.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale();var O=T.selectAll("g.nv-wrap.nv-historicalBarChart").data([d]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBarChart").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-legendWrap"),f&&(i.width(C),_.select(".nv-legendWrap").datum(d).call(i),s.top!=i.height()&&(s.top=i.height(),k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom),O.select(".nv-legendWrap").attr("transform","translate(0,"+ -s.top+")")),O.attr("transform","translate("+s.left+","+s.top+")"),h&&_.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),t.width(C).height(k).color(d.map(function(e,t){return e.color||o(e,t)}).filter(function(e,t){return!d[t].disabled}));var D=_.select(".nv-barsWrap").datum(d.filter(function(e){return!e.disabled}));D.transition().call(t),l&&(n.scale(v).tickSize(-k,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+m.range()[0]+")"),_.select(".nv-x.nv-axis").transition().call(n)),c&&(r.scale(m).ticks(k/36).tickSize(-C,0),_.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("legendClick",function(t,n){t.disabled=!t.disabled,d.filter(function(e){return!e.disabled}).length||d.map(function(e){return e.disabled=!1,O.selectAll(".nv-series").classed("disabled",!1),e}),g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),e.transition().call(x)}),i.dispatch.on("legendDblclick",function(e){d.forEach(function(e){e.disabled=!0}),e.disabled=!1,g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),x.update()}),w.on("tooltipShow",function(e){p&&S(e,N.parentNode)}),w.on("changeState",function(e){typeof e.disabled!="undefined"&&(d.forEach(function(t,n){t.disabled=e.disabled[n]}),g.disabled=e.disabled),x.update()})}),x}var t=e.models.historicalBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s={top:30,right:90,bottom:50,left:90},o=e.utils.defaultColor(),u=null,a=null,f=!1,l=!0,c=!0,h=!1,p=!0,d=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},v,m,g={},y=null,b="No Data Available.",w=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),E=250;n.orient("bottom").tickPadding(7),r.orient(h?"right":"left");var S=function(i,s){if(s){var o=d3.select(s).select("svg"),u=o.node()?o.attr("viewBox"):null;if(u){u=u.split(" ");var a=parseInt(o.style("width"))/u[2];i.pos[0]=i.pos[0]*a,i.pos[1]=i.pos[1]*a}}var f=i.pos[0]+(s.offsetLeft||0),l=i.pos[1]+(s.offsetTop||0),c=n.tickFormat()(t.x()(i.point,i.pointIndex)),h=r.tickFormat()(t.y()(i.point,i.pointIndex)),p=d(i.series.key,c,h,i,x);e.tooltip.show([f,l],p,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+s.left,e.pos[1]+s.top],w.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){w.tooltipHide(e)}),w.on("tooltipHide",function(){p&&e.tooltip.cleanup()}),x.dispatch=w,x.bars=t,x.legend=i,x.xAxis=n,x.yAxis=r,d3.rebind(x,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id","interpolate","highlightPoint","clearHighlights","interactive"),x.options=e.utils.optionsFunc.bind(x),x.margin=function(e){return arguments.length?(s.top=typeof e.top!="undefined"?e.top:s.top,s.right=typeof e.right!="undefined"?e.right:s.right,s.bottom=typeof e.bottom!="undefined"?e.bottom:s.bottom,s.left=typeof e.left!="undefined"?e.left:s.left,x):s},x.width=function(e){return arguments.length?(u=e,x):u},x.height=function(e){return arguments.length?(a=e,x):a},x.color=function(t){return arguments.length?(o=e.utils.getColor(t),i.color(o),x):o},x.showLegend=function(e){return arguments.length?(f=e,x):f},x.showXAxis=function(e){return arguments.length?(l=e,x):l},x.showYAxis=function(e){return arguments.length?(c=e,x):c},x.rightAlignYAxis=function(e){return arguments.length?(h=e,r.orient(e?"right":"left"),x):h},x.tooltips=function(e){return arguments.length?(p=e,x):p},x.tooltipContent=function(e){return arguments.length?(d=e,x):d},x.state=function(e){return arguments.length?(g=e,x):g},x.defaultState=function(e){return arguments.length?(y=e,x):y},x.noData=function(e){return arguments.length?(b=e,x):b},x.transitionDuration=function(e){return arguments.length?(E=e,x):E},x},e.models.indentedTree=function(){"use strict";function g(e){return e.each(function(e){function k(e,t,n){d3.event.stopPropagation();if(d3.event.shiftKey&&!n)return d3.event.shiftKey=!1,e.values&&e.values.forEach(function(e){(e.values||e._values)&&k(e,0,!0)}),!0;if(!O(e))return!0;e.values?(e._values=e.values,e.values=null):(e.values=e._values,e._values=null),g.update()}function L(e){return e._values&&e._values.length?h:e.values&&e.values.length?p:""}function A(e){return e._values&&e._values.length}function O(e){var t=e.values||e._values;return t&&t.length}var t=1,n=d3.select(this),i=d3.layout.tree().children(function(e){return e.values}).size([r,f]);g.update=function(){n.transition().duration(600).call(g)},e[0]||(e[0]={key:a});var s=i.nodes(e[0]),y=d3.select(this).selectAll("div").data([[s]]),b=y.enter().append("div").attr("class","nvd3 nv-wrap nv-indentedtree"),w=b.append("table"),E=y.select("table").attr("width","100%").attr("class",c);if(o){var S=w.append("thead"),x=S.append("tr");l.forEach(function(e){x.append("th").attr("width",e.width?e.width:"10%").style("text-align",e.type=="numeric"?"right":"left").append("span").text(e.label)})}var T=E.selectAll("tbody").data(function(e){return e});T.enter().append("tbody"),t=d3.max(s,function(e){return e.depth}),i.size([r,t*f]);var N=T.selectAll("tr").data(function(e){return e.filter(function(e){return u&&!e.children?u(e):!0})},function(e,t){return e.id||e.id||++m});N.exit().remove(),N.select("img.nv-treeicon").attr("src",L).classed("folded",A);var C=N.enter().append("tr");l.forEach(function(e,t){var n=C.append("td").style("padding-left",function(e){return(t?0:e.depth*f+12+(L(e)?0:16))+"px"},"important").style("text-align",e.type=="numeric"?"right":"left");t==0&&n.append("img").classed("nv-treeicon",!0).classed("nv-folded",A).attr("src",L).style("width","14px").style("height","14px").style("padding","0 1px").style("display",function(e){return L(e)?"inline-block":"none"}).on("click",k),n.each(function(n){!t&&v(n)?d3.select(this).append("a").attr("href",v).attr("class",d3.functor(e.classes)).append("span"):d3.select(this).append("span"),d3.select(this).select("span").attr("class",d3.functor(e.classes)).text(function(t){return e.format?e.format(t):t[e.key]||"-"})}),e.showCount&&(n.append("span").attr("class","nv-childrenCount"),N.selectAll("span.nv-childrenCount").text(function(e){return e.values&&e.values.length||e._values&&e._values.length?"("+(e.values&&e.values.filter(function(e){return u?u(e):!0}).length||e._values&&e._values.filter(function(e){return u?u(e):!0}).length||0)+")":""}))}),N.order().on("click",function(e){d.elementClick({row:this,data:e,pos:[e.x,e.y]})}).on("dblclick",function(e){d.elementDblclick({row:this,data:e,pos:[e.x,e.y]})}).on("mouseover",function(e){d.elementMouseover({row:this,data:e,pos:[e.x,e.y]})}).on("mouseout",function(e){d.elementMouseout({row:this,data:e,pos:[e.x,e.y]})})}),g}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e4),o=!0,u=!1,a="No Data Available.",f=20,l=[{key:"key",label:"Name",type:"text"}],c=null,h="images/grey-plus.png",p="images/grey-minus.png",d=d3.dispatch("elementClick","elementDblclick","elementMouseover","elementMouseout"),v=function(e){return e.url},m=0;return g.options=e.utils.optionsFunc.bind(g),g.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,g):t},g.width=function(e){return arguments.length?(n=e,g):n},g.height=function(e){return arguments.length?(r=e,g):r},g.color=function(t){return arguments.length?(i=e.utils.getColor(t),scatter.color(i),g):i},g.id=function(e){return arguments.length?(s=e,g):s},g.header=function(e){return arguments.length?(o=e,g):o},g.noData=function(e){return arguments.length?(a=e,g):a},g.filterZero=function(e){return arguments.length?(u=e,g):u},g.columns=function(e){return arguments.length?(l=e,g):l},g.tableClass=function(e){return arguments.length?(c=e,g):c},g.iconOpen=function(e){return arguments.length?(h=e,g):h},g.iconClose=function(e){return arguments.length?(p=e,g):p},g.getUrl=function(e){return arguments.length?(v=e,g):v},g},e.models.legend=function(){"use strict";function c(h){return h.each(function(c){var h=n-t.left-t.right,p=d3.select(this),d=p.selectAll("g.nv-legend").data([c]),v=d.enter().append("g").attr("class","nvd3 nv-legend").append("g"),m=d.select("g");d.attr("transform","translate("+t.left+","+t.top+")");var g=m.selectAll(".nv-series").data(function(e){return e}),y=g.enter().append("g").attr("class","nv-series").on("mouseover",function(e,t){l.legendMouseover(e,t)}).on("mouseout",function(e,t){l.legendMouseout(e,t)}).on("click",function(e,t){l.legendClick(e,t),a&&(f?(c.forEach(function(e){e.disabled=!0}),e.disabled=!1):(e.disabled=!e.disabled,c.every(function(e){return e.disabled})&&c.forEach(function(e){e.disabled=!1})),l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))}).on("dblclick",function(e,t){l.legendDblclick(e,t),a&&(c.forEach(function(e){e.disabled=!0}),e.disabled=!1,l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))});y.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),y.append("text").attr("text-anchor","start").attr("class","nv-legend-text").attr("dy",".32em").attr("dx","8"),g.classed("disabled",function(e){return e.disabled}),g.exit().remove(),g.select("circle").style("fill",function(e,t){return e.color||s(e,t)}).style("stroke",function(e,t){return e.color||s(e,t)}),g.select("text").text(i);if(o){var b=[];g.each(function(t,n){var r=d3.select(this).select("text"),i;try{i=r.getComputedTextLength();if(i<=0)throw Error()}catch(s){i=e.utils.calcApproxTextWidth(r)}b.push(i+28)});var w=0,E=0,S=[];while(Eh&&w>1){S=[],w--;for(var x=0;x(S[x%w]||0)&&(S[x%w]=b[x]);E=S.reduce(function(e,t,n,r){return e+t})}var T=[];for(var N=0,C=0;NA&&(A=L),"translate("+O+","+k+")"}),m.attr("transform","translate("+(n-t.right-A)+","+t.top+")"),r=t.top+t.bottom+k+15}}),c}var t={top:5,right:0,bottom:5,left:0},n=400,r=20,i=function(e){return e.key},s=e.utils.defaultColor(),o=!0,u=!0,a=!0,f=!1,l=d3.dispatch("legendClick","legendDblclick","legendMouseover","legendMouseout","stateChange");return c.dispatch=l,c.options=e.utils.optionsFunc.bind(c),c.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,c):t},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.key=function(e){return arguments.length?(i=e,c):i},c.color=function(t){return arguments.length?(s=e.utils.getColor(t),c):s},c.align=function(e){return arguments.length?(o=e,c):o},c.rightAlign=function(e){return arguments.length?(u=e,c):u},c.updateState=function(e){return arguments.length?(a=e,c):a},c.radioButtonMode=function(e){return arguments.length?(f=e,c):f},c},e.models.line=function(){"use strict";function m(g){return g.each(function(m){var g=r-n.left-n.right,b=i-n.top-n.bottom,w=d3.select(this);c=t.xScale(),h=t.yScale(),d=d||c,v=v||h;var E=w.selectAll("g.nv-wrap.nv-line").data([m]),S=E.enter().append("g").attr("class","nvd3 nv-wrap nv-line"),T=S.append("defs"),N=S.append("g"),C=E.select("g");N.append("g").attr("class","nv-groups"),N.append("g").attr("class","nv-scatterWrap"),E.attr("transform","translate("+n.left+","+n.top+")"),t.width(g).height(b);var k=E.select(".nv-scatterWrap");k.transition().call(t),T.append("clipPath").attr("id","nv-edge-clip-"+t.id()).append("rect"),E.select("#nv-edge-clip-"+t.id()+" rect").attr("width",g).attr("height",b),C.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":""),k.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":"");var L=E.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});L.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),L.exit().remove(),L.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return s(e,t)}).style("stroke",function(e,t){return s(e,t)}),L.transition().style("stroke-opacity",1).style("fill-opacity",.5);var A=L.selectAll("path.nv-area").data(function(e){return f(e)?[e]:[]});A.enter().append("path").attr("class","nv-area").attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e. +utils.NaNtoZero(d(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))}).y1(function(e,t){return v(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])}),L.exit().selectAll("path.nv-area").remove(),A.transition().attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))}).y1(function(e,t){return h(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])});var O=L.selectAll("path.nv-line").data(function(e){return[e.values]});O.enter().append("path").attr("class","nv-line").attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(d(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))})),O.transition().attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))})),d=c.copy(),v=h.copy()}),m}var t=e.models.scatter(),n={top:0,right:0,bottom:0,left:0},r=960,i=500,s=e.utils.defaultColor(),o=function(e){return e.x},u=function(e){return e.y},a=function(e,t){return!isNaN(u(e,t))&&u(e,t)!==null},f=function(e){return e.area},l=!1,c,h,p="linear";t.size(16).sizeDomain([16,256]);var d,v;return m.dispatch=t.dispatch,m.scatter=t,d3.rebind(m,t,"id","interactive","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","forceX","forceY","forceSize","clipVoronoi","useVoronoi","clipRadius","padData","highlightPoint","clearHighlights"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.height=function(e){return arguments.length?(i=e,m):i},m.x=function(e){return arguments.length?(o=e,t.x(e),m):o},m.y=function(e){return arguments.length?(u=e,t.y(e),m):u},m.clipEdge=function(e){return arguments.length?(l=e,m):l},m.color=function(n){return arguments.length?(s=e.utils.getColor(n),t.color(s),m):s},m.interpolate=function(e){return arguments.length?(p=e,m):p},m.defined=function(e){return arguments.length?(a=e,m):a},m.isArea=function(e){return arguments.length?(f=d3.functor(e),m):f},m},e.models.lineChart=function(){"use strict";function N(m){return m.each(function(m){var C=d3.select(this),k=this,L=(a||parseInt(C.style("width"))||960)-o.left-o.right,A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom;N.update=function(){C.transition().duration(x).call(N)},N.container=this,b.disabled=m.map(function(e){return!!e.disabled});if(!w){var O;w={};for(O in b)b[O]instanceof Array?w[O]=b[O].slice(0):w[O]=b[O]}if(!m||!m.length||!m.filter(function(e){return e.values.length}).length){var M=C.selectAll(".nv-noData").data([E]);return M.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),M.attr("x",o.left+L/2).attr("y",o.top+A/2).text(function(e){return e}),N}C.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var _=C.selectAll("g.nv-wrap.nv-lineChart").data([m]),D=_.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),P=_.select("g");D.append("rect").style("opacity",0),D.append("g").attr("class","nv-x nv-axis"),D.append("g").attr("class","nv-y nv-axis"),D.append("g").attr("class","nv-linesWrap"),D.append("g").attr("class","nv-legendWrap"),D.append("g").attr("class","nv-interactive"),P.select("rect").attr("width",L).attr("height",A>0?A:0),l&&(i.width(L),P.select(".nv-legendWrap").datum(m).call(i),o.top!=i.height()&&(o.top=i.height(),A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom),_.select(".nv-legendWrap").attr("transform","translate(0,"+ -o.top+")")),_.attr("transform","translate("+o.left+","+o.top+")"),p&&P.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),d&&(s.width(L).height(A).margin({left:o.left,top:o.top}).svgContainer(C).xScale(g),_.select(".nv-interactive").call(s)),t.width(L).height(A).color(m.map(function(e,t){return e.color||u(e,t)}).filter(function(e,t){return!m[t].disabled}));var H=P.select(".nv-linesWrap").datum(m.filter(function(e){return!e.disabled}));H.transition().call(t),c&&(n.scale(g).ticks(L/100).tickSize(-A,0),P.select(".nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),P.select(".nv-x.nv-axis").transition().call(n)),h&&(r.scale(y).ticks(A/36).tickSize(-L,0),P.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),N.update()}),s.dispatch.on("elementMousemove",function(i){t.clearHighlights();var a,f,l,c=[];m.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,N.x()),t.highlightPoint(r,f,!0);var s=n.values[f];if(typeof s=="undefined")return;typeof a=="undefined"&&(a=s),typeof l=="undefined"&&(l=N.xScale()(N.x()(s,f))),c.push({key:n.key,value:N.y()(s,f),color:u(n,n.seriesIndex)})});if(c.length>2){var h=N.yScale().invert(i.mouseY),p=Math.abs(N.yScale().domain()[0]-N.yScale().domain()[1]),d=.03*p,g=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);g!==null&&(c[g].highlight=!0)}var y=n.tickFormat()(N.x()(a,f));s.tooltip.position({left:l+o.left,top:i.mouseY+o.top}).chartContainer(k.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:y,series:c})(),s.renderGuideLine(l)}),s.dispatch.on("elementMouseout",function(e){S.tooltipHide(),t.clearHighlights()}),S.on("tooltipShow",function(e){v&&T(e,k.parentNode)}),S.on("changeState",function(e){typeof e.disabled!="undefined"&&m.length===e.disabled.length&&(m.forEach(function(t,n){t.disabled=e.disabled[n]}),b.disabled=e.disabled),N.update()})}),N}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.interactiveGuideline(),o={top:30,right:20,bottom:50,left:60},u=e.utils.defaultColor(),a=null,f=null,l=!0,c=!0,h=!0,p=!1,d=!1,v=!0,m=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=250;n.orient("bottom").tickPadding(7),r.orient(p?"right":"left");var T=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,N);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),N.dispatch=S,N.lines=t,N.legend=i,N.xAxis=n,N.yAxis=r,N.interactiveLayer=s,d3.rebind(N,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id","interpolate"),N.options=e.utils.optionsFunc.bind(N),N.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,N):o},N.width=function(e){return arguments.length?(a=e,N):a},N.height=function(e){return arguments.length?(f=e,N):f},N.color=function(t){return arguments.length?(u=e.utils.getColor(t),i.color(u),N):u},N.showLegend=function(e){return arguments.length?(l=e,N):l},N.showXAxis=function(e){return arguments.length?(c=e,N):c},N.showYAxis=function(e){return arguments.length?(h=e,N):h},N.rightAlignYAxis=function(e){return arguments.length?(p=e,r.orient(e?"right":"left"),N):p},N.useInteractiveGuideline=function(e){return arguments.length?(d=e,e===!0&&(N.interactive(!1),N.useVoronoi(!1)),N):d},N.tooltips=function(e){return arguments.length?(v=e,N):v},N.tooltipContent=function(e){return arguments.length?(m=e,N):m},N.state=function(e){return arguments.length?(b=e,N):b},N.defaultState=function(e){return arguments.length?(w=e,N):w},N.noData=function(e){return arguments.length?(E=e,N):E},N.transitionDuration=function(e){return arguments.length?(x=e,N):x},N},e.models.linePlusBarChart=function(){"use strict";function T(e){return e.each(function(e){var l=d3.select(this),c=this,v=(a||parseInt(l.style("width"))||960)-u.left-u.right,N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom;T.update=function(){l.transition().call(T)},b.disabled=e.map(function(e){return!!e.disabled});if(!w){var C;w={};for(C in b)b[C]instanceof Array?w[C]=b[C].slice(0):w[C]=b[C]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var k=l.selectAll(".nv-noData").data([E]);return k.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),k.attr("x",u.left+v/2).attr("y",u.top+N/2).text(function(e){return e}),T}l.selectAll(".nv-noData").remove();var L=e.filter(function(e){return!e.disabled&&e.bar}),A=e.filter(function(e){return!e.bar});m=A.filter(function(e){return!e.disabled}).length&&A.filter(function(e){return!e.disabled})[0].values.length?t.xScale():n.xScale(),g=n.yScale(),y=t.yScale();var O=d3.select(this).selectAll("g.nv-wrap.nv-linePlusBar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y1 nv-axis"),M.append("g").attr("class","nv-y2 nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-linesWrap"),M.append("g").attr("class","nv-legendWrap"),p&&(o.width(v/2),_.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(o),u.top!=o.height()&&(u.top=o.height(),N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom),_.select(".nv-legendWrap").attr("transform","translate("+v/2+","+ -u.top+")")),O.attr("transform","translate("+u.left+","+u.top+")"),t.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar})),n.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar}));var D=_.select(".nv-barsWrap").datum(L.length?L:[{values:[]}]),P=_.select(".nv-linesWrap").datum(A[0]&&!A[0].disabled?A:[{values:[]}]);d3.transition(D).call(n),d3.transition(P).call(t),r.scale(m).ticks(v/100).tickSize(-N,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+g.range()[0]+")"),d3.transition(_.select(".nv-x.nv-axis")).call(r),i.scale(g).ticks(N/36).tickSize(-v,0),d3.transition(_.select(".nv-y1.nv-axis")).style("opacity",L.length?1:0).call(i),s.scale(y).ticks(N/36).tickSize(L.length?0:-v,0),_.select(".nv-y2.nv-axis").style("opacity",A.length?1:0).attr("transform","translate("+v+",0)"),d3.transition(_.select(".nv-y2.nv-axis")).call(s),o.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),T.update()}),S.on("tooltipShow",function(e){d&&x(e,c.parentNode)}),S.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),b.disabled=t.disabled),T.update()})}),T}var t=e.models.line(),n=e.models.historicalBar(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.legend(),u={top:30,right:60,bottom:50,left:60},a=null,f=null,l=function(e){return e.x},c=function(e){return e.y},h=e.utils.defaultColor(),p=!0,d=!0,v=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},m,g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState");n.padData(!0),t.clipEdge(!1).padData(!0),r.orient("bottom").tickPadding(7).highlightZero(!1),i.orient("left"),s.orient("right");var x=function(n,o){var u=n.pos[0]+(o.offsetLeft||0),a=n.pos[1]+(o.offsetTop||0),f=r.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?i:s).tickFormat()(t.y()(n.point,n.pointIndex)),c=v(n.series.key,f,l,n,T);e.tooltip.show([u,a],c,n.value<0?"n":"s",null,o)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),n.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),n.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){d&&e.tooltip.cleanup()}),T.dispatch=S,T.legend=o,T.lines=t,T.bars=n,T.xAxis=r,T.y1Axis=i,T.y2Axis=s,d3.rebind(T,t,"defined","size","clipVoronoi","interpolate"),T.options=e.utils.optionsFunc.bind(T),T.x=function(e){return arguments.length?(l=e,t.x(e),n.x(e),T):l},T.y=function(e){return arguments.length?(c=e,t.y(e),n.y(e),T):c},T.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,T):u},T.width=function(e){return arguments.length?(a=e,T):a},T.height=function(e){return arguments.length?(f=e,T):f},T.color=function(t){return arguments.length?(h=e.utils.getColor(t),o.color(h),T):h},T.showLegend=function(e){return arguments.length?(p=e,T):p},T.tooltips=function(e){return arguments.length?(d=e,T):d},T.tooltipContent=function(e){return arguments.length?(v=e,T):v},T.state=function(e){return arguments.length?(b=e,T):b},T.defaultState=function(e){return arguments.length?(w=e,T):w},T.noData=function(e){return arguments.length?(E=e,T):E},T},e.models.lineWithFocusChart=function(){"use strict";function k(e){return e.each(function(e){function U(e){var t=+(e=="e"),n=t?1:-1,r=M/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function z(){a.empty()||a.extent(w),I.data([a.empty()?g.domain():w]).each(function(e,t){var n=g(e[0])-v.range()[0],r=v.range()[1]-g(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",g(e[1])).attr("width",r<0?0:r)})}function W(){w=a.empty()?null:a.extent();var n=a.empty()?g.domain():a.extent();if(Math.abs(n[0]-n[1])<=1)return;T.brush({extent:n,brush:a}),z();var s=H.select(".nv-focus .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}).map(function(e,r){return{key:e.key,values:e.values.filter(function(e,r){return t.x()(e,r)>=n[0]&&t.x()(e,r)<=n[1]})}}));s.transition().duration(N).call(t),H.select(".nv-focus .nv-x.nv-axis").transition().duration(N).call(r),H.select(".nv-focus .nv-y.nv-axis").transition().duration(N).call(i)}var S=d3.select(this),L=this,A=(h||parseInt(S.style("width"))||960)-f.left-f.right,O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d,M=d-l.top-l.bottom;k.update=function(){S.transition().duration(N).call(k)},k.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var _=S.selectAll(".nv-noData").data([x]);return _.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),_.attr("x",f.left+A/2).attr("y",f.top+O/2).text(function(e){return e}),k}S.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale(),g=n.xScale(),y=n.yScale();var D=S.selectAll("g.nv-wrap.nv-lineWithFocusChart").data([e]),P=D.enter().append("g").attr("class","nvd3 nv-wrap nv-lineWithFocusChart").append("g"),H=D.select("g");P.append("g").attr("class","nv-legendWrap");var B=P.append("g").attr("class","nv-focus");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-linesWrap");var j=P.append("g").attr("class","nv-context");j.append("g").attr("class","nv-x nv-axis"),j.append("g").attr("class","nv-y nv-axis"),j.append("g").attr("class","nv-linesWrap"),j.append("g").attr("class","nv-brushBackground"),j.append("g").attr("class","nv-x nv-brush"),b&&(u.width(A),H.select(".nv-legendWrap").datum(e).call(u),f.top!=u.height()&&(f.top=u.height(),O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d),H.select(".nv-legendWrap").attr("transform","translate(0,"+ -f.top+")")),D.attr("transform","translate("+f.left+","+f.top+")"),t.width(A).height(O).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),n.defined(t.defined()).width(A).height(M).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),H.select(".nv-context").attr("transform","translate(0,"+(O+f.bottom+l.top)+")");var F=H.select(".nv-context .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}));d3.transition(F).call(n),r.scale(v).ticks(A/100).tickSize(-O,0),i.scale(m).ticks(O/36).tickSize(-A,0),H.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+O+")"),a.x(g).on("brush",function(){var e=k.transitionDuration();k.transitionDuration(0),W(),k.transitionDuration(e)}),w&&a.extent(w);var I=H.select(".nv-brushBackground").selectAll("g").data([w||a.extent()]),q=I.enter().append("g");q.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",M),q.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",M);var R=H.select(".nv-x.nv-brush").call(a);R.selectAll("rect").attr("height",M),R.selectAll(".resize").append("path").attr("d",U),W(),s.scale(g).ticks(A/100).tickSize(-M,0),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),d3.transition(H.select(".nv-context .nv-x.nv-axis")).call(s),o.scale(y).ticks(M/36).tickSize(-A,0),d3.transition(H.select(".nv-context .nv-y.nv-axis")).call(o),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),u.dispatch.on("stateChange",function(e){k.update()}),T.on("tooltipShow",function(e){E&&C(e,L.parentNode)})}),k}var t=e.models.line(),n=e.models.line(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.axis(),u=e.models.legend(),a=d3.svg.brush(),f={top:30,right:30,bottom:30,left:60},l={top:0,right:30,bottom:20,left:60},c=e.utils.defaultColor(),h=null,p=null,d=100,v,m,g,y,b=!0,w=null,E=!0,S=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},x="No Data Available.",T=d3.dispatch("tooltipShow","tooltipHide","brush"),N=250;t.clipEdge(!0),n.interactive(!1),r.orient("bottom").tickPadding(5),i.orient("left"),s.orient("bottom").tickPadding(5),o.orient("left");var C=function(n,s){var o=n.pos[0]+(s.offsetLeft||0),u=n.pos[1]+(s.offsetTop||0),a=r.tickFormat()(t.x()(n.point,n.pointIndex)),f=i.tickFormat()(t.y()(n.point,n.pointIndex)),l=S(n.series.key,a,f,n,k);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+f.left,e.pos[1]+f.top],T.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){E&&e.tooltip.cleanup()}),k.dispatch=T,k.legend=u,k.lines=t,k.lines2=n,k.xAxis=r,k.yAxis=i,k.x2Axis=s,k.y2Axis=o,d3.rebind(k,t,"defined","isArea","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id"),k.options=e.utils.optionsFunc.bind(k),k.x=function(e){return arguments.length?(t.x(e),n.x(e),k):t.x},k.y=function(e){return arguments.length?(t.y(e),n.y(e),k):t.y},k.margin=function(e){return arguments.length?(f.top=typeof e.top!="undefined"?e.top:f.top,f.right=typeof e.right!="undefined"?e.right:f.right,f.bottom=typeof e.bottom!="undefined"?e.bottom:f.bottom,f.left=typeof e.left!="undefined"?e.left:f.left,k):f},k.margin2=function(e){return arguments.length?(l=e,k):l},k.width=function(e){return arguments.length?(h=e,k):h},k.height=function(e){return arguments.length?(p=e,k):p},k.height2=function(e){return arguments.length?(d=e,k):d},k.color=function(t){return arguments.length?(c=e.utils.getColor(t),u.color(c),k):c},k.showLegend=function(e){return arguments.length?(b=e,k):b},k.tooltips=function(e){return arguments.length?(E=e,k):E},k.tooltipContent=function(e){return arguments.length?(S=e,k):S},k.interpolate=function(e){return arguments.length?(t.interpolate(e),n.interpolate(e),k):t.interpolate()},k.noData=function(e){return arguments.length?(x=e,k):x},k.xTickFormat=function(e){return arguments.length?(r.tickFormat(e),s.tickFormat(e),k):r.tickFormat()},k.yTickFormat=function(e){return arguments.length?(i.tickFormat(e),o.tickFormat(e),k):i.tickFormat()},k.brushExtent=function(e){return arguments.length?(w=e,k):w},k.transitionDuration=function(e){return arguments.length?(N=e,k):N},k},e.models.linePlusBarWithFocusChart=function(){"use strict";function B(e){return e.each(function(e){function nt(e){var t=+(e=="e"),n=t?1:-1,r=q/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function rt(){h.empty()||h.extent(x),Z.data([h.empty()?k.domain():x]).each(function(e,t){var n=k(e[0])-k.range()[0],r=k.range()[1]-k(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",k(e[1])).attr("width",r<0?0:r)})}function it(){x=h.empty()?null:h.extent(),S=h.empty()?k.domain():h.extent(),D.brush({extent:S,brush:h}),rt(),r.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),t.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var n=J.select(".nv-focus .nv-barsWrap").datum(U.length?U.map(function(e,t){return{key:e.key,values:e.values.filter(function(e,t){return r.x()(e,t)>=S[0]&&r.x()(e,t)<=S[1]})}}):[{values:[]}]),i=J.select(".nv-focus .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z.map(function(e,n){return{key:e.key,values:e.values.filter(function(e,n){return t.x()(e,n)>=S[0]&&t.x()(e,n)<=S[1]})}}));U.length?C=r.xScale():C=t.xScale(),s.scale(C).ticks(F/100).tickSize(-I,0),s.domain([Math.ceil(S[0]),Math.floor(S[1])]),J.select(".nv-x.nv-axis").transition().duration(P).call(s),n.transition().duration(P).call(r),i.transition().duration(P).call(t),J.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+L.range()[0]+")"),u.scale(L).ticks(I/36).tickSize(-F,0),J.select(".nv-focus .nv-y1.nv-axis").style("opacity",U.length?1:0),a.scale(A).ticks(I/36).tickSize(U.length?0:-F,0),J.select(".nv-focus .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+C.range()[1]+",0)"),J.select(".nv-focus .nv-y1.nv-axis").transition().duration(P).call(u),J.select(".nv-focus .nv-y2.nv-axis").transition().duration(P).call(a)}var N=d3.select(this),j=this,F=(v||parseInt(N.style("width"))||960)-p.left-p.right,I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g,q=g-d.top-d.bottom;B.update=function(){N.transition().duration(P).call(B)},B.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=N.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",p.left+F/2).attr("y",p.top+I/2).text(function(e){return e}),B}N.selectAll(".nv-noData").remove();var U=e.filter(function(e){return!e.disabled&&e.bar}),z=e.filter(function(e){return!e.bar});C=r.xScale(),k=o.scale(),L=r.yScale(),A=t.yScale(),O=i.yScale(),M=n.yScale();var W=e.filter(function(e){return!e.disabled&&e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})}),X=e.filter(function(e){return!e.disabled&&!e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})});C.range([0,F]),k.domain(d3.extent(d3.merge(W.concat(X)),function(e){return e.x})).range([0,F]);var V=N.selectAll("g.nv-wrap.nv-linePlusBar").data([e]),$=V.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),J=V.select("g");$.append("g").attr("class","nv-legendWrap");var K=$.append("g").attr("class","nv-focus");K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y1 nv-axis"),K.append("g").attr("class","nv-y2 nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-linesWrap");var Q=$.append("g").attr("class","nv-context");Q.append("g").attr("class","nv-x nv-axis"),Q.append("g").attr("class","nv-y1 nv-axis"),Q.append("g").attr("class","nv-y2 nv-axis"),Q.append("g").attr("class","nv-barsWrap"),Q.append("g").attr("class","nv-linesWrap"),Q.append("g").attr("class","nv-brushBackground"),Q.append("g").attr("class","nv-x nv-brush"),E&&(c.width(F/2),J.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(c),p.top!=c.height()&&(p.top=c.height(),I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g),J.select(".nv-legendWrap").attr("transform","translate("+F/2+","+ -p.top+")")),V.attr("transform","translate("+p.left+","+p.top+")"),i.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),n.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var G=J.select(".nv-context .nv-barsWrap").datum(U.length?U:[{values:[]}]),Y=J.select(".nv-context .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z);J.select(".nv-context").attr("transform","translate(0,"+(I+p.bottom+d.top)+")"),G.transition().call(i),Y.transition().call(n),h.x(k).on("brush",it),x&&h.extent(x);var Z=J.select(".nv-brushBackground").selectAll("g").data([x||h.extent()]),et=Z.enter().append("g");et.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",q),et.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",q);var tt=J.select(".nv-x.nv-brush").call(h);tt.selectAll("rect").attr("height",q),tt.selectAll(".resize").append("path").attr("d",nt),o.ticks(F/100).tickSize(-q,0),J.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+O.range()[0]+")"),J.select(".nv-context .nv-x.nv-axis").transition().call(o),f.scale(O).ticks(q/36).tickSize(-F,0),J.select(".nv-context .nv-y1.nv-axis").style("opacity",U.length?1:0).attr("transform","translate(0,"+k.range()[0]+")"),J.select(".nv-context .nv-y1.nv-axis").transition().call(f),l.scale(M).ticks(q/36).tickSize(U.length?0:-F,0),J.select(".nv-context .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+k.range()[1]+",0)"),J.select(".nv-context .nv-y2.nv-axis").transition().call(l),c.dispatch.on("stateChange",function(e){B.update()}),D.on("tooltipShow",function(e){T&&H(e,j.parentNode)}),it()}),B}var t=e.models.line(),n=e.models.line(),r=e.models.historicalBar(),i=e.models.historicalBar(),s=e.models.axis(),o=e.models.axis(),u=e.models.axis(),a=e.models.axis(),f=e.models.axis(),l=e.models.axis(),c=e.models.legend(),h=d3.svg.brush(),p={top:30,right:30,bottom:30,left:60},d={top:0,right:30,bottom:20,left:60},v=null,m=null,g=100,y=function(e){return e.x},b=function(e){return e.y},w=e.utils.defaultColor(),E=!0,S,x=null,T=!0,N=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},C,k,L,A,O,M,_="No Data Available.",D=d3.dispatch("tooltipShow","tooltipHide","brush"),P=0;t.clipEdge(!0),n.interactive(!1),s.orient("bottom").tickPadding(5),u.orient("left"),a.orient("right"),o.orient("bottom").tickPadding(5),f.orient("left"),l.orient("right");var H=function(n,r){S&&(n.pointIndex+=Math.ceil(S[0]));var i=n.pos[0]+(r.offsetLeft||0),o=n.pos[1]+(r.offsetTop||0),f=s.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?u:a).tickFormat()(t.y()(n.point,n.pointIndex)),c=N(n.series.key,f,l,n,B);e.tooltip.show([i,o],c,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),r.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),r.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),D.on("tooltipHide",function(){T&&e.tooltip.cleanup()}),B.dispatch=D,B.legend=c,B.lines=t,B.lines2=n,B.bars=r,B.bars2=i,B.xAxis=s,B.x2Axis=o,B.y1Axis=u,B.y2Axis=a,B.y3Axis=f,B.y4Axis=l,d3.rebind(B,t,"defined","size","clipVoronoi","interpolate"),B.options=e.utils.optionsFunc.bind(B),B.x=function(e){return arguments.length?(y=e,t.x(e),r.x(e),B):y},B.y=function(e){return arguments.length?(b=e,t.y(e),r.y(e),B):b},B.margin=function(e){return arguments.length?(p.top=typeof e.top!="undefined"?e.top:p.top,p.right=typeof e.right!="undefined"?e.right:p.right,p.bottom=typeof e.bottom!="undefined"?e.bottom:p.bottom,p.left=typeof e.left!="undefined"?e.left:p.left,B):p},B.width=function(e){return arguments.length?(v=e,B):v},B.height=function(e){return arguments.length?(m=e,B):m},B.color=function(t){return arguments.length?(w=e.utils.getColor(t),c.color(w),B):w},B.showLegend=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(T=e,B):T},B.tooltipContent=function(e){return arguments.length?(N=e,B):N},B.noData=function(e){return arguments.length?(_=e,B):_},B.brushExtent=function(e){return arguments.length?(x=e,B):x},B},e.models.multiBar=function(){"use strict";function C(e){return e.each(function(e){var C=n-t.left-t.right,k=r-t.top-t.bottom,L=d3.select(this);d&&e.length&&(d=[{values:e[0].values.map(function(e){return{x:e.x,y:0,series:e.series,size:.01}})}]),c&&(e=d3.layout.stack().offset(h).values(function(e){return e.values}).y(a)(!e.length&&d?d:e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),c&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i,i-=t.size):(t.y1=t.size+r,r+=t.size)})});var A=y&&b?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});i.domain(y||d3.merge(A).map(function(e){return e.x})).rangeBands(w||[0,C],S),s.domain(b||d3.extent(d3.merge(A).map(function(e){return c?e.y>0?e.y1:e.y1+e.y:e.y}).concat(f))).range(E||[k,0]),i.domain()[0]===i.domain()[1]&&(i.domain()[0]?i.domain([i.domain()[0]-i.domain()[0]*.01,i.domain()[1]+i.domain()[1]*.01]):i.domain([-1,1])),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]+s.domain()[0]*.01,s.domain()[1]-s.domain()[1]*.01]):s.domain([-1,1])),T=T||i,N=N||s;var O=L.selectAll("g.nv-wrap.nv-multibar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-multibar"),_=M.append("defs"),D=M.append("g"),P=O.select("g");D.append("g").attr("class","nv-groups"),O.attr("transform","translate("+t.left+","+t.top+")"),_.append("clipPath").attr("id","nv-edge-clip-"+o).append("rect"),O.select("#nv-edge-clip-"+o+" rect").attr("width",C).attr("height",k),P.attr("clip-path",l?"url(#nv-edge-clip-"+o+")":"");var H=O.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});H.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),H.exit().transition().selectAll("rect.nv-bar").delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e){return c?N(e.y0):N(0)}).attr("height",0).remove(),H.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return p(e,t)}).style("stroke",function(e,t){return p(e,t)}),H.transition().style("stroke-opacity",1).style("fill-opacity",.75);var B=H.selectAll("rect.nv-bar").data(function(t){return d&&!e.length?d.values:t.values});B.exit().remove();var j=B.enter().append("rect").attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).attr("x",function(t,n,r){return c?0:r*i.rangeBand()/e.length}).attr("y",function(e){return N(c?e.y0:0)}).attr("height",0).attr("width",i.rangeBand()/(c?1:e.length)).attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"});B.style("fill",function(e,t,n){return p(e,n,t)}).style("stroke",function(e,t,n){return p(e,n,t)}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length +,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),B.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).transition().attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"}),v&&(m||(m=e.map(function(){return!0})),B.style("fill",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()})),c?B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e,t){return s(c?e.y1:0)}).attr("height",function(e,t){return Math.max(Math.abs(s(e.y+(c?e.y0:0))-s(c?e.y0:0)),1)}).attr("x",function(t,n){return c?0:t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/(c?1:e.length)):B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("x",function(t,n){return t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/e.length).attr("y",function(e,t){return a(e,t)<0?s(0):s(0)-s(a(e,t))<1?s(0)-1:s(a(e,t))||0}).attr("height",function(e,t){return Math.max(Math.abs(s(a(e,t))-s(0)),1)||0}),T=i.copy(),N=s.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=d3.scale.ordinal(),s=d3.scale.linear(),o=Math.floor(Math.random()*1e4),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=!0,c=!1,h="zero",p=e.utils.defaultColor(),d=!1,v=null,m,g=1200,y,b,w,E,S=.1,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(i=e,C):i},C.yScale=function(e){return arguments.length?(s=e,C):s},C.xDomain=function(e){return arguments.length?(y=e,C):y},C.yDomain=function(e){return arguments.length?(b=e,C):b},C.xRange=function(e){return arguments.length?(w=e,C):w},C.yRange=function(e){return arguments.length?(E=e,C):E},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(c=e,C):c},C.stackOffset=function(e){return arguments.length?(h=e,C):h},C.clipEdge=function(e){return arguments.length?(l=e,C):l},C.color=function(t){return arguments.length?(p=e.utils.getColor(t),C):p},C.barColor=function(t){return arguments.length?(v=e.utils.getColor(t),C):v},C.disabled=function(e){return arguments.length?(m=e,C):m},C.id=function(e){return arguments.length?(o=e,C):o},C.hideable=function(e){return arguments.length?(d=e,C):d},C.delay=function(e){return arguments.length?(g=e,C):g},C.groupSpacing=function(e){return arguments.length?(S=e,C):S},C},e.models.multiBarChart=function(){"use strict";function A(e){return e.each(function(e){var b=d3.select(this),O=this,M=(u||parseInt(b.style("width"))||960)-o.left-o.right,_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom;A.update=function(){b.transition().duration(k).call(A)},A.container=this,S.disabled=e.map(function(e){return!!e.disabled});if(!x){var D;x={};for(D in S)S[D]instanceof Array?x[D]=S[D].slice(0):x[D]=S[D]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var P=b.selectAll(".nv-noData").data([T]);return P.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),P.attr("x",o.left+M/2).attr("y",o.top+_/2).text(function(e){return e}),A}b.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();var H=b.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([e]),B=H.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),j=H.select("g");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-barsWrap"),B.append("g").attr("class","nv-legendWrap"),B.append("g").attr("class","nv-controlsWrap"),c&&(i.width(M-C()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),j.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom),j.select(".nv-legendWrap").attr("transform","translate("+C()+","+ -o.top+")"));if(l){var F=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(C()).color(["#444","#444","#444"]),j.select(".nv-controlsWrap").datum(F).attr("transform","translate(0,"+ -o.top+")").call(s)}H.attr("transform","translate("+o.left+","+o.top+")"),d&&j.select(".nv-y.nv-axis").attr("transform","translate("+M+",0)"),t.disabled(e.map(function(e){return e.disabled})).width(M).height(_).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var I=j.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));I.transition().call(t);if(h){n.scale(w).ticks(M/100).tickSize(-_,0),j.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),j.select(".nv-x.nv-axis").transition().call(n);var q=j.select(".nv-x.nv-axis > g").selectAll("g");q.selectAll("line, text").style("opacity",1);if(m){var R=function(e,t){return"translate("+e+","+t+")"},U=5,z=17;q.selectAll("text").attr("transform",function(e,t,n){return R(0,n%2==0?U:z)});var W=d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;j.selectAll(".nv-x.nv-axis .nv-axisMaxMin text").attr("transform",function(e,t){return R(0,t===0||W%2!==0?z:U)})}v&&q.filter(function(t,n){return n%Math.ceil(e[0].values.length/(M/100))!==0}).selectAll("text, line").style("opacity",0),g&&q.selectAll(".tick text").attr("transform","rotate("+g+" 0,0)").style("text-anchor",g>0?"start":"end"),j.select(".nv-x.nv-axis").selectAll("g.nv-axisMaxMin text").style("opacity",1)}p&&(r.scale(E).ticks(_/36).tickSize(-M,0),j.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){S=e,N.stateChange(S),A.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;F=F.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}S.stacked=t.stacked(),N.stateChange(S),A.update()}),N.on("tooltipShow",function(e){y&&L(e,O.parentNode)}),N.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),S.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),S.stacked=n.stacked),A.update()})}),A}var t=e.models.multiBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=!1,g=0,y=!0,b=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" on "+t+"

    "},w,E,S={stacked:!1},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=function(){return l?180:0},k=250;t.stacked(!1),n.orient("bottom").tickPadding(7).highlightZero(!0).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(d?"right":"left").tickFormat(d3.format(",.1f")),s.updateState(!1);var L=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,A);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],N.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){y&&e.tooltip.cleanup()}),A.dispatch=N,A.multibar=t,A.legend=i,A.xAxis=n,A.yAxis=r,d3.rebind(A,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","stacked","stackOffset","delay","barColor","groupSpacing"),A.options=e.utils.optionsFunc.bind(A),A.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,A):o},A.width=function(e){return arguments.length?(u=e,A):u},A.height=function(e){return arguments.length?(a=e,A):a},A.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),A):f},A.showControls=function(e){return arguments.length?(l=e,A):l},A.showLegend=function(e){return arguments.length?(c=e,A):c},A.showXAxis=function(e){return arguments.length?(h=e,A):h},A.showYAxis=function(e){return arguments.length?(p=e,A):p},A.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),A):d},A.reduceXTicks=function(e){return arguments.length?(v=e,A):v},A.rotateLabels=function(e){return arguments.length?(g=e,A):g},A.staggerLabels=function(e){return arguments.length?(m=e,A):m},A.tooltip=function(e){return arguments.length?(b=e,A):b},A.tooltips=function(e){return arguments.length?(y=e,A):y},A.tooltipContent=function(e){return arguments.length?(b=e,A):b},A.state=function(e){return arguments.length?(S=e,A):S},A.defaultState=function(e){return arguments.length?(x=e,A):x},A.noData=function(e){return arguments.length?(T=e,A):T},A.transitionDuration=function(e){return arguments.length?(k=e,A):k},A},e.models.multiBarHorizontal=function(){"use strict";function C(e){return e.each(function(e){var i=n-t.left-t.right,y=r-t.top-t.bottom,C=d3.select(this);p&&(e=d3.layout.stack().offset("zero").values(function(e){return e.values}).y(a)(e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),p&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i-t.size,i-=t.size):(t.y1=r,r+=t.size)})});var k=b&&w?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});s.domain(b||d3.merge(k).map(function(e){return e.x})).rangeBands(E||[0,y],.1),o.domain(w||d3.extent(d3.merge(k).map(function(e){return p?e.y>0?e.y1+e.y:e.y1:e.y}).concat(f))),d&&!p?o.range(S||[o.domain()[0]<0?m:0,i-(o.domain()[1]>0?m:0)]):o.range(S||[0,i]),T=T||s,N=N||d3.scale.linear().domain(o.domain()).range([o(0),o(0)]);var L=d3.select(this).selectAll("g.nv-wrap.nv-multibarHorizontal").data([e]),A=L.enter().append("g").attr("class","nvd3 nv-wrap nv-multibarHorizontal"),O=A.append("defs"),M=A.append("g"),_=L.select("g");M.append("g").attr("class","nv-groups"),L.attr("transform","translate("+t.left+","+t.top+")");var D=L.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});D.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),D.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),D.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return l(e,t)}).style("stroke",function(e,t){return l(e,t)}),D.transition().style("stroke-opacity",1).style("fill-opacity",.75);var P=D.selectAll("g.nv-bar").data(function(e){return e.values});P.exit().remove();var H=P.enter().append("g").attr("transform",function(t,n,r){return"translate("+N(p?t.y0:0)+","+(p?0:r*s.rangeBand()/e.length+s(u(t,n)))+")"});H.append("rect").attr("width",0).attr("height",s.rangeBand()/(p?1:e.length)),P.on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[o(a(t,n)+(p?t.y0:0)),s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),H.append("text"),d&&!p?(P.select("text").attr("text-anchor",function(e,t){return a(e,t)<0?"end":"start"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return g(a(e,t))}),P.transition().select("text").attr("x",function(e,t){return a(e,t)<0?-4:o(a(e,t))-o(0)+4})):P.selectAll("text").text(""),v&&!p?(H.append("text").classed("nv-bar-label",!0),P.select("text.nv-bar-label").attr("text-anchor",function(e,t){return a(e,t)<0?"start":"end"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return u(e,t)}),P.transition().select("text.nv-bar-label").attr("x",function(e,t){return a(e,t)<0?o(0)-o(a(e,t))+4:-4})):P.selectAll("text.nv-bar-label").text(""),P.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}),c&&(h||(h=e.map(function(){return!0})),P.style("fill",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()})),p?P.transition().attr("transform",function(e,t){return"translate("+o(e.y1)+","+s(u(e,t))+")"}).select("rect").attr("width",function(e,t){return Math.abs(o(a(e,t)+e.y0)-o(e.y0))}).attr("height",s.rangeBand()):P.transition().attr("transform",function(t,n){return"translate("+(a(t,n)<0?o(a(t,n)):o(0))+","+(t.series*s.rangeBand()/e.length+s(u(t,n)))+")"}).select("rect").attr("height",s.rangeBand()/e.length).attr("width",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(0)),1)}),T=s.copy(),N=o.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=null,h,p=!1,d=!1,v=!1,m=60,g=d3.format(",.2f"),y=1200,b,w,E,S,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(s=e,C):s},C.yScale=function(e){return arguments.length?(o=e,C):o},C.xDomain=function(e){return arguments.length?(b=e,C):b},C.yDomain=function(e){return arguments.length?(w=e,C):w},C.xRange=function(e){return arguments.length?(E=e,C):E},C.yRange=function(e){return arguments.length?(S=e,C):S},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(p=e,C):p},C.color=function(t){return arguments.length?(l=e.utils.getColor(t),C):l},C.barColor=function(t){return arguments.length?(c=e.utils.getColor(t),C):c},C.disabled=function(e){return arguments.length?(h=e,C):h},C.id=function(e){return arguments.length?(i=e,C):i},C.delay=function(e){return arguments.length?(y=e,C):y},C.showValues=function(e){return arguments.length?(d=e,C):d},C.showBarLabels=function(e){return arguments.length?(v=e,C):v},C.valueFormat=function(e){return arguments.length?(g=e,C):g},C.valuePadding=function(e){return arguments.length?(m=e,C):m},C},e.models.multiBarHorizontalChart=function(){"use strict";function C(e){return e.each(function(e){var d=d3.select(this),m=this,k=(u||parseInt(d.style("width"))||960)-o.left-o.right,L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom;C.update=function(){d.transition().duration(T).call(C)},C.container=this,b.disabled=e.map(function(e){return!!e.disabled});if(!w){var A;w={};for(A in b)b[A]instanceof Array?w[A]=b[A].slice(0):w[A]=b[A]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var O=d.selectAll(".nv-noData").data([E]);return O.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),O.attr("x",o.left+k/2).attr("y",o.top+L/2).text(function(e){return e}),C}d.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var M=d.selectAll("g.nv-wrap.nv-multiBarHorizontalChart").data([e]),_=M.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarHorizontalChart").append("g"),D=M.select("g");_.append("g").attr("class","nv-x nv-axis"),_.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),_.append("g").attr("class","nv-barsWrap"),_.append("g").attr("class","nv-legendWrap"),_.append("g").attr("class","nv-controlsWrap"),c&&(i.width(k-x()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),D.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom),D.select(".nv-legendWrap").attr("transform","translate("+x()+","+ -o.top+")"));if(l){var P=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(x()).color(["#444","#444","#444"]),D.select(".nv-controlsWrap").datum(P).attr("transform","translate(0,"+ -o.top+")").call(s)}M.attr("transform","translate("+o.left+","+o.top+")"),t.disabled(e.map(function(e){return e.disabled})).width(k).height(L).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var H=D.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));H.transition().call(t);if(h){n.scale(g).ticks(L/24).tickSize(-k,0),D.select(".nv-x.nv-axis").transition().call(n);var B=D.select(".nv-x.nv-axis").selectAll("g");B.selectAll("line, text")}p&&(r.scale(y).ticks(k/100).tickSize(-L,0),D.select(".nv-y.nv-axis").attr("transform","translate(0,"+L+")"),D.select(".nv-y.nv-axis").transition().call(r)),D.select(".nv-zeroLine line").attr("x1",y(0)).attr("x2",y(0)).attr("y1",0).attr("y2",-L),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),C.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;P=P.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}b.stacked=t.stacked(),S.stateChange(b),C.update()}),S.on("tooltipShow",function(e){v&&N(e,m.parentNode)}),S.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),b.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),b.stacked=n.stacked),C.update()})}),C}var t=e.models.multiBarHorizontal(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend().height(30),s=e.models.legend().height(30),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=function(e,t,n,r,i){return"

    "+e+" - "+t+"

    "+"

    "+n+"

    "},g,y,b={stacked:d},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=function(){return l?180:0},T=250;t.stacked(d),n.orient("left").tickPadding(5).highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient("bottom").tickFormat(d3.format(",.1f")),s.updateState(!1);var N=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,C);e.tooltip.show([o,u],l,i.value<0?"e":"w",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),C.dispatch=S,C.multibar=t,C.legend=i,C.xAxis=n,C.yAxis=r,d3.rebind(C,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","delay","showValues","showBarLabels","valueFormat","stacked","barColor"),C.options=e.utils.optionsFunc.bind(C),C.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,C):o},C.width=function(e){return arguments.length?(u=e,C):u},C.height=function(e){return arguments.length?(a=e,C):a},C.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),C):f},C.showControls=function(e){return arguments.length?(l=e,C):l},C.showLegend=function(e){return arguments.length?(c=e,C):c},C.showXAxis=function(e){return arguments.length?(h=e,C):h},C.showYAxis=function(e){return arguments.length?(p=e,C):p},C.tooltip=function(e){return arguments.length?(m=e,C):m},C.tooltips=function(e){return arguments.length?(v=e,C):v},C.tooltipContent=function(e){return arguments.length?(m=e,C):m},C.state=function(e){return arguments.length?(b=e,C):b},C.defaultState=function(e){return arguments.length?(w=e,C):w},C.noData=function(e){return arguments.length?(E=e,C):E},C.transitionDuration=function(e){return arguments.length?(T=e,C):T},C},e.models.multiChart=function(){"use strict";function C(e){return e.each(function(e){var u=d3.select(this),f=this;C.update=function(){u.transition().call(C)},C.container=this;var k=(r||parseInt(u.style("width"))||960)-t.left-t.right,L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom,A=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==1}),O=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==2}),M=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==1}),_=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==2}),D=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==1}),P=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==2}),H=e.filter(function(e){return!e.disabled&&e.yAxis==1}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})}),B=e.filter(function(e){return!e.disabled&&e.yAxis==2}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})});a.domain(d3.extent(d3.merge(H.concat(B)),function(e){return e.x})).range([0,k]);var j=u.selectAll("g.wrap.multiChart").data([e]),F=j.enter().append("g").attr("class","wrap nvd3 multiChart").append("g");F.append("g").attr("class","x axis"),F.append("g").attr("class","y1 axis"),F.append("g").attr("class","y2 axis"),F.append("g").attr("class","lines1Wrap"),F.append("g").attr("class","lines2Wrap"),F.append("g").attr("class","bars1Wrap"),F.append("g").attr("class","bars2Wrap"),F.append("g").attr("class","stack1Wrap"),F.append("g").attr("class","stack2Wrap"),F.append("g").attr("class","legendWrap");var I=j.select("g");s&&(x.width(k/2),I.select(".legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.yAxis==1?"":" (right axis)"),e})).call(x),t.top!=x.height()&&(t.top=x.height(),L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom),I.select(".legendWrap").attr("transform","translate("+k/2+","+ -t.top+")")),d.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="line"})),v.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="line"})),m.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="bar"})),g.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="bar"})),y.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="area"})),b.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="area"})),I.attr("transform","translate("+t.left+","+t.top+")");var q=I.select(".lines1Wrap").datum(A),R=I.select(".bars1Wrap").datum(M),U=I.select(".stack1Wrap").datum(D),z=I.select(".lines2Wrap").datum(O),W=I.select(".bars2Wrap").datum(_),X=I.select(".stack2Wrap").datum(P),V=D.length?D.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[],$=P.length?P.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[];h.domain(l||d3.extent(d3.merge(H).concat(V),function(e){return e.y})).range([0,L]),p.domain(c||d3.extent(d3.merge(B).concat($),function(e){return e.y})).range([0,L]),d.yDomain(h.domain()),m.yDomain(h.domain()),y.yDomain(h.domain()),v.yDomain(p.domain()),g.yDomain(p.domain()),b.yDomain(p.domain()),D.length&&d3.transition(U).call(y),P.length&&d3.transition(X).call(b),M.length&&d3.transition(R).call(m),_.length&&d3.transition(W).call(g),A.length&&d3.transition(q).call(d),O.length&&d3.transition(z).call(v),w.ticks(k/100).tickSize(-L,0),I.select(".x.axis").attr("transform","translate(0,"+L+")"),d3.transition(I.select(".x.axis")).call(w),E.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y1.axis")).call(E),S.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y2.axis")).call(S),I.select(".y2.axis").style("opacity",B.length?1:0).attr("transform","translate("+a.range()[1]+",0)"),x.dispatch.on("stateChange",function(e){C.update()}),T.on("tooltipShow",function(e){o&&N(e,f.parentNode)})}),C}var t={top:30,right:20,bottom:50,left:60},n=d3.scale.category20().range(),r=null,i=null,s=!0,o=!0,u=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" at "+t+"

    "},a,f,l,c,a=d3.scale.linear(),h=d3.scale.linear(),p=d3.scale.linear(),d=e.models.line().yScale(h),v=e.models.line().yScale(p),m=e.models.multiBar().stacked(!1).yScale(h),g=e.models.multiBar().stacked(!1).yScale(p),y=e.models.stackedArea().yScale(h),b=e.models.stackedArea().yScale(p),w=e.models.axis().scale(a).orient("bottom").tickPadding(5),E=e.models.axis().scale(h).orient("left"),S=e.models.axis().scale(p).orient("right"),x=e.models.legend().height(30),T=d3.dispatch("tooltipShow","tooltipHide"),N=function(t,n){var r=t.pos[0]+(n.offsetLeft||0),i=t.pos[1]+(n.offsetTop||0),s=w.tickFormat()(d.x()(t.point,t.pointIndex)),o=(t.series.yAxis==2?S:E).tickFormat()(d.y()(t.point,t.pointIndex)),a=u(t.series.key,s,o,t,C);e.tooltip.show([r,i],a,undefined,undefined,n.offsetParent)};return d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),m.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),m.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),g.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),g.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),y.dispatch.on("tooltipShow",function(e){if(!Math.round(y.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),y.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),b.dispatch.on("tooltipShow",function(e){if(!Math.round(b.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),b.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){o&&e.tooltip.cleanup()}),C.dispatch=T,C.lines1=d,C.lines2=v,C.bars1=m,C.bars2=g,C.stack1=y,C.stack2=b,C.xAxis=w,C.yAxis1=E,C.yAxis2=S,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(getX=e,d.x(e),m.x(e),C):getX},C.y=function(e){return arguments.length?(getY=e,d.y(e),m.y(e),C):getY},C.yDomain1=function(e){return arguments.length?(l=e,C):l},C.yDomain2=function(e){return arguments.length?(c=e,C):c},C.margin=function(e){return arguments.length?(t=e,C):t},C.width=function(e){return arguments.length?(r=e,C):r},C.height=function(e){return arguments.length?(i=e,C):i},C.color=function(e){return arguments.length?(n=e,x.color(e),C):n},C.showLegend=function(e){return arguments.length?(s=e,C):s},C.tooltips=function(e){return arguments.length?(o=e,C):o},C.tooltipContent=function(e){return arguments.length?(u=e,C):u},C},e.models.ohlcBar=function(){"use strict";function x(e){return e.each(function(e){var g=n-t.left-t.right,x=r-t.top-t.bottom,T=d3.select(this);s.domain(y||d3.extent(e[0].values.map(u).concat(p))),v?s.range(w||[g*.5/e[0].values.length,g*(e[0].values.length-.5)/e[0].values.length]):s.range(w||[0,g]),o.domain(b||[d3.min(e[0].values.map(h).concat(d)),d3.max(e[0].values.map(c).concat(d))]).range(E||[x,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=d3.select(this).selectAll("g.nv-wrap.nv-ohlcBar").data([e[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-ohlcBar"),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-ticks"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){S.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",g).attr("height",x),A.attr("clip-path",m?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-ticks").selectAll(".nv-tick").data(function(e){return e});O.exit().remove();var M=O.enter().append("path").attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"}).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),S.elementMouseover({point:t,series:e[0],pos:[s(u(t,n)),o(a(t,n))],pointIndex:n,seriesIndex:0,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),S.elementMouseout({point:t,series:e[0],pointIndex:n,seriesIndex:0,e:d3.event})}).on("click",function(e,t){S.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){S.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}),d3.transition(O).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"})}),x}var t={top:0 +,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=function(e){return e.open},l=function(e){return e.close},c=function(e){return e.high},h=function(e){return e.low},p=[],d=[],v=!1,m=!0,g=e.utils.defaultColor(),y,b,w,E,S=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return x.dispatch=S,x.options=e.utils.optionsFunc.bind(x),x.x=function(e){return arguments.length?(u=e,x):u},x.y=function(e){return arguments.length?(a=e,x):a},x.open=function(e){return arguments.length?(f=e,x):f},x.close=function(e){return arguments.length?(l=e,x):l},x.high=function(e){return arguments.length?(c=e,x):c},x.low=function(e){return arguments.length?(h=e,x):h},x.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,x):t},x.width=function(e){return arguments.length?(n=e,x):n},x.height=function(e){return arguments.length?(r=e,x):r},x.xScale=function(e){return arguments.length?(s=e,x):s},x.yScale=function(e){return arguments.length?(o=e,x):o},x.xDomain=function(e){return arguments.length?(y=e,x):y},x.yDomain=function(e){return arguments.length?(b=e,x):b},x.xRange=function(e){return arguments.length?(w=e,x):w},x.yRange=function(e){return arguments.length?(E=e,x):E},x.forceX=function(e){return arguments.length?(p=e,x):p},x.forceY=function(e){return arguments.length?(d=e,x):d},x.padData=function(e){return arguments.length?(v=e,x):v},x.clipEdge=function(e){return arguments.length?(m=e,x):m},x.color=function(t){return arguments.length?(g=e.utils.getColor(t),x):g},x.id=function(e){return arguments.length?(i=e,x):i},x},e.models.pie=function(){"use strict";function S(e){return e.each(function(e){function q(e){var t=(e.startAngle+e.endAngle)*90/Math.PI-90;return t>90?t-180:t}function R(e){e.endAngle=isNaN(e.endAngle)?0:e.endAngle,e.startAngle=isNaN(e.startAngle)?0:e.startAngle,m||(e.innerRadius=0);var t=d3.interpolate(this._current,e);return this._current=t(0),function(e){return A(t(e))}}function U(e){e.innerRadius=0;var t=d3.interpolate({startAngle:0,endAngle:0},e);return function(e){return A(t(e))}}var o=n-t.left-t.right,f=r-t.top-t.bottom,S=Math.min(o,f)/2,x=S-S/5,T=d3.select(this),N=T.selectAll(".nv-wrap.nv-pie").data(e),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-pie nv-chart-"+u),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-pie"),k.append("g").attr("class","nv-pieLabels"),N.attr("transform","translate("+t.left+","+t.top+")"),L.select(".nv-pie").attr("transform","translate("+o/2+","+f/2+")"),L.select(".nv-pieLabels").attr("transform","translate("+o/2+","+f/2+")"),T.on("click",function(e,t){E.chartClick({data:e,index:t,pos:d3.event,id:u})});var A=d3.svg.arc().outerRadius(x);y&&A.startAngle(y),b&&A.endAngle(b),m&&A.innerRadius(S*w);var O=d3.layout.pie().sort(null).value(function(e){return e.disabled?0:s(e)}),M=N.select(".nv-pie").selectAll(".nv-slice").data(O),_=N.select(".nv-pieLabels").selectAll(".nv-label").data(O);M.exit().remove(),_.exit().remove();var D=M.enter().append("g").attr("class","nv-slice").on("mouseover",function(e,t){d3.select(this).classed("hover",!0),E.elementMouseover({label:i(e.data),value:s(e.data),point:e.data,pointIndex:t,pos:[d3.event.pageX,d3.event.pageY],id:u})}).on("mouseout",function(e,t){d3.select(this).classed("hover",!1),E.elementMouseout({label:i(e.data),value:s(e.data),point:e.data,index:t,id:u})}).on("click",function(e,t){E.elementClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()}).on("dblclick",function(e,t){E.elementDblClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()});M.attr("fill",function(e,t){return a(e,t)}).attr("stroke",function(e,t){return a(e,t)});var P=D.append("path").each(function(e){this._current=e});M.select("path").transition().attr("d",A).attrTween("d",R);if(l){var H=d3.svg.arc().innerRadius(0);c&&(H=A),h&&(H=d3.svg.arc().outerRadius(A.outerRadius())),_.enter().append("g").classed("nv-label",!0).each(function(e,t){var n=d3.select(this);n.attr("transform",function(e){if(g){e.outerRadius=x+10,e.innerRadius=x+15;var t=(e.startAngle+e.endAngle)/2*(180/Math.PI);return(e.startAngle+e.endAngle)/2v?r[p]:""})}}),S}var t={top:0,right:0,bottom:0,left:0},n=500,r=500,i=function(e){return e.x},s=function(e){return e.y},o=function(e){return e.description},u=Math.floor(Math.random()*1e4),a=e.utils.defaultColor(),f=d3.format(",.2f"),l=!0,c=!0,h=!1,p="key",v=.02,m=!1,g=!1,y=!1,b=!1,w=.5,E=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return S.dispatch=E,S.options=e.utils.optionsFunc.bind(S),S.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,S):t},S.width=function(e){return arguments.length?(n=e,S):n},S.height=function(e){return arguments.length?(r=e,S):r},S.values=function(t){return e.log("pie.values() is no longer supported."),S},S.x=function(e){return arguments.length?(i=e,S):i},S.y=function(e){return arguments.length?(s=d3.functor(e),S):s},S.description=function(e){return arguments.length?(o=e,S):o},S.showLabels=function(e){return arguments.length?(l=e,S):l},S.labelSunbeamLayout=function(e){return arguments.length?(g=e,S):g},S.donutLabelsOutside=function(e){return arguments.length?(h=e,S):h},S.pieLabelsOutside=function(e){return arguments.length?(c=e,S):c},S.labelType=function(e){return arguments.length?(p=e,p=p||"key",S):p},S.donut=function(e){return arguments.length?(m=e,S):m},S.donutRatio=function(e){return arguments.length?(w=e,S):w},S.startAngle=function(e){return arguments.length?(y=e,S):y},S.endAngle=function(e){return arguments.length?(b=e,S):b},S.id=function(e){return arguments.length?(u=e,S):u},S.color=function(t){return arguments.length?(a=e.utils.getColor(t),S):a},S.valueFormat=function(e){return arguments.length?(f=e,S):f},S.labelThreshold=function(e){return arguments.length?(v=e,S):v},S},e.models.pieChart=function(){"use strict";function v(e){return e.each(function(e){var u=d3.select(this),a=this,f=(i||parseInt(u.style("width"))||960)-r.left-r.right,d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom;v.update=function(){u.transition().call(v)},v.container=this,l.disabled=e.map(function(e){return!!e.disabled});if(!c){var m;c={};for(m in l)l[m]instanceof Array?c[m]=l[m].slice(0):c[m]=l[m]}if(!e||!e.length){var g=u.selectAll(".nv-noData").data([h]);return g.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),g.attr("x",r.left+f/2).attr("y",r.top+d/2).text(function(e){return e}),v}u.selectAll(".nv-noData").remove();var y=u.selectAll("g.nv-wrap.nv-pieChart").data([e]),b=y.enter().append("g").attr("class","nvd3 nv-wrap nv-pieChart").append("g"),w=y.select("g");b.append("g").attr("class","nv-pieWrap"),b.append("g").attr("class","nv-legendWrap"),o&&(n.width(f).key(t.x()),y.select(".nv-legendWrap").datum(e).call(n),r.top!=n.height()&&(r.top=n.height(),d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom),y.select(".nv-legendWrap").attr("transform","translate(0,"+ -r.top+")")),y.attr("transform","translate("+r.left+","+r.top+")"),t.width(f).height(d);var E=w.select(".nv-pieWrap").datum([e]);d3.transition(E).call(t),n.dispatch.on("stateChange",function(e){l=e,p.stateChange(l),v.update()}),t.dispatch.on("elementMouseout.tooltip",function(e){p.tooltipHide(e)}),p.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),l.disabled=t.disabled),v.update()})}),v}var t=e.models.pie(),n=e.models.legend(),r={top:30,right:20,bottom:20,left:20},i=null,s=null,o=!0,u=e.utils.defaultColor(),a=!0,f=function(e,t,n,r){return"

    "+e+"

    "+"

    "+t+"

    "},l={},c=null,h="No Data Available.",p=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),d=function(n,r){var i=t.description()(n.point)||t.x()(n.point),s=n.pos[0]+(r&&r.offsetLeft||0),o=n.pos[1]+(r&&r.offsetTop||0),u=t.valueFormat()(t.y()(n.point)),a=f(i,u,n,v);e.tooltip.show([s,o],a,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+r.left,e.pos[1]+r.top],p.tooltipShow(e)}),p.on("tooltipShow",function(e){a&&d(e)}),p.on("tooltipHide",function(){a&&e.tooltip.cleanup()}),v.legend=n,v.dispatch=p,v.pie=t,d3.rebind(v,t,"valueFormat","values","x","y","description","id","showLabels","donutLabelsOutside","pieLabelsOutside","labelType","donut","donutRatio","labelThreshold"),v.options=e.utils.optionsFunc.bind(v),v.margin=function(e){return arguments.length?(r.top=typeof e.top!="undefined"?e.top:r.top,r.right=typeof e.right!="undefined"?e.right:r.right,r.bottom=typeof e.bottom!="undefined"?e.bottom:r.bottom,r.left=typeof e.left!="undefined"?e.left:r.left,v):r},v.width=function(e){return arguments.length?(i=e,v):i},v.height=function(e){return arguments.length?(s=e,v):s},v.color=function(r){return arguments.length?(u=e.utils.getColor(r),n.color(u),t.color(u),v):u},v.showLegend=function(e){return arguments.length?(o=e,v):o},v.tooltips=function(e){return arguments.length?(a=e,v):a},v.tooltipContent=function(e){return arguments.length?(f=e,v):f},v.state=function(e){return arguments.length?(l=e,v):l},v.defaultState=function(e){return arguments.length?(c=e,v):c},v.noData=function(e){return arguments.length?(h=e,v):h},v},e.models.scatter=function(){"use strict";function I(q){return q.each(function(I){function Q(){if(!g)return!1;var e,i=d3.merge(I.map(function(e,t){return e.values.map(function(e,n){var r=f(e,n),i=l(e,n);return[o(r)+Math.random()*1e-7,u(i)+Math.random()*1e-7,t,n,e]}).filter(function(e,t){return b(e[4],t)})}));if(D===!0){if(x){var a=X.select("defs").selectAll(".nv-point-clips").data([s]).enter();a.append("clipPath").attr("class","nv-point-clips").attr("id","nv-points-clip-"+s);var c=X.select("#nv-points-clip-"+s).selectAll("circle").data(i);c.enter().append("circle").attr("r",T),c.exit().remove(),c.attr("cx",function(e){return e[0]}).attr("cy",function(e){return e[1]}),X.select(".nv-point-paths").attr("clip-path","url(#nv-points-clip-"+s+")")}i.length&&(i.push([o.range()[0]-20,u.range()[0]-20,null,null]),i.push([o.range()[1]+20,u.range()[1]+20,null,null]),i.push([o.range()[0]-20,u.range()[0]+20,null,null]),i.push([o.range()[1]+20,u.range()[1]-20,null,null]));var h=d3.geom.polygon([[-10,-10],[-10,r+10],[n+10,r+10],[n+10,-10]]),p=d3.geom.voronoi(i).map(function(e,t){return{data:h.clip(e),series:i[t][2],point:i[t][3]}}),d=X.select(".nv-point-paths").selectAll("path").data(p);d.enter().append("path").attr("class",function(e,t){return"nv-path-"+t}),d.exit().remove(),d.attr("d",function(e){return e.data.length===0?"M 0 0":"M"+e.data.join("L")+"Z"});var v=function(e,n){if(F)return 0;var r=I[e.series];if(typeof r=="undefined")return;var i=r.values[e.point];n({point:i,series:r,pos:[o(f(i,e.point))+t.left,u(l(i,e.point))+t.top],seriesIndex:e.series,pointIndex:e.point})};d.on("click",function(e){v(e,_.elementClick)}).on("mouseover",function(e){v(e,_.elementMouseover)}).on("mouseout",function(e,t){v(e,_.elementMouseout)})}else X.select(".nv-groups").selectAll(".nv-group").selectAll(".nv-point").on("click",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementClick({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseover",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementMouseover({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseout",function(e,t){if(F||!I[e.series])return 0;var n=I[e.series],r=n.values[t];_.elementMouseout({point:r,series:n,seriesIndex:e.series,pointIndex:t})});F=!1}var q=n-t.left-t.right,R=r-t.top-t.bottom,U=d3.select(this);I.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var W=N&&C&&A?[]:d3.merge(I.map(function(e){return e.values.map(function(e,t){return{x:f(e,t),y:l(e,t),size:c(e,t)}})}));o.domain(N||d3.extent(W.map(function(e){return e.x}).concat(d))),w&&I[0]?o.range(k||[(q*E+q)/(2*I[0].values.length),q-q*(1+E)/(2*I[0].values.length)]):o.range(k||[0,q]),u.domain(C||d3.extent(W.map(function(e){return e.y}).concat(v))).range(L||[R,0]),a.domain(A||d3.extent(W.map(function(e){return e.size}).concat(m))).range(O||[16,256]);if(o.domain()[0]===o.domain()[1]||u.domain()[0]===u.domain()[1])M=!0;o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]-o.domain()[0]*.01,o.domain()[1]+o.domain()[1]*.01]):o.domain([-1,1])),u.domain()[0]===u.domain()[1]&&(u.domain()[0]?u.domain([u.domain()[0]-u.domain()[0]*.01,u.domain()[1]+u.domain()[1]*.01]):u.domain([-1,1])),isNaN(o.domain()[0])&&o.domain([-1,1]),isNaN(u.domain()[0])&&u.domain([-1,1]),P=P||o,H=H||u,B=B||a;var X=U.selectAll("g.nv-wrap.nv-scatter").data([I]),V=X.enter().append("g").attr("class","nvd3 nv-wrap nv-scatter nv-chart-"+s+(M?" nv-single-point":"")),$=V.append("defs"),J=V.append("g"),K=X.select("g");J.append("g").attr("class","nv-groups"),J.append("g").attr("class","nv-point-paths"),X.attr("transform","translate("+t.left+","+t.top+")"),$.append("clipPath").attr("id","nv-edge-clip-"+s).append("rect"),X.select("#nv-edge-clip-"+s+" rect").attr("width",q).attr("height",R>0?R:0),K.attr("clip-path",S?"url(#nv-edge-clip-"+s+")":""),F=!0;var G=X.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});G.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),G.exit().remove(),G.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),G.transition().style("fill",function(e,t){return i(e,t)}).style("stroke",function(e,t){return i(e,t)}).style("stroke-opacity",1).style("fill-opacity",.5);if(p){var Y=G.selectAll("circle.nv-point").data(function(e){return e.values},y);Y.enter().append("circle").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("cx",function(t,n){return e.utils.NaNtoZero(P(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(H(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)}),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)})}else{var Y=G.selectAll("path.nv-point").data(function(e){return e.values});Y.enter().append("path").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("transform",function(e,t){return"translate("+P(f(e,t))+","+H(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))})),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))}))}clearTimeout(j),j=setTimeout(Q,300),P=o.copy(),H=u.copy(),B=a.copy()}),I}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e5),o=d3.scale.linear(),u=d3.scale.linear(),a=d3.scale.linear(),f=function(e){return e.x},l=function(e){return e.y},c=function(e){return e.size||1},h=function(e){return e.shape||"circle"},p=!0,d=[],v=[],m=[],g=!0,y=null,b=function(e){return!e.notActive},w=!1,E=.1,S=!1,x=!0,T=function(){return 25},N=null,C=null,k=null,L=null,A=null,O=null,M=!1,_=d3.dispatch("elementClick","elementMouseover","elementMouseout"),D=!0,P,H,B,j,F=!1;return I.clearHighlights=function(){d3.selectAll(".nv-chart-"+s+" .nv-point.hover").classed("hover",!1)},I.highlightPoint=function(e,t,n){d3.select(".nv-chart-"+s+" .nv-series-"+e+" .nv-point-"+t).classed("hover",n)},_.on("elementMouseover.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!0)}),_.on("elementMouseout.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!1)}),I.dispatch=_,I.options=e.utils.optionsFunc.bind(I),I.x=function(e){return arguments.length?(f=d3.functor(e),I):f},I.y=function(e){return arguments.length?(l=d3.functor(e),I):l},I.size=function(e){return arguments.length?(c=d3.functor(e),I):c},I.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,I):t},I.width=function(e){return arguments.length?(n=e,I):n},I.height=function(e){return arguments.length?(r=e,I):r},I.xScale=function(e){return arguments.length?(o=e,I):o},I.yScale=function(e){return arguments.length?(u=e,I):u},I.zScale=function(e){return arguments.length?(a=e,I):a},I.xDomain=function(e){return arguments.length?(N=e,I):N},I.yDomain=function(e){return arguments.length?(C=e,I):C},I.sizeDomain=function(e){return arguments.length?(A=e,I):A},I.xRange=function(e){return arguments.length?(k=e,I):k},I.yRange=function(e){return arguments.length?(L=e,I):L},I.sizeRange=function(e){return arguments.length?(O=e,I):O},I.forceX=function(e){return arguments.length?(d=e,I):d},I.forceY=function(e){return arguments.length?(v=e,I):v},I.forceSize=function(e){return arguments.length?(m=e,I):m},I.interactive=function(e){return arguments.length?(g=e,I):g},I.pointKey=function(e){return arguments.length?(y=e,I):y},I.pointActive=function(e){return arguments.length?(b=e,I):b},I.padData=function(e){return arguments.length?(w=e,I):w},I.padDataOuter=function(e){return arguments.length?(E=e,I):E},I.clipEdge=function(e){return arguments.length?(S=e,I):S},I.clipVoronoi=function(e){return arguments.length?(x=e,I):x},I.useVoronoi=function(e){return arguments.length?(D=e,D===!1&&(x=!1),I):D},I.clipRadius=function(e){return arguments.length?(T=e,I):T},I.color=function(t){return arguments.length?(i=e.utils.getColor(t),I):i},I.shape=function(e){return arguments.length?(h=e,I):h},I.onlyCircles=function(e){return arguments.length?(p=e,I):p},I.id=function(e){return arguments.length?(s=e,I):s},I.singlePoint=function(e){return arguments.length?(M=e,I):M},I},e.models.scatterChart=function(){"use strict";function F(e){return e.each(function(e){function K(){if(T)return X.select(".nv-point-paths").style("pointer-events","all"),!1;X.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(x).focus(i[0]),p.distortion(x).focus(i[1]),X.select(".nv-scatterWrap").call(t),b&&X.select(".nv-x.nv-axis").call(n),w&&X.select(".nv-y.nv-axis").call(r),X.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),X.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var C=d3.select(this),k=this,L=(f||parseInt(C.style("width"))||960)-a.left-a.right,I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom;F.update=function(){C.transition().duration(D).call(F)},F.container=this,A.disabled=e.map(function(e){return!!e.disabled});if(!O){var q;O={};for(q in A)A[q]instanceof Array?O[q]=A[q].slice(0):O[q]=A[q]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=C.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",a.left+L/2).attr("y",a.top+I/2).text(function(e){return e}),F}C.selectAll(".nv-noData").remove(),P=P||h,H=H||p;var U=C.selectAll("g.nv-wrap.nv-scatterChart").data([e]),z=U.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),W=z.append("g"),X=U.select("g");W.append("rect").attr("class","nvd3 nv-background"),W.append("g").attr("class","nv-x nv-axis"),W.append("g").attr("class","nv-y nv-axis"),W.append("g").attr("class","nv-scatterWrap"),W.append("g").attr("class","nv-distWrap"),W.append("g").attr("class","nv-legendWrap"),W.append("g").attr("class","nv-controlsWrap");if(y){var V=S?L/2:L;i.width(V),U.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom),U.select(".nv-legendWrap").attr("transform","translate("+(L-V)+","+ -a.top+")")}S&&(s.width(180).color(["#444"]),X.select(".nv-controlsWrap").datum(j).attr("transform","translate(0,"+ -a.top+")").call(s)),U.attr("transform","translate("+a.left+","+a.top+")"),E&&X.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),t.width(L).height(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),d!==0&&t.xDomain(null),v!==0&&t.yDomain(null),U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t);if(d!==0){var $=h.domain()[1]-h.domain()[0];t.xDomain([h.domain()[0]-d*$,h.domain()[1]+d*$])}if(v!==0){var J=p.domain()[1]-p.domain()[0];t.yDomain([p.domain()[0]-v*J,p.domain()[1]+v*J])}(v!==0||d!==0)&&U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),b&&(n.scale(h).ticks(n.ticks()&&n.ticks().length?n.ticks():L/100).tickSize(-I,0),X.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),w&&(r.scale(p).ticks(r.ticks()&&r.ticks().length?r.ticks():I/36).tickSize(-L,0),X.select(".nv-y.nv-axis").call(r)),m&&(o.getData(t.x()).scale(h).width(L).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),X.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),g&&(u.getData(t.y()).scale(p).width(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),X.select(".nv-distributionY").attr("transform","translate("+(E?L:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(X.select(".nv-background").attr("width",L).attr("height",I),X.select(".nv-background").on("mousemove",K),X.select(".nv-background").on("click",function(){T=!T}),t.dispatch.on("elementClick.freezeFisheye",function(){T=!T})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,x=e.disabled?0:2.5,X.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),X.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(x).focus(0),p.distortion(x).focus(0),X.select(".nv-scatterWrap").call(t),X.select(".nv-x.nv-axis").call(n),X.select(".nv-y.nv-axis").call(r)):T=!1,F.update()}),i.dispatch.on("stateChange",function(e){A.disabled=e.disabled,M.stateChange(A),F.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",function(t,n){return e.pos[1]-I}),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],M.tooltipShow(e)}),M.on("tooltipShow",function(e){N&&B(e,k.parentNode)}),M.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),A.disabled=t.disabled),F.update()}),P=h.copy(),H=p.copy()}),F}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=0,v=0,m=!1,g=!1,y=!0,b=!0,w=!0,E=!1,S=!!d3.fisheye,x=0,T=!1,N=!0,C=function(e,t,n){return""+t+""},k=function(e,t,n){return""+n+""},L=null,A={},O=null,M=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),_="No Data Available.",D=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(E?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var P,H,B=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));C!=null&&e.tooltip.show([f,l],C(i.series.key,v,m,i,F),"n",1,s,"x-nvtooltip"),k!=null&&e.tooltip.show([c,d],k(i.series.key,v,m,i,F),"e",1,s,"y-nvtooltip"),L!=null&&e.tooltip.show([o,u],L(i.series.key,v,m,i,F),i.value<0?"n":"s",null,s)},j=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){M.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),M.on("tooltipHide",function(){N&&e.tooltip.cleanup()}),F.dispatch=M,F.scatter=t,F.legend=i,F.controls=s,F.xAxis=n,F.yAxis=r,F.distX=o,F.distY=u,d3.rebind(F,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),F.options=e.utils.optionsFunc.bind(F),F.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,F):a},F.width=function(e){return arguments.length?(f=e,F):f},F.height=function(e){return arguments.length?(l=e,F):l},F.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),F):c},F.showDistX=function(e){return arguments.length?(m=e,F):m},F.showDistY=function(e){return arguments.length?(g=e,F):g},F.showControls=function(e){return arguments.length?(S=e,F):S},F.showLegend=function(e){return arguments.length?(y=e,F):y},F.showXAxis=function(e){return arguments.length?(b=e,F):b},F.showYAxis=function(e){return arguments.length?(w=e,F):w},F.rightAlignYAxis=function(e){return arguments.length?(E=e,r.orient(e?"right":"left"),F):E},F.fisheye=function(e){return arguments.length?(x=e,F):x},F.xPadding=function(e){return arguments.length?(d=e,F):d},F.yPadding=function(e){return arguments.length?(v=e,F):v},F.tooltips=function(e){return arguments.length?(N=e,F):N},F.tooltipContent=function(e){return arguments.length?(L=e,F):L},F.tooltipXContent=function(e){return arguments.length?(C=e,F):C},F.tooltipYContent=function(e){return arguments.length?(k=e,F):k},F.state=function(e){return arguments.length?(A=e,F):A},F.defaultState=function(e){return arguments.length?(O=e,F):O},F.noData=function(e){return arguments.length?(_=e,F):_},F.transitionDuration=function(e){return arguments.length?(D=e,F):D},F},e.models.scatterPlusLineChart=function(){"use strict";function B(e){return e.each(function(e){function $(){if(S)return z.select(".nv-point-paths").style("pointer-events","all"),!1;z.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(E).focus(i[0]),p.distortion(E).focus(i[1]),z.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),g&&z.select(".nv-x.nv-axis").call(n),y&&z.select(".nv-y.nv-axis").call(r),z.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),z.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var T=d3.select(this),N=this,C=(f||parseInt(T.style("width"))||960)-a.left-a.right,j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom;B.update=function(){T.transition().duration(M).call(B)},B.container=this,k.disabled=e.map(function(e){return!!e.disabled});if(!L){var F;L={};for(F in k)k[F]instanceof Array?L[F]=k[F].slice(0):L[F]=k[F]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var I=T.selectAll(".nv-noData").data([O]);return I.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),I.attr("x",a.left+C/2).attr("y",a.top+j/2).text(function(e){return e}),B}T.selectAll(".nv-noData").remove(),h=t.xScale(),p=t.yScale(),_=_||h,D=D||p;var q=T.selectAll("g.nv-wrap.nv-scatterChart").data([e]),R=q.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),U=R.append("g"),z=q.select("g");U.append("rect").attr("class","nvd3 nv-background").style("pointer-events","none"),U.append("g").attr("class","nv-x nv-axis"),U.append("g").attr("class","nv-y nv-axis"),U.append("g").attr("class","nv-scatterWrap"),U.append("g").attr("class","nv-regressionLinesWrap"),U.append("g").attr("class","nv-distWrap"),U.append("g").attr("class","nv-legendWrap"),U.append("g").attr("class","nv-controlsWrap"),q.attr("transform","translate("+a.left+","+a.top+")"),b&&z.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),m&&(i.width(C/2),q.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom),q.select(".nv-legendWrap").attr("transform","translate("+C/2+","+ -a.top+")")),w&&(s.width(180).color(["#444"]),z.select(".nv-controlsWrap").datum(H).attr("transform","translate(0,"+ -a.top+")").call(s)),t.width(C).height(j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),q.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),q.select(".nv-regressionLinesWrap").attr("clip-path","url(#nv-edge-clip-"+t.id()+")");var W=q.select(".nv-regressionLinesWrap").selectAll(".nv-regLines").data(function(e){return e});W.enter().append("g").attr("class","nv-regLines");var X=W.selectAll(".nv-regLine").data(function(e){return[e]}),V=X.enter().append("line").attr("class","nv-regLine").style("stroke-opacity",0);X.transition().attr("x1",h.range()[0]).attr("x2",h.range()[1]).attr("y1",function(e,t){return p(h.domain()[0]*e.slope+e.intercept)}).attr("y2",function(e,t){return p(h.domain()[1]*e.slope+e.intercept)}).style("stroke",function(e,t,n){return c(e,n)}).style("stroke-opacity",function(e,t){return e.disabled||typeof e.slope=="undefined"||typeof e.intercept=="undefined"?0:1}),g&&(n.scale(h).ticks(n.ticks()?n.ticks():C/100).tickSize(-j,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),y&&(r.scale(p).ticks(r.ticks()?r.ticks():j/36).tickSize(-C,0),z.select(".nv-y.nv-axis").call(r)),d&&(o.getData(t.x()).scale(h).width(C).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),z.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),v&&(u.getData(t.y()).scale(p).width( +j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),z.select(".nv-distributionY").attr("transform","translate("+(b?C:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(z.select(".nv-background").attr("width",C).attr("height",j),z.select(".nv-background").on("mousemove",$),z.select(".nv-background").on("click",function(){S=!S}),t.dispatch.on("elementClick.freezeFisheye",function(){S=!S})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,E=e.disabled?0:2.5,z.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),z.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(E).focus(0),p.distortion(E).focus(0),z.select(".nv-scatterWrap").call(t),z.select(".nv-x.nv-axis").call(n),z.select(".nv-y.nv-axis").call(r)):S=!1,B.update()}),i.dispatch.on("stateChange",function(e){k=e,A.stateChange(k),B.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",e.pos[1]-j),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],A.tooltipShow(e)}),A.on("tooltipShow",function(e){x&&P(e,N.parentNode)}),A.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),k.disabled=t.disabled),B.update()}),_=h.copy(),D=p.copy()}),B}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=!1,v=!1,m=!0,g=!0,y=!0,b=!1,w=!!d3.fisheye,E=0,S=!1,x=!0,T=function(e,t,n){return""+t+""},N=function(e,t,n){return""+n+""},C=function(e,t,n,r){return"

    "+e+"

    "+"

    "+r+"

    "},k={},L=null,A=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),O="No Data Available.",M=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(b?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var _,D,P=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));T!=null&&e.tooltip.show([f,l],T(i.series.key,v,m,i,B),"n",1,s,"x-nvtooltip"),N!=null&&e.tooltip.show([c,d],N(i.series.key,v,m,i,B),"e",1,s,"y-nvtooltip"),C!=null&&e.tooltip.show([o,u],C(i.series.key,v,m,i.point.tooltip,i,B),i.value<0?"n":"s",null,s)},H=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){A.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),A.on("tooltipHide",function(){x&&e.tooltip.cleanup()}),B.dispatch=A,B.scatter=t,B.legend=i,B.controls=s,B.xAxis=n,B.yAxis=r,B.distX=o,B.distY=u,d3.rebind(B,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),B.options=e.utils.optionsFunc.bind(B),B.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,B):a},B.width=function(e){return arguments.length?(f=e,B):f},B.height=function(e){return arguments.length?(l=e,B):l},B.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),B):c},B.showDistX=function(e){return arguments.length?(d=e,B):d},B.showDistY=function(e){return arguments.length?(v=e,B):v},B.showControls=function(e){return arguments.length?(w=e,B):w},B.showLegend=function(e){return arguments.length?(m=e,B):m},B.showXAxis=function(e){return arguments.length?(g=e,B):g},B.showYAxis=function(e){return arguments.length?(y=e,B):y},B.rightAlignYAxis=function(e){return arguments.length?(b=e,r.orient(e?"right":"left"),B):b},B.fisheye=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(x=e,B):x},B.tooltipContent=function(e){return arguments.length?(C=e,B):C},B.tooltipXContent=function(e){return arguments.length?(T=e,B):T},B.tooltipYContent=function(e){return arguments.length?(N=e,B):N},B.state=function(e){return arguments.length?(k=e,B):k},B.defaultState=function(e){return arguments.length?(L=e,B):L},B.noData=function(e){return arguments.length?(O=e,B):O},B.transitionDuration=function(e){return arguments.length?(M=e,B):M},B},e.models.sparkline=function(){"use strict";function d(e){return e.each(function(e){var i=n-t.left-t.right,d=r-t.top-t.bottom,v=d3.select(this);s.domain(l||d3.extent(e,u)).range(h||[0,i]),o.domain(c||d3.extent(e,a)).range(p||[d,0]);var m=v.selectAll("g.nv-wrap.nv-sparkline").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline"),b=g.append("g"),w=m.select("g");m.attr("transform","translate("+t.left+","+t.top+")");var E=m.selectAll("path").data(function(e){return[e]});E.enter().append("path"),E.exit().remove(),E.style("stroke",function(e,t){return e.color||f(e,t)}).attr("d",d3.svg.line().x(function(e,t){return s(u(e,t))}).y(function(e,t){return o(a(e,t))}));var S=m.selectAll("circle.nv-point").data(function(e){function n(t){if(t!=-1){var n=e[t];return n.pointIndex=t,n}return null}var t=e.map(function(e,t){return a(e,t)}),r=n(t.lastIndexOf(o.domain()[1])),i=n(t.indexOf(o.domain()[0])),s=n(t.length-1);return[i,r,s].filter(function(e){return e!=null})});S.enter().append("circle"),S.exit().remove(),S.attr("cx",function(e,t){return s(u(e,e.pointIndex))}).attr("cy",function(e,t){return o(a(e,e.pointIndex))}).attr("r",2).attr("class",function(e,t){return u(e,e.pointIndex)==s.domain()[1]?"nv-point nv-currentValue":a(e,e.pointIndex)==o.domain()[0]?"nv-point nv-minValue":"nv-point nv-maxValue"})}),d}var t={top:2,right:0,bottom:2,left:0},n=400,r=32,i=!0,s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=e.utils.getColor(["#000"]),l,c,h,p;return d.options=e.utils.optionsFunc.bind(d),d.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,d):t},d.width=function(e){return arguments.length?(n=e,d):n},d.height=function(e){return arguments.length?(r=e,d):r},d.x=function(e){return arguments.length?(u=d3.functor(e),d):u},d.y=function(e){return arguments.length?(a=d3.functor(e),d):a},d.xScale=function(e){return arguments.length?(s=e,d):s},d.yScale=function(e){return arguments.length?(o=e,d):o},d.xDomain=function(e){return arguments.length?(l=e,d):l},d.yDomain=function(e){return arguments.length?(c=e,d):c},d.xRange=function(e){return arguments.length?(h=e,d):h},d.yRange=function(e){return arguments.length?(p=e,d):p},d.animate=function(e){return arguments.length?(i=e,d):i},d.color=function(t){return arguments.length?(f=e.utils.getColor(t),d):f},d},e.models.sparklinePlus=function(){"use strict";function v(e){return e.each(function(c){function O(){if(a)return;var e=C.selectAll(".nv-hoverValue").data(u),r=e.enter().append("g").attr("class","nv-hoverValue").style("stroke-opacity",0).style("fill-opacity",0);e.exit().transition().duration(250).style("stroke-opacity",0).style("fill-opacity",0).remove(),e.attr("transform",function(e){return"translate("+s(t.x()(c[e],e))+",0)"}).transition().duration(250).style("stroke-opacity",1).style("fill-opacity",1);if(!u.length)return;r.append("line").attr("x1",0).attr("y1",-n.top).attr("x2",0).attr("y2",b),r.append("text").attr("class","nv-xValue").attr("x",-6).attr("y",-n.top).attr("text-anchor","end").attr("dy",".9em"),C.select(".nv-hoverValue .nv-xValue").text(f(t.x()(c[u[0]],u[0]))),r.append("text").attr("class","nv-yValue").attr("x",6).attr("y",-n.top).attr("text-anchor","start").attr("dy",".9em"),C.select(".nv-hoverValue .nv-yValue").text(l(t.y()(c[u[0]],u[0])))}function M(){function r(e,n){var r=Math.abs(t.x()(e[0],0)-n),i=0;for(var s=0;s2){var h=M.yScale().invert(i.mouseY),p=Infinity,d=null;c.forEach(function(e,t){h=Math.abs(h);var n=Math.abs(e.stackedValue.y0),r=Math.abs(e.stackedValue.y);if(h>=n&&h<=r+n){d=t;return}}),d!=null&&(c[d].highlight=!0)}var v=n.tickFormat()(M.x()(s,a)),m=t.style()=="expand"?function(e,t){return d3.format(".1%")(e)}:function(e,t){return r.tickFormat()(e)};o.tooltip.position({left:f+u.left,top:i.mouseY+u.top}).chartContainer(D.parentNode).enabled(g).valueFormatter(m).data({value:v,series:c})(),o.renderGuideLine(f)}),o.dispatch.on("elementMouseout",function(e){N.tooltipHide(),t.clearHighlights()}),N.on("tooltipShow",function(e){g&&O(e,D.parentNode)}),N.on("changeState",function(e){typeof e.disabled!="undefined"&&y.length===e.disabled.length&&(y.forEach(function(t,n){t.disabled=e.disabled[n]}),S.disabled=e.disabled),typeof e.style!="undefined"&&t.style(e.style),M.update()})}),M}var t=e.models.stackedArea(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:25,bottom:50,left:60},a=null,f=null,l=e.utils.defaultColor(),c=!0,h=!0,p=!0,d=!0,v=!1,m=!1,g=!0,y=function(e,t,n,r,i){return"

    "+e+"

    "+"

    "+n+" on "+t+"

    "},b,w,E=d3.format(",.2f"),S={style:t.style()},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=250,k=["Stacked","Stream","Expanded"],L={},A=250;n.orient("bottom").tickPadding(7),r.orient(v?"right":"left"),s.updateState(!1);var O=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=y(i.series.key,a,f,i,M);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("tooltipShow",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],N.tooltipShow(e)}),t.dispatch.on("tooltipHide",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){g&&e.tooltip.cleanup()}),M.dispatch=N,M.stacked=t,M.legend=i,M.controls=s,M.xAxis=n,M.yAxis=r,M.interactiveLayer=o,d3.rebind(M,t,"x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","sizeDomain","interactive","useVoronoi","offset","order","style","clipEdge","forceX","forceY","forceSize","interpolate"),M.options=e.utils.optionsFunc.bind(M),M.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,M):u},M.width=function(e){return arguments.length?(a=e,M):a},M.height=function(e){return arguments.length?(f=e,M):f},M.color=function(n){return arguments.length?(l=e.utils.getColor(n),i.color(l),t.color(l),M):l},M.showControls=function(e){return arguments.length?(c=e,M):c},M.showLegend=function(e){return arguments.length?(h=e,M):h},M.showXAxis=function(e){return arguments.length?(p=e,M):p},M.showYAxis=function(e){return arguments.length?(d=e,M):d},M.rightAlignYAxis=function(e){return arguments.length?(v=e,r.orient(e?"right":"left"),M):v},M.useInteractiveGuideline=function(e){return arguments.length?(m=e,e===!0&&(M.interactive(!1),M.useVoronoi(!1)),M):m},M.tooltip=function(e){return arguments.length?(y=e,M):y},M.tooltips=function(e){return arguments.length?(g=e,M):g},M.tooltipContent=function(e){return arguments.length?(y=e,M):y},M.state=function(e){return arguments.length?(S=e,M):S},M.defaultState=function(e){return arguments.length?(x=e,M):x},M.noData=function(e){return arguments.length?(T=e,M):T},M.transitionDuration=function(e){return arguments.length?(A=e,M):A},M.controlsData=function(e){return arguments.length?(k=e,M):k},M.controlLabels=function(e){return arguments.length?typeof e!="object"?L:(L=e,M):L},r.setTickFormat=r.tickFormat,r.tickFormat=function(e){return arguments.length?(E=e,r):E},M}})(); \ No newline at end of file diff --git a/website/static/stats.js b/website/static/stats.js new file mode 100644 index 0000000..4d5b885 --- /dev/null +++ b/website/static/stats.js @@ -0,0 +1,150 @@ +var poolWorkerData = []; +var poolHashrateData = []; +var poolBlockData = []; + +var poolWorkerChart; +var poolHashrateChart; +var poolBlockChart; + +function buildChartData(data){ + + var pools = {}; + + for (var i = 0; i < data.length; i++){ + var time = data[i].time * 1000; + for (var pool in data[i].pools){ + var a = pools[pool] = (pools[pool] || { + hashrate: [], + workers: [], + blocks: [] + }); + a.hashrate.push([time, data[i].pools[pool].hashrate || 0]); + a.workers.push([time, data[i].pools[pool].workers || 0]); + a.blocks.push([time, data[i].pools[pool].blocks.pending]) + } + } + + for (var pool in pools){ + poolWorkerData.push({ + key: pool, + values: pools[pool].workers + }); + poolHashrateData.push({ + key: pool, + values: pools[pool].hashrate + }); + poolBlockData.push({ + key: pool, + values: pools[pool].blocks + }) + } +} + +function getReadableHashRateString(hashrate){ + var i = -1; + var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ]; + do { + hashrate = hashrate / 1024; + i++; + } while (hashrate > 1024); + return hashrate.toFixed(2) + byteUnits[i]; +} + +function displayCharts(){ + + nv.addGraph(function() { + poolWorkerChart = nv.models.stackedAreaChart() + .x(function(d){ return d[0] }) + .y(function(d){ return d[1] }) + .useInteractiveGuideline(true) + .clipEdge(true); + + poolWorkerChart.xAxis.tickFormat(function(d) { + return d3.time.format('%X')(new Date(d)) + }); + + poolWorkerChart.yAxis.tickFormat(d3.format('d')); + + d3.select('#poolWorkers').datum(poolWorkerData).call(poolWorkerChart); + + return poolWorkerChart; + }); + + + nv.addGraph(function() { + poolHashrateChart = nv.models.lineChart() + .x(function(d){ return d[0] }) + .y(function(d){ return d[1] }) + .useInteractiveGuideline(true); + + poolHashrateChart.xAxis.tickFormat(function(d) { + return d3.time.format('%X')(new Date(d)) + }); + + poolHashrateChart.yAxis.tickFormat(function(d){ + return getReadableHashRateString(d); + }); + + d3.select('#poolHashrate').datum(poolHashrateData).call(poolHashrateChart); + + return poolHashrateChart; + }); + + + nv.addGraph(function() { + poolBlockChart = nv.models.multiBarChart() + .x(function(d){ return d[0] }) + .y(function(d){ return d[1] }); + + poolBlockChart.xAxis.tickFormat(function(d) { + return d3.time.format('%X')(new Date(d)) + }); + + d3.select('#poolBlocks').datum(poolBlockData).call(poolBlockChart); + + return poolBlockChart; + }); +} + +function TriggerChartUpdates(){ + poolWorkerChart.update(); + poolHashrateChart.update(); + poolBlockChart.update(); +} + +nv.utils.windowResize(TriggerChartUpdates); + +$.getJSON('/api/pool_stats', function(data){ + buildChartData(data); + displayCharts(); +}); + +statsSource.addEventListener('message', function(e){ + var stats = JSON.parse(e.data); + var time = stats.time * 1000; + for (var pool in stats.pools){ + for (var i = 0; i < poolWorkerData.length; i++){ + if (poolWorkerData[i].key === pool){ + poolWorkerData[i].values.shift(); + poolWorkerData[i].values.push([time, stats.pools[pool].workerCount]); + break; + } + } + for (var i = 0; i < poolHashrateData.length; i++){ + if (poolHashrateData[i].key === pool){ + poolHashrateData[i].values.shift(); + poolHashrateData[i].values.push([time, stats.pools[pool].hashrate]); + break; + } + } + for (var i = 0; i < poolBlockData.length; i++){ + if (poolBlockData[i].key === pool){ + poolBlockData[i].values.shift(); + poolBlockData[i].values.push([time, stats.pools[pool].blocks.pending]); + break; + } + } + } + + TriggerChartUpdates(); +}); \ No newline at end of file diff --git a/website/static/style.css b/website/static/style.css index 01c4c43..7133db2 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -1,121 +1,67 @@ +html, button, input, select, textarea, .pure-g [class *= "pure-u"], .pure-g-r [class *= "pure-u"]{ + font-family: 'Open Sans', sans-serif; +} + html{ - height: 100%; + background: #2d2d2d; } + body{ - min-height: 100vh; - overflow: hidden; - - display: -moz-box; - display: -webkit-flexbox; - display: -ms-flexbox; - display: -webkit-flex; - display: -moz-flex; - display: flex; - -webkit-flex-direction: column; - -moz-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - - -} -header{ - background-color: #f3f2ef; -} - -header > div{ - display: -moz-box; - display: -webkit-flexbox; - display: -ms-flexbox; - display: -webkit-flex; - display: -moz-flex; - display: flex; -} -header > div > ul{ - flex: 1; -} - - -header .selected, header a:active, header a:focus{ - background-color: #404040 !important; -} - -.selected > a { - color: white !important; -} - -header a { - line-height: 2.5em !important; - font-size: 1.1em !important; -} -header a:hover { - background-color: #4d4d4d !important; - color: white !important; -} - - -.stats{ display: flex; flex-direction: column; + max-width: 1160px; + margin: 0 auto; +} + +header > .home-menu{ + background: inherit !important; + height: 54px; + display: flex; +} + +header > .home-menu > a.pure-menu-heading, header > .home-menu > ul, header > .home-menu > ul > li{ + display: flex !important; + align-items: center; justify-content: center; + line-height: normal !important; } -.stats > div{ - padding: 2px 15px; - border-radius: 4px; - color: #f2f2f2; - font-size: 0.95em; - width: 120px; - text-align: center; +header > .home-menu > a.pure-menu-heading{ + color: white; + font-size: 1.5em; } -.stats > div:nth-child(1){ - background-color: #71a380; - margin-bottom: 4px; -} -.stats > div:nth-child(2){ - background-color: #7196a3; +header > .home-menu > ul > li > a{ + color: #ced4d9; } -.pure-menu-heading{ - padding-left: 0 !important; +header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li > a:focus{ + background: inherit !important; } -header > div{ - background-color: transparent !important; - margin-left: auto; - margin-right: auto; +header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li.pure-menu-selected > a{ + color: white; } -main > div, header > div{ - max-width: 800px; +main{ + background-color: #ebf4fa; + position: relative; } -main { - flex: 1 1 auto; - padding: 20px 0; -} - -main > div{ - margin-left: auto; - margin-right: auto; -} - - footer{ text-align: center; color: #b3b3b3; - background-color: #404040; text-decoration: none; font-size: 0.8em; padding: 15px; line-height: 24px; } + footer a{ color: #fff; text-decoration: none; } - footer iframe{ vertical-align: middle; } \ No newline at end of file From be37d9c0ee21f986d6db043c3630cdaf2d9b43aa Mon Sep 17 00:00:00 2001 From: Alejandro Reyero Date: Fri, 11 Apr 2014 11:32:33 +0200 Subject: [PATCH 112/150] Update blocknotify.c Changed sendto to send using TCP so we can control if the block notify produces a connection error. Added a note about "localhost" usage an possible error 13. --- scripts/blocknotify.c | 50 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/scripts/blocknotify.c b/scripts/blocknotify.c index 74c455a..c0622bd 100644 --- a/scripts/blocknotify.c +++ b/scripts/blocknotify.c @@ -9,6 +9,7 @@ /* Contributed by Alex Petrov aka SysMan at sysman.net +Updated by Alejandro Reyero - TodoJuegos.com Part of NOMP project Simple lightweight & fast - a more efficient block notify script in pure C. @@ -22,8 +23,9 @@ Build with: Usage in daemon coin.conf - blocknotify="/bin/blocknotify localhost:8117 mySuperSecurePassword dogecoin %s" + blocknotify="/bin/blocknotify 127.0.01:8117 mySuperSecurePassword dogecoin %s" +*NOTE* If you use "localhost" as hostname you may get a "13" error (socket / connect / send may consider "localhost" as a broadcast address) // {"password":"notepas","coin":"Xcoin","hash":"d2191a8b644c9cd903439edf1d89ee060e196b3e116e0d48a3f11e5e3987a03b"} // simplest connect + send json string to server @@ -49,37 +51,41 @@ int main(int argc, char **argv) exit(1); } - strncpy(host,argv[1],(sizeof(host)-1)); - p=host; + strncpy(host,argv[1],(sizeof(host)-1)); + p=host; -if ( (arg=strchr(p,':')) ) - { *arg='\0'; + if ( (arg=strchr(p,':')) ) + { + *arg='\0'; - errno=0; // reset errno - port=strtol(++arg,&errptr,10); + errno=0; // reset errno + port=strtol(++arg,&errptr,10); -if ( (errno != 0) || (errptr == arg) ) { fprintf(stderr, "port number fail [%s]\n",errptr); } -// if(strlen(arg) > (errptr-arg) ) also fail, but we ignore it for now -// printf("host %s:%d\n",host,port); -} + if ( (errno != 0) || (errptr == arg) ) { fprintf(stderr, "port number fail [%s]\n",errptr); } + // if(strlen(arg) > (errptr-arg) ) also fail, but we ignore it for now + // printf("host %s:%d\n",host,port); + } -// printf("pass: %s coin: %s block:[%s]\n",argv[2],argv[3],argv[4]); -snprintf(sendline,sizeof(sendline)-1, - "{\"password\":\"%s\",\"coin\":\"%s\",\"hash\":\"%s\"}\n", - argv[2], argv[3], argv[4]); + // printf("pass: %s coin: %s block:[%s]\n",argv[2],argv[3],argv[4]); + snprintf(sendline,sizeof(sendline)-1, + "{\"password\":\"%s\",\"coin\":\"%s\",\"hash\":\"%s\"}\n", + argv[2], argv[3], argv[4]); -// printf("sendline:[%s]",sendline); - - sockfd=socket(AF_INET,SOCK_STREAM,0); + // printf("sendline:[%s]",sendline); + sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr=inet_addr(host); servaddr.sin_port=htons(port); - connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); - sendto(sockfd,sendline,strlen(sendline),0, - (struct sockaddr *)&servaddr,sizeof(servaddr)); -exit(0); + int result = send(sockfd,sendline,strlen(sendline),0); + close(sockfd); + + if(result == -1) { + printf("Error sending: %i\n",errno); + exit(-1); + } + exit(0); } From 7fdfea8abff482b809848b41314370faed0b1884 Mon Sep 17 00:00:00 2001 From: Alejandro Reyero Date: Fri, 11 Apr 2014 14:23:11 +0200 Subject: [PATCH 113/150] Update blocknotify.c --- scripts/blocknotify.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/blocknotify.c b/scripts/blocknotify.c index c0622bd..78c9db7 100644 --- a/scripts/blocknotify.c +++ b/scripts/blocknotify.c @@ -23,7 +23,7 @@ Build with: Usage in daemon coin.conf - blocknotify="/bin/blocknotify 127.0.01:8117 mySuperSecurePassword dogecoin %s" + blocknotify="/bin/blocknotify 127.0.0.1:8117 mySuperSecurePassword dogecoin %s" *NOTE* If you use "localhost" as hostname you may get a "13" error (socket / connect / send may consider "localhost" as a broadcast address) From b9891c86240023b332570984fb0c4bb598a5c1dc Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 11 Apr 2014 13:24:12 +0000 Subject: [PATCH 114/150] fix proxy state loading bug when key not present --- libs/poolWorker.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 4855507..3dbfe86 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -210,11 +210,15 @@ module.exports = function(logger){ redisClient.on('ready', function(){ redisClient.hgetall("proxyState", function(error, obj) { if (error) { + proxyState = {}; logger.debug(logSystem, logComponent, logSubCat, 'No last proxy state found in redis'); } else { - proxyState = obj; - logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); + if (obj != null) + { + proxyState = obj; + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); + } } // From 5f6b09a877f689bea16bbdea2972da36619453ab Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 11 Apr 2014 13:26:42 +0000 Subject: [PATCH 115/150] fix proxy state loading bug when key not present --- libs/poolWorker.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 3dbfe86..71da314 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -209,16 +209,12 @@ module.exports = function(logger){ var redisClient = redis.createClient(6379, "localhost") redisClient.on('ready', function(){ redisClient.hgetall("proxyState", function(error, obj) { - if (error) { - proxyState = {}; + if (error || obj == null) { logger.debug(logSystem, logComponent, logSubCat, 'No last proxy state found in redis'); } else { - if (obj != null) - { - proxyState = obj; - logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); - } + proxyState = obj; + logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis'); } // From cf74cd41196a1f324778a8bf970eb5ec2a4eceb0 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 11 Apr 2014 17:26:45 -0600 Subject: [PATCH 116/150] Fixed stat page erroring when coins were added. Changed default stat update interval to 60 seconds. --- config_example.json | 2 +- libs/api.js | 3 +- libs/stats.js | 12 +--- website/pages/stats.html | 32 +++++---- website/static/stats.js | 138 ++++++++++++++++++++++++++------------- 5 files changed, 119 insertions(+), 68 deletions(-) diff --git a/config_example.json b/config_example.json index 8759fcf..8cd417c 100644 --- a/config_example.json +++ b/config_example.json @@ -11,7 +11,7 @@ "port": 80, "stratumHost": "cryppit.com", "stats": { - "updateInterval": 15, + "updateInterval": 60, "historicalRetention": 43200, "hashrateWindow": 300, "redis": { diff --git a/libs/api.js b/libs/api.js index 53ebd70..3a433ab 100644 --- a/libs/api.js +++ b/libs/api.js @@ -18,8 +18,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ res.end(portalStats.statsString); return; case 'pool_stats': - res.writeHead(200, {'content-encoding': 'gzip'}); - res.end(portalStats.statPoolHistoryBuffer); + res.end(JSON.stringify(portalStats.statPoolHistory)); return; case 'live_stats': res.writeHead(200, { diff --git a/libs/stats.js b/libs/stats.js index 459ff14..62b7f00 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -20,7 +20,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ this.statHistory = []; this.statPoolHistory = []; - this.statPoolHistoryBuffer; this.stats = {}; this.statsString = ''; @@ -84,7 +83,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ _this.statHistory.forEach(function(stats){ addStatPoolHistory(stats); }); - deflateStatPoolHistory(); }); } @@ -96,7 +94,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ for (var pool in stats.pools){ data.pools[pool] = { hashrate: stats.pools[pool].hashrate, - workers: stats.pools[pool].workerCount, + workerCount: stats.pools[pool].workerCount, blocks: stats.pools[pool].blocks } } @@ -104,11 +102,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ } - function deflateStatPoolHistory(){ - zlib.gzip(JSON.stringify(_this.statPoolHistory), function(err, buffer){ - _this.statPoolHistoryBuffer = buffer; - }); - } + this.getGlobalStats = function(callback){ @@ -245,8 +239,6 @@ module.exports = function(logger, portalConfig, poolConfigs){ } } - deflateStatPoolHistory(); - redisStats.multi([ ['zadd', 'statHistory', statGatherTime, _this.statsString], ['zremrangebyscore', 'statHistory', '-inf', '(' + retentionTime] diff --git a/website/pages/stats.html b/website/pages/stats.html index d7193b3..435a28f 100644 --- a/website/pages/stats.html +++ b/website/pages/stats.html @@ -4,11 +4,18 @@ padding: 18px; } - #topCharts > div > svg{ + #topCharts > div > div > svg{ display: block; height: 280px; } + .chartWrapper{ + border: solid 1px #c7c7c7; + border-radius: 5px; + padding: 5px; + margin-bottom: 18px; + } + .chartLabel{ font-size: 1.2em; text-align: center; @@ -16,24 +23,27 @@ } .chartHolder{ - border: solid 1px #c7c7c7; - border-radius: 5px; - padding: 5px; - margin-bottom: 18px; + }
    -
    Workers Per Pool
    -
    +
    +
    Workers Per Pool
    +
    +
    -
    Hashrate Per Pool
    -
    +
    +
    Hashrate Per Pool
    +
    +
    -
    Blocks Pending Per Pool
    -
    +
    +
    Blocks Pending Per Pool
    +
    +
    diff --git a/website/static/stats.js b/website/static/stats.js index 4d5b885..10e43d6 100644 --- a/website/static/stats.js +++ b/website/static/stats.js @@ -1,29 +1,57 @@ -var poolWorkerData = []; -var poolHashrateData = []; -var poolBlockData = []; +var poolWorkerData; +var poolHashrateData; +var poolBlockData; var poolWorkerChart; var poolHashrateChart; var poolBlockChart; -function buildChartData(data){ +var statData; +var poolKeys; + +function buildChartData(){ var pools = {}; - for (var i = 0; i < data.length; i++){ - var time = data[i].time * 1000; - for (var pool in data[i].pools){ - var a = pools[pool] = (pools[pool] || { + poolKeys = []; + for (var i = 0; i < statData.length; i++){ + for (var pool in statData[i].pools){ + if (poolKeys.indexOf(pool) === -1) + poolKeys.push(pool); + } + } + + + for (var i = 0; i < statData.length; i++){ + + var time = statData[i].time * 1000; + + for (var f = 0; f < poolKeys.length; f++){ + var pName = poolKeys[f]; + var a = pools[pName] = (pools[pName] || { hashrate: [], workers: [], blocks: [] }); - a.hashrate.push([time, data[i].pools[pool].hashrate || 0]); - a.workers.push([time, data[i].pools[pool].workers || 0]); - a.blocks.push([time, data[i].pools[pool].blocks.pending]) + if (pName in statData[i].pools){ + a.hashrate.push([time, statData[i].pools[pName].hashrate]); + a.workers.push([time, statData[i].pools[pName].workerCount]); + a.blocks.push([time, statData[i].pools[pName].blocks.pending]) + } + else{ + a.hashrate.push([time, 0]); + a.workers.push([time, 0]); + a.blocks.push([time, 0]) + } + } + } + poolWorkerData = []; + poolHashrateData = []; + poolBlockData = []; + for (var pool in pools){ poolWorkerData.push({ key: pool, @@ -47,21 +75,26 @@ function getReadableHashRateString(hashrate){ hashrate = hashrate / 1024; i++; } while (hashrate > 1024); - return hashrate.toFixed(2) + byteUnits[i]; + return Math.round(hashrate) + byteUnits[i]; +} + +function timeOfDayFormat(timestamp){ + var dStr = d3.time.format('%I:%M %p')(new Date(timestamp)); + if (dStr.indexOf('0') === 0) dStr = dStr.slice(1); + return dStr; } function displayCharts(){ nv.addGraph(function() { poolWorkerChart = nv.models.stackedAreaChart() + .margin({left: 40, right: 40}) .x(function(d){ return d[0] }) .y(function(d){ return d[1] }) .useInteractiveGuideline(true) .clipEdge(true); - poolWorkerChart.xAxis.tickFormat(function(d) { - return d3.time.format('%X')(new Date(d)) - }); + poolWorkerChart.xAxis.tickFormat(timeOfDayFormat); poolWorkerChart.yAxis.tickFormat(d3.format('d')); @@ -73,13 +106,12 @@ function displayCharts(){ nv.addGraph(function() { poolHashrateChart = nv.models.lineChart() + .margin({left: 60, right: 40}) .x(function(d){ return d[0] }) .y(function(d){ return d[1] }) .useInteractiveGuideline(true); - poolHashrateChart.xAxis.tickFormat(function(d) { - return d3.time.format('%X')(new Date(d)) - }); + poolHashrateChart.xAxis.tickFormat(timeOfDayFormat); poolHashrateChart.yAxis.tickFormat(function(d){ return getReadableHashRateString(d); @@ -96,9 +128,7 @@ function displayCharts(){ .x(function(d){ return d[0] }) .y(function(d){ return d[1] }); - poolBlockChart.xAxis.tickFormat(function(d) { - return d3.time.format('%X')(new Date(d)) - }); + poolBlockChart.xAxis.tickFormat(timeOfDayFormat); d3.select('#poolBlocks').datum(poolBlockData).call(poolBlockChart); @@ -115,36 +145,56 @@ function TriggerChartUpdates(){ nv.utils.windowResize(TriggerChartUpdates); $.getJSON('/api/pool_stats', function(data){ - buildChartData(data); + statData = data; + buildChartData(); displayCharts(); }); statsSource.addEventListener('message', function(e){ var stats = JSON.parse(e.data); - var time = stats.time * 1000; - for (var pool in stats.pools){ - for (var i = 0; i < poolWorkerData.length; i++){ - if (poolWorkerData[i].key === pool){ - poolWorkerData[i].values.shift(); - poolWorkerData[i].values.push([time, stats.pools[pool].workerCount]); - break; - } - } - for (var i = 0; i < poolHashrateData.length; i++){ - if (poolHashrateData[i].key === pool){ - poolHashrateData[i].values.shift(); - poolHashrateData[i].values.push([time, stats.pools[pool].hashrate]); - break; - } - } - for (var i = 0; i < poolBlockData.length; i++){ - if (poolBlockData[i].key === pool){ - poolBlockData[i].values.shift(); - poolBlockData[i].values.push([time, stats.pools[pool].blocks.pending]); - break; + statData.push(stats); + + + var newPoolAdded = (function(){ + for (var p in stats.pools){ + if (poolKeys.indexOf(p) === -1) + return true; + } + return false; + })(); + + if (newPoolAdded || Object.keys(stats.pools).length > poolKeys.length){ + buildChartData(); + displayCharts(); + } + else { + var time = stats.time * 1000; + for (var f = 0; f < poolKeys.length; f++) { + var pool = poolKeys[f]; + for (var i = 0; i < poolWorkerData.length; i++) { + if (poolWorkerData[i].key === pool) { + poolWorkerData[i].values.shift(); + poolWorkerData[i].values.push([time, pool in stats.pools ? stats.pools[pool].workerCount : 0]); + break; + } + } + for (var i = 0; i < poolHashrateData.length; i++) { + if (poolHashrateData[i].key === pool) { + poolHashrateData[i].values.shift(); + poolHashrateData[i].values.push([time, pool in stats.pools ? stats.pools[pool].hashrate : 0]); + break; + } + } + for (var i = 0; i < poolBlockData.length; i++) { + if (poolBlockData[i].key === pool) { + poolBlockData[i].values.shift(); + poolBlockData[i].values.push([time, pool in stats.pools ? stats.pools[pool].blocks.pending : 0]); + break; + } } } + TriggerChartUpdates(); } - TriggerChartUpdates(); + }); \ No newline at end of file From 2c7c1a19c00b9adcdc5e1f408e1a00e1f7655599 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 11 Apr 2014 17:38:40 -0600 Subject: [PATCH 117/150] Integers only for yaxis on blocks chart --- website/static/stats.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/static/stats.js b/website/static/stats.js index 10e43d6..843720f 100644 --- a/website/static/stats.js +++ b/website/static/stats.js @@ -130,6 +130,8 @@ function displayCharts(){ poolBlockChart.xAxis.tickFormat(timeOfDayFormat); + poolBlockChart.yAxis.tickFormat(d3.format('d')); + d3.select('#poolBlocks').datum(poolBlockData).call(poolBlockChart); return poolBlockChart; From da22dad4825a808e06dcd07747e27d0bab4f994a Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 12 Apr 2014 16:06:32 -0600 Subject: [PATCH 118/150] Added coin-switching proxy ports to getting started menu. Fixed bug with coin stats on home page not live-updating. Switched zepto to jquery --- website/index.html | 2 +- website/pages/getting_started.html | 68 ++++++++++++++++++++++++------ website/pages/home.html | 25 ++++++++++- website/static/main.js | 8 ---- website/static/style.css | 1 + 5 files changed, 79 insertions(+), 25 deletions(-) diff --git a/website/index.html b/website/index.html index e613e65..0542569 100644 --- a/website/index.html +++ b/website/index.html @@ -12,7 +12,7 @@ - + diff --git a/website/pages/getting_started.html b/website/pages/getting_started.html index 37e8dad..c624952 100644 --- a/website/pages/getting_started.html +++ b/website/pages/getting_started.html @@ -17,26 +17,31 @@ min-width: 170px; } - #menu > div:first-child{ + #menu > .menuHeader{ color: #e3f7ff; border-bottom: 1px solid #7f878b; font-size: 1.2em; - padding: 16px 0 4px 15px; + padding: 16px 16px 4px 15px; } - #coinList{ - padding: 20px; + .menuList{ transition-duration: 200ms; } - #coinList > a{ + .menuList > a:first-child{ + margin-top: 10px; + } + + .menuList > a{ display: block; color: #e3f7ff; text-decoration: none; - padding: 5px; + padding: 7px; + padding-left: 25px; + text-transform: capitalize; } - #coinList > a:hover{ + .menuList > a:hover{ color: #f69b3a; } @@ -125,6 +130,9 @@ position: absolute; background-color: #f06350; } + #coinInfo .coinInfoName{ + text-transform: capitalize; + } #coinInfo > div:first-of-type{ font-size: 1.8em; text-align: center; @@ -175,11 +183,40 @@
    -
    \ No newline at end of file +
    + + \ No newline at end of file diff --git a/website/static/main.js b/website/static/main.js index 23a88ef..3f8a05c 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -1,4 +1,3 @@ - $(function(){ var hotSwap = function(page, pushSate){ @@ -27,12 +26,5 @@ $(function(){ }); window.statsSource = new EventSource("/api/live_stats"); - statsSource.addEventListener('message', function(e){ - var stats = JSON.parse(e.data); - for (algo in stats.algos) { - $('#statsMiners'+algo).text(stats.algos[algo].workers); - $('#statsHashrate'+algo).text(stats.algos[algo].hashrateString); - } - }); }); diff --git a/website/static/style.css b/website/static/style.css index 7133db2..4b7b03b 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -4,6 +4,7 @@ html, button, input, select, textarea, .pure-g [class *= "pure-u"], .pure-g-r [c html{ background: #2d2d2d; + overflow-y: scroll; } body{ From 473322c72f2d27fe27e7f9bd173d9f16cf51f7cf Mon Sep 17 00:00:00 2001 From: Matt Culpepper Date: Sat, 12 Apr 2014 23:29:57 -0500 Subject: [PATCH 119/150] handle x11 difficulty for mpos compatibility --- libs/mposCompatibility.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index 26b30a2..bc2ae02 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -66,9 +66,9 @@ module.exports = function(logger, poolConfig){ var dbData = [ shareData.ip, shareData.worker, - isValidShare ? 'Y' : 'N', + isValidShare ? 'Y' : 'N', isValidBlock ? 'Y' : 'N', - shareData.difficulty, + poolConfig.coin.algorithm === 'x11' ? shareData.difficulty * 256 : shareData.difficulty, typeof(shareData.error) === 'undefined' ? null : shareData.error, shareData.blockHash ? shareData.blockHash : (shareData.blockHashInvalid ? shareData.blockHashInvalid : '') ]; From 35ab95608a3ca6346b95ea8872405ecf8d168613 Mon Sep 17 00:00:00 2001 From: Alex Petrov Date: Sun, 13 Apr 2014 08:48:09 -0400 Subject: [PATCH 120/150] new coins trc,zet,bte,uno --- coins/bytecoin.json | 5 +++++ coins/terracoin.json | 5 +++++ coins/unobtanium.json | 5 +++++ coins/zetacoin.json | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 coins/bytecoin.json create mode 100644 coins/terracoin.json create mode 100644 coins/unobtanium.json create mode 100644 coins/zetacoin.json diff --git a/coins/bytecoin.json b/coins/bytecoin.json new file mode 100644 index 0000000..45f1b18 --- /dev/null +++ b/coins/bytecoin.json @@ -0,0 +1,5 @@ +{ + "name": "Bytecoin", + "symbol": "BTE", + "algorithm": "sha256" +} diff --git a/coins/terracoin.json b/coins/terracoin.json new file mode 100644 index 0000000..2a72866 --- /dev/null +++ b/coins/terracoin.json @@ -0,0 +1,5 @@ +{ + "name": "Terracoin", + "symbol": "TRC", + "algorithm": "sha256" +} diff --git a/coins/unobtanium.json b/coins/unobtanium.json new file mode 100644 index 0000000..13b4de8 --- /dev/null +++ b/coins/unobtanium.json @@ -0,0 +1,5 @@ +{ + "name": "Unobtanium", + "symbol": "UNO", + "algorithm": "sha256" +} diff --git a/coins/zetacoin.json b/coins/zetacoin.json new file mode 100644 index 0000000..e7515af --- /dev/null +++ b/coins/zetacoin.json @@ -0,0 +1,5 @@ +{ + "name": "Zetacoin", + "symbol": "ZTC", + "algorithm": "sha256" +} From 0af27316d3abb08b70f1e85ec3bac596dea7af1a Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 16:44:12 +0000 Subject: [PATCH 121/150] profitability data collection from Poloniex --- config_example.json | 8 +- init.js | 40 ++++++-- libs/apiPoloniex.js | 212 ++++++++++++++++++++++++++++++++++++++++ libs/profitSwitch.js | 227 +++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- 5 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 libs/apiPoloniex.js create mode 100644 libs/profitSwitch.js diff --git a/config_example.json b/config_example.json index 8cd417c..d08d140 100644 --- a/config_example.json +++ b/config_example.json @@ -68,10 +68,16 @@ } }, + "profitSwitch": { + "enabled": false, + "updateInterval": 60, + "depth": 0.80 + }, + "redisBlockNotifyListener": { "enabled": false, "redisPort": 6379, "redisHost": "hostname", "psubscribeKey": "newblocks:*" } -} \ No newline at end of file +} diff --git a/init.js b/init.js index 0daf3fd..95f5a99 100644 --- a/init.js +++ b/init.js @@ -12,6 +12,7 @@ var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); var PaymentProcessor = require('./libs/paymentProcessor.js'); var Website = require('./libs/website.js'); +var ProfitSwitch = require('./libs/profitSwitch.js'); var algos = require('stratum-pool/lib/algoProperties.js'); @@ -58,7 +59,7 @@ catch(e){ if (cluster.isWorker){ - + switch(process.env.workerType){ case 'pool': new PoolWorker(logger); @@ -69,6 +70,9 @@ if (cluster.isWorker){ case 'website': new Website(logger); break; + case 'profitSwitch': + new ProfitSwitch(logger); + break; } return; @@ -203,23 +207,19 @@ var startCoinswitchListener = function(portalConfig){ logger.debug('Master', 'Coinswitch', text); }); listener.on('switchcoin', function(message){ - var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash}; Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send(ipcMessage); }); var ipcMessage = { - type:'switch', - coin: message.coin - }; + type:'switch', + coin: message.coin + }; Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send(ipcMessage); }); - }); listener.start(); - - }; var startRedisBlockListener = function(portalConfig){ @@ -287,6 +287,28 @@ var startWebsite = function(portalConfig, poolConfigs){ }; +var startProfitSwitch = function(portalConfig, poolConfigs){ + + if (!portalConfig.profitSwitch.enabled){ + logger.error('Master', 'Profit', 'Profit auto switching disabled'); + return; + } + + var worker = cluster.fork({ + workerType: 'profitSwitch', + pools: JSON.stringify(poolConfigs), + portalConfig: JSON.stringify(portalConfig) + }); + worker.on('exit', function(code, signal){ + logger.error('Master', 'Profit', 'Profit switching process died, spawning replacement...'); + setTimeout(function(){ + startWebsite(portalConfig, poolConfigs); + }, 2000); + }); +}; + + + (function init(){ var poolConfigs = buildPoolConfigs(); @@ -305,4 +327,6 @@ var startWebsite = function(portalConfig, poolConfigs){ startWebsite(portalConfig, poolConfigs); + startProfitSwitch(portalConfig, poolConfigs); + })(); diff --git a/libs/apiPoloniex.js b/libs/apiPoloniex.js new file mode 100644 index 0000000..35f330c --- /dev/null +++ b/libs/apiPoloniex.js @@ -0,0 +1,212 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'http://poloniex.com/public', + PRIVATE_API_URL = 'https://poloniex.com/tradingApi', + USER_AGENT = 'npm-crypto-apis/' + version + + // Constructor + function Poloniex(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Poloniex: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Poloniex.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Poloniex.prototype = { + constructor: Poloniex, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Poloniex.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + command: 'returnTicker' + }; + + return this._public(parameters, callback); + }, + + get24hVolume: function(callback){ + var parameters = { + command: 'return24hVolume' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Poloniex; +}(); diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js new file mode 100644 index 0000000..be743c9 --- /dev/null +++ b/libs/profitSwitch.js @@ -0,0 +1,227 @@ +var async = require('async'); + +var Poloniex = require('./apiPoloniex.js'); + +module.exports = function(logger){ + + var _this = this; + + var portalConfig = JSON.parse(process.env.portalConfig); + var poolConfigs = JSON.parse(process.env.pools); + + var logSystem = 'Profit'; + + // + // build status tracker for collecting coin market information + // + var profitStatus = {}; + var profitSymbols = {}; + Object.keys(poolConfigs).forEach(function(coin){ + + var poolConfig = poolConfigs[coin]; + var algo = poolConfig.coin.algorithm; + + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + avgPrice: { BTC: 0, LTC: 0 }, + avgDepth: { BTC: 0, LTC: 0 }, + avgVolume: { BTC: 0, LTC: 0 }, + prices: {}, + depths: {}, + volumes: {}, + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + profitSymbols[poolConfig.coin.symbol] = algo; + }); + + + // + // ensure we have something to switch + // + var isMoreThanOneCoin = false; + Object.keys(profitStatus).forEach(function(algo){ + if (Object.keys(profitStatus[algo]).length > 1) { + isMoreThanOneCoin = true; + } + }); + if (!isMoreThanOneCoin){ + logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); + return; + } + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); + + + // + // setup APIs + // + var poloApi = new Poloniex( + // 'API_KEY', + // 'API_SECRET' + ); + + // + // market data collection from Poloniex + // + this.getProfitDataPoloniex = function(callback){ + async.series([ + function(taskCallback){ + poloApi.getTicker(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcPrice = new Number(0); + var ltcPrice = new Number(0); + + if (data.hasOwnProperty('BTC_' + symbol)) { + btcPrice = new Number(data['BTC_' + symbol]); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcPrice = new Number(data['LTC_' + symbol]); + } + + if (btcPrice > 0 || ltcPrice > 0) { + var prices = { + BTC: btcPrice, + LTC: ltcPrice + }; + profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + poloApi.get24hVolume(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcVolume = new Number(0); + var ltcVolume = new Number(0); + + if (data.hasOwnProperty('BTC_' + symbol)) { + btcVolume = new Number(data['BTC_' + symbol].BTC); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcVolume = new Number(data['LTC_' + symbol].LTC); + } + + if (btcVolume > 0 || ltcVolume > 0) { + var volumes = { + BTC: btcVolume, + LTC: ltcVolume + }; + profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(profitSymbols).forEach(function(symbol){ + var coinVolumes = profitStatus[profitSymbols[symbol]][symbol].volumes; + var coinPrices = profitStatus[profitSymbols[symbol]][symbol].prices; + + if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ + var btcDepth = new Number(0); + var ltcDepth = new Number(0); + + if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['BTC']); + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('BTC', symbol, coinPrice, callback) + }); + } + if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['LTC']); + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('LTC', symbol, coinPrice, callback) + }); + } + } + }); + + if (depthTasks.length == 0){ + taskCallback; + return; + } + async.parallel(depthTasks, function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ + poloApi.getOrderBook(symbolA, symbolB, function(err, data){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (data.hasOwnProperty('bids')){ + data['bids'].forEach(function(order){ + var price = new Number(order[0]); + var qty = new Number(order[1]); + // only measure the depth down to configured depth + if (price >= coinPrice * portalConfig.profitSwitch.depth){ + depth += (qty * price); + } + }); + } + + if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { + BTC: 0, + LTC: 0 + }; + } + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; + callback(); + }); + }; + + // TODO + this.getProfitDataCryptsy = function(callback){ + callback(null); + }; + + + var checkProfitability = function(){ + logger.debug(logSystem, 'Check', 'Running profitability checks.'); + + async.parallel([ + _this.getProfitDataPoloniex, + _this.getProfitDataCryptsy + ], function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); + }); + }; + setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); + +}; diff --git a/package.json b/package.json index c1a28a3..2140f2c 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,11 @@ "compression": "*", "dot": "*", "colors": "*", - "node-watch": "*" + "node-watch": "*", + "request": "*", + "nonce": "*" }, "engines": { "node": ">=0.10" } -} \ No newline at end of file +} From 550b09767e779d06995268664295316af44fb4b7 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 16:46:06 +0000 Subject: [PATCH 122/150] whitespace formatting --- config_example.json | 8 +- libs/profitSwitch.js | 354 +++++++++++++++++++++---------------------- 2 files changed, 181 insertions(+), 181 deletions(-) diff --git a/config_example.json b/config_example.json index d08d140..0ddaa74 100644 --- a/config_example.json +++ b/config_example.json @@ -68,11 +68,11 @@ } }, - "profitSwitch": { - "enabled": false, + "profitSwitch": { + "enabled": false, "updateInterval": 60, - "depth": 0.80 - }, + "depth": 0.80 + }, "redisBlockNotifyListener": { "enabled": false, diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index be743c9..3de8b9c 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -11,217 +11,217 @@ module.exports = function(logger){ var logSystem = 'Profit'; - // - // build status tracker for collecting coin market information - // - var profitStatus = {}; - var profitSymbols = {}; + // + // build status tracker for collecting coin market information + // + var profitStatus = {}; + var profitSymbols = {}; Object.keys(poolConfigs).forEach(function(coin){ var poolConfig = poolConfigs[coin]; - var algo = poolConfig.coin.algorithm; + var algo = poolConfig.coin.algorithm; - if (!profitStatus.hasOwnProperty(algo)) { - profitStatus[algo] = {}; - } - var coinStatus = { - name: poolConfig.coin.name, - symbol: poolConfig.coin.symbol, - difficulty: 0, - reward: 0, - avgPrice: { BTC: 0, LTC: 0 }, - avgDepth: { BTC: 0, LTC: 0 }, - avgVolume: { BTC: 0, LTC: 0 }, - prices: {}, - depths: {}, - volumes: {}, - }; - profitStatus[algo][poolConfig.coin.symbol] = coinStatus; - profitSymbols[poolConfig.coin.symbol] = algo; - }); + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + avgPrice: { BTC: 0, LTC: 0 }, + avgDepth: { BTC: 0, LTC: 0 }, + avgVolume: { BTC: 0, LTC: 0 }, + prices: {}, + depths: {}, + volumes: {}, + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + profitSymbols[poolConfig.coin.symbol] = algo; + }); - // - // ensure we have something to switch - // - var isMoreThanOneCoin = false; - Object.keys(profitStatus).forEach(function(algo){ - if (Object.keys(profitStatus[algo]).length > 1) { - isMoreThanOneCoin = true; - } - }); - if (!isMoreThanOneCoin){ - logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); - return; - } - logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); - logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); + // + // ensure we have something to switch + // + var isMoreThanOneCoin = false; + Object.keys(profitStatus).forEach(function(algo){ + if (Object.keys(profitStatus[algo]).length > 1) { + isMoreThanOneCoin = true; + } + }); + if (!isMoreThanOneCoin){ + logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); + return; + } + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); - // - // setup APIs - // - var poloApi = new Poloniex( + // + // setup APIs + // + var poloApi = new Poloniex( // 'API_KEY', - // 'API_SECRET' - ); + // 'API_SECRET' + ); - // - // market data collection from Poloniex - // + // + // market data collection from Poloniex + // this.getProfitDataPoloniex = function(callback){ - async.series([ - function(taskCallback){ - poloApi.getTicker(function(err, data){ - if (err){ - taskCallback(err); - return; - } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcPrice = new Number(0); - var ltcPrice = new Number(0); + async.series([ + function(taskCallback){ + poloApi.getTicker(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcPrice = new Number(0); + var ltcPrice = new Number(0); - if (data.hasOwnProperty('BTC_' + symbol)) { - btcPrice = new Number(data['BTC_' + symbol]); - } - if (data.hasOwnProperty('LTC_' + symbol)) { - ltcPrice = new Number(data['LTC_' + symbol]); - } + if (data.hasOwnProperty('BTC_' + symbol)) { + btcPrice = new Number(data['BTC_' + symbol]); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcPrice = new Number(data['LTC_' + symbol]); + } - if (btcPrice > 0 || ltcPrice > 0) { - var prices = { - BTC: btcPrice, - LTC: ltcPrice - }; - profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; - } - }); - taskCallback(); - }); - }, - function(taskCallback){ - poloApi.get24hVolume(function(err, data){ - if (err){ - taskCallback(err); - return; - } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcVolume = new Number(0); - var ltcVolume = new Number(0); + if (btcPrice > 0 || ltcPrice > 0) { + var prices = { + BTC: btcPrice, + LTC: ltcPrice + }; + profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + poloApi.get24hVolume(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcVolume = new Number(0); + var ltcVolume = new Number(0); - if (data.hasOwnProperty('BTC_' + symbol)) { - btcVolume = new Number(data['BTC_' + symbol].BTC); - } - if (data.hasOwnProperty('LTC_' + symbol)) { - ltcVolume = new Number(data['LTC_' + symbol].LTC); - } + if (data.hasOwnProperty('BTC_' + symbol)) { + btcVolume = new Number(data['BTC_' + symbol].BTC); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcVolume = new Number(data['LTC_' + symbol].LTC); + } - if (btcVolume > 0 || ltcVolume > 0) { - var volumes = { - BTC: btcVolume, - LTC: ltcVolume - }; - profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; - } - }); - taskCallback(); - }); - }, - function(taskCallback){ - var depthTasks = []; - Object.keys(profitSymbols).forEach(function(symbol){ + if (btcVolume > 0 || ltcVolume > 0) { + var volumes = { + BTC: btcVolume, + LTC: ltcVolume + }; + profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(profitSymbols).forEach(function(symbol){ var coinVolumes = profitStatus[profitSymbols[symbol]][symbol].volumes; var coinPrices = profitStatus[profitSymbols[symbol]][symbol].prices; - if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ - var btcDepth = new Number(0); - var ltcDepth = new Number(0); + if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ + var btcDepth = new Number(0); + var ltcDepth = new Number(0); - if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['BTC']); - depthTasks.push(function(callback){ + if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['BTC']); + depthTasks.push(function(callback){ _this.getMarketDepthFromPoloniex('BTC', symbol, coinPrice, callback) - }); - } - if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['LTC']); - depthTasks.push(function(callback){ + }); + } + if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['LTC']); + depthTasks.push(function(callback){ _this.getMarketDepthFromPoloniex('LTC', symbol, coinPrice, callback) - }); - } - } - }); + }); + } + } + }); - if (depthTasks.length == 0){ - taskCallback; - return; - } - async.parallel(depthTasks, function(err){ - if (err){ - logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); - return; - } - taskCallback(); - }); - } - ], function(err){ - if (err){ - callback(err); - return; - } - callback(null); - }); - + if (depthTasks.length == 0){ + taskCallback; + return; + } + async.parallel(depthTasks, function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ - poloApi.getOrderBook(symbolA, symbolB, function(err, data){ - if (err){ - callback(err); - return; - } + poloApi.getOrderBook(symbolA, symbolB, function(err, data){ + if (err){ + callback(err); + return; + } var depth = new Number(0); - if (data.hasOwnProperty('bids')){ - data['bids'].forEach(function(order){ - var price = new Number(order[0]); - var qty = new Number(order[1]); - // only measure the depth down to configured depth - if (price >= coinPrice * portalConfig.profitSwitch.depth){ - depth += (qty * price); - } - }); - } + if (data.hasOwnProperty('bids')){ + data['bids'].forEach(function(order){ + var price = new Number(order[0]); + var qty = new Number(order[1]); + // only measure the depth down to configured depth + if (price >= coinPrice * portalConfig.profitSwitch.depth){ + depth += (qty * price); + } + }); + } - if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { - BTC: 0, - LTC: 0 - }; - } - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; + if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { + BTC: 0, + LTC: 0 + }; + } + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; callback(); - }); - }; + }); + }; - // TODO + // TODO this.getProfitDataCryptsy = function(callback){ - callback(null); + callback(null); }; var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running profitability checks.'); + logger.debug(logSystem, 'Check', 'Running profitability checks.'); - async.parallel([ - _this.getProfitDataPoloniex, - _this.getProfitDataCryptsy - ], function(err){ - if (err){ - logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); - return; - } - logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); - }); - }; + async.parallel([ + _this.getProfitDataPoloniex, + _this.getProfitDataCryptsy + ], function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); + }); + }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); }; From 7b6044d674a220a17066c6ee5552ddeab092a2fc Mon Sep 17 00:00:00 2001 From: Matt Culpepper Date: Sun, 13 Apr 2014 13:59:19 -0500 Subject: [PATCH 123/150] create a config option for difficulty multiplier --- README.md | 1 + coins/darkcoin.json | 5 +++-- coins/hirocoin.json | 5 +++-- libs/mposCompatibility.js | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index baa047b..fd11497 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ Here is an example of the required fields: "symbol": "ltc", "algorithm": "scrypt", //or "sha256", "scrypt-jane", "scrypt-n", "quark", "x11" "txMessages": false, //or true (not required, defaults to false) + "mposDiffMultiplier": 256, //only for x11 coins in mpos mode, set to 256 (optional) } ```` diff --git a/coins/darkcoin.json b/coins/darkcoin.json index ed863c3..fa806d4 100644 --- a/coins/darkcoin.json +++ b/coins/darkcoin.json @@ -1,5 +1,6 @@ { "name": "Darkcoin", "symbol": "DRK", - "algorithm": "x11" -} \ No newline at end of file + "algorithm": "x11", + "mposDiffMultiplier": 256 +} diff --git a/coins/hirocoin.json b/coins/hirocoin.json index 3b21d84..cd7da82 100644 --- a/coins/hirocoin.json +++ b/coins/hirocoin.json @@ -1,5 +1,6 @@ { "name": "Hirocoin", "symbol": "hic", - "algorithm": "x11" -} \ No newline at end of file + "algorithm": "x11", + "mposDiffMultiplier": 256 +} diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index bc2ae02..d0bf31c 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -68,7 +68,7 @@ module.exports = function(logger, poolConfig){ shareData.worker, isValidShare ? 'Y' : 'N', isValidBlock ? 'Y' : 'N', - poolConfig.coin.algorithm === 'x11' ? shareData.difficulty * 256 : shareData.difficulty, + shareData.difficulty * (poolConfig.coin.mposDiffMultiplier || 1) typeof(shareData.error) === 'undefined' ? null : shareData.error, shareData.blockHash ? shareData.blockHash : (shareData.blockHashInvalid ? shareData.blockHashInvalid : '') ]; @@ -102,4 +102,4 @@ module.exports = function(logger, poolConfig){ }; -}; \ No newline at end of file +}; From 2d6470cf3f74ae43423a6bce1d85133a69bcc934 Mon Sep 17 00:00:00 2001 From: Matt Culpepper Date: Sun, 13 Apr 2014 14:06:24 -0500 Subject: [PATCH 124/150] missing comma --- libs/mposCompatibility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index d0bf31c..2ee3f19 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -68,7 +68,7 @@ module.exports = function(logger, poolConfig){ shareData.worker, isValidShare ? 'Y' : 'N', isValidBlock ? 'Y' : 'N', - shareData.difficulty * (poolConfig.coin.mposDiffMultiplier || 1) + shareData.difficulty * (poolConfig.coin.mposDiffMultiplier || 1), typeof(shareData.error) === 'undefined' ? null : shareData.error, shareData.blockHash ? shareData.blockHash : (shareData.blockHashInvalid ? shareData.blockHashInvalid : '') ]; From 93274f907b84010a0dff01aeb840a28c2bb3f039 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 23:17:40 +0000 Subject: [PATCH 125/150] added support for difficulty and blockreward --- libs/profitSwitch.js | 344 +++++++++++++++++++++++++------------------ 1 file changed, 198 insertions(+), 146 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 3de8b9c..7d57fd7 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,6 +1,7 @@ var async = require('async'); var Poloniex = require('./apiPoloniex.js'); +var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -19,25 +20,22 @@ module.exports = function(logger){ Object.keys(poolConfigs).forEach(function(coin){ var poolConfig = poolConfigs[coin]; - var algo = poolConfig.coin.algorithm; + var algo = poolConfig.coin.algorithm; - if (!profitStatus.hasOwnProperty(algo)) { - profitStatus[algo] = {}; - } - var coinStatus = { - name: poolConfig.coin.name, - symbol: poolConfig.coin.symbol, - difficulty: 0, - reward: 0, - avgPrice: { BTC: 0, LTC: 0 }, - avgDepth: { BTC: 0, LTC: 0 }, - avgVolume: { BTC: 0, LTC: 0 }, - prices: {}, - depths: {}, - volumes: {}, - }; - profitStatus[algo][poolConfig.coin.symbol] = coinStatus; - profitSymbols[poolConfig.coin.symbol] = algo; + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + prices: {}, + depths: {}, + volumes: {}, + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + profitSymbols[poolConfig.coin.symbol] = algo; }); @@ -46,13 +44,13 @@ module.exports = function(logger){ // var isMoreThanOneCoin = false; Object.keys(profitStatus).forEach(function(algo){ - if (Object.keys(profitStatus[algo]).length > 1) { - isMoreThanOneCoin = true; - } + if (Object.keys(profitStatus[algo]).length > 1) { + isMoreThanOneCoin = true; + } }); if (!isMoreThanOneCoin){ - logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); - return; + logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); + return; } logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); @@ -63,143 +61,143 @@ module.exports = function(logger){ // var poloApi = new Poloniex( // 'API_KEY', - // 'API_SECRET' + // 'API_SECRET' ); // // market data collection from Poloniex // this.getProfitDataPoloniex = function(callback){ - async.series([ - function(taskCallback){ - poloApi.getTicker(function(err, data){ - if (err){ - taskCallback(err); - return; - } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcPrice = new Number(0); - var ltcPrice = new Number(0); + async.series([ + function(taskCallback){ + poloApi.getTicker(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcPrice = new Number(0); + var ltcPrice = new Number(0); - if (data.hasOwnProperty('BTC_' + symbol)) { - btcPrice = new Number(data['BTC_' + symbol]); - } - if (data.hasOwnProperty('LTC_' + symbol)) { - ltcPrice = new Number(data['LTC_' + symbol]); - } + if (data.hasOwnProperty('BTC_' + symbol)) { + btcPrice = new Number(data['BTC_' + symbol]); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcPrice = new Number(data['LTC_' + symbol]); + } - if (btcPrice > 0 || ltcPrice > 0) { - var prices = { - BTC: btcPrice, - LTC: ltcPrice - }; - profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; - } - }); - taskCallback(); - }); - }, - function(taskCallback){ - poloApi.get24hVolume(function(err, data){ - if (err){ - taskCallback(err); - return; - } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcVolume = new Number(0); - var ltcVolume = new Number(0); + if (btcPrice > 0 || ltcPrice > 0) { + var prices = { + BTC: btcPrice, + LTC: ltcPrice + }; + profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + poloApi.get24hVolume(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcVolume = new Number(0); + var ltcVolume = new Number(0); - if (data.hasOwnProperty('BTC_' + symbol)) { - btcVolume = new Number(data['BTC_' + symbol].BTC); - } - if (data.hasOwnProperty('LTC_' + symbol)) { - ltcVolume = new Number(data['LTC_' + symbol].LTC); - } + if (data.hasOwnProperty('BTC_' + symbol)) { + btcVolume = new Number(data['BTC_' + symbol].BTC); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcVolume = new Number(data['LTC_' + symbol].LTC); + } - if (btcVolume > 0 || ltcVolume > 0) { - var volumes = { - BTC: btcVolume, - LTC: ltcVolume - }; - profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; - } - }); - taskCallback(); - }); - }, - function(taskCallback){ - var depthTasks = []; - Object.keys(profitSymbols).forEach(function(symbol){ + if (btcVolume > 0 || ltcVolume > 0) { + var volumes = { + BTC: btcVolume, + LTC: ltcVolume + }; + profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(profitSymbols).forEach(function(symbol){ var coinVolumes = profitStatus[profitSymbols[symbol]][symbol].volumes; var coinPrices = profitStatus[profitSymbols[symbol]][symbol].prices; - if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ - var btcDepth = new Number(0); - var ltcDepth = new Number(0); + if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ + var btcDepth = new Number(0); + var ltcDepth = new Number(0); - if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['BTC']); - depthTasks.push(function(callback){ + if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['BTC']); + depthTasks.push(function(callback){ _this.getMarketDepthFromPoloniex('BTC', symbol, coinPrice, callback) - }); - } - if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['LTC']); - depthTasks.push(function(callback){ + }); + } + if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['LTC']); + depthTasks.push(function(callback){ _this.getMarketDepthFromPoloniex('LTC', symbol, coinPrice, callback) - }); - } - } - }); + }); + } + } + }); - if (depthTasks.length == 0){ - taskCallback; - return; - } - async.parallel(depthTasks, function(err){ - if (err){ - logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); - return; - } - taskCallback(); - }); - } - ], function(err){ - if (err){ + if (depthTasks.length == 0){ + taskCallback; + return; + } + async.parallel(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ callback(err); - return; - } - callback(null); - }); - + return; + } + callback(null); + }); + }; this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ - poloApi.getOrderBook(symbolA, symbolB, function(err, data){ - if (err){ - callback(err); - return; - } + poloApi.getOrderBook(symbolA, symbolB, function(err, data){ + if (err){ + callback(err); + return; + } var depth = new Number(0); - if (data.hasOwnProperty('bids')){ - data['bids'].forEach(function(order){ - var price = new Number(order[0]); - var qty = new Number(order[1]); - // only measure the depth down to configured depth - if (price >= coinPrice * portalConfig.profitSwitch.depth){ - depth += (qty * price); - } - }); - } + if (data.hasOwnProperty('bids')){ + data['bids'].forEach(function(order){ + var price = new Number(order[0]); + var qty = new Number(order[1]); + // only measure the depth down to configured depth + if (price >= coinPrice * portalConfig.profitSwitch.depth){ + depth += (qty * price); + } + }); + } - if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { - BTC: 0, - LTC: 0 - }; - } - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; + if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { + BTC: 0, + LTC: 0 + }; + } + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; callback(); - }); + }); }; // TODO @@ -207,20 +205,74 @@ module.exports = function(logger){ callback(null); }; + this.getCoindDaemonInfo = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinName = profitStatus[algo][symbol].name; + var poolConfig = poolConfigs[coinName]; + var daemonConfig = poolConfig.shareProcessing.internal.daemon; + daemonTasks.push(function(callback){ + _this.getDaemonInfoForCoin(symbol, daemonConfig, callback) + }); + }); + }); + + if (daemonTasks.length == 0){ + callback(); + return; + } + async.parallel(daemonTasks, function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getDaemonInfoForCoin = function(symbol, cfg, callback){ + var daemon = new Stratum.daemon.interface([cfg]); + daemon.once('online', function(){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + callback(result[0].error); + return; + } + profitStatus[profitSymbols[symbol]][symbol].difficulty = result[0].response; + + daemon.cmd('getblocktemplate', + [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], + function(result){ + if (result[0].error != null){ + callback(result[0].error); + return; + } + profitStatus[profitSymbols[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + }); + callback(null) + }); + }).once('connectionFailed', function(error){ + callback(error); + }).on('error', function(error){ + callback(error); + }).init(); + }; + var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running profitability checks.'); + logger.debug(logSystem, 'Check', 'Running mining profitability check.'); - async.parallel([ - _this.getProfitDataPoloniex, - _this.getProfitDataCryptsy - ], function(err){ - if (err){ + async.parallel([ + _this.getProfitDataPoloniex, + _this.getProfitDataCryptsy, + _this.getCoindDaemonInfo + ], function(err){ + if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); - return; - } + return; + } logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); - }); + }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 4888aaffba0b29bd8ddbca3535dea54ffd1d6c43 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 15 Apr 2014 16:38:51 -0600 Subject: [PATCH 126/150] Changed all references to localhost to 127.0.0.1 for better system compatibility. Implemented thread-wide IP banning via IPC. Added redis config for portal-wide related db interactions (stats and proxy) --- README.md | 78 ++++++++++++++++++++++-------- config_example.json | 11 +++-- init.js | 23 +++++---- libs/mposCompatibility.js | 2 +- libs/poolWorker.js | 13 ++++- libs/stats.js | 2 +- libs/workerListener.js | 26 ---------- pool_configs/litecoin_example.json | 14 +++--- scripts/blockNotify.js | 2 +- scripts/coinSwitch.js | 2 +- 10 files changed, 101 insertions(+), 72 deletions(-) delete mode 100644 libs/workerListener.js diff --git a/README.md b/README.md index baa047b..d333499 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,34 @@ #### Node Open Mining Portal This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written -entirely in Node.js. It contains a stratum poolserver, reward/payment/share processor, and a (*not yet completed*) -front-end website. +entirely in Node.js. It contains a stratum poolserver; reward/payment/share processor; and a (*not yet completed*) +responsive user-friendly front-end website featuring mining instructions, in-depth live statistics, and an admin center. -#### Features +#### Table of Contents +* [Features](#features) + * [Attack Mitigation](##attack-mitigation) + * [Security](#security) + * [Planned Features](#planned-features) +* [Community Support](#community--support) +* [Usage](#usage) + * [Requirements](#requirements) + * [Setting Up Coin Daemon](#0-setting-up-coin-daemon) + * [Downloading & Installing](#1-downloading--installing) + * [Configuration](#2-configuration) + * [Portal Config](#portal-config) + * [Coin Config](#coin-config) + * [Pool Config](#pool-config) + * [Setting Up Blocknotify](#optional-recommended-setting-up-blocknotify) + * [Starting the Portal](#3-start-the-portal) + * [Upgrading NOMP](#upgrading-nomp) +* [Donations](#donations) +* [Credits](#credits) +* [License](#license) + + + + +### Features * For the pool server it uses the highly efficient [node-stratum-pool](//github.com/zone117x/node-stratum-pool) module which supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, [several hashing algorithms](//github.com/zone117x/node-stratum-pool#hashing-algorithms-supported). @@ -64,7 +88,7 @@ allow your own pool to connect upstream to a larger pool server. It will request redistribute the work to our own connected miners. -#### Community / Support +### Community / Support IRC * Support / general discussion join #nomp: https://webchat.freenode.net/?channels=#nomp * Development discussion join #nomp-dev: https://webchat.freenode.net/?channels=#nomp-dev @@ -80,8 +104,11 @@ If your pool uses NOMP let us know and we will list your website here. * http://chunkypools.com * http://clevermining.com * http://rapidhash.net +* http://suchpool.pw * http://hashfaster.com +* http://miningpoolhub.com * http://kryptochaos.com +* http://pool.uberpools.org Usage @@ -156,12 +183,7 @@ Explanation for each field: /* How many seconds to hold onto historical stats. Currently set to 24 hours. */ "historicalRetention": 43200, /* How many seconds worth of shares should be gathered to generate hashrate. */ - "hashrateWindow": 300, - /* Redis instance of where to store historical stats. */ - "redis": { - "host": "localhost", - "port": 6379 - } + "hashrateWindow": 300 }, /* Not done yet. */ "adminCenter": { @@ -170,6 +192,13 @@ Explanation for each field: } }, + /* Redis instance of where to store global portal data such as historical stats, proxy states, + ect.. */ + "redis": { + "host": "127.0.0.1", + "port": 6379 + }, + /* With this enabled, the master process listen on the configured port for messages from the 'scripts/blockNotify.js' script which your coin daemons can be configured to run when a new block is available. When a blocknotify message is received, the master process uses @@ -290,6 +319,12 @@ Description of options: due to mining apps using incorrect max diffs and this pool using correct max diffs. */ "shareVariancePercent": 10, + /* Enable for client IP addresses to be detected when using a load balancer with TCP proxy + protocol enabled, such as HAProxy with 'send-proxy' param: + http://haproxy.1wt.eu/download/1.5/doc/configuration.txt */ + "tcpProxyProtocol": false, + + /* This determines what to do with submitted shares (and stratum worker authentication). You have two options: 1) Enable internal and disable mpos = this portal to handle all share payments. @@ -338,7 +373,7 @@ Description of options: configured 'address' that receives the block rewards, otherwise the daemon will not be able to confirm blocks or send out payments. */ "daemon": { - "host": "localhost", + "host": "127.0.0.1", "port": 19332, "user": "litecoinrpc", "password": "testnet" @@ -346,7 +381,7 @@ Description of options: /* Redis database used for storing share and block submission data. */ "redis": { - "host": "localhost", + "host": "127.0.0.1", "port": 6379 } }, @@ -355,7 +390,7 @@ Description of options: also want to use the "emitInvalidBlockHashes" option below if you require it. */ "mpos": { "enabled": false, - "host": "localhost", //MySQL db host + "host": "127.0.0.1", //MySQL db host "port": 3306, //MySQL db port "user": "me", //MySQL db user "password": "mypass", //MySQL db password @@ -368,8 +403,10 @@ Description of options: } }, - /* If a worker is submitting a high threshold of invalid shares we can temporarily ban them - to reduce system/network load. Also useful to fight against flooding attacks. */ + /* If a worker is submitting a high threshold of invalid shares we can temporarily ban their IP + to reduce system/network load. Also useful to fight against flooding attacks. The worker's + If running behind something like HAProxy be sure to enable the TCP Proxy Protocol config, + otherwise you'll end up banning your own IP address (and therefore all workers). */ "banning": { "enabled": true, "time": 600, //How many seconds to ban worker for @@ -404,13 +441,13 @@ Description of options: drops out-of-sync or offline. */ "daemons": [ { //Main daemon instance - "host": "localhost", + "host": "127.0.0.1", "port": 19332, "user": "litecoinrpc", "password": "testnet" }, { //Backup daemon instance - "host": "localhost", + "host": "127.0.0.1", "port": 19344, "user": "litecoinrpc", "password": "testnet" @@ -423,7 +460,7 @@ Description of options: intensive than blocknotify script). However its still under development (not yet working). */ "p2p": { "enabled": false, - "host": "localhost", + "host": "127.0.0.1", "port": 19333, /* Magic value is different for main/testnet and for each coin. It is found in the daemon @@ -455,7 +492,7 @@ node [path to scripts/blockNotify.js] [listener host]:[listener port] [listener ``` Example: inside `dogecoin.conf` add the line ``` -blocknotify="node scripts/blockNotify.js localhost:8117 mySuperSecurePassword dogecoin %s" +blocknotify="node scripts/blockNotify.js 127.0.0.1:8117 mySuperSecurePassword dogecoin %s" ``` Alternatively, you can use a more efficient block notify script written in pure C. Build and usage instructions @@ -506,7 +543,8 @@ Credits * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing * [Alex Petrov / sysmanalex](https://github.com/sysmanalex) - contributed the pure C block notify script -* Those that contributed to [node-stratum-pool](//github.com/zone117x/node-stratum-pool) +* [svirusxxx](//github.com/svirusxxx) - sponsored development of MPOS mode +* Those that contributed to [node-stratum-pool](//github.com/zone117x/node-stratum-pool#credits) License diff --git a/config_example.json b/config_example.json index 8cd417c..55e5e9b 100644 --- a/config_example.json +++ b/config_example.json @@ -13,11 +13,7 @@ "stats": { "updateInterval": 60, "historicalRetention": 43200, - "hashrateWindow": 300, - "redis": { - "host": "localhost", - "port": 6379 - } + "hashrateWindow": 300 }, "adminCenter": { "enabled": true, @@ -25,6 +21,11 @@ } }, + "redis": { + "host": "127.0.0.1", + "port": 6379 + }, + "blockNotifyListener": { "enabled": false, "port": 8117, diff --git a/init.js b/init.js index 0daf3fd..8044bb0 100644 --- a/init.js +++ b/init.js @@ -8,7 +8,6 @@ var PoolLogger = require('./libs/logUtil.js'); var BlocknotifyListener = require('./libs/blocknotifyListener.js'); var CoinswitchListener = require('./libs/coinswitchListener.js'); var RedisBlocknotifyListener = require('./libs/redisblocknotifyListener.js'); -var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); var PaymentProcessor = require('./libs/paymentProcessor.js'); var Website = require('./libs/website.js'); @@ -139,6 +138,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ return portalConfig.clustering.forks; })(); + var poolWorkers = {}; var createPoolWorker = function(forkId){ var worker = cluster.fork({ @@ -147,11 +147,24 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ pools: serializedConfigs, portalConfig: JSON.stringify(portalConfig) }); + worker.forkId = forkId; + worker.type = 'pool'; + poolWorkers[forkId] = worker; worker.on('exit', function(code, signal){ logger.error('Master', 'PoolSpanwer', 'Fork ' + forkId + ' died, spawning replacement worker...'); setTimeout(function(){ createPoolWorker(forkId); }, 2000); + }).on('message', function(msg){ + switch(msg.type){ + case 'banIP': + Object.keys(cluster.workers).forEach(function(id) { + if (cluster.workers[id].type === 'pool'){ + cluster.workers[id].send({type: 'banIP', ip: msg.ip}); + } + }); + break; + } }); }; @@ -168,12 +181,6 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ }; -var startWorkerListener = function(poolConfigs){ - var workerListener = new WorkerListener(logger, poolConfigs); - workerListener.init(); -}; - - var startBlockListener = function(portalConfig){ //block notify options //setup block notify here and use IPC to tell appropriate pools @@ -301,8 +308,6 @@ var startWebsite = function(portalConfig, poolConfigs){ startRedisBlockListener(portalConfig); - startWorkerListener(poolConfigs); - startWebsite(portalConfig, poolConfigs); })(); diff --git a/libs/mposCompatibility.js b/libs/mposCompatibility.js index 26b30a2..80bc0d8 100644 --- a/libs/mposCompatibility.js +++ b/libs/mposCompatibility.js @@ -41,7 +41,7 @@ module.exports = function(logger, poolConfig){ connection.query( 'SELECT password FROM pool_worker WHERE username = LOWER(?)', - [workerName], + [workerName.toLowerCase()], function(err, result){ if (err){ logger.error(logIdentify, logComponent, 'Database error when authenticating worker: ' + diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 71da314..c8b6680 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -22,6 +22,13 @@ module.exports = function(logger){ process.on('message', function(message) { switch(message.type){ + case 'banIP': + for (var p in pools){ + if (pools[p].stratumServer) + pools[p].stratumServer.addBannedIP(message.ip); + } + break; + case 'blocknotify': var messageCoin = message.coin.toLowerCase(); @@ -75,7 +82,7 @@ module.exports = function(logger){ ); proxySwitch[algo].currentPool = newCoin; - var redisClient = redis.createClient(6379, "localhost") + var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host) redisClient.on('ready', function(){ redisClient.hset('proxyState', algo, newCoin, function(error, obj) { if (error) { @@ -186,6 +193,8 @@ module.exports = function(logger){ handlers.diff(workerName, diff); }).on('log', function(severity, text) { logger[severity](logSystem, logComponent, logSubCat, text); + }).on('banIP', function(ip, worker){ + process.send({type: 'banIP', ip: ip}); }); pool.start(); @@ -206,7 +215,7 @@ module.exports = function(logger){ // on the last pool it was using when reloaded or restarted // logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis'); - var redisClient = redis.createClient(6379, "localhost") + var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host) redisClient.on('ready', function(){ redisClient.hgetall("proxyState", function(error, obj) { if (error || obj == null) { diff --git a/libs/stats.js b/libs/stats.js index 62b7f00..0893506 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -59,7 +59,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ function setupStatsRedis(){ - redisStats = redis.createClient(portalConfig.website.stats.redis.port, portalConfig.website.stats.redis.host); + redisStats = redis.createClient(portalConfig.redis.port, portalConfig.redis.host); redisStats.on('error', function(err){ logger.error(logSystem, 'Historics', 'Redis for stats had an error ' + JSON.stringify(err)); }); diff --git a/libs/workerListener.js b/libs/workerListener.js deleted file mode 100644 index 6bb669b..0000000 --- a/libs/workerListener.js +++ /dev/null @@ -1,26 +0,0 @@ -var events = require('events'); -var cluster = require('cluster'); - -var MposCompatibility = require('./mposCompatibility.js'); -var ShareProcessor = require('./shareProcessor.js'); - - -var processor = module.exports = function processor(logger, poolConfigs){ - - var _this = this; - - - this.init = function(){ - - Object.keys(cluster.workers).forEach(function(id) { - cluster.workers[id].on('message', function(data){ - switch(data.type){ - - } - }); - }); - } -}; - - -processor.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index a753c31..82b243e 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -10,6 +10,8 @@ "emitInvalidBlockHashes": false, "shareVariancePercent": 15, + "tcpProxyProtocol": false, + "shareProcessing": { "internal": { "enabled": true, @@ -22,19 +24,19 @@ "feeReceiveAddress": "mppaGeNaSbG1Q7S6V3gL5uJztMhucgL9Vh", "feeWithdrawalThreshold": 5, "daemon": { - "host": "localhost", + "host": "127.0.0.1", "port": 19332, "user": "litecoinrpc", "password": "testnet" }, "redis": { - "host": "localhost", + "host": "127.0.0.1", "port": 6379 } }, "mpos": { "enabled": false, - "host": "localhost", + "host": "127.0.0.1", "port": 3306, "user": "me", "password": "mypass", @@ -72,13 +74,13 @@ "daemons": [ { - "host": "localhost", + "host": "127.0.0.1", "port": 19332, "user": "litecoinrpc", "password": "testnet" }, { - "host": "localhost", + "host": "127.0.0.1", "port": 19344, "user": "litecoinrpc", "password": "testnet" @@ -87,7 +89,7 @@ "p2p": { "enabled": false, - "host": "localhost", + "host": "127.0.0.1", "port": 19333, "protocolVersion": 70002, "magic": "fcc1b7dc" diff --git a/scripts/blockNotify.js b/scripts/blockNotify.js index 67ed235..42a0f8c 100644 --- a/scripts/blockNotify.js +++ b/scripts/blockNotify.js @@ -1,6 +1,6 @@ /* This script should be hooked to the coin daemon as follow: - litecoind -blocknotify="node /path/to/this/script/blockNotify.js localhost:8117 password litecoin %s" + litecoind -blocknotify="node /path/to/this/script/blockNotify.js 127.0.0.1:8117 password litecoin %s" The above will send tell litecoin to launch this script with those parameters every time a block is found. This script will then send the blockhash along with other information to a listening tcp socket */ diff --git a/scripts/coinSwitch.js b/scripts/coinSwitch.js index 18dc1c8..acf6ac1 100644 --- a/scripts/coinSwitch.js +++ b/scripts/coinSwitch.js @@ -2,7 +2,7 @@ This script demonstrates sending a coin switch request and can be invoked from the command line with: - "node coinSwitch.js localhost:8118 password %s" + "node coinSwitch.js 127.0.0.1:8118 password %s" where <%s> is the name of the coin proxy miners will be switched onto. From 1d848c7d5199686fb0fa5862a833b45235d0e7d0 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 15 Apr 2014 16:56:35 -0600 Subject: [PATCH 127/150] Made readme for banning more clear --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 351dd14..a4166b8 100644 --- a/README.md +++ b/README.md @@ -405,9 +405,9 @@ Description of options: }, /* If a worker is submitting a high threshold of invalid shares we can temporarily ban their IP - to reduce system/network load. Also useful to fight against flooding attacks. The worker's - If running behind something like HAProxy be sure to enable the TCP Proxy Protocol config, - otherwise you'll end up banning your own IP address (and therefore all workers). */ + to reduce system/network load. Also useful to fight against flooding attacks. If running + behind something like HAProxy be sure to enable 'tcpProxyProtocol', otherwise you'll end up + banning your own IP address (and therefore all workers). */ "banning": { "enabled": true, "time": 600, //How many seconds to ban worker for From 1b2c2f9e7d72712d5002048765e85a75dce2dd2c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 15 Apr 2014 18:09:26 -0600 Subject: [PATCH 128/150] Made readme for banning more clear --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a4166b8..80fdbea 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ supports vardiff, POW & POS, transaction messages, anti-DDoS, IP banning, [sever * The portal has an [MPOS](//github.com/MPOS/php-mpos) compatibility mode so that the it can function as a drop-in-replacement for [python-stratum-mining](//github.com/Crypto-Expert/stratum-mining). This mode can be enabled in the configuration and will insert shares into a MySQL database in the format which MPOS expects. +For a direct tutorial see the wiki page [Setting up NOMP for MPOS usage](//github.com/zone117x/node-open-mining-portal/wiki/Setting-up-NOMP-for-MPOS-usage). * Multi-pool ability - this software was built from the ground up to run with multiple coins simultaneously (which can have different properties and hashing algorithms). It can be used to create a pool for a single coin or for multiple From 71e3883b4504283235f82a06ea71ce67e6daac0b Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 16 Apr 2014 06:43:35 -0600 Subject: [PATCH 129/150] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 80fdbea..fff3008 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Usage * [Node.js](http://nodejs.org/) v0.10+ ([follow these installation instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager)) * [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart)) +##### Seriously, those are real requirements. If you use old versions of Node.js or Redis that may come with your system package manager then you will have problems. Follow the linked instructions to get the last stable versions. #### 0) Setting up coin daemon Follow the build/install instructions for your coin daemon. Your coin.conf file should end up looking something like this: From b17f782ecc32ca2010a77574400fcd4eb47f5fd2 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 16 Apr 2014 06:44:57 -0600 Subject: [PATCH 130/150] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fff3008..8d32ae8 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ Usage * [Node.js](http://nodejs.org/) v0.10+ ([follow these installation instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager)) * [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart)) -##### Seriously, those are real requirements. If you use old versions of Node.js or Redis that may come with your system package manager then you will have problems. Follow the linked instructions to get the last stable versions. +##### Seriously +Those are legitimate requirements. If you use old versions of Node.js or Redis that may come with your system package manager then you will have problems. Follow the linked instructions to get the last stable versions. #### 0) Setting up coin daemon Follow the build/install instructions for your coin daemon. Your coin.conf file should end up looking something like this: From db9767f08e2c34e75770bf2a18f2f4bcb2cc2213 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 16 Apr 2014 11:50:33 -0600 Subject: [PATCH 131/150] Added documentation for p2p usage --- README.md | 27 ++++++++++++++++++--------- libs/poolWorker.js | 2 +- pool_configs/litecoin_example.json | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 80fdbea..91e627f 100644 --- a/README.md +++ b/README.md @@ -457,23 +457,30 @@ Description of options: ], - /* This allows the pool to connect to the daemon as a node peer to recieve block updates. + /* This allows the pool to connect to the daemon as a node peer to receive block updates. It may be the most efficient way to get block updates (faster than polling, less - intensive than blocknotify script). However its still under development (not yet working). */ + intensive than blocknotify script). It requires additional setup: the 'magic' field must + be exact (extracted from the coin source code). */ "p2p": { "enabled": false, + + /* Host for daemon */ "host": "127.0.0.1", + + /* Port configured for daemon (this is the actual peer port not RPC port) */ "port": 19333, + + /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p feature + that prevents the daemon from spamming our peer node with unnecessary transaction data. + Assume its supported but if you have problems try disabling it. */ + "disableTransactions": true, + /* Magic value is different for main/testnet and for each coin. It is found in the daemon source code as the pchMessageStart variable. For example, litecoin mainnet magic: http://git.io/Bi8YFw - And for litecoin testnet magic: http://git.io/NXBYJA - */ - "magic": "fcc1b7dc", - - //Found in src as the PROTOCOL_VERSION variable, for example: http://git.io/KjuCrw - "protocolVersion": 70002, + And for litecoin testnet magic: http://git.io/NXBYJA */ + "magic": "fcc1b7dc" } } @@ -494,7 +501,7 @@ node [path to scripts/blockNotify.js] [listener host]:[listener port] [listener ``` Example: inside `dogecoin.conf` add the line ``` -blocknotify="node scripts/blockNotify.js 127.0.0.1:8117 mySuperSecurePassword dogecoin %s" +blocknotify=node scripts/blockNotify.js 127.0.0.1:8117 mySuperSecurePassword dogecoin %s ``` Alternatively, you can use a more efficient block notify script written in pure C. Build and usage instructions @@ -541,11 +548,13 @@ Credits ------- * [Jerry Brady / mintyfresh68](https://github.com/bluecircle) - got coin-switching fully working and developed proxy-per-algo feature * [Tony Dobbs](http://anthonydobbs.com) - designs for front-end and created the NOMP logo +* [LucasJones(//github.com/LucasJones) - getting p2p block notify script working * [vekexasia](//github.com/vekexasia) - co-developer & great tester * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing * [Alex Petrov / sysmanalex](https://github.com/sysmanalex) - contributed the pure C block notify script * [svirusxxx](//github.com/svirusxxx) - sponsored development of MPOS mode +* [icecube45](//github.com/icecube45) - helping out with the repo wiki * Those that contributed to [node-stratum-pool](//github.com/zone117x/node-stratum-pool#credits) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index c8b6680..69d130f 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -37,7 +37,7 @@ module.exports = function(logger){ })[0]; if (poolTarget) - pools[poolTarget].processBlockNotify(message.hash); + pools[poolTarget].processBlockNotify(message.hash, 'blocknotify script'); break; diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 82b243e..7f75c58 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -91,7 +91,7 @@ "enabled": false, "host": "127.0.0.1", "port": 19333, - "protocolVersion": 70002, + "disableTransactions": true, "magic": "fcc1b7dc" } } From d4cac4a10563f640cadb602d6301fc8f56075334 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 16 Apr 2014 11:56:08 -0600 Subject: [PATCH 132/150] Updated readme overscrolling text --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c5edcfc..8be38b8 100644 --- a/README.md +++ b/README.md @@ -472,10 +472,9 @@ Description of options: /* Port configured for daemon (this is the actual peer port not RPC port) */ "port": 19333, - - /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p feature - that prevents the daemon from spamming our peer node with unnecessary transaction data. - Assume its supported but if you have problems try disabling it. */ + /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p + feature that prevents the daemon from spamming our peer node with unnecessary + transaction data. Assume its supported but if you have problems try disabling it. */ "disableTransactions": true, /* Magic value is different for main/testnet and for each coin. It is found in the daemon From e5d25cf11c20b545302efee667260f3cba48d7ff Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 16 Apr 2014 16:28:09 -0600 Subject: [PATCH 133/150] Added some keccak coins as its now officially supported. --- coins/copperlark.json | 7 +++++++ coins/cryptometh.json | 5 +++++ coins/ecoin.json | 7 +++++++ 3 files changed, 19 insertions(+) create mode 100644 coins/copperlark.json create mode 100644 coins/cryptometh.json create mode 100644 coins/ecoin.json diff --git a/coins/copperlark.json b/coins/copperlark.json new file mode 100644 index 0000000..7f07f12 --- /dev/null +++ b/coins/copperlark.json @@ -0,0 +1,7 @@ +{ + "name": "Copperlark", + "symbol": "CLR", + "algorithm": "keccak", + "normalHashing": true, + "diffShift": 32 +} \ No newline at end of file diff --git a/coins/cryptometh.json b/coins/cryptometh.json new file mode 100644 index 0000000..6015361 --- /dev/null +++ b/coins/cryptometh.json @@ -0,0 +1,5 @@ +{ + "name": "Cryptometh", + "symbol": "METH", + "algorithm": "keccak" +} \ No newline at end of file diff --git a/coins/ecoin.json b/coins/ecoin.json new file mode 100644 index 0000000..9703edf --- /dev/null +++ b/coins/ecoin.json @@ -0,0 +1,7 @@ +{ + "name": "Ecoin", + "symbol": "ECN", + "algorithm": "keccak", + "normalHashing": true, + "diffShift": 32 +} \ No newline at end of file From 384a10d82548da9f16df456c212436bdc3914e92 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 01:52:57 +0000 Subject: [PATCH 134/150] checkpoint --- libs/apiCryptsy.js | 204 +++++++++++++++++++++++++++++++++++++++++++++ libs/apiMintpal.js | 204 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 libs/apiCryptsy.js create mode 100644 libs/apiMintpal.js diff --git a/libs/apiCryptsy.js b/libs/apiCryptsy.js new file mode 100644 index 0000000..6563ab5 --- /dev/null +++ b/libs/apiCryptsy.js @@ -0,0 +1,204 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'http://pubapi.cryptsy.com/api.php', + PRIVATE_API_URL = 'https://api.cryptsy.com/api', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Cryptsy: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Cryptsy.prototype = { + constructor: Cryptsy, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Cryptsy.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + method: 'marketdatav2' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Cryptsy; +}(); diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js new file mode 100644 index 0000000..6563ab5 --- /dev/null +++ b/libs/apiMintpal.js @@ -0,0 +1,204 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'http://pubapi.cryptsy.com/api.php', + PRIVATE_API_URL = 'https://api.cryptsy.com/api', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Cryptsy: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Cryptsy.prototype = { + constructor: Cryptsy, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Cryptsy.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + method: 'marketdatav2' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Cryptsy; +}(); From 9ad11516d2aef63e69db39fb54f5b70a8c0118ea Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 01:53:40 +0000 Subject: [PATCH 135/150] checkpoint --- libs/apiPoloniex.js | 2 +- libs/poolWorker.js | 38 +++---- libs/profitSwitch.js | 262 ++++++++++++++++++++++++++----------------- 3 files changed, 180 insertions(+), 122 deletions(-) diff --git a/libs/apiPoloniex.js b/libs/apiPoloniex.js index 35f330c..fc483e1 100644 --- a/libs/apiPoloniex.js +++ b/libs/apiPoloniex.js @@ -8,7 +8,7 @@ module.exports = function() { // Constants var version = '0.1.0', - PUBLIC_API_URL = 'http://poloniex.com/public', + PUBLIC_API_URL = 'https://poloniex.com/public', PRIVATE_API_URL = 'https://poloniex.com/tradingApi', USER_AGENT = 'npm-crypto-apis/' + version diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 69d130f..0035eeb 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -63,10 +63,10 @@ module.exports = function(logger){ var oldPool = pools[oldCoin]; var proxyPort = proxySwitch[algo].port; - if (newCoin == oldCoin) { + if (newCoin == oldCoin) { logger.debug(logSystem, logComponent, logSubCat, 'Switch message would have no effect - ignoring ' + newCoin); - break; - } + break; + } logger.debug(logSystem, logComponent, logSubCat, 'Proxy message for ' + algo + ' from ' + oldCoin + ' to ' + newCoin); @@ -91,8 +91,8 @@ module.exports = function(logger){ else { logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo); } - }); - }); + }); + }); } break; } @@ -230,9 +230,9 @@ module.exports = function(logger){ // Setup proxySwitch object to control proxy operations from configuration and any restored // state. Each algorithm has a listening port, current coin name, and an active pool to // which traffic is directed when activated in the config. - // - // In addition, the proxy config also takes diff and varDiff parmeters the override the - // defaults for the standard config of the coin. + // + // In addition, the proxy config also takes diff and varDiff parmeters the override the + // defaults for the standard config of the coin. // Object.keys(portalConfig.proxy).forEach(function(algorithm) { @@ -245,21 +245,21 @@ module.exports = function(logger){ }; - // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up - // - // Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was - // routed into instead. - // - if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { + // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up + // + // Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was + // routed into instead. + // + if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { proxySwitch[algorithm].varDiff = new Stratum.varDiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff); proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff; - } + } Object.keys(pools).forEach(function (coinName) { var a = poolConfigs[coinName].coin.algorithm; var p = pools[coinName]; - if (a === algorithm) { + if (a === algorithm) { p.setVarDiff(proxySwitch[algorithm].port, proxySwitch[algorithm].varDiff); - } + } }); proxySwitch[algorithm].proxy = net.createServer(function(socket) { @@ -267,12 +267,12 @@ module.exports = function(logger){ var logSubCat = 'Thread ' + (parseInt(forkId) + 1); logger.debug(logSystem, 'Connect', logSubCat, 'Proxy connect from ' + socket.remoteAddress + ' on ' + proxySwitch[algorithm].port - + ' routing to ' + currentPool); + + ' routing to ' + currentPool); pools[currentPool].getStratumServer().handleNewClient(socket); }).listen(parseInt(proxySwitch[algorithm].port), function() { logger.debug(logSystem, logComponent, logSubCat, 'Proxy listening for ' + algorithm + ' on port ' + proxySwitch[algorithm].port - + ' into ' + proxySwitch[algorithm].currentPool); + + ' into ' + proxySwitch[algorithm].currentPool); }); } else { diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 7d57fd7..3d06d77 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,5 +1,6 @@ var async = require('async'); +var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); var Stratum = require('stratum-pool'); @@ -16,7 +17,7 @@ module.exports = function(logger){ // build status tracker for collecting coin market information // var profitStatus = {}; - var profitSymbols = {}; + var symbolToAlgorithmMap = {}; Object.keys(poolConfigs).forEach(function(coin){ var poolConfig = poolConfigs[coin]; @@ -30,12 +31,10 @@ module.exports = function(logger){ symbol: poolConfig.coin.symbol, difficulty: 0, reward: 0, - prices: {}, - depths: {}, - volumes: {}, + exchangeInfo: {} }; profitStatus[algo][poolConfig.coin.symbol] = coinStatus; - profitSymbols[poolConfig.coin.symbol] = algo; + symbolToAlgorithmMap[poolConfig.coin.symbol] = algo; }); @@ -52,8 +51,6 @@ module.exports = function(logger){ logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); return; } - logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); - logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); // @@ -63,6 +60,10 @@ module.exports = function(logger){ // 'API_KEY', // 'API_SECRET' ); + var cryptsyApi = new Cryptsy( + // 'API_KEY', + // 'API_SECRET' + ); // // market data collection from Poloniex @@ -75,51 +76,34 @@ module.exports = function(logger){ taskCallback(err); return; } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcPrice = new Number(0); - var ltcPrice = new Number(0); + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Poloniex')) + exchangeInfo['Poloniex'] = {}; + var marketData = exchangeInfo['Poloniex']; if (data.hasOwnProperty('BTC_' + symbol)) { - btcPrice = new Number(data['BTC_' + symbol]); + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = data['BTC_' + symbol]; + marketData['BTC'].ask = new Number(btcData.lowestAsk); + marketData['BTC'].bid = new Number(btcData.highestBid); + marketData['BTC'].last = new Number(btcData.last); + marketData['BTC'].baseVolume = new Number(btcData.baseVolume); + marketData['BTC'].quoteVolume = new Number(btcData.quoteVolume); } if (data.hasOwnProperty('LTC_' + symbol)) { - ltcPrice = new Number(data['LTC_' + symbol]); - } + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - if (btcPrice > 0 || ltcPrice > 0) { - var prices = { - BTC: btcPrice, - LTC: ltcPrice - }; - profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; - } - }); - taskCallback(); - }); - }, - function(taskCallback){ - poloApi.get24hVolume(function(err, data){ - if (err){ - taskCallback(err); - return; - } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcVolume = new Number(0); - var ltcVolume = new Number(0); - - if (data.hasOwnProperty('BTC_' + symbol)) { - btcVolume = new Number(data['BTC_' + symbol].BTC); - } - if (data.hasOwnProperty('LTC_' + symbol)) { - ltcVolume = new Number(data['LTC_' + symbol].LTC); - } - - if (btcVolume > 0 || ltcVolume > 0) { - var volumes = { - BTC: btcVolume, - LTC: ltcVolume - }; - profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; + var ltcData = data['LTC_' + symbol]; + marketData['LTC'].ask = new Number(ltcData.lowestAsk); + marketData['LTC'].bid = new Number(ltcData.highestBid); + marketData['LTC'].last = new Number(ltcData.last); + marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); + marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } }); taskCallback(); @@ -127,34 +111,25 @@ module.exports = function(logger){ }, function(taskCallback){ var depthTasks = []; - Object.keys(profitSymbols).forEach(function(symbol){ - var coinVolumes = profitStatus[profitSymbols[symbol]][symbol].volumes; - var coinPrices = profitStatus[profitSymbols[symbol]][symbol].prices; - - if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ - var btcDepth = new Number(0); - var ltcDepth = new Number(0); - - if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['BTC']); - depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('BTC', symbol, coinPrice, callback) - }); - } - if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ - var coinPrice = new Number(coinPrices['Poloniex']['LTC']); - depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('LTC', symbol, coinPrice, callback) - }); - } + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Poloniex']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); - if (depthTasks.length == 0){ - taskCallback; + if (!depthTasks.length){ + taskCallback(); return; } - async.parallel(depthTasks, function(err){ + async.series(depthTasks, function(err){ if (err){ taskCallback(err); return; @@ -181,37 +156,110 @@ module.exports = function(logger){ if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order[1]); // only measure the depth down to configured depth - if (price >= coinPrice * portalConfig.profitSwitch.depth){ + if (price >= limit){ depth += (qty * price); } }); } - if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { - BTC: 0, - LTC: 0 - }; - } - profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; + marketData[symbolA].depth = depth; callback(); }); }; - // TODO + this.getProfitDataCryptsy = function(callback){ - callback(null); + async.series([ + function(taskCallback){ + cryptsyApi.getTicker(function(err, data){ + if (err || data.success != 1){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Cryptsy')) + exchangeInfo['Cryptsy'] = {}; + + var marketData = exchangeInfo['Cryptsy']; + var results = data.return.markets; + + if (results.hasOwnProperty(symbol + '/BTC')) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = results[symbol + '/BTC']; + marketData['BTC'].last = new Number(btcData.lasttradeprice); + marketData['BTC'].baseVolume = new Number(marketData['BTC'].last / btcData.volume); + marketData['BTC'].quoteVolume = new Number(btcData.volume); + if (btcData.sellorders != null) + marketData['BTC'].ask = new Number(btcData.sellorders[0].price); + if (btcData.buyorders != null) { + marketData['BTC'].bid = new Number(btcData.buyorders[0].price); + var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + btcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + } + }); + marketData['BTC'].depth = depth; + } + } + + if (data.hasOwnProperty(symbol + '/LTC')) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + var ltcData = results[symbol + '/LTC']; + marketData['LTC'].last = new Number(ltcData.lasttradeprice); + marketData['LTC'].baseVolume = new Number(marketData['LTC'].last / ltcData.volume); + marketData['LTC'].quoteVolume = new Number(ltcData.volume); + if (ltcData.sellorders != null) + marketData['LTC'].ask = new Number(ltcData.sellorders[0].price); + if (ltcData.buyorders != null) { + marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); + var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + ltcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + } + }); + marketData['LTC'].depth = depth; + } + } + }); + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getCoindDaemonInfo = function(callback){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ var coinName = profitStatus[algo][symbol].name; var poolConfig = poolConfigs[coinName]; - var daemonConfig = poolConfig.shareProcessing.internal.daemon; + var daemonConfig = poolConfig.shareProcessing.internal.daemon; daemonTasks.push(function(callback){ _this.getDaemonInfoForCoin(symbol, daemonConfig, callback) }); @@ -222,7 +270,7 @@ module.exports = function(logger){ callback(); return; } - async.parallel(daemonTasks, function(err){ + async.series(daemonTasks, function(err){ if (err){ callback(err); return; @@ -232,26 +280,36 @@ module.exports = function(logger){ }; this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); - daemon.once('online', function(){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - callback(result[0].error); - return; - } - profitStatus[profitSymbols[symbol]][symbol].difficulty = result[0].response; - - daemon.cmd('getblocktemplate', - [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], - function(result){ - if (result[0].error != null){ - callback(result[0].error); - return; - } - profitStatus[profitSymbols[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - }); - callback(null) - }); - }).once('connectionFailed', function(error){ + daemon.once('online', function(){ + async.parallel([ + function(taskCallback){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; + taskCallback(null); + }); + }, + function(taskCallback){ + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + taskCallback(null); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }).once('connectionFailed', function(error){ callback(error); }).on('error', function(error){ callback(error); From 603c6d150b745d1e3f8ff48a8eada54ac8a47c43 Mon Sep 17 00:00:00 2001 From: Matt Culpepper Date: Wed, 16 Apr 2014 21:17:55 -0500 Subject: [PATCH 136/150] lots of new coin configs --- coins/defcoin.json | 5 +++++ coins/jennycoin.json | 5 +++++ coins/klondikecoin.json | 5 +++++ coins/muniti.json | 6 ++++++ coins/potcoin.json | 5 +++++ coins/procoin.json | 5 +++++ coins/ronpaulcoin.json | 5 +++++ coins/rubycoin.json | 5 +++++ coins/spartancoin.json | 5 +++++ coins/stoopidcoin.json | 5 +++++ coins/suncoin.json | 5 +++++ coins/whitecoin.json | 5 +++++ 12 files changed, 61 insertions(+) create mode 100644 coins/defcoin.json create mode 100644 coins/jennycoin.json create mode 100644 coins/klondikecoin.json create mode 100644 coins/muniti.json create mode 100644 coins/potcoin.json create mode 100644 coins/procoin.json create mode 100644 coins/ronpaulcoin.json create mode 100644 coins/rubycoin.json create mode 100644 coins/spartancoin.json create mode 100644 coins/stoopidcoin.json create mode 100644 coins/suncoin.json create mode 100644 coins/whitecoin.json diff --git a/coins/defcoin.json b/coins/defcoin.json new file mode 100644 index 0000000..fecc90a --- /dev/null +++ b/coins/defcoin.json @@ -0,0 +1,5 @@ +{ + "name": "Defcoin", + "symbol": "DEF", + "algorithm": "scrypt" +} diff --git a/coins/jennycoin.json b/coins/jennycoin.json new file mode 100644 index 0000000..41aae54 --- /dev/null +++ b/coins/jennycoin.json @@ -0,0 +1,5 @@ +{ + "name": "Jennycoin", + "symbol": "JNY", + "algorithm": "scrypt" +} diff --git a/coins/klondikecoin.json b/coins/klondikecoin.json new file mode 100644 index 0000000..0d0aa3a --- /dev/null +++ b/coins/klondikecoin.json @@ -0,0 +1,5 @@ +{ + "name": "Klondikecoin", + "symbol": "KDC", + "algorithm": "scrypt" +} diff --git a/coins/muniti.json b/coins/muniti.json new file mode 100644 index 0000000..ba5e430 --- /dev/null +++ b/coins/muniti.json @@ -0,0 +1,6 @@ +{ + "name": "Muniti", + "symbol": "MUN", + "algorithm": "x11", + "mposDiffMultiplier": 256 +} diff --git a/coins/potcoin.json b/coins/potcoin.json new file mode 100644 index 0000000..785e511 --- /dev/null +++ b/coins/potcoin.json @@ -0,0 +1,5 @@ +{ + "name": "Potcoin", + "symbol": "POT", + "algorithm": "scrypt" +} diff --git a/coins/procoin.json b/coins/procoin.json new file mode 100644 index 0000000..54cbfee --- /dev/null +++ b/coins/procoin.json @@ -0,0 +1,5 @@ +{ + "name": "Procoin", + "symbol": "PCN", + "algorithm": "scrypt" +} diff --git a/coins/ronpaulcoin.json b/coins/ronpaulcoin.json new file mode 100644 index 0000000..3122c5e --- /dev/null +++ b/coins/ronpaulcoin.json @@ -0,0 +1,5 @@ +{ + "name": "RonPaulCoin", + "symbol": "RPC", + "algorithm": "scrypt" +} diff --git a/coins/rubycoin.json b/coins/rubycoin.json new file mode 100644 index 0000000..d8de44b --- /dev/null +++ b/coins/rubycoin.json @@ -0,0 +1,5 @@ +{ + "name": "Rubycoin", + "symbol": "RUBY", + "algorithm": "scrypt" +} diff --git a/coins/spartancoin.json b/coins/spartancoin.json new file mode 100644 index 0000000..65c21ca --- /dev/null +++ b/coins/spartancoin.json @@ -0,0 +1,5 @@ +{ + "name": "Spartancoin", + "symbol": "SPN", + "algorithm": "scrypt" +} diff --git a/coins/stoopidcoin.json b/coins/stoopidcoin.json new file mode 100644 index 0000000..ce8d420 --- /dev/null +++ b/coins/stoopidcoin.json @@ -0,0 +1,5 @@ +{ + "name": "Stoopidcoin", + "symbol": "STP", + "algorithm": "scrypt" +} diff --git a/coins/suncoin.json b/coins/suncoin.json new file mode 100644 index 0000000..fea2aed --- /dev/null +++ b/coins/suncoin.json @@ -0,0 +1,5 @@ +{ + "name": "Suncoin", + "symbol": "SUN", + "algorithm": "scrypt" +} diff --git a/coins/whitecoin.json b/coins/whitecoin.json new file mode 100644 index 0000000..fe4326d --- /dev/null +++ b/coins/whitecoin.json @@ -0,0 +1,5 @@ +{ + "name": "Whitecoin", + "symbol": "WC", + "algorithm": "scrypt" +} From 4469cf546c9643f3e0c3eb314d31416cd225f075 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 02:42:38 +0000 Subject: [PATCH 137/150] profit data working --- config_example.json | 9 ++++-- libs/apiMintpal.js | 28 +++++++++--------- libs/profitSwitch.js | 67 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/config_example.json b/config_example.json index 5a47449..d28b0aa 100644 --- a/config_example.json +++ b/config_example.json @@ -70,9 +70,12 @@ }, "profitSwitch": { - "enabled": false, - "updateInterval": 60, - "depth": 0.80 + "enabled": false, + "updateInterval": 600, + "depth": 0.90, + "usePoloniex": true, + "useCryptsy": true, + "useMintpal": true }, "redisBlockNotifyListener": { diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js index 6563ab5..bd0610c 100644 --- a/libs/apiMintpal.js +++ b/libs/apiMintpal.js @@ -8,19 +8,19 @@ module.exports = function() { // Constants var version = '0.1.0', - PUBLIC_API_URL = 'http://pubapi.cryptsy.com/api.php', - PRIVATE_API_URL = 'https://api.cryptsy.com/api', + PUBLIC_API_URL = 'https://api.mintpal.com/v2/market', + PRIVATE_API_URL = 'https://api.mintpal.com/v2/market', USER_AGENT = 'nomp/node-open-mining-portal' // Constructor - function Cryptsy(key, secret){ + function Mintpal(key, secret){ // Generate headers signed by this user's key and secret. // The secret is encapsulated and never exposed this._getPrivateHeaders = function(parameters){ var paramString, signature; if (!key || !secret){ - throw 'Cryptsy: Error. API key and secret required'; + throw 'Mintpal: Error. API key and secret required'; } // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` @@ -38,7 +38,7 @@ module.exports = function() { } // If a site uses non-trusted SSL certificates, set this value to false - Cryptsy.STRICT_SSL = true; + Mintpal.STRICT_SSL = true; // Helper methods function joinCurrencies(currencyA, currencyB){ @@ -46,8 +46,8 @@ module.exports = function() { } // Prototype - Cryptsy.prototype = { - constructor: Cryptsy, + Mintpal.prototype = { + constructor: Mintpal, // Make an API request _request: function(options, callback){ @@ -57,7 +57,7 @@ module.exports = function() { options.headers['User-Agent'] = USER_AGENT; options.json = true; - options.strictSSL = Cryptsy.STRICT_SSL; + options.strictSSL = Mintpal.STRICT_SSL; request(options, function(err, response, body) { callback(err, body); @@ -99,11 +99,13 @@ module.exports = function() { // PUBLIC METHODS getTicker: function(callback){ - var parameters = { - method: 'marketdatav2' - }; + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/summary', + qs: null + }; - return this._public(parameters, callback); + return this._request(options, callback); }, getOrderBook: function(currencyA, currencyB, callback){ @@ -200,5 +202,5 @@ module.exports = function() { } }; - return Cryptsy; + return Mintpal; }(); diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 3d06d77..d8dac3d 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -2,6 +2,7 @@ var async = require('async'); var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); +var Mintpal = require('./apiMintpal.js'); var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -64,6 +65,10 @@ module.exports = function(logger){ // 'API_KEY', // 'API_SECRET' ); + var mintpalApi = new Mintpal( + // 'API_KEY', + // 'API_SECRET' + ); // // market data collection from Poloniex @@ -253,6 +258,50 @@ module.exports = function(logger){ }; + this.getProfitDataMintpal = function(callback){ + async.series([ + function(taskCallback){ + mintpalApi.getTicker(function(err, response){ + if (err){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.data.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Mintpal')) + exchangeInfo['Mintpal'] = {}; + + var marketData = exchangeInfo['Mintpal']; + + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + marketData['BTC'].last = new Number(market.last_price); + marketData['BTC'].baseVolume = new Number(market['24hvol']); + marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['BTC'].ask = new Number(market.top_ask); + marketData['BTC'].bid = new Number(market.top_bid); + } + + }); + }); + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + + this.getCoindDaemonInfo = function(callback){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ @@ -320,11 +369,19 @@ module.exports = function(logger){ var checkProfitability = function(){ logger.debug(logSystem, 'Check', 'Running mining profitability check.'); - async.parallel([ - _this.getProfitDataPoloniex, - _this.getProfitDataCryptsy, - _this.getCoindDaemonInfo - ], function(err){ + profitabilityTasks = []; + if (portalConfig.profitSwitch.usePoloniex) + profitabilityTasks.push(_this.getProfitDataPoloniex); + + if (portalConfig.profitSwitch.useCryptsy) + profitabilityTasks.push(_this.getProfitDataCryptsy); + + if (portalConfig.profitSwitch.useMintpal) + profitabilityTasks.push(_this.getProfitDataMintpal); + + profitabilityTasks.push(_this.getCoindDaemonInfo); + + async.parallel(profitabilityTasks, function(err){ if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; From 2e77108b3c47206f0d952e4dd7b75490836c4d2c Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 03:29:37 +0000 Subject: [PATCH 138/150] final mintpal changes --- libs/apiMintpal.js | 10 +++++++ libs/profitSwitch.js | 66 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js index bd0610c..fec22e6 100644 --- a/libs/apiMintpal.js +++ b/libs/apiMintpal.js @@ -108,6 +108,16 @@ module.exports = function() { return this._request(options, callback); }, + getBuyOrderBook: function(currencyA, currencyB, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/orders/' + currencyB + '/' + currencyA + '/BUY', + qs: null + }; + + return this._request(options, callback); + }, + getOrderBook: function(currencyA, currencyB, callback){ var parameters = { command: 'returnOrderBook', diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index d8dac3d..a17f640 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -286,10 +286,49 @@ module.exports = function(logger){ marketData['BTC'].bid = new Number(market.top_bid); } + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + marketData['LTC'].last = new Number(market.last_price); + marketData['LTC'].baseVolume = new Number(market['24hvol']); + marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['LTC'].ask = new Number(market.top_ask); + marketData['LTC'].bid = new Number(market.top_bid); + } + }); }); taskCallback(); }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Mintpal']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) + }); + } + }); + + if (!depthTasks.length){ + taskCallback(); + return; + } + async.series(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); } ], function(err){ if (err){ @@ -298,7 +337,31 @@ module.exports = function(logger){ } callback(null); }); - + }; + this.getMarketDepthFromMintpal = function(symbolA, symbolB, coinPrice, callback){ + mintpalApi.getBuyOrderBook(symbolA, symbolB, function(err, response){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (response.hasOwnProperty('data')){ + response['data'].forEach(function(order){ + var price = new Number(order.price); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var qty = new Number(order.amount); + // only measure the depth down to configured depth + if (price >= limit){ + depth += (qty * price); + } + }); + } + + + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; + marketData[symbolA].depth = depth; + callback(); + }); }; @@ -386,7 +449,6 @@ module.exports = function(logger){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } - logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 57be035ff29519b4d21336bc111c8645a41f8618 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 17 Apr 2014 12:51:44 -0600 Subject: [PATCH 139/150] Include actual share diff in log ouput --- libs/poolWorker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 69d130f..cb77079 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -180,7 +180,7 @@ module.exports = function(logger){ logger.debug(logSystem, logComponent, logSubCat, 'Block found: ' + data.blockHash); if (isValidShare) - logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + ' by ' + data.worker + ' [' + data.ip + ']' ); + logger.debug(logSystem, logComponent, logSubCat, 'Share accepted at diff ' + data.difficulty + '/' + data.shareDiff + ' by ' + data.worker + ' [' + data.ip + ']' ); else if (!isValidShare) logger.debug(logSystem, logComponent, logSubCat, 'Share rejected: ' + shareData); From ba30133bb2529d54405e09949193a28ebae7b8b1 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 22:55:54 +0000 Subject: [PATCH 140/150] final changes --- libs/profitSwitch.js | 113 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index a17f640..8074e33 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,4 +1,5 @@ var async = require('async'); +var net = require('net'); var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); @@ -42,13 +43,16 @@ module.exports = function(logger){ // // ensure we have something to switch // - var isMoreThanOneCoin = false; Object.keys(profitStatus).forEach(function(algo){ - if (Object.keys(profitStatus[algo]).length > 1) { - isMoreThanOneCoin = true; + if (Object.keys(profitStatus[algo]).length <= 1) { + delete profitStatus[algo]; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + if (symbolToAlgorithmMap[symbol] === algo) + delete symbolToAlgorithmMap[symbol]; + }); } }); - if (!isMoreThanOneCoin){ + if (Object.keys(profitStatus).length == 0){ logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); return; } @@ -110,7 +114,13 @@ module.exports = function(logger){ marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } + // save LTC to BTC exchange rate + if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) { + var btcLtc = data['BTC_LTC']; + marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid); + } }); + taskCallback(); }); }, @@ -158,6 +168,7 @@ module.exports = function(logger){ return; } var depth = new Number(0); + var totalQty = new Number(0); if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); @@ -166,12 +177,15 @@ module.exports = function(logger){ // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); }; @@ -194,7 +208,7 @@ module.exports = function(logger){ var marketData = exchangeInfo['Cryptsy']; var results = data.return.markets; - if (results.hasOwnProperty(symbol + '/BTC')) { + if (results && results.hasOwnProperty(symbol + '/BTC')) { if (!marketData.hasOwnProperty('BTC')) marketData['BTC'] = {}; @@ -208,18 +222,22 @@ module.exports = function(logger){ marketData['BTC'].bid = new Number(btcData.buyorders[0].price); var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); + var totalQty = new Number(0); btcData['buyorders'].forEach(function(order){ var price = new Number(order.price); var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); marketData['BTC'].depth = depth; + if (totalQty > 0) + marketData['BTC'].weightedBid = new Number(depth / totalQty); } } - if (data.hasOwnProperty(symbol + '/LTC')) { + if (results && results.hasOwnProperty(symbol + '/LTC')) { if (!marketData.hasOwnProperty('LTC')) marketData['LTC'] = {}; @@ -233,14 +251,18 @@ module.exports = function(logger){ marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); + var totalQty = new Number(0); ltcData['buyorders'].forEach(function(order){ var price = new Number(order.price); var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); marketData['LTC'].depth = depth; + if (totalQty > 0) + marketData['LTC'].weightedBid = new Number(depth / totalQty); } } }); @@ -262,7 +284,7 @@ module.exports = function(logger){ async.series([ function(taskCallback){ mintpalApi.getTicker(function(err, response){ - if (err){ + if (err || !response.data){ taskCallback(err); return; } @@ -346,6 +368,7 @@ module.exports = function(logger){ } var depth = new Number(0); if (response.hasOwnProperty('data')){ + var totalQty = new Number(0); response['data'].forEach(function(order){ var price = new Number(order.price); var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); @@ -353,13 +376,15 @@ module.exports = function(logger){ // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); } - var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); }; @@ -429,8 +454,70 @@ module.exports = function(logger){ }; + this.getMiningRate = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); + }); + }); + callback(null); + }; + + + this.switchToMostProfitableCoins = function() { + Object.keys(profitStatus).forEach(function(algo) { + var algoStatus = profitStatus[algo]; + + var bestExchange; + var bestCoin; + var bestBtcPerMhPerHour = new Number(0); + + Object.keys(profitStatus[algo]).forEach(function(symbol) { + var coinStatus = profitStatus[algo][symbol]; + + Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ + var exchangeData = coinStatus.exchangeInfo[exchange]; + if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + } + if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); + logger.debug(logSystem, 'LTC Check', 'btcPerMhPerHour = ' + btcPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + } + }); + }); + logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + if (portalConfig.coinSwitchListener.enabled){ + var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { + client.write(JSON.stringify({ + password: portalConfig.coinSwitchListener.password, + coin: bestCoin + }) + '\n'); + }); + } + }); + }; + + var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running mining profitability check.'); + logger.debug(logSystem, 'Check', 'Collecting profitability data.'); profitabilityTasks = []; if (portalConfig.profitSwitch.usePoloniex) @@ -443,12 +530,18 @@ module.exports = function(logger){ profitabilityTasks.push(_this.getProfitDataMintpal); profitabilityTasks.push(_this.getCoindDaemonInfo); + profitabilityTasks.push(_this.getMiningRate); - async.parallel(profitabilityTasks, function(err){ + // has to be series + async.series(profitabilityTasks, function(err){ if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } + // + // TODO offer support for a userConfigurable function for deciding on coin to override the default + // + _this.switchToMostProfitableCoins(); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 60b73a43a8350978f096579ec4decd3bda7999f3 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 22:57:10 +0000 Subject: [PATCH 141/150] final changes --- config_example.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_example.json b/config_example.json index d28b0aa..869b3db 100644 --- a/config_example.json +++ b/config_example.json @@ -34,6 +34,7 @@ "coinSwitchListener": { "enabled": false, + "host": "127.0.0.1", "port": 8118, "password": "test" }, From 1134f13a4051ab03239af39851d6ae951862eaa6 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 23:09:53 +0000 Subject: [PATCH 142/150] remove extra logging --- libs/profitSwitch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 8074e33..c1a96bc 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -492,7 +492,6 @@ module.exports = function(logger){ } if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); - logger.debug(logSystem, 'LTC Check', 'btcPerMhPerHour = ' + btcPerMhPerHour); if (btcPerMhPerHour > bestBtcPerMhPerHour){ bestBtcPerMhPerHour = btcPerMhPerHour; bestExchange = exchange; From b7e8f67f701a644e88516eabf617696618c703eb Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 18 Apr 2014 12:48:59 +0000 Subject: [PATCH 143/150] removed tabs --- libs/profitSwitch.js | 328 +++++++++++++++++++++---------------------- 1 file changed, 164 insertions(+), 164 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index c1a96bc..1705daa 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -46,10 +46,10 @@ module.exports = function(logger){ Object.keys(profitStatus).forEach(function(algo){ if (Object.keys(profitStatus[algo]).length <= 1) { delete profitStatus[algo]; - Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ - if (symbolToAlgorithmMap[symbol] === algo) - delete symbolToAlgorithmMap[symbol]; - }); + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + if (symbolToAlgorithmMap[symbol] === algo) + delete symbolToAlgorithmMap[symbol]; + }); } }); if (Object.keys(profitStatus).length == 0){ @@ -88,15 +88,15 @@ module.exports = function(logger){ Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Poloniex')) - exchangeInfo['Poloniex'] = {}; - var marketData = exchangeInfo['Poloniex']; + if (!exchangeInfo.hasOwnProperty('Poloniex')) + exchangeInfo['Poloniex'] = {}; + var marketData = exchangeInfo['Poloniex']; if (data.hasOwnProperty('BTC_' + symbol)) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - var btcData = data['BTC_' + symbol]; + var btcData = data['BTC_' + symbol]; marketData['BTC'].ask = new Number(btcData.lowestAsk); marketData['BTC'].bid = new Number(btcData.highestBid); marketData['BTC'].last = new Number(btcData.last); @@ -104,21 +104,21 @@ module.exports = function(logger){ marketData['BTC'].quoteVolume = new Number(btcData.quoteVolume); } if (data.hasOwnProperty('LTC_' + symbol)) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - var ltcData = data['LTC_' + symbol]; + var ltcData = data['LTC_' + symbol]; marketData['LTC'].ask = new Number(ltcData.lowestAsk); marketData['LTC'].bid = new Number(ltcData.highestBid); marketData['LTC'].last = new Number(ltcData.last); marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } - // save LTC to BTC exchange rate + // save LTC to BTC exchange rate if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) { - var btcLtc = data['BTC_LTC']; + var btcLtc = data['BTC_LTC']; marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid); - } + } }); taskCallback(); @@ -130,13 +130,13 @@ module.exports = function(logger){ var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Poloniex']; if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) - }); + _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) + }); } if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) - }); + _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); @@ -172,19 +172,19 @@ module.exports = function(logger){ if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); - var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order[1]); // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; marketData[symbolA].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); @@ -202,25 +202,25 @@ module.exports = function(logger){ Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Cryptsy')) - exchangeInfo['Cryptsy'] = {}; + if (!exchangeInfo.hasOwnProperty('Cryptsy')) + exchangeInfo['Cryptsy'] = {}; - var marketData = exchangeInfo['Cryptsy']; - var results = data.return.markets; + var marketData = exchangeInfo['Cryptsy']; + var results = data.return.markets; if (results && results.hasOwnProperty(symbol + '/BTC')) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - var btcData = results[symbol + '/BTC']; + var btcData = results[symbol + '/BTC']; marketData['BTC'].last = new Number(btcData.lasttradeprice); marketData['BTC'].baseVolume = new Number(marketData['BTC'].last / btcData.volume); marketData['BTC'].quoteVolume = new Number(btcData.volume); - if (btcData.sellorders != null) + if (btcData.sellorders != null) marketData['BTC'].ask = new Number(btcData.sellorders[0].price); - if (btcData.buyorders != null) { + if (btcData.buyorders != null) { marketData['BTC'].bid = new Number(btcData.buyorders[0].price); - var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); + var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); var totalQty = new Number(0); btcData['buyorders'].forEach(function(order){ @@ -228,28 +228,28 @@ module.exports = function(logger){ var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } - }); + }); marketData['BTC'].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData['BTC'].weightedBid = new Number(depth / totalQty); - } - } + } + } if (results && results.hasOwnProperty(symbol + '/LTC')) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - var ltcData = results[symbol + '/LTC']; + var ltcData = results[symbol + '/LTC']; marketData['LTC'].last = new Number(ltcData.lasttradeprice); marketData['LTC'].baseVolume = new Number(marketData['LTC'].last / ltcData.volume); marketData['LTC'].quoteVolume = new Number(ltcData.volume); - if (ltcData.sellorders != null) + if (ltcData.sellorders != null) marketData['LTC'].ask = new Number(ltcData.sellorders[0].price); - if (ltcData.buyorders != null) { + if (ltcData.buyorders != null) { marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); - var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); + var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); var totalQty = new Number(0); ltcData['buyorders'].forEach(function(order){ @@ -257,13 +257,13 @@ module.exports = function(logger){ var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } - }); + }); marketData['LTC'].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData['LTC'].weightedBid = new Number(depth / totalQty); - } + } } }); taskCallback(); @@ -289,38 +289,38 @@ module.exports = function(logger){ return; } - Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ - response.data.forEach(function(market){ - var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Mintpal')) - exchangeInfo['Mintpal'] = {}; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.data.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Mintpal')) + exchangeInfo['Mintpal'] = {}; - var marketData = exchangeInfo['Mintpal']; + var marketData = exchangeInfo['Mintpal']; - if (market.exchange == 'BTC' && market.code == symbol) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - marketData['BTC'].last = new Number(market.last_price); - marketData['BTC'].baseVolume = new Number(market['24hvol']); - marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); - marketData['BTC'].ask = new Number(market.top_ask); - marketData['BTC'].bid = new Number(market.top_bid); - } + marketData['BTC'].last = new Number(market.last_price); + marketData['BTC'].baseVolume = new Number(market['24hvol']); + marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['BTC'].ask = new Number(market.top_ask); + marketData['BTC'].bid = new Number(market.top_bid); + } - if (market.exchange == 'LTC' && market.code == symbol) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - marketData['LTC'].last = new Number(market.last_price); - marketData['LTC'].baseVolume = new Number(market['24hvol']); - marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); - marketData['LTC'].ask = new Number(market.top_ask); - marketData['LTC'].bid = new Number(market.top_bid); - } + marketData['LTC'].last = new Number(market.last_price); + marketData['LTC'].baseVolume = new Number(market['24hvol']); + marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['LTC'].ask = new Number(market.top_ask); + marketData['LTC'].bid = new Number(market.top_bid); + } - }); - }); + }); + }); taskCallback(); }); }, @@ -330,13 +330,13 @@ module.exports = function(logger){ var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Mintpal']; if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) - }); + _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) + }); } if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) - }); + _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); @@ -371,19 +371,19 @@ module.exports = function(logger){ var totalQty = new Number(0); response['data'].forEach(function(order){ var price = new Number(order.price); - var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order.amount); // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; marketData[symbolA].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); @@ -417,36 +417,36 @@ module.exports = function(logger){ }; this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); - daemon.once('online', function(){ - async.parallel([ - function(taskCallback){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; - taskCallback(null); - }); - }, - function(taskCallback){ - daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - taskCallback(null); - }); - } - ], function(err){ - if (err){ - callback(err); - return; - } - callback(null); - }); - }).once('connectionFailed', function(error){ + daemon.once('online', function(){ + async.parallel([ + function(taskCallback){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; + taskCallback(null); + }); + }, + function(taskCallback){ + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + taskCallback(null); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }).once('connectionFailed', function(error){ callback(error); }).on('error', function(error){ callback(error); @@ -458,78 +458,78 @@ module.exports = function(logger){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ - var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; - coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); - coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); }); }); callback(null); }; - this.switchToMostProfitableCoins = function() { - Object.keys(profitStatus).forEach(function(algo) { - var algoStatus = profitStatus[algo]; + this.switchToMostProfitableCoins = function() { + Object.keys(profitStatus).forEach(function(algo) { + var algoStatus = profitStatus[algo]; var bestExchange; var bestCoin; var bestBtcPerMhPerHour = new Number(0); - Object.keys(profitStatus[algo]).forEach(function(symbol) { - var coinStatus = profitStatus[algo][symbol]; + Object.keys(profitStatus[algo]).forEach(function(symbol) { + var coinStatus = profitStatus[algo][symbol]; - Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ - var exchangeData = coinStatus.exchangeInfo[exchange]; - if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ - var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); - if (btcPerMhPerHour > bestBtcPerMhPerHour){ - bestBtcPerMhPerHour = btcPerMhPerHour; - bestExchange = exchange; - bestCoin = profitStatus[algo][symbol].name; - } - coinStatus.btcPerMhPerHour = btcPerMhPerHour; + Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ + var exchangeData = coinStatus.exchangeInfo[exchange]; + if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - } - if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ - var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); - if (btcPerMhPerHour > bestBtcPerMhPerHour){ - bestBtcPerMhPerHour = btcPerMhPerHour; - bestExchange = exchange; - bestCoin = profitStatus[algo][symbol].name; - } - coinStatus.btcPerMhPerHour = btcPerMhPerHour; + } + if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - } - }); - }); + } + }); + }); logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - if (portalConfig.coinSwitchListener.enabled){ - var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { - client.write(JSON.stringify({ - password: portalConfig.coinSwitchListener.password, - coin: bestCoin - }) + '\n'); - }); - } - }); - }; + if (portalConfig.coinSwitchListener.enabled){ + var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { + client.write(JSON.stringify({ + password: portalConfig.coinSwitchListener.password, + coin: bestCoin + }) + '\n'); + }); + } + }); + }; var checkProfitability = function(){ logger.debug(logSystem, 'Check', 'Collecting profitability data.'); - profitabilityTasks = []; - if (portalConfig.profitSwitch.usePoloniex) - profitabilityTasks.push(_this.getProfitDataPoloniex); + profitabilityTasks = []; + if (portalConfig.profitSwitch.usePoloniex) + profitabilityTasks.push(_this.getProfitDataPoloniex); - if (portalConfig.profitSwitch.useCryptsy) - profitabilityTasks.push(_this.getProfitDataCryptsy); + if (portalConfig.profitSwitch.useCryptsy) + profitabilityTasks.push(_this.getProfitDataCryptsy); - if (portalConfig.profitSwitch.useMintpal) - profitabilityTasks.push(_this.getProfitDataMintpal); + if (portalConfig.profitSwitch.useMintpal) + profitabilityTasks.push(_this.getProfitDataMintpal); - profitabilityTasks.push(_this.getCoindDaemonInfo); - profitabilityTasks.push(_this.getMiningRate); + profitabilityTasks.push(_this.getCoindDaemonInfo); + profitabilityTasks.push(_this.getMiningRate); // has to be series async.series(profitabilityTasks, function(err){ @@ -537,10 +537,10 @@ module.exports = function(logger){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } - // - // TODO offer support for a userConfigurable function for deciding on coin to override the default - // - _this.switchToMostProfitableCoins(); + // + // TODO offer support for a userConfigurable function for deciding on coin to override the default + // + _this.switchToMostProfitableCoins(); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 167be4baf0753f80a0ec388b5a86710cf696212b Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 18 Apr 2014 13:44:19 -0600 Subject: [PATCH 144/150] Minor change in logging: does not log 'Share data and stats recorded' anymore --- libs/shareProcessor.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 815eb1a..a4e88c9 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -42,7 +42,6 @@ module.exports = function(logger, poolConfig){ this.handleShare = function(isValidShare, isValidBlock, shareData){ - var redisCommands = []; if (isValidShare){ @@ -71,8 +70,8 @@ module.exports = function(logger, poolConfig){ connection.multi(redisCommands).exec(function(err, replies){ if (err) logger.error(logSystem, logComponent, logSubCat, 'Error with share processor multi ' + JSON.stringify(err)); - else - logger.debug(logSystem, logComponent, logSubCat, 'Share data and stats recorded'); + //else + //logger.debug(logSystem, logComponent, logSubCat, 'Share data and stats recorded'); }); From f6610a914d62c8d318b3773e36bc36d27f4f78f4 Mon Sep 17 00:00:00 2001 From: icecube45 Date: Sat, 19 Apr 2014 12:32:47 -0700 Subject: [PATCH 145/150] Update quarkcoin.json --- coins/quarkcoin.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coins/quarkcoin.json b/coins/quarkcoin.json index 797e7cd..983f340 100644 --- a/coins/quarkcoin.json +++ b/coins/quarkcoin.json @@ -1,5 +1,6 @@ { "name": "Quarkcoin", "symbol": "QRK", - "algorithm": "quark" -} \ No newline at end of file + "algorithm": "quark", + "mposDiffMultiplier": 256 +} From 31f6091a149662580c80e0b511602277b0549551 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sat, 19 Apr 2014 23:08:52 +0000 Subject: [PATCH 146/150] diff computed from block info --- libs/profitSwitch.js | 64 ++++++++++++++++++++------------------------ package.json | 3 ++- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 1705daa..7c1f449 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,10 +1,13 @@ -var async = require('async'); -var net = require('net'); +var async = require('async'); +var net = require('net'); +var bignum = require('bignum'); +var algos = require('stratum-pool/lib/algoProperties.js'); +var util = require('stratum-pool/lib/util.js'); -var Cryptsy = require('./apiCryptsy.js'); +var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); -var Mintpal = require('./apiMintpal.js'); -var Stratum = require('stratum-pool'); +var Mintpal = require('./apiMintpal.js'); +var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -418,38 +421,29 @@ module.exports = function(logger){ this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); daemon.once('online', function(){ - async.parallel([ - function(taskCallback){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; - taskCallback(null); - }); - }, - function(taskCallback){ - daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - taskCallback(null); - }); - } - ], function(err){ - if (err){ - callback(err); + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + logger.error(logSystem, symbol, 'Error while reading daemon info: ' + JSON.stringify(result[0])); + callback(null); // fail gracefully for each coin return; } + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + var response = result[0].response; + + // some shitcoins dont provide target, only bits, so we need to deal with both + var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits); + coinStatus.difficulty = parseFloat((diff1.toNumber() / target.toNumber()).toFixed(9)); + logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty); + + coinStatus.reward = new Number(response.coinbasevalue / 100000000); callback(null); }); }).once('connectionFailed', function(error){ - callback(error); + logger.error(logSystem, symbol, JSON.stringify(error)); + callback(null); // fail gracefully for each coin }).on('error', function(error){ - callback(error); + logger.error(logSystem, symbol, JSON.stringify(error)); + callback(null); // fail gracefully for each coin }).init(); }; @@ -459,7 +453,7 @@ module.exports = function(logger){ Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; - coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.blocksPerMhPerHour = new Number(86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); }); }); @@ -488,7 +482,7 @@ module.exports = function(logger){ bestCoin = profitStatus[algo][symbol].name; } coinStatus.btcPerMhPerHour = btcPerMhPerHour; - logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); } if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); @@ -498,11 +492,11 @@ module.exports = function(logger){ bestCoin = profitStatus[algo][symbol].name; } coinStatus.btcPerMhPerHour = btcPerMhPerHour; - logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); } }); }); - logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); if (portalConfig.coinSwitchListener.enabled){ var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { client.write(JSON.stringify({ diff --git a/package.json b/package.json index 2140f2c..dc3e7c1 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "colors": "*", "node-watch": "*", "request": "*", - "nonce": "*" + "nonce": "*", + "bignum": "*" }, "engines": { "node": ">=0.10" From e58689956dd17dd616da9f6926464c00005e1574 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 20 Apr 2014 12:04:41 -0600 Subject: [PATCH 147/150] Changed stats to use algo multiplier --- README.md | 4 ++-- libs/poolWorker.js | 6 +++--- libs/stats.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8be38b8..c2e9cb2 100644 --- a/README.md +++ b/README.md @@ -302,8 +302,8 @@ Description of options: job broadcast. */ "txRefreshInterval": 20000, - /* Some miner software is bugged and will consider the pool offline if it doesn't receive - anything for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast + /* Some miner apps will consider the pool dead/offline if it doesn't receive anything new jobs + for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast in this many seconds unless we find a new job. Set to zero or remove to disable this. */ "jobRebroadcastTimeout": 55, diff --git a/libs/poolWorker.js b/libs/poolWorker.js index cb77079..adec236 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -117,7 +117,7 @@ module.exports = function(logger){ //Functions required for MPOS compatibility if (shareProcessing && shareProcessing.mpos && shareProcessing.mpos.enabled){ - var mposCompat = new MposCompatibility(logger, poolOptions) + var mposCompat = new MposCompatibility(logger, poolOptions); handlers.auth = function(workerName, password, authCallback){ mposCompat.handleAuth(workerName, password, authCallback); @@ -135,7 +135,7 @@ module.exports = function(logger){ //Functions required for internal payment processing else if (shareProcessing && shareProcessing.internal && shareProcessing.internal.enabled){ - var shareProcessor = new ShareProcessor(logger, poolOptions) + var shareProcessor = new ShareProcessor(logger, poolOptions); handlers.auth = function(workerName, password, authCallback){ if (shareProcessing.internal.validateWorkerAddress !== true) @@ -215,7 +215,7 @@ module.exports = function(logger){ // on the last pool it was using when reloaded or restarted // logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis'); - var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host) + var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host); redisClient.on('ready', function(){ redisClient.hgetall("proxyState", function(error, obj) { if (error || obj == null) { diff --git a/libs/stats.js b/libs/stats.js index 0893506..ade763b 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -191,7 +191,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ else coinStats.workers[worker] = workerShares; }); - var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; + var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier; var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow; coinStats.hashrate = hashratePre | 0; coinStats.workerCount = Object.keys(coinStats.workers).length; From 59cfa8b1fe7fdc5bba53439f4d1396a9ae176a16 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 20 Apr 2014 12:10:19 -0600 Subject: [PATCH 148/150] Minor readme fix with anchor link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2e9cb2..d99804c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ responsive user-friendly front-end website featuring mining instructions, in-dep #### Table of Contents * [Features](#features) - * [Attack Mitigation](##attack-mitigation) + * [Attack Mitigation](#attack-mitigation) * [Security](#security) * [Planned Features](#planned-features) * [Community Support](#community--support) From a4a22784437b1dde397b9ddf95572e4a7ac954ec Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 20 Apr 2014 12:17:44 -0600 Subject: [PATCH 149/150] diff1 is a normal JS number now rather than a bignum --- libs/profitSwitch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 7c1f449..83491a0 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -432,7 +432,7 @@ module.exports = function(logger){ // some shitcoins dont provide target, only bits, so we need to deal with both var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits); - coinStatus.difficulty = parseFloat((diff1.toNumber() / target.toNumber()).toFixed(9)); + coinStatus.difficulty = parseFloat((diff1 / target.toNumber()).toFixed(9)); logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty); coinStatus.reward = new Number(response.coinbasevalue / 100000000); From 9722c054f90b430ae0a090afbdde632a667a1821 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 20 Apr 2014 21:16:58 -0600 Subject: [PATCH 150/150] Added mintcoin --- coins/mintcoin.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 coins/mintcoin.json diff --git a/coins/mintcoin.json b/coins/mintcoin.json new file mode 100644 index 0000000..fb93cfb --- /dev/null +++ b/coins/mintcoin.json @@ -0,0 +1,5 @@ +{ + "name": "Mintcoin", + "symbol": "MINT", + "algorithm": "scrypt" +} \ No newline at end of file