var debug = require("debug"); var debugLog = debug("btcexp:utils"); var debugErrorLog = debug("btcexp:error"); var debugErrorVerboseLog = debug("btcexp:errorVerbose"); var Decimal = require("decimal.js"); var request = require("request"); var qrcode = require("qrcode"); var config = require("./config.js"); var coins = require("./coins.js"); var coinConfig = coins[config.coin]; var redisCache = require("./redisCache.js"); var exponentScales = [ {val:1000000000000000000000000000000000, name:"?", abbreviation:"V", exponent:"33"}, {val:1000000000000000000000000000000, name:"?", abbreviation:"W", exponent:"30"}, {val:1000000000000000000000000000, name:"?", abbreviation:"X", exponent:"27"}, {val:1000000000000000000000000, name:"yotta", abbreviation:"Y", exponent:"24"}, {val:1000000000000000000000, name:"zetta", abbreviation:"Z", exponent:"21"}, {val:1000000000000000000, name:"exa", abbreviation:"E", exponent:"18"}, {val:1000000000000000, name:"peta", abbreviation:"P", exponent:"15", textDesc:"Q"}, {val:1000000000000, name:"tera", abbreviation:"T", exponent:"12", textDesc:"T"}, {val:1000000000, name:"giga", abbreviation:"G", exponent:"9", textDesc:"B"}, {val:1000000, name:"mega", abbreviation:"M", exponent:"6", textDesc:"M"}, {val:1000, name:"kilo", abbreviation:"K", exponent:"3", textDesc:"thou"} ]; var ipMemoryCache = {}; var ipRedisCache = null; if (redisCache.active) { var onRedisCacheEvent = function(cacheType, eventType, cacheKey) { global.cacheStats.redis[eventType]++; //debugLog(`cache.${cacheType}.${eventType}: ${cacheKey}`); } ipRedisCache = redisCache.createCache("v0", onRedisCacheEvent); } var ipCache = { get:function(key) { return new Promise(function(resolve, reject) { if (ipMemoryCache[key] != null) { resolve({key:key, value:ipMemoryCache[key]}); return; } if (ipRedisCache != null) { ipRedisCache.get("ip-" + key).then(function(redisResult) { if (redisResult != null) { resolve({key:key, value:redisResult}); return; } resolve({key:key, value:null}); }); } else { resolve({key:key, value:null}); } }); }, set:function(key, value, expirationMillis) { ipMemoryCache[key] = value; if (ipRedisCache != null) { ipRedisCache.set("ip-" + key, value, expirationMillis); } } }; function redirectToConnectPageIfNeeded(req, res) { if (!req.session.host) { req.session.redirectUrl = req.originalUrl; res.redirect("/"); res.end(); return true; } return false; } function hex2ascii(hex) { var str = ""; for (var i = 0; i < hex.length; i += 2) { str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return str; } function splitArrayIntoChunks(array, chunkSize) { var j = array.length; var chunks = []; for (var i = 0; i < j; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } function splitArrayIntoChunksByChunkCount(array, chunkCount) { var bigChunkSize = Math.ceil(array.length / chunkCount); var bigChunkCount = chunkCount - (chunkCount * bigChunkSize - array.length); var chunks = []; var chunkStart = 0; for (var chunk = 0; chunk < chunkCount; chunk++) { var chunkSize = (chunk < bigChunkCount ? bigChunkSize : (bigChunkSize - 1)); chunks.push(array.slice(chunkStart, chunkStart + chunkSize)); chunkStart += chunkSize; } return chunks; } function getRandomString(length, chars) { var mask = ''; if (chars.indexOf('a') > -1) { mask += 'abcdefghijklmnopqrstuvwxyz'; } if (chars.indexOf('A') > -1) { mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; } if (chars.indexOf('#') > -1) { mask += '0123456789'; } if (chars.indexOf('!') > -1) { mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; } var result = ''; for (var i = length; i > 0; --i) { result += mask[Math.floor(Math.random() * mask.length)]; } return result; } 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) { var formatInfo = getCurrencyFormatInfo(formatType); if (formatInfo != null) { var dec = new Decimal(amount); var decimalPlaces = formatInfo.decimalPlaces; //if (decimalPlaces == 0 && dec < 1) { // decimalPlaces = 5; //} if (forcedDecimalPlaces >= 0) { decimalPlaces = forcedDecimalPlaces; } if (formatInfo.type == "native") { dec = dec.times(formatInfo.multiplier); if (forcedDecimalPlaces >= 0) { // toFixed will keep trailing zeroes var baseStr = addThousandsSeparators(dec.toFixed(decimalPlaces)); return {val:baseStr, currencyUnit:formatInfo.name, simpleVal:baseStr}; } else { // toDP will strip trailing zeroes var baseStr = addThousandsSeparators(dec.toDP(decimalPlaces)); var returnVal = {currencyUnit:formatInfo.name, simpleVal:baseStr}; // max digits in "val" var maxValDigits = config.site.valueDisplayMaxLargeDigits; if (baseStr.indexOf(".") == -1) { returnVal.val = baseStr; } else { if (baseStr.length - baseStr.indexOf(".") - 1 > maxValDigits) { returnVal.val = baseStr.substring(0, baseStr.indexOf(".") + maxValDigits + 1); returnVal.lessSignificantDigits = baseStr.substring(baseStr.indexOf(".") + maxValDigits + 1); } else { returnVal.val = baseStr; } } return returnVal; } } else if (formatInfo.type == "exchanged") { if (global.exchangeRates != null && global.exchangeRates[formatInfo.multiplier] != null) { dec = dec.times(global.exchangeRates[formatInfo.multiplier]); var baseStr = addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)); return {val:baseStr, currencyUnit:formatInfo.name, simpleVal:baseStr}; } else { return formatCurrencyAmountWithForcedDecimalPlaces(amount, coinConfig.defaultCurrencyUnit.name, forcedDecimalPlaces); } } } return amount; } function formatCurrencyAmount(amount, formatType) { return formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, -1); } function formatCurrencyAmountInSmallestUnits(amount, forcedDecimalPlaces) { return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].baseCurrencyUnit.name, forcedDecimalPlaces); } // ref: https://stackoverflow.com/a/2901298/673828 function addThousandsSeparators(x) { var parts = x.toString().split("."); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return parts.join("."); } function formatValueInActiveCurrency(amount) { if (global.currencyFormatType && global.exchangeRates[global.currencyFormatType.toLowerCase()]) { return formatExchangedCurrency(amount, global.currencyFormatType); } else { return formatExchangedCurrency(amount, "usd"); } } function satoshisPerUnitOfActiveCurrency() { if (global.currencyFormatType != null && global.exchangeRates != null) { var exchangeType = global.currencyFormatType.toLowerCase(); if (!global.exchangeRates[global.currencyFormatType.toLowerCase()]) { // if current display currency is a native unit, default to USD for exchange values exchangeType = "usd"; } var dec = new Decimal(1); var one = new Decimal(1); dec = dec.times(global.exchangeRates[exchangeType]); // USD/BTC -> BTC/USD dec = one.dividedBy(dec); var unitName = coins[config.coin].baseCurrencyUnit.name; var formatInfo = getCurrencyFormatInfo(unitName); // BTC/USD -> sat/USD dec = dec.times(formatInfo.multiplier); var exchangedAmt = parseInt(dec); if (exchangeType == "eur") { return {amt:addThousandsSeparators(exchangedAmt), unit:`${unitName}/€`}; } else { return {amt:addThousandsSeparators(exchangedAmt), unit:`${unitName}/$`}; } } return null; if (global.currencyFormatType) { return formatExchangedCurrency(amount, global.currencyFormatType); } else { return formatExchangedCurrency(amount, "usd"); } } function formatExchangedCurrency(amount, exchangeType) { if (global.exchangeRates != null && global.exchangeRates[exchangeType.toLowerCase()] != null) { var dec = new Decimal(amount); dec = dec.times(global.exchangeRates[exchangeType.toLowerCase()]); var exchangedAmt = parseFloat(Math.round(dec * 100) / 100).toFixed(2); if (exchangeType == "eur") { return "€" + addThousandsSeparators(exchangedAmt); } else { return "$" + addThousandsSeparators(exchangedAmt); } } return ""; } function seededRandom(seed) { var x = Math.sin(seed++) * 10000; return x - Math.floor(x); } function seededRandomIntBetween(seed, min, max) { var rand = seededRandom(seed); return (min + (max - min) * rand); } function ellipsize(str, length, ending="…") { if (str.length <= length) { return str; } else { return str.substring(0, length - ending.length) + ending; } } function shortenTimeDiff(str) { str = str.replace(" years", "y"); str = str.replace(" year", "y"); str = str.replace(" months", "mo"); str = str.replace(" month", "mo"); str = str.replace(" weeks", "w"); str = str.replace(" week", "w"); str = str.replace(" days", "d"); str = str.replace(" day", "d"); str = str.replace(" hours", "hr"); str = str.replace(" hour", "hr"); str = str.replace(" minutes", "min"); str = str.replace(" minute", "min"); return str; } function logMemoryUsage() { var mbUsed = process.memoryUsage().heapUsed / 1024 / 1024; mbUsed = Math.round(mbUsed * 100) / 100; var mbTotal = process.memoryUsage().heapTotal / 1024 / 1024; mbTotal = Math.round(mbTotal * 100) / 100; //debugLog("memoryUsage: heapUsed=" + mbUsed + ", heapTotal=" + mbTotal + ", ratio=" + parseInt(mbUsed / mbTotal * 100)); } function getMinerFromCoinbaseTx(tx) { if (tx == null || tx.vin == null || tx.vin.length == 0) { return null; } if (global.miningPoolsConfigs) { for (var i = 0; i < global.miningPoolsConfigs.length; i++) { var miningPoolsConfig = global.miningPoolsConfigs[i]; for (var payoutAddress in miningPoolsConfig.payout_addresses) { if (miningPoolsConfig.payout_addresses.hasOwnProperty(payoutAddress)) { if (tx.vout && tx.vout.length > 0 && tx.vout[0].scriptPubKey && tx.vout[0].scriptPubKey.addresses && tx.vout[0].scriptPubKey.addresses.length > 0) { if (tx.vout[0].scriptPubKey.addresses[0] == payoutAddress) { var minerInfo = miningPoolsConfig.payout_addresses[payoutAddress]; minerInfo.identifiedBy = "payout address " + payoutAddress; return minerInfo; } } } } for (var coinbaseTag in miningPoolsConfig.coinbase_tags) { if (miningPoolsConfig.coinbase_tags.hasOwnProperty(coinbaseTag)) { if (hex2ascii(tx.vin[0].coinbase).indexOf(coinbaseTag) != -1) { var minerInfo = miningPoolsConfig.coinbase_tags[coinbaseTag]; minerInfo.identifiedBy = "coinbase tag '" + coinbaseTag + "'"; return minerInfo; } } } for (var blockHash in miningPoolsConfig.block_hashes) { if (blockHash == tx.blockhash) { var minerInfo = miningPoolsConfig.block_hashes[blockHash]; minerInfo.identifiedBy = "known block hash '" + blockHash + "'"; return minerInfo; } } } } return null; } function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { var totalInputValue = new Decimal(0); var totalOutputValue = new Decimal(0); try { for (var i = 0; i < tx.vin.length; i++) { if (tx.vin[i].coinbase) { totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain))); } else { var txInput = txInputs[i]; if (txInput) { try { var vout = txInput; if (vout.value) { totalInputValue = totalInputValue.plus(new Decimal(vout.value)); } } catch (err) { logError("2397gs0gsse", err, {txid:tx.txid, vinIndex:i}); } } } } for (var i = 0; i < tx.vout.length; i++) { totalOutputValue = totalOutputValue.plus(new Decimal(tx.vout[i].value)); } } catch (err) { logError("2308sh0sg44", err, {tx:tx, txInputs:txInputs, blockHeight:blockHeight}); } return {input:totalInputValue, output:totalOutputValue}; } function getBlockTotalFeesFromCoinbaseTxAndBlockHeight(coinbaseTx, blockHeight) { if (coinbaseTx == null) { return 0; } var blockReward = coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain); var totalOutput = new Decimal(0); for (var i = 0; i < coinbaseTx.vout.length; i++) { var outputValue = coinbaseTx.vout[i].value; if (outputValue > 0) { totalOutput = totalOutput.plus(new Decimal(outputValue)); } } return totalOutput.minus(new Decimal(blockReward)); } function refreshExchangeRates() { if (!config.queryExchangeRates || config.privacyMode) { return; } if (coins[config.coin].exchangeRateData) { request(coins[config.coin].exchangeRateData.jsonUrl, function(error, response, body) { if (error == null && response && response.statusCode && response.statusCode == 200) { var responseBody = JSON.parse(body); var exchangeRates = coins[config.coin].exchangeRateData.responseBodySelectorFunction(responseBody); if (exchangeRates != null) { global.exchangeRates = exchangeRates; global.exchangeRatesUpdateTime = new Date(); debugLog("Using exchange rates: " + JSON.stringify(global.exchangeRates) + " starting at " + global.exchangeRatesUpdateTime); } else { debugLog("Unable to get exchange rate data"); } } else { logError("39r7h2390fgewfgds", {error:error, response:response, body:body}); } }); } } // Uses ipstack.com API function geoLocateIpAddresses(ipAddresses, provider) { return new Promise(function(resolve, reject) { if (config.privacyMode || config.credentials.ipStackComApiAccessKey === undefined) { resolve({}); return; } var ipDetails = {ips:ipAddresses, detailsByIp:{}}; var promises = []; for (var i = 0; i < ipAddresses.length; i++) { var ipStr = ipAddresses[i]; promises.push(new Promise(function(resolve2, reject2) { ipCache.get(ipStr).then(function(result) { if (result.value == null) { var apiUrl = "http://api.ipstack.com/" + result.key + "?access_key=" + config.credentials.ipStackComApiAccessKey; debugLog("Requesting IP-geo: " + apiUrl); request(apiUrl, function(error, response, body) { if (error) { reject2(error); } else { resolve2({needToProcess:true, response:response}); } }); } else { ipDetails.detailsByIp[result.key] = result.value; resolve2({needToProcess:false}); } }); })); } Promise.all(promises).then(function(results) { for (var i = 0; i < results.length; i++) { if (results[i].needToProcess) { var res = results[i].response; if (res != null && res["statusCode"] == 200) { var resBody = JSON.parse(res["body"]); var ip = resBody["ip"]; ipDetails.detailsByIp[ip] = resBody; ipCache.set(ip, resBody, 1000 * 60 * 60 * 24 * 365); } } } resolve(ipDetails); }).catch(function(err) { logError("80342hrf78wgehdf07gds", err); reject(err); }); }); } function parseExponentStringDouble(val) { var [lead,decimal,pow] = val.toString().split(/e|\./); return +pow <= 0 ? "0." + "0".repeat(Math.abs(pow)-1) + lead + decimal : lead + ( +pow >= decimal.length ? (decimal + "0".repeat(+pow-decimal.length)) : (decimal.slice(0,+pow)+"."+decimal.slice(+pow))); } function formatLargeNumber(n, decimalPlaces) { for (var i = 0; i < exponentScales.length; i++) { var item = exponentScales[i]; var fraction = new Decimal(n / item.val); if (fraction >= 1) { return [fraction.toDecimalPlaces(decimalPlaces), item]; } } 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); } // https://stackoverflow.com/a/31424853/673828 const reflectPromise = p => p.then(v => ({v, status: "resolved" }), e => ({e, status: "rejected" })); global.errorStats = {}; function logError(errorId, err, optionalUserData = null) { if (!global.errorLog) { global.errorLog = []; } if (!global.errorStats[errorId]) { global.errorStats[errorId] = { count: 0, firstSeen: new Date().getTime() }; } global.errorStats[errorId].count++; global.errorStats[errorId].lastSeen = new Date().getTime(); global.errorLog.push({errorId:errorId, error:err, userData:optionalUserData, date:new Date()}); while (global.errorLog.length > 100) { global.errorLog.splice(0, 1); } debugErrorLog("Error " + errorId + ": " + err + ", json: " + JSON.stringify(err) + (optionalUserData != null ? (", userData: " + optionalUserData + " (json: " + JSON.stringify(optionalUserData) + ")") : "")); if (err && err.stack) { debugErrorVerboseLog("Stack: " + err.stack); } var returnVal = {errorId:errorId, error:err}; if (optionalUserData) { returnVal.userData = optionalUserData; } return returnVal; } 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) { logError("2q3ur8fhudshfs", err, str); reject(err); return; } results[str] = url; resolve(); }); }); } function outputTypeAbbreviation(outputType) { var map = { "pubkey": "p2pk", "pubkeyhash": "p2pkh", "scripthash": "p2sh", "witness_v0_keyhash": "v0_p2wpkh", "witness_v0_scripthash": "v0_p2wsh", "nonstandard": "nonstandard", "nulldata": "nulldata" }; if (map[outputType]) { return map[outputType]; } else { return "???"; } } function outputTypeName(outputType) { var map = { "pubkey": "Pay to Public Key", "pubkeyhash": "Pay to Public Key Hash", "scripthash": "Pay to Script Hash", "witness_v0_keyhash": "Witness, v0 Key Hash", "witness_v0_scripthash": "Witness, v0 Script Hash", "nonstandard": "Non-Standard", "nulldata": "Null Data" }; if (map[outputType]) { return map[outputType]; } else { return "???"; } } module.exports = { reflectPromise: reflectPromise, redirectToConnectPageIfNeeded: redirectToConnectPageIfNeeded, hex2ascii: hex2ascii, splitArrayIntoChunks: splitArrayIntoChunks, splitArrayIntoChunksByChunkCount: splitArrayIntoChunksByChunkCount, getRandomString: getRandomString, getCurrencyFormatInfo: getCurrencyFormatInfo, formatCurrencyAmount: formatCurrencyAmount, formatCurrencyAmountWithForcedDecimalPlaces: formatCurrencyAmountWithForcedDecimalPlaces, formatExchangedCurrency: formatExchangedCurrency, formatValueInActiveCurrency: formatValueInActiveCurrency, satoshisPerUnitOfActiveCurrency: satoshisPerUnitOfActiveCurrency, addThousandsSeparators: addThousandsSeparators, formatCurrencyAmountInSmallestUnits: formatCurrencyAmountInSmallestUnits, seededRandom: seededRandom, seededRandomIntBetween: seededRandomIntBetween, logMemoryUsage: logMemoryUsage, getMinerFromCoinbaseTx: getMinerFromCoinbaseTx, getBlockTotalFeesFromCoinbaseTxAndBlockHeight: getBlockTotalFeesFromCoinbaseTxAndBlockHeight, refreshExchangeRates: refreshExchangeRates, parseExponentStringDouble: parseExponentStringDouble, formatLargeNumber: formatLargeNumber, geoLocateIpAddresses: geoLocateIpAddresses, getTxTotalInputOutputValues: getTxTotalInputOutputValues, rgbToHsl: rgbToHsl, colorHexToRgb: colorHexToRgb, colorHexToHsl: colorHexToHsl, logError: logError, buildQrCodeUrls: buildQrCodeUrls, ellipsize: ellipsize, shortenTimeDiff: shortenTimeDiff, outputTypeAbbreviation: outputTypeAbbreviation, outputTypeName: outputTypeName };