Browse Source

init

pull/1/head
Unknown 6 years ago
parent
commit
4e891a88f0
  1. 5
      .gitignore
  2. 7
      LICENSE
  3. 11
      Old/CHANGELOG.md
  4. 7
      coins/komodo.json
  5. 517
      init.js
  6. 135
      libs/api.js
  7. 222
      libs/apiBittrex.js
  8. 115
      libs/apiCoinWarz.js
  9. 204
      libs/apiCryptsy.js
  10. 216
      libs/apiMintpal.js
  11. 212
      libs/apiPoloniex.js
  12. 45
      libs/cliListener.js
  13. 89
      libs/logUtil.js
  14. 122
      libs/mposCompatibility.js
  15. 1426
      libs/paymentProcessor.js
  16. 329
      libs/poolWorker.js
  17. 666
      libs/profitSwitch.js
  18. 103
      libs/shareProcessor.js
  19. 774
      libs/stats.js
  20. 362
      libs/website.js
  21. 56
      libs/workerapi.js
  22. 53
      package.json
  23. BIN
      scripts/blocknotify
  24. 84
      scripts/blocknotify.c
  25. 37
      scripts/cli.js
  26. 95
      website/index.html
  27. 2798
      website/key.html
  28. 50
      website/pages/admin.html
  29. 13
      website/pages/api.html
  30. 318
      website/pages/getting_started.html
  31. 140
      website/pages/home.html
  32. 105
      website/pages/miner_stats.html
  33. 25
      website/pages/mining_key.html
  34. 93
      website/pages/payments.html
  35. 350
      website/pages/stats.html
  36. 64
      website/pages/tbs.html
  37. 94
      website/pages/workers.html
  38. 100
      website/static/admin.js
  39. BIN
      website/static/favicon.png
  40. 13
      website/static/logo.svg
  41. 30
      website/static/main.js
  42. 246
      website/static/miner_stats.js
  43. 1
      website/static/nvd3.css
  44. 6
      website/static/nvd3.js
  45. 143
      website/static/stats.js
  46. 68
      website/static/style.css

5
.gitignore

@ -0,0 +1,5 @@
node_modules/
.idea/
config.json
pool_configs/*.json
!pool_configs/zclassic_example.json

7
LICENSE

@ -0,0 +1,7 @@
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

11
Old/CHANGELOG.md

@ -0,0 +1,11 @@
# Changelog
### 20161127
* Fixed payment processor crashes
* Fixed varDiff and set default to 0.125
* Fixed merkleRoot issue with multiple transactions in each block
* Fixed incorrect hashrates
* Begin compatibility testing with Node 7+
* Added t_address -> z_address -> t_address coin transfer function

7
coins/komodo.json

@ -0,0 +1,7 @@
{
"name": "komodo",
"symbol": "KMD",
"algorithm": "equihash",
"peerMagic": "f9eee48d",
"txfee": 0.0001
}

517
init.js

@ -0,0 +1,517 @@
var fs = require('fs');
var path = require('path');
var os = require('os');
var cluster = require('cluster');
var async = require('async');
var extend = require('extend');
var redis = require('redis');
var PoolLogger = require('./libs/logUtil.js');
var CliListener = require('./libs/cliListener.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');
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'})));
var poolConfigs;
var logger = new PoolLogger({
logLevel: portalConfig.logLevel,
logColors: portalConfig.logColors
});
try {
require('newrelic');
if (cluster.isMaster)
logger.debug('NewRelic', 'Monitor', 'New Relic initiated');
} catch(e) {}
//Try to give process ability to handle 100k concurrent connections
try{
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');
}
finally {
// Find out which user used sudo through the environment variable
var uid = parseInt(process.env.SUDO_UID);
// Set our server's uid to that user
if (uid) {
process.setuid(uid);
logger.debug('POSIX', 'Connection Limit', 'Raised to 100K concurrent connections, now running as non-root user: ' + process.getuid());
}
}
}
catch(e){
if (cluster.isMaster)
logger.debug('POSIX', 'Connection Limit', '(Safe to ignore) POSIX module not installed and resource (connection) limit was not raised');
}
if (cluster.isWorker){
switch(process.env.workerType){
case 'pool':
new PoolWorker(logger);
break;
case 'paymentProcessor':
new PaymentProcessor(logger);
break;
case 'website':
new Website(logger);
break;
case 'profitSwitch':
new ProfitSwitch(logger);
break;
}
return;
}
//Read all pool configs from pool_configs and join them with their coin profile
var buildPoolConfigs = function(){
var configs = {};
var configDir = 'pool_configs/';
var poolConfigFiles = [];
/* Get filenames of pool config json files that are enabled */
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;
poolOptions.fileName = file;
poolConfigFiles.push(poolOptions);
});
/* Ensure no pool uses any of the same ports as another pool */
for (var i = 0; i < poolConfigFiles.length; i++){
var ports = Object.keys(poolConfigFiles[i].ports);
for (var f = 0; f < poolConfigFiles.length; f++){
if (f === i) continue;
var portsF = Object.keys(poolConfigFiles[f].ports);
for (var g = 0; g < portsF.length; g++){
if (ports.indexOf(portsF[g]) !== -1){
logger.error('Master', poolConfigFiles[f].fileName, 'Has same configured port of ' + portsF[g] + ' as ' + poolConfigFiles[i].fileName);
process.exit(1);
return;
}
}
if (poolConfigFiles[f].coin === poolConfigFiles[i].coin){
logger.error('Master', poolConfigFiles[f].fileName, 'Pool has same configured coin file coins/' + poolConfigFiles[f].coin + ' as ' + poolConfigFiles[i].fileName + ' pool');
process.exit(1);
return;
}
}
}
poolConfigFiles.forEach(function(poolOptions){
poolOptions.coinFileName = poolOptions.coin;
var coinFilePath = 'coins/' + poolOptions.coinFileName;
if (!fs.existsSync(coinFilePath)){
logger.error('Master', poolOptions.coinFileName, 'could not find file: ' + coinFilePath);
return;
}
var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'})));
poolOptions.coin = coinProfile;
poolOptions.coin.name = poolOptions.coin.name.toLowerCase();
if (poolOptions.coin.name in configs){
logger.error('Master', poolOptions.fileName, 'coins/' + poolOptions.coinFileName
+ ' has same configured coin name ' + poolOptions.coin.name + ' as coins/'
+ configs[poolOptions.coin.name].coinFileName + ' used by pool config '
+ configs[poolOptions.coin.name].fileName);
process.exit(1);
return;
}
for (var option in portalConfig.defaultPoolConfigs){
if (!(option in poolOptions)){
var toCloneOption = portalConfig.defaultPoolConfigs[option];
var clonedOption = {};
if (toCloneOption.constructor === Object)
extend(true, clonedOption, toCloneOption);
else
clonedOption = toCloneOption;
poolOptions[option] = clonedOption;
}
}
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;
};
function roundTo(n, digits) {
if (digits === undefined) {
digits = 0;
}
var multiplicator = Math.pow(10, digits);
n = parseFloat((n * multiplicator).toFixed(11));
var test =(Math.round(n) / multiplicator);
return +(test.toFixed(digits));
}
var _lastStartTimes = [];
var _lastShareTimes = [];
var spawnPoolWorkers = function(){
var redisConfig;
var connection;
Object.keys(poolConfigs).forEach(function(coin){
var pcfg = poolConfigs[coin];
if (!Array.isArray(pcfg.daemons) || pcfg.daemons.length < 1){
logger.error('Master', coin, 'No daemons configured so a pool cannot be started for this coin.');
delete poolConfigs[coin];
} else if (!connection) {
redisConfig = pcfg.redis;
connection = redis.createClient(redisConfig.port, redisConfig.host);
connection.on('ready', function(){
logger.debug('PPLNT', coin, 'TimeShare processing setup with redis (' + redisConfig.host +
':' + redisConfig.port + ')');
});
}
});
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(){
if (!portalConfig.clustering || !portalConfig.clustering.enabled)
return 1;
if (portalConfig.clustering.forks === 'auto')
return os.cpus().length;
if (!portalConfig.clustering.forks || isNaN(portalConfig.clustering.forks))
return 1;
return portalConfig.clustering.forks;
})();
var poolWorkers = {};
var createPoolWorker = function(forkId){
var worker = cluster.fork({
workerType: 'pool',
forkId: forkId,
pools: serializedConfigs,
portalConfig: JSON.stringify(portalConfig)
});
worker.forkId = forkId;
worker.type = 'pool';
poolWorkers[forkId] = worker;
worker.on('exit', function(code, signal){
logger.error('Master', 'PoolSpawner', '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;
case 'shareTrack':
// pplnt time share tracking of workers
if (msg.isValidShare && !msg.isValidBlock) {
var now = Date.now();
var lastShareTime = now;
var lastStartTime = now;
var workerAddress = msg.data.worker.split('.')[0];
// if needed, initialize PPLNT objects for coin
if (!_lastShareTimes[msg.coin]) {
_lastShareTimes[msg.coin] = {};
}
if (!_lastStartTimes[msg.coin]) {
_lastStartTimes[msg.coin] = {};
}
// did they just join in this round?
if (!_lastShareTimes[msg.coin][workerAddress] || !_lastStartTimes[msg.coin][workerAddress]) {
_lastShareTimes[msg.coin][workerAddress] = now;
_lastStartTimes[msg.coin][workerAddress] = now;
logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+' joined.');
}
// grab last times from memory objects
if (_lastShareTimes[msg.coin][workerAddress] != null && _lastShareTimes[msg.coin][workerAddress] > 0) {
lastShareTime = _lastShareTimes[msg.coin][workerAddress];
lastStartTime = _lastStartTimes[msg.coin][workerAddress];
}
var redisCommands = [];
// if its been less than 15 minutes since last share was submitted
var timeChangeSec = roundTo(Math.max(now - lastShareTime, 0) / 1000, 4);
//var timeChangeTotal = roundTo(Math.max(now - lastStartTime, 0) / 1000, 4);
if (timeChangeSec < 900) {
// loyal miner keeps mining :)
redisCommands.push(['hincrbyfloat', msg.coin + ':shares:timesCurrent', workerAddress + "." + poolConfigs[msg.coin].poolId, timeChangeSec]);
//logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+':{totalTimeSec:'+timeChangeTotal+', timeChangeSec:'+timeChangeSec+'}');
connection.multi(redisCommands).exec(function(err, replies){
if (err)
logger.error('PPLNT', msg.coin, 'Thread '+msg.thread, 'Error with time share processor call to redis ' + JSON.stringify(err));
});
} else {
// they just re-joined the pool
_lastStartTimes[workerAddress] = now;
logger.debug('PPLNT', msg.coin, 'Thread '+msg.thread, workerAddress+' re-joined.');
}
// track last time share
_lastShareTimes[msg.coin][workerAddress] = now;
}
if (msg.isValidBlock) {
// reset pplnt share times for next round
_lastShareTimes[msg.coin] = {};
_lastStartTimes[msg.coin] = {};
}
break;
}
});
};
var i = 0;
var spawnInterval = setInterval(function(){
createPoolWorker(i);
i++;
if (i === numForks){
clearInterval(spawnInterval);
logger.debug('Master', 'PoolSpawner', 'Spawned ' + Object.keys(poolConfigs).length + ' pool(s) on ' + numForks + ' thread(s)');
}
}, 250);
};
var startCliListener = function(){
var cliPort = portalConfig.cliPort;
var listener = new CliListener(cliPort);
listener.on('log', function(text){
logger.debug('Master', 'CLI', text);
}).on('command', function(command, params, options, reply){
switch(command){
case 'blocknotify':
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({type: 'blocknotify', coin: params[0], hash: params[1]});
});
reply('Pool workers notified');
break;
case 'coinswitch':
processCoinSwitchCommand(params, options, reply);
break;
case 'reloadpool':
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({type: 'reloadpool', coin: params[0] });
});
reply('reloaded pool ' + params[0]);
break;
default:
reply('unrecognized command "' + command + '"');
break;
}
}).start();
};
var processCoinSwitchCommand = function(params, options, reply){
var logSystem = 'CLI';
var logComponent = 'coinswitch';
var replyError = function(msg){
reply(msg);
logger.error(logSystem, logComponent, msg);
};
if (!params[0]) {
replyError('Coin name required');
return;
}
if (!params[1] && !options.algorithm){
replyError('If switch key is not provided then algorithm options must be specified');
return;
}
else if (params[1] && !portalConfig.switching[params[1]]){
replyError('Switch key not recognized: ' + params[1]);
return;
}
else if (options.algorithm && !Object.keys(portalConfig.switching).filter(function(s){
return portalConfig.switching[s].algorithm === options.algorithm;
})[0]){
replyError('No switching options contain the algorithm ' + options.algorithm);
return;
}
var messageCoin = params[0].toLowerCase();
var newCoin = Object.keys(poolConfigs).filter(function(p){
return p.toLowerCase() === messageCoin;
})[0];
if (!newCoin){
replyError('Switch message to coin that is not recognized: ' + messageCoin);
return;
}
var switchNames = [];
if (params[1]) {
switchNames.push(params[1]);
}
else{
for (var name in portalConfig.switching){
if (portalConfig.switching[name].enabled && portalConfig.switching[name].algorithm === options.algorithm)
switchNames.push(name);
}
}
switchNames.forEach(function(name){
if (poolConfigs[newCoin].coin.algorithm !== portalConfig.switching[name].algorithm){
replyError('Cannot switch a '
+ portalConfig.switching[name].algorithm
+ ' algo pool to coin ' + newCoin + ' with ' + poolConfigs[newCoin].coin.algorithm + ' algo');
return;
}
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send({type: 'coinswitch', coin: newCoin, switchName: name });
});
});
reply('Switch message sent to pool workers');
};
var startPaymentProcessor = function(){
var enabledForAny = false;
for (var pool in poolConfigs){
var p = poolConfigs[pool];
var enabled = p.enabled && p.paymentProcessing && p.paymentProcessing.enabled;
if (enabled){
enabledForAny = true;
break;
}
}
if (!enabledForAny)
return;
var worker = cluster.fork({
workerType: 'paymentProcessor',
pools: JSON.stringify(poolConfigs)
});
worker.on('exit', function(code, signal){
logger.error('Master', 'Payment Processor', 'Payment processor died, spawning replacement...');
setTimeout(function(){
startPaymentProcessor(poolConfigs);
}, 2000);
});
};
var startWebsite = function(){
if (!portalConfig.website.enabled) return;
var worker = cluster.fork({
workerType: 'website',
pools: JSON.stringify(poolConfigs),
portalConfig: JSON.stringify(portalConfig)
});
worker.on('exit', function(code, signal){
logger.error('Master', 'Website', 'Website process died, spawning replacement...');
setTimeout(function(){
startWebsite(portalConfig, poolConfigs);
}, 2000);
});
};
var startProfitSwitch = function(){
if (!portalConfig.profitSwitch || !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(){
poolConfigs = buildPoolConfigs();
spawnPoolWorkers();
startPaymentProcessor();
startWebsite();
startProfitSwitch();
startCliListener();
})();

135
libs/api.js

@ -0,0 +1,135 @@
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.header('Content-Type', 'application/json');
res.end(portalStats.statsString);
return;
case 'pool_stats':
res.header('Content-Type', 'application/json');
res.end(JSON.stringify(portalStats.statPoolHistory));
return;
case 'blocks':
case 'getblocksstats':
portalStats.getBlocks(function(data){
res.header('Content-Type', 'application/json');
res.end(JSON.stringify(data));
});
break;
case 'payments':
var poolBlocks = [];
for(var pool in portalStats.stats.pools) {
poolBlocks.push({name: pool, pending: portalStats.stats.pools[pool].pending, payments: portalStats.stats.pools[pool].payments});
}
res.header('Content-Type', 'application/json');
res.end(JSON.stringify(poolBlocks));
return;
case 'worker_stats':
res.header('Content-Type', 'application/json');
if (req.url.indexOf("?")>0) {
var url_parms = req.url.split("?");
if (url_parms.length > 0) {
var history = {};
var workers = {};
var address = url_parms[1] || null;
//res.end(portalStats.getWorkerStats(address));
if (address != null && address.length > 0) {
// make sure it is just the miners address
address = address.split(".")[0];
// get miners balance along with worker balances
portalStats.getBalanceByAddress(address, function(balances) {
// get current round share total
portalStats.getTotalSharesByAddress(address, function(shares) {
var totalHash = parseFloat(0.0);
var totalShares = shares;
var networkSols = 0;
for (var h in portalStats.statHistory) {
for(var pool in portalStats.statHistory[h].pools) {
for(var w in portalStats.statHistory[h].pools[pool].workers){
if (w.startsWith(address)) {
if (history[w] == null) {
history[w] = [];
}
if (portalStats.statHistory[h].pools[pool].workers[w].hashrate) {
history[w].push({time: portalStats.statHistory[h].time, hashrate:portalStats.statHistory[h].pools[pool].workers[w].hashrate});
}
}
}
// order check...
//console.log(portalStats.statHistory[h].time);
}
}
for(var pool in portalStats.stats.pools) {
for(var w in portalStats.stats.pools[pool].workers){
if (w.startsWith(address)) {
workers[w] = portalStats.stats.pools[pool].workers[w];
for (var b in balances.balances) {
if (w == balances.balances[b].worker) {
workers[w].paid = balances.balances[b].paid;
workers[w].balance = balances.balances[b].balance;
}
}
workers[w].balance = (workers[w].balance || 0);
workers[w].paid = (workers[w].paid || 0);
totalHash += portalStats.stats.pools[pool].workers[w].hashrate;
networkSols = portalStats.stats.pools[pool].poolStats.networkSols;
}
}
}
res.end(JSON.stringify({miner: address, totalHash: totalHash, totalShares: totalShares, networkSols: networkSols, immature: balances.totalImmature, balance: balances.totalHeld, paid: balances.totalPaid, workers: workers, history: history}));
});
});
} else {
res.end(JSON.stringify({result: "error"}));
}
} else {
res.end(JSON.stringify({result: "error"}));
}
} else {
res.end(JSON.stringify({result: "error"}));
}
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;
res.flush();
req.on("close", function() {
delete _this.liveStatConnections[uid];
});
return;
default:
next();
}
};
this.handleAdminApiRequest = function(req, res, next){
switch(req.params.method){
case 'pools': {
res.end(JSON.stringify({result: poolConfigs}));
return;
}
default:
next();
}
};
};

