From e7cd9303ea9d2d8f31e97a9d864d1f7b3cfbcc6f Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Thu, 9 Apr 2020 18:07:23 -0400 Subject: [PATCH 1/6] Added endpoints with various statistics - index.js changed to load PoolUIServer.js instead of PoolServer.js and to pass the pool host when constructing a PoolServer object. - PoolServer.js changed to support child classes overriding how the httpsServer is created. - PoolUIServer added which extends PoolServer.js and adds the new endpoints. --- index.js | 5 +- src/PoolServer.js | 15 +- src/PoolUIServer.js | 388 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 src/PoolUIServer.js diff --git a/index.js b/index.js index 8850964..16e76b4 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,8 @@ const Nimiq = require('@nimiq/core'); const argv = require('minimist')(process.argv.slice(2)); const config = require('./src/Config.js')(argv.config); -const PoolServer = require('./src/PoolServer.js'); +//const PoolServer = require('./src/PoolServer.js'); +const PoolServer = require('./src/PoolUIServer.js'); const PoolService = require('./src/PoolService.js'); const PoolPayout = require('./src/PoolPayout.js'); const MetricsServer = require('./src/MetricsServer.js'); @@ -113,7 +114,7 @@ for (const key in config.constantOverrides) { } if (config.poolServer.enabled) { - const poolServer = new PoolServer($.consensus, config.pool, config.poolServer.port, config.poolServer.mySqlPsw, config.poolServer.mySqlHost, config.poolServer.sslKeyPath, config.poolServer.sslCertPath, config.reverseProxy); + const poolServer = new PoolServer($.consensus, config.pool, config.poolServer.port, config.poolServer.mySqlPsw, config.poolServer.mySqlHost, config.poolServer.sslKeyPath, config.poolServer.sslCertPath, config.reverseProxy, config.host); if (config.poolMetricsServer.enabled) { $.metricsServer = new MetricsServer(config.poolServer.sslKeyPath, config.poolServer.sslCertPath, config.poolMetricsServer.port, config.poolMetricsServer.password); diff --git a/src/PoolServer.js b/src/PoolServer.js index c4f74fe..adf44ef 100644 --- a/src/PoolServer.js +++ b/src/PoolServer.js @@ -135,7 +135,11 @@ class PoolServer extends Nimiq.Observable { database: 'pool' }); - this._wss = PoolServer.createServer(this.port, this._sslKeyPath, this._sslCertPath); + let httpsServer = await this.startServer(this.port, this._sslKeyPath, this._sslCertPath); + // We have to access socket.remoteAddress here because otherwise req.connection.remoteAddress won't be set in the WebSocket's 'connection' event (yay) + httpsServer.on('secureConnection', socket => socket.remoteAddress); + + this._wss = new WebSocket.Server({server: httpsServer}); this._wss.on('connection', (ws, req) => this._onConnection(ws, req)); this.consensus.blockchain.on('head-changed', (head) => { @@ -145,6 +149,10 @@ class PoolServer extends Nimiq.Observable { }); } + async startServer(...args) { + return await PoolServer.createServer(this.port, this._sslKeyPath, this._sslCertPath); + } + static createServer(port, sslKeyPath, sslCertPath) { const sslOptions = { key: fs.readFileSync(sslKeyPath), @@ -155,11 +163,8 @@ class PoolServer extends Nimiq.Observable { res.end('Nimiq Pool Server\n'); }).listen(port); - // We have to access socket.remoteAddress here because otherwise req.connection.remoteAddress won't be set in the WebSocket's 'connection' event (yay) - httpsServer.on('secureConnection', socket => socket.remoteAddress); - Nimiq.Log.i(PoolServer, "Started server on port " + port); - return new WebSocket.Server({server: httpsServer}); + return httpsServer; } stop() { diff --git a/src/PoolUIServer.js b/src/PoolUIServer.js new file mode 100644 index 0000000..21fe320 --- /dev/null +++ b/src/PoolUIServer.js @@ -0,0 +1,388 @@ +const POOL_DESCRIPTION = { + description : "", + websiteLink : "", + communityLink : "", + payoutType : "Manual", + payoutFrequency : "", + // payoutType : "Automatic", + // payoutFrequency : "every 3 hours", + supportsNano : false +}; + +const Nimiq = require("@nimiq/core"); +const https = require("https"); +const mysql = require("mysql2/promise"); +const fs = require("fs"); +const PoolServer = require("./PoolServer.js"); + +// Exists for development purposes when only running the endpoints. +const isFake = !!PoolServer.isFake; + +const QUERIES = { + currentStats : `SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 10 MINUTE THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) as hashrate0, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate1, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 3 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate3, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate6, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate24 + FROM shares s JOIN user u on u.id = s.user JOIN block b on b.id = s.prev_block WHERE + FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR;`, + statsForDevices : `SELECT s.device as deviceID, + u.address as address, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) as hashrate1, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares1, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate6, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares6, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate24, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares24 + FROM shares s JOIN user u on u.id = s.user JOIN block b on b.id = s.prev_block WHERE + FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY device WITH ROLLUP;`, + statsForAddresses : `SELECT u.address as address, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) as hashrate1, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares1, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate6, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares6, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate24, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares24 + FROM shares s JOIN user u on u.id = s.user JOIN block b on b.id = s.prev_block WHERE + FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY address WITH ROLLUP;`, + statsForAddress : `SELECT u.address as address, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) as hashrate1, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares1, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate6, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares6, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate24, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares24 + FROM shares s JOIN user u on u.id = s.user JOIN block b on b.id = s.prev_block WHERE + address=? AND FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY user;`, + devicesForAddress : `SELECT s.device as deviceID, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) as hashrate1, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 1 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares1, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 21600) as hashrate6, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 6 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares6, + ROUND(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 86400) as hashrate24, + CAST(SUM(CASE WHEN FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR THEN s.count ELSE 0 END) AS UNSIGNED) as shares24 + FROM shares s JOIN user u on u.id = s.user JOIN block b on b.id = s.prev_block WHERE + u.address=? AND FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY device;`, + payoutsForAddress : `SELECT address, amount, datetime as timestamp, transaction as txHash FROM payout p INNER JOIN user u ON u.id=p.user WHERE u.address=? ORDER BY datetime DESC LIMIT 50;`, + blocksMinedForever : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id GROUP BY b.main_chain;`, + blocksMined24Hours : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id WHERE FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY b.main_chain;`, + blocksMinedList : `SELECT height, hash, datetime FROM payin p INNER JOIN block b ON p.block=b.id WHERE main_chain=1 GROUP BY height ORDER BY height DESC LIMIT 50;`, + hashrateHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id WHERE FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR) AS avgHR FROM block b WHERE height%60=0 ORDER BY height DESC LIMIT 24;`, + minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE height%60=0 ORDER BY height DESC LIMIT 24;`, + minerTotalPayedOut : `SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=? GROUP BY address;`, + minerTotalEarned : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 GROUP BY address;`, + minerTotalOwed : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND (datetime * 1000) > (SELECT MAX(datetime) FROM payout) GROUP BY address;` +}; + +class PoolUIServer extends PoolServer { + constructor(consensus, config, port, mySqlPsw, mySqlHost, sslKeyPath, sslCertPath, reverseProxy, poolHost) { + super(consensus, config, port, mySqlPsw, mySqlHost, sslKeyPath, sslCertPath, reverseProxy); + this.host = poolHost; + } + + async start() { + if (this._started) return; + super.start(); + + this.readOnlyConnection = await mysql.createPool({ + host: this._mySqlHost, + user: "pool_info", + password: "", + database: "pool" + }); + } + + async startServer(...args) { + return await PoolUIServer.createServer(...args, this); + } + + async queryDB(query, ...args) { + let result = await this.readOnlyConnection.execute(query, args); + return JSON.parse(JSON.stringify(result[0])); + } + + static parseAddress(str) { + try { + let addr = Nimiq.Address.fromAny(str); + return addr; + } catch { + return false; + } + } + + static createServer(port, sslKeyPath, sslCertPath, poolServer) { + const sslOptions = { + key: fs.readFileSync(sslKeyPath), + cert: fs.readFileSync(sslCertPath) + }; + const replyToRequest = (res, what, code = 200) => { + res.writeHead(code); + res.end(typeof what == "object" ? JSON.stringify(what, null, "\t") : what); + }; + const rejectRequest = (res, why, code = 400) => { + res.writeHead(code); + res.end(typeof why == "object" ? JSON.stringify(why, null, "\t") : why); + }; + + let lastHRHistory = { + time : 0, + result : [] + }; + + const routes = [ + { + name : "Landing Page", + path : "/", + runs : (req, res) => { + replyToRequest(res, "Nimiq Pool UI Server\n"); + } + }, { + name : "Pool Config", + path : "/api/pool/config", + runs : (req, res) => { + let obj = { + name : poolServer.name, + address : poolServer.poolAddress.toUserFriendlyAddress(), + host : poolServer.host, + port : poolServer.port.toString(), + description : POOL_DESCRIPTION.description, + website : POOL_DESCRIPTION.websiteLink, + community : POOL_DESCRIPTION.communityLink, + fees : (poolServer.config.poolFee * 100).toFixed(2) + "%", + payouts : POOL_DESCRIPTION.payoutType + " payouts " + POOL_DESCRIPTION.payoutFrequency + (POOL_DESCRIPTION.payoutFrequency ? " " : "") + "for balances over " + (poolServer.config.autoPayOutLimit / 100000).toFixed(2) + " NIM", + supportsNano : POOL_DESCRIPTION.supportsNano + }; + + replyToRequest(res, JSON.stringify(obj)); + } + }, { + name : "Pool Stats", + path : "/api/pool/stats", + runs : async (req, res) => { + let currentlyConnectedClients = isFake ? {} : poolServer.getClientModeCounts(); + currentlyConnectedClients.total = Object.values(currentlyConnectedClients).reduce((t, c) => t + c, 0); + + let blockStats24 = (await poolServer.queryDB(QUERIES.blocksMined24Hours)).filter(it => it.mainChain == 1).map(it => { + return it.blockCount; + }); + let blockStatsForever = (await poolServer.queryDB(QUERIES.blocksMinedForever)).filter(it => it.mainChain == 1).map(it => { + return it.blockCount; + }); + let obj = { + name : poolServer.name, + clientCounts : currentlyConnectedClients, + averageHashrate : poolServer.averageHashrate, + blocksMined : { + sinceLastRestart : poolServer.numBlocksMined, + inLast24Hours : blockStats24[0], + total : blockStatsForever[0] + }, + }; + + replyToRequest(res, obj); + } + }, { + name : "Pool History", + path : "/api/pool/history", + runs : async (req, res) => { + let historyHR = null; + + let now = Date.now(); + if (now - lastHRHistory.time > (1000 * 60 * 15)) { + historyHR = (await poolServer.queryDB(QUERIES.hashrateHistory)).map(it => { + it.time = it.time * 1000; + return it; + }); + historyHR.reverse(); + + lastHRHistory.time = now; + lastHRHistory.result = historyHR; + } else { + historyHR = lastHRHistory.result; + } + + let obj = { + hashrate : historyHR + }; + + replyToRequest(res, obj); + } + }, { + name : "Scary Pool Stats", + path : "/api/owner/stats", + runs : async (req, res) => { + let currentlyConnectedClients = isFake ? {} : poolServer.getClientModeCounts(); + currentlyConnectedClients.total = Object.keys(currentlyConnectedClients).reduce((t, c) => t + c, 0); + + let blockStats24 = (await poolServer.queryDB(QUERIES.blocksMined24Hours)).filter(it => it.mainChain == 0).map(it => { + return it.blockCount; + }); + let blockStatsForever = (await poolServer.queryDB(QUERIES.blocksMinedForever)).filter(it => it.mainChain == 0).map(it => { + return it.blockCount; + }); + let obj = { + unclesMined : { + inLast24Hours : blockStats24[0], + total : blockStatsForever[0] + }, + }; + + replyToRequest(res, obj); + } + }, { + name : "Miner Stats", + path : "/api/miner/(.*)", + runs : async (req, res, arr) => { + if (arr.length < 1) { + return rejectRequest(res, { + error : "URL Param missing, address expected after 'miner/'" + }); + } + + let addr = PoolUIServer.parseAddress(arr[1]); + + if (!addr) { + return rejectRequest(res, { + error : "Incorrectly formatted address : '" + arr[1] + "'" + }); + } + + let miningStats = (await poolServer.queryDB(QUERIES.statsForAddress, addr.toBase64())).map(it => { + return { + address : PoolUIServer.parseAddress(it.address).toUserFriendlyAddress(), + stats24 : { hash : it.hashrate24 , shares : it.shares24 }, + stats6 : { hash : it.hashrate6 , shares : it.shares6 }, + stats1 : { hash : it.hashrate1 , shares : it.shares1 } + }; + }); + + let totalPayedOut = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); + let totalEarned = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); + let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); + + let payoutStats = (await poolServer.queryDB(QUERIES.payoutsForAddress, addr.toBase64())).map(it => { + return { + amount : it.amount, + timestamp : it.timestamp, + txHash : /* "0x" + */ Nimiq.BufferUtils.toHex(Nimiq.SerialBuffer.from(it.txHash.data)) + }; + }); + + let deviceStats = (await poolServer.queryDB(QUERIES.devicesForAddress, addr.toBase64())).map(it => { + return { + deviceID : it.deviceID, + stats24 : { hash : it.hashrate24 , shares : it.shares24 }, + stats6 : { hash : it.hashrate6 , shares : it.shares6 }, + stats1 : { hash : it.hashrate1 , shares : it.shares1 } + }; + }); + + let historyHR = (await poolServer.queryDB(QUERIES.minerHistory, addr.toBase64())).map(it => { + it.time = it.time * 1000; + return it; + }); + historyHR.reverse(); + + replyToRequest(res, { + general : miningStats.length > 0 ? miningStats[0] : null, + payouts : payoutStats.length > 0 ? payoutStats : null, + devices : deviceStats.length > 0 ? deviceStats : null, + hashrate : historyHR.length > 0 ? historyHR : null, + balance : { + payedOut : totalPayedOut.length > 0 ? totalPayedOut[0] : 0, + earned : totalEarned.length > 0 ? totalEarned[0] : 0, + owed : totalOwed.length > 0 ? totalOwed[0] : 0 + } + }); + } + }, { + name : "Miner List", + path : "/api/list/miners", + runs : async (req, res, arr) => { + let result = await poolServer.queryDB(QUERIES.statsForAddresses); + replyToRequest(res, result.map(it => { + return { + address : !it.address ? it.address : PoolUIServer.parseAddress(it.address).toUserFriendlyAddress(), + stats24 : { hash : it.hashrate24 , shares : it.shares24 }, + stats6 : { hash : it.hashrate6 , shares : it.shares6 }, + stats1 : { hash : it.hashrate1 , shares : it.shares1 } + }; + })); + } + }, { + name : "Device List", + path : "/api/list/devices", + runs : async (req, res, arr) => { + let result = await poolServer.queryDB(QUERIES.statsForDevices); + replyToRequest(res, result.map(it => { + return { + deviceID : it.deviceID, + // ownerAddress : PoolUIServer.parseAddress(it.address).toUserFriendlyAddress(), + stats24 : { hash : it.hashrate24 , shares : it.shares24 }, + stats6 : { hash : it.hashrate6 , shares : it.shares6 }, + stats1 : { hash : it.hashrate1 , shares : it.shares1 } + }; + })); + } + }, { + name : "Blocks Mined List", + path : "/api/list/blocks", + runs : async (req, res, arr) => { + let result = (await poolServer.queryDB(QUERIES.blocksMinedList)).map(it => { + return { + height : it.height, + timestamp : it.datetime * 1000, + blockHash : /* "0x" + */ Nimiq.BufferUtils.toHex(Nimiq.SerialBuffer.from(it.hash.data)) + }; + }); + replyToRequest(res, result); + } + } + ].map(it => { + it.path = "/" + it.path.split("/").filter(it => !!it).join("/"); + return it; + }); + + const httpsServer = https.createServer(sslOptions, (req, res) => { + let matchPath = "/" + req.url.split("/").filter(it => !!it).join("/"); + let matching = routes.filter(it => { + return matchPath.match("^" + it.path + "$"); + }).filter(it => !!it); + + if (matching.length > 0) { + try { + let match = matchPath.match("^" + matching[0].path + "$"); + match = match ? match.map(unescape) : []; + + if (!matching[0].restrictAccess) { + res.setHeader("Access-Control-Allow-Origin", "*"); + } + + if (req.method.toUpperCase() == "POST") { + let body = ""; + req.on("data", chunk => { + body += chunk.toString(); // convert Buffer to string + }); + req.on("end", () => { + req.body = body; + matching[0].runs(req, res, match); + }); + } else { + matching[0].runs(req, res, match); + } + } catch (e) { + console.error("Error while processing route : " + matching[0].name, e); + } + } else { + res.writeHead(404); + res.end("Page not found!"); + } + }); + httpsServer.listen(port); + + Nimiq.Log.i(PoolUIServer, "Started server on port " + port); + return httpsServer; + } +} + +module.exports = PoolUIServer; From eaeb686bd584e381aa07677bcd2f9988a92d1acc Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Sat, 11 Apr 2020 10:46:17 -0400 Subject: [PATCH 2/6] Fixed issues with queries - hashrateHistory for both pool and miner now include the latest block rather than the closest block divisible by 60. This just makes the last point on the graph slightly more accurate and is only really important for brand new pools trying to make sure the set up is working. - /api/miner/? was returning payout information using the minerTotalPayedOut query for totalPayedOut, totalEarned, and totalOwed. totalEarned and totalOwed are now using the correct queries for their info. --- src/PoolUIServer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PoolUIServer.js b/src/PoolUIServer.js index 21fe320..7c744de 100644 --- a/src/PoolUIServer.js +++ b/src/PoolUIServer.js @@ -67,8 +67,8 @@ const QUERIES = { blocksMinedForever : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id GROUP BY b.main_chain;`, blocksMined24Hours : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id WHERE FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY b.main_chain;`, blocksMinedList : `SELECT height, hash, datetime FROM payin p INNER JOIN block b ON p.block=b.id WHERE main_chain=1 GROUP BY height ORDER BY height DESC LIMIT 50;`, - hashrateHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id WHERE FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR) AS avgHR FROM block b WHERE height%60=0 ORDER BY height DESC LIMIT 24;`, - minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE height%60=0 ORDER BY height DESC LIMIT 24;`, + hashrateHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id WHERE FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24`, + minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24;`, minerTotalPayedOut : `SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=? GROUP BY address;`, minerTotalEarned : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 GROUP BY address;`, minerTotalOwed : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND (datetime * 1000) > (SELECT MAX(datetime) FROM payout) GROUP BY address;` @@ -257,8 +257,8 @@ class PoolUIServer extends PoolServer { }); let totalPayedOut = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); - let totalEarned = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); - let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); + let totalEarned = (await poolServer.queryDB(QUERIES.minerTotalEarned, addr.toBase64())).map(it => it.amount); + let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalOwed, addr.toBase64())).map(it => it.amount); let payoutStats = (await poolServer.queryDB(QUERIES.payoutsForAddress, addr.toBase64())).map(it => { return { From 515b98c71d29c32b9c44d42e4e01b65257d13e4a Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Sat, 11 Apr 2020 16:33:44 -0400 Subject: [PATCH 3/6] Cleanup + Improvements - Moved functions to Helper.js and configurable settings to Config.js - Improved accuracy of some endpoints by taking into account whether blocks are confirmed or not (has more than X confirmations where X is the payoutConfirmations config option) --- src/Config.js | 8 ++++++++ src/Helper.js | 5 +++++ src/PoolUIServer.js | 45 ++++++++++++++++++++------------------------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Config.js b/src/Config.js index 4227adb..f9b42ad 100644 --- a/src/Config.js +++ b/src/Config.js @@ -42,6 +42,14 @@ const DEFAULT_CONFIG = /** @type {Config} */ { payoutConfirmations: 10, autoPayOutLimit: 5000000, // 50 NIM poolFee: 0.01, // 1% + poolInfo: { + description : "", + websiteLink : "", + communityLink : "", + payoutType : "Manual", + payoutFrequency : "", + supportsNano : false + }, networkFee: 1, // satoshi per byte startDifficulty: 1, minDifficulty: 1, diff --git a/src/Helper.js b/src/Helper.js index e1af5cb..472a842 100644 --- a/src/Helper.js +++ b/src/Helper.js @@ -11,6 +11,11 @@ class Helper { return (1 - config.poolFee) * (Nimiq.Policy.blockRewardAt(block.height) + block.transactions.reduce((sum, tx) => sum + tx.fee, 0)); } + async queryDB(connectionPool, query, ...args) { + let result = await connectionPool.execute(query, args); + return JSON.parse(JSON.stringify(result[0])); + } + /** * @param {PoolConfig} config * @param {mysql2.Pool} connectionPool diff --git a/src/PoolUIServer.js b/src/PoolUIServer.js index 7c744de..f14618d 100644 --- a/src/PoolUIServer.js +++ b/src/PoolUIServer.js @@ -1,21 +1,12 @@ -const POOL_DESCRIPTION = { - description : "", - websiteLink : "", - communityLink : "", - payoutType : "Manual", - payoutFrequency : "", - // payoutType : "Automatic", - // payoutFrequency : "every 3 hours", - supportsNano : false -}; - const Nimiq = require("@nimiq/core"); const https = require("https"); const mysql = require("mysql2/promise"); const fs = require("fs"); const PoolServer = require("./PoolServer.js"); -// Exists for development purposes when only running the endpoints. +const Helper = require('./Helper.js'); + +// Exists for development purposes when only running the endpoints using a stub index.js and PoolServer class. const isFake = !!PoolServer.isFake; const QUERIES = { @@ -66,12 +57,12 @@ const QUERIES = { payoutsForAddress : `SELECT address, amount, datetime as timestamp, transaction as txHash FROM payout p INNER JOIN user u ON u.id=p.user WHERE u.address=? ORDER BY datetime DESC LIMIT 50;`, blocksMinedForever : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id GROUP BY b.main_chain;`, blocksMined24Hours : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id WHERE FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY b.main_chain;`, - blocksMinedList : `SELECT height, hash, datetime FROM payin p INNER JOIN block b ON p.block=b.id WHERE main_chain=1 GROUP BY height ORDER BY height DESC LIMIT 50;`, + blocksMinedList : `SELECT height, hash, datetime, height <= ? AS confirmed FROM payin p INNER JOIN block b ON p.block=b.id WHERE main_chain=1 GROUP BY height ORDER BY height DESC LIMIT 50;`, hashrateHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id WHERE FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24`, minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24;`, minerTotalPayedOut : `SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=? GROUP BY address;`, minerTotalEarned : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 GROUP BY address;`, - minerTotalOwed : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND (datetime * 1000) > (SELECT MAX(datetime) FROM payout) GROUP BY address;` + minerTotalOwed : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND (datetime * 1000) > (SELECT MAX(datetime) FROM payout) AND height <= ? GROUP BY address;` }; class PoolUIServer extends PoolServer { @@ -97,8 +88,7 @@ class PoolUIServer extends PoolServer { } async queryDB(query, ...args) { - let result = await this.readOnlyConnection.execute(query, args); - return JSON.parse(JSON.stringify(result[0])); + return Helper.queryDB(this.readOnlyConnection, query, ...args); } static parseAddress(str) { @@ -145,12 +135,12 @@ class PoolUIServer extends PoolServer { address : poolServer.poolAddress.toUserFriendlyAddress(), host : poolServer.host, port : poolServer.port.toString(), - description : POOL_DESCRIPTION.description, - website : POOL_DESCRIPTION.websiteLink, - community : POOL_DESCRIPTION.communityLink, + description : poolServer.config.poolInfo.description, + website : poolServer.config.poolInfo.websiteLink, + community : poolServer.config.poolInfo.communityLink, fees : (poolServer.config.poolFee * 100).toFixed(2) + "%", - payouts : POOL_DESCRIPTION.payoutType + " payouts " + POOL_DESCRIPTION.payoutFrequency + (POOL_DESCRIPTION.payoutFrequency ? " " : "") + "for balances over " + (poolServer.config.autoPayOutLimit / 100000).toFixed(2) + " NIM", - supportsNano : POOL_DESCRIPTION.supportsNano + payouts : poolServer.config.poolInfo.payoutType + " payouts " + poolServer.config.poolInfo.payoutFrequency + (poolServer.config.poolInfo.payoutFrequency ? " " : "") + "for balances over " + (poolServer.config.autoPayOutLimit / 100000).toFixed(2) + " NIM", + supportsNano : poolServer.config.poolInfo.supportsNano }; replyToRequest(res, JSON.stringify(obj)); @@ -241,7 +231,7 @@ class PoolUIServer extends PoolServer { let addr = PoolUIServer.parseAddress(arr[1]); - if (!addr) { + if (!addr || (arr[1].startsWith("NQ") && arr[1].split(" ").join("") != addr.toUserFriendlyAddress().split(" ").join(""))) { return rejectRequest(res, { error : "Incorrectly formatted address : '" + arr[1] + "'" }); @@ -256,9 +246,11 @@ class PoolUIServer extends PoolServer { }; }); + const blocksConfirmedHeight = poolServer.consensus.blockchain.height - poolServer.config.payoutConfirmations; + let totalPayedOut = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); let totalEarned = (await poolServer.queryDB(QUERIES.minerTotalEarned, addr.toBase64())).map(it => it.amount); - let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalOwed, addr.toBase64())).map(it => it.amount); + let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalOwed, addr.toBase64(), blocksConfirmedHeight)).map(it => it.amount); let payoutStats = (await poolServer.queryDB(QUERIES.payoutsForAddress, addr.toBase64())).map(it => { return { @@ -328,11 +320,14 @@ class PoolUIServer extends PoolServer { name : "Blocks Mined List", path : "/api/list/blocks", runs : async (req, res, arr) => { - let result = (await poolServer.queryDB(QUERIES.blocksMinedList)).map(it => { + const blocksConfirmedHeight = poolServer.consensus.blockchain.height - poolServer.config.payoutConfirmations; + + let result = (await poolServer.queryDB(QUERIES.blocksMinedList, blocksConfirmedHeight)).map(it => { return { height : it.height, timestamp : it.datetime * 1000, - blockHash : /* "0x" + */ Nimiq.BufferUtils.toHex(Nimiq.SerialBuffer.from(it.hash.data)) + blockHash : /* "0x" + */ Nimiq.BufferUtils.toHex(Nimiq.SerialBuffer.from(it.hash.data)), + confirmed : it.confirmed == 1 }; }); replyToRequest(res, result); From b35c668b09bfb7bea8cddef67e23a502f79610ca Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Sat, 11 Apr 2020 16:42:02 -0400 Subject: [PATCH 4/6] Updated sample configs with new options --- combined.sample.conf | 12 +++++++++++- payout.sample.conf | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/combined.sample.conf b/combined.sample.conf index 8a859c0..3c9fb08 100644 --- a/combined.sample.conf +++ b/combined.sample.conf @@ -56,6 +56,17 @@ // Default: 0.01 //poolFee: 0.01, + // The following object is used by the endpoints to describe your pool. + // Default: description and both links are blank, payoutType is "Manual", payoutFrequency is blank, supportsNano is false + //poolInfo: { + // description : "", + // websiteLink : "", + // communityLink : "", + // payoutType : "Automatic", + // payoutFrequency : "every 3 hours", + // supportsNano : false + //}, + // Network fee used by the pool for payouts (in satoshi per byte). // Default: 1 //networkFee: 1, @@ -119,4 +130,3 @@ //level: "verbose" } } - diff --git a/payout.sample.conf b/payout.sample.conf index de771aa..1264ff8 100644 --- a/payout.sample.conf +++ b/payout.sample.conf @@ -33,6 +33,17 @@ // Default: 0.01 //poolFee: 0.01, + // The following object is used by the endpoints to describe your pool. + // Default: description and both links are blank, payoutType is "Manual", payoutFrequency is blank, supportsNano is false + //poolInfo: { + // description : "", + // websiteLink : "", + // communityLink : "", + // payoutType : "Automatic", + // payoutFrequency : "every 3 hours", + // supportsNano : false + //}, + // Network fee used by the pool for payouts (in satoshi per byte). // Default: 1 //networkFee: 1, From e6cf022d817de6ab865af87aa63e3c0a5962b99b Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Sat, 11 Apr 2020 18:33:24 -0400 Subject: [PATCH 5/6] Fixing Helper.queryDB bug and touching up endpoints some more - Helper.queryDB wasn't listed as a static function and therefore couldn't be called from PoolUIServer which needs it. - /api/pool/config endpoint now prints JSON prettily like all other endpoints. --- src/Helper.js | 2 +- src/PoolUIServer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Helper.js b/src/Helper.js index 472a842..b592f7e 100644 --- a/src/Helper.js +++ b/src/Helper.js @@ -11,7 +11,7 @@ class Helper { return (1 - config.poolFee) * (Nimiq.Policy.blockRewardAt(block.height) + block.transactions.reduce((sum, tx) => sum + tx.fee, 0)); } - async queryDB(connectionPool, query, ...args) { + static async queryDB(connectionPool, query, ...args) { let result = await connectionPool.execute(query, args); return JSON.parse(JSON.stringify(result[0])); } diff --git a/src/PoolUIServer.js b/src/PoolUIServer.js index f14618d..08036af 100644 --- a/src/PoolUIServer.js +++ b/src/PoolUIServer.js @@ -143,7 +143,7 @@ class PoolUIServer extends PoolServer { supportsNano : poolServer.config.poolInfo.supportsNano }; - replyToRequest(res, JSON.stringify(obj)); + replyToRequest(res, obj); } }, { name : "Pool Stats", From 69dba1d4a6ab73f30df5a61c0184ecd679696f10 Mon Sep 17 00:00:00 2001 From: Matthew Ludwig Date: Sun, 12 Apr 2020 11:16:32 -0400 Subject: [PATCH 6/6] Updated totalOwed query to be more accurate - Original query was looking for all blocks with a datetime after the most recent payout's datetime, but this was an incorrect assumption and resulted in values that would be off by a few block rewards (payouts were still accurate, only endpoint was affected) --- src/PoolUIServer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PoolUIServer.js b/src/PoolUIServer.js index 08036af..60dde1e 100644 --- a/src/PoolUIServer.js +++ b/src/PoolUIServer.js @@ -59,10 +59,10 @@ const QUERIES = { blocksMined24Hours : `SELECT main_chain as mainChain, count(distinct p.block) AS blockCount FROM block b JOIN payin p ON p.block = b.id WHERE FROM_UNIXTIME(b.datetime) > NOW() - INTERVAL 24 HOUR GROUP BY b.main_chain;`, blocksMinedList : `SELECT height, hash, datetime, height <= ? AS confirmed FROM payin p INNER JOIN block b ON p.block=b.id WHERE main_chain=1 GROUP BY height ORDER BY height DESC LIMIT 50;`, hashrateHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id WHERE FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24`, - minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24;`, - minerTotalPayedOut : `SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=? GROUP BY address;`, - minerTotalEarned : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 GROUP BY address;`, - minerTotalOwed : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND (datetime * 1000) > (SELECT MAX(datetime) FROM payout) AND height <= ? GROUP BY address;` + minerHistory : `SELECT datetime AS time, (SELECT ROUND(SUM(CASE WHEN FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR THEN s.difficulty ELSE 0 END) * POW(2, 16) / 3600) FROM shares s JOIN block a ON s.prev_block=a.id JOIN user u ON u.id=s.user WHERE u.address=? AND FROM_UNIXTIME(a.datetime) < FROM_UNIXTIME(b.datetime) AND FROM_UNIXTIME(a.datetime) > FROM_UNIXTIME(b.datetime) - INTERVAL 1 HOUR GROUP BY user) AS avgHR FROM block b WHERE (height%60=0 AND height<(SELECT MAX(height)-60 FROM block)) OR height=(SELECT MAX(height) FROM block) ORDER BY datetime DESC LIMIT 24`, + minerTotalPayedOut : `SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=? GROUP BY address`, + minerTotalEarned : `SELECT SUM(amount) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 GROUP BY address`, + minerTotalOwed : `SELECT SUM(amount) - (SELECT SUM(amount) as amount FROM payout p JOIN user u ON u.id=p.user WHERE u.address=?) as amount FROM payin p JOIN user u ON u.id=p.user JOIN block b ON p.block=b.id WHERE u.address=? AND main_chain=1 AND height <= ? GROUP BY address` }; class PoolUIServer extends PoolServer { @@ -250,7 +250,7 @@ class PoolUIServer extends PoolServer { let totalPayedOut = (await poolServer.queryDB(QUERIES.minerTotalPayedOut, addr.toBase64())).map(it => it.amount); let totalEarned = (await poolServer.queryDB(QUERIES.minerTotalEarned, addr.toBase64())).map(it => it.amount); - let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalOwed, addr.toBase64(), blocksConfirmedHeight)).map(it => it.amount); + let totalOwed = (await poolServer.queryDB(QUERIES.minerTotalOwed, addr.toBase64(), addr.toBase64(), blocksConfirmedHeight)).map(it => it.amount); let payoutStats = (await poolServer.queryDB(QUERIES.payoutsForAddress, addr.toBase64())).map(it => { return {