Browse Source

exchange rate / currency improvements:

- support for multiple currency exchange rates
- support for inline display of exchange currencies
- a little frontend cleanup
fix-133-memory-crash
Dan Janosik 6 years ago
parent
commit
e9e8b57d3f
  1. 6
      app.js
  2. 52
      app/coins/btc.js
  3. 27
      app/coins/ltc.js
  4. 188
      app/utils.js
  5. 11
      views/includes/value-display.pug
  6. 4
      views/index.pug
  7. 20
      views/mempool-summary.pug

6
app.js

@ -338,12 +338,12 @@ app.runOnStartup = function() {
});
}
if (global.exchangeRate == null) {
utils.refreshExchangeRate();
if (global.exchangeRates == null) {
utils.refreshExchangeRates();
}
// refresh exchange rate periodically
setInterval(utils.refreshExchangeRate, 1800000);
setInterval(utils.refreshExchangeRates, 1800000);
utils.logMemoryUsage();
setInterval(utils.logMemoryUsage, 5000);

52
app/coins/btc.js

@ -1,8 +1,9 @@
var Decimal = require("decimal.js");
Decimal8 = Decimal.clone({ precision:8, rounding:8 });
var btcCurrencyUnits = [
var currencyUnits = [
{
type:"native",
name:"BTC",
multiplier:1,
default:true,
@ -10,23 +11,42 @@ var btcCurrencyUnits = [
decimalPlaces:8
},
{
type:"native",
name:"mBTC",
multiplier:1000,
values:["mbtc"],
decimalPlaces:5
},
{
type:"native",
name:"bits",
multiplier:1000000,
values:["bits"],
decimalPlaces:2
},
{
type:"native",
name:"sat",
multiplier:100000000,
values:["sat", "satoshi"],
decimalPlaces:0
}
},
{
type:"exchanged",
name:"USD",
multiplier:"usd",
values:["usd"],
decimalPlaces:2,
symbol:"$"
},
{
type:"exchanged",
name:"EUR",
multiplier:"eur",
values:["eur"],
decimalPlaces:2,
symbol:"€"
},
];
module.exports = {
@ -43,9 +63,10 @@ module.exports = {
"https://raw.githubusercontent.com/btccom/Blockchain-Known-Pools/master/pools.json"
],
maxBlockWeight: 4000000,
currencyUnits:btcCurrencyUnits,
currencyUnitsByName:{"BTC":btcCurrencyUnits[0], "mBTC":btcCurrencyUnits[1], "bits":btcCurrencyUnits[2], "sat":btcCurrencyUnits[3]},
baseCurrencyUnit:btcCurrencyUnits[3],
currencyUnits:currencyUnits,
currencyUnitsByName:{"BTC":currencyUnits[0], "mBTC":currencyUnits[1], "bits":currencyUnits[2], "sat":currencyUnits[3]},
baseCurrencyUnit:currencyUnits[3],
defaultCurrencyUnit:currencyUnits[0],
feeSatoshiPerByteBucketMaxima: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 75, 100, 150],
genesisBlockHash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
genesisCoinbaseTransactionId: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
@ -189,14 +210,25 @@ module.exports = {
}
],
exchangeRateData:{
jsonUrl:"https://api.coinmarketcap.com/v1/ticker/Bitcoin/",
exchangedCurrencyName:"usd",
jsonUrl:"https://api.coindesk.com/v1/bpi/currentprice.json",
responseBodySelectorFunction:function(responseBody) {
if (responseBody[0] && responseBody[0].price_usd) {
return responseBody[0].price_usd;
//console.log("Exchange Rate Response: " + JSON.stringify(responseBody));
var exchangedCurrencies = ["USD", "GBP", "EUR"];
if (responseBody.bpi) {
var exchangeRates = {};
for (var i = 0; i < exchangedCurrencies.length; i++) {
if (responseBody.bpi[exchangedCurrencies[i]]) {
exchangeRates[exchangedCurrencies[i].toLowerCase()] = responseBody.bpi[exchangedCurrencies[i]].rate_float;
}
}
return exchangeRates;
}
return -1;
return null;
}
},
blockRewardFunction:function(blockHeight) {

27
app/coins/ltc.js

@ -1,8 +1,9 @@
var Decimal = require("decimal.js");
Decimal8 = Decimal.clone({ precision:8, rounding:8 });
var ltcCurrencyUnits = [
var currencyUnits = [
{
type:"native",
name:"LTC",
multiplier:1,
default:true,
@ -10,23 +11,34 @@ var ltcCurrencyUnits = [
decimalPlaces:8
},
{
type:"native",
name:"lite",
multiplier:1000,
values:["lite"],
decimalPlaces:5
},
{
type:"native",
name:"photon",
multiplier:1000000,
values:["photon"],
decimalPlaces:2
},
{
type:"native",
name:"litoshi",
multiplier:100000000,
values:["litoshi", "lit"],
decimalPlaces:0
}
},
{
type:"exchanged",
name:"USD",
multiplier:"usd",
values:["usd"],
decimalPlaces:2,
symbol:"$"
},
];
module.exports = {
@ -41,9 +53,10 @@ module.exports = {
"https://raw.githubusercontent.com/hashstream/pools/master/pools.json",
],
maxBlockWeight: 4000000,
currencyUnits:ltcCurrencyUnits,
currencyUnitsByName:{"LTC":ltcCurrencyUnits[0], "lite":ltcCurrencyUnits[1], "photon":ltcCurrencyUnits[2], "litoshi":ltcCurrencyUnits[3]},
baseCurrencyUnit:ltcCurrencyUnits[3],
currencyUnits:currencyUnits,
currencyUnitsByName:{"LTC":currencyUnits[0], "lite":currencyUnits[1], "photon":currencyUnits[2], "litoshi":currencyUnits[3]},
baseCurrencyUnit:currencyUnits[3],
defaultCurrencyUnit:currencyUnits[0],
feeSatoshiPerByteBucketMaxima: [5, 10, 25, 50, 100, 150, 200, 250],
genesisBlockHash: "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2",
genesisCoinbaseTransactionId: "97ddfbbae6be97fd6cdf3e7ca13232a3afff2353e29badfab7f73011edd4ced9",
@ -119,10 +132,10 @@ module.exports = {
exchangedCurrencyName:"usd",
responseBodySelectorFunction:function(responseBody) {
if (responseBody[0] && responseBody[0].price_usd) {
return responseBody[0].price_usd;
return {"usd":responseBody[0].price_usd};
}
return -1;
return null;
}
},
blockRewardFunction:function(blockHeight) {

188
app/utils.js

@ -1,5 +1,6 @@
var Decimal = require("decimal.js");
var request = require("request");
var qrcode = require("qrcode");
var config = require("./config.js");
var coins = require("./coins.js");
@ -83,12 +84,34 @@ function getRandomString(length, chars) {
var formatCurrencyCache = {};
function getCurrencyFormatInfo(formatType) {
if (formatCurrencyCache[formatType] == null) {
for (var x = 0; x < coins[config.coin].currencyUnits.length; x++) {
var currencyUnit = coins[config.coin].currencyUnits[x];
for (var y = 0; y < currencyUnit.values.length; y++) {
var currencyUnitValue = currencyUnit.values[y];
if (currencyUnitValue == formatType) {
formatCurrencyCache[formatType] = currencyUnit;
}
}
}
}
if (formatCurrencyCache[formatType] != null) {
return formatCurrencyCache[formatType];
}
return null;
}
function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedDecimalPlaces) {
if (formatCurrencyCache[formatType]) {
var formatInfo = getCurrencyFormatInfo(formatType);
if (formatInfo != null) {
var dec = new Decimal(amount);
dec = dec.times(formatCurrencyCache[formatType].multiplier);
var decimalPlaces = formatCurrencyCache[formatType].decimalPlaces;
var decimalPlaces = formatInfo.decimalPlaces;
if (decimalPlaces == 0 && dec < 1) {
decimalPlaces = 5;
}
@ -97,31 +120,16 @@ function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedD
decimalPlaces = forcedDecimalPlaces;
}
return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + formatCurrencyCache[formatType].name;
}
for (var x = 0; x < coins[config.coin].currencyUnits.length; x++) {
var currencyUnit = coins[config.coin].currencyUnits[x];
for (var y = 0; y < currencyUnit.values.length; y++) {
var currencyUnitValue = currencyUnit.values[y];
if (formatInfo.type == "native") {
dec = dec.times(formatInfo.multiplier);
if (currencyUnitValue == formatType) {
formatCurrencyCache[formatType] = currencyUnit;
return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + formatInfo.name;
var dec = new Decimal(amount);
dec = dec.times(currencyUnit.multiplier);
var decimalPlaces = currencyUnit.decimalPlaces;
if (decimalPlaces == 0 && dec < 1) {
decimalPlaces = 5;
}
} else if (formatInfo.type == "exchanged") {
if (global.exchangeRates != null && global.exchangeRates[formatInfo.multiplier] != null) {
dec = dec.times(global.exchangeRates[formatInfo.multiplier]);
if (forcedDecimalPlaces >= 0) {
decimalPlaces = forcedDecimalPlaces;
}
return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + currencyUnit.name;
return formatInfo.symbol + addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces));
}
}
}
@ -134,7 +142,7 @@ function formatCurrencyAmount(amount, formatType) {
}
function formatCurrencyAmountInSmallestUnits(amount, forcedDecimalPlaces) {
return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].currencyUnits[coins[config.coin].currencyUnits.length - 1].name, forcedDecimalPlaces);
return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].baseCurrencyUnit.name, forcedDecimalPlaces);
}
// ref: https://stackoverflow.com/a/2901298/673828
@ -145,12 +153,12 @@ function addThousandsSeparators(x) {
return parts.join(".");
}
function formatExchangedCurrency(amount) {
if (global.exchangeRate != null) {
function formatExchangedCurrency(amount, exchangeType) {
if (global.exchangeRates != null && global.exchangeRates[exchangeType.toLowerCase()] != null) {
var dec = new Decimal(amount);
dec = dec.times(global.exchangeRate);
dec = dec.times(global.exchangeRates[exchangeType.toLowerCase()]);
return addThousandsSeparators(dec.toDecimalPlaces(2)) + " " + coins[config.coin].exchangeRateData.exchangedCurrencyName;
return "$" + addThousandsSeparators(dec.toDecimalPlaces(2));
}
return "";
@ -267,28 +275,18 @@ function getBlockTotalFeesFromCoinbaseTxAndBlockHeight(coinbaseTx, blockHeight)
return totalOutput.minus(new Decimal(blockReward));
}
function refreshExchangeRate() {
function refreshExchangeRates() {
if (coins[config.coin].exchangeRateData) {
request(coins[config.coin].exchangeRateData.jsonUrl, function(error, response, body) {
if (!error && response && response.statusCode && response.statusCode == 200) {
var responseBody = JSON.parse(body);
var exchangeRate = coins[config.coin].exchangeRateData.responseBodySelectorFunction(responseBody);
if (exchangeRate > 0) {
global.exchangeRate = exchangeRate;
global.exchangeRateUpdateTime = new Date();
var exchangeRates = coins[config.coin].exchangeRateData.responseBodySelectorFunction(responseBody);
if (exchangeRates != null) {
global.exchangeRates = exchangeRates;
global.exchangeRatesUpdateTime = new Date();
if (global.influxdb) {
global.influxdb.writePoints([{
measurement: `exchange_rates.${coins[config.coin].ticker.toLowerCase()}_usd`,
fields:{value:parseFloat(exchangeRate)}
}]).catch(err => {
console.error(`Error saving data to InfluxDB: ${err.stack}`)
});
}
console.log("Using exchange rate: " + global.exchangeRate + " USD/" + coins[config.coin].name + " starting at " + global.exchangeRateUpdateTime);
console.log("Using exchange rates: " + JSON.stringify(global.exchangeRates) + " starting at " + global.exchangeRatesUpdateTime);
} else {
console.log("Unable to get exchange rate data");
@ -386,12 +384,101 @@ function formatLargeNumber(n, decimalPlaces) {
return [new Decimal(n).toDecimalPlaces(decimalPlaces), {}];
}
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return {h:h, s:s, l:l};
}
function colorHexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function colorHexToHsl(hex) {
var rgb = colorHexToRgb(hex);
return rgbToHsl(rgb.r, rgb.g, rgb.b);
}
function logError(errorId, err, optionalUserData = null) {
console.log("Error " + errorId + ": " + err + ", json: " + JSON.stringify(err) + (optionalUserData != null ? (", userData: " + optionalUserData) : ""));
}
function buildQrCodeUrls(strings) {
return new Promise(function(resolve, reject) {
var promises = [];
var qrcodeUrls = {};
for (var i = 0; i < strings.length; i++) {
promises.push(new Promise(function(resolve2, reject2) {
buildQrCodeUrl(strings[i], qrcodeUrls).then(function() {
resolve2();
}).catch(function(err) {
reject2(err);
});
}));
}
Promise.all(promises).then(function(results) {
resolve(qrcodeUrls);
}).catch(function(err) {
reject(err);
});
});
}
function buildQrCodeUrl(str, results) {
return new Promise(function(resolve, reject) {
qrcode.toDataURL(str, function(err, url) {
if (err) {
utils.logError("2q3ur8fhudshfs", err, str);
reject(err);
return;
}
results[str] = url;
resolve();
});
});
}
module.exports = {
redirectToConnectPageIfNeeded: redirectToConnectPageIfNeeded,
hex2ascii: hex2ascii,
splitArrayIntoChunks: splitArrayIntoChunks,
getRandomString: getRandomString,
getCurrencyFormatInfo: getCurrencyFormatInfo,
formatCurrencyAmount: formatCurrencyAmount,
formatCurrencyAmountWithForcedDecimalPlaces: formatCurrencyAmountWithForcedDecimalPlaces,
formatExchangedCurrency: formatExchangedCurrency,
@ -402,9 +489,14 @@ module.exports = {
logMemoryUsage: logMemoryUsage,
getMinerFromCoinbaseTx: getMinerFromCoinbaseTx,
getBlockTotalFeesFromCoinbaseTxAndBlockHeight: getBlockTotalFeesFromCoinbaseTxAndBlockHeight,
refreshExchangeRate: refreshExchangeRate,
refreshExchangeRates: refreshExchangeRates,
parseExponentStringDouble: parseExponentStringDouble,
formatLargeNumber: formatLargeNumber,
geoLocateIpAddresses: geoLocateIpAddresses,
getTxTotalInputOutputValues: getTxTotalInputOutputValues
getTxTotalInputOutputValues: getTxTotalInputOutputValues,
rgbToHsl: rgbToHsl,
colorHexToRgb: colorHexToRgb,
colorHexToHsl: colorHexToHsl,
logError: logError,
buildQrCodeUrls: buildQrCodeUrls
};

11
views/includes/value-display.pug

@ -1,8 +1,15 @@
- var currencyFormatInfo = utils.getCurrencyFormatInfo(currencyFormatType);
if (currencyValue > 0)
span(class="monospace") #{utils.formatCurrencyAmount(currencyValue, currencyFormatType)}
if (global.exchangeRate)
if (currencyFormatInfo.type == "native")
if (global.exchangeRates)
span
span(data-toggle="tooltip", title=utils.formatExchangedCurrency(currencyValue, "usd"))
i(class="fas fa-exchange-alt")
else if (currencyFormatInfo.type == "exchanged")
span
span(data-toggle="tooltip", title=utils.formatExchangedCurrency(currencyValue))
span(data-toggle="tooltip", title=utils.formatCurrencyAmount(currencyValue, coinConfig.defaultCurrencyUnit.name))
i(class="fas fa-exchange-alt")
else
span(class="monospace") 0

4
views/index.pug

@ -96,8 +96,8 @@ block content
span(data-toggle="tooltip", title=("Exchange-rate data from: " + coinConfig.exchangeRateData.jsonUrl))
i(class="fas fa-info-circle")
if (global.exchangeRate)
p(class="lead") #{utils.formatExchangedCurrency(1.0)}
if (global.exchangeRates)
p(class="lead") #{utils.formatExchangedCurrency(1.0, "usd")}
else
p(class="lead") -

20
views/mempool-summary.pug

@ -39,11 +39,9 @@ block content
if (getmempoolinfo.size > 0)
tr
td(class="properties-header") Average Fee
td(class="monospace") #{utils.formatCurrencyAmount(mempoolstats["averageFee"], currencyFormatType)}
if (global.exchangeRate)
span
span(data-toggle="tooltip", title=utils.formatExchangedCurrency(mempoolstats["averageFee"]))
i(class="fas fa-exchange-alt")
td(class="monospace")
- var currencyValue = mempoolstats["averageFee"];
include ./includes/value-display.pug
tr
td(class="properties-header") Average Fee per Byte
@ -119,17 +117,17 @@ block content
tr
td #{mempoolstats["satoshiPerByteBucketLabels"][index]}
td(class="text-right monospace") #{item["count"].toLocaleString()}
td(class="text-right monospace") #{utils.formatCurrencyAmount(item["totalFees"], currencyFormatType)}
td(class="text-right monospace")
- var currencyValue = item["totalFees"];
include ./includes/value-display.pug
if (item["totalBytes"] > 0)
- var avgFee = item["totalFees"] / item["count"];
- var avgFeeRate = item["totalFees"] / item["totalBytes"];
td(class="text-right monospace") #{utils.formatCurrencyAmount(avgFee, currencyFormatType)}
if (global.exchangeRate)
span
span(data-toggle="tooltip", title=utils.formatExchangedCurrency(avgFee))
i(class="fas fa-exchange-alt")
td(class="text-right monospace")
- var currencyValue = avgFee;
include ./includes/value-display.pug
td(class="text-right monospace") #{utils.formatCurrencyAmountInSmallestUnits(avgFeeRate, 2)}/B
else

Loading…
Cancel
Save