222
libs/apiBittrex.js

@ -0,0 +1,222 @@
var request = require('request');
var nonce = require('nonce');
module.exports = function() {
'use strict';
// Module dependencies
// Constants
var version = '0.1.0',
PUBLIC_API_URL = 'https://bittrex.com/api/v1/public',
PRIVATE_API_URL = 'https://bittrex.com/api/v1/market',
USER_AGENT = 'nomp/node-open-mining-portal'
// Constructor
function Bittrex(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 'Bittrex: 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
Bittrex.STRICT_SSL = true;
// Helper methods
function joinCurrencies(currencyA, currencyB){
return currencyA + '-' + currencyB;
}
// Prototype
Bittrex.prototype = {
constructor: Bittrex,
// 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 = Bittrex.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 options = {
method: 'GET',
url: PUBLIC_API_URL + '/getmarketsummaries',
qs: null
};
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 = {
market: joinCurrencies(currencyA, currencyB),
type: 'buy',
depth: '50'
}
var options = {
method: 'GET',
url: PUBLIC_API_URL + '/getorderbook',
qs: parameters
}
return this._request(options, 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 Bittrex;
}();

115
libs/apiCoinWarz.js

@ -0,0 +1,115 @@
var request = require('request');
var nonce = require('nonce');
module.exports = function() {
'use strict';
// Module dependencies
// Constants
var version = '0.0.1',
PUBLIC_API_URL = 'http://www.coinwarz.com/v1/api/profitability/?apikey=YOUR_API_KEY&algo=all',
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 'CoinWarz: 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
CoinWarz.prototype = {
constructor: CoinWarz,
// 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 = CoinWarz.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);
},
/////
// 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);
},
////
return CoinWarz;
}();

204
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;
}();

216
libs/apiMintpal.js

@ -0,0 +1,216 @@
var request = require('request');
var nonce = require('nonce');
module.exports = function() {
'use strict';
// Module dependencies
// Constants
var version = '0.1.0',
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 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 'Mintpal: 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
Mintpal.STRICT_SSL = true;
// Helper methods
function joinCurrencies(currencyA, currencyB){
return currencyA + '_' + currencyB;
}
// Prototype
Mintpal.prototype = {
constructor: Mintpal,
// 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 = Mintpal.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 options = {
method: 'GET',
url: PUBLIC_API_URL + '/summary',
qs: null
};
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',
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 Mintpal;
}();

212
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 = 'https://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;
}();

45
libs/cliListener.js

@ -0,0 +1,45 @@
var events = require('events');
var net = require('net');
var listener = module.exports = function listener(port){
var _this = this;
var emitLog = function(text){
_this.emit('log', text);
};
this.start = function(){
net.createServer(function(c) {
var data = '';
try {
c.on('data', function (d) {
data += d;
if (data.slice(-1) === '\n') {
var message = JSON.parse(data);
_this.emit('command', message.command, message.params, message.options, function(message){
c.end(message);
});
}
});
c.on('end', function () {
});
c.on('error', function () {
});
}
catch(e){
emitLog('CLI listener failed to parse message ' + data);
}
}).listen(port, '127.0.0.1', function() {
emitLog('CLI listening on port ' + port)
});
}
};
listener.prototype.__proto__ = events.EventEmitter.prototype;

89
libs/logUtil.js

@ -0,0 +1,89 @@
var dateFormat = require('dateformat');
var colors = require('colors');
var severityToColor = function(severity, text) {
switch(severity) {
case 'special':
return text.cyan.underline;
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 severityValues = {
'debug': 1,
'warning': 2,
'error': 3,
'special': 4
};
var PoolLogger = function (configuration) {
var logLevelInt = severityValues[configuration.logLevel];
var logColors = configuration.logColors;
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';
if (logColors) {
entryDesc = severityToColor(severity, entryDesc);
var logString =
entryDesc +
('[' + component + '] ').italic;
if (subcat)
logString += ('(' + subcat + ') ').bold.grey;
logString += text.grey;
}
else {
var logString =
entryDesc +
'[' + component + '] ';
if (subcat)
logString += '(' + subcat + ') ';
logString += text;
}
console.log(logString);
};
// public
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;

122
libs/mposCompatibility.js

@ -0,0 +1,122 @@
var mysql = require('mysql');
var cluster = require('cluster');
module.exports = function(logger, poolConfig){
var mposConfig = poolConfig.mposMode;
var coin = poolConfig.coin.name;
var connection = mysql.createPool({
host: mposConfig.host,
port: mposConfig.port,
user: mposConfig.user,
password: mposConfig.password,
database: mposConfig.database
});
var logIdentify = 'MySQL';
var logComponent = coin;
this.handleAuth = function(workerName, password, authCallback){
if (poolConfig.validateWorkerUsername !== true && mposConfig.autoCreateWorker !== true){
authCallback(true);
return;
}
connection.query(
'SELECT password FROM pool_worker WHERE username = LOWER(?)',
[workerName.toLowerCase()],
function(err, result){
if (err){
logger.error(logIdentify, logComponent, 'Database error when authenticating worker: ' +
JSON.stringify(err));
authCallback(false);
}
else if (!result[0]){
if(mposConfig.autoCreateWorker){
var account = workerName.split('.')[0];
connection.query(
'SELECT id,username FROM accounts WHERE username = LOWER(?)',
[account.toLowerCase()],
function(err, result){
if (err){
logger.error(logIdentify, logComponent, 'Database error when authenticating account: ' +
JSON.stringify(err));
authCallback(false);
}else if(!result[0]){
authCallback(false);
}else{
connection.query(
"INSERT INTO `pool_worker` (`account_id`, `username`, `password`) VALUES (?, ?, ?);",
[result[0].id,workerName.toLowerCase(),password],
function(err, result){
if (err){
logger.error(logIdentify, logComponent, 'Database error when insert worker: ' +
JSON.stringify(err));
authCallback(false);
}else {
authCallback(true);
}
})
}
}
);
}
else{
authCallback(false);
}
}
else if (mposConfig.checkPassword && result[0].password !== password)
authCallback(false);
else
authCallback(true);
}
);
};
this.handleShare = function(isValidShare, isValidBlock, shareData){
var dbData = [
shareData.ip,
shareData.worker,
isValidShare ? 'Y' : 'N',
isValidBlock ? 'Y' : 'N',
shareData.difficulty * (poolConfig.coin.mposDiffMultiplier || 1),
typeof(shareData.error) === 'undefined' ? null : shareData.error,
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 = ?',
dbData,
function(err, result) {
if (err)
logger.error(logIdentify, logComponent, 'Insert error when adding share: ' + JSON.stringify(err));
else
logger.debug(logIdentify, logComponent, 'Share inserted');
}
);
};
this.handleDifficultyUpdate = function(workerName, diff){
connection.query(
'UPDATE `pool_worker` SET `difficulty` = ' + diff + ' WHERE `username` = ' + connection.escape(workerName),
function(err, result){
if (err)
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});
}
else
console.log('Updated difficulty successfully', result);
}
);
};
};

1426
libs/paymentProcessor.js

File diff suppressed because it is too large

329
libs/poolWorker.js

@ -0,0 +1,329 @@
var Stratum = require('stratum-pool');
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);
var forkId = process.env.forkId;
var pools = {};
var proxySwitch = {};
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
if (portalConfig.redis.password) {
redisClient.auth(portalConfig.redis.password);
}
//Handle messages from master process sent via IPC
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();
var poolTarget = Object.keys(pools).filter(function(p){
return p.toLowerCase() === messageCoin;
})[0];
if (poolTarget)
pools[poolTarget].processBlockNotify(message.hash, 'blocknotify script');
break;
// IPC message for pool switching
case 'coinswitch':
var logSystem = 'Proxy';
var logComponent = 'Switch';
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
var switchName = message.switchName;
var newCoin = message.coin;
var algo = poolConfigs[newCoin].coin.algorithm;
var newPool = pools[newCoin];
var oldCoin = proxySwitch[switchName].currentPool;
var oldPool = pools[oldCoin];
var proxyPorts = Object.keys(proxySwitch[switchName].ports);
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(proxyPorts.indexOf(miner.client.socket.localPort.toString()) !== -1)
},
function (clients) {
newPool.attachMiners(clients);
}
);
proxySwitch[switchName].currentPool = newCoin;
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;
}
});
Object.keys(poolConfigs).forEach(function(coin) {
var poolOptions = poolConfigs[coin];
var logSystem = 'Pool';
var logComponent = coin;
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
var handlers = {
auth: function(){},
share: function(){},
diff: function(){}
};
//Functions required for MPOS compatibility
if (poolOptions.mposMode && poolOptions.mposMode.enabled){
var mposCompat = new MposCompatibility(logger, poolOptions);
handlers.auth = function(port, workerName, password, authCallback){
mposCompat.handleAuth(workerName, password, authCallback);
};
handlers.share = function(isValidShare, isValidBlock, data){
mposCompat.handleShare(isValidShare, isValidBlock, data);
};
handlers.diff = function(workerName, diff){
mposCompat.handleDifficultyUpdate(workerName, diff);
}
}
//Functions required for internal payment processing
else{
var shareProcessor = new ShareProcessor(logger, poolOptions);
handlers.auth = function(port, workerName, password, authCallback){
if (poolOptions.validateWorkerUsername !== true)
authCallback(true);
else {
pool.daemon.cmd('validateaddress', [String(workerName).split(".")[0]], function (results) {
var isValid = results.filter(function (r) {
return r.response.isvalid
}).length > 0;
authCallback(isValid);
});
}
};
handlers.share = function(isValidShare, isValidBlock, data){
shareProcessor.handleShare(isValidShare, isValidBlock, data);
};
}
var authorizeFN = function (ip, port, workerName, password, callback) {
handlers.auth(port, workerName, password, function(authorized){
var authString = authorized ? 'Authorized' : 'Unauthorized ';
logger.debug(logSystem, logComponent, logSubCat, authString + ' ' + workerName + ':' + password + ' [' + ip + ']');
callback({
error: null,
authorized: authorized,
disconnect: false
});
});
};
var pool = Stratum.createPool(poolOptions, authorizeFN, logger);
pool.on('share', function(isValidShare, isValidBlock, data){
var shareData = JSON.stringify(data);
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 found: ' + data.blockHash + ' by ' + data.worker);
if (isValidShare) {
if(data.shareDiff > 1000000000) {
logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000.000!');
} else if(data.shareDiff > 1000000) {
logger.debug(logSystem, logComponent, logSubCat, 'Share was found with diff higher than 1.000.000!');
}
//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);
}
// handle the share
handlers.share(isValidShare, isValidBlock, data);
// send to master for pplnt time tracking
process.send({type: 'shareTrack', thread:(parseInt(forkId)+1), coin:poolOptions.coin.name, isValidShare:isValidShare, isValidBlock:isValidBlock, data: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);
}).on('banIP', function(ip, worker){
process.send({type: 'banIP', ip: ip});
}).on('started', function(){
_this.setDifficultyForProxyPort(pool, poolOptions.coin.name, poolOptions.coin.algorithm);
});
pool.start();
pools[poolOptions.coin.name] = pool;
});
if (portalConfig.switching) {
var logSystem = 'Switching';
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');
/*redisClient.on('error', function(err){
logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err);
});*/
redisClient.hgetall("proxyState", function(error, obj) {
if (!error && obj) {
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.switching).forEach(function(switchName) {
var algorithm = portalConfig.switching[switchName].algorithm;
if (!portalConfig.switching[switchName].enabled) return;
var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm] : _this.getFirstPoolForAlgorithm(algorithm);
proxySwitch[switchName] = {
algorithm: algorithm,
ports: portalConfig.switching[switchName].ports,
currentPool: initalPool,
servers: []
};
Object.keys(proxySwitch[switchName].ports).forEach(function(port){
var f = net.createServer(function(socket) {
var currentPool = proxySwitch[switchName].currentPool;
logger.debug(logSystem, 'Connect', logSubCat, 'Connection to '
+ switchName + ' from '
+ socket.remoteAddress + ' on '
+ port + ' routing to ' + currentPool);
if (pools[currentPool])
pools[currentPool].getStratumServer().handleNewClient(socket);
else
pools[initalPool].getStratumServer().handleNewClient(socket);
}).listen(parseInt(port), function() {
logger.debug(logSystem, logComponent, logSubCat, 'Switching "' + switchName
+ '" listening for ' + algorithm
+ ' on port ' + port
+ ' into ' + proxySwitch[switchName].currentPool);
});
proxySwitch[switchName].servers.push(f);
});
});
});
}
this.getFirstPoolForAlgorithm = function(algorithm) {
var foundCoin = "";
Object.keys(poolConfigs).forEach(function(coinName) {
if (poolConfigs[coinName].coin.algorithm == algorithm) {
if (foundCoin === "")
foundCoin = coinName;
}
});
return foundCoin;
};
//
// Called when stratum pool emits its 'started' event to copy the initial diff and vardiff
// configuation for any proxy switching ports configured into the stratum pool object.
//
this.setDifficultyForProxyPort = function(pool, coin, algo) {
logger.debug(logSystem, logComponent, algo, 'Setting proxy difficulties after pool start');
Object.keys(portalConfig.switching).forEach(function(switchName) {
if (!portalConfig.switching[switchName].enabled) return;
var switchAlgo = portalConfig.switching[switchName].algorithm;
if (pool.options.coin.algorithm !== switchAlgo) return;
// we know the switch configuration matches the pool's algo, so setup the diff and
// vardiff for each of the switch's ports
for (var port in portalConfig.switching[switchName].ports) {
if (portalConfig.switching[switchName].ports[port].varDiff)
pool.setVarDiff(port, portalConfig.switching[switchName].ports[port].varDiff);
if (portalConfig.switching[switchName].ports[port].diff){
if (!pool.options.ports.hasOwnProperty(port))
pool.options.ports[port] = {};
pool.options.ports[port].diff = portalConfig.switching[switchName].ports[port].diff;
}
}
});
};
};

