mirror of
https://github.com/jwinterm/nodejs-pool.git
synced 2026-10-17 05:21:27 +02:00
Initial commit, moving from private repo. Welcome to the light.
This commit is contained in:
+783
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
Reference in New Issue
Block a user