417 lines
13 KiB
JavaScript
417 lines
13 KiB
JavaScript
var fs = require('fs');
|
|
var http = require('http');
|
|
var url = require("url");
|
|
var zlib = require('zlib');
|
|
|
|
var async = require('async');
|
|
var redis = require('redis');
|
|
|
|
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
|
|
|
|
var logSystem = 'api';
|
|
require('./exceptionWriter.js')(logSystem);
|
|
|
|
var redisCommands = [
|
|
['zremrangebyscore', config.coin + ':hashrate', '-inf', ''],
|
|
['zrange', config.coin + ':hashrate', 0, -1],
|
|
['hgetall', config.coin + ':stats'],
|
|
['zrange', config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES'],
|
|
['zrevrange', config.coin + ':blocks:matured', 0, config.api.blocks, 'WITHSCORES'],
|
|
['hgetall', config.coin + ':shares:roundCurrent'],
|
|
['hgetall', config.coin + ':stats'],
|
|
['zcard', config.coin + ':blocks:matured']
|
|
];
|
|
|
|
var currentStats = "";
|
|
var currentStatsCompressed = "";
|
|
|
|
var minerStats = {};
|
|
|
|
var liveConnections = {};
|
|
var addressConnections = {};
|
|
|
|
|
|
|
|
function collectStats(){
|
|
|
|
var startTime = Date.now();
|
|
var redisFinished;
|
|
var daemonFinished;
|
|
|
|
var windowTime = (((Date.now() / 1000) - config.api.hashrateWindow) | 0).toString();
|
|
redisCommands[0][3] = '(' + windowTime;
|
|
redisCommands[1][2] = windowTime;
|
|
|
|
async.parallel({
|
|
pool: function(callback){
|
|
redisClient.multi(redisCommands).exec(function(error, replies){
|
|
|
|
redisFinished = Date.now();
|
|
|
|
if (error){
|
|
log('error', logSystem, 'Error getting redis data %j', [error]);
|
|
callback(true);
|
|
return;
|
|
}
|
|
|
|
var data = {
|
|
stats: replies[2],
|
|
blocks: replies[3].concat(replies[4]),
|
|
totalBlocks: parseInt(replies[7]) + replies[3].length
|
|
};
|
|
|
|
var hashrates = replies[1];
|
|
|
|
minerStats = {};
|
|
|
|
for (var i = 0; i < hashrates.length; i++){
|
|
var hashParts = hashrates[i].split(':');
|
|
minerStats[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
|
|
}
|
|
|
|
var totalShares = 0;
|
|
|
|
for (var miner in minerStats){
|
|
var shares = minerStats[miner];
|
|
totalShares += shares;
|
|
minerStats[miner] = getReadableHashRateString(shares / config.api.hashrateWindow);
|
|
}
|
|
|
|
data.miners = Object.keys(minerStats).length;
|
|
|
|
data.hashrate = totalShares / config.api.hashrateWindow;
|
|
|
|
data.roundHashes = 0;
|
|
|
|
if (replies[5]){
|
|
for (var miner in replies[5]){
|
|
data.roundHashes += parseInt(replies[5][miner]);
|
|
}
|
|
}
|
|
|
|
if (replies[6]) {
|
|
data.lastBlockFound = replies[6].lastBlockFound;
|
|
}
|
|
|
|
callback(null, data);
|
|
});
|
|
},
|
|
network: function(callback){
|
|
apiInterfaces.rpcDaemon('getlastblockheader', {}, function(error, reply){
|
|
daemonFinished = Date.now();
|
|
if (error){
|
|
log('error', logSystem, 'Error getting daemon data %j', [error]);
|
|
callback(true);
|
|
return;
|
|
}
|
|
var blockHeader = reply.block_header;
|
|
callback(null, {
|
|
difficulty: blockHeader.difficulty,
|
|
height: blockHeader.height,
|
|
timestamp: blockHeader.timestamp,
|
|
reward: blockHeader.reward,
|
|
hash: blockHeader.hash
|
|
});
|
|
});
|
|
},
|
|
config: function(callback){
|
|
callback(null, {
|
|
ports: config.poolServer.ports,
|
|
hashrateWindow: config.api.hashrateWindow,
|
|
fee: config.blockUnlocker.poolFee,
|
|
coin: config.coin,
|
|
symbol: config.symbol,
|
|
depth: config.blockUnlocker.depth,
|
|
version: config.version,
|
|
donation: config.blockUnlocker.devDonation
|
|
});
|
|
}
|
|
}, function(error, results){
|
|
|
|
log('info', logSystem, 'Stat collection finished: %d ms redis, %d ms daemon', [redisFinished - startTime, daemonFinished - startTime]);
|
|
|
|
if (error){
|
|
log('error', logSystem, 'Error collecting all stats');
|
|
}
|
|
else{
|
|
currentStats = JSON.stringify(results);
|
|
zlib.deflateRaw(currentStats, function(error, result){
|
|
currentStatsCompressed = result;
|
|
broadcastLiveStats();
|
|
});
|
|
|
|
}
|
|
|
|
setTimeout(collectStats, config.api.updateInterval * 1000);
|
|
});
|
|
|
|
}
|
|
|
|
function getReadableHashRateString(hashrate){
|
|
var i = 0;
|
|
var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ];
|
|
while (hashrate > 1024){
|
|
hashrate = hashrate / 1024;
|
|
i++;
|
|
}
|
|
return hashrate.toFixed(2) + byteUnits[i];
|
|
}
|
|
|
|
function broadcastLiveStats(){
|
|
|
|
log('info', logSystem, 'Broadcasting to %d visitors and %d address lookups', [Object.keys(liveConnections).length, Object.keys(addressConnections).length]);
|
|
|
|
for (var uid in liveConnections){
|
|
var res = liveConnections[uid];
|
|
res.end(currentStatsCompressed);
|
|
}
|
|
|
|
var redisCommands = [];
|
|
for (var address in addressConnections){
|
|
redisCommands.push(['hgetall', config.coin + ':workers:' + address]);
|
|
}
|
|
redisClient.multi(redisCommands).exec(function(error, replies){
|
|
|
|
var addresses = Object.keys(addressConnections);
|
|
|
|
for (var i = 0; i < addresses.length; i++){
|
|
var address = addresses[i];
|
|
var stats = replies[i];
|
|
var res = addressConnections[address];
|
|
res.end(stats ? formatMinerStats(stats, address) : '{"error": "not found"');
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleMinerStats(urlParts, response){
|
|
response.writeHead(200, {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'no-cache',
|
|
'Content-Type': 'application/json',
|
|
'Connection': 'keep-alive'
|
|
});
|
|
response.write('\n');
|
|
var address = urlParts.query.address;
|
|
|
|
if (urlParts.query.longpoll === 'true'){
|
|
redisClient.exists(config.coin + ':workers:' + address, function(error, result){
|
|
if (!result){
|
|
response.end(JSON.stringify({error: 'not found'}));
|
|
return;
|
|
}
|
|
addressConnections[address] = response;
|
|
response.on('finish', function(){
|
|
delete addressConnections[address];
|
|
})
|
|
});
|
|
}
|
|
else{
|
|
redisClient.hgetall(config.coin + ':workers:' + address, function(error, stats){
|
|
if (!stats){
|
|
response.end(JSON.stringify({error: 'not found'}));
|
|
return;
|
|
}
|
|
response.end(formatMinerStats(stats, address));
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
function formatMinerStats(redisData, address){
|
|
redisData.hashrate = minerStats[address];
|
|
redisData.symbol = config.symbol;
|
|
return JSON.stringify({stats: redisData});
|
|
}
|
|
|
|
|
|
function handleGetBlocks(urlParts, response){
|
|
redisClient.zrevrangebyscore(config.coin + ':blocks:matured', '(' + urlParts.query.height, '-inf', 'WITHSCORES', 'LIMIT', 0, config.api.blocks, function(err, result){
|
|
|
|
var reply;
|
|
|
|
if (err)
|
|
reply = JSON.stringify({error: 'query failed'});
|
|
else
|
|
reply = JSON.stringify(result);
|
|
|
|
response.writeHead("200", {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'no-cache',
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': reply.length
|
|
});
|
|
response.end(reply);
|
|
|
|
});
|
|
}
|
|
|
|
|
|
collectStats();
|
|
|
|
function authorize(request, response){
|
|
|
|
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
var sentPass = url.parse(request.url, true).query.password;
|
|
|
|
if (sentPass !== config.api.password){
|
|
response.statusCode = 401;
|
|
response.end('invalid password');
|
|
return;
|
|
}
|
|
|
|
response.statusCode = 200;
|
|
response.setHeader('Cache-Control', 'no-cache');
|
|
response.setHeader('Content-Type', 'application/json');
|
|
|
|
return true;
|
|
}
|
|
|
|
function handleAdminStats(response){
|
|
|
|
async.waterfall([
|
|
|
|
//Get worker keys & unlocked blocks
|
|
function(callback){
|
|
redisClient.multi([
|
|
['keys', config.coin + ':workers:*'],
|
|
['zrange', config.coin + ':blocks:matured', 0, -1]
|
|
]).exec(function(error, replies) {
|
|
if (error) {
|
|
log('error', logSystem, 'Error trying to get admin data from redis %j', [error]);
|
|
callback(true);
|
|
return;
|
|
}
|
|
callback(null, replies[0], replies[1]);
|
|
});
|
|
},
|
|
|
|
//Get worker balances
|
|
function(workerKeys, blocks, callback){
|
|
var redisCommands = workerKeys.map(function(k){
|
|
return ['hmget', k, 'balance', 'paid'];
|
|
});
|
|
redisClient.multi(redisCommands).exec(function(error, replies){
|
|
if (error){
|
|
log('error', logSystem, 'Error with getting balances from redis %j', [error]);
|
|
callback(true);
|
|
return;
|
|
}
|
|
|
|
callback(null, replies, blocks);
|
|
});
|
|
},
|
|
function(workerData, blocks, callback){
|
|
var stats = {
|
|
totalOwed: 0,
|
|
totalPaid: 0,
|
|
totalRevenue: 0,
|
|
totalDiff: 0,
|
|
totalShares: 0,
|
|
blocksOrphaned: 0,
|
|
blocksUnlocked: 0,
|
|
totalWorkers: 0
|
|
};
|
|
|
|
for (var i = 0; i < workerData.length; i++){
|
|
stats.totalOwed += parseInt(workerData[i][0]) || 0;
|
|
stats.totalPaid += parseInt(workerData[i][1]) || 0;
|
|
stats.totalWorkers++;
|
|
}
|
|
|
|
for (var i = 0; i < blocks.length; i++){
|
|
var block = blocks[i].split(':');
|
|
if (block[5]) {
|
|
stats.blocksUnlocked++;
|
|
stats.totalDiff += parseInt(block[2]);
|
|
stats.totalShares += parseInt(block[3]);
|
|
stats.totalRevenue += parseInt(block[5]);
|
|
}
|
|
else{
|
|
stats.blocksOrphaned++;
|
|
}
|
|
}
|
|
callback(null, stats);
|
|
}
|
|
], function(error, stats){
|
|
if (error){
|
|
response.end(JSON.stringify({error: 'error collecting stats'}));
|
|
return;
|
|
}
|
|
response.end(JSON.stringify(stats));
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
var server = http.createServer(function(request, response){
|
|
|
|
if (request.method.toUpperCase() === "OPTIONS"){
|
|
|
|
response.writeHead("204", "No Content", {
|
|
"access-control-allow-origin": '*',
|
|
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
"access-control-allow-headers": "content-type, accept",
|
|
"access-control-max-age": 10, // Seconds.
|
|
"content-length": 0
|
|
});
|
|
|
|
return(response.end());
|
|
}
|
|
|
|
|
|
var urlParts = url.parse(request.url, true);
|
|
|
|
switch(urlParts.pathname){
|
|
case '/stats':
|
|
var reply = currentStatsCompressed;
|
|
response.writeHead("200", {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'no-cache',
|
|
'Content-Type': 'application/json',
|
|
'Content-Encoding': 'deflate',
|
|
'Content-Length': reply.length
|
|
});
|
|
response.end(reply);
|
|
break;
|
|
case '/live_stats':
|
|
response.writeHead(200, {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'no-cache',
|
|
'Content-Type': 'application/json',
|
|
'Content-Encoding': 'deflate',
|
|
'Connection': 'keep-alive'
|
|
});
|
|
var uid = Math.random().toString();
|
|
liveConnections[uid] = response;
|
|
response.on("finish", function() {
|
|
delete liveConnections[uid];
|
|
});
|
|
break;
|
|
case '/stats_address':
|
|
handleMinerStats(urlParts, response);
|
|
break;
|
|
case '/get_blocks':
|
|
handleGetBlocks(urlParts, response);
|
|
break;
|
|
case '/admin_stats':
|
|
if (!authorize(request, response))
|
|
return;
|
|
log('warn', logSystem, 'Admin authorized');
|
|
handleAdminStats(response);
|
|
break;
|
|
default:
|
|
response.writeHead(404, {
|
|
'Access-Control-Allow-Origin': '*'
|
|
});
|
|
response.end('Invalid API call');
|
|
break;
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
server.listen(config.api.port, function(){
|
|
log('info', logSystem, 'API started & listening on port %d', [config.api.port]);
|
|
}); |