666
libs/profitSwitch.js

@ -0,0 +1,666 @@
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 Poloniex = require('./apiPoloniex.js');
var Mintpal = require('./apiMintpal.js');
var Bittrex = require('./apiBittrex.js');
var Stratum = require('stratum-pool');
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 symbolToAlgorithmMap = {};
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,
exchangeInfo: {}
};
profitStatus[algo][poolConfig.coin.symbol] = coinStatus;
symbolToAlgorithmMap[poolConfig.coin.symbol] = algo;
});
//
// ensure we have something to switch
//
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];
});
}
});
if (Object.keys(profitStatus).length == 0){
logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.');
return;
}
//
// setup APIs
//
var poloApi = new Poloniex(
// 'API_KEY',
// 'API_SECRET'
);
var cryptsyApi = new Cryptsy(
// 'API_KEY',
// 'API_SECRET'
);
var mintpalApi = new Mintpal(
// 'API_KEY',
// 'API_SECRET'
);
var bittrexApi = new Bittrex(
// '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(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)) {
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)) {
if (!marketData.hasOwnProperty('LTC'))
marketData['LTC'] = {};
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
if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) {
var btcLtc = data['BTC_LTC'];
marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid);
}
});
taskCallback();
});
},
function(taskCallback){
var depthTasks = [];
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){
taskCallback();
return;
}
async.series(depthTasks, function(err){
if (err){
taskCallback(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);
var totalQty = new Number(0);
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 >= 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();
});
};
this.getProfitDataCryptsy = function(callback){
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 && 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);
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 (results && results.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);
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);
}
}
});
taskCallback();
});
}
], function(err){
if (err){
callback(err);
return;
}
callback(null);
});
};
this.getProfitDataMintpal = function(callback){
async.series([
function(taskCallback){
mintpalApi.getTicker(function(err, response){
if (err || !response.data){
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);
}
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){
callback(err);
return;
}
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')){
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 qty = new Number(order.amount);
// 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();
});
};
this.getProfitDataBittrex = function(callback){
async.series([
function(taskCallback){
bittrexApi.getTicker(function(err, response){
if (err || !response.result){
taskCallback(err);
return;
}
Object.keys(symbolToAlgorithmMap).forEach(function(symbol){
response.result.forEach(function(market){
var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo;
if (!exchangeInfo.hasOwnProperty('Bittrex'))
exchangeInfo['Bittrex'] = {};
var marketData = exchangeInfo['Bittrex'];
var marketPair = market.MarketName.match(/([\w]+)-([\w-_]+)/)
market.exchange = marketPair[1]
market.code = marketPair[2]
if (market.exchange == 'BTC' && market.code == symbol) {
if (!marketData.hasOwnProperty('BTC'))
marketData['BTC'] = {};
marketData['BTC'].last = new Number(market.Last);
marketData['BTC'].baseVolume = new Number(market.BaseVolume);
marketData['BTC'].quoteVolume = new Number(market.BaseVolume / market.Last);
marketData['BTC'].ask = new Number(market.Ask);
marketData['BTC'].bid = new Number(market.Bid);
}
if (market.exchange == 'LTC' && market.code == symbol) {
if (!marketData.hasOwnProperty('LTC'))
marketData['LTC'] = {};
marketData['LTC'].last = new Number(market.Last);
marketData['LTC'].baseVolume = new Number(market.BaseVolume);
marketData['LTC'].quoteVolume = new Number(market.BaseVolume / market.Last);
marketData['LTC'].ask = new Number(market.Ask);
marketData['LTC'].bid = new Number(market.Bid);
}
});
});
taskCallback();
});
},
function(taskCallback){
var depthTasks = [];
Object.keys(symbolToAlgorithmMap).forEach(function(symbol){
var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Bittrex'];
if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){
depthTasks.push(function(callback){
_this.getMarketDepthFromBittrex('BTC', symbol, marketData['BTC'].bid, callback)
});
}
if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){
depthTasks.push(function(callback){
_this.getMarketDepthFromBittrex('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){
callback(err);
return;
}
callback(null);
});
};
this.getMarketDepthFromBittrex = function(symbolA, symbolB, coinPrice, callback){
bittrexApi.getOrderBook(symbolA, symbolB, function(err, response){
if (err){
callback(err);
return;
}
var depth = new Number(0);
if (response.hasOwnProperty('result')){
var totalQty = new Number(0);
response['result'].forEach(function(order){
var price = new Number(order.Rate);
var limit = new Number(coinPrice * portalConfig.profitSwitch.depth);
var qty = new Number(order.Quantity);
// only measure the depth down to configured depth
if (price >= limit){
depth += (qty * price);
totalQty += qty;
}
});
}
var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Bittrex'];
marketData[symbolA].depth = depth;
if (totalQty > 0)
marketData[symbolA].weightedBid = new Number(depth / totalQty);
callback();
});
};
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.paymentProcessing.daemon;
daemonTasks.push(function(callback){
_this.getDaemonInfoForCoin(symbol, daemonConfig, callback)
});
});
});
if (daemonTasks.length == 0){
callback();
return;
}
async.series(daemonTasks, function(err){
if (err){
callback(err);
return;
}
callback(null);
});
};
this.getDaemonInfoForCoin = function(symbol, cfg, callback){
var daemon = new Stratum.daemon.interface([cfg], function(severity, message){
logger[severity](logSystem, symbol, message);
callback(null); // fail gracefully for each coin
});
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 / target.toNumber()).toFixed(9));
logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty);
coinStatus.reward = response.coinbasevalue / 100000000;
callback(null);
});
};
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 = 86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000));
coinStatus.coinsPerMhPerHour = 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 = 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 = 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/day per Mh/s');
}
if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){
var btcPerMhPerHour = (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/day per Mh/s');
}
});
});
logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s');
var client = net.connect(portalConfig.cliPort, function () {
client.write(JSON.stringify({
command: 'coinswitch',
params: [bestCoin],
options: {algorithm: algo}
}) + '\n');
}).on('error', function(error){
if (error.code === 'ECONNREFUSED')
logger.error(logSystem, 'CLI', 'Could not connect to NOMP instance on port ' + portalConfig.cliPort);
else
logger.error(logSystem, 'CLI', 'Socket error ' + JSON.stringify(error));
});
});
};
var checkProfitability = function(){
logger.debug(logSystem, 'Check', 'Collecting profitability data.');
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);
if (portalConfig.profitSwitch.useBittrex)
profitabilityTasks.push(_this.getProfitDataBittrex);
profitabilityTasks.push(_this.getCoindDaemonInfo);
profitabilityTasks.push(_this.getMiningRate);
// 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);
};

103
libs/shareProcessor.js

@ -0,0 +1,103 @@
var redis = require('redis');
var Stratum = require('stratum-pool');
/*
This module deals with handling shares when in internal payment processing mode. It connects to a redis
database and inserts shares with the database structure of:
key: coin_name + ':' + block_height
value: a hash with..
key:
*/
module.exports = function(logger, poolConfig){
var redisConfig = poolConfig.redis;
var coin = poolConfig.coin.name;
var forkId = process.env.forkId;
var logSystem = 'Pool';
var logComponent = coin;
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
var connection = redis.createClient(redisConfig.port, redisConfig.host);
if (redisConfig.password) {
connection.auth(redisConfig.password);
}
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 has been ended');
});
connection.info(function(error, response){
if (error){
logger.error(logSystem, logComponent, logSubCat, 'Redis version check failed');
return;
}
var parts = response.split('\r\n');
var version;
var versionString;
for (var i = 0; i < parts.length; i++){
if (parts[i].indexOf(':') !== -1){
var valParts = parts[i].split(':');
if (valParts[0] === 'redis_version'){
versionString = valParts[1];
version = parseFloat(versionString);
break;
}
}
}
if (!version){
logger.error(logSystem, logComponent, logSubCat, 'Could not detect redis version - but be super old or broken');
}
else if (version < 2.6){
logger.error(logSystem, logComponent, logSubCat, "You're using redis version " + versionString + " the minimum required version is 2.6. Follow the damn usage instructions...");
}
});
this.handleShare = function(isValidShare, isValidBlock, shareData) {
var redisCommands = [];
if (isValidShare) {
redisCommands.push(['hincrbyfloat', coin + ':shares:roundCurrent', shareData.worker, shareData.difficulty]);
redisCommands.push(['hincrby', coin + ':stats', 'validShares', 1]);
} else {
redisCommands.push(['hincrby', coin + ':stats', 'invalidShares', 1]);
}
/* 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. */
var dateNow = Date.now();
var hashrateData = [ isValidShare ? shareData.difficulty : -shareData.difficulty, shareData.worker, dateNow];
redisCommands.push(['zadd', coin + ':hashrate', dateNow / 1000 | 0, hashrateData.join(':')]);
if (isValidBlock){
redisCommands.push(['rename', coin + ':shares:roundCurrent', coin + ':shares:round' + shareData.height]);
redisCommands.push(['rename', coin + ':shares:timesCurrent', coin + ':shares:times' + shareData.height]);
redisCommands.push(['sadd', coin + ':blocksPending', [shareData.blockHash, shareData.txHash, shareData.height, shareData.worker, dateNow].join(':')]);
redisCommands.push(['hincrby', coin + ':stats', 'validBlocks', 1]);
}
else if (shareData.blockHash){
redisCommands.push(['hincrby', coin + ':stats', 'invalidBlocks', 1]);
}
connection.multi(redisCommands).exec(function(err, replies){
if (err)
logger.error(logSystem, logComponent, logSubCat, 'Error with share processor multi ' + JSON.stringify(err));
});
};
};

774
libs/stats.js

