Initial commit, moving from private repo. Welcome to the light.

This commit is contained in:
Alexander Blair
2017-02-10 21:33:17 -08:00
commit fd58d56ac5
28 changed files with 5427 additions and 0 deletions
+783
View File
@@ -0,0 +1,783 @@
"use strict";
const express = require('express'); // call express
const app = express(); // define our app using express
const cluster = require('cluster');
const async = require("async");
const debug = require("debug")("api");
const btcValidator = require('wallet-address-validator');
const cnUtil = require('cryptonote-util');
let bodyParser = require('body-parser');
let jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens
const crypto = require('crypto');
let addressBase58Prefix = cnUtil.address_decode(new Buffer(global.config.pool.address));
let threadName = "";
let workerList = [];
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
// 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 * global.config.general.sigDivisor,
pool_dev_donation: global.config.payout.poolDevDonation * global.config.general.sigDivisor,
maturity_depth: global.config.payout.blocksRequired,
min_denom: global.config.payout.denom * global.config.general.sigDivisor
});
});
// 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);
res.json({pool_list: ['pplns', 'pps', 'solo'], pool_statistics: localCache});
});
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) {
res.json(global.database.getBlockList(req.params.pool_type));
});
app.get('/pool/blocks', function (req, res) {
res.json(global.database.getBlockList());
});
app.get('/pool/payments/:pool_type', function (req, res) {
let pool_type = req.params.pool_type;
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";
let response = [];
global.mysql.query(query, [pool_type]).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 query = "SELECT * FROM transactions ORDER BY id DESC";
global.mysql.query(query).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 miner 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 address_parts = req.params.address.split('.');
let address = address_parts[0];
let payment_id = address_parts[1];
let query = "SELECT amount, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " +
"payments WHERE payment_address = ? AND paid_time < ? AND payment_id = ? ORDER BY paid_time DESC LIMIT 25";
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 paid_time < ? AND payment_id IS ? ORDER BY paid_time DESC LIMIT 25";
}
let start = req.query.start || Date.now() / 1000;
start *= 1000;
let response = [];
global.mysql.query(query, [address, global.support.formatDate(start), payment_id]).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);
}
});
});
});
}).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) {
let address = req.params.address;
let identifiers = global.database.getCache(address + '_identifiers').sort();
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;
identifiers.forEach(function(identifier){
let cachedData = global.database.getCache(req.params.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 res.json(returnData);
}
});
});
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
});
});
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) {
let address = req.params.address;
let identifiers = global.database.getCache(address + '_identifiers').sort();
let returnData = {global: global.database.getCache(req.params.address)['hashHistory']};
let intCounter = 0;
identifiers.forEach(function(identifier){
returnData[identifier] = global.database.getCache(req.params.address+"_"+identifier)['hashHistory'];
intCounter += 1;
if (intCounter === identifiers.length){
return res.json(returnData);
}
});
});
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) {
let address = req.params.address;
let address_parts = req.params.address.split('.');
let address_pt = address_parts[0];
let payment_id = address_parts[1];
let paidQuery = "SELECT SUM(amount) 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 ?";
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: global.database.getCache(address).hash, identifier: 'global'});
},
function (returnData, callback) {
// TODO: Fixme once we have total hash counts...
returnData.totalHashes = global.database.getCache(address).totalHashes;
return callback(null, returnData);
},
function (returnData, callback) {
debug(threadName + "Checking Influx for last share for /miner/address/stats");
returnData.lastHash = Math.floor(global.database.getCache(address).lastHash / 1000);
return callback(null, returnData);
},
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(true, returnData);
});
}
], function (err, result) {
debug(threadName + "Result information for " + address + ": " + JSON.stringify(result));
if (err === true) {
return res.json(result);
}
if (err) {
console.error(threadName + "Error within the miner stats identifier func");
return res.json({'error': err.toString()});
}
});
});
// Authentication
app.post('/authenticate', function (req, res) {
let hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex');
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.json({'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 FROM users WHERE id = ?", [req.decoded.id]).then(function(row){
return res.json({msg: row[0].payout_threshold});
});
return res.json({msg: 0});
});
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('/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.config.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 (err) {
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.decoded.port]).then(function (rows) {
if (rows.length !== 0) {
return "Port already exists with that port number.";
}
if (req.decoded.diff > global.config.pool.maxDifficulty || req.decoded.diff < global.config.pool.minDifficulty) {
return "Invalid difficulty.";
}
if (["pplns", "solo", "pps"].indexOf(req.decoded.portType) === -1) {
return "Invalid port type";
}
global.mysql.query("INSERT INTO port_config (poolPort, difficulty, portDesc, portType, hidden, ssl) VALUES (?, ?, ?, ?, ?, ?)",
[req.decoded.port, req.decoded.diff, req.decoded.desc, req.decoded.portType, req.decoded.hidden === 1, req.decoded.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.decoded.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.decoded.diff > global.config.pool.maxDifficulty || req.decoded.diff < global.config.pool.minDifficulty) {
return "Invalid difficulty.";
}
if (["pplns", "solo", "pps"].indexOf(req.decoded.portType) === -1) {
return "Invalid port type";
}
global.mysql.query("UPDATE port_config SET difficulty=?, portDesc=?, portType=?, hidden=?, ssl=? WHERE poolPort = ?",
[req.decoded.diff, req.decoded.desc, req.decoded.portType, req.decoded.hidden === 1, req.decoded.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.decoded.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.decoded.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.decoded.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
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');
});
}
+504
View File
@@ -0,0 +1,504 @@
"use strict";
const range = require("range");
const debug = require("debug")("blockManager");
const async = require("async");
// This file is for managing the block databases within the SQL database.
// Primary Tasks:
// Sync the chain into the block_log database. - Scan on startup for missing data, starting from block 0
// Maintain a check for valid blocks in the system. (Only last number of blocks required for validation of payouts) - Perform every 2 minutes. Scan on the main blocks table as well for sanity sake.
// Maintain the block_log database in order to ensure payments happen smoothly. - Scan every 1 second for a change in lastblockheader, if it changes, insert into the DB.
let blockIDCache = [];
let scanInProgress = false;
let blockHexCache = {};
let lastBlock = 0;
let balanceIDCache = {};
let blockQueue = async.queue(function (task, callback) {
global.support.rpcDaemon('getblockheaderbyheight', {"height": task.blockID}, function (body) {
let blockData = body.result.block_header;
if (blockData.hash in blockHexCache) {
return callback();
}
debug("Adding block to block_log, ID: " + task.blockID);
blockIDCache.push(task.blockID);
blockHexCache[body.result.block_header.hash] = null;
global.mysql.query("INSERT INTO block_log (id, orphan, hex, find_time, reward, difficulty, major_version, minor_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[task.blockID, blockData.orphan_status, blockData.hash, global.support.formatDate(blockData.timestamp * 1000), blockData.reward, blockData.difficulty, blockData.major_version, blockData.minor_version]).then(function () {
return calculatePPSPayments(blockData, callback);
}).catch(function (err) {
debug("BlockHexCache Check: " + blockData.hash in blockHexCache);
debug("BlockIDCache Check: " + blockIDCache.hasOwnProperty(task.blockID));
debug("Hex: " + blockData.hash + " Height:" + task.blockID);
console.error("Tried to reprocess a block that'd already been processed");
console.error(JSON.stringify(err));
return callback();
});
});
}, 16);
blockQueue.drain = function () {
console.log("Scan complete, unlocking remainder of blockManager functionality.");
scanInProgress = false;
};
let createBalanceQueue = async.queue(function (task, callback) {
let pool_type = task.pool_type;
let payment_address = task.payment_address;
let payment_id = task.payment_id;
let bitcoin = task.bitcoin;
let query = "SELECT id FROM balance WHERE payment_address = ? AND payment_id is ? AND pool_type = ? AND bitcoin = ?";
if (payment_id !== null) {
query = "SELECT id FROM balance WHERE payment_address = ? AND payment_id = ? AND pool_type = ? AND bitcoin = ?";
}
let cacheKey = payment_address + pool_type + bitcoin + payment_id;
debug("Processing a account add/check for:" + JSON.stringify(task));
global.mysql.query(query, [payment_address, payment_id, pool_type, bitcoin]).then(function (rows) {
if (rows.length === 0) {
global.mysql.query("INSERT INTO balance (payment_address, payment_id, pool_type, bitcoin) VALUES (?, ?, ?, ?)", [payment_address, payment_id, pool_type, bitcoin]).then(function (result) {
debug("Added to the SQL database: " + result.insertId);
balanceIDCache[cacheKey] = result.insertId;
return callback();
});
} else {
debug("Found it in MySQL: " + rows[0].id);
balanceIDCache[cacheKey] = rows[0].id;
return callback();
}
});
}, 1);
let balanceQueue = async.queue(function (task, callback) {
let pool_type = task.pool_type;
let payment_address = task.payment_address;
let payment_id = null;
if (typeof(task.payment_id) !== 'undefined' && task.payment_id !== null && task.payment_id.length > 10){
payment_id = task.payment_id;
}
task.payment_id = payment_id;
let bitcoin = task.bitcoin;
let amount = task.amount;
debug("Processing balance increment task: " + JSON.stringify(task));
async.waterfall([
function (intCallback) {
let cacheKey = payment_address + pool_type + bitcoin + payment_id;
if (cacheKey in balanceIDCache) {
return intCallback(null, balanceIDCache[cacheKey]);
} else {
createBalanceQueue.push(task, function () {
});
async.until(function () {
return cacheKey in balanceIDCache;
}, function (intCallback) {
createBalanceQueue.push(task, function () {
return intCallback(null, balanceIDCache[cacheKey]);
});
}, function () {
return intCallback(null, balanceIDCache[cacheKey]);
}
);
}
},
function (balance_id, intCallback) {
debug("Made it to the point that I can update the balance for: " + balance_id + " for the amount: " + amount);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE id = ?", [amount, balance_id]).then(function () {
return intCallback(null);
});
}
],
function () {
return callback();
}
)
;
}, 24
);
function calculatePPSPayments(blockHeader, callback) {
console.log("Performing PPS payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
let totalPayments = 0;
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockHeader.height) === blockHeader.height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let blockDiff = blockHeader.difficulty;
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.PPS) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'pps',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let amountToPay = Math.floor((shareData.shares / blockDiff) * rewardTotal);
let feesToPay = Math.floor(amountToPay * (global.config.payout.ppsFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(amountToPay * (global.config.payout.btcFee / 100));
}
amountToPay -= feesToPay;
paymentData[userIdentifier].amount = paymentData[userIdentifier].amount + amountToPay;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = paymentData[global.config.payout.feeAddress].amount + feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("PPS payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
return callback();
}
function calculatePPLNSPayments(blockHeader) {
console.log("Performing PPLNS payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let rewardTotal = blockHeader.reward;
let blockCheckHeight = blockHeader.height;
let totalPaid = 0;
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
async.doWhilst(function (callback) {
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockCheckHeight) === blockCheckHeight); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let blockDiff = blockHeader.difficulty;
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.PPLNS) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'pplns',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let amountToPay = Math.floor((shareData.shares / (blockDiff*2)) * rewardTotal);
if (totalPaid + amountToPay > rewardTotal) {
amountToPay = rewardTotal - totalPaid;
}
totalPaid += amountToPay;
let feesToPay = Math.floor(amountToPay * (global.config.payout.pplnsFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(amountToPay * (global.config.payout.btcFee / 100));
}
amountToPay -= feesToPay;
paymentData[userIdentifier].amount = paymentData[userIdentifier].amount + amountToPay;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = paymentData[global.config.payout.feeAddress].amount + feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
callback(null, totalPaid);
}, function (totalPayment) {
blockCheckHeight = blockCheckHeight - 1;
debug("Decrementing the block chain check height to:" + blockCheckHeight);
if (totalPayment >= rewardTotal) {
debug("Loop 1: Total Payment: " + totalPayment + " Amount Paid: " + rewardTotal + " Amount Total: " + totalPaid);
return false;
} else {
debug("Loop 2: Total Payment: " + totalPayment + " Amount Paid: " + rewardTotal + " Amount Total: " + totalPaid);
return blockCheckHeight !== 0;
}
}, function (err) {
let totalPayments = 0;
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("PPLNS payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
});
}
function calculateSoloPayments(blockHeader) {
console.log("Performing Solo payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
let totalPayments = 0;
for (let found = (cursor.goToRange(blockHeader.height) === blockHeader.height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.SOLO && shareData.foundBlock === true) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'solo',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let feesToPay = Math.floor(rewardTotal * (global.config.payout.soloFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(rewardTotal * (global.config.payout.btcFee / 100));
}
rewardTotal -= feesToPay;
paymentData[userIdentifier].amount = rewardTotal;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("Solo payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
}
function blockInvalidator() {
if (scanInProgress) {
debug("Skipping block invalidator run as there's a scan in progress");
return;
}
debug("Running block invalidator");
global.mysql.query("SELECT id, hex FROM block_log ORDER BY find_time DESC LIMIT ?", [global.config.payout.blocksRequired]).then(function (rows) {
rows.forEach(function (row) {
global.support.rpcDaemon('getblockheaderbyheight', {"height": row.id}, function (body) {
// DO NOT TRUST THIS. GET THE BLOCK HEADER BY HEIGHT AND VERIFY THAT THE HASHES MATCH.
if (body.result.block_header.hash !== row.hex) {
global.database.invalidateBlock(row.id);
global.mysql.query("UPDATE block_log SET orphan = true WHERE hex = ?", [row.hex]);
blockIDCache.splice(blockIDCache.indexOf(body.result.block_header.height));
console.log("Invalidating block " + body.result.block_header.height + " due to being an orphan block");
}
});
});
});
}
function blockUnlocker() {
if (scanInProgress) {
debug("Skipping block unlocker run as there's a scan in progress");
return;
}
debug("Running block unlocker");
let blockList = global.database.getValidLockedBlocks();
global.support.rpcDaemon('getlastblockheader', [], function (body) {
let blockHeight = body.result.block_header.height;
blockList.forEach(function (row) {
if (blockHeight - row.height > global.config.payout.blocksRequired) {
blockPayments(row);
}
});
});
}
function blockPayments(block) {
switch (block.poolType) {
case global.protos.POOLTYPE.PPS:
// PPS is paid out per share find per block, so this is handled in the main block-find loop.
break;
case global.protos.POOLTYPE.PPLNS:
global.coinFuncs.getBlockHeaderByHash(block.hash, function (header) {
calculatePPLNSPayments(header);
});
break;
case global.protos.POOLTYPE.SOLO:
global.coinFuncs.getBlockHeaderByHash(block.hash, function (header) {
calculateSoloPayments(header);
});
break;
default:
console.log("Unknown payment type. FREAKOUT");
break;
}
global.database.unlockBlock(block.hash);
}
function blockScanner() {
let inc_check = 0;
if (scanInProgress) {
debug("Skipping scan as there's one in progress.");
return;
}
scanInProgress = true;
global.coinFuncs.getLastBlockHeader(function (blockHeader) {
if (lastBlock === blockHeader.height) {
debug("No new work to be performed, block header matches last block");
scanInProgress = false;
return;
}
debug("Parsing data for new blocks");
lastBlock = blockHeader.height;
range.range(0, (blockHeader.height - global.config.payout.blocksRequired)).forEach(function (blockID) {
if (!blockIDCache.hasOwnProperty(blockID)) {
inc_check += 1;
blockQueue.push({blockID: blockID}, function (err) {
debug("Completed block scan on " + blockID);
if (err) {
console.error("Error processing " + blockID);
}
});
}
});
if (inc_check === 0) {
debug("No new work to be performed, initial scan complete");
scanInProgress = false;
}
});
}
function initial_sync() {
console.log("Performing boot-sync");
global.mysql.query("SELECT id, hex FROM block_log WHERE orphan = 0").then(function (rows) {
let intCount = 0;
rows.forEach(function (row) {
intCount += 1;
blockIDCache.push(row.id);
blockHexCache[row.hex] = null;
});
}).then(function () {
// Enable block scanning for 1 seconds to update the block log.
setInterval(blockScanner, 1000);
// Scan every 120 seconds for invalidated blocks
setInterval(blockInvalidator, 120000);
setInterval(blockUnlocker, 120000);
blockInvalidator();
blockUnlocker();
debug("Blocks loaded from SQL: " + blockIDCache.length);
console.log("Boot-sync from SQL complete. Pending completion of queued jobs to get back to work.");
});
}
initial_sync();
+121
View File
@@ -0,0 +1,121 @@
"use strict";
const bignum = require('bignum');
const cnUtil = require('cryptonote-util');
const multiHashing = require('multi-hashing');
const crypto = require('crypto');
function Coin(data){
this.data = data;
let instanceId = crypto.randomBytes(4);
this.coinDevAddress = "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A"; // Developer Address
this.poolDevAddress = "44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr"; // Snipa Address
this.blockedAddresses = [
this.coinDevAddress,
this.poolDevAddress,
"43SLUTpyTgXCNXsL43uD8FWZ5wLAdX7Ak67BgGp7dxnGhLmrffDTXoeGm2GBRm8JjigN9PTg2gnShQn5gkgE1JGWJr4gsEU", // Wolf0's address
"42QWoLF7pdwMcTXDviJvNkWEHJ4TXnMBh2Cx6HNkVAW57E48Zfw6wLwDUYFDYJAqY7PLJUTz9cHWB5C4wUA7UJPu5wPf4sZ", // Wolf0's address
"46gq64YYgCk88LxAadXbKLeQtCJtsLSD63NiEc3XHLz8NyPAyobACP161JbgyH2SgTau3aPUsFAYyK2RX4dHQoaN1ats6iT" // Claymore's Fee Address.
];
this.exchangeAddresses = [
"46yzCCD3Mza9tRj7aqPSaxVbbePtuAeKzf8Ky2eRtcXGcEgCg1iTBio6N4sPmznfgGEUGDoBz5CLxZ2XPTyZu1yoCAG7zt6", // Shapeshift.io
"463tWEBn5XZJSxLU6uLQnQ2iY9xuNcDbjLSjkn3XAXHCbLrTTErJrBWYgHJQyrCwkNgYvyV3z8zctJLPCZy24jvb3NiTcTJ", // Bittrex
"44TVPcCSHebEQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV4UpB5n4", // Xmr.to
"47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12noXmi4ZyBZLc99e66NtnKff34fHsGRoyZk3ES1s1V4QVcB" // Poloniex
]; // These are addresses that MUST have a paymentID to perform logins with.
this.prefix = 18;
this.intPrefix = 19;
if (global.config.general.testnet === true){
this.prefix = 53;
this.intPrefix = 54;
}
this.getBlockHeaderByID = function(blockId, callback){
global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) {
if (body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
}
});
};
this.getBlockHeaderByHash = function(blockHash, callback){
global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) {
if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
return callback(false);
}
});
};
this.getLastBlockHeader = function(callback){
global.support.rpcDaemon('getlastblockheader', [], function (body) {
if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
}
});
};
this.getBlockTemplate = function(walletAddress, callback){
global.support.rpcDaemon('getblocktemplate', {
reserve_size: 8,
wallet_address: walletAddress
}, function(body){
return callback(body);
});
};
this.baseDiff = function(){
return bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16);
};
this.validateAddress = function(address){
// This function should be able to be called from the async library, as we need to BLOCK ever so slightly to verify the address.
address = new Buffer(address);
if (cnUtil.address_decode(address) === this.prefix){
return true;
}
return cnUtil.address_decode_integrated(address) === this.intPrefix;
};
this.convertBlob = function(blobBuffer){
return cnUtil.convert_blob(blobBuffer);
};
this.constructNewBlob = function(blockTemplate, NonceBuffer){
return cnUtil.construct_block_blob(blockTemplate, NonceBuffer);
};
this.getBlockID = function(blockBuffer){
return cnUtil.get_block_id(blockBuffer);
};
this.BlockTemplate = function(template) {
this.blob = template.blocktemplate_blob;
this.difficulty = template.difficulty;
this.height = template.height;
this.reserveOffset = template.reserved_offset;
this.buffer = new Buffer(this.blob, 'hex');
instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3);
this.previous_hash = new Buffer(32);
this.buffer.copy(this.previous_hash, 0, 7, 39);
this.extraNonce = 0;
this.nextBlob = function () {
this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset);
return global.coinFuncs.convertBlob(this.buffer).toString('hex');
};
};
this.cryptoNight = multiHashing['cryptonight'];
}
module.exports = Coin;
+51
View File
@@ -0,0 +1,51 @@
enum POOLTYPE {
PPLNS = 0;
PPS = 1;
PROP = 2;
SOLO = 3;
}
enum MESSAGETYPE {
SHARE = 0;
BLOCK = 1;
INVALIDSHARE = 2;
}
message WSData {
required MESSAGETYPE msgType = 1;
required string key = 2;
required bytes msg = 3;
required int32 exInt = 4;
}
message InvalidShare{
required string paymentAddress = 1;
optional string paymentID = 2;
required string identifier = 3;
}
message Share {
required int32 shares = 1;
required string paymentAddress = 2;
required bool foundBlock = 3;
optional string paymentID = 4;
required bool trustedShare = 5;
required POOLTYPE poolType = 6;
required int32 poolID = 7;
required int64 blockDiff = 8;
required bool bitcoin = 9;
required int32 blockHeight = 10;
required int64 timestamp = 11;
required string identifier = 12;
}
message Block {
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
optional int64 value = 8;
}
+467
View File
@@ -0,0 +1,467 @@
"use strict";
let range = require('range');
let debug = require('debug')('db');
let async = require('async');
function Database(){
this.lmdb = require('node-lmdb');
this.env = null;
this.shareDB = null;
this.blockDB = null;
this.cacheDB = null;
this.dirtyenv = false;
this.initEnv = function(){
global.database.env = new this.lmdb.Env();
global.database.env.open({
path: global.config.db_storage_path,
maxDbs: 10,
mapSize: 24 * 1024 * 1024 * 1024,
noSync: true,
mapAsync: true,
useWritemap: true,
noMetaSync: true,
maxReaders: 512
});
global.database.shareDB = this.env.openDbi({
name: 'shares',
create: true,
dupSort: true,
dupFixed: false,
integerDup: true,
integerKey: true,
keyIsUint32: true
});
global.database.blockDB = this.env.openDbi({
name: 'blocks',
create: true,
integerKey: true,
keyIsUint32: true
});
global.database.cacheDB = this.env.openDbi({
name: 'cache',
create: true
});
global.database.intervalID = setInterval(function(){
global.database.env.sync(function(){});
}, 60000); // Sync the DB every 60 seconds
global.database.dirtyenv = false;
console.log("Database Worker: LMDB Env Initialized.");
};
this.incrementCacheData = function(key, data){
this.refreshEnv();
let txn = this.env.beginTxn();
let cached = txn.getString(this.cacheDB, key);
if (cached !== null){
cached = JSON.parse(cached);
data.forEach(function(intDict){
if (!cached.hasOwnProperty(intDict.location)){
cached[intDict.location] = 0;
}
cached[intDict.location] += intDict.value;
});
txn.putString(this.cacheDB, key, JSON.stringify(cached));
txn.commit();
return;
}
txn.abort();
};
this.getBlockList = function(pool_type){
debug("Getting block list");
switch (pool_type) {
case 'pplns':
pool_type = global.protos.POOLTYPE.PPLNS;
break;
case 'pps':
pool_type = global.protos.POOLTYPE.PPS;
break;
case 'solo':
pool_type = global.protos.POOLTYPE.SOLO;
break;
default:
pool_type = false;
}
let response = [];
try{
this.refreshEnv();
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.blockDB);
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
/*
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
*/
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let blockData = global.protos.Block.decode(data);
let poolType;
switch (blockData.poolType){
case (global.protos.POOLTYPE.PPLNS):
poolType = 'pplns';
break;
case (global.protos.POOLTYPE.SOLO):
poolType = 'solo';
break;
case (global.protos.POOLTYPE.PPS):
poolType = 'pps';
break;
default:
poolType = 'Unknown';
break;
}
if (blockData.poolType === pool_type || pool_type === false) {
response.push({
ts: blockData.timestamp,
hash: blockData.hash,
diff: blockData.difficulty,
shares: blockData.shares,
height: key,
valid: blockData.valid,
unlocked: blockData.unlocked,
pool_type: poolType,
value: blockData.value
});
}
});
}
cursor.close();
txn.abort();
return response.sort(global.support.blockCompare);
} catch (e){
return response;
}
};
this.storeShare = function(blockId, shareData, callback){
// This function needs the blockID in question, and the shareData in binary format.
// The binary data should be packed as per the data.proto Share protobuf format.
try {
let share = global.protos.Share.decode(shareData);
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
this.incrementCacheData('global_stats', [{location: 'totalHashes', value: share.shares}]);
switch (share.poolType) {
case global.protos.POOLTYPE.PPLNS:
this.incrementCacheData('pplns_stats', [{location: 'totalHashes', value: share.shares}]);
break;
case global.protos.POOLTYPE.PPS:
this.incrementCacheData('pps_stats', [{location: 'totalHashes', value: share.shares}]);
break;
case global.protos.POOLTYPE.SOLO:
this.incrementCacheData('solo_stats', [{location: 'totalHashes', value: share.shares}]);
break;
}
this.incrementCacheData(minerIDWithIdentifier, [{location: 'totalHashes', value: share.shares},{location: 'goodShares', value: 1}]);
this.incrementCacheData(minerID, [{location: 'totalHashes', value: share.shares},{location: 'goodShares', value: 1}]);
} catch (e){
callback(false);
return;
}
this.refreshEnv();
let txn = this.env.beginTxn();
txn.putBinary(this.shareDB, blockId, shareData);
txn.commit();
callback(true);
};
this.storeInvalidShare = function(shareData, callback){
try {
let share = global.protos.InvalidShare.decode(shareData);
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
this.incrementCacheData(minerIDWithIdentifier, [{location: 'badShares', value: 1}]);
this.incrementCacheData(minerID, [{location: 'badShares', value: 1}]);
callback(true);
} catch (e){
console.error("Ran into an error string an invalid share. Damn!");
callback(false);
}
};
this.getLastBlock = function(blockType){
this.refreshEnv();
debug("Getting the last block for: "+ blockType);
let txn = this.env.beginTxn({readOnly: true});
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
let highestBlock = 0;
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.poolType === blockType || typeof(blockType) === 'undefined'){
if (found > highestBlock){
highestBlock = found;
}
}
});
}
cursor.close();
txn.commit();
debug("Done getting the last block for: "+ blockType + " height of: "+ highestBlock);
return highestBlock;
};
this.calculateShares = function(blockData, blockHeight){
debug("Calculating shares for "+ blockData.hash);
this.refreshEnv();
let lastBlock = this.getLastBlock(blockData.poolType);
let shareCount = 0;
range.range(lastBlock+1, blockHeight+1).forEach(function (blockID) {
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockID) === blockID); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
try{
let shareData = global.protos.Share.decode(data);
if (shareData.poolType === blockData.poolType){
shareCount = shareCount + shareData.shares;
}
} catch(e){
console.error("Invalid share");
}
});
}
cursor.close();
txn.commit();
});
blockData.shares = shareCount;
debug("Share calculator for "+ blockData.hash + " complete, found " + shareCount + " shares.");
return global.protos.Block.encode(blockData);
};
this.storeBlock = function(blockId, blockData, callback){
this.refreshEnv();
try{
let blockDataDecoded = global.protos.Block.decode(blockData);
global.coinFuncs.getBlockHeaderByHash(blockDataDecoded.hash, function(header){
if (typeof(header) === 'undefined' || !header){
return callback(false);
}
blockDataDecoded.value = header.reward;
blockData = global.database.calculateShares(blockDataDecoded, blockId);
let txn = global.database.env.beginTxn();
txn.putBinary(global.database.blockDB, blockId, blockData);
txn.commit();
return callback(true);
});
} catch (e) {
console.error("ERROR IN STORING BLOCK. LOOK INTO ME PLZ: " + JSON.stringify(e));
throw new Error("Error in block storage");
}
};
this.fixBlockShares = function(blockId){
let txn = global.database.env.beginTxn();
let blockData = txn.getBinary(global.database.blockDB, blockId);
txn.abort();
let blockDataDecoded = global.protos.Block.decode(blockData);
global.coinFuncs.getBlockHeaderByHash(blockDataDecoded.hash, function(header){
blockDataDecoded.value = header.reward;
blockData = global.database.calculateShares(blockDataDecoded, blockId);
let txn = global.database.env.beginTxn();
txn.putBinary(global.database.blockDB, blockId, blockData);
txn.commit();
});
};
this.invalidateBlock = function(blockId){
this.refreshEnv();
let txn = this.env.beginTxn();
let blockData = global.protos.Block.decode(txn.getBinary(this.blockDB, blockId));
blockData.valid = false;
txn.putBinary(this.blockDB, blockId, global.protos.Block.encode(blockData));
txn.commit();
};
this.getValidLockedBlocks = function(){
this.refreshEnv();
let txn = this.env.beginTxn({readOnly: true});
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
let blockList = [];
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.valid === true && blockData.unlocked === false){
blockData.height = key;
blockList.push(blockData);
}
});
}
cursor.close();
txn.commit();
return blockList;
};
this.unlockBlock = function(blockHex){
this.refreshEnv();
let txn = this.env.beginTxn();
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
let blockDB = this.blockDB;
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.hash === blockHex){
blockData.unlocked = true;
txn.putBinary(blockDB, key, global.protos.Block.encode(blockData));
}
});
blockDB = null;
}
cursor.close();
txn.commit();
};
this.getCache = function(cacheKey){
debug("Getting Key: "+cacheKey);
try {
this.refreshEnv();
let txn = this.env.beginTxn({readOnly: true});
let cached = txn.getString(this.cacheDB, cacheKey);
txn.abort();
if (cached !== null){
debug("Result for Key: " + cacheKey + " is: " + cached);
return JSON.parse(cached);
}
} catch (e) {
return false;
}
return false;
};
this.setCache = function(cacheKey, cacheData){
debug("Setting Key: "+cacheKey+ " Data: " + JSON.stringify(cacheData));
this.refreshEnv();
let txn = this.env.beginTxn();
txn.putString(this.cacheDB, cacheKey, JSON.stringify(cacheData));
txn.commit();
};
this.cleanShareDB = function() {
/*
This function takes the difficulty of the current block, and the last PPS block. If it's 0, save everything,
UNLESS global.config.pps.enable is FALSE, then feel free to trash it.
Due to LMDB under current config, we must delete entire keys, due to this, we save diff * shareMultiLog * 1.5
global.config.pplns.shareMultiLog should be at least 1.5x your shareMulti, in case of diff spikiing
*/
let lastPPSBlock = this.getLastBlock();
if (global.config.pps.enable){
lastPPSBlock = this.getLastBlock(global.protos.POOLTYPE.PPS);
if (lastPPSBlock === 0){
return;
}
}
let lastPPLNSBlock = this.getLastBlock(global.protos.POOLTYPE.PPLNS);
debug("Last PPS block: "+lastPPSBlock);
// Hopping into async, we need the current block height to know where to start our indexing...
async.waterfall([
function(callback){
global.coinFuncs.getLastBlockHeader(function(body){
callback(null, body.height, Math.floor(body.difficulty * 1.5 * global.config.pplns.shareMultiLog));
});
},
function (lastBlock, difficulty, callback) {
let shareCount = 0;
let ppsFound = false;
let pplnsFound = false;
let blockList = [];
debug("Scanning from: "+lastBlock + " for more than: " + difficulty + " shares");
range.range(0, lastBlock+1).forEach(function (blockID) {
blockID = (blockID - lastBlock+1) * -1;
if (blockID < 0){
return;
}
debug("Scanning block: " + blockID);
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockID) === blockID); found; found = cursor.goToNextDup()) {
if (ppsFound && pplnsFound){
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
if (blockList.indexOf(key) === -1){
blockList.push(key);
}
});
} else {
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
if (key < lastPPSBlock){
ppsFound = true;
}
try{
let shareData = global.protos.Share.decode(data);
if (shareData.poolType === global.protos.POOLTYPE.PPLNS){
shareCount = shareCount + shareData.shares;
}
} catch(e){
console.error("Invalid share");
}
});
if (shareCount >= difficulty){
pplnsFound = true;
}
}
}
cursor.close();
txn.abort();
});
callback(null, blockList);
}
], function(err, data){
if (global.config.general.blockCleaner === true){
if(data.length > 0){
global.database.refreshEnv();
let blockList = global.database.getBlockList();
debug("Got the block list");
let totalDeleted = 0;
data.forEach(function(block){
if ((blockList.indexOf(block) !== -1 && !blockList.unlocked) || block > lastPPLNSBlock){
// Don't delete locked blocks. ffs.
// Don't delete blocks that could contain shares. Even if it's unlikely as all getout.
debug("Skipped deleting block: " + block);
return;
}
totalDeleted += 1;
let txn = global.database.env.beginTxn();
txn.del(global.database.shareDB, block);
txn.commit();
debug("Deleted block: " + block);
});
console.log("Block cleaning enabled. Removed: " +totalDeleted+ " block share records");
}
global.database.env.sync(function(){
});
} else {
console.log("Block cleaning disabled. Would of removed: " + JSON.stringify(data));
}
});
};
this.refreshEnv = function(){
if (this.dirtyenv === true){
console.log("Database Worker: Reloading LMDB Env");
global.database.env.sync(function(){
});
clearInterval(this.intervalID);
global.database.env.close();
this.initEnv();
}
};
setInterval(function(){
global.database.dirtyenv = true;
}, 900000); // Set DB env reload for every 15 minutes.
}
module.exports = Database;
+10
View File
@@ -0,0 +1,10 @@
"use strict";
console.log("Cleaning up the share DB");
global.database.cleanShareDB();
console.log("Done cleaning up the shareDB");
setInterval(function(){
console.log("Cleaning up the share DB");
global.database.cleanShareDB();
console.log("Done cleaning up the shareDB");
}, 3600000);
+668
View File
@@ -0,0 +1,668 @@
"use strict";
const shapeshift = require('shapeshift.io');
const async = require("async");
const debug = require("debug")("payments");
const request = require('request-json');
const range = require('range');
let hexChars = new RegExp("[0-9a-f]+");
let bestExchange = global.config.payout.bestExchange;
let xmrAPIClient = request.createClient('https://xmr.to/api/v1/xmr2btc/');
let shapeshiftQueue = async.queue(function (task, callback) {
// Amount needs to be shifted in as a non-completed value, as the wallet will only take non-complete values..
let amount = task.amount -= task.fee;
// Address is the destination address IN BTC.
let address = task.address;
// PaymentIDs are the paymentID's to flag as paid by this transaction.
// Should be a massive list of ID's so we can bulk-update them, by merging them with 's.
// Here we go! General process: Scan shapeshift for valid amounts of funds to xfer around.
// Once there's enough funds, then we active txn
// Do a wallet call to xfer.
// Setup a monitor on the transaction
async.waterfall([
function (intCallback) {
// Verify if the coin is active in ShapeShift first.
shapeshift.coins(function (err, coinData) {
if (err) {
intCallback(err);
} else if (!coinData.hasOwnProperty(global.config.general.coinCode) || coinData[global.config.general.coinCode].status !== "available") {
intCallback("Coin " + global.config.general.coinCode + " Is not available at this time on shapeshift.");
} else {
intCallback(null);
}
});
},
function (intCallback) {
// Get the market information from shapeshift, which includes deposit limits, minimum deposits, rates, etc.
shapeshift.marketInfo(global.config.payout.shapeshiftPair, function (err, marketInfo) {
if (err) {
intCallback(err);
} else if (!marketInfo.hasOwnProperty("limit") || marketInfo.limit <= global.support.coinToDecimal(amount)) {
intCallback("Not enough coin in shapeshift to process at this time.");
} else if (!marketInfo.hasOwnProperty("min") || marketInfo.min >= global.support.coinToDecimal(amount)) {
intCallback("Not enough coin to hit the shapeshift minimum deposits.");
} else {
intCallback(null, marketInfo);
}
});
},
function (marketInfo, intCallback) {
// Validated there's enough coin. Time to make our dank txn.
// Return:
/*
{
"orderId": "cc49c556-e645-4c15-a943-d50a935274e4",
"sAddress": "46yzCCD3Mza9tRj7aqPSaxVbbePtuAeKzf8Ky2eRtcXGcEgCg1iTBio6N4sPmznfgGEUGDoBz5CLxZ2XPTyZu1yoCAG7zt6",
"deposit": "d8041668718e6e9d9d0fd335ee5ecd923e6fd074c41316d041cc18b779ade10e",
"depositType": "XMR",
"withdrawal": "1DbxcoCBSA9N7uZvkcvWxuLxSau9q9Pwiu",
"withdrawalType": "BTC",
"public": null,
"apiPubKey": "shapeshift",
"returnAddress": "46XWBqE1iwsVxSDP1qDrxhE1XvsZV6eALG5LwnoMdjbT4GPdy2bZTb99kagzxp2MMjUamTYZ4WgvZdFadvMimTjvR6Gv8hL",
"returnAddressType": "XMR"
}
Valid Statuses:
"received"
"complete"
"error"
"no_deposits"
Complete State Information:
{
"status": "complete",
"address": "d8041668718e6e9d9d0fd335ee5ecd923e6fd074c41316d041cc18b779ade10e",
"withdraw": "1DbxcoCBSA9N7uZvkcvWxuLxSau9q9Pwiu",
"incomingCoin": 3,
"incomingType": "XMR",
"outgoingCoin": "0.04186155",
"outgoingType": "BTC",
"transaction": "be9d97f6fc75262151f8f63e035c6ed638b9eb2a4e93fef43ea63124b045dbfb"
}
*/
shapeshift.shift(address, global.config.payout.shapeshiftPair, {returnAddress: global.config.pool.address}, function (err, returnData) {
if (err) {
intCallback(err);
} else {
global.mysql.query("INSERT INTO shapeshiftTxn (id, address, paymentID, depositType, withdrawl, withdrawlType, returnAddress, returnAddressType, txnStatus) VALUES (?,?,?,?,?,?,?,?,?)",
[returnData.orderId, returnData.sAddress, returnData.deposit, returnData.depositType, returnData.withdrawl, returnData.withdrawlType, returnData.returnAddress, returnData.returnAddressType, 'no_deposits']).then(function () {
intCallback(null, marketInfo, returnData);
}).catch(function (error) {
intCallback(error);
});
}
});
},
function (marketInfo, shapeshiftTxnData, intCallback) {
// Make the payment to ShapeShift
let paymentDetails = {
destinations: [
{
amount: amount,
address: shapeshiftTxnData.sAddress
}
],
mixin: global.config.payout.mixIn,
payment_id: shapeshiftTxnData.deposit
};
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
intCallback(null, marketInfo, shapeshiftTxnData, body);
} else {
intCallback("Unknown error from the wallet.");
}
});
},
function (marketInfo, shapeshiftTxnData, body, intCallback) {
// body.tx_hash = XMR transaction hash.
// Need to add transaction.
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees, exchange_rate, exchange_name, exchange_txn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[1, address, null, task.amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, global.support.decimalToCoin(marketInfo.minerFee), 1, global.support.decimalToCoin(marketInfo.rate), 'shapeshift', shapeshiftTxnData.orderId]).then(function (result) {
intCallback(null, result.insertId);
}).catch(function (error) {
intCallback(error);
});
}
], function (err, result) {
if (err) {
console.error("Error processing shapeshift txn: " + JSON.stringify(err));
callback();
} else {
// Need to fill out this data pronto!
console.log("Processed ShapeShift transaction for: " + address + " Paid out: " + result + " payments in the db");
callback(null, result);
}
});
}, 2);
let xmrToQueue = async.queue(function (task, callback) {
// http://xmrto-api.readthedocs.io/en/latest/introduction.html
// Documentation looks good!
// Amount needs to be shifted in as a non-completed value, as the wallet will only take non-complete values..
let amount = task.amount -= task.fee;
// Address is the destination address IN BTC.
let address = task.address;
// PaymentIDs are the paymentID's to flag as paid by this transaction.
// Should be a massive list of ID's so we can bulk-update them, by merging them with 's.
// Here we go! General process: Scan shapeshift for valid amounts of funds to xfer around.
// Once there's enough funds, then we active txn
// Do a wallet call to xfer.
// Setup a monitor on the transaction
async.waterfall([
function (intCallback) {
// Verify if XMR.to is ready to get to work.
xmrAPIClient.get('order_parameter_query/', function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
let amtOfBTC = ((amount / global.config.general.sigDivisor) * body.price).toPrecision(8);
console.log("Attempting to pay: " + address + " Amount: " + amtOfBTC + " BTC or " + amount / global.config.general.sigDivisor + " XMR");
console.log("Response from XMR.to: " + JSON.stringify(body));
if (body.lower_limit >= amtOfBTC) {
return intCallback("Not enough XMR to hit the minimum deposit");
} else if (body.upper_limit <= amtOfBTC) {
return intCallback("Too much XMR to pay out to xmr.to");
} else {
return intCallback(null, amtOfBTC);
}
}
});
},
function (btcValue, intCallback) {
// Validated there's enough coin. Time to make our dank txn.
// Return:
/*
{
"state": "TO_BE_CREATED",
"btc_amount": <requested_amount_in_btc_as_float>,
"btc_dest_address": "<requested_destination_address_as_string>",
"uuid": "<unique_order_identifier_as_12_character_string>"
}
Valid Statuses:
"TO_BE_CREATED"
"UNPAID"
"UNDERPAID"
"PAID_UNCONFIRMED"
"PAID"
"BTC_SENT"
"TIMED_OUT"
"NOT_FOUND"
// Create, then immediately update with the new information w/ a status call.
*/
console.log("Amount of BTC to pay: " + btcValue);
xmrAPIClient.post('order_create/', {
btc_amount: btcValue,
btc_dest_address: address
}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
return intCallback(null, body.uuid);
}
});
},
function (txnID, intCallback) {
// This function only exists because xmr.to is a pretty little fucking princess.
async.doUntil(function (xmrCallback) {
xmrAPIClient.post('order_status_query/', {uuid: txnID}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
xmrCallback(null, body.state);
}
});
},
function (xmrCallback) {
return xmrCallback !== "TO_BE_CREATED";
},
function () {
intCallback(null, txnID);
});
},
function (txnID, intCallback) {
xmrAPIClient.post('order_status_query/', {uuid: txnID}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
console.log(JSON.stringify(body));
global.mysql.query("INSERT INTO xmrtoTxn (id, address, paymentID, depositType, withdrawl, withdrawlType, returnAddress, returnAddressType, txnStatus, amountDeposited, amountSent) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
[txnID, body.xmr_receiving_address, body.xmr_required_payment_id, 'XMR', body.btc_dest_address, 'BTC', global.config.pool.address, 'XMR', body.state_str, global.support.decimalToCoin(body.xmr_amount_total), global.support.decimalToCoin(body.btc_amount)]).then(function () {
return intCallback(null, body, global.support.decimalToCoin(body.xmr_amount_total));
}).catch(function (error) {
return intCallback(error);
});
}
});
},
function (orderStatus, xmrDeposit, intCallback) {
// Make the payment to ShapeShift
let paymentDetails = {
destinations: [
{
amount: xmrDeposit,
address: orderStatus.xmr_receiving_address
}
],
mixin: global.config.payout.mixIn,
payment_id: orderStatus.xmr_required_payment_id
};
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
return intCallback(null, orderStatus, body);
} else {
return intCallback("Unknown error from the wallet.");
}
});
},
function (orderStatus, body, intCallback) {
// body.tx_hash = XMR transaction hash.
// Need to add transaction.
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees, exchange_rate, exchange_name, exchange_txn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[1, address, null, global.support.decimalToCoin(orderStatus.xmr_amount_total), body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1, global.support.decimalToCoin(orderStatus.xmr_price_btc), 'xmrto', orderStatus.uuid]).then(function (result) {
return intCallback(null, result.insertId);
}).catch(function (error) {
return intCallback(error);
});
}
], function (err, result) {
if (err) {
console.error("Error processing XMRTo txn: " + JSON.stringify(err));
return callback("Error!");
} else {
// Need to fill out this data pronto!
console.log("Processed XMRTo transaction for: " + address + " Paid out: " + result + " payments in the db");
return callback(null, result);
}
});
}, 2);
let paymentQueue = async.queue(function (paymentDetails, callback) {
/*
support JSON URI: http://10.0.0.2:28082/json_rpc Args: {"id":"0","jsonrpc":"2.0","method":"transfer","params":{"destinations":[{"amount":68130252045355,"address":"A2MSrn49ziBPJBh8ZNEhhbfyLMou6mao4C1F5TLGUatmUnCxZArDYkcbAnVkVEopWVeak2rKDrmc8JpoS7n5dvfN9YDPBTG"}],"mixin":4,"payment_id":"7e52c5266de9fede7fb3abc0cd88f937b38b51426f7b34ff99729d28ce4e1142"}} +1ms
payments Payment made: {"id":"0","jsonrpc":"2.0","result":{"fee":40199391255,"tx_hash":"c418708643f72635edf522490bfb2cae9d42a6dc1df30dcde844862dfd88f5b3","tx_key":""}} +2s
*/
debug("Making payment based on: " + JSON.stringify(paymentDetails));
let transferFunc = 'transfer';
global.support.rpcWallet(transferFunc, paymentDetails, function (body) {
debug("Payment made: " + JSON.stringify(body));
if (body.hasOwnProperty('error')) {
console.error("Issue making payments" + JSON.stringify(body.error));
console.error("Will not make more payments until the payment daemon is restarted!");
//toAddress, subject, body
global.support.sendEmail(global.config.general.adminEmail, "Payment daemon unable to make payment",
"Hello,\r\nThe payment daemon has hit an issue making a payment: " + JSON.stringify(body.error) +
". Please investigate and restart the payment daemon as appropriate");
return;
}
if (paymentDetails.hasOwnProperty('payment_id')) {
console.log("Payment made to " + paymentDetails.destinations[0].address + " with PaymentID: " + paymentDetails.payment_id + " For: " + global.support.coinToDecimal(paymentDetails.destinations[0].amount) + " XMR with a " + global.support.coinToDecimal(body.result.fee) + " XMR Mining Fee");
return callback(body.result);
} else {
if (transferFunc === 'transfer') {
console.log("Payment made out to multiple people, total fee: " + global.support.coinToDecimal(body.result.fee) + " XMR");
}
let intCount = 0;
paymentDetails.destinations.forEach(function (details) {
console.log("Payment made to: " + details.address + " For: " + global.support.coinToDecimal(details.amount) + " XMR");
intCount += 1;
if (intCount === paymentDetails.destinations.length) {
return callback(body.result);
}
});
}
});
}, 1);
function updateShapeshiftCompletion() {
global.mysql.query("SELECT * FROM shapeshiftTxn WHERE txnStatus NOT IN ('complete', 'error')").then(function (rows) {
rows.forEach(function (row) {
shapeshift.status(row.paymentID, function (err, status, returnData) {
if (err) {
return;
}
global.mysql.query("UPDATE shapeshiftTxn SET txnStatus = ? WHERE id = ?", [status, row.id]).then(function () {
if (status === 'complete') {
global.mysql.query("UPDATE shapeshiftTxn SET amountDeposited = ?, amountSent = ?, transactionHash = ? WHERE id = ?",
[global.support.decimalToCoin(returnData.incomingCoin), global.support.bitcoinDecimalToCoin(returnData.outgoingCoin), returnData.transaction, row.id]).then(function () {
global.mysql.query("UPDATE transactions SET confirmed = 1, confirmed_time = now(), btc_amt = ? WHERE exchange_txn_id = ?", [global.support.bitcoinDecimalToCoin(returnData.outgoingCoin), row.id]);
});
} else if (status === 'error') {
// Failed txn. Need to rollback and delete all related data. Here we go!
global.mysql.query("DELETE FROM shapeshiftTxn WHERE id = ?", [row.id]);
global.mysql.query("SELECT id, xmr_amt, address FROM transactions WHERE exchange_txn_id = ?", [row.id]).then(function (rows) {
global.mysql.query("DELETE FROM transactions WHERE id = ?", [rows[0].id]);
global.mysql.query("DELETE payments WHERE transaction_id = ?", [rows[0].id]);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE payment_address = ? limit 1", [rows[0].xmr_amt, rows[0].address]);
});
console.error("Failed transaction from ShapeShift " + JSON.stringify(returnData));
}
});
});
});
});
}
function updateXMRToCompletion() {
global.mysql.query("SELECT * FROM xmrtoTxn WHERE txnStatus NOT IN ('PAID', 'TIMED_OUT', 'NOT_FOUND', 'BTC_SENT')").then(function (rows) {
rows.forEach(function (row) {
xmrAPIClient.post('order_status_query/', {uuid: row.id}, function (err, res, body) {
if (err) {
console.log("Error in getting order status: " + JSON.stringify(err));
return;
}
if (body.error_msg) {
console.log("Error in getting order status: " + body.error_msg);
return;
}
global.mysql.query("UPDATE xmrtoTxn SET txnStatus = ? WHERE id = ?", [body.state, row.id]).then(function () {
if (body.status === 'BTC_SENT') {
global.mysql.query("UPDATE xmrtoTxn SET transactionHash = ? WHERE id = ?", [body.btc_transaction_id, row.id]).then(function () {
global.mysql.query("UPDATE transactions SET confirmed = 1, confirmed_time = now(), btc_amt = ? WHERE exchange_txn_id = ?", [global.support.bitcoinDecimalToCoin(body.btc_amount), row.id]);
});
} else if (body.status === 'TIMED_OUT' || body.status === 'NOT_FOUND') {
global.mysql.query("DELETE FROM xmrtoTxn WHERE id = ?", [row.id]);
global.mysql.query("SELECT id, xmr_amt, address FROM transactions WHERE exchange_txn_id = ?", [row.id]).then(function (rows) {
global.mysql.query("DELETE FROM transactions WHERE id = ?", [rows[0].id]);
global.mysql.query("DELETE payments WHERE transaction_id = ?", [rows[0].id]);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE payment_address = ? limit 1", [rows[0].xmr_amt, rows[0].address]);
});
console.error("Failed transaction from XMRto " + JSON.stringify(body));
}
});
});
});
});
}
function determineBestExchange() {
async.waterfall([
function (callback) {
// Verify if the coin is active in ShapeShift first.
shapeshift.coins(function (err, coinData) {
if (err) {
return callback(err);
} else if (!coinData.hasOwnProperty(global.config.general.coinCode) || coinData[global.config.general.coinCode].status !== "available") {
return callback("Coin " + global.config.general.coinCode + " Is not available at this time on shapeshift.");
} else {
return callback(null);
}
});
},
function (callback) {
// Get the market information from shapeshift, which includes deposit limits, minimum deposits, rates, etc.
shapeshift.marketInfo(global.config.payout.shapeshiftPair, function (err, marketInfo) {
if (err) {
return callback(err);
} else if (!marketInfo.hasOwnProperty("rate")) {
return callback("Shapeshift did not return the rate.");
} else {
return callback(null, global.support.bitcoinDecimalToCoin(marketInfo.rate));
}
});
},
function (ssValue, callback) {
xmrAPIClient.get('order_parameter_query/', function (err, res, body) {
console.log("XMR.to pricing body: " + JSON.stringify(body));
if (err) {
return callback(err);
} else if (body.error_msg) {
return callback(body.error_msg);
} else {
return callback(null, ssValue, global.support.bitcoinDecimalToCoin(body.price));
}
});
}
], function (err, ssValue, xmrToValue) {
if (err) {
return console.error("Error processing exchange value: " + JSON.stringify(err));
}
debug("ShapeShift Value: " + global.support.bitcoinCoinToDecimal(ssValue) + " XMR.to Value: " + global.support.bitcoinCoinToDecimal(xmrToValue));
if (ssValue >= xmrToValue) {
console.log("ShapeShift is the better BTC exchange, current rate: " + global.support.bitcoinCoinToDecimal(ssValue));
bestExchange = 'shapeshift';
global.mysql.query("UPDATE config SET item_value = 'shapeshift' where item='bestExchange'");
global.mysql.query("UPDATE config SET item_value = ? where item='exchangeRate'", [ssValue]);
} else {
console.log("XMR.to is the better BTC exchange, current rate: " + global.support.bitcoinCoinToDecimal(xmrToValue));
bestExchange = 'xmrto';
global.mysql.query("UPDATE config SET item_value = 'xmrto' where item='bestExchange'");
global.mysql.query("UPDATE config SET item_value = ? where item='exchangeRate'", [xmrToValue]);
}
});
}
function Payee(amount, address, paymentID, bitcoin) {
this.amount = amount;
this.address = address;
this.paymentID = paymentID;
this.bitcoin = bitcoin;
this.blockID = 0;
this.poolType = '';
this.transactionID = 0;
this.sqlID = 0;
if (paymentID === null) {
this.id = address;
} else {
this.id = address + "." + paymentID;
}
this.fee = 0;
this.baseFee = global.support.decimalToCoin(global.config.payout.feeSlewAmount);
this.setFeeAmount = function () {
if (this.amount <= global.support.decimalToCoin(global.config.payout.walletMin)) {
this.fee = this.baseFee;
} else if (this.amount <= global.support.decimalToCoin(global.config.payout.feeSlewEnd)) {
let feeValue = this.baseFee / (global.support.decimalToCoin(global.config.payout.feeSlewEnd) - global.support.decimalToCoin(global.config.payout.walletMin));
this.fee = this.baseFee - ((this.amount - global.support.decimalToCoin(global.config.payout.walletMin)) * feeValue);
}
this.fee = Math.floor(this.fee);
};
this.makePaymentWithID = function () {
let paymentDetails = {
destinations: [
{
amount: this.amount - this.fee,
address: this.address
}
],
mixin: global.config.payout.mixIn,
payment_id: this.paymentID
};
let identifier = this.id;
let amount = this.amount;
let address = this.address;
let paymentID = this.paymentID;
let payee = this;
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
debug("Successful payment sent to: " + identifier);
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[0, address, paymentID, amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1]).then(function (result) {
payee.transactionID = result.insertId;
payee.trackPayment();
});
} else {
console.error("Unknown error from the wallet.");
}
});
};
this.makePaymentAsIntegrated = function () {
let paymentDetails = {
destinations: [
{
amount: this.amount - this.fee,
address: this.address
}
],
mixin: global.config.payout.mixIn
};
let identifier = this.id;
let amount = this.amount;
let address = this.address;
let payee = this;
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
debug("Successful payment sent to: " + identifier);
global.mysql.query("INSERT INTO transactions (bitcoin, address, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?)",
[0, address, amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1]).then(function (result) {
payee.transactionID = result.insertId;
payee.trackPayment();
});
} else {
console.error("Unknown error from the wallet.");
}
});
};
this.makeBitcoinPayment = function () {
let functionalData = {address: this.address, amount: this.amount};
let payee = this;
if (bestExchange === 'xmrto') {
xmrToQueue.push(functionalData, function (err, transactionID) {
if (err) {
return console.error("Error processing payment for " + functionalData.address);
}
payee.transactionID = transactionID;
payee.trackPayment();
});
} else {
shapeshiftQueue.push(functionalData, function (err, transactionID) {
if (err) {
return console.error("Error processing payment for " + functionalData.address);
}
payee.transactionID = transactionID;
payee.trackPayment();
});
}
};
this.trackPayment = function () {
global.mysql.query("UPDATE balance SET amount = amount - ? WHERE id = ?", [this.amount, this.sqlID]);
global.mysql.query("INSERT INTO payments (unlocked_time, paid_time, pool_type, payment_address, transaction_id, bitcoin, amount, payment_id, transfer_fee)" +
" VALUES (now(), now(), ?, ?, ?, ?, ?, ?, ?)", [this.poolType, this.address, this.transactionID, this.bitcoin, this.amount - this.fee, this.paymentID, this.fee]);
};
}
function makePayments() {
global.mysql.query("SELECT * FROM balance WHERE amount >= ?", [global.support.decimalToCoin(global.config.payout.walletMin)]).then(function (rows) {
console.log("Loaded all payees into the system for processing");
let paymentDestinations = [];
let totalAmount = 0;
let roundCount = 0;
let payeeList = [];
let payeeObjects = {};
rows.forEach(function (row) {
debug("Starting round for: " + JSON.stringify(row));
let payee = new Payee(row.amount, row.payment_address, row.payment_id, row.bitcoin);
payeeObjects[row.payment_address] = payee;
global.mysql.query("SELECT payout_threshold FROM users WHERE username = ?", [payee.id]).then(function (userRow) {
roundCount += 1;
let threshold = 0;
if (userRow.length !== 0) {
threshold = userRow[0].payout_threshold;
}
payee.poolType = row.pool_type;
payee.sqlID = row.id;
if (payee.poolType === "fees" && payee.address === global.config.payout.feeAddress && payee.amount >= ((global.support.decimalToCoin(global.config.payout.feesForTXN) + global.support.decimalToCoin(global.config.payout.exchangeMin)))) {
debug("This is the fee address internal check for value");
payee.amount -= global.support.decimalToCoin(global.config.payout.feesForTXN);
} else if (payee.address === global.config.payout.feeAddress && payee.poolType === "fees") {
debug("Unable to pay fee address.");
payee.amount = 0;
}
let remainder = payee.amount % (global.config.payout.denom * global.config.general.sigDivisor);
if (remainder !== 0) {
payee.amount -= remainder;
}
if (payee.amount > threshold) {
payee.setFeeAmount();
if (payee.bitcoin === 0 && payee.paymentID === null && payee.amount !== 0 && payee.amount > 0 && payee.address.length !== 106) {
debug("Adding " + payee.id + " to the list of people to pay (OG Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
paymentDestinations.push({amount: payee.amount - payee.fee, address: payee.address});
totalAmount += payee.amount;
payeeList.push(payee);
} else if (payee.bitcoin === 0 && payee.paymentID === null && payee.amount !== 0 && payee.amount > 0 && payee.address.length === 106 && (payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0))) {
// Special code to handle integrated payment addresses. What a pain in the rear.
// These are exchange addresses though, so they need to hit the exchange payout amount.
debug("Adding " + payee.id + " to the list of people to pay (Integrated Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makePaymentAsIntegrated();
} else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0)) && payee.bitcoin === 0) {
debug("Adding " + payee.id + " to the list of people to pay (Payment ID Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makePaymentWithID();
} else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0)) && payee.bitcoin === 1) {
debug("Adding " + payee.id + " to the list of people to pay (Bitcoin Payout). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makeBitcoinPayment();
}
}
debug("Went: " + roundCount + " With: " + paymentDestinations.length + " Possible destinations and: " + rows.length + " Rows");
if (roundCount === rows.length && paymentDestinations.length > 0) {
while (paymentDestinations.length > 0) {
let paymentDetails = {
destinations: paymentDestinations.splice(0, global.config.payout.maxPaymentTxns),
mixin: global.config.payout.mixIn
};
console.log("Paying out: " + paymentDetails.destinations.length + " people");
paymentQueue.push(paymentDetails, function (body) {
// This is the only section that could potentially contain multiple txns. Lets do this safely eh?
if (body.fee && body.fee > 10) {
debug("Made it to the SQL insert for transactions");
let totalAmount = 0;
paymentDetails.destinations.forEach(function (payeeItem) {
totalAmount += payeeObjects[payeeItem.address].amount;
totalAmount += payeeObjects[payeeItem.address].fee;
});
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[0, null, null, totalAmount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, paymentDetails.destinations.length]).then(function (result) {
paymentDetails.destinations.forEach(function (payeeItem) {
payee = payeeObjects[payeeItem.address];
payee.transactionID = result.insertId;
payee.trackPayment();
});
});
} else {
console.error("Unknown error from the wallet.");
}
});
}
}
});
});
});
}
function init() {
determineBestExchange();
global.support.rpcWallet("store", [], function () {
});
if (global.config.allowBitcoin) {
setInterval(updateXMRToCompletion, 90000);
setInterval(updateShapeshiftCompletion, 90000);
}
setInterval(function () {
global.support.rpcWallet("sweep_dust", [], function () {
});
}, 86400000 * 3);
setInterval(function () {
global.support.rpcWallet("store", [], function () {
});
}, 60000);
setInterval(determineBestExchange, 60000);
setInterval(makePayments, 7200000);
makePayments();
}
init();
+834
View File
@@ -0,0 +1,834 @@
"use strict";
const debug = require('debug')('pool');
const uuidV4 = require('uuid/v4');
const crypto = require('crypto');
const bignum = require('bignum');
const cluster = require('cluster');
const btcValidator = require('wallet-address-validator');
const async = require('async');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
let nonceCheck = new RegExp("^[0-9a-f]{8}$");
let bannedIPs = [];
let bannedAddresses = [];
let baseDiff = global.coinFuncs.baseDiff();
let pastBlockTemplates = global.support.circularBuffer(4);
let activeMiners = [];
let activeBlockTemplate;
let workerList = [];
let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 18\n\nMining Pool Online';
let threadName;
let minerCount = [];
let BlockTemplate = global.coinFuncs.BlockTemplate;
Buffer.prototype.toByteArray = function () {
return Array.prototype.slice.call(this, 0);
};
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
function registerPool() {
global.mysql.query("SELECT * FROM pools WHERE id = ?", [global.config.pool_id]).then(function (rows) {
rows.forEach(function (row) {
if (row.ip !== global.config.bind_ip) {
console.error("Pool ID in use already for a different IP. Update MySQL or change pool ID.");
process.exit(1);
}
});
}).then(function () {
global.mysql.query("INSERT INTO pools (id, ip, last_checkin, active, hostname) VALUES (?, ?, now(), ?, ?) ON DUPLICATE KEY UPDATE last_checkin=now(), active=?",
[global.config.pool_id, global.config.bind_ip, true, global.config.hostname, true]);
global.mysql.query("DELETE FROM ports WHERE pool_id = ?", [global.config.pool_id]).then(function () {
global.config.ports.forEach(function (port) {
if ('ssl' in port && port['ssl'] === true) {
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 1)",
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
} else {
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 0)",
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
}
});
});
});
}
// Master/Slave communication Handling
function messageHandler(message) {
switch (message.type) {
case 'banIP':
debug(threadName + "Received ban IP update from nodes");
if (cluster.isMaster) {
sendToWorkers(message);
} else {
bannedIPs.push(message.data);
}
break;
case 'newBlockTemplate':
debug(threadName + "Received new block template");
if (cluster.isMaster) {
sendToWorkers(message);
newBlockTemplate(message.data);
} else {
newBlockTemplate(message.data);
}
break;
case 'removeMiner':
if (cluster.isMaster) {
minerCount[message.data] -= 1;
}
break;
case 'newMiner':
if (cluster.isMaster) {
minerCount[message.data] += 1;
}
break;
}
}
process.on('message', messageHandler);
function sendToWorkers(data) {
let goodWorkers = [];
workerList.forEach(function (worker) {
try {
if (worker.send(data)) {
goodWorkers.push(worker);
}
} catch (e) {
console.log(threadName + "Worker dead. Not sending new messages.");
}
});
workerList = goodWorkers;
}
function retargetMiners() {
debug(threadName + "Performing difficulty check on miners");
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
if (!miner.fixed_diff) {
miner.updateDifficulty();
}
}
}
}
function checkAliveMiners() {
debug(threadName + "Verifying if miners are still alive");
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
if (Date.now() - miner.lastContact > global.config.pool.minerTimeout * 1000) {
process.send({type: 'removeMiner', data: miner.port});
delete activeMiners[minerId];
}
}
}
}
function templateUpdate() {
global.coinFuncs.getBlockTemplate(global.config.pool.address, function (rpcResponse) {
if (rpcResponse) {
rpcResponse = rpcResponse.result;
let buffer = new Buffer(rpcResponse.blocktemplate_blob, 'hex');
let new_hash = new Buffer(32);
buffer.copy(new_hash, 0, 7, 39);
if (!activeBlockTemplate || new_hash.toString('hex') !== activeBlockTemplate.previous_hash.toString('hex')) {
debug(threadName + "New block template found at " + rpcResponse.height + " height with hash: " + new_hash.toString('hex'));
if (cluster.isMaster) {
sendToWorkers({type: 'newBlockTemplate', data: rpcResponse});
newBlockTemplate(rpcResponse);
} else {
process.send({type: 'newBlockTemplate', data: rpcResponse});
newBlockTemplate(rpcResponse);
}
}
}
});
}
function newBlockTemplate(template) {
let buffer = new Buffer(template.blocktemplate_blob, 'hex');
let previous_hash = new Buffer(32);
buffer.copy(previous_hash, 0, 7, 39);
console.log(threadName + 'New block to mine at height: ' + template.height + '. Difficulty: ' + template.difficulty);
if (activeBlockTemplate) {
pastBlockTemplates.enq(activeBlockTemplate);
}
activeBlockTemplate = new BlockTemplate(template);
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
debug(threadName + "Updating worker " + miner.payout + " With new work at height: " + template.height);
miner.messageSender('job', miner.getJob());
}
}
}
let VarDiff = (function () {
let variance = global.config.pool.varDiffVariance / 100 * global.config.pool.targetTime;
return {
tMin: global.config.pool.targetTime - variance,
tMax: global.config.pool.targetTime + variance
};
})();
function Miner(id, login, pass, ipAddress, startingDiff, messageSender, protoVersion, portType, port) {
// Username Layout - <address in BTC or XMR>.<Difficulty>
// Password Layout - <password>.<miner identifier>.<payment ID for XMR>
// Default function is to use the password so they can login. Identifiers can be unique, payment ID is last.
// If there is no miner identifier, then the miner identifier is set to the password
// If the password is x, aka, old-logins, we're not going to allow detailed review of miners.
// Miner Variables
let pass_split = pass.split(":");
this.error = "";
this.identifier = pass_split[0];
this.paymentID = null;
this.valid_miner = true;
this.port = port;
this.portType = portType;
switch (portType) {
case 'pplns':
this.poolTypeEnum = global.protos.POOLTYPE.PPLNS;
break;
case 'pps':
this.poolTypeEnum = global.protos.POOLTYPE.PPS;
break;
case 'solo':
this.poolTypeEnum = global.protos.POOLTYPE.SOLO;
break;
case 'prop':
this.poolTypeEnum = global.protos.POOLTYPE.PROP;
break;
}
let diffSplit = login.split("+");
let addressSplit = diffSplit[0].split('.');
this.address = addressSplit[0];
this.payout = addressSplit[0];
this.fixed_diff = false;
this.difficulty = startingDiff;
this.connectTime = Date.now();
if (diffSplit.length === 2) {
this.fixed_diff = true;
this.difficulty = Number(diffSplit[1]);
if (this.difficulty < global.config.pool.minDifficulty) {
this.difficulty = global.config.pool.minDifficulty;
}
if (this.difficulty > global.config.pool.maxDifficulty) {
this.difficulty = global.config.pool.maxDifficulty;
}
} else if (diffSplit.length > 2) {
this.error = "Too many options in the login field";
this.valid_miner = false;
}
if (typeof(addressSplit[1]) !== 'undefined' && (addressSplit[1].length === 16 || addressSplit[1].length === 64)) {
this.paymentID = addressSplit[1];
this.payout = this.address + "." + this.paymentID;
}
if (pass_split.length === 2) {
/*
Email address is: pass_split[1]
Need to do an initial registration call here. Might as well do it right...
*/
let payoutAddress = this.payout;
global.mysql.query("SELECT id FROM users WHERE username = ? LIMIT 1", [this.payout]).then(function (rows) {
if (rows.length > 0) {
return;
}
if (global.coinFuncs.blockedAddresses.indexOf(payoutAddress) !== -1) {
return;
}
global.mysql.query("INSERT INTO users (username, email) VALUES (?, ?)", [payoutAddress, pass_split[1]]);
});
} else if (pass_split.length > 2) {
this.error = "Too many options in the password field";
this.valid_miner = false;
}
if (global.coinFuncs.validateAddress(this.address)) {
this.bitcoin = 0;
} else if (btcValidator.validate(this.address) && global.config.general.allowBitcoin) {
this.bitcoin = 1;
} else if (btcValidator.validate(this.address)) {
this.error = "This pool does not allow payouts to bitcoin.";
this.valid_miner = false;
} else {
// Invalid Addresses
this.error = "Invalid payment address provided";
this.valid_miner = false;
}
if (bannedAddresses.indexOf(this.address) !== -1) {
// Banned Address
this.error = "Banned payment address provided";
this.valid_miner = false;
}
if (global.coinFuncs.exchangeAddresses.indexOf(this.address) !== -1 && !(this.paymentID)) {
this.error = "Exchange addresses need payment IDs";
this.valid_miner = false;
}
this.id = id;
this.ipAddress = ipAddress;
this.messageSender = messageSender;
this.heartbeat = function () {
this.lastContact = Date.now();
};
this.heartbeat();
// VarDiff System
this.shareTimeBuffer = global.support.circularBuffer(8);
this.shareTimeBuffer.enq(global.config.pool.targetTime);
this.lastShareTime = Date.now() / 1000 || 0;
this.validShares = 0;
this.invalidShares = 0;
this.hashes = 0;
this.logString = this.address + " ID: " + this.identifier + " IP: " + this.ipAddress;
if (global.config.pool.trustedMiners) {
this.trust = {
threshold: global.config.pool.trustThreshold,
probability: 256,
penalty: 0
};
}
this.validJobs = global.support.circularBuffer(10);
this.invalidShareProto = global.protos.InvalidShare.encode({
paymentAddress: this.address,
paymentID: this.paymentID,
identifier: this.identifier
});
// Support functions for how miners activate and run.
this.updateDifficultyOld = function () {
let now = Math.round(Date.now() / 1000);
let avg = this.shareTimeBuffer.average(this.lastShareTime);
let sinceLast = now - this.lastShareTime;
let decreaser = sinceLast > VarDiff.tMax;
let newDiff;
let direction;
if (avg > VarDiff.tMax && this.difficulty > global.config.pool.minDifficulty) {
newDiff = global.config.pool.targetTime / avg * this.difficulty;
direction = -1;
}
else if (avg < VarDiff.tMin && this.difficulty < global.config.pool.maxDifficulty) {
newDiff = global.config.pool.targetTime / avg * this.difficulty;
direction = 1;
}
else {
return;
}
if (Math.abs(newDiff - this.difficulty) / this.difficulty * 100 > global.config.pool.maxDiffChange) {
let change = global.config.pool.maxDiffChange / 100 * this.difficulty * direction;
newDiff = this.difficulty + change;
}
this.setNewDiff(newDiff);
this.shareTimeBuffer.clear();
if (decreaser) {
this.lastShareTime = now;
}
};
this.updateDifficulty = function(){
if (this.hashes > 0) {
this.setNewDiff(Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * global.config.pool.targetTime);
} else {
this.updateDifficultyOld();
}
};
this.setNewDiff = function (difficulty) {
this.newDiff = Math.round(difficulty);
debug(threadName + "Difficulty: " + this.newDiff + " For: " + this.logString + " Time Average: " + this.shareTimeBuffer.average(this.lastShareTime) + " Entries: " + this.shareTimeBuffer.size() + " Sum: " + this.shareTimeBuffer.sum());
if (this.newDiff > global.config.pool.maxDifficulty) {
this.newDiff = global.config.pool.maxDifficulty;
}
if (this.difficulty === this.newDiff) {
return;
}
if (this.newDiff < global.config.pool.minDifficulty) {
this.newDiff = global.config.pool.minDifficulty;
}
console.log(threadName + "Difficulty change to: " + this.newDiff + " For: " + this.logString);
if (this.hashes > 0){
debug(threadName + "Hashes: " + this.hashes + " in: " + Math.floor((Date.now() - this.connectTime)/1000) + " seconds gives: " +
Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) + " hashes/second or: " +
Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) * global.config.pool.targetTime + " difficulty versus: " + this.newDiff);
}
this.messageSender('job', this.getJob());
};
this.checkBan = function (validShare) {
if (!global.config.pool.banEnabled) {
return;
}
// Valid stats are stored by the pool.
if (validShare) {
this.validShares += 1;
} else {
this.invalidShares += 1;
}
if (this.validShares + this.invalidShares >= global.config.pool.banThreshold) {
if (this.invalidShares / this.validShares >= global.config.pool.banPercent / 100) {
delete activeMiners[this.id];
process.send({type: 'banIP', data: this.ipAddress});
}
else {
this.invalidShares = 0;
this.validShares = 0;
}
}
};
if (protoVersion === 1) {
this.getTargetHex = function () {
if (this.newDiff) {
this.difficulty = this.newDiff;
this.newDiff = null;
}
let padded = new Buffer(32);
padded.fill(0);
let diffBuff = baseDiff.div(this.difficulty).toBuffer();
diffBuff.copy(padded, 32 - diffBuff.length);
let buff = padded.slice(0, 4);
let buffArray = buff.toByteArray().reverse();
let buffReversed = new Buffer(buffArray);
this.target = buffReversed.readUInt32BE(0);
return buffReversed.toString('hex');
};
this.getJob = function () {
if (this.lastBlockHeight === activeBlockTemplate.height && !this.newDiff) {
return {
blob: '',
job_id: '',
target: ''
};
}
let blob = activeBlockTemplate.nextBlob();
let target = this.getTargetHex();
this.lastBlockHeight = activeBlockTemplate.height;
let newJob = {
id: uuidV4(),
extraNonce: activeBlockTemplate.extraNonce,
height: activeBlockTemplate.height,
difficulty: this.difficulty,
diffHex: this.diffHex,
submissions: []
};
this.validJobs.enq(newJob);
return {
blob: blob,
job_id: newJob.id,
target: target
};
};
}
}
function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate) {
miner.hashes += job.difficulty;
global.database.storeShare(job.height, global.protos.Share.encode({
shares: job.difficulty,
paymentAddress: miner.address,
paymentID: miner.paymentID,
foundBlock: blockCandidate,
trustedShare: shareType,
poolType: miner.poolTypeEnum,
poolID: global.config.pool_id,
blockDiff: activeBlockTemplate.difficulty,
bitcoin: miner.bitcoin,
blockHeight: job.height,
timestamp: Date.now(),
identifier: miner.identifier
}));
if (blockCandidate) {
global.database.storeBlock(job.height, global.protos.Block.encode({
hash: hashHex,
difficulty: blockTemplate.difficulty,
shares: 0,
timestamp: Date.now(),
poolType: miner.poolTypeEnum,
unlocked: false,
valid: true
}));
}
if (shareType) {
console.log(threadName + "Accepted trusted share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
} else {
console.log(threadName + "Accepted valid share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
}
}
function processShare(miner, job, blockTemplate, nonce, resultHash) {
let template = new Buffer(blockTemplate.buffer.length);
blockTemplate.buffer.copy(template);
template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset);
let shareBuffer = global.coinFuncs.constructNewBlob(template, new Buffer(nonce, 'hex'));
let convertedBlob;
let hash;
let shareType;
if (global.config.pool.trustedMiners && miner.trust.threshold <= 0 && miner.trust.penalty <= 0 &&
crypto.randomBytes(1).readUIntBE(0, 1) > miner.trust.probability) {
hash = new Buffer(resultHash, 'hex');
shareType = true;
}
else {
convertedBlob = global.coinFuncs.convertBlob(shareBuffer);
hash = global.coinFuncs.cryptoNight(convertedBlob);
shareType = false;
}
if (hash.toString('hex') !== resultHash) {
console.error(threadName + "Bad hash from miner " + miner.logString);
miner.newDiff = miner.difficulty + 1;
miner.messageSender('job', miner.getJob());
return false;
}
let hashArray = hash.toByteArray().reverse();
let hashNum = bignum.fromBuffer(new Buffer(hashArray));
let hashDiff = baseDiff.div(hashNum);
if (hashDiff.ge(blockTemplate.difficulty)) {
// Submit block to the RPC Daemon.
global.support.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function (rpcResult) {
if (rpcResult.error) {
// Did not manage to submit a block. Log and continue on.
console.error(threadName + "Error submitting block at height " + job.height + " from " + miner.logString + ", share type: " + shareType + " error: " + JSON.stringify(rpcResult.error));
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
// Error on submit, so we'll submit a sanity check for good measure.
templateUpdate();
} else if (rpcResult) {
//Success! Submitted a block without an issue.
let blockFastHash = global.coinFuncs.getBlockID(shareBuffer).toString('hex');
console.log(threadName + "Block " + blockFastHash.substr(0, 6) + " found at height " + job.height + " by " + miner.logString +
", share type: " + shareType + " - submit result: " + JSON.stringify(rpcResult.result));
recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate);
templateUpdate();
} else {
// RPC bombed out massively.
console.error(threadName + "RPC Error. Please check logs for details");
}
});
}
else if (hashDiff.lt(job.difficulty)) {
console.warn(threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " +
miner.identifier + " IP: " + miner.ipAddress);
return false;
}
else {
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
}
return true;
}
function handleMinerData(method, params, ip, portData, sendReply, pushMessage) {
let miner = activeMiners[params.id];
// Check for ban here, so preconnected attackers can't continue to screw you
if (bannedIPs.indexOf(ip) !== -1) {
// Handle IP ban off clip.
sendReply("IP Address currently banned");
return;
}
switch (method) {
case 'login':
if (!params.login || !params.pass) {
sendReply("No login/password specified");
return;
}
let difficulty = portData.difficulty;
let minerId = uuidV4();
miner = new Miner(minerId, params.login, params.pass, ip, difficulty, pushMessage, 1, portData.portType, portData.port);
if (!miner.valid_miner) {
console.log("Invalid miner, disconnecting due to: " + miner.error);
sendReply(miner.error);
return;
}
process.send({type: 'newMiner', data: miner.port});
activeMiners[minerId] = miner;
sendReply(null, {
id: minerId,
job: miner.getJob(),
status: 'OK'
});
break;
case 'getjob':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
sendReply(null, miner.getJob());
break;
case 'submit':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
let job = miner.validJobs.toarray().filter(function (job) {
return job.id === params.job_id;
})[0];
if (!job) {
sendReply('Invalid job id');
return;
}
params.nonce = params.nonce.substr(0, 8).toLowerCase();
if (!nonceCheck.test(params.nonce)) {
console.warn(threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString);
miner.checkBan(false);
sendReply('Duplicate share');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
if (job.submissions.indexOf(params.nonce) !== -1) {
console.warn(threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString);
miner.checkBan(false);
sendReply('Duplicate share');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
job.submissions.push(params.nonce);
let blockTemplate = activeBlockTemplate.height === job.height ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) {
return t.height === job.height;
})[0];
if (!blockTemplate) {
console.warn(threadName + 'Block expired, Height: ' + job.height + ' from ' + miner.logString);
miner.newDiff = miner.difficulty + 1;
miner.messageSender('job', miner.getJob());
sendReply('Block expired');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
let shareAccepted = processShare(miner, job, blockTemplate, params.nonce, params.result);
miner.checkBan(shareAccepted);
if (global.config.pool.trustedMiners) {
if (shareAccepted) {
miner.trust.probability -= 1;
if (miner.trust.probability < (global.config.pool.trustMin)) {
miner.trust.probability = global.config.pool.trustMin;
}
miner.trust.penalty--;
miner.trust.threshold--;
}
else {
console.log(threadName + "Share trust broken by " + miner.logString);
global.database.storeInvalidShare(miner.invalidShareProto);
miner.trust.probability = 256;
miner.trust.penalty = global.config.pool.trustPenalty;
miner.trust.threshold = global.config.pool.trustThreshold;
}
}
if (!shareAccepted) {
sendReply('Low difficulty share');
return;
}
let now = Date.now() / 1000 || 0;
miner.shareTimeBuffer.enq(now - miner.lastShareTime);
miner.lastShareTime = now;
sendReply(null, {status: 'OK'});
break;
}
}
if (cluster.isMaster) {
let numWorkers = require('os').cpus().length;
global.config.ports.forEach(function (portData) {
minerCount[portData.port] = 0;
});
registerPool();
setInterval(function () {
global.mysql.query("UPDATE pools SET last_checkin = ?, active = ? WHERE id = ?", [global.support.formatDate(Date.now()), true, global.config.pool_id]);
global.mysql.query("UPDATE pools SET blockIDTime = now(), blockID = ? where id = ?", [activeBlockTemplate.height, global.config.pool_id]);
global.config.ports.forEach(function (portData) {
global.mysql.query("UPDATE ports SET lastSeen = now(), miners = ? WHERE pool_id = ? AND network_port = ?", [minerCount[portData.port], global.config.pool_id, portData.port]);
});
}, 10000);
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for (let i = 0; i < numWorkers; i++) {
let worker = cluster.fork();
worker.on('message', messageHandler);
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();
worker.on('message', messageHandler);
workerList.push(worker);
});
setInterval(templateUpdate, 300);
global.support.sendEmail(global.config.general.adminEmail, "Pool server "+global.config.hostname+" online", "The pool server: "+global.config.hostname+" with IP: "+global.config.bind_ip+" is online");
} else {
setInterval(checkAliveMiners, 30000);
setInterval(retargetMiners, global.config.pool.retargetTime * 1000);
templateUpdate();
setInterval(function () {
bannedIPs = [];
}, 60000);
async.each(global.config.ports, function (portData) {
let handleMessage = function (socket, jsonData, pushMessage) {
if (!jsonData.id) {
console.warn('Miner RPC request missing RPC id');
return;
}
else if (!jsonData.method) {
console.warn('Miner RPC request missing RPC method');
return;
}
else if (!jsonData.params) {
console.warn('Miner RPC request missing RPC params');
return;
}
let sendReply = function (error, result) {
if (!socket.writable) {
return;
}
let sendData = JSON.stringify({
id: jsonData.id,
jsonrpc: "2.0",
error: error ? {code: -1, message: error} : null,
result: result
}) + "\n";
socket.write(sendData);
};
handleMinerData(jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage);
};
function socketConn(socket) {
socket.setKeepAlive(true);
socket.setEncoding('utf8');
let dataBuffer = '';
let pushMessage = function (method, params) {
if (!socket.writable) {
return;
}
let sendData = JSON.stringify({
jsonrpc: "2.0",
method: method,
params: params
}) + "\n";
socket.write(sendData);
};
socket.on('data', function (d) {
dataBuffer += d;
if (Buffer.byteLength(dataBuffer, 'utf8') > 10240) { //10KB
dataBuffer = null;
console.warn(threadName + 'Excessive packet size from: ' + socket.remoteAddress);
socket.destroy();
return;
}
if (dataBuffer.indexOf('\n') !== -1) {
let messages = dataBuffer.split('\n');
let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop();
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.trim() === '') {
continue;
}
let jsonData;
try {
jsonData = JSON.parse(message);
}
catch (e) {
if (message.indexOf('GET /') === 0) {
if (message.indexOf('HTTP/1.1') !== -1) {
socket.end('HTTP/1.1' + httpResponse);
break;
}
else if (message.indexOf('HTTP/1.0') !== -1) {
socket.end('HTTP/1.0' + httpResponse);
break;
}
}
console.warn(threadName + "Malformed message from " + socket.remoteAddress + " Message: " + message);
socket.destroy();
break;
}
handleMessage(socket, jsonData, pushMessage);
}
dataBuffer = incomplete;
}
}).on('error', function (err) {
if (err.code !== 'ECONNRESET') {
console.warn(threadName + "Socket Error from " + socket.remoteAddress + " Error: " + err);
}
}).on('close', function () {
pushMessage = function () {
};
});
}
if ('ssl' in portData && portData['ssl'] === true) {
tls.createServer({
key: fs.readFileSync('cert.key'),
cert: fs.readFileSync('cert.pem')
}, socketConn).listen(portData.port, global.config.bind_ip, function (error) {
if (error) {
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
return;
}
console.log(threadName + "Started server on port: " + portData.port);
});
} else {
net.createServer(socketConn).listen(portData.port, global.config.bind_ip, function (error) {
if (error) {
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
return;
}
console.log(threadName + "Started server on port: " + portData.port);
});
}
});
}
+90
View File
@@ -0,0 +1,90 @@
"use strict";
const express = require('express'); // call express
const app = express(); // define our app using express
const cluster = require('cluster');
const debug = require("debug")("remoteShare");
let concat = require('concat-stream');
let threadName = "";
let workerList = [];
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
// Websocket Stuffs.
app.use(function(req, res, next){
req.pipe(concat(function(data){
req.body = data;
next();
}));
});
app.post('/leafApi', function (req, res) {
try {
let msgData = global.protos.WSData.decode(req.body);
if (msgData.key !== global.config.api.authKey) {
return res.status(403).end();
}
switch (msgData.msgType) {
case global.protos.MESSAGETYPE.SHARE:
global.database.storeShare(msgData.exInt, msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
case global.protos.MESSAGETYPE.BLOCK:
global.database.storeBlock(msgData.exInt, msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
case global.protos.MESSAGETYPE.INVALIDSHARE:
global.database.storeInvalidShare(msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
default:
return res.status(400).end();
}
} catch (e) {
console.log("Invalid WS frame");
return res.status(400).end();
}
});
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(8000, function () {
console.log('Process ' + process.pid + ' is listening to all incoming requests');
});
}
+63
View File
@@ -0,0 +1,63 @@
"use strict";
const request = require('request');
const async = require('async');
function Database() {
this.sendQueue = async.queue(function (task, callback) {
async.doUntil(
function (intCallback) {
request.post({url: global.config.general.shareHost, body: task.body}, function (error, response, body) {
if (!error) {
return intCallback(null, response.statusCode);
}
return intCallback(null, 0);
});
},
function (data) {
return data === 200;
},
function () {
callback();
});
}, 32);
this.storeShare = function (blockId, shareData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.SHARE,
key: global.config.api.authKey,
msg: shareData,
exInt: blockId
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.storeBlock = function (blockId, blockData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.BLOCK,
key: global.config.api.authKey,
msg: blockData,
exInt: blockId
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.storeInvalidShare = function (minerData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.INVALIDSHARE,
key: global.config.api.authKey,
msg: minerData,
exInt: 1
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.initEnv = function(){
this.data = null;
};
}
module.exports = Database;
+185
View File
@@ -0,0 +1,185 @@
"use strict";
const CircularBuffer = require('circular-buffer');
const request = require('request');
const requestJson = require('request-json');
const moment = require('moment');
const debug = require('debug')('support');
const fs = require('fs');
function circularBuffer(size) {
let buffer = CircularBuffer(size);
buffer.sum = function () {
if (this.size() === 0) {
return 1;
}
return this.toarray().reduce(function (a, b) {
return a + b;
});
};
buffer.average = function (lastShareTime) {
if (this.size() === 0) {
return global.config.pool.targetTime * 1.5;
}
let extra_entry = (Date.now() / 1000) - lastShareTime;
return (this.sum() + Math.round(extra_entry)) / (this.size() + 1);
};
buffer.clear = function () {
let i = this.size();
while (i > 0) {
this.deq();
i = this.size();
}
};
return buffer;
}
function sendEmail(toAddress, subject, body){
request.post(global.config.general.mailgunURL + "/messages", {
auth: {
user: 'api',
pass: global.config.general.mailgunKey
},
form: {
from: global.config.general.emailFrom,
to: toAddress,
subject: subject,
text: body
}
}, function(err, response, body){
if (!err && response.statusCode === 200) {
console.log("Email sent successfully! Response: " + body);
} else {
console.error("Did not send e-mail successfully! Response: " + body + " Response: "+JSON.stringify(response));
}
});
}
function jsonRequest(host, port, data, callback, path) {
path = path || 'json_rpc';
let uri;
if (global.config.rpc.https) {
uri = "https://" + host + ":" + port + "/";
} else {
uri = "http://" + host + ":" + port + "/";
}
debug("JSON URI: " + uri + path + " Args: " + JSON.stringify(data));
let client = requestJson.createClient(uri);
client.headers["Content-Type"] = "application/json";
client.headers["Content-Length"] = data.length;
client.headers["Accept"] = "application/json";
if (global.config.payout.rpcPasswordEnabled && host === global.config.wallet.address && port === global.config.wallet.port){
fs.readFile(global.config.payout.rpcPasswordPath, 'utf8', function(err, data){
if (err){
console.error("RPC password enabled, unable to read the file due to: " + JSON.stringify(err));
return;
}
let passData = data.split(":");
client.setBasicAuth(passData[0], passData[1]);
request.post(uri, {
auth:{
user: passData[0],
pass: passData[1],
sendImmediately: false
},
data: JSON.stringify(data)
}, function (err, res, body) {
if (err) {
return callback(err);
}
return callback(body);
});
});
} else {
client.post(path, data, function (err, res, body) {
if (err) {
return callback(err);
}
return callback(body);
});
}
}
function rpc(host, port, method, params, callback) {
let data = {
id: "0",
jsonrpc: "2.0",
method: method,
params: params
};
return jsonRequest(host, port, data, callback);
}
function formatDate(date) {
// Date formatting for MySQL date time fields.
return moment(date).format('YYYY-MM-DD HH:mm:ss');
}
function formatDateFromSQL(date) {
// Date formatting for MySQL date time fields.
let ts = new Date(date);
return Math.floor(ts.getTime() / 1000);
}
function coinToDecimal(amount) {
return amount / global.config.coin.sigDigits;
}
function decimalToCoin(amount) {
return amount * global.config.coin.sigDigits;
}
function bitcoinDecimalToCoin(amount) {
return amount * 100000000;
}
function bitcoinCoinToDecimal(amount) {
return amount / 100000000;
}
function blockCompare(a, b) {
if (a.height < b.height) {
return 1;
}
if (a.height > b.height) {
return -1;
}
return 0;
}
function tsCompare(a, b) {
if (a.ts < b.ts) {
return 1;
}
if (a.ts > b.ts) {
return -1;
}
return 0;
}
module.exports = function () {
return {
rpcDaemon: function (method, params, callback) {
rpc(global.config.daemon.address, global.config.daemon.port, method, params, callback);
},
rpcWallet: function (method, params, callback) {
rpc(global.config.wallet.address, global.config.wallet.port, method, params, callback);
},
jsonRequest: jsonRequest,
circularBuffer: circularBuffer,
formatDate: formatDate,
coinToDecimal: coinToDecimal,
decimalToCoin: decimalToCoin,
bitcoinDecimalToCoin: bitcoinDecimalToCoin,
bitcoinCoinToDecimal: bitcoinCoinToDecimal,
formatDateFromSQL: formatDateFromSQL,
blockCompare: blockCompare,
sendEmail: sendEmail,
tsCompare: tsCompare
};
};
+503
View File
@@ -0,0 +1,503 @@
"use strict";
const debug = require("debug")("worker");
const async = require("async");
let threadName = "Worker Server ";
let cycleCount = 0;
function updateShareStats() {
// This is an omni-worker to deal with all things share-stats related
// Time based averages are worked out on ring buffers.
// Buffer lengths? You guessed it, configured in SQL.
// Stats timeouts are 30 seconds, so everything for buffers should be there.
let currentTime = Math.floor(Date.now() / 1000);
async.waterfall([
function (callback) {
global.coinFuncs.getLastBlockHeader(function (body) {
callback(null, body.height + 1);
});
},
function (height, callback) {
let locTime = Date.now() - 600000;
let identifierTime = Date.now() - 1800000;
let localStats = {pplns: 0, pps: 0, solo: 0, prop: 0, global: 0, miners: {}};
let localMinerCount = {pplns: 0, pps: 0, solo: 0, prop: 0, global: 0};
let localTimes = {
pplns: locTime, pps: locTime, solo: locTime, prop: locTime,
global: locTime, miners: {}
};
let minerList = [];
let identifiers = {};
let loopBreakout = 0;
async.doUntil(function (callback_until) {
let oldestTime = Date.now();
let loopCount = 0;
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(height) === height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, share) { // jshint ignore:line
try {
share = global.protos.Share.decode(share);
} catch (e) {
console.error(share);
return;
}
if (share.timestamp < oldestTime) {
oldestTime = share.timestamp;
}
if (share.timestamp <= identifierTime) {
return;
}
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
if (minerID in identifiers && identifiers[minerID].indexOf(share.identifier) >= 0) {
loopCount += 1;
} else if (minerID in identifiers) {
identifiers[minerID].push(share.identifier);
} else {
identifiers[minerID] = [share.identifier];
}
if (share.timestamp <= locTime) {
return;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
localStats.global += share.shares;
if (localTimes.global <= share.timestamp) {
localTimes.global = share.timestamp;
}
let minerType;
switch (share.poolType) {
case global.protos.POOLTYPE.PPLNS:
minerType = 'pplns';
localStats.pplns += share.shares;
if (localTimes.pplns <= share.timestamp) {
localTimes.pplns = share.timestamp;
}
break;
case global.protos.POOLTYPE.PPS:
localStats.pps += share.shares;
minerType = 'pps';
if (localTimes.pps <= share.timestamp) {
localTimes.pps = share.timestamp;
}
break;
case global.protos.POOLTYPE.SOLO:
localStats.solo += share.shares;
minerType = 'solo';
if (localTimes.solo <= share.timestamp) {
localTimes.solo = share.timestamp;
}
break;
}
if (Object.keys(localStats.miners).indexOf(minerID) >= 0) {
localStats.miners[minerID] += share.shares;
if (localTimes.miners[minerID] < share.timestamp) {
localTimes.miners[minerID] = share.timestamp;
}
} else {
localMinerCount[minerType] += 1;
localMinerCount.global += 1;
localStats.miners[minerID] = share.shares;
localTimes.miners[minerID] = share.timestamp;
minerList.push(minerID);
}
if (Object.keys(localStats.miners).indexOf(minerIDWithIdentifier) >= 0) {
localStats.miners[minerIDWithIdentifier] += share.shares;
if (localTimes.miners[minerIDWithIdentifier] < share.timestamp) {
localTimes.miners[minerIDWithIdentifier] = share.timestamp;
}
} else {
localStats.miners[minerIDWithIdentifier] = share.shares;
localTimes.miners[minerIDWithIdentifier] = share.timestamp;
minerList.push(minerIDWithIdentifier);
}
});
}
cursor.close();
txn.abort();
return callback_until(null, oldestTime);
}, function (oldestTime) {
height -= 1;
loopBreakout += 1;
if (loopBreakout > 60) {
return true;
}
return oldestTime <= identifierTime;
}, function (err) {
// todo: Need to finish parsing the cached data into caches for caching purproses.
let globalMinerList = global.database.getCache('minerList');
if (globalMinerList === false) {
globalMinerList = [];
}
minerList.forEach(function (miner) {
if (globalMinerList.indexOf(miner) === -1) {
globalMinerList.push(miner);
}
let cachedData = global.database.getCache(miner);
if (cachedData !== false) {
cachedData.hash = Math.floor(localStats.miners[miner] / 600);
cachedData.lastHash = localTimes.miners[miner];
if (!cachedData.hasOwnProperty("hashHistory")) {
cachedData.hashHistory = [];
}
if (cycleCount === 0){
cachedData.hashHistory.unshift({ts: Date.now(), hs: cachedData.hash});
if (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
while (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
cachedData.hashHistory.pop();
}
}
}
} else {
cachedData = {
hash: Math.floor(localStats.miners[miner] / 600),
totalHashes: 0,
lastHash: localTimes.miners[miner],
hashHistory: [{ts: currentTime, hs: cachedData.hash}],
goodShares: 0,
badShares: 0
};
}
global.database.setCache(miner, cachedData);
});
// pplns: 0, pps: 0, solo: 0, prop: 0, global: 0
['pplns', 'pps', 'solo', 'prop', 'global'].forEach(function (key) {
let cachedData = global.database.getCache(key + "_stats");
if (cachedData !== false) {
cachedData.hash = Math.floor(localStats[key] / 600);
cachedData.lastHash = localTimes[key];
cachedData.minerCount = localMinerCount[key];
if (!cachedData.hasOwnProperty("hashHistory")) {
cachedData.hashHistory = [];
cachedData.minerHistory = [];
}
if (cycleCount === 0) {
cachedData.hashHistory.unshift({ts: Date.now(), hs: cachedData.hash});
if (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
while (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
cachedData.hashHistory.pop();
}
}
cachedData.minerHistory.unshift({ts: Date.now(), cn: cachedData.minerCount});
if (cachedData.minerHistory.length > global.config.general.statsBufferLength) {
while (cachedData.minerHistory.length > global.config.general.statsBufferLength) {
cachedData.minerHistory.pop();
}
}
}
} else {
cachedData = {
hash: Math.floor(localStats[key] / 600),
totalHashes: 0,
lastHash: localTimes[key],
minerCount: localMinerCount[key],
hashHistory: [{ts: currentTime, hs: cachedData.hash}],
minerHistory: [{ts: currentTime, cn: cachedData.hash}]
};
}
global.database.setCache(key + "_stats", cachedData);
});
globalMinerList.forEach(function (miner) {
if (minerList.indexOf(miner) === -1) {
let minerStats = global.database.getCache(miner);
if (minerStats.hash !== 0) {
console.log("Removing: " + miner + " as an active miner from the cache.");
if (miner.indexOf('_') > -1) {
// This is a worker case.
let address_parts = miner.split('_');
let address = address_parts[0];
let worker = address_parts[1];
global.mysql.query("SELECT email FROM users WHERE username = ? limit 1", [address]).then(function (rows) {
if (rows.length === 0) {
return;
}
// toAddress, subject, body
global.support.sendEmail(rows[0].email, "Worker " + worker + " not hashing",
"Hello,\r\n\r\nYour worker: " + worker + " has stopped submitting hashes at: " + global.support.formatDate(Date.now()) +
" UTC\r\n\r\nThank you,\r\nXMRPool.net Administration Team");
});
}
minerStats.hash = 0;
global.database.setCache(miner, minerStats);
}
}
});
Object.keys(identifiers).forEach(function (key) {
global.database.setCache(key + '_identifiers', identifiers[key]);
});
global.database.setCache('minerList', globalMinerList);
callback(null);
});
}
], function (err, result) {
cycleCount += 1;
if (cycleCount === 6){
cycleCount = 0;
}
});
setTimeout(updateShareStats, 10000);
}
function updatePoolStats(poolType) {
async.series([
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").hash || 0);
}
return callback(null, global.database.getCache("global_stats").hash || 0);
},
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").minerCount || 0);
}
return callback(null, global.database.getCache("global_stats").minerCount || 0);
},
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").totalHashes || 0);
}
return callback(null, global.database.getCache("global_stats").totalHashes || 0);
},
function (callback) {
debug(threadName + "Checking MySQL for last block find time for pool stats");
let cacheData = global.database.getBlockList(poolType);
if (cacheData.length === 0) {
return callback(null, 0);
}
return callback(null, Math.floor(cacheData[0].ts / 1000));
},
function (callback) {
debug(threadName + "Checking MySQL for last block find time for pool stats");
let cacheData = global.database.getBlockList(poolType);
if (cacheData.length === 0) {
return callback(null, 0);
}
return callback(null, cacheData[0].height);
},
function (callback) {
debug(threadName + "Checking MySQL for block count for pool stats");
return callback(null, global.database.getBlockList(poolType).length);
},
function (callback) {
debug(threadName + "Checking MySQL for total miners paid");
if (typeof(poolType) !== 'undefined') {
global.mysql.query("SELECT id FROM payments WHERE pool_type = ? group by payment_address, payment_id", [poolType]).then(function (rows) {
return callback(null, rows.length);
});
} else {
global.mysql.query("SELECT id FROM payments group by payment_address, payment_id").then(function (rows) {
return callback(null, rows.length);
});
}
},
function (callback) {
debug(threadName + "Checking MySQL for total transactions count");
if (typeof(poolType) !== 'undefined') {
global.mysql.query("SELECT distinct(transaction_id) from payments WHERE pool_type = ?", [poolType]).then(function (rows) {
return callback(null, rows.length);
});
} else {
global.mysql.query("SELECT count(id) as txn_count FROM transactions").then(function (rows) {
if (typeof(rows[0]) !== 'undefined') {
return callback(null, rows[0].txn_count);
} else {
return callback(null, 0);
}
});
}
},
], function (err, result) {
if (typeof(poolType) === 'undefined') {
poolType = 'global';
}
global.database.setCache('pool_stats_' + poolType, {
hashRate: result[0],
miners: result[1],
totalHashes: result[2],
lastBlockFoundTime: result[3] || 0,
lastBlockFound: result[4] || 0,
totalBlocksFound: result[5] || 0,
totalMinersPaid: result[6] || 0,
totalPayments: result[7] || 0
});
});
}
function updatePoolPorts(poolServers) {
debug(threadName + "Updating pool ports");
let local_cache = {global: []};
let portCount = 0;
global.mysql.query("select * from ports where hidden = 0 and lastSeen >= NOW() - INTERVAL 10 MINUTE").then(function (rows) {
rows.forEach(function (row) {
portCount += 1;
if (!local_cache.hasOwnProperty(row.port_type)) {
local_cache[row.port_type] = [];
}
local_cache[row.port_type].push({
host: poolServers[row.pool_id],
port: row.network_port,
difficulty: row.starting_diff,
description: row.description,
miners: row.miners
});
if (portCount === rows.length) {
let local_counts = {};
let port_diff = {};
let port_miners = {};
let pool_type_count = 0;
let localPortInfo = {};
for (let pool_type in local_cache) { // jshint ignore:line
pool_type_count += 1;
local_cache[pool_type].forEach(function (portData) { // jshint ignore:line
if (!local_counts.hasOwnProperty(portData.port)) {
local_counts[portData.port] = 0;
}
if (!port_diff.hasOwnProperty(portData.port)) {
port_diff[portData.port] = portData.difficulty;
}
if (!port_miners.hasOwnProperty(portData.port)) {
port_miners[portData.port] = 0;
}
if (port_diff[portData.port] === portData.difficulty) {
local_counts[portData.port] += 1;
port_miners[portData.port] += portData.miners;
}
localPortInfo[portData.port] = portData.description;
if (local_counts[portData.port] === Object.keys(poolServers).length) {
local_cache.global.push({
host: {
blockID: local_cache[pool_type][0].host.blockID,
blockIDTime: local_cache[pool_type][0].host.blockIDTime,
hostname: global.config.pool.geoDNS,
},
port: portData.port,
pool_type: pool_type,
difficulty: portData.difficulty,
miners: port_miners[portData.port],
description: localPortInfo[portData.port]
});
}
});
if (pool_type_count === Object.keys(local_cache).length) {
debug(threadName + "Sending the following to the workers: " + JSON.stringify(local_cache));
global.database.setCache('poolPorts', local_cache);
}
}
}
});
});
}
function updatePoolInformation() {
let local_cache = {};
debug(threadName + "Updating pool information");
global.mysql.query("select * from pools where last_checkin >= NOW() - INTERVAL 10 MINUTE").then(function (rows) {
rows.forEach(function (row) {
local_cache[row.id] = {
ip: row.ip,
blockID: row.blockID,
blockIDTime: global.support.formatDateFromSQL(row.blockIDTime),
hostname: row.hostname
};
if (Object.keys(local_cache).length === rows.length) {
global.database.setCache('poolServers', local_cache);
updatePoolPorts(local_cache);
}
});
});
}
function updateBlockHeader() {
global.support.rpcDaemon('getlastblockheader', [], function (body) {
if (body.result) {
global.database.setCache('networkBlockInfo', {
difficulty: body.result.block_header.difficulty,
hash: body.result.block_header.hash,
height: body.result.block_header.height,
value: body.result.block_header.reward,
ts: body.result.block_header.timestamp
});
} else {
console.error("GetLastBlockHeader Error during block header update");
}
});
}
function updateWalletStats() {
async.waterfall([
function (callback) {
global.support.rpcWallet('getbalance', [], function (body) {
if (body.result) {
return callback(null, {
balance: body.result.balance,
unlocked: body.result.unlocked_balance,
ts: Date.now()
});
} else {
return callback(true, "Unable to process balance");
}
});
},
function (state, callback) {
global.support.rpcWallet('getheight', [], function (body) {
if (body.result) {
state.height = body.result.height;
return callback(null, state);
} else {
return callback(true, "Unable to get current wallet height");
}
});
}
], function (err, results) {
if (err) {
return console.error(err);
}
global.database.setCache('walletStateInfo', results);
let history = global.database.getCache('walletHistory');
if (history === false) {
history = [];
}
history.unshift(results);
history = history.sort(global.support.tsCompare);
if (history.length > global.config.general.statsBufferLength) {
while (history.length > global.config.general.statsBufferLength) {
history.pop();
}
}
global.database.setCache('walletHistory', history);
});
}
function monitorNodes() {
global.mysql.query("SELECT blockID FROM pools WHERE last_checkin > date_sub(now(), interval 30 minute)").then(function (rows) {
global.coinFuncs.getLastBlockHeader(function (block) {
rows.forEach(function (row) {
if (row.blockID < block.height - 3) {
global.support.sendEmail(global.config.general.adminEmail, "Pool server behind in blocks", "The pool server: "+row.hostname+" with IP: "+row.ip+" is "+block.height - row.blockID+ " blocks behind");
}
}
);
});
});
}
updateShareStats();
updateBlockHeader();
updatePoolStats();
updatePoolInformation();
updateWalletStats();
monitorNodes();
setInterval(updateBlockHeader, 10000);
setInterval(updatePoolStats, 5000);
setInterval(updatePoolStats, 5000, 'pplns');
setInterval(updatePoolStats, 5000, 'pps');
setInterval(updatePoolStats, 5000, 'solo');
setInterval(updatePoolInformation, 5000);
setInterval(updateWalletStats, 60000);
setInterval(monitorNodes, 300000);