Feat/pool fee in api (#39)

* Initial poolFee commit

This is defined by the following in the pool_config

```
    "poolFee": 0.2,
    "_comment_poolFee": "This is only used for API purpose to display pool fee percentage",
```

* Added poolFee function

This function looks at the user defined `poolFee` in their pool_config and if it doesn't exist it will calculate it based on `rewardRecipients`

* Fix to show %

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Added poolFee

* Spacing fix

* Changing gears -- instead of providing additional configuration, we're
just going to expose the rewardRecipients. This allows the consuming
client to filter community donation addresses, etc. as needed.

In addition, I've replaced all tabs in libs/stats.js with spaces.
This commit is contained in:
zzzpotato 2018-10-31 16:47:01 -05:00 committed by Nate Smith
parent d7826dcad7
commit 250377de5a
1 changed files with 269 additions and 266 deletions

View File

@ -26,29 +26,29 @@ function rediscreateClient(port, host, pass) {
* @returns {Array} array of items in [[key,value],[key,value],...] format.
*/
function sortProperties(obj, sortedBy, isNumericSort, reverse) {
sortedBy = sortedBy || 1; // by default first key
isNumericSort = isNumericSort || false; // by default text sort
reverse = reverse || false; // by default no reverse
sortedBy = sortedBy || 1; // by default first key
isNumericSort = isNumericSort || false; // by default text sort
reverse = reverse || false; // by default no reverse
var reversed = (reverse) ? -1 : 1;
var reversed = (reverse) ? -1 : 1;
var sortable = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
sortable.push([key, obj[key]]);
}
}
if (isNumericSort)
sortable.sort(function (a, b) {
return reversed * (a[1][sortedBy] - b[1][sortedBy]);
});
else
sortable.sort(function (a, b) {
var x = a[1][sortedBy].toLowerCase(),
y = b[1][sortedBy].toLowerCase();
return x < y ? reversed * -1 : x > y ? reversed : 0;
});
return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
var sortable = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
sortable.push([key, obj[key]]);
}
}
if (isNumericSort)
sortable.sort(function (a, b) {
return reversed * (a[1][sortedBy] - b[1][sortedBy]);
});
else
sortable.sort(function (a, b) {
var x = a[1][sortedBy].toLowerCase(),
y = b[1][sortedBy].toLowerCase();
return x < y ? reversed * -1 : x > y ? reversed : 0;
});
return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
}
module.exports = function(logger, portalConfig, poolConfigs){
@ -134,33 +134,33 @@ module.exports = function(logger, portalConfig, poolConfigs){
});
}
function getWorkerStats(address) {
address = address.split(".")[0];
if (address.length > 0 && address.startsWith('t')) {
for (var h in statHistory) {
for(var pool in statHistory[h].pools) {
function getWorkerStats(address) {
address = address.split(".")[0];
if (address.length > 0 && address.startsWith('t')) {
for (var h in statHistory) {
for(var pool in statHistory[h].pools) {
statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate);
statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate);
for(var w in statHistory[h].pools[pool].workers){
if (w.startsWith(address)) {
if (history[w] == null) {
history[w] = [];
}
if (workers[w] == null && stats.pools[pool].workers[w] != null) {
workers[w] = stats.pools[pool].workers[w];
}
if (statHistory[h].pools[pool].workers[w].hashrate) {
history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate});
}
}
}
}
}
return JSON.stringify({"workers": workers, "history": history});
}
return null;
}
for(var w in statHistory[h].pools[pool].workers){
if (w.startsWith(address)) {
if (history[w] == null) {
history[w] = [];
}
if (workers[w] == null && stats.pools[pool].workers[w] != null) {
workers[w] = stats.pools[pool].workers[w];
}
if (statHistory[h].pools[pool].workers[w].hashrate) {
history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate});
}
}
}
}
}
return JSON.stringify({"workers": workers, "history": history});
}
return null;
}
function addStatPoolHistory(stats){
var data = {
@ -233,37 +233,37 @@ module.exports = function(logger, portalConfig, poolConfigs){
});
};
this.getTotalSharesByAddress = function(address, cback) {
var a = address.split(".")[0];
this.getTotalSharesByAddress = function(address, cback) {
var a = address.split(".")[0];
var client = redisClients[0].client,
coins = redisClients[0].coins,
shares = [];
var pindex = parseInt(0);
var totalShares = parseFloat(0);
async.each(_this.stats.pools, function(pool, pcb) {
var totalShares = parseFloat(0);
async.each(_this.stats.pools, function(pool, pcb) {
pindex++;
var coin = String(_this.stats.pools[pool.name].name);
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) {
var coin = String(_this.stats.pools[pool.name].name);
client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) {
if (error) {
pcb(error);
return;
}
var workerName="";
var shares = 0;
for (var i in result[1]) {
if (Math.abs(i % 2) != 1) {
workerName = String(result[1][i]);
} else {
shares += parseFloat(result[1][i]);
}
}
var workerName="";
var shares = 0;
for (var i in result[1]) {
if (Math.abs(i % 2) != 1) {
workerName = String(result[1][i]);
} else {
shares += parseFloat(result[1][i]);
}
}
if (shares>0) {
totalShares = shares;
}
pcb();
});
}, function(err) {
});
}, function(err) {
if (err) {
cback(0);
return;
@ -272,25 +272,25 @@ module.exports = function(logger, portalConfig, poolConfigs){
cback(totalShares);
return;
}
});
};
});
};
this.getBalanceByAddress = function(address, cback){
var a = address.split(".")[0];
var a = address.split(".")[0];
var client = redisClients[0].client,
coins = redisClients[0].coins,
balances = [];
var totalHeld = parseFloat(0);
var totalPaid = parseFloat(0);
var totalHeld = parseFloat(0);
var totalPaid = parseFloat(0);
var totalImmature = parseFloat(0);
async.each(_this.stats.pools, function(pool, pcb) {
var coin = String(_this.stats.pools[pool.name].name);
// get all immature balances from address
client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) {
async.each(_this.stats.pools, function(pool, pcb) {
var coin = String(_this.stats.pools[pool.name].name);
// get all immature balances from address
client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) {
// get all balances from address
client.hscan(coin + ':balances', 0, "match", a+"*", "count", 10000, function(error, bals) {
// get all payouts from address
@ -347,18 +347,18 @@ module.exports = function(logger, portalConfig, poolConfigs){
});
});
});
}, function(err) {
if (err) {
callback("There was an error getting balances");
return;
}
}, function(err) {
if (err) {
callback("There was an error getting balances");
return;
}
_this.stats.balances = balances;
_this.stats.address = address;
_this.stats.balances = balances;
_this.stats.address = address;
cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances});
});
};
cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances});
});
};
this.getGlobalStats = function(callback){
@ -377,9 +377,9 @@ module.exports = function(logger, portalConfig, poolConfigs){
['scard', ':blocksPending'],
['scard', ':blocksConfirmed'],
['scard', ':blocksKicked'],
['smembers', ':blocksPending'],
['smembers', ':blocksConfirmed'],
['hgetall', ':shares:roundCurrent'],
['smembers', ':blocksPending'],
['smembers', ':blocksConfirmed'],
['hgetall', ':shares:roundCurrent'],
['hgetall', ':blocksPendingConfirms'],
['zrange', ':payments', -100, -1],
['hgetall', ':shares:timesCurrent']
@ -401,6 +401,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
callback(err);
}
else{
for(var i = 0; i < replies.length; i += commandsPerCoin){
var coinName = client.coins[i / commandsPerCoin | 0];
var marketStats = {};
@ -409,21 +410,23 @@ module.exports = function(logger, portalConfig, poolConfigs){
marketStats = replies[i + 2] ? (JSON.parse(replies[i + 2].coinmarketcap)[0] || 0) : 0;
}
}
var coinStats = {
name: coinName,
symbol: poolConfigs[coinName].coin.symbol.toUpperCase(),
algorithm: poolConfigs[coinName].coin.algorithm,
poolFees: poolConfigs[coinName].rewardRecipients,
hashrates: replies[i + 1],
poolStats: {
validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0,
validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0,
invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0,
totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0,
networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0,
networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0,
networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0),
networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0,
networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0,
networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0,
networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0,
networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0),
networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0,
networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0,
networkVersion: replies[i + 2] ? (replies[i + 2].networkSubVersion || 0) : 0,
networkProtocolVersion: replies[i + 2] ? (replies[i + 2].networkProtocolVersion || 0) : 0
},
@ -435,16 +438,16 @@ module.exports = function(logger, portalConfig, poolConfigs){
orphaned: replies[i + 5]
},
/* show all pending blocks */
pending: {
blocks: replies[i + 6].sort(sortBlocks),
pending: {
blocks: replies[i + 6].sort(sortBlocks),
confirms: (replies[i + 9] || {})
},
},
/* show last 50 found blocks */
confirmed: {
blocks: replies[i + 7].sort(sortBlocks).slice(0,50)
},
confirmed: {
blocks: replies[i + 7].sort(sortBlocks).slice(0,50)
},
payments: [],
currentRoundShares: (replies[i + 8] || {}),
currentRoundShares: (replies[i + 8] || {}),
currentRoundTimes: (replies[i + 11] || {}),
maxRoundTime: 0,
shareCount: 0
@ -487,106 +490,106 @@ module.exports = function(logger, portalConfig, poolConfigs){
Object.keys(allCoinStats).forEach(function(coin){
var coinStats = allCoinStats[coin];
coinStats.workers = {};
coinStats.miners = {};
coinStats.miners = {};
coinStats.shares = 0;
coinStats.hashrates.forEach(function(ins){
var parts = ins.split(':');
var workerShares = parseFloat(parts[0]);
var miner = parts[1].split('.')[0];
var miner = parts[1].split('.')[0];
var worker = parts[1];
var diff = Math.round(parts[0] * 8192);
var diff = Math.round(parts[0] * 8192);
if (workerShares > 0) {
coinStats.shares += workerShares;
// build worker stats
// build worker stats
if (worker in coinStats.workers) {
coinStats.workers[worker].shares += workerShares;
coinStats.workers[worker].diff = diff;
coinStats.workers[worker].diff = diff;
} else {
coinStats.workers[worker] = {
name: worker,
diff: diff,
name: worker,
diff: diff,
shares: workerShares,
invalidshares: 0,
currRoundShares: 0,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
};
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].shares += workerShares;
} else {
coinStats.miners[miner] = {
name: miner,
shares: workerShares,
invalidshares: 0,
currRoundShares: 0,
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].shares += workerShares;
} else {
coinStats.miners[miner] = {
name: miner,
shares: workerShares,
invalidshares: 0,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
}
else {
// build worker stats
// build worker stats
if (worker in coinStats.workers) {
coinStats.workers[worker].invalidshares -= workerShares; // workerShares is negative number!
coinStats.workers[worker].diff = diff;
coinStats.workers[worker].diff = diff;
} else {
coinStats.workers[worker] = {
name: worker,
diff: diff,
name: worker,
diff: diff,
shares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
luckDays: null,
luckHours: null,
paid: 0,
balance: 0
};
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number!
} else {
coinStats.miners[miner] = {
name: miner,
shares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
}
// build miner stats
if (miner in coinStats.miners) {
coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number!
} else {
coinStats.miners[miner] = {
name: miner,
shares: 0,
invalidshares: -workerShares,
currRoundShares: 0,
currRoundTime: 0,
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
hashrate: null,
hashrateString: null,
luckDays: null,
luckHours: null
};
}
}
});
// sort miners
coinStats.miners = sortMinersByHashrate(coinStats.miners);
// sort miners
coinStats.miners = sortMinersByHashrate(coinStats.miners);
var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier;
coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate);
var _blocktime = 160;
var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2;
var _myHashRate = (coinStats.hashrate / 1000000) * 2;
coinStats.luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.minerCount = Object.keys(coinStats.miners).length;
var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2;
var _myHashRate = (coinStats.hashrate / 1000000) * 2;
coinStats.luckDays = ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.minerCount = Object.keys(coinStats.miners).length;
coinStats.workerCount = Object.keys(coinStats.workers).length;
portalStats.global.workers += coinStats.workerCount;
@ -617,7 +620,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
for (var worker in coinStats.currentRoundTimes) {
var time = parseFloat(coinStats.currentRoundTimes[worker]);
if (_maxTimeShare < time) { _maxTimeShare = time; }
var miner = worker.split(".")[0]; // split poolId from minerAddress
var miner = worker.split(".")[0]; // split poolId from minerAddress
if (miner in coinStats.miners && coinStats.miners[miner].currRoundTime < time) {
coinStats.miners[miner].currRoundTime = time;
}
@ -628,28 +631,28 @@ module.exports = function(logger, portalConfig, poolConfigs){
coinStats.maxRoundTimeString = readableSeconds(_maxTimeShare);
for (var worker in coinStats.workers) {
var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.workers[worker].hashrate = _workerRate;
coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate);
var miner = worker.split('.')[0];
if (miner in coinStats.miners) {
coinStats.workers[worker].currRoundTime = coinStats.miners[miner].currRoundTime;
}
var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.workers[worker].hashrate = _workerRate;
coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate);
var miner = worker.split('.')[0];
if (miner in coinStats.miners) {
coinStats.workers[worker].currRoundTime = coinStats.miners[miner].currRoundTime;
}
}
for (var miner in coinStats.miners) {
var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.miners[miner].hashrate = _workerRate;
coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate);
for (var miner in coinStats.miners) {
var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow;
var _wHashRate = (_workerRate / 1000000) * 2;
coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
coinStats.miners[miner].hashrate = _workerRate;
coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate);
}
// sort workers by name
coinStats.workers = sortWorkersByName(coinStats.workers);
// sort workers by name
coinStats.workers = sortWorkersByName(coinStats.workers);
delete coinStats.hashrates;
delete coinStats.shares;
@ -675,7 +678,7 @@ module.exports = function(logger, portalConfig, poolConfigs){
_this.statsString = JSON.stringify(saveStats);
_this.statHistory.push(saveStats);
addStatPoolHistory(portalStats);
addStatPoolHistory(portalStats);
var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0);
@ -702,14 +705,14 @@ module.exports = function(logger, portalConfig, poolConfigs){
};
function sortPoolsByName(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortBlocks(a, b) {
@ -720,55 +723,55 @@ module.exports = function(logger, portalConfig, poolConfigs){
return 0;
}
function sortWorkersByName(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortWorkersByName(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'name', false, false);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortMinersByHashrate(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'shares', true, true);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortMinersByHashrate(objects) {
var newObject = {};
var sortedArray = sortProperties(objects, 'shares', true, true);
for (var i = 0; i < sortedArray.length; i++) {
var key = sortedArray[i][0];
var value = sortedArray[i][1];
newObject[key] = value;
}
return newObject;
}
function sortWorkersByHashrate(a, b) {
if (a.hashrate === b.hashrate) {
return 0;
}
else {
return (a.hashrate < b.hashrate) ? -1 : 1;
}
}
function sortWorkersByHashrate(a, b) {
if (a.hashrate === b.hashrate) {
return 0;
}
else {
return (a.hashrate < b.hashrate) ? -1 : 1;
}
}
this.getReadableHashRateString = function(hashrate){
hashrate = (hashrate * 2);
if (hashrate < 1000000) {
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
}
hashrate = (hashrate * 2);
if (hashrate < 1000000) {
return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
}
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
};
function getReadableNetworkHashRateString(hashrate) {
hashrate = (hashrate * 1000000);
if (hashrate < 1000000)
return '0 Sol';
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
}
function getReadableNetworkHashRateString(hashrate) {
hashrate = (hashrate * 1000000);
if (hashrate < 1000000)
return '0 Sol';
var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
return hashrate.toFixed(2) + byteUnits[i];
}
};