You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
852 lines
33 KiB
852 lines
33 KiB
"use strict";
|
|
const express = require('express'); // call express
|
|
const app = express(); // define our app using express
|
|
const server = require('http').createServer(app);
|
|
const cluster = require('cluster');
|
|
const async = require("async");
|
|
const debug = require("debug")("api");
|
|
const btcValidator = require('wallet-address-validator');
|
|
const cnUtil = require('cryptonote-util');
|
|
const bodyParser = require('body-parser');
|
|
const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens
|
|
const crypto = require('crypto');
|
|
const cors = require('cors');
|
|
|
|
let addressBase58Prefix = cnUtil.address_decode(new Buffer(global.config.pool.address));
|
|
let threadName = "";
|
|
|
|
if (cluster.isMaster) {
|
|
threadName = "(Master) ";
|
|
} else {
|
|
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
|
|
}
|
|
|
|
let pool_list = [];
|
|
if(global.config.pplns.enable === true){
|
|
pool_list.push('pplns');
|
|
}
|
|
if(global.config.pps.enable === true){
|
|
pool_list.push('pps');
|
|
}
|
|
if(global.config.solo.enable === true){
|
|
pool_list.push('solo');
|
|
}
|
|
|
|
app.use(cors({origin: true}));
|
|
app.use(bodyParser.urlencoded({extended: false}));
|
|
app.use(bodyParser.json());
|
|
|
|
// Support Functions that are reused now
|
|
function getAllWorkerHashCharts(address, callback){
|
|
let identifiers = global.database.getCache(address + '_identifiers');
|
|
let returnData = {global: global.database.getCache(address)['hashHistory']};
|
|
if (identifiers !== false){
|
|
identifiers.sort();
|
|
} else {
|
|
return returnData;
|
|
}
|
|
let intCounter = 0;
|
|
identifiers.forEach(function(identifier){
|
|
returnData[identifier] = global.database.getCache(address+"_"+identifier)['hashHistory'];
|
|
intCounter += 1;
|
|
if (intCounter === identifiers.length){
|
|
return callback(null, returnData);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getAllWorkerStats(address, callback){
|
|
let identifiers = global.database.getCache(address + '_identifiers');
|
|
let globalCache = global.database.getCache(address);
|
|
let returnData = {global: {
|
|
lts: Math.floor(globalCache.lastHash / 1000),
|
|
identifer: 'global',
|
|
hash: globalCache.hash,
|
|
totalHash: globalCache.totalHashes
|
|
}};
|
|
let intCounter = 0;
|
|
if (identifiers === false){
|
|
return callback(null, returnData);
|
|
}
|
|
identifiers.sort().forEach(function(identifier){
|
|
let cachedData = global.database.getCache(address+"_"+identifier);
|
|
returnData[identifier] = {
|
|
lts: Math.floor(cachedData.lastHash / 1000),
|
|
identifer: identifier,
|
|
hash: cachedData.hash,
|
|
totalHash: cachedData.totalHashes
|
|
};
|
|
intCounter += 1;
|
|
if (intCounter === identifiers.length){
|
|
return callback(null, returnData);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getAddressStats(address, extCallback){
|
|
let address_parts = address.split('.');
|
|
let address_pt = address_parts[0];
|
|
let payment_id = address_parts[1];
|
|
let cachedData = global.database.getCache(address);
|
|
let paidQuery = "SELECT SUM(amount) as amt FROM payments WHERE payment_address = ? AND payment_id = ?";
|
|
let txnQuery = "SELECT count(id) as amt FROM payments WHERE payment_address = ? AND payment_id = ?";
|
|
let unpaidQuery = "SELECT SUM(amount) as amt FROM balance WHERE payment_address = ? AND payment_id = ?";
|
|
if (typeof(payment_id) === 'undefined') {
|
|
paidQuery = "SELECT SUM(amount) as amt FROM payments WHERE payment_address = ? AND payment_id IS ?";
|
|
txnQuery = "SELECT count(id) as amt FROM payments WHERE payment_address = ? AND payment_id IS ?";
|
|
unpaidQuery = "SELECT SUM(amount) as amt FROM balance WHERE payment_address = ? AND payment_id IS ?";
|
|
}
|
|
async.waterfall([
|
|
function (callback) {
|
|
debug(threadName + "Checking Influx for last 10min avg for /miner/address/stats");
|
|
return callback(null, {hash: cachedData.hash, identifier: 'global', lastHash: Math.floor(cachedData.lastHash / 1000),
|
|
totalHashes: cachedData.totalHashes, validShares: Number(cachedData.goodShares), invalidShares: Number(cachedData.badShares)});
|
|
},
|
|
function (returnData, callback) {
|
|
debug(threadName + "Checking MySQL total amount paid for /miner/address/stats");
|
|
global.mysql.query(paidQuery, [address_pt, payment_id]).then(function (rows) {
|
|
if (typeof(rows[0]) === 'undefined') {
|
|
returnData.amtPaid = 0;
|
|
} else {
|
|
returnData.amtPaid = rows[0].amt;
|
|
if (returnData.amtPaid === null) {
|
|
returnData.amtPaid = 0;
|
|
}
|
|
}
|
|
return callback(null, returnData);
|
|
});
|
|
},
|
|
function (returnData, callback) {
|
|
debug(threadName + "Checking MySQL total amount unpaid for /miner/address/stats");
|
|
global.mysql.query(unpaidQuery, [address_pt, payment_id]).then(function (rows) {
|
|
if (typeof(rows[0]) === 'undefined') {
|
|
returnData.amtDue = 0;
|
|
} else {
|
|
returnData.amtDue = rows[0].amt;
|
|
if (returnData.amtDue === null) {
|
|
returnData.amtDue = 0;
|
|
}
|
|
}
|
|
return callback(null, returnData);
|
|
});
|
|
},
|
|
function (returnData, callback) {
|
|
debug(threadName + "Checking MySQL total amount unpaid for /miner/address/stats");
|
|
global.mysql.query(txnQuery, [address_pt, payment_id]).then(function (rows) {
|
|
if (typeof(rows[0]) === 'undefined') {
|
|
returnData.txnCount = 0;
|
|
} else {
|
|
returnData.txnCount = rows[0].amt;
|
|
if (returnData.txnCount === null) {
|
|
returnData.txnCount = 0;
|
|
}
|
|
}
|
|
return callback(true, returnData);
|
|
});
|
|
}
|
|
], function (err, result) {
|
|
debug(threadName + "Result information for " + address + ": " + JSON.stringify(result));
|
|
if (err === true) {
|
|
return extCallback(null, result);
|
|
}
|
|
if (err) {
|
|
console.error(threadName + "Error within the miner stats identifier func");
|
|
return extCallback(err.toString());
|
|
}
|
|
});
|
|
}
|
|
|
|
// ROUTES FOR OUR API
|
|
// =============================================================================
|
|
|
|
// test route to make sure everything is working (accessed at GET http://localhost:8080/api)
|
|
|
|
// Config API
|
|
app.get('/config', function (req, res) {
|
|
res.json({
|
|
pplns_fee: global.config.payout.pplnsFee,
|
|
pps_fee: global.config.payout.ppsFee,
|
|
solo_fee: global.config.payout.soloFee,
|
|
btc_fee: global.config.payout.btcFee,
|
|
min_wallet_payout: global.config.payout.walletMin * global.config.general.sigDivisor,
|
|
min_btc_payout: global.config.payout.exchangeMin * global.config.general.sigDivisor,
|
|
min_exchange_payout: global.config.payout.exchangeMin * global.config.general.sigDivisor,
|
|
dev_donation: global.config.payout.devDonation,
|
|
pool_dev_donation: global.config.payout.poolDevDonation,
|
|
maturity_depth: global.config.payout.blocksRequired,
|
|
min_denom: global.config.payout.denom * global.config.general.sigDivisor,
|
|
coin_code: global.config.general.coinCode
|
|
});
|
|
});
|
|
|
|
// Pool APIs
|
|
app.get('/pool/address_type/:address', function (req, res) {
|
|
let address = req.params.address;
|
|
if (addressBase58Prefix === cnUtil.address_decode(new Buffer(address))) {
|
|
res.json({valid: true, address_type: 'XMR'});
|
|
} else if (btcValidator.validate(this.address) && global.config.general.allowBitcoin) {
|
|
res.json({valid: true, address_type: 'BTC'});
|
|
} else {
|
|
res.json({valid: false});
|
|
}
|
|
});
|
|
|
|
app.get('/pool/stats', function (req, res) {
|
|
let localCache = global.database.getCache('pool_stats_global');
|
|
delete(localCache.minerHistory);
|
|
delete(localCache.hashHistory);
|
|
let lastPayment = global.database.getCache('lastPaymentCycle');
|
|
res.json({pool_list: pool_list, pool_statistics: localCache, last_payment: !lastPayment ? 0 : lastPayment});
|
|
});
|
|
|
|
app.get('/pool/chart/hashrate', function (req, res) {
|
|
res.json(global.database.getCache('global_stats')['hashHistory']);
|
|
});
|
|
|
|
app.get('/pool/chart/miners', function (req, res) {
|
|
res.json(global.database.getCache('global_stats')['minerHistory']);
|
|
});
|
|
|
|
app.get('/pool/chart/hashrate/:pool_type', function (req, res) {
|
|
let pool_type = req.params.pool_type;
|
|
let localCache;
|
|
switch (pool_type) {
|
|
case 'pplns':
|
|
localCache = global.database.getCache('pplns_stats');
|
|
break;
|
|
case 'pps':
|
|
localCache = global.database.getCache('pps_stats');
|
|
break;
|
|
case 'solo':
|
|
localCache = global.database.getCache('solo_stats');
|
|
break;
|
|
case 'default':
|
|
return res.json({'error': 'Invalid pool type'});
|
|
}
|
|
res.json(localCache['hashHistory']);
|
|
});
|
|
|
|
app.get('/pool/chart/miners/:pool_type', function (req, res) {
|
|
let pool_type = req.params.pool_type;
|
|
let localCache;
|
|
switch (pool_type) {
|
|
case 'pplns':
|
|
localCache = global.database.getCache('stats_pplns');
|
|
break;
|
|
case 'pps':
|
|
localCache = global.database.getCache('stats_pps');
|
|
break;
|
|
case 'solo':
|
|
localCache = global.database.getCache('stats_solo');
|
|
break;
|
|
case 'default':
|
|
return res.json({'error': 'Invalid pool type'});
|
|
}
|
|
res.json(localCache['minerHistory']);
|
|
});
|
|
|
|
app.get('/pool/stats/:pool_type', function (req, res) {
|
|
let pool_type = req.params.pool_type;
|
|
let localCache;
|
|
switch (pool_type) {
|
|
case 'pplns':
|
|
localCache = global.database.getCache('pool_stats_pplns');
|
|
localCache.fee = global.config.payout.pplnsFee;
|
|
break;
|
|
case 'pps':
|
|
localCache = global.database.getCache('pool_stats_pps');
|
|
localCache.fee = global.config.payout.ppsFee;
|
|
break;
|
|
case 'solo':
|
|
localCache = global.database.getCache('pool_stats_solo');
|
|
localCache.fee = global.config.payout.soloFee;
|
|
break;
|
|
case 'default':
|
|
return res.json({'error': 'Invalid pool type'});
|
|
}
|
|
delete(localCache.minerHistory);
|
|
delete(localCache.hashHistory);
|
|
res.json({pool_statistics: localCache});
|
|
});
|
|
|
|
app.get('/pool/ports', function (req, res) {
|
|
res.json(global.database.getCache('poolPorts'));
|
|
});
|
|
|
|
app.get('/pool/blocks/:pool_type', function (req, res) {
|
|
let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25;
|
|
let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0;
|
|
res.json(global.database.getBlockList(req.params.pool_type).slice(page*limit, (page + 1) * limit));
|
|
});
|
|
|
|
app.get('/pool/blocks', function (req, res) {
|
|
let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25;
|
|
let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0;
|
|
res.json(global.database.getBlockList().slice(page*limit, (page + 1) * limit));
|
|
});
|
|
|
|
app.get('/pool/payments/:pool_type', function (req, res) {
|
|
let pool_type = req.params.pool_type;
|
|
let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 10;
|
|
let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0;
|
|
switch (pool_type) {
|
|
case 'pplns':
|
|
break;
|
|
case 'pps':
|
|
break;
|
|
case 'solo':
|
|
break;
|
|
case 'default':
|
|
return res.json({'error': 'Invalid pool type'});
|
|
}
|
|
let paymentIds = [];
|
|
let query = "SELECT distinct(transaction_id) as txnID FROM payments WHERE pool_type = ? ORDER BY transaction_id DESC LIMIT ? OFFSET ?";
|
|
let response = [];
|
|
global.mysql.query(query, [pool_type, limit, page * limit]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
rows.forEach(function (row, index, array) {
|
|
paymentIds.push(row.txnID);
|
|
if (array.length === paymentIds.length) {
|
|
global.mysql.query("SELECT * FROM transactions WHERE id IN (" + paymentIds.join() + ") ORDER BY id DESC").then(function (txnIDRows) {
|
|
txnIDRows.forEach(function (txnrow) {
|
|
let ts = new Date(txnrow.submitted_time);
|
|
response.push({
|
|
id: txnrow.id,
|
|
hash: txnrow.transaction_hash,
|
|
mixins: txnrow.mixin,
|
|
payees: txnrow.payees,
|
|
fee: txnrow.fees,
|
|
value: txnrow.xmr_amt,
|
|
ts: ts.getTime(),
|
|
});
|
|
if (response.length === txnIDRows.length) {
|
|
return res.json(response.sort(global.support.tsCompare));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}).catch(function (err) {
|
|
console.error(threadName + "Error getting pool payments: " + JSON.stringify(err));
|
|
return res.json({error: 'Issue getting pool payments'});
|
|
});
|
|
});
|
|
|
|
app.get('/pool/payments', function (req, res) {
|
|
let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 10;
|
|
let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0;
|
|
let query = "SELECT * FROM transactions ORDER BY id DESC LIMIT ? OFFSET ?";
|
|
global.mysql.query(query, [limit, page * limit]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
let response = [];
|
|
rows.forEach(function (row, index, array) {
|
|
global.mysql.query("SELECT pool_type FROM payments WHERE transaction_id = ? LIMIT 1", [row.id]).then(function (ptRows) {
|
|
let ts = new Date(row.submitted_time);
|
|
response.push({
|
|
id: row.id,
|
|
hash: row.transaction_hash,
|
|
mixins: row.mixin,
|
|
payees: row.payees,
|
|
fee: row.fees,
|
|
value: row.xmr_amt,
|
|
ts: ts.getTime(),
|
|
pool_type: ptRows[0].pool_type
|
|
});
|
|
if (array.length === response.length) {
|
|
res.json(response.sort(global.support.tsCompare));
|
|
}
|
|
});
|
|
});
|
|
}).catch(function (err) {
|
|
console.error(threadName + "Error getting miner payments: " + JSON.stringify(err));
|
|
res.json({error: 'Issue getting pool payments'});
|
|
});
|
|
});
|
|
|
|
// Network APIs
|
|
app.get('/network/stats', function (req, res) {
|
|
res.json(global.database.getCache('networkBlockInfo'));
|
|
});
|
|
|
|
// Miner APIs
|
|
app.get('/miner/:address/identifiers', function (req, res) {
|
|
let address = req.params.address;
|
|
return res.json(global.database.getCache(address + '_identifiers'));
|
|
});
|
|
|
|
app.get('/miner/:address/payments', function (req, res) {
|
|
let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25;
|
|
let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0;
|
|
let address_parts = req.params.address.split('.');
|
|
let address = address_parts[0];
|
|
let payment_id = address_parts[1];
|
|
let query = "SELECT amount as amt, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " +
|
|
"payments WHERE payment_address = ? AND payment_id = ? ORDER BY paid_time DESC LIMIT ? OFFSET ?";
|
|
if (typeof(payment_id) === 'undefined') {
|
|
query = "SELECT amount as amt, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " +
|
|
"payments WHERE payment_address = ? AND payment_id IS ? ORDER BY paid_time DESC LIMIT ? OFFSET ?";
|
|
}
|
|
let response = [];
|
|
global.mysql.query(query, [address, payment_id, limit, page * limit]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return res.json(response);
|
|
}
|
|
rows.forEach(function (row, index, array) {
|
|
debug(threadName + "Got rows from initial SQL query: " + JSON.stringify(row));
|
|
global.mysql.query("SELECT transaction_hash, mixin FROM transactions WHERE id = ? ORDER BY id DESC", [row.transaction_id]).then(function (txnrows) {
|
|
txnrows.forEach(function (txnrow) {
|
|
debug(threadName + "Got a row that's a transaction ID: " + JSON.stringify(txnrow));
|
|
response.push({
|
|
pt: row.pool_type,
|
|
ts: Math.ceil(row.ts),
|
|
amount: row.amt,
|
|
txnHash: txnrow.transaction_hash,
|
|
mixin: txnrow.mixin
|
|
});
|
|
if (array.length === response.length) {
|
|
return res.json(response.sort(global.support.tsCompare));
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}).catch(function (err) {
|
|
console.error(threadName + "Error getting miner payments: " + JSON.stringify(err));
|
|
return res.json({error: 'Issue getting miner payments'});
|
|
});
|
|
});
|
|
|
|
app.get('/miner/:address/stats/allWorkers', function (req, res) {
|
|
getAllWorkerStats(req.params.address, function(err, data){
|
|
return res.json(data);
|
|
});
|
|
});
|
|
|
|
app.get('/miner/:address/stats/:identifier', function (req, res) {
|
|
let address = req.params.address;
|
|
let identifier = req.params.identifier;
|
|
let memcKey = address + "_" + identifier;
|
|
/*
|
|
hash: Math.floor(localStats.miners[miner] / 600),
|
|
totalHashes: 0,
|
|
lastHash: localTimes.miners[miner]
|
|
*/
|
|
let cachedData = global.database.getCache(memcKey);
|
|
return res.json({
|
|
lts: Math.floor(cachedData.lastHash / 1000),
|
|
identifer: identifier,
|
|
hash: cachedData.hash,
|
|
totalHash: cachedData.totalHashes,
|
|
validShares: Number(cachedData.goodShares),
|
|
invalidShares: Number(cachedData.badShares)
|
|
});
|
|
});
|
|
|
|
app.get('/miner/:address/chart/hashrate', function (req, res) {
|
|
return res.json(global.database.getCache(req.params.address)['hashHistory']);
|
|
});
|
|
|
|
app.get('/miner/:address/chart/hashrate/allWorkers', function (req, res) {
|
|
getAllWorkerHashCharts(req.params.address, function(err, data){
|
|
return res.json(data);
|
|
});
|
|
});
|
|
|
|
app.get('/miner/:address/chart/hashrate/:identifier', function (req, res) {
|
|
return res.json(global.database.getCache(req.params.address + "_" + req.params.identifier)['hashHistory']);
|
|
});
|
|
|
|
app.get('/miner/:address/stats', function (req, res) {
|
|
getAddressStats(req.params.address, function(err, data){
|
|
return res.json(data);
|
|
});
|
|
});
|
|
|
|
// Authentication
|
|
app.post('/authenticate', function (req, res) {
|
|
let hmac;
|
|
try{
|
|
hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex');
|
|
} catch (e) {
|
|
return res.status(401).send({'success': false, msg: 'Invalid username/password'});
|
|
}
|
|
global.mysql.query("SELECT * FROM users WHERE username = ? AND ((pass IS null AND email = ?) OR (pass = ?))", [req.body.username, req.body.password, hmac]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return res.status(401).send({'success': false, msg: 'Invalid username/password'});
|
|
}
|
|
let token = jwt.sign({id: rows[0].id, admin: rows[0].admin}, global.config.api.secKey, {expiresIn: '1d'});
|
|
return res.json({'success': true, 'msg': token});
|
|
});
|
|
});
|
|
|
|
// JWT Verification
|
|
// get an instance of the router for api routes
|
|
let secureRoutes = express.Router();
|
|
let adminRoutes = express.Router();
|
|
|
|
// route middleware to verify a token
|
|
secureRoutes.use(function (req, res, next) {
|
|
let token = req.body.token || req.query.token || req.headers['x-access-token'];
|
|
if (token) {
|
|
jwt.verify(token, global.config.api.secKey, function (err, decoded) {
|
|
if (err) {
|
|
return res.json({success: false, msg: 'Failed to authenticate token.'});
|
|
} else {
|
|
req.decoded = decoded;
|
|
next();
|
|
}
|
|
});
|
|
|
|
} else {
|
|
return res.status(403).send({
|
|
success: false,
|
|
msg: 'No token provided.'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Secure/logged in routes.
|
|
|
|
secureRoutes.get('/tokenRefresh', function (req, res) {
|
|
let token = jwt.sign({id: req.decoded.id, admin: req.decoded.admin}, global.config.api.secKey, {expiresIn: '1d'});
|
|
return res.json({'msg': token});
|
|
});
|
|
|
|
secureRoutes.get('/', function (req, res) {
|
|
global.mysql.query("SELECT payout_threshold, enable_email FROM users WHERE id = ?", [req.decoded.id]).then(function(row){
|
|
return res.json({msg: {payout_threshold: row[0].payout_threshold, email_enabled: row[0].enable_email}});
|
|
});
|
|
});
|
|
|
|
secureRoutes.post('/changePassword', function (req, res) {
|
|
let hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex');
|
|
global.mysql.query("UPDATE users SET pass = ? WHERE id = ?", [hmac, req.decoded.id]).then(function () {
|
|
return res.json({'msg': 'Password updated'});
|
|
});
|
|
});
|
|
|
|
secureRoutes.post('/toggleEmail', function (req, res) {
|
|
global.mysql.query("UPDATE users SET enable_email = NOT enable_email WHERE id = ?", [req.decoded.id]).then(function () {
|
|
return res.json({'msg': 'Email toggled'});
|
|
});
|
|
});
|
|
|
|
secureRoutes.post('/changePayoutThreshold', function (req, res) {
|
|
let threshold = req.body.threshold;
|
|
if (threshold < global.config.payout.walletMin) {
|
|
threshold = global.config.payout.walletMin;
|
|
}
|
|
threshold = global.support.decimalToCoin(threshold);
|
|
global.mysql.query("UPDATE users SET payout_threshold = ? WHERE id = ?", [threshold, req.decoded.id]).then(function () {
|
|
return res.json({'msg': 'Threshold updated, set to: ' + global.support.coinToDecimal(threshold)});
|
|
});
|
|
});
|
|
|
|
// Administrative routes/APIs
|
|
|
|
adminRoutes.use(function (req, res, next) {
|
|
let token = req.body.token || req.query.token || req.headers['x-access-token'];
|
|
if (token) {
|
|
jwt.verify(token, global.config.api.secKey, function (err, decoded) {
|
|
if (decoded.admin !== 1) {
|
|
return res.status(403).send({
|
|
success: false,
|
|
msg: 'You are not an admin.'
|
|
});
|
|
}
|
|
if (err) {
|
|
return res.json({success: false, msg: 'Failed to authenticate token.'});
|
|
} else {
|
|
req.decoded = decoded;
|
|
next();
|
|
}
|
|
});
|
|
|
|
} else {
|
|
return res.status(403).send({
|
|
success: false,
|
|
msg: 'No token provided.'
|
|
});
|
|
}
|
|
});
|
|
|
|
adminRoutes.get('/stats', function (req, res) {
|
|
/*
|
|
Admin interface stats.
|
|
For each pool type + global, we need the following:
|
|
Total Owed, Total Paid, Total Mined, Total Blocks, Average Luck
|
|
*/
|
|
let intCache = {
|
|
'pplns': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
|
|
'pps': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
|
|
'solo': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
|
|
'global': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
|
|
'fees': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0}
|
|
};
|
|
async.series([
|
|
function (callback) {
|
|
global.mysql.query("select * from balance").then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
intCache[row.pool_type].owed += row.amount;
|
|
intCache.global.owed += row.amount;
|
|
});
|
|
}).then(function () {
|
|
return callback(null);
|
|
});
|
|
},
|
|
function (callback) {
|
|
global.mysql.query("select * from payments").then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
intCache[row.pool_type].paid += row.amount;
|
|
intCache.global.paid += row.amount;
|
|
});
|
|
}).then(function () {
|
|
return callback(null);
|
|
});
|
|
},
|
|
function (callback) {
|
|
global.database.getBlockList().forEach(function (block) {
|
|
intCache[block.pool_type].mined += block.value;
|
|
intCache.global.mined += block.value;
|
|
intCache[block.pool_type].shares += block.shares;
|
|
intCache.global.shares += block.shares;
|
|
intCache[block.pool_type].targetShares += block.diff;
|
|
intCache.global.targetShares += block.diff;
|
|
});
|
|
return callback(null);
|
|
}
|
|
], function () {
|
|
return res.json(intCache);
|
|
});
|
|
});
|
|
|
|
adminRoutes.get('/wallet', function (req, res) {
|
|
// Stats for the admin interface.
|
|
// Load the wallet state from cache, NOTHING HAS DIRECT ACCESS.
|
|
// walletStateInfo
|
|
return res.json(global.database.getCache('walletStateInfo'));
|
|
});
|
|
|
|
adminRoutes.get('/wallet/history', function (req, res) {
|
|
// walletHistory
|
|
if (req.decoded.admin === 1) {
|
|
return res.json(global.database.getCache('walletHistory'));
|
|
}
|
|
});
|
|
|
|
adminRoutes.get('/ports', function (req, res) {
|
|
let retVal = [];
|
|
global.mysql.query("SELECT * FROM port_config").then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
retVal.push({
|
|
port: row.poolPort,
|
|
diff: row.difficulty,
|
|
desc: row.portDesc,
|
|
portType: row.portType,
|
|
hidden: row.hidden === 1,
|
|
ssl: row.ssl === 1
|
|
});
|
|
});
|
|
}).then(function () {
|
|
return res.json(retVal);
|
|
});
|
|
});
|
|
|
|
adminRoutes.post('/ports', function (req, res) {
|
|
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [req.body.port]).then(function (rows) {
|
|
if (rows.length !== 0) {
|
|
return "Port already exists with that port number.";
|
|
}
|
|
if (req.body.diff > global.config.pool.maxDifficulty || req.body.diff < global.config.pool.minDifficulty) {
|
|
return "Invalid difficulty.";
|
|
}
|
|
if (["pplns", "solo", "pps"].indexOf(req.body.portType) === -1) {
|
|
return "Invalid port type";
|
|
}
|
|
global.mysql.query("INSERT INTO port_config (poolPort, difficulty, portDesc, portType, hidden, ssl) VALUES (?, ?, ?, ?, ?, ?)",
|
|
[req.body.port, req.body.diff, req.body.desc, req.body.portType, req.body.hidden === 1, req.body.ssl === 1]);
|
|
}).then(function (err) {
|
|
if (typeof(err) === 'string') {
|
|
return res.json({success: false, msg: err});
|
|
}
|
|
return res.json({success: true, msg: "Added port to database"});
|
|
});
|
|
});
|
|
|
|
adminRoutes.put('/ports', function (req, res) {
|
|
let portNumber = Number(req.body.portNum);
|
|
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [portNumber]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return "Port doesn't exist in the database";
|
|
}
|
|
if (req.body.diff > global.config.pool.maxDifficulty || req.body.diff < global.config.pool.minDifficulty) {
|
|
return "Invalid difficulty.";
|
|
}
|
|
if (["pplns", "solo", "pps"].indexOf(req.body.portType) === -1) {
|
|
return "Invalid port type";
|
|
}
|
|
global.mysql.query("UPDATE port_config SET difficulty=?, portDesc=?, portType=?, hidden=?, ssl=? WHERE poolPort = ?",
|
|
[req.body.diff, req.body.desc, req.body.portType, req.body.hidden === 1, req.body.ssl === 1, portNumber]);
|
|
}).then(function (err) {
|
|
if (typeof(err) === 'string') {
|
|
return res.json({success: false, msg: err});
|
|
}
|
|
return res.json({success: true, msg: "Updated port in database"});
|
|
});
|
|
});
|
|
|
|
adminRoutes.delete('/ports', function (req, res) {
|
|
let portNumber = Number(req.body.portNum);
|
|
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [portNumber]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return "Port doesn't exist in the database";
|
|
}
|
|
global.mysql.query("DELETE FROM port_config WHERE poolPort = ?", [portNumber]);
|
|
}).then(function (err) {
|
|
if (typeof(err) === 'string') {
|
|
return res.json({success: false, msg: err});
|
|
}
|
|
return res.json({success: true, msg: "Added port to database"});
|
|
});
|
|
});
|
|
|
|
adminRoutes.get('/config', function (req, res) {
|
|
let retVal = [];
|
|
global.mysql.query("SELECT * FROM config").then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
retVal.push({
|
|
id: row.id,
|
|
module: row.module,
|
|
item: row.item,
|
|
value: row.item_value,
|
|
type: row.item_type,
|
|
desc: row.item_desc
|
|
});
|
|
});
|
|
}).then(function () {
|
|
return res.json(retVal);
|
|
});
|
|
});
|
|
|
|
adminRoutes.put('/config', function (req, res) {
|
|
let configID = Number(req.body.id);
|
|
global.mysql.query("SELECT * FROM config WHERE id = ?", [configID]).then(function (rows) {
|
|
if (rows.length === 0) {
|
|
return "Config item doesn't exist in the database";
|
|
}
|
|
global.mysql.query("UPDATE config SET item_value=? WHERE id = ?", [req.body.value, configID]);
|
|
}).then(function (err) {
|
|
if (typeof(err) === 'string') {
|
|
return res.json({success: false, msg: err});
|
|
}
|
|
return res.json({success: true, msg: "Updated port in database"});
|
|
});
|
|
});
|
|
|
|
adminRoutes.get('/userList', function (req, res) {
|
|
/*
|
|
List of all the users in the system.
|
|
Might as well do it all, right? :3
|
|
Data Format to be documented.
|
|
*/
|
|
let intCache = {};
|
|
global.mysql.query("select sum(balance.amount) as amt_due, sum(payments.amount) as amt_paid," +
|
|
"balance.payment_address as address, balance.payment_id as payment_id from balance LEFT JOIN payments on " +
|
|
"payments.payment_address=balance.payment_address or payments.payment_id=balance.payment_id " +
|
|
"group by address, payment_id").then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
let key = row.address;
|
|
if (row.payment_id !== null) {
|
|
key += '.' + row.payment_id;
|
|
}
|
|
intCache[key] = {
|
|
paid: row.amt_paid,
|
|
due: row.amt_due,
|
|
address: key,
|
|
workers: [],
|
|
lastHash: 0,
|
|
totalHashes: 0,
|
|
hashRate: 0,
|
|
goodShares: 0,
|
|
badShares: 0
|
|
};
|
|
});
|
|
}).then(function () {
|
|
let minerList = global.database.getCache('minerList');
|
|
if (minerList) {
|
|
minerList.forEach(function (miner) {
|
|
let minerData = miner.split('_');
|
|
let minerCache = global.database.getCache(miner);
|
|
if (!minerCache.hasOwnProperty('goodShares')) {
|
|
minerCache.goodShares = 0;
|
|
minerCache.badShares = 0;
|
|
}
|
|
if (!intCache.hasOwnProperty(minerData[0])) {
|
|
intCache[minerData[0]] = {paid: 0, due: 0, address: minerData[0], workers: []};
|
|
}
|
|
if (typeof(minerData[1]) !== 'undefined') {
|
|
intCache[minerData[0]].workers.push({
|
|
worker: minerData[1],
|
|
hashRate: minerCache.hash,
|
|
lastHash: minerCache.lastHash,
|
|
totalHashes: minerCache.totalHashes,
|
|
goodShares: minerCache.goodShares,
|
|
badShares: minerCache.badShares
|
|
});
|
|
} else {
|
|
intCache[minerData[0]].lastHash = minerCache.lastHash;
|
|
intCache[minerData[0]].totalHashes = minerCache.totalHashes;
|
|
intCache[minerData[0]].hashRate = minerCache.hash;
|
|
intCache[minerData[0]].goodShares = minerCache.goodShares;
|
|
intCache[minerData[0]].badShares = minerCache.badShares;
|
|
}
|
|
});
|
|
let retList = [];
|
|
for (let minerId in intCache) {
|
|
if (intCache.hasOwnProperty(minerId)) {
|
|
let miner = intCache[minerId];
|
|
retList.push(miner);
|
|
}
|
|
}
|
|
return res.json(retList);
|
|
}
|
|
return res.json([]);
|
|
});
|
|
});
|
|
|
|
// apply the routes to our application with the prefix /api
|
|
app.use('/authed', secureRoutes);
|
|
app.use('/admin', adminRoutes);
|
|
|
|
// Authenticated routes
|
|
|
|
let workerList = [];
|
|
|
|
if (cluster.isMaster) {
|
|
let numWorkers = require('os').cpus().length;
|
|
console.log('Master cluster setting up ' + numWorkers + ' workers...');
|
|
|
|
for (let i = 0; i < numWorkers; i++) {
|
|
let worker = cluster.fork();
|
|
workerList.push(worker);
|
|
}
|
|
|
|
cluster.on('online', function (worker) {
|
|
console.log('Worker ' + worker.process.pid + ' is online');
|
|
});
|
|
|
|
cluster.on('exit', function (worker, code, signal) {
|
|
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
|
|
console.log('Starting a new worker');
|
|
worker = cluster.fork();
|
|
workerList.push(worker);
|
|
});
|
|
} else {
|
|
app.listen(8001, function () {
|
|
console.log('Process ' + process.pid + ' is listening to all incoming requests');
|
|
});
|
|
}
|