cryptonote-universal-pool/lib/api.js

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]);
});