@ -0,0 +1,774 @@
var zlib = require('zlib');
var redis = require('redis');
var async = require('async');
var os = require('os');
var algos = require('stratum-pool/lib/algoProperties.js');
// redis callback Ready check failed bypass trick
function rediscreateClient(port, host, pass) {
var client = redis.createClient(port, host);
if (pass) {
client.auth(pass);
}
return client;
}
/**
* Sort object properties (only own properties will be sorted).
* @param {object} obj object to sort properties
* @param {string|int} sortedBy 1 - sort object properties by specific value.
* @param {bool} isNumericSort true - sort object properties as numeric value, false - sort as string value.
* @param {bool} reverse false - reverse sorting.
* @returns {Array} array of items in [[key,value],[key,value],...] format.
*/
function sortProperties(obj, sortedBy, isNumericSort, reverse) {
sortedBy = sortedBy || 1; // by default first key
isNumericSort = isNumericSort || false; // by default text sort
reverse = reverse || false; // by default no reverse
var reversed = (reverse) ? -1 : 1;
var sortable = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
sortable.push([key, obj[key]]);
}
}
if (isNumericSort)
sortable.sort(function (a, b) {
return reversed * (a[1][sortedBy] - b[1][sortedBy]);
});
else
sortable.sort(function (a, b) {
var x = a[1][sortedBy].toLowerCase(),
y = b[1][sortedBy].toLowerCase();
return x < y ? reversed * -1 : x > y ? reversed : 0;
});
return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
}
module.exports = function(logger, portalConfig, poolConfigs){
var _this = this;
var logSystem = 'Stats';
var redisClients = [];
var redisStats;
this.statHistory = [];
this.statPoolHistory = [];
this.stats = {};
this.statsString = '';
setupStatsRedis();
gatherStatHistory();
var canDoStats = true;
Object.keys(poolConfigs).forEach(function(coin){
if (!canDoStats) return;
var poolConfig = poolConfigs[coin];
var redisConfig = poolConfig.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: rediscreateClient(redisConfig.port, redisConfig.host, redisConfig.password)
});
});
function setupStatsRedis(){
redisStats = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
redisStats.on('error', function(err){
redisStats.auth(portalConfig.redis.password);
});
}
this.getBlocks = function (cback) {
var allBlocks = {};
async.each(_this.stats.pools, function(pool, pcb) {
if (_this.stats.pools[pool.name].pending && _this.stats.pools[pool.name].pending.blocks)
for (var i=0; i<_this.stats.pools[pool.name].pending.blocks.length; i++)
allBlocks[pool.name+"-"+_this.stats.pools[pool.name].pending.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].pending.blocks[i];
if (_this.stats.pools[pool.name].confirmed && _this.stats.pools[pool.name].confirmed.blocks)
for (var i=0; i<_this.stats.pools[pool.name].confirmed.blocks.length; i++)
allBlocks[pool.name+"-"+_this.stats.pools[pool.name].confirmed.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].confirmed.blocks[i];
pcb();
}, function(err) {
cback(allBlocks);
});
};
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);
});
});
}
function getWorkerStats(address) {
address = address.split(".")[0];
if (address.length > 0 && address.startsWith('t')) {
for (var h in statHistory) {
for(var pool in statHistory[h].pools) {
statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate);
for(var w in statHistory[h].pools[pool].workers){
if (w.startsWith(address)) {
if (history[w] == null) {
history[w] = [];
}
if (workers[w] == null && stats.pools[pool].workers[w] != null) {
workers[w] = stats.pools[pool].workers[w];
}
if (statHistory[h].pools[pool].workers[w].hashrate) {
history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate});
}
}
}
}
}
return JSON.stringify({"workers": workers, "history": history});
}
return null;
}
function addStatPoolHistory(stats){
var data = {
time: stats.time,
pools: {}
};
for (var pool in stats.pools){
data.pools[pool] = {
hashrate: stats.pools[pool].hashrate,
workerCount: stats.pools[pool].workerCount,
blocks: stats.pools[pool].blocks
}
}
_this.statPoolHistory.push(data);
}
var magnitude = 100000000;
var coinPrecision = magnitude.toString().length - 1;
function roundTo(n, digits) {
if (digits === undefined) {
digits = 0;
}
var multiplicator = Math.pow(10, digits);
n = parseFloat((n * multiplicator).toFixed(11));
var test =(Math.round(n) / multiplicator);
return +(test.toFixed(digits));
}
var satoshisToCoins = function(satoshis){
return roundTo((satoshis / magnitude), coinPrecision);
};
var coinsToSatoshies = function(coins){
return Math.round(coins * magnitude);
};
function coinsRound(number) {
return roundTo(number, coinPrecision);
}
function readableSeconds(t) {
var seconds = Math.round(t);
var minutes = Math.floor(seconds/60);
var hours = Math.floor(minutes/60);
var days = Math.floor(hours/24);
hours = hours-(days*24);
minutes = minutes-(days*24*60)-(hours*60);
seconds = seconds-(days*24*60*60)-(hours*60*60)-(minutes*60);
if (days > 0) { return (days + "d " + hours + "h " + minutes + "m " + seconds + "s"); }
if (hours > 0) { return (hours + "h " + minutes + "m " + seconds + "s"); }
if (minutes > 0) {return (minutes + "m " + seconds + "s"); }
return (seconds + "s");
}
this.getCoins = function(cback){
_this.stats.coins = redisClients[0].coins;
cback();
};
this.getPayout = function(address, cback){
async.waterfall([
function(callback){
_this.getBalanceByAddress(address, function(){
callback(null, 'test');
});
}
], function(err, total){
cback(coinsRound(total).toFixed(8));
});
};
this.getTotalSharesByAddress = function(address, cback) {
var a = address.split(".")[0];
var client = redisClients[0].client,
coins = redisClients[0].coins,
shares = [];
var pindex = parseInt(0);
var totalShares = parseFloat(0);
async.each(_this.stats.pools, function(pool, pcb) {
pindex++;
var coin = String(_this.stats.pools[pool.name].name);
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) {
if (error) {
pcb(error);
return;
}
var workerName="";
var shares = 0;
for (var i in result[1]) {
if (Math.abs(i % 2) != 1) {
workerName = String(result[1][i]);
} else {
shares += parseFloat(result[1][i]);
}
}
if (shares>0) {
totalShares = shares;
}
pcb();
});
}, function(err) {
if (err) {
cback(0);
return;
}
if (totalShares > 0 || (pindex >= Object.keys(_this.stats.pools).length)) {
cback(totalShares);
return;
}
});
};
this.getBalanceByAddress = function(address, cback){
var a = address.split(".")[0];
var client = redisClients[0].client,
coins = redisClients[0].coins,
balances = [];
var totalHeld = parseFloat(0);
var totalPaid = parseFloat(0);
var totalImmature = parseFloat(0);
async.each(_this.stats.pools, function(pool, pcb) {
var coin = String(_this.stats.pools[pool.name].name);
// get all immature balances from address
client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) {
// get all balances from address
client.hscan(coin + ':balances', 0, "match", a+"*", "count", 10000, function(error, bals) {
// get all payouts from address
client.hscan(coin + ':payouts', 0, "match", a+"*", "count", 10000, function(error, pays) {
var workerName = "";
var balAmount = 0;
var paidAmount = 0;
var pendingAmount = 0;
var workers = {};
for (var i in pays[1]) {
if (Math.abs(i % 2) != 1) {
workerName = String(pays[1][i]);
workers[workerName] = (workers[workerName] || {});
} else {
paidAmount = parseFloat(pays[1][i]);
workers[workerName].paid = coinsRound(paidAmount);
totalPaid += paidAmount;
}
}
for (var b in bals[1]) {
if (Math.abs(b % 2) != 1) {
workerName = String(bals[1][b]);
workers[workerName] = (workers[workerName] || {});
} else {
balAmount = parseFloat(bals[1][b]);
workers[workerName].balance = coinsRound(balAmount);
totalHeld += balAmount;
}
}
for (var b in pends[1]) {
if (Math.abs(b % 2) != 1) {
workerName = String(pends[1][b]);
workers[workerName] = (workers[workerName] || {});
} else {
pendingAmount = parseFloat(pends[1][b]);
workers[workerName].immature = coinsRound(pendingAmount);
totalImmature += pendingAmount;
}
}
for (var w in workers) {
balances.push({
worker:String(w),
balance:workers[w].balance,
paid:workers[w].paid,
immature:workers[w].immature
});
}
pcb();
});
});
});
}, function(err) {
if (err) {
callback("There was an error getting balances");
return;
}
_this.stats.balances = balances;
_this.stats.address = address;
cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances});
});
};
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.stats.hashrateWindow) | 0).toString();
var redisCommands = [];
var redisCommandTemplates = [
['zremrangebyscore', ':hashrate', '-inf', '(' + windowTime],
['zrangebyscore', ':hashrate', windowTime, '+inf'],
['hgetall', ':stats'],
['scard', ':blocksPending'],
['scard', ':blocksConfirmed'],
['scard', ':blocksKicked'],
['smembers', ':blocksPending'],
['smembers', ':blocksConfirmed'],
['hgetall', ':shares:roundCurrent'],
['hgetall', ':blocksPendingConfirms'],
['zrange', ':payments', -100, -1],
['hgetall', ':shares:timesCurrent']
];
var commandsPerCoin = redisCommandTemplates.length;
client.coins.map(function(coin){
redisCommandTemplates.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){
logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err));
callback(err);
}
else{
for(var i = 0; i < replies.length; i += commandsPerCoin){
var coinName = client.coins[i / commandsPerCoin | 0];
var marketStats = {};
if (replies[i + 2]) {
if (replies[i + 2].coinmarketcap) {
marketStats = replies[i + 2] ? (JSON.parse(replies[i + 2].coinmarketcap)[0] || 0) : 0;
}
}
var coinStats = {
name: coinName,
symbol: poolConfigs[coinName].coin.symbol.toUpperCase(),
algorithm: poolConfigs[coinName].coin.algorithm,
hashrates: replies[i + 1],
poolStats: {
validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0,
validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0,
invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0,
totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0,
networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0,
networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0,
networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0),
networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0,
networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0,
networkVersion: replies[i + 2] ? (replies[i + 2].networkSubVersion || 0) : 0,
networkProtocolVersion: replies[i + 2] ? (replies[i + 2].networkProtocolVersion || 0) : 0
},
marketStats: marketStats,
/* block stat counts */
blocks: {
pending: replies[i + 3],
confirmed: replies[i + 4],
orphaned: replies[i + 5]
},
/* show all pending blocks */
pending: {
blocks: replies[i + 6].sort(sortBlocks),
confirms: (replies[i + 9] || {})
},
/* show last 50 found blocks */
confirmed: {
blocks: replies[i + 7].sort(sortBlocks).slice(0,50)
},
payments: [],
currentRoundShares: (replies[i + 8] || {}),
currentRoundTimes: (replies[i + 11] || {}),
maxRoundTime: 0,
shareCount: 0
};
for(var j = replies[i + 10].length; j > 0; j--){
var jsonObj;
try {
jsonObj = JSON.parse(replies[i + 10][j-1]);
} catch(e) {
jsonObj = null;
}
if (jsonObj !== null) {
coinStats.payments.push(jsonObj);
}
}
allCoinStats[coinStats.name] = (coinStats);
}
// sort pools alphabetically
allCoinStats = sortPoolsByName(allCoinStats);
callback();
}
});
}, function(err){
if (err){
logger.error(logSystem, 'Global', 'error getting all stats' + JSON.stringify(err));
callback();
return;
}
var portalStats = {
time: statGatherTime,
global:{
workers: 0,
hashrate: 0
},
algos: {},
pools: allCoinStats
};
Object.keys(allCoinStats).forEach(function(coin){
var coinStats = allCoinStats[coin];
coinStats.workers = {};
coinStats.miners = {};
coinStats.shares = 0;
coinStats.hashrates.forEach(function(ins){
var parts = ins.split(':');
var workerShares = parseFloat(parts[0]);
var miner = parts[1].split('.')[0];
var worker = parts[1];
var diff = Math.round(parts[0] * 8192);
if (workerShares > 0) {
coinStats.shares += workerShares;
// build worker stats
if (worker in coinStats.workers) {
coinStats.workers[worker].shares += workerShares;
coinStats.workers[worker].diff = diff;
} else {
coinStats.workers[worker] = {
name: worker,
diff: diff,
shares: workerShares,
invalidshares: 0,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
};
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].shares += workerShares;
} else {
coinStats.miners[miner] = {
name: miner,
shares: workerShares,
invalidshares: 0,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
}
else {
// build worker stats
if (worker in coinStats.workers) {
coinStats.workers[worker].invalidshares -= workerShares; // workerShares is negative number!
coinStats.workers[worker].diff = diff;
} else {
coinStats.workers[worker] = {
name: worker,
diff: diff,
shares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
};
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number!
} else {
coinStats.miners[miner] = {
name: miner,
shares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
}
});
// sort miners
coinStats.miners = sortMinersByHashrate(coinStats.miners);
var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier;
coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate);
var _blocktime = 160;
var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2;
var _myHashRate = (coinStats.hashrate / 1000000) * 2;
coinStats.luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.minerCount = Object.keys(coinStats.miners).length;
coinStats.workerCount = Object.keys(coinStats.workers).length;
portalStats.global.workers += coinStats.workerCount;
/* 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;
var _shareTotal = parseFloat(0);
var _maxTimeShare = parseFloat(0);
for (var worker in coinStats.currentRoundShares) {
var miner = worker.split(".")[0];
if (miner in coinStats.miners) {
coinStats.miners[miner].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
}
if (worker in coinStats.workers) {
coinStats.workers[worker].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
}
_shareTotal += parseFloat(coinStats.currentRoundShares[worker]);
}
for (var worker in coinStats.currentRoundTimes) {
var time = parseFloat(coinStats.currentRoundTimes[worker]);
if (_maxTimeShare < time) { _maxTimeShare = time; }
var miner = worker.split(".")[0]; // split poolId from minerAddress
if (miner in coinStats.miners && coinStats.miners[miner].currRoundTime < time) {
coinStats.miners[miner].currRoundTime = time;
}
}
coinStats.shareCount = _shareTotal;
coinStats.maxRoundTime = _maxTimeShare;
coinStats.maxRoundTimeString = readableSeconds(_maxTimeShare);
for (var worker in coinStats.workers) {
var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.workers[worker].hashrate = _workerRate;
coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate);
var miner = worker.split('.')[0];
if (miner in coinStats.miners) {
coinStats.workers[worker].currRoundTime = coinStats.miners[miner].currRoundTime;
}
}
for (var miner in coinStats.miners) {
var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.miners[miner].hashrate = _workerRate;
coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate);
}
// sort workers by name
coinStats.workers = sortWorkersByName(coinStats.workers);
delete coinStats.hashrates;
delete coinStats.shares;
});
Object.keys(portalStats.algos).forEach(function(algo){
var algoStats = portalStats.algos[algo];
algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate);
});
_this.stats = portalStats;
// save historical hashrate, not entire stats!
var saveStats = JSON.parse(JSON.stringify(portalStats));
Object.keys(saveStats.pools).forEach(function(pool){
delete saveStats.pools[pool].pending;
delete saveStats.pools[pool].confirmed;
delete saveStats.pools[pool].currentRoundShares;
delete saveStats.pools[pool].currentRoundTimes;
delete saveStats.pools[pool].payments;
delete saveStats.pools[pool].miners;
});
_this.statsString = JSON.stringify(saveStats);
_this.statHistory.push(saveStats);
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;
}
}
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();
});
};
function sortPoolsByName(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortBlocks(a, b) {
var as = parseInt(a.split(":")[2]);
var bs = parseInt(b.split(":")[2]);
if (as > bs) return -1;
if (as < bs) return 1;
return 0;
}
function sortWorkersByName(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortMinersByHashrate(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'shares', true, true);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortWorkersByHashrate(a, b) {
if (a.hashrate === b.hashrate) {
return 0;
}
else {
return (a.hashrate < b.hashrate) ? -1 : 1;
}
}
this.getReadableHashRateString = function(hashrate){
hashrate = (hashrate * 2);
if (hashrate < 1000000) {
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
}
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
};
function getReadableNetworkHashRateString(hashrate) {
hashrate = (hashrate * 1000000);
if (hashrate < 1000000)
return '0 Sol';
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
}
};

362
libs/website.js

@ -0,0 +1,362 @@
var https = require('https');
var fs = require('fs');
var path = require('path');
var async = require('async');
var watch = require('node-watch');
var redis = require('redis');
var dot = require('dot');
var express = require('express');
var bodyParser = require('body-parser');
var compress = require('compression');
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
var api = require('./api.js');
module.exports = function(logger){
dot.templateSettings.strip = false;
var portalConfig = JSON.parse(process.env.portalConfig);
var poolConfigs = JSON.parse(process.env.pools);
var websiteConfig = portalConfig.website;
var portalApi = new api(logger, portalConfig, poolConfigs);
var portalStats = portalApi.stats;
var logSystem = 'Website';
var pageFiles = {
'index.html': 'index',
'home.html': '',
'getting_started.html': 'getting_started',
'stats.html': 'stats',
'tbs.html': 'tbs',
'workers.html': 'workers',
'api.html': 'api',
'admin.html': 'admin',
'mining_key.html': 'mining_key',
'miner_stats.html': 'miner_stats',
'payments.html': 'payments'
};
var pageTemplates = {};
var pageProcessed = {};
var indexesProcessed = {};
var keyScriptTemplate = '';
var keyScriptProcessed = '';
var processTemplates = function(){
for (var pageName in pageTemplates){
if (pageName === 'index') continue;
pageProcessed[pageName] = pageTemplates[pageName]({
poolsConfigs: poolConfigs,
stats: portalStats.stats,
portalConfig: portalConfig
});
indexesProcessed[pageName] = pageTemplates.index({
page: pageProcessed[pageName],
selected: pageName,
stats: portalStats.stats,
poolConfigs: poolConfigs,
portalConfig: portalConfig
});
}
//logger.debug(logSystem, 'Stats', 'Website updated to latest stats');
};
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);
pageTemplates[pageFiles[fileName]] = pTemp
callback();
});
}, function(err){
if (err){
console.log('error reading files for creating dot templates: '+ JSON.stringify(err));
return;
}
processTemplates();
});
};
// if an html file was changed reload it
/* requires node-watch 0.5.0 or newer */
watch(['./website', './website/pages'], function(evt, filename){
var basename;
// support older versions of node-watch automatically
if (!filename && evt)
basename = path.basename(evt);
else
basename = path.basename(filename);
if (basename in pageFiles){
readPageFiles([basename]);
logger.special(logSystem, 'Server', 'Reloaded file ' + basename);
}
});
portalStats.getGlobalStats(function(){
readPageFiles(Object.keys(pageFiles));
});
var buildUpdatedWebsite = function(){
portalStats.getGlobalStats(function(){
processTemplates();
var statData = 'data: ' + JSON.stringify(portalStats.stats) + '\n\n';
for (var uid in portalApi.liveStatConnections){
var res = portalApi.liveStatConnections[uid];
res.write(statData);
}
});
};
setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000);
var buildKeyScriptPage = function(){
async.waterfall([
function(callback){
var client = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
if (portalConfig.redis.password) {
client.auth(portalConfig.redis.password);
}
client.hgetall('coinVersionBytes', function(err, coinBytes){
if (err){
client.quit();
return callback('Failed grabbing coin version bytes from redis ' + JSON.stringify(err));
}
callback(null, client, coinBytes || {});
});
},
function (client, coinBytes, callback){
var enabledCoins = Object.keys(poolConfigs).map(function(c){return c.toLowerCase()});
var missingCoins = [];
enabledCoins.forEach(function(c){
if (!(c in coinBytes))
missingCoins.push(c);
});
callback(null, client, coinBytes, missingCoins);
},
function(client, coinBytes, missingCoins, callback){
var coinsForRedis = {};
async.each(missingCoins, function(c, cback){
var coinInfo = (function(){
for (var pName in poolConfigs){
if (pName.toLowerCase() === c)
return {
daemon: poolConfigs[pName].paymentProcessing.daemon,
address: poolConfigs[pName].address
}
}
})();
var daemon = new Stratum.daemon.interface([coinInfo.daemon], function(severity, message){
logger[severity](logSystem, c, message);
});
daemon.cmd('dumpprivkey', [coinInfo.address], function(result){
if (result[0].error){
logger.error(logSystem, c, 'Could not dumpprivkey for ' + c + ' ' + JSON.stringify(result[0].error));
cback();
return;
}
var vBytePub = util.getVersionByte(coinInfo.address)[0];
var vBytePriv = util.getVersionByte(result[0].response)[0];
coinBytes[c] = vBytePub.toString() + ',' + vBytePriv.toString();
coinsForRedis[c] = coinBytes[c];
cback();
});
}, function(err){
callback(null, client, coinBytes, coinsForRedis);
});
},
function(client, coinBytes, coinsForRedis, callback){
if (Object.keys(coinsForRedis).length > 0){
client.hmset('coinVersionBytes', coinsForRedis, function(err){
if (err)
logger.error(logSystem, 'Init', 'Failed inserting coin byte version into redis ' + JSON.stringify(err));
client.quit();
});
}
else{
client.quit();
}
callback(null, coinBytes);
}
], function(err, coinBytes){
if (err){
logger.error(logSystem, 'Init', err);
return;
}
try{
keyScriptTemplate = dot.template(fs.readFileSync('website/key.html', {encoding: 'utf8'}));
keyScriptProcessed = keyScriptTemplate({coins: coinBytes});
}
catch(e){
logger.error(logSystem, 'Init', 'Failed to read key.html file');
}
});
};
buildKeyScriptPage();
var getPage = function(pageId){
if (pageId in pageProcessed){
var requestedPage = pageProcessed[pageId];
return requestedPage;
}
};
var minerpage = function(req, res, next){
var address = req.params.address || null;
if (address != null) {
address = address.split(".")[0];
portalStats.getBalanceByAddress(address, function(){
processTemplates();
res.header('Content-Type', 'text/html');
res.end(indexesProcessed['miner_stats']);
});
}
else
next();
};
var payout = function(req, res, next){
var address = req.params.address || null;
if (address != null){
portalStats.getPayout(address, function(data){
res.write(data.toString());
res.end();
});
}
else
next();
};
var shares = function(req, res, next){
portalStats.getCoins(function(){
processTemplates();
res.end(indexesProcessed['user_shares']);
});
};
var usershares = function(req, res, next){
var coin = req.params.coin || null;
if(coin != null){
portalStats.getCoinTotals(coin, null, function(){
processTemplates();
res.end(indexesProcessed['user_shares']);
});
}
else
next();
};
var route = function(req, res, next){
var pageId = req.params.page || '';
if (pageId in indexesProcessed){
res.header('Content-Type', 'text/html');
res.end(indexesProcessed[pageId]);
}
else
next();
};
var app = express();
app.use(bodyParser.json());
app.get('/get_page', function(req, res, next){
var requestedPage = getPage(req.query.id);
if (requestedPage){
res.end(requestedPage);
return;
}
next();
});
app.get('/key.html', function(req, res, next){
res.end(keyScriptProcessed);
});
//app.get('/stats/shares/:coin', usershares);
//app.get('/stats/shares', shares);
//app.get('/payout/:address', payout);
app.use(compress());
app.get('/workers/:address', minerpage);
app.get('/:page', route);
app.get('/', route);
app.get('/api/:method', function(req, res, next){
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.send(500, 'Something broke!');
});
try {
if (portalConfig.website.tlsOptions && portalConfig.website.tlsOptions.enabled === true) {
var TLSoptions = {
key: fs.readFileSync(portalConfig.website.tlsOptions.key),
cert: fs.readFileSync(portalConfig.website.tlsOptions.cert)
};
https.createServer(TLSoptions, app).listen(portalConfig.website.port, portalConfig.website.host, function() {
logger.debug(logSystem, 'Server', 'TLS Website started on ' + portalConfig.website.host + ':' + portalConfig.website.port);
});
} else {
app.listen(portalConfig.website.port, portalConfig.website.host, function () {
logger.debug(logSystem, 'Server', 'Website started on ' + portalConfig.website.host + ':' + portalConfig.website.port);
});
}
}
catch(e){
console.log(e)
logger.error(logSystem, 'Server', 'Could not start website on ' + portalConfig.website.host + ':' + portalConfig.website.port
+ ' - its either in use or you do not have permission');
}
};

56
libs/workerapi.js

@ -0,0 +1,56 @@
var express = require('express');
var os = require('os');
function workerapi(listen) {
var _this = this;
var app = express();
var counters = {
validShares : 0,
validBlocks : 0,
invalidShares : 0
};
var lastEvents = {
lastValidShare : 0 ,
lastValidBlock : 0,
lastInvalidShare : 0
};
app.get('/stats', function (req, res) {
res.send({
"clients" : Object.keys(_this.poolObj.stratumServer.getStratumClients()).length,
"counters" : counters,
"lastEvents" : lastEvents
});
});
this.start = function (poolObj) {
this.poolObj = poolObj;
this.poolObj.once('started', function () {
app.listen(listen, function (lol) {
console.log("LISTENING ");
});
})
.on('share', function(isValidShare, isValidBlock, shareData) {
var now = Date.now();
if (isValidShare) {
counters.validShares ++;
lastEvents.lastValidShare = now;
if (isValidBlock) {
counters.validBlocks ++;
lastEvents.lastValidBlock = now;
}
} else {
counters.invalidShares ++;
lastEvents.lastInvalidShare = now;
}
});
}
}
module.exports = workerapi;

53
package.json

@ -0,0 +1,53 @@
{
"name": "Komodo-mining",
"version": "0.0.1",
"description": "High performance Stratum poolserver in Node.js",
"keywords": [
"stratum",
"mining",
"pool",
"poolserver",
"komodo"
],
"homepage": "https://github.com/TheComputerGenie/Knomp",
"bugs": {
"url": "https://github.com/TheComputerGenie/Knomp/issues"
},
"license": "GPL-2.0",
"author": "ComputerGenie",
"contributors": [
"z-classic"
],
"main": "init.js",
"bin": {
"block-notify": "./scripts/blockNotify.js"
},
"repository": {
"type": "git",
"url": "https://github.com/TheComputerGenie/Knomp.git"
},
"dependencies": {
"async": "2.3.0",
"bignum": "0.12.5",
"body-parser": "1.17.1",
"colors": "1.1.2",
"compression": "1.6.2",
"dateformat": "2.0.0",
"dot": "1.1.1",
"express": "4.15.2",
"extend": "3.0.0",
"mysql": "2.13.0",
"node-json-minify": "1.0.0",
"node-watch": "0.5.2",
"nonce": "1.0.4",
"redis": "2.7.1",
"request": "2.81.0",
"stratum-pool": "git+https://github.com/TheComputerGenie/node-stratum-pool.git"
},
"engines": {
"node": ">=0.10"
},
"scripts": {
"start": "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/node_modules/stratum-pool/node_modules/equihashverify/build/Release/:$PWD/node_modules/equihashverify/build/Release/ node init.js"
}
}

BIN
scripts/blocknotify

Binary file not shown.

84
scripts/blocknotify.c

@ -0,0 +1,84 @@
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/*
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.
(may also work as coin switch)
Platforms : Linux, BSD, Solaris (mostly OS independent)
Build with:
gcc blocknotify.c -o blocknotify
Example usage in daemon coin.conf using default NOMP CLI port of 17117
blocknotify="/bin/blocknotify 127.0.0.1:17117 dogecoin %s"
*/
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 < 3)
{
// print help
printf("NOMP pool block notify\n usage: <host:port> <coin> <block>\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);
}
}
snprintf(sendline, sizeof(sendline) - 1, "{\"command\":\"blocknotify\",\"params\":[\"%s\",\"%s\"]}\n", argv[2], argv[3]);
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));
int result = send(sockfd, sendline, strlen(sendline), 0);
close(sockfd);
if(result == -1) {
printf("Error sending: %i\n", errno);
exit(-1);
}
exit(0);
}

