@@ -256,7 +256,9 @@ Explanation for each field: | |||
"enabled": true, | |||
"hashrateWindow": 600, //how many second worth of shares used to estimate hash rate | |||
"updateInterval": 3, //gather stats and broadcast every this many seconds | |||
"port": 8117 | |||
"port": 8117, | |||
"blocks": 30, //amount of blocks to send at a time | |||
"password": "test" //password required for admin stats | |||
}, | |||
/* Coin daemon connection details. */ | |||
@@ -82,6 +82,7 @@ | |||
"hashrateWindow": 600, | |||
"updateInterval": 5, | |||
"port": 8117, | |||
"blocks": 30, | |||
"password": "your_password" | |||
}, | |||
@@ -5,24 +5,7 @@ var os = require('os'); | |||
var redis = require('redis'); | |||
var configFile = (function(){ | |||
for (var i = 0; i < process.argv.length; i++){ | |||
if (process.argv[i].indexOf('-config=') === 0) | |||
return process.argv[i].split('=')[1]; | |||
} | |||
return 'config.json'; | |||
})(); | |||
try { | |||
global.config = JSON.parse(fs.readFileSync(configFile)); | |||
} | |||
catch(e){ | |||
console.error('Failed to read config file ' + configFile + '\n\n' + e); | |||
return; | |||
} | |||
config.version = "v0.99.0.6"; | |||
require('./lib/configReader.js'); | |||
require('./lib/logger.js'); | |||
@@ -13,13 +13,13 @@ require('./exceptionWriter.js')(logSystem); | |||
var redisCommands = [ | |||
['zremrangebyscore', config.coin + ':hashrate', '-inf', ''], | |||
['zrangebyscore', config.coin + ':hashrate', '', '+inf'], | |||
['hgetall', config.coin + ':stats'], | |||
['smembers', config.coin + ':blocksPending'], | |||
['smembers', config.coin + ':blocksUnlocked'], | |||
['smembers', config.coin + ':blocksOrphaned'], | |||
['zrange', config.coin + ':hashrate', 0, -1], | |||
['hgetall', config.coin + ':stats'], | |||
['zrange', config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES'], | |||
['zrevrange', config.coin + ':blocks:matured', 0, config.api.blocks, 'WITHSCORES'], | |||
['hgetall', config.coin + ':shares:roundCurrent'], | |||
['hgetall', config.coin + ':stats'] | |||
['hgetall', config.coin + ':stats'], | |||
['zcard', config.coin + ':blocks:matured'] | |||
]; | |||
var currentStats = ""; | |||
@@ -56,11 +56,8 @@ function collectStats(){ | |||
var data = { | |||
stats: replies[2], | |||
blocks: { | |||
pending: replies[3], | |||
unlocked: replies[4], | |||
orphaned: replies[5] | |||
} | |||
blocks: replies[3].concat(replies[4]), | |||
totalBlocks: parseInt(replies[7]) + replies[3].length | |||
}; | |||
var hashrates = replies[1]; | |||
@@ -86,14 +83,14 @@ function collectStats(){ | |||
data.roundHashes = 0; | |||
if (replies[6]){ | |||
for (var miner in replies[6]){ | |||
data.roundHashes += parseInt(replies[6][miner]); | |||
if (replies[5]){ | |||
for (var miner in replies[5]){ | |||
data.roundHashes += parseInt(replies[5][miner]); | |||
} | |||
} | |||
if (replies[7]) { | |||
data.lastBlockFound = replies[7].lastBlockFound; | |||
if (replies[6]) { | |||
data.lastBlockFound = replies[6].lastBlockFound; | |||
} | |||
callback(null, data); | |||
@@ -227,6 +224,28 @@ function formatMinerStats(redisData, address){ | |||
} | |||
function handleGetBlocks(urlParts, response){ | |||
redisClient.zrevrangebyscore(config.coin + ':blocks:matured', '(' + urlParts.query.height, '-inf', 'WITHSCORES', 'LIMIT', 0, config.api.blocks, function(err, result){ | |||
var reply; | |||
if (err) | |||
reply = JSON.stringify({error: 'query failed'}); | |||
else | |||
reply = JSON.stringify(result); | |||
response.writeHead("200", { | |||
'Access-Control-Allow-Origin': '*', | |||
'Cache-Control': 'no-cache', | |||
'Content-Type': 'application/json', | |||
'Content-Length': reply.length | |||
}); | |||
response.end(reply); | |||
}); | |||
} | |||
collectStats(); | |||
function authorize(request, response){ | |||
@@ -252,21 +271,24 @@ function handleAdminStats(response){ | |||
async.waterfall([ | |||
//Get worker keys | |||
//Get worker keys & unlocked blocks | |||
function(callback){ | |||
redisClient.keys(config.coin + ':workers:*', function(error, result) { | |||
redisClient.multi([ | |||
['keys', config.coin + ':workers:*'], | |||
['zrange', config.coin + ':blocks:matured', 0, -1] | |||
]).exec(function(error, replies) { | |||
if (error) { | |||
log('error', logSystem, 'Error trying to get worker balances from redis %j', [error]); | |||
log('error', logSystem, 'Error trying to get admin data from redis %j', [error]); | |||
callback(true); | |||
return; | |||
} | |||
callback(null, result); | |||
callback(null, replies[0], replies[1]); | |||
}); | |||
}, | |||
//Get worker balances | |||
function(keys, callback){ | |||
var redisCommands = keys.map(function(k){ | |||
function(workerKeys, blocks, callback){ | |||
var redisCommands = workerKeys.map(function(k){ | |||
return ['hmget', k, 'balance', 'paid']; | |||
}); | |||
redisClient.multi(redisCommands).exec(function(error, replies){ | |||
@@ -276,19 +298,42 @@ function handleAdminStats(response){ | |||
return; | |||
} | |||
var stats = { | |||
totalOwed: 0, | |||
totalPaid: 0 | |||
}; | |||
callback(null, replies, blocks); | |||
}); | |||
}, | |||
function(workerData, blocks, callback){ | |||
var stats = { | |||
totalOwed: 0, | |||
totalPaid: 0, | |||
totalRevenue: 0, | |||
totalDiff: 0, | |||
totalShares: 0, | |||
blocksOrphaned: 0, | |||
blocksUnlocked: 0, | |||
totalWorkers: 0 | |||
}; | |||
for (var i = 0; i < workerData.length; i++){ | |||
stats.totalOwed += parseInt(workerData[i][0]) || 0; | |||
stats.totalPaid += parseInt(workerData[i][1]) || 0; | |||
stats.totalWorkers++; | |||
} | |||
for (var i = 0; i < replies.length; i++){ | |||
stats.totalOwed += parseInt(replies[i][0]) || 0; | |||
stats.totalPaid += parseInt(replies[i][1]) || 0; | |||
for (var i = 0; i < blocks.length; i++){ | |||
var block = blocks[i].split(':'); | |||
if (block[5]) { | |||
stats.blocksUnlocked++; | |||
stats.totalDiff += parseInt(block[2]); | |||
stats.totalShares += parseInt(block[3]); | |||
stats.totalRevenue += parseInt(block[5]); | |||
} | |||
callback(null, stats); | |||
}); | |||
}], function(error, stats){ | |||
else{ | |||
stats.blocksOrphaned++; | |||
} | |||
} | |||
callback(null, stats); | |||
} | |||
], function(error, stats){ | |||
if (error){ | |||
response.end(JSON.stringify({error: 'error collecting stats'})); | |||
return; | |||
@@ -346,6 +391,9 @@ var server = http.createServer(function(request, response){ | |||
case '/stats_address': | |||
handleMinerStats(urlParts, response); | |||
break; | |||
case '/get_blocks': | |||
handleGetBlocks(urlParts, response); | |||
break; | |||
case '/admin_stats': | |||
if (!authorize(request, response)) | |||
return; | |||
@@ -32,28 +32,34 @@ var doDonations = config.blockUnlocker.devDonation > 0 && devDonationAddress[0] | |||
function runInterval(){ | |||
async.waterfall([ | |||
//Get all pending blocks in redis | |||
//Get all block candidates in redis | |||
function(callback){ | |||
redisClient.smembers(config.coin + ':blocksPending', function(error, result){ | |||
redisClient.zrange(config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES', function(error, results){ | |||
if (error){ | |||
log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); | |||
callback(true); | |||
return; | |||
} | |||
if (result.length === 0){ | |||
log('info', logSystem, 'No pending blocks in redis'); | |||
if (results.length === 0){ | |||
log('info', logSystem, 'No blocks candidates in redis'); | |||
callback(true); | |||
return; | |||
} | |||
var blocks = result.map(function(item){ | |||
var parts = item.split(':'); | |||
return { | |||
height: parseInt(parts[0]), | |||
difficulty: parseInt(parts[1]), | |||
hash: parts[2], | |||
serialized: item | |||
}; | |||
}); | |||
var blocks = []; | |||
for (var i = 0; i < results.length; i += 2){ | |||
var parts = results[i].split(':'); | |||
blocks.push({ | |||
serialized: results[i], | |||
height: parseInt(results[i + 1]), | |||
hash: parts[0], | |||
time: parts[1], | |||
difficulty: parts[2], | |||
shares: parts[3] | |||
}); | |||
} | |||
callback(null, blocks); | |||
}); | |||
}, | |||
@@ -75,7 +81,7 @@ function runInterval(){ | |||
return; | |||
} | |||
var blockHeader = result.block_header; | |||
block.orphan = (blockHeader.hash !== block.hash); | |||
block.orphaned = blockHeader.hash === block.hash ? 0 : 1; | |||
block.unlocked = blockHeader.depth >= config.blockUnlocker.depth; | |||
block.reward = blockHeader.reward; | |||
mapCback(block.unlocked); | |||
@@ -83,7 +89,7 @@ function runInterval(){ | |||
}, function(unlockedBlocks){ | |||
if (unlockedBlocks.length === 0){ | |||
log('info', logSystem, 'No pending blocks are unlocked or orphaned yet (%d pending)', [blocks.length]); | |||
log('info', logSystem, 'No pending blocks are unlocked yet (%d pending)', [blocks.length]); | |||
callback(true); | |||
return; | |||
} | |||
@@ -110,9 +116,6 @@ function runInterval(){ | |||
for (var i = 0; i < replies.length; i++){ | |||
var workerShares = replies[i]; | |||
blocks[i].workerShares = workerShares; | |||
blocks[i].totalShares = Object.keys(workerShares).reduce(function(p, c){ | |||
return p + parseInt(workerShares[c]) | |||
}, 0); | |||
} | |||
callback(null, blocks); | |||
}); | |||
@@ -121,19 +124,29 @@ function runInterval(){ | |||
//Handle orphaned blocks | |||
function(blocks, callback){ | |||
var orphanCommands = []; | |||
blocks.forEach(function(block){ | |||
if (!block.orphan) return; | |||
var workerShares = block.workerShares; | |||
orphanCommands.push(['del', config.coin + ':shares:round' + block.height]); | |||
if (!block.orphaned) return; | |||
orphanCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); | |||
orphanCommands.push(['sadd', config.coin + ':blocksOrphaned', block.serialized + ':' + block.totalShares]); | |||
orphanCommands.push(['del', config.coin + ':shares:round' + block.height]); | |||
Object.keys(workerShares).forEach(function(worker){ | |||
orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', | |||
worker, workerShares[worker]]); | |||
}); | |||
orphanCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); | |||
orphanCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ | |||
block.hash, | |||
block.time, | |||
block.difficulty, | |||
block.shares, | |||
block.orphaned | |||
].join(':')]); | |||
if (block.workerShares) { | |||
var workerShares = block.workerShares; | |||
Object.keys(workerShares).forEach(function (worker) { | |||
orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', worker, workerShares[worker]]); | |||
}); | |||
} | |||
}); | |||
if (orphanCommands.length > 0){ | |||
redisClient.multi(orphanCommands).exec(function(error, replies){ | |||
if (error){ | |||
@@ -155,13 +168,19 @@ function runInterval(){ | |||
var payments = {}; | |||
var totalBlocksUnlocked = 0; | |||
blocks.forEach(function(block){ | |||
if (block.orphan) return; | |||
if (block.orphaned) return; | |||
totalBlocksUnlocked++; | |||
unlockedBlocksCommands.push(['del', config.coin + ':shares:round' + block.height]); | |||
unlockedBlocksCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); | |||
unlockedBlocksCommands.push(['sadd', config.coin + ':blocksUnlocked', block.serialized + ':' + block.totalShares]); | |||
unlockedBlocksCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); | |||
unlockedBlocksCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ | |||
block.hash, | |||
block.time, | |||
block.difficulty, | |||
block.shares, | |||
block.orphaned, | |||
block.reward | |||
].join(':')]); | |||
var feePercent = config.blockUnlocker.poolFee / 100; | |||
@@ -173,12 +192,14 @@ function runInterval(){ | |||
var reward = block.reward - (block.reward * feePercent); | |||
var totalShares = block.totalShares; | |||
Object.keys(block.workerShares).forEach(function(worker){ | |||
var percent = block.workerShares[worker] / totalShares; | |||
var workerReward = reward * percent; | |||
payments[worker] = (payments[worker] || 0) + workerReward; | |||
}); | |||
if (block.workerShares) { | |||
var totalShares = parseInt(block.shares); | |||
Object.keys(block.workerShares).forEach(function (worker) { | |||
var percent = block.workerShares[worker] / totalShares; | |||
var workerReward = reward * percent; | |||
payments[worker] = (payments[worker] || 0) + workerReward; | |||
}); | |||
} | |||
}); | |||
for (var worker in payments) { | |||
@@ -205,9 +226,7 @@ function runInterval(){ | |||
log('info', logSystem, 'Unlocked %d blocks and update balances for %d workers', [totalBlocksUnlocked, Object.keys(payments).length]); | |||
callback(null); | |||
}); | |||
} | |||
], function(error, result){ | |||
setTimeout(runInterval, config.blockUnlocker.interval * 1000); | |||
}) | |||
@@ -0,0 +1,20 @@ | |||
var fs = require('fs'); | |||
var configFile = (function(){ | |||
for (var i = 0; i < process.argv.length; i++){ | |||
if (process.argv[i].indexOf('-config=') === 0) | |||
return process.argv[i].split('=')[1]; | |||
} | |||
return 'config.json'; | |||
})(); | |||
try { | |||
global.config = JSON.parse(fs.readFileSync(configFile)); | |||
} | |||
catch(e){ | |||
console.error('Failed to read config file ' + configFile + '\n\n' + e); | |||
return; | |||
} | |||
config.version = "v0.99.1"; |
@@ -350,15 +350,34 @@ function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareTy | |||
]; | |||
if (blockCandidate){ | |||
redisCommands.push(['sadd', config.coin + ':blocksPending', [job.height, currentBlockTemplate.difficulty, hashHex, Date.now() / 1000 | 0].join(':')]); | |||
redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); | |||
//redisCommands.push(['sadd', config.coin + ':blocksPending', [job.height, currentBlockTemplate.difficulty, hashHex, Date.now() / 1000 | 0].join(':')]); | |||
redisCommands.push(['hset', config.coin + ':stats', 'lastBlockFound', Date.now()]); | |||
redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); | |||
redisCommands.push(['hgetall', config.coin + ':shares:round' + job.height]); | |||
} | |||
redisClient.multi(redisCommands).exec(function(err, replies){ | |||
if (err){ | |||
log('error', logSystem, 'Failed to insert share data into redis %j', [err]); | |||
log('error', logSystem, 'Failed to insert share data into redis %j \n %j', [err, redisCommands]); | |||
return; | |||
} | |||
if (blockCandidate){ | |||
var workerShares = replies[replies.length - 1]; | |||
var totalShares = Object.keys(workerShares).reduce(function(p, c){ | |||
return p + parseInt(workerShares[c]) | |||
}, 0); | |||
redisClient.zadd(config.coin + ':blocks:candidates', job.height, [ | |||
hashHex, | |||
Date.now() / 1000 | 0, | |||
currentBlockTemplate.difficulty, | |||
totalShares | |||
].join(':'), function(err, result){ | |||
if (err){ | |||
log('error', logSystem, 'Failed inserting block candidate %s \n %j', [hashHex, err]); | |||
} | |||
}); | |||
} | |||
}); | |||
log('info', logSystem, 'Accepted %s share at difficulty %d/%d from %s@%s', [shareType, job.difficulty, shareDiff, miner.login, miner.ip]); | |||
@@ -396,10 +415,9 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ | |||
var hashNum = bignum.fromBuffer(new Buffer(hashArray)); | |||
var hashDiff = diff1.div(hashNum); | |||
var blockFastHash; | |||
if (hashDiff.ge(blockTemplate.difficulty)){ | |||
blockFastHash = cryptoNightFast(convertedBlob || cnUtil.convert_blob(shareBuffer)).toString('hex'); | |||
apiInterfaces.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function(error, result){ | |||
if (error){ | |||
@@ -407,6 +425,7 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ | |||
recordShareData(miner, job, hashDiff.toString(), false, null, shareType); | |||
} | |||
else{ | |||
var blockFastHash = cryptoNightFast(convertedBlob || cnUtil.convert_blob(shareBuffer)).toString('hex'); | |||
log('info', logSystem, | |||
'Block %s found at height %d by miner %s@%s - submit result: %j', | |||
[blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result] | |||
@@ -0,0 +1,191 @@ | |||
/* | |||
This script converts the block data in redis from the old format (v0.99.0.6 and earlier) to the new format | |||
used in v0.99.1+ | |||
*/ | |||
var util = require('util'); | |||
var async = require('async'); | |||
var redis = require('redis'); | |||
require('./lib/configReader.js'); | |||
var apiInterfaces = require('./lib/apiInterfaces.js')(config.daemon, config.wallet); | |||
function log(severity, system, text, data){ | |||
var formattedMessage = text; | |||
if (data) { | |||
data.unshift(text); | |||
formattedMessage = util.format.apply(null, data); | |||
} | |||
console.log(severity + ': ' + formattedMessage); | |||
} | |||
var logSystem = 'reward script'; | |||
var redisClient = redis.createClient(config.redis.port, config.redis.host); | |||
function getTotalShares(height, callback){ | |||
redisClient.hgetall(config.coin + ':shares:round' + height, function(err, workerShares){ | |||
if (err) { | |||
callback(err); | |||
return; | |||
} | |||
var totalShares = Object.keys(workerShares).reduce(function(p, c){ | |||
return p + parseInt(workerShares[c]) | |||
}, 0); | |||
callback(null, totalShares); | |||
}); | |||
} | |||
async.series([ | |||
function(callback){ | |||
redisClient.smembers(config.coin + ':blocksUnlocked', function(error, result){ | |||
if (error){ | |||
log('error', logSystem, 'Error trying to get unlocke blocks from redis %j', [error]); | |||
callback(); | |||
return; | |||
} | |||
if (result.length === 0){ | |||
log('info', logSystem, 'No unlocked blocks in redis'); | |||
callback(); | |||
return; | |||
} | |||
var blocks = result.map(function(item){ | |||
var parts = item.split(':'); | |||
return { | |||
height: parseInt(parts[0]), | |||
difficulty: parts[1], | |||
hash: parts[2], | |||
time: parts[3], | |||
shares: parts[4], | |||
orphaned: 0 | |||
}; | |||
}); | |||
async.map(blocks, function(block, mapCback){ | |||
apiInterfaces.rpcDaemon('getblockheaderbyheight', {height: block.height}, function(error, result){ | |||
if (error){ | |||
log('error', logSystem, 'Error with getblockheaderbyheight RPC request for block %s - %j', [block.serialized, error]); | |||
mapCback(null, block); | |||
return; | |||
} | |||
if (!result.block_header){ | |||
log('error', logSystem, 'Error with getblockheaderbyheight, no details returned for %s - %j', [block.serialized, result]); | |||
mapCback(null, block); | |||
return; | |||
} | |||
var blockHeader = result.block_header; | |||
block.reward = blockHeader.reward; | |||
mapCback(null, block); | |||
}); | |||
}, function(err, blocks){ | |||
if (blocks.length === 0){ | |||
log('info', logSystem, 'No unlocked blocks'); | |||
callback(); | |||
return; | |||
} | |||
var zaddCommands = [config.coin + ':blocks:matured']; | |||
for (var i = 0; i < blocks.length; i++){ | |||
var block = blocks[i]; | |||
zaddCommands.push(block.height); | |||
zaddCommands.push([ | |||
block.hash, | |||
block.time, | |||
block.difficulty, | |||
block.shares, | |||
block.orphaned, | |||
block.reward | |||
].join(':')); | |||
} | |||
redisClient.zadd(zaddCommands, function(err, result){ | |||
if (err){ | |||
console.log('failed zadd ' + JSON.stringify(err)); | |||
callback(); | |||
return; | |||
} | |||
console.log('successfully converted unlocked blocks to matured blocks'); | |||
callback(); | |||
}); | |||
}); | |||
}); | |||
}, | |||
function(callback){ | |||
redisClient.smembers(config.coin + ':blocksPending', function(error, result) { | |||
if (error) { | |||
log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); | |||
callback(); | |||
return; | |||
} | |||
if (result.length === 0) { | |||
log('info', logSystem, 'No pending blocks in redis'); | |||
callback(); | |||
return; | |||
} | |||
async.map(result, function(item, mapCback){ | |||
var parts = item.split(':'); | |||
var block = { | |||
height: parseInt(parts[0]), | |||
difficulty: parts[1], | |||
hash: parts[2], | |||
time: parts[3], | |||
serialized: item | |||
}; | |||
getTotalShares(block.height, function(err, shares){ | |||
block.shares = shares; | |||
mapCback(null, block); | |||
}); | |||
}, function(err, blocks){ | |||
var zaddCommands = [config.coin + ':blocks:candidates']; | |||
for (var i = 0; i < blocks.length; i++){ | |||
var block = blocks[i]; | |||
zaddCommands.push(block.height); | |||
zaddCommands.push([ | |||
block.hash, | |||
block.time, | |||
block.difficulty, | |||
block.shares | |||
].join(':')); | |||
} | |||
redisClient.zadd(zaddCommands, function(err, result){ | |||
if (err){ | |||
console.log('failed zadd ' + JSON.stringify(err)); | |||
return; | |||
} | |||
console.log('successfully converted pending blocks to block candidates'); | |||
}); | |||
}); | |||
}); | |||
} | |||
], function(){ | |||
process.exit(); | |||
}); |
@@ -13,29 +13,112 @@ | |||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet"> | |||
<style> | |||
#statsHolder{ | |||
margin-bottom: 0; | |||
} | |||
.luckGood{ | |||
color: darkgreen; | |||
} | |||
.luckBad{ | |||
color: darkred; | |||
} | |||
</style> | |||
<script src="config.js"></script> | |||
<script> | |||
var coinDecimals = coinUnits.toString().length - 1; | |||
$(function(){ | |||
getStats(); | |||
}); | |||
var docCookies = { | |||
getItem: function (sKey) { | |||
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; | |||
}, | |||
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) { | |||
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; } | |||
var sExpires = ""; | |||
if (vEnd) { | |||
switch (vEnd.constructor) { | |||
case Number: | |||
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; | |||
break; | |||
case String: | |||
sExpires = "; expires=" + vEnd; | |||
break; | |||
case Date: | |||
sExpires = "; expires=" + vEnd.toUTCString(); | |||
break; | |||
} | |||
} | |||
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); | |||
return true; | |||
}, | |||
removeItem: function (sKey, sPath, sDomain) { | |||
if (!sKey || !this.hasItem(sKey)) { return false; } | |||
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : ""); | |||
return true; | |||
}, | |||
hasItem: function (sKey) { | |||
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); | |||
} | |||
}; | |||
function getReadableCoins(coins){ | |||
return (parseInt(coins || 0) / coinUnits).toFixed(coinDecimals); | |||
return (parseInt(coins || 0) / coinUnits).toFixed(coinUnits.toString().length - 1); | |||
} | |||
$(function(){ | |||
function getStats(promptPassword){ | |||
var password = docCookies.getItem('password'); | |||
var password = prompt('Admin password:'); | |||
if (!password || promptPassword) | |||
password = prompt('Enter admin password'); | |||
$.get(api + '/admin_stats', {password: password}, function(data){ | |||
renderData(data); | |||
$('#loading').show(); | |||
$.ajax({ | |||
url: api + '/admin_stats', | |||
data: {password: password}, | |||
success: function(data){ | |||
docCookies.setItem('password', password, Infinity); | |||
$('#loading').hide(); | |||
renderData(data); | |||
}, | |||
error: function(e){ | |||
docCookies.removeItem('password'); | |||
getStats(true); | |||
} | |||
}); | |||
} | |||
}); | |||
var formatLuck = function(difficulty, shares){ | |||
if (difficulty > shares){ | |||
var percent = 100 - Math.round(shares / difficulty * 100); | |||
return '<span class="luckGood">' + percent + '%</span>'; | |||
} | |||
else{ | |||
var percent = (100 - Math.round(difficulty / shares * 100)) * -1; | |||
return '<span class="luckBad">' + percent + '%</span>'; | |||
} | |||
}; | |||
function renderData(data){ | |||
$('#totalOwed').text(getReadableCoins(data.totalOwed)); | |||
$('#totalPaid').text(getReadableCoins(data.totalPaid)); | |||
$('#totalMined').text(getReadableCoins(data.totalRevenue)); | |||
$('#profit').text(getReadableCoins(data.totalRevenue - data.totalOwed - data.totalPaid)); | |||
$('#averageLuck').html(formatLuck(data.totalDiff, data.totalShares)); | |||
$('#orphanPercent').text((data.blocksOrphaned / data.blocksUnlocked * 100).toFixed(2)); | |||
$('#registeredAddresses').text(data.totalWorkers); | |||
} | |||
</script> | |||
@@ -43,11 +126,29 @@ | |||
</head> | |||
<body> | |||
<div> | |||
<span>Total Owed: </span><span id="totalOwed"></span> | |||
</div> | |||
<div> | |||
<span>Total Paid: </span><span id="totalPaid"></span> | |||
<div class="container"> | |||
<h3>Admin Center <i id="loading" class="fa fa-circle-o-notch fa-spin"></i></h3> | |||
<hr> | |||
<h4>Stats</h4> | |||
<dl class="dl-horizontal" id="statsHolder"> | |||
<dt>Total Owed</dt><dd id="totalOwed">...</dd> | |||
<dt>Total Paid</dt><dd id="totalPaid">...</dd> | |||
<dt>Total Mined</dt><dd id="totalMined">...</dd> | |||
<dt>Profit (before tx fees)</dt><dd id="profit">...</dd> | |||
<dt>Average Luck</dt><dd id="averageLuck">...</dd> | |||
<dt>Orphan Percent</dt><dd id="orphanPercent">...</dd> | |||
<dt>Registered Addresses</dt><dd id="registeredAddresses">...</dd> | |||
</dl> | |||
<br> | |||
<hr> | |||
<h4>Miner Lookup</h4> | |||
</div> | |||
</body> |
@@ -68,18 +68,15 @@ | |||
margin-left: 5px; | |||
} | |||
#lastHash{ | |||
font-family: monospace; | |||
font-family: 'Inconsolata', monospace; | |||
font-size: 0.8em; | |||
} | |||
#yourStatsInput{ | |||
width: 820px; | |||
max-width: 100%; | |||
display: inline-block; | |||
vertical-align: bottom; | |||
font-family: monospace; | |||
z-index: inherit; | |||
font-family: 'Inconsolata', monospace; | |||
} | |||
#yourAddressDisplay > span { | |||
font-family: monospace; | |||
font-family: 'Inconsolata', monospace; | |||
} | |||
.yourStats{ | |||
display: none; | |||
@@ -90,8 +87,8 @@ | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
vertical-align: middle; | |||
font-family: monospace; | |||
font-size: 0.8em; | |||
font-family: 'Inconsolata', monospace; | |||
font-size: 0.9em; | |||
} | |||
#addressError{ | |||
color: red; | |||
@@ -121,7 +118,7 @@ | |||
#blocks_rows > tr > td{ | |||
vertical-align: middle; | |||
font-family: 'Inconsolata', monospace; | |||
font-size: 0.9em; | |||
font-size: 0.95em; | |||
} | |||
.luckGood{ | |||
@@ -183,18 +180,12 @@ | |||
$(function(){ | |||
$('#siteInfo').load('info.html'); | |||
/*$.get('info.html', function(html){ | |||
$('#siteInfo').html(html); | |||
}, 'html');*/ | |||
$.get(api + '/stats', function(data){ | |||
renderStats(data); | |||
updateMarkets(); | |||
}); | |||
function fetchLiveStats() { | |||
$.ajax({ | |||
url: api + '/live_stats', | |||
@@ -338,10 +329,16 @@ | |||
setInterval(updateMarkets, 300000); //poll market data every 5 minutes | |||
var lastStats; | |||
function renderStats(stats){ | |||
lastStats = stats; | |||
$('#coinName').text(stats.config.coin); | |||
$('#blocksTotal').text(stats.pool.totalBlocks); | |||
$('#networkHashrate').text(getReadableHashRateString(stats.network.difficulty / 60) + '/sec'); | |||
$('#networkLastBlockFound').timeago('update', new Date(stats.network.timestamp * 1000).toISOString()); | |||
@@ -408,7 +405,11 @@ | |||
$('#blocksMaturityCount').text(stats.config.depth); | |||
renderBlocks(stats.pool.blocks, stats.config.depth, stats.network.height, blockchainExplorer); | |||
var blocksJson = JSON.stringify(stats.pool.blocks); | |||
if (lastBlocksJson !== blocksJson) { | |||
lastBlocksJson = blocksJson; | |||
renderBlocks(stats.pool.blocks); | |||
} | |||
$('#blocks_rows').find('tr[class=""]').each(function(){ | |||
var height = parseInt(this.children[0].innerHTML); | |||
@@ -434,27 +435,13 @@ | |||
var lastBlocksJson = ''; | |||
function renderBlocks(blocksResults, depth, chainHeight, explorer){ | |||
var blocksJson = JSON.stringify(blocksResults); | |||
if (lastBlocksJson === blocksJson) return; | |||
lastBlocksJson = blocksJson; | |||
var blocks = []; | |||
for (var status in blocksResults){ | |||
var blockArray = blocksResults[status]; | |||
for (var i = 0; i < blockArray.length; i++){ | |||
var blockData = blockArray[i].split(':'); | |||
blockData[0] = parseInt(blockData[0]); | |||
blockData.unshift(status); | |||
blocks.push(blockData); | |||
} | |||
} | |||
blocks.sort(function(a, b){ | |||
return b[1] - a[1]; | |||
}); | |||
var $blockRows = $('#blocks_rows'); | |||
function renderBlocks(blocksResults){ | |||
var depth = lastStats.config.depth; | |||
var chainHeight = lastStats.network.height; | |||
var explorer = blockchainExplorer; | |||
var blockStatusClasses = { | |||
'pending': '', | |||
@@ -467,39 +454,78 @@ | |||
return new Date(parseInt(time) * 1000).toLocaleString(); | |||
}; | |||
var formatLuck = function(percent){ | |||
if (!percent) return ''; | |||
return '<span class="' + (percent < 100 ? 'luckBad' : 'luckGood') + '">' + percent + '%</span>'; | |||
}; | |||
var formatLuck = function(difficulty, shares){ | |||
var rows = ''; | |||
if (difficulty > shares){ | |||
var percent = 100 - Math.round(shares / difficulty * 100); | |||
return '<span class="luckGood"> ' + percent + '%</span>'; | |||
} | |||
else{ | |||
var percent = (100 - Math.round(difficulty / shares * 100)) * -1; | |||
return '<span class="luckBad">' + percent + '%</span>'; | |||
} | |||
}; | |||
var totalLuck = 0; | |||
var totalLuckBlocks = 0; | |||
var pendingRows = ''; | |||
for (var i = 0; i < blocksResults.length; i += 2){ | |||
var parts = blocksResults[i].split(':'); | |||
var block = { | |||
height: parseInt(blocksResults[i + 1]), | |||
hash: parts[0], | |||
time: parts[1], | |||
difficulty: parseInt(parts[2]), | |||
shares: parseInt(parts[3]), | |||
orphaned: parts[4], | |||
reward: parts[5] | |||
}; | |||
switch (block.orphaned){ | |||
case '0': | |||
block.status = 'unlocked'; | |||
break; | |||
case '1': | |||
block.status = 'orphaned'; | |||
break; | |||
default: | |||
block.status = 'pending'; | |||
break; | |||
} | |||
for (var i = 0; i < blocks.length; i++){ | |||
var block = blocks[i]; | |||
var blockLuck = null; | |||
if (block[5]){ | |||
blockLuck = Math.round(parseInt(block[2]) / parseInt(block[5]) * 100); | |||
totalLuck += blockLuck; | |||
totalLuckBlocks++; | |||
var row = '<tr data-height="' + block.height + '" id="blockMatured' + block.height + '" title="' + block.status | |||
+ '" class="' + blockStatusClasses[block.status] + '">' + | |||
'<td>' + block.height + '</td>' + | |||
'<td>' + (block.status === 'pending' ? getMaturity(depth, chainHeight, block.height) : '') + '</td>' + | |||
'<td>' + block.difficulty + '</td>' + | |||
'<td><a target="_blank" href="' + explorer + block.hash + '">' + block.hash + '</a></td>' + | |||
'<td>' + formatDate(block.time) + '</td>' + | |||
'<td>' + formatLuck(block.difficulty, block.shares) + '</td>' + | |||
'</tr>'; | |||
if (block.status === 'pending'){ | |||
pendingRows += row; | |||
} | |||
else if (!$blockRows.find('#blockMatured' + block.height).length){ | |||
var inserted = false; | |||
$blockRows.children().each(function(){ | |||
var bHeight = parseInt(this.getAttribute('data-height')); | |||
if (bHeight < block.height){ | |||
$(this).after(row); | |||
inserted = true; | |||
return false; | |||
} | |||
}); | |||
if (!inserted){ | |||
$blockRows.append(row); | |||
} | |||
} | |||
rows += ('<tr title="' + block[0] + '" class="' + blockStatusClasses[block[0]] + '">' + | |||
'<td>' + block[1] + '</td>' + | |||
'<td>' + (block[0] === 'pending' ? getMaturity(depth, chainHeight, blocks[1]) : '') + '</td>' + | |||
'<td>' + block[2] + '</td>' + | |||
'<td><a target="_blank" href="' + explorer + block[3] + '">' + block[3] + '</a></td>' + | |||
'<td>' + formatDate(block[4]) + '</td>' + | |||
'<td>' + formatLuck(blockLuck) + '</td>' + | |||
'</tr>'); | |||
} | |||
$('#blocks_rows').empty().append(rows); | |||
$('#averageBlockLuck').html(formatLuck(Math.round(totalLuck / totalLuckBlocks))); | |||
$('#blocksCountPending').text(Object.keys(blocksResults['pending']).length); | |||
$('#blocksCountUnlocked').text(Object.keys(blocksResults['unlocked']).length); | |||
$('#blocksCountOrphaned').text(Object.keys(blocksResults['orphaned']).length); | |||
if (pendingRows) { | |||
$blockRows.children('[title="pending"]').remove(); | |||
$blockRows.prepend(pendingRows); | |||
} | |||
} | |||
@@ -607,9 +633,25 @@ | |||
$('#lookUp').click(); | |||
}); | |||
}); | |||
var getBlocksAjax; | |||
$('#loadMoreBlocks').click(function(){ | |||
if (getBlocksAjax) | |||
getBlocksAjax.abort(); | |||
getBlocksAjax = $.ajax({ | |||
url: api + '/get_blocks', | |||
data: { | |||
height: $blockRows.children().last().data('height') | |||
}, | |||
dataType: 'json', | |||
cache: 'false', | |||
success: function(data){ | |||
renderBlocks(data); | |||
} | |||
}) | |||
}); | |||
}); | |||
</script> | |||
@@ -674,9 +716,13 @@ | |||
<div class="stats"> | |||
<h3>Your Stats</h3> | |||
<label for="yourStatsInput"><i class="fa fa-key"></i> Address</label><br> | |||
<input class="form-control" id="yourStatsInput" type="text"> | |||
<button class="btn btn-default" id="lookUp"><i class="fa fa-search"></i> Lookup</button> | |||
<div class="input-group"> | |||
<label class="input-group-addon" for="yourStatsInput"><i class="fa fa-key"></i> Address</label> | |||
<input class="form-control" id="yourStatsInput" type="text"> | |||
<span class="input-group-btn"><button class="btn btn-default" type="button" id="lookUp"><i class="fa fa-search"></i> Lookup</button></span> | |||
</div> | |||
<div id="addressError"></div> | |||
<div class="yourStats"><i class="fa fa-key"></i> Address: <span id="yourAddressDisplay"></span></div> | |||
<div class="yourStats"><i class="fa fa-bank"></i> Pending Balance: <span id="yourPendingBalance"></span></div> | |||
@@ -690,12 +736,8 @@ | |||
<div class="page" id="page_pool_blocks"> | |||
<div class="blocksStatHolder"> | |||
<h4>Block Candidates</h4> | |||
<span><span id="blocksCountPending"></span> Maturing</span> | |||
<span class="bg-success"><span id="blocksCountUnlocked"></span> Unlocked</span> | |||
<span class="bg-danger"><span id="blocksCountOrphaned"></span> Orphaned</span> | |||
<span class="bg-primary"><span id="blocksTotal"></span> Total Blocks Mined</span> | |||
<span class="bg-info">Maturity requires <span id="blocksMaturityCount"></span> blocks</span> | |||
<span><span id="averageBlockLuck"></span> Luck Average</span> | |||
</div> | |||
<hr> | |||
<div class="table-responsive"> | |||
@@ -714,6 +756,11 @@ | |||
</tbody> | |||
</table> | |||
<p class="text-center"> | |||
<button type="button" class="btn btn-default" id="loadMoreBlocks">Load More</button> | |||
</p> | |||
</div> | |||
</div> | |||