37
scripts/cli.js

@ -0,0 +1,37 @@
var net = require('net');
var defaultPort = 17117;
var defaultHost = '127.0.0.1';
var args = process.argv.slice(2);
var params = [];
var options = {};
for(var i = 0; i < args.length; i++){
if (args[i].indexOf('-') === 0 && args[i].indexOf('=') !== -1){
var s = args[i].substr(1).split('=');
options[s[0]] = s[1];
}
else
params.push(args[i]);
}
var command = params.shift();
var client = net.connect(options.port || defaultPort, options.host || defaultHost, function () {
client.write(JSON.stringify({
command: command,
params: params,
options: options
}) + '\n');
}).on('error', function(error){
if (error.code === 'ECONNREFUSED')
console.log('Could not connect to NOMP instance at ' + defaultHost + ':' + defaultPort);
else
console.log('Socket error ' + JSON.stringify(error));
}).on('data', function(data) {
console.log(data.toString());
}).on('close', function () {
});

95
website/index.html

@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" type="image/png" href="/static/favicon.png"/>
<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.5/d3.min.js"></script>
<script src="/static/nvd3.js"></script>
<link rel="stylesheet" href="/static/nvd3.css">
<script src="/static/main.js"></script>
<link rel="stylesheet" href="/static/style.css">
<title>z-nomp</title>
</head>
<body>
<header>
<div class="home-menu pure-menu pure-menu-open pure-menu-horizontal">
<a class="pure-menu-heading hot-swapper" href="/"><i class="fa fa-home"></i>&nbsp;z-nomp</a>
<ul>
<li class="{{? it.selected === 'getting_started' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/getting_started">
<i class="fa fa-rocket"></i>&nbsp;
Getting Started
</a>
</li>
<li class="{{? it.selected === 'stats' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/stats">
<i class="fa fa-bar-chart-o"></i>&nbsp;
Graph Stats
</a>
</li>
<li class="{{? it.selected === 'tbs' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/tbs">
<i class="fa fa-table"></i>&nbsp;
Tab Stats
</a>
</li>
<li class="{{? it.selected === 'workers' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/workers">
<i class="fa fa-cogs"></i>&nbsp;
Workers Stats
</a>
</li>
<li class="{{? it.selected === 'api' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/api">
<i class="fa fa-code"></i>&nbsp;
API
</a>
</li>
</ul>
</div>
</header>
<main>
{{=it.page}}
</main>
<footer>
<div>
This site is powered by the open source <a target="_blank" href="https://github.com/joshuayabut/z-nomp/">z-nomp</a>
project created by Joshua Yabut and the Zclassic Community and is licensed under the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT License</a>
</div>
<div>
<i class="fa fa-heart"></i>&nbsp; Support this project by donating&nbsp;<i class="fa fa-btc"></i>&nbsp;BTC: 18vHMxVzotQ9EPyESrf7Z1hNM9AwJeVHgD
</div>
<div id="communityFooter">
Community&nbsp;<i class="fa fa-comment"></i>&nbsp;: &nbsp;<a target="_blank" href="https://zclassic.herokuapp.com">#zclassic IRC</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a target="_blank" href="http://reddit.com/r/zclassic">/r/zclassic</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<iframe src="http://ghbtns.com/github-btn.html?user=joshuayabut&repo=z-nomp&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="140" height="20"></iframe>
</div>
</footer>
</body>
</html>

2798
website/key.html

File diff suppressed because it is too large

50
website/pages/admin.html

@ -0,0 +1,50 @@
<div>
<style>
#passwordForm, #adminCenter{
display: none;
}
#adminCenter{
display: flex;
flex-flow: row;
}
#leftMenu{
flex: 0 0 200px;
}
#editForm{
flex: 1 1 auto;
}
</style>
<form class="pure-form pure-form-stacked" id="passwordForm">
<fieldset>
<legend>Password</legend>
<input id="password" type="password" placeholder="Password">
<label for="remember" class="pure-checkbox">
<input id="remember" type="checkbox"> Stay Logged In
</label>
<button type="submit" class="pure-button pure-button-primary">Log In</button>
</fieldset>
</form>
<div id="adminCenter">
<div class="pure-menu pure-menu-open" id="leftMenu">
<a class="pure-menu-heading">Administration</a>
<ul>
<li id="addPool"><a href="#">Add Pool</a></li>
<li class="pure-menu-heading" id="poolList">Current Pools</li>
</ul>
</div>
<div id="editForm"></div>
</div>
<script src="/static/admin.js"></script>
</div>

13
website/pages/api.html

@ -0,0 +1,13 @@
<div style="margin: 18px;">
API - The API is work in progress and is subject to change during development.
<ul>
<li><a href="/api/stats">/stats</a> global pool stats</li>
<li><a href="/api/blocks">/stats</a> global block stats</li>
<li><a href="/api/pool_stats">/pool_stats</a> - historical stats</li>
<li><a href="/api/payments">/payments</a> - payment history</li>
<li><a href="/api/worker_stats?taddr">/worker_stats?taddr</a> - historical time per pool json </li>
<li><a href="/api/live_stats">/live_stats</a> - live stats </li>
</ul>
</div>

318
website/pages/getting_started.html

@ -0,0 +1,318 @@
<style>
#holder{
display: flex;
flex-direction: row;
}
.glow{
box-shadow: inset 0 0 12px 4px #ff6c00;
}
.hidden{
display: none !important;
}
#menu{
background-color: #3d3d3d;
min-width: 170px;
}
#menu > .menuHeader{
color: #e3f7ff;
border-bottom: 1px solid #7f878b;
font-size: 1.2em;
padding: 16px 16px 4px 15px;
}
.menuList{
transition-duration: 200ms;
}
.menuList > a:first-child{
margin-top: 10px;
}
.menuList > a{
display: block;
color: #e3f7ff;
text-decoration: none;
padding: 7px;
padding-left: 25px;
text-transform: capitalize;
}
.menuList > a:hover{
color: #f69b3a;
}
#main{
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin: 18px;
}
.miningOption{
color: white;
display: flex;
flex: 1 1 auto;
flex-direction: row;
flex-wrap: wrap;
min-height: 215px;
justify-content: center;
align-items: center;
text-decoration: none;
}
a.miningOption:hover{
color: #f69b3a;
}
.miningOption:first-child{
background-color: #0eafc7;
}
.miningOption:last-child{
background-color: #b064e1;
}
.miningOptionNum{
font-size: 6em;
padding-right: 20px;
width: 140px;
text-align: center;
}
.miningOptionInstructions{
flex: 1 1 auto;
}
.miningOptionInstructions > div:first-child{
font-size: 2.4em;
}
.miningOptionInstructions > div:last-child{
margin-top: 20px;
font-size: 1.3em;
}
#orHolder{
height: 37px;
text-align: center;
}
#orLine{
border-bottom: 1px solid #c2cacf;
height: 19px;
margin-bottom: -13px;
}
#orText{
background-color: #ebf4fa;
color: #5c5c5c;
display: inline-block;
width: 35px;
font-style: italic;
}
#coinInfoBackground{
transition-duration: 400ms;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: black;
opacity: 0.0;
}
#coinInfo{
display: flex;
flex-direction: column;
color: white;
width: 750px;
min-height: 400px;
top: 50px;
left: 50%;
margin-left: -375px;
position: absolute;
background-color: #f06350;
}
#coinInfo .coinInfoName{
text-transform: capitalize;
}
#coinInfo > div:first-of-type{
font-size: 1.8em;
text-align: center;
margin-top: 40px;
margin-bottom: 35px;
}
#coinInfoRows{
display: flex;
flex-direction: row;
justify-content: center;
flex: 1 1 auto;
margin-bottom: 70px;
}
#coinInfoRows > div{
display: flex;
flex-direction: column;
justify-content: center;
}
#coinInfoRows > div > div{
padding: 3px;
}
#coinInfoRowKeys{
font-weight: bold;
padding-right: 30px;
}
#coinInfoRowKeys .coinInfoSubtle{
font-weight: normal;
}
#coinInfoRowValues{
}
#coinInfoClose{
position: absolute;
font-size: 3em;
top: 0;
right: 0;
width: 60px;
height: 60px;
text-align: center;
color: white;
text-decoration: none;
}
#coinInfoClose:hover{
color: #50f0e3;
}
</style>
<div id="holder">
<div id="menu">
{{? (function(){
if (!it.portalConfig.switching) return false;
for (var p in it.portalConfig.switching){
if (it.portalConfig.switching[p].enabled)
return true;
}
return false;
})()
}}
<div class="menuHeader">Coin-Switching Ports</div>
{{?}}
<div class="menuList">
{{ for (var p in it.portalConfig.switching){
if (!it.portalConfig.switching[p].enabled) continue;
var info = {
algo: p,
ports: {},
host: it.portalConfig.website.stratumHost
};
info.ports[it.portalConfig.switching[p].port] = {diff: it.portalConfig.switching[p].diff};
info = JSON.stringify(info).replace(/"/g, '&quot;');
}}
<a href="#" class="poolOption" data-info="{{=info}}">{{=p}}</a>
{{ } }}
</div>
<div class="menuHeader">Coins</div>
<div class="menuList" id="coinList">
{{ for(var pool in it.poolsConfigs) {
var info = JSON.stringify({
coin: it.poolsConfigs[pool].coin,
algo: it.poolsConfigs[pool].coin.algorithm,
ports: it.poolsConfigs[pool].ports,
host: it.portalConfig.website.stratumHost
}).replace(/"/g, '&quot;');
}}
<a href="#" class="poolOption" data-info="{{=info}}">{{=pool}}</a>
{{ } }}
</div>
</div>
<div id="main">
<a href="#" class="miningOption" id="nompAppDownload">
<div class="miningOptionNum">1.</div>
<div class="miningOptionInstructions">
<div>Download NOMP App</div>
<div>Our preconfigured app makes mining that easy</div>
</div>
</a>
<div id="orHolder">
<div id="orLine"></div>
<div id="orText">or</div>
</div>
<a href="#" class="miningOption" id="coinGlowTrigger">
<div class="miningOptionNum">2.</div>
<div class="miningOptionInstructions">
<div>Select a coin for connection details</div>
<div>Configurations for each coin are available for advanced miners</div>
</div>
</a>
</div>
</div>
<a href="#" id="coinInfoBackground" class="hidden"></a>
<div id="coinInfo" class="hidden">
<a href="#" id="coinInfoClose">×</a>
<div><span class="coinInfoName"></span> Configuration:</div>
<div id="coinInfoRows">
<div id="coinInfoRowKeys">
<div>Username:</div>
<div>Password:</div>
</div>
<div id="coinInfoRowValues">
<div id="coinInfoUsername"></div>
<div>anything</div>
</div>
</div>
</div>
<script>
function showCoinConfig(info){
var htmlKeys = '<div class="coinInfoData">Algorithm:</div>';
var htmlValues = '<div class="coinInfoData">' + info.algo + '</div>';
for (var port in info.ports){
htmlKeys += '<div class="coinInfoData">URL <span class="coinInfoSubtle">(difficulty ' + info.ports[port].diff + ')</span>:</div>';
htmlValues += '<div class="coinInfoData">stratum+tcp://' + info.host + ':' + port + '</div>';
}
if (info.coin)
$('#coinInfoUsername').text('your ' + info.coin.name + ' wallet address');
else
$('#coinInfoUsername').text('your public key');
$('.coinInfoData').remove();
$('#coinInfoRowKeys').append(htmlKeys);
$('#coinInfoRowValues').append(htmlValues);
}
$('#coinGlowTrigger').click(function(event){
event.preventDefault();
$('.menuList').addClass('glow');
setTimeout(function(){
$('.menuList').removeClass('glow');
}, 200);
return false;
});
$('.poolOption').click(function(event){
event.preventDefault();
showCoinConfig($(this).data('info'));
$('#coinInfoBackground,#coinInfo').removeClass('hidden');
$('#coinInfoBackground').css('opacity', 0.7);
return false;
});
$('#coinInfoBackground,#coinInfoClose').click(function(event){
event.preventDefault();
$('#coinInfoBackground,#coinInfo').addClass('hidden');
$('#coinInfoBackground').css('opacity', 0.0);
return false;
});
$('#nompAppDownload').click(function(event){
event.preventDefault();
alert('NOMP App development still in progress...');
return false;
});
</script>

140
website/pages/home.html

@ -0,0 +1,140 @@
<style>
#boxWelcome{
background-color: #0eafc7;
color: white;
margin: 18px;
}
#logoImg{
height: 285px;
margin: 55px;
}
#welcomeText{
font-size: 2.7em;
margin: 50px 18px 10px 18px;
}
#welcomeItems{
list-style-type: none;
font-size: 1.3em;
padding: 0 !important;
margin: 0 0 0 18px !important;
}
#welcomeItems > li{
margin: 30px !important;
}
#boxesLower {
margin: 0 9px;
}
#boxesLower > div {
display: flex;
}
#boxesLower > div > div {
flex: 1 1 auto;
margin: 0 9px 18px 9px;
padding: 10px;
display: flex;
flex-direction: column;
}
.boxLowerHeader{
font-size: 1.3em;
margin: 0 0 5px 10px;
}
#boxStatsLeft{
background-color: #b064e1;
}
#boxStatsRight{
background-color: #10bb9c;
}
.boxStats{
color: white;
}
.boxStatsList{
display: flex;
flex-flow: row wrap;
justify-content: space-around;
opacity: 0.77;
margin-bottom: 5px;
flex: 1 1 auto;
align-content: center;
}
.boxStatsList i.fa{
height: 15px;
width: 33px;
text-align: center;
}
.boxStatsList > div{
padding: 5px 20px;
}
.boxStatsList > div > div{
padding: 3px;
}
</style>
<div class="pure-g-r" id="boxWelcome">
<div class="pure-u-1-3">
<img id="logoImg" src="/static/logo.svg">
</div>
<div class="pure-u-2-3">
<div id="welcomeText">Welcome to the future of mining</div>
<ul id="welcomeItems">
<li>Low fees</li>
<li>High performance Node.js backend</li>
<li>User friendly mining client</li>
<li>Multi-coin / multi-pool</li>
</ul>
</div>
</div>
<div class="pure-g-r" id="boxesLower">
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsLeft">
<div class="boxLowerHeader">Global Stats</div>
<div class="boxStatsList">
{{ for(var algo in it.stats.algos) { }}
<div>
<div><i class="fa fa-flask"></i>{{=algo}}</div>
<div><i class="fa fa-users"></i><span id="statsMiners{{=algo}}">{{=it.stats.algos[algo].workers}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=algo}}">{{=it.stats.algos[algo].hashrateString}}</span></div>
</div>
{{ } }}
</div>
</div>
</div>
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsRight">
<div class="boxLowerHeader">Pools / Coins</div>
<div class="boxStatsList">
{{ for(var pool in it.stats.pools) { }}
<div>
<div><i class="fa fa-dot-circle-o"></i>{{=pool}}</div>
<div><i class="fa fa-users"></i><span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=pool}}">{{=it.stats.pools[pool].hashrateString}}</span></div>
</div>
{{ } }}
</div>
</div>
</div>
</div>
<script>
$(function() {
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);
}
for (var pool in stats.pools) {
$('#statsMiners' + pool).text(stats.pools[pool].workerCount);
$('#statsHashrate' + pool).text(stats.pools[pool].hashrateString);
}
});
});
</script>

105
website/pages/miner_stats.html

@ -0,0 +1,105 @@
<style>
#topCharts{
padding-left: 18px;
padding-right: 18px;
padding-top: 18px;
padding-bottom: 0px;
}
#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;
padding: 4px;
}
.chartHolder{
}
#boxesWorkers {
margin: 0 9px;
}
#boxesWorkers > div {
display: flex;
}
#boxesWorkers > div > div {
flex: 1 1 auto;
margin: 0 9px 18px 9px;
padding: 10px;
display: flex;
flex-direction: column;
}
.boxLowerHeader{
font-size: 1.3em;
margin: 0 0 5px 10px;
}
#boxStatsLeft{
color: black;
background-color: #cccccc;
}
#boxStatsRight{
color: black;
background-color: #cccccc;
}
.boxStats{
color: white;
}
.boxStatsList{
display: flex;
flex-flow: row wrap;
justify-content: space-around;
opacity: 0.77;
margin-bottom: 5px;
flex: 1 1 auto;
align-content: center;
}
.boxStatsList i.fa{
height: 15px;
width: 33px;
text-align: center;
}
.boxStatsList > div{
padding: 5px 20px;
}
.boxStatsList > div > div{
padding: 3px;
}
</style>
<div id="topCharts">
<div class="chartWrapper">
<div class="chartLabel">
<!--<div style="float:left; padding-right: 18px;"><i class="fa fa-users"></i><span id="statsWorkers">...</span></div>-->
<div style="float:left; margin-right: 9px;">{{=String(it.stats.address).split(".")[0]}}</div>
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-tachometer"></i> <span id="statsHashrateAvg">...</span> (Avg)</small></div>
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-tachometer"></i> <span id="statsHashrate">...</span> (Now)</small></div>
<div style="float:right; padding-left: 18px;"><small><i class="fa fa-gavel"></i> Luck <span id="statsLuckDays">...</span> Days</small></div>
</div>
<div class="chartHolder"><svg id="workerHashrate" /></div>
<div>
<div style="float:right; padding-top: 9px; padding-right: 18px;"><i class="fa fa-cog"></i> Shares: <span id="statsTotalShares">...</span></div>
<div style="float:left; padding-top: 9px; padding-left: 18px; padding-right: 18px;"><i class="fa fa-money"></i> Immature: <span id="statsTotalImmature">...</span> </div>
<div style="float:left; padding-top: 9px; padding-left: 18px; padding-right: 18px;"><i class="fa fa-money"></i> Bal: <span id="statsTotalBal">...</span> </div>
<div style="padding-top: 9px; padding-left: 18px;"><i class="fa fa-money"></i> Paid: <span id="statsTotalPaid">...</span> </div>
</div>
</div>
</div>
<div id="boxesWorkers"> </div>
<script>
var _miner = "{{=String(it.stats.address).split(".")[0]}}";
var _workerCount = 0;
window.statsSource = new EventSource("/api/live_stats");
document.querySelector('main').appendChild(document.createElement('script')).src = '/static/miner_stats.js';
</script>

25
website/pages/mining_key.html

@ -0,0 +1,25 @@
<style>
#miningKeyPage{
margin: 15px;
}
#keyFrame{
padding: 0;
border: 0;
width: 100%;
height: 750px;
display: block;
}
</style>
<div id="miningKeyPage">
<p>
This script run client-side (in your browser). For maximum security <a href="/key.html" download="key.html">download</a> the script and run it locally and
offline in a modern web browser.
</p>
<iframe id="keyFrame" src="/key.html">
</iframe>
</div>

93
website/pages/payments.html

@ -0,0 +1,93 @@
<style>
#bottomNotes {
display: block;
padding-left: 18px;
padding-right: 18px;
padding-bottom: 18px;
}
#topPool {
padding-top: 18px;
padding-left: 18px;
padding-right: 18px;
}
#topPool > div > div > svg {
display: block;
height: 280px;
}
.poolWrapper {
border: solid 1px #c7c7c7;
border-radius: 5px;
padding: 5px;
margin-bottom: 18px;
}
.poolLabel {
font-size: 1.2em;
text-align: center;
padding: 4px;
}
.poolMinerTable {
}
table {
width: 100%;
}
</style>
<script type="text/javascript">
$(function () {
$(document).tooltip({
content: function () {
return $(this).prop('title');
},
show: null,
close: function (event, ui) {
ui.tooltip.hover(
function () {
$(this).stop(true).fadeTo(400, 1);
},
function () {
$(this).fadeOut("400", function () {
$(this).remove();
})
});
}
});
});
</script>
{{ function readableDate(a){ return new Date(parseInt(a)).toString(); } }}
{{ for(var pool in it.stats.pools) { }}
<table>
<thead>
<tr>
<th>Blocks</th>
<th>Time</th>
<th>Miners</th>
<th>Shares</th>
<th>Amount</th>
</tr>
</thead>
{{ for(var p in it.stats.pools[pool].payments) { }}
<tr>
<td>
{{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }}
<a href="https://explorer.zcha.in/tx/{{=it.stats.pools[pool].payments[p].txid}}" title="View transaction" target="_blank">{{=it.stats.pools[pool].payments[p].blocks}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }}
<a href="https://classic.zcha.in/tx/{{=it.stats.pools[pool].payments[p].txid}}" title="View transaction" target="_blank">{{=it.stats.pools[pool].payments[p].blocks}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("hush")) { }}
<a href="https://explorer.myhush.org/tx/{{=it.stats.pools[pool].payments[p].txid}}" title="View transaction" target="_blank">{{=it.stats.pools[pool].payments[p].blocks}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("zen")) { }}
<a href="http://node1.zenchain.info:8886/tx/{{=it.stats.pools[pool].payments[p].txid}}" title="View transaction" target="_blank">{{=it.stats.pools[pool].payments[p].blocks}}</a>
{{ } else { }}
{{=it.stats.pools[pool].payments[p].blocks}}
{{ } }}
</td>
<td>{{=readableDate(it.stats.pools[pool].payments[p].time)}}</td>
<td>{{=it.stats.pools[pool].payments[p].miners}}</td>
<td>{{=Math.round(it.stats.pools[pool].payments[p].shares)}}</td>
<td>{{=it.stats.pools[pool].payments[p].paid}} {{=it.stats.pools[pool].symbol}}</td>
</tr>
{{ } }}
</table>
</div>
{{ } }}

350
website/pages/stats.html

@ -0,0 +1,350 @@
<style>
#topCharts{
padding: 18px;
}
#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;
padding: 4px;
}
.chartHolder{
}
#boxesLower {
margin: 0 9px;
}
#boxesLower > div {
display: flex;
}
#boxesLower > div > div {
flex: 1 1 auto;
margin: 0 9px 18px 9px;
padding: 10px;
display: flex;
flex-direction: column;
}
.boxLowerHeader{
font-size: 1.3em;
margin: 0 0 5px 10px;
}
#boxStatsLeft{
color: black;
background-color: #cccccc;
}
#boxStatsRight{
color: black;
background-color: #cccccc;
}
.boxStats{
color: white;
}
.boxStatsList{
display: flex;
flex-flow: row wrap;
justify-content: space-around;
opacity: 0.77;
margin-bottom: 5px;
flex: 1 1 auto;
align-content: center;
}
.boxStatsList i.fa{
height: 15px;
width: 33px;
text-align: center;
}
.boxStatsList > div{
padding: 5px 20px;
}
.boxStatsList > div > div{
padding: 3px;
}
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
#tooltip.hidden {
opacity: 0;
}
</style>
<div id="topCharts">
<div class="chartWrapper">
<div class="chartLabel">Pool Historical Hashrate</div>
<div class="chartHolder"><svg id="poolHashrate"/></div>
</div>
</div>
{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }}
{{ function readableDate(a){ return new Date(parseInt(a)).toString(); } }}
<div class="pure-g-r" id="boxesLower">
{{ for(var pool in it.stats.pools) { }}
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsLeft">
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Pool Stats</div>
<div class="boxStatsList">
<div>
<div><i class="fa fa-users"></i><span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].minerCount}}</span> Miners</div>
<div><i class="fa fa-rocket"></i><span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Workers</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=pool}}">{{=it.stats.pools[pool].hashrateString}}</span> (Now)</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrateAvg{{=pool}}">...</span> (Avg)</div>
<div><i class="fa fa-gavel"></i>Luck <span id="statsLuckDays{{=pool}}">{{=it.stats.pools[pool].luckDays}}</span> Days</div>
</div>
</div>
</div>
</div>
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsRight">
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Network Stats</div>
<div class="boxStatsList">
<div>
<div><i class="fa fa-bars" aria-hidden="true"></i><small>Block Height:</small> <span id="statsNetworkBlocks{{=pool}}">{{=it.stats.pools[pool].poolStats.networkBlocks}}</span></div>
<div><i class="fa fa-tachometer"></i><small>Network Hash/s:</small> <span id="statsNetworkSols{{=pool}}">{{=it.stats.pools[pool].poolStats.networkSolsString}}</span></div>
<div><i class="fa fa-unlock-alt" aria-hidden="true"></i><small>Difficulty:</small> <span id="statsNetworkDiff{{=pool}}">{{=it.stats.pools[pool].poolStats.networkDiff}}</span></div>
<div><i class="fa fa-users"></i><small>Node Connections:</small> <span id="statsNetworkConnections{{=pool}}">{{=it.stats.pools[pool].poolStats.networkConnections}}</span></div>
</div>
</div>
</div>
</div>
{{ } }}
</div>
{{ for(var pool in it.stats.pools) { }}
{{ var blockscomb = new Array; }}
<div class="pure-g-r" id="boxesLower">
<div class="pure-u-1-1">
<div class="boxStats" id="boxStatsRight">
<div class="boxLowerHeader">{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Blocks Found &nbsp;&nbsp;
<span style="float:right;"><small>
<i class="fa fa-bars"></i> <span id="statsValidBlocks{{=pool}}">{{=it.stats.pools[pool].poolStats.validBlocks}}</span> Blocks &nbsp;&nbsp;
<i class="fa fa-money"></i> Paid: <span id="statsTotalPaid{{=pool}}">{{=(parseFloat(it.stats.pools[pool].poolStats.totalPaid)).toFixed(8)}}</span> {{=it.stats.pools[pool].symbol}}</small>&nbsp;&nbsp;</span>
</div>
<div class="boxStatsList" style="margin-top: 9px;">
<!--<div id="{{=it.stats.pools[pool].name}}NewBlocks"></div>-->
{{ for(var b in it.stats.pools[pool].pending.blocks) { }}
{{ var block = it.stats.pools[pool].pending.blocks[b].split(":"); }}
<div style="margin-bottom: 9px; background-color: #eeeeee; min-width:600px;"><i class="fa fa-bars"></i>
<small>Block:</small>
{{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }}
<a href="https://explorer.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }}
<a href="https://classic.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("hush")) { }}
<a href="https://explorer.myhush.org/block/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
{{ } else { }}
{{=block[2]}}
{{ } }}
{{if (block[4] != null) { }}
<span style="padding-left: 18px;"><small>{{=readableDate(block[4])}}</small></span>
{{ } }}
{{if (it.stats.pools[pool].pending.confirms) { }}
{{if (it.stats.pools[pool].pending.confirms[block[0]]) { }}
<span style="float:right; color: red;"><small>{{=it.stats.pools[pool].pending.confirms[block[0]]}} of 100</small></span>
{{ } else { }}
<span style="float:right; color: red;"><small>*PENDING*</small></span>
{{ } }}
{{ } else { }}
<span style="float:right; color: red;"><small>*PENDING*</small></span>
{{ } }}
<div><i class="fa fa-gavel"></i><small>Mined By:</small> <a href="/workers/{{=block[3].split('.')[0]}}">{{=block[3]}}</a></div>
</div>
{{ blockscomb.push(block);}}
{{ } }}
{{ var i=0; for(var b in it.stats.pools[pool].confirmed.blocks) { }}
{{ if (i < 8) { i++; }}
{{ var block = it.stats.pools[pool].confirmed.blocks[b].split(":"); }}
<div style="margin-bottom: 9px; background-color: #eeeeee; min-width:600px;"><i class="fa fa-bars"></i>
<small>Block:</small>
{{if (String(it.stats.pools[pool].name).startsWith("zcash")) { }}
<a href="https://explorer.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
{{ } else if (String(it.stats.pools[pool].name).startsWith("zclassic")) { }}
<a href="https://classic.zcha.in/blocks/{{=block[0]}}" target="_blank">{{=block[2]}}</a>
{{ } else { }}
{{=block[2]}}
{{ } }}
{{if (block[4] != null) { }}
<span style="padding-left: 18px;"><small>{{=readableDate(block[4])}}</small></span>
{{ } }}
<span style="float:right; padding-left: 18px; color: green;"><small>*PAID*</small></span>
<div><i class="fa fa-gavel"></i><small>Mined By:</small> <a href="/workers/{{=block[3].split('.')[0]}}">{{=block[3]}}</a></div>
</div>
{{blockscomb.push(block);}}
{{ } }}
{{ } }}
<!--{{=JSON.stringify(blockscomb)}}-->
<script>
var blockscomb = ({{=JSON.stringify(blockscomb)}})
</script>
</div>
</div>
</div>
</div>
</div>
<center><div id="bottomCharts{{=pool}}" style="text-align:center;" align="center">
<div class="chartWrapper" style="text-align:center;">
<div class="chartLabel">Finders of the last {{=blockscomb.length}} blocks</div>
<div class="hidden" id="tooltip{{=pool}}"><p><span id="value{{=pool}}"></span> blocks found by <span id="finderr{{=pool}}"></span></p></div>
<div class="chartHolder" id="pie{{=pool}}"><svg id="blocksPie{{=pool}}" style="display: block; margin: auto; text-align:center;"/></div>
</div>
</div></center>
<script>
var groupedByFinder = {};
var data = [];
for (var i=0; i < blockscomb.length; i++) {
finder=blockscomb[i][3]; // if other doesn 't already have a property for the current letter
// create it and assign it to a new empty array
if (!(finder in groupedByFinder))
groupedByFinder[finder] = [];
groupedByFinder[finder].push(blockscomb[i]);
}
Object.keys(groupedByFinder).forEach(function(i) {
var obj = {};
obj.label = i
obj.value = groupedByFinder[i].length
data.push(obj)
});
console.log(JSON.stringify(data))
var w = 400;
var h = 400;
var r = h/2;
var legendRectSize = 18;
var legendSpacing = 5;
var color = d3.scale.category20c();
var div = d3.select("#pie{{=pool}}").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var vis = d3.select('#blocksPie{{=pool}}')
.data([data])
.attr("width", 1000)
.attr("height", h)
.attr("style", "display: block; margin: auto;")
.attr("preserveAspectRatio", "xMidYMin")
.append("svg:g")
.attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function(d){return d.value;});
// declare an arc generator function
var arc = d3.svg.arc().outerRadius(r);
// select paths, use arc generator to draw
var arcs = vis.selectAll("g.slice{{=pool}}")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice{{=pool}}")
.attr("id", "slice")
.on("mouseover", function(d){
d3.select("#tooltip{{=pool}}")
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + "px")
.style("opacity", 1)
.select("#value{{=pool}}")
.text(d.data.value);
d3.select("#tooltip{{=pool}}")
.select("#finderr{{=pool}}")
.text(d.data.label);
});
arcs.append("svg:path")
.attr("fill", function(d, i){
return color(i);
})
.attr("d", function (d) {
return arc(d);
});
var legend = vis.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('id', {{=JSON.stringify(pool)}})
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = 12 * legendRectSize;
var vert = i * height;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d, i) {
return data[i].label;
});
</script>
{{ } }}
<script>
document.querySelector('main').appendChild(document.createElement('script')).src = '/static/stats.js';
</script>
<script>
window.statsSource = new EventSource("/api/live_stats");
$(function() {
statsSource.addEventListener('message', function (e) {
var stats = JSON.parse(e.data);
for (var pool in stats.pools) {
$('#statsMiners' + pool).text(stats.pools[pool].minerCount);
$('#statsWorkers' + pool).text(stats.pools[pool].workerCount);
$('#statsHashrate' + pool).text(stats.pools[pool].hashrateString);
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
$('#statsLuckDays' + pool).text(stats.pools[pool].luckDays);
$('#statsValidBlocks' + pool).text(stats.pools[pool].poolStats.validBlocks);
$('#statsTotalPaid' + pool).text((parseFloat(stats.pools[pool].poolStats.totalPaid)).toFixed(8));
$('#statsNetworkBlocks' + pool).text(stats.pools[pool].poolStats.networkBlocks);
$('#statsNetworkDiff' + pool).text(stats.pools[pool].poolStats.networkDiff);
$('#statsNetworkSols' + pool).text(getReadableNetworkHashRateString(stats.pools[pool].poolStats.networkSols));
$('#statsNetworkConnections' + pool).text(stats.pools[pool].poolStats.networkConnections);
}
});
});
function getReadableNetworkHashRateString(hashrate){
hashrate = (hashrate * 1000000);
if (hashrate < 1000000)
return '0 Sol';
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
}
</script>

64
website/pages/tbs.html

@ -0,0 +1,64 @@
<style>
#topCharts {
padding: 18px;
}
#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;
padding: 4px;
}
.chartHolder {
}
table {
width: 100%;
}
</style>
<table class="pure-table">
<thead>
<tr>
<th>Pool</th>
<th>Algo</th>
<th>Workers</th>
<th>Valid Shares</th>
<th>Invalid Shares</th>
<th>Total Blocks</th>
<th>Pending</th>
<th>Confirmed</th>
<th>Orphaned</th>
<th>Hashrate</th>
</tr>
</thead>
{{ for(var pool in it.stats.pools) { }}
<tr class="pure-table-odd">
<td>{{=it.stats.pools[pool].name}}</td>
<td>{{=it.stats.pools[pool].algorithm}}</td>
<td>{{=Object.keys(it.stats.pools[pool].workers).length}}</td>
<td>{{=it.stats.pools[pool].poolStats.validShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.invalidShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.validBlocks}}</td>
<td>{{=it.stats.pools[pool].blocks.pending}}</td>
<td>{{=it.stats.pools[pool].blocks.confirmed}}</td>
<td>{{=it.stats.pools[pool].blocks.orphaned}}</td>
<td>{{=it.stats.pools[pool].hashrateString}}</td>
</tr>
{{ } }}
</table>

94
website/pages/workers.html

@ -0,0 +1,94 @@
<style>
#bottomNotes {
display: block;
padding-left: 18px;
padding-right: 18px;
padding-bottom: 18px;
}
#topPool {
padding-top: 18px;
padding-left: 18px;
padding-right: 18px;
}
#topPool > div > div > svg {
display: block;
height: 280px;
}
.poolWrapper {
border: solid 1px #c7c7c7;
border-radius: 5px;
padding: 5px;
margin-bottom: 18px;
}
.poolLabel {
font-size: 1.2em;
text-align: center;
padding: 4px;
}
.poolMinerTable {
}
table {
width: 100%;
}
</style>
<script type="text/javascript">
function searchKeyPress(e)
{
// look for window.event in case event isn't passed in
e = e || window.event;
if (e.keyCode == 13)
{
document.getElementById('btnSearch').click();
return false;
}
return true;
}
$(document).ready(function(){
$('.btn-lg').click(function(){
window.location = "workers/" + $('.input-lg').val();
});
});
</script>
{{ function capitalizeFirstLetter(t){return t.charAt(0).toUpperCase()+t.slice(1)} }}
{{ var i=0; for(var pool in it.stats.pools) { }}
<div id="topPool">
<div class="poolWrapper">
<div class="poolLabel">
<span style="float:right; margin-bottom: 8px;">
<small>Miner Lookup:
<input type="text" class="form-control input-lg" onkeypress="return searchKeyPress(event);">
<span class="input-group-btn">
<button class="btn btn-default btn-lg" type="button">Lookup</button>
</span>
</small>
</span>
{{=capitalizeFirstLetter(it.stats.pools[pool].name)}} Top Miners &nbsp;&nbsp;
<small><i class="fa fa-users"></i> <span id="statsMiners{{=pool}}">{{=it.stats.pools[pool].minerCount}}</span> Miners &nbsp;&nbsp;
<i class="fa fa-rocket"></i> <span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].workerCount}}</span> Workers &nbsp;&nbsp;
<i class="fa fa-cog"></i> <span id="statsWorkers{{=pool}}">{{=it.stats.pools[pool].shareCount}}</span> Shares </small>
</div>
<div class="poolMinerTable">
<table class="pure-table">
<thead>
<tr>
<th>Address</th>
<th>Shares</th>
<th>Efficiency</th>
<th>Hashrate</th>
</tr>
</thead>
{{ for(var worker in it.stats.pools[pool].miners) { }}
{{var workerstat = it.stats.pools[pool].miners[worker];}}
<tr class="pure-table-odd">
<td><a href="/workers/{{=worker.split('.')[0]}}">{{=worker}}</a></td>
<td>{{=Math.round(workerstat.currRoundShares * 100) / 100}}</td>
<td>{{? workerstat.shares > 0}} {{=Math.floor(10000 * workerstat.shares / (workerstat.shares + workerstat.invalidshares)) / 100}}% {{??}} 0% {{?}}</td>
<td>{{=workerstat.hashrateString}}</td>
</tr>
{{ } }}
</table>
</div>
</div>
</div>
{{ } }}

100
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 '<li class="poolMenuItem"><a href="#">' + poolName + '</a></li>';
}).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;
});

BIN
website/static/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

13
website/static/logo.svg

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" width="350px" height="350px" viewBox="0 0 350 350" enable-background="new 0 0 350 350" xml:space="preserve" xmlns:xml="http://www.w3.org/XML/1998/namespace">
<g>
<path fill="#F48438" d="M138,346H93c0,0-9,0-9-9s0-18,0-18h63c0,0,0,9,0,18S138,346,138,346z"/>
<path fill="#F48438" d="M255,346h-45c0,0-9,0-9-9s0-18,0-18h63c0,0,0,9,0,18S255,346,255,346z"/>
<path fill="#F48438" d="M30,139c0-63,105.099-79.872,143.186-80C210.458,58.875,318,76,318,139c0,0,0,84.71,0,153c0,27-27,27-27,27 H57c0,0-27,0-27-27V139z"/>
<path fill="#FFFFFF" d="M246,264.056c0,4.643-3.428,8.944-9.12,8.944H112.805c-5.691,0-10.805-4.301-10.805-8.944v-65.15 c0-4.643,5.114-7.906,10.805-7.906H236.88c5.691,0,9.12,3.264,9.12,7.906V264.056z"/>
<circle fill="#FFFFFF" cx="94.667" cy="141.667" r="20.333"/>
<circle fill="#FFFFFF" cx="252.667" cy="141.667" r="20.333"/>
<path fill="#F48438" d="M174.24,45.664c24.69-30.635,70.778-25.601,70.778-25.601l6.603,13.863c0,0-48.596-0.658-62.896,24.075 S149.551,76.3,174.24,45.664z"/>
<circle fill="#F48438" cx="259.083" cy="24.083" r="21.083"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

30
website/static/main.js

@ -0,0 +1,30 @@
$(function(){
var hotSwap = function(page, pushSate){
if (pushSate) history.pushState(null, null, '/' + page);
$('.pure-menu-selected').removeClass('pure-menu-selected');
$('a[href="/' + page + '"]').parent().addClass('pure-menu-selected');
$.get("/get_page", {id: page}, function(data){
$('main').html(data);
}, 'html')
};
$('.hot-swapper').click(function(event){
if (event.which !== 1) return;
var pageId = $(this).attr('href').slice(1);
hotSwap(pageId, true);
event.preventDefault();
return false;
});
window.addEventListener('load', function() {
setTimeout(function() {
window.addEventListener("popstate", function(e) {
hotSwap(location.pathname.slice(1));
});
}, 0);
});
window.statsSource = new EventSource("/api/live_stats");
});

246
website/static/miner_stats.js

@ -0,0 +1,246 @@
var workerHashrateData;
var workerHashrateChart;
var workerHistoryMax = 160;
var statData;
var totalHash;
var totalImmature;
var totalBal;
var totalPaid;
var totalShares;
function getReadableHashRateString(hashrate){
hashrate = (hashrate * 2);
if (hashrate < 1000000) {
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
}
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + 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 getWorkerNameFromAddress(w) {
var worker = w;
if (w.split(".").length > 1) {
worker = w.split(".")[1];
if (worker == null || worker.length < 1) {
worker = "noname";
}
} else {
worker = "noname";
}
return worker;
}
function buildChartData(){
var workers = {};
for (var w in statData.history) {
var worker = getWorkerNameFromAddress(w);
var a = workers[worker] = (workers[worker] || {
hashrate: []
});
for (var wh in statData.history[w]) {
a.hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
}
if (a.hashrate.length > workerHistoryMax) {
workerHistoryMax = a.hashrate.length;
}
}
var i=0;
workerHashrateData = [];
for (var worker in workers){
workerHashrateData.push({
key: worker,
disabled: (i > Math.min((_workerCount-1), 3)),
values: workers[worker].hashrate
});
i++;
}
}
function updateChartData(){
var workers = {};
for (var w in statData.history) {
var worker = getWorkerNameFromAddress(w);
// get a reference to lastest workerhistory
for (var wh in statData.history[w]) { }
//var wh = statData.history[w][statData.history[w].length - 1];
var foundWorker = false;
for (var i = 0; i < workerHashrateData.length; i++) {
if (workerHashrateData[i].key === worker) {
foundWorker = true;
if (workerHashrateData[i].values.length >= workerHistoryMax) {
workerHashrateData[i].values.shift();
}
workerHashrateData[i].values.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
break;
}
}
if (!foundWorker) {
var hashrate = [];
hashrate.push([statData.history[w][wh].time * 1000, statData.history[w][wh].hashrate]);
workerHashrateData.push({
key: worker,
values: hashrate
});
rebuildWorkerDisplay();
return true;
}
}
triggerChartUpdates();
return false;
}
function calculateAverageHashrate(worker) {
var count = 0;
var total = 1;
var avg = 0;
for (var i = 0; i < workerHashrateData.length; i++) {
count = 0;
for (var ii = 0; ii < workerHashrateData[i].values.length; ii++) {
if (worker == null || workerHashrateData[i].key === worker) {
count++;
avg += parseFloat(workerHashrateData[i].values[ii][1]);
}
}
if (count > total)
total = count;
}
avg = avg / total;
return avg;
}
function triggerChartUpdates(){
workerHashrateChart.update();
}
function displayCharts() {
nv.addGraph(function() {
workerHashrateChart = nv.models.lineChart()
.margin({left: 80, right: 30})
.x(function(d){ return d[0] })
.y(function(d){ return d[1] })
.useInteractiveGuideline(true);
workerHashrateChart.xAxis.tickFormat(timeOfDayFormat);
workerHashrateChart.yAxis.tickFormat(function(d){
return getReadableHashRateString(d);
});
d3.select('#workerHashrate').datum(workerHashrateData).call(workerHashrateChart);
return workerHashrateChart;
});
}
function updateStats() {
totalHash = statData.totalHash;
totalPaid = statData.paid;
totalBal = statData.balance;
totalImmature = statData.immature;
totalShares = statData.totalShares;
// do some calculations
var _blocktime = 250;
var _networkHashRate = parseFloat(statData.networkSols) * 1.2;
var _myHashRate = (totalHash / 1000000) * 2;
var luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
// update miner stats
$("#statsHashrate").text(getReadableHashRateString(totalHash));
$("#statsHashrateAvg").text(getReadableHashRateString(calculateAverageHashrate(null)));
$("#statsLuckDays").text(luckDays);
$("#statsTotalImmature").text(totalImmature);
$("#statsTotalBal").text(totalBal);
$("#statsTotalPaid").text(totalPaid);
$("#statsTotalShares").text(totalShares.toFixed(2));
}
function updateWorkerStats() {
// update worker stats
var i=0;
for (var w in statData.workers) { i++;
var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, '');
var saneWorkerName = getWorkerNameFromAddress(w);
$("#statsHashrate"+htmlSafeWorkerName).text(getReadableHashRateString(statData.workers[w].hashrate));
$("#statsHashrateAvg"+htmlSafeWorkerName).text(getReadableHashRateString(calculateAverageHashrate(saneWorkerName)));
$("#statsLuckDays"+htmlSafeWorkerName).text(statData.workers[w].luckDays);
$("#statsPaid"+htmlSafeWorkerName).text(statData.workers[w].paid);
$("#statsBalance"+htmlSafeWorkerName).text(statData.workers[w].balance);
$("#statsShares"+htmlSafeWorkerName).text(Math.round(statData.workers[w].currRoundShares * 100) / 100);
$("#statsDiff"+htmlSafeWorkerName).text(statData.workers[w].diff);
}
}
function addWorkerToDisplay(name, htmlSafeName, workerObj) {
var htmlToAdd = "";
htmlToAdd = '<div class="boxStats" id="boxStatsLeft" style="float:left; margin: 9px; min-width: 260px;"><div class="boxStatsList">';
if (htmlSafeName.indexOf("_") >= 0) {
htmlToAdd+= '<div class="boxLowerHeader">'+htmlSafeName.substr(htmlSafeName.indexOf("_")+1,htmlSafeName.length)+'</div>';
} else {
htmlToAdd+= '<div class="boxLowerHeader">noname</div>';
}
htmlToAdd+='<div><i class="fa fa-tachometer"></i> <span id="statsHashrate'+htmlSafeName+'">'+getReadableHashRateString(workerObj.hashrate)+'</span> (Now)</div>';
htmlToAdd+='<div><i class="fa fa-tachometer"></i> <span id="statsHashrateAvg'+htmlSafeName+'">'+getReadableHashRateString(calculateAverageHashrate(name))+'</span> (Avg)</div>';
htmlToAdd+='<div><i class="fa fa-shield"></i> <small>Diff:</small> <span id="statsDiff'+htmlSafeName+'">'+workerObj.diff+'</span></div>';
htmlToAdd+='<div><i class="fa fa-cog"></i> <small>Shares:</small> <span id="statsShares'+htmlSafeName+'">'+(Math.round(workerObj.currRoundShares * 100) / 100)+'</span></div>';
htmlToAdd+='<div><i class="fa fa-gavel"></i> <small>Luck <span id="statsLuckDays'+htmlSafeName+'">'+workerObj.luckDays+'</span> Days</small></div>';
htmlToAdd+='<div><i class="fa fa-money"></i> <small>Bal: <span id="statsBalance'+htmlSafeName+'">'+workerObj.balance+'</span></small></div>';
htmlToAdd+='<div><i class="fa fa-money"></i> <small>Paid: <span id="statsPaid'+htmlSafeName+'">'+workerObj.paid+'</span></small></div>';
htmlToAdd+='</div></div></div>';
$("#boxesWorkers").html($("#boxesWorkers").html()+htmlToAdd);
}
function rebuildWorkerDisplay() {
$("#boxesWorkers").html("");
var i=0;
for (var w in statData.workers) { i++;
var htmlSafeWorkerName = w.split('.').join('_').replace(/[^\w\s]/gi, '');
var saneWorkerName = getWorkerNameFromAddress(w);
addWorkerToDisplay(saneWorkerName, htmlSafeWorkerName, statData.workers[w]);
}
}
// resize chart on window resize
nv.utils.windowResize(triggerChartUpdates);
// grab initial stats
$.getJSON('/api/worker_stats?'+_miner, function(data){
statData = data;
for (var w in statData.workers) { _workerCount++; }
buildChartData();
displayCharts();
rebuildWorkerDisplay();
updateStats();
});
// live stat updates
statsSource.addEventListener('message', function(e){
// TODO, create miner_live_stats...
// miner_live_stats will return the same josn except without the worker history
// FOR NOW, use this to grab updated stats
$.getJSON('/api/worker_stats?'+_miner, function(data){
statData = data;
// check for missing workers
var wc = 0;
var rebuilt = false;
// update worker stats
for (var w in statData.workers) { wc++; }
// TODO, this isn't 100% fool proof!
if (_workerCount != wc) {
if (_workerCount > wc) {
rebuildWorkerDisplay();
rebuilt = true;
}
_workerCount = wc;
}
rebuilt = (rebuilt || updateChartData());
updateStats();
if (!rebuilt) {
updateWorkerStats();
}
});
});

1
website/static/nvd3.css

File diff suppressed because one or more lines are too long

6
website/static/nvd3.js

File diff suppressed because one or more lines are too long

143
website/static/stats.js

@ -0,0 +1,143 @@
var poolHashrateData;
var poolHashrateChart;
var statData;
var poolKeys;
function buildChartData(){
var pools = {};
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: []
});
if (pName in statData[i].pools){
a.hashrate.push([time, statData[i].pools[pName].hashrate]);
}
else{
a.hashrate.push([time, 0]);
}
}
}
poolHashrateData = [];
for (var pool in pools){
poolHashrateData.push({
key: pool,
values: pools[pool].hashrate
});
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
}
}
function calculateAverageHashrate(pool) {
var count = 0;
var total = 1;
var avg = 0;
for (var i = 0; i < poolHashrateData.length; i++) {
count = 0;
for (var ii = 0; ii < poolHashrateData[i].values.length; ii++) {
if (pool == null || poolHashrateData[i].key === pool) {
count++;
avg += parseFloat(poolHashrateData[i].values[ii][1]);
}
}
if (count > total)
total = count;
}
avg = avg / total;
return avg;
}
function getReadableHashRateString(hashrate){
hashrate = (hashrate * 2);
if (hashrate < 1000000) {
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
}
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + 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() {
poolHashrateChart = nv.models.lineChart()
.margin({left: 80, right: 30})
.x(function(d){ return d[0] })
.y(function(d){ return d[1] })
.useInteractiveGuideline(true);
poolHashrateChart.xAxis.tickFormat(timeOfDayFormat);
poolHashrateChart.yAxis.tickFormat(function(d){
return getReadableHashRateString(d);
});
d3.select('#poolHashrate').datum(poolHashrateData).call(poolHashrateChart);
return poolHashrateChart;
});
}
function triggerChartUpdates(){
poolHashrateChart.update();
}
nv.utils.windowResize(triggerChartUpdates);
$.getJSON('/api/pool_stats', function(data){
statData = data;
buildChartData();
displayCharts();
});
statsSource.addEventListener('message', function(e){
var stats = JSON.parse(e.data);
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 < 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]);
$('#statsHashrateAvg' + pool).text(getReadableHashRateString(calculateAverageHashrate(pool)));
break;
}
}
}
triggerChartUpdates();
}
});

68
website/static/style.css

@ -0,0 +1,68 @@
html, button, input, select, textarea, .pure-g [class *= "pure-u"], .pure-g-r [class *= "pure-u"]{
font-family: 'Open Sans', sans-serif;
}
html{
background: #2d2d2d;
overflow-y: scroll;
}
body{
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;
}
header > .home-menu > a.pure-menu-heading{
color: white;
font-size: 1.5em;
}
header > .home-menu > ul > li > a{
color: #ced4d9;
}
header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li > a:focus{
background: inherit !important;
}
header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li.pure-menu-selected > a{
color: white;
}
main{
background-color: #ebf4fa;
position: relative;
}
footer{
text-align: center;
color: #b3b3b3;
text-decoration: none;
font-size: 0.8em;
padding: 15px;
line-height: 24px;
}
footer a{
color: #fff;
text-decoration: none;
}
footer iframe{
vertical-align: middle;
}
Loading…
Cancel
Save