btc-rpc-explorer
Version:
Open-source, self-hosted Bitcoin explorer
1,670 lines (1,292 loc) • 47.2 kB
JavaScript
;
const fs = require("fs");
const debug = require("debug");
const debugLog = debug("btcexp:utils");
const debugErrorLog = debug("btcexp:error");
const debugErrorVerboseLog = debug("btcexp:errorVerbose");
const Decimal = require("decimal.js");
const axios = require("axios");
const qrcode = require("qrcode");
const bs58check = require("bs58check");
const ecc = require('tiny-secp256k1');
const { BIP32Factory } = require('bip32');
const moment = require("moment");
const { bech32, bech32m } = require("bech32");
// You must wrap a tiny-secp256k1 compatible implementation
const bip32 = BIP32Factory(ecc);
const bitcoinjs = require('bitcoinjs-lib');
const config = require("./config.js");
const coins = require("./coins.js");
const coinConfig = coins[config.coin];
const redisCache = require("./redisCache.js");
const statTracker = require("./statTracker.js");
const 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"}
];
const crawlerBotUserAgentStrings = {
"google": new RegExp("adsbot-google|Googlebot|mediapartners-google", "i"),
"microsoft": new RegExp("Bingbot|bingpreview|msnbot", "i"),
"yahoo": new RegExp("Slurp", "i"),
"duckduckgo": new RegExp("DuckDuckBot", "i"),
"baidu": new RegExp("Baidu", "i"),
"yandex": new RegExp("YandexBot", "i"),
"teoma": new RegExp("teoma", "i"),
"sogou": new RegExp("Sogou", "i"),
"exabot": new RegExp("Exabot", "i"),
"facebook": new RegExp("facebot", "i"),
"alexa": new RegExp("ia_archiver", "i"),
"aol": new RegExp("aolbuild", "i"),
"moz": new RegExp("dotbot", "i"),
"semrush": new RegExp("SemrushBot", "i"),
"majestic": new RegExp("MJ12bot", "i"),
"python-requests": new RegExp("python-requests", "i")
};
const ipMemoryCache = {};
let ipRedisCache = null;
if (redisCache.active) {
const onRedisCacheEvent = function(cacheType, eventType, cacheKey) {
global.cacheStats.redis[eventType]++;
//debugLog(`cache.${cacheType}.${eventType}: ${cacheKey}`);
}
ipRedisCache = redisCache.createCache("v0", onRedisCacheEvent);
}
let ipMemoryCacheNewItems = false;
const ipCacheFile = `${config.filesystemCacheDir}/ip-address-cache.json`;
if (fs.existsSync(ipCacheFile)) {
try {
let rawData = fs.readFileSync(ipCacheFile);
ipMemoryCache = JSON.parse(rawData);
debugLog(`Loaded ip address cache (${rawData.length.toLocaleString()} bytes)`);
} catch (err) {
// failed to read cache file, delete it in case it's corrupted
fs.unlinkSync(ipCacheFile);
}
}
setInterval(() => {
if (ipMemoryCacheNewItems) {
try {
if (!fs.existsSync(config.filesystemCacheDir)){
fs.mkdirSync(config.filesystemCacheDir);
}
debugLog(`Saved updated ip address cache`);
fs.writeFileSync(ipCacheFile, JSON.stringify(ipMemoryCache, null, 4));
} catch (e) {
logError("24308tew7hgde", e);
}
ipMemoryCacheNewItems = false;
}
}, 60000);
const 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;
ipMemoryCacheNewItems = true;
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 formatHex(hex, outputFormat="utf8") {
return Buffer.from(hex, "hex").toString(outputFormat);
}
function splitArrayIntoChunks(array, chunkSize) {
let j = array.length;
let chunks = [];
for (let i = 0; i < j; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
function splitArrayIntoChunksByChunkCount(array, chunkCount) {
let bigChunkSize = Math.ceil(array.length / chunkCount);
let bigChunkCount = chunkCount - (chunkCount * bigChunkSize - array.length);
let chunks = [];
let chunkStart = 0;
for (let chunk = 0; chunk < chunkCount; chunk++) {
let chunkSize = (chunk < bigChunkCount ? bigChunkSize : (bigChunkSize - 1));
chunks.push(array.slice(chunkStart, chunkStart + chunkSize));
chunkStart += chunkSize;
}
return chunks;
}
function getRandomString(length, chars) {
let 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 += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\';
}
let result = '';
for (let i = length; i > 0; --i) {
result += mask[Math.floor(Math.random() * mask.length)];
}
return result;
}
function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedDecimalPlaces) {
formatType = formatType.toLowerCase();
let currencyType = global.currencyTypes[formatType];
if (currencyType == null) {
throw `Unknown currency type: ${formatType}`;
}
let dec = new Decimal(amount);
let decimalPlaces = currencyType.decimalPlaces;
//if (decimalPlaces == 0 && dec < 1) {
// decimalPlaces = 5;
//}
if (forcedDecimalPlaces >= 0) {
decimalPlaces = forcedDecimalPlaces;
}
if (currencyType.type == "native") {
dec = dec.times(currencyType.multiplier);
if (forcedDecimalPlaces >= 0) {
// toFixed will keep trailing zeroes
let baseStr = addThousandsSeparators(dec.toFixed(decimalPlaces));
return {val:baseStr, currencyUnit:currencyType.name, simpleVal:baseStr, intVal:parseInt(dec)};
} else {
// toDP excludes trailing zeroes but doesn't "fix" numbers like 1e-8
// instead, we use toFixed and (optionally) manually strip trailing zeroes
// old method is kept for reference since this is sensitive, high-volume code
let baseStr = addThousandsSeparators(dec.toFixed(decimalPlaces).replace(/\.$/, ""));
// with Issue #500, the idea was raised that stripping trailing zeroes can
// make values more difficult to parse visually; now the stripping is
// dynamic, based on the value - if any of the 4 least-significant digits
// are non-zero (i.e. sat-value is NOT evenly divisible by 10,000), then
// no stripping is performed, otherwise it is performed, to preserve some
// of the UX benefit of larger, "even" amounts (e.g. 0.1BTC).
let trailingZeroesStrippedStr = baseStr.replace(/0+$/, "");
if (baseStr.length - trailingZeroesStrippedStr.length >= 4) {
baseStr = trailingZeroesStrippedStr
if (baseStr.endsWith(".")) {
baseStr = baseStr.slice(0, -1);
}
}
//let baseStr = addThousandsSeparators(dec.toDP(decimalPlaces)); // old version, failed to properly format "1e-8" (left unchanged)
let returnVal = {currencyUnit:currencyType.name, simpleVal:baseStr, intVal:parseInt(dec)};
// max digits in "val"
let maxValDigits = config.site.valueDisplayMaxLargeDigits;
// todo: make this section locale-aware (don't hardcode ".")
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 (currencyType.type == "exchanged") {
//console.log(JSON.stringify(global.exchangeRates) + " - " + currencyType.name);
if (global.exchangeRates != null && global.exchangeRates[currencyType.id] != null) {
dec = dec.times(global.exchangeRates[currencyType.id]);
let baseStr = addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces));
return {val:baseStr, currencyUnit:currencyType.name, simpleVal:baseStr, intVal:parseInt(dec)};
} else {
return formatCurrencyAmountWithForcedDecimalPlaces(amount, coinConfig.defaultCurrencyUnit.name, forcedDecimalPlaces);
}
} else {
throw `Unknown currency type: ${currencyType.type}`;
}
}
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) {
let parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
function satoshisPerUnitOfLocalCurrency(localCurrency) {
if (global.exchangeRates != null) {
let exchangeType = localCurrency;
if (!global.exchangeRates[localCurrency]) {
// if current display currency is a native unit, default to USD for exchange values
exchangeType = "usd";
}
let dec = new Decimal(1);
let one = new Decimal(1);
dec = dec.times(global.exchangeRates[exchangeType]);
// USD/BTC -> BTC/USD
dec = one.dividedBy(dec);
let unitName = coins[config.coin].baseCurrencyUnit.name;
let satCurrencyType = global.currencyTypes["sat"];
let localCurrencyType = global.currencyTypes[localCurrency];
// BTC/USD -> sat/USD
dec = dec.times(satCurrencyType.multiplier);
let exchangedAmt = parseInt(dec);
return {amt:addThousandsSeparators(exchangedAmt),amtRaw:exchangedAmt, unit:`sat/${localCurrencyType.symbol}`}
}
return null;
}
function getExchangedCurrencyFormatData(amount, exchangeType, includeUnit=true) {
if (global.exchangeRates != null && global.exchangeRates[exchangeType.toLowerCase()] != null) {
let dec = new Decimal(amount);
dec = dec.times(global.exchangeRates[exchangeType.toLowerCase()]);
let exchangedAmt = parseFloat(Math.round(dec * 100) / 100).toFixed(2);
return {
symbol: global.currencySymbols[exchangeType],
value: addThousandsSeparators(exchangedAmt),
unit: exchangeType
}
} else if (exchangeType == "au") {
if (global.exchangeRates != null && global.goldExchangeRates != null) {
let dec = new Decimal(amount);
dec = dec.times(global.exchangeRates.usd).dividedBy(global.goldExchangeRates.usd);
let exchangedAmt = parseFloat(Math.round(dec * 100) / 100).toFixed(2);
return {
symbol: "AU",
value: addThousandsSeparators(exchangedAmt),
unit: "oz"
}
}
}
return "";
}
function formatExchangedCurrency(amount, exchangeType, decimals=2) {
if (global.exchangeRates != null && global.exchangeRates[exchangeType.toLowerCase()] != null) {
let dec = new Decimal(amount);
dec = dec.times(global.exchangeRates[exchangeType.toLowerCase()]);
let exchangedAmt = parseFloat(Math.round(dec * 100) / 100).toFixed(decimals);
return {
val: addThousandsSeparators(exchangedAmt),
symbol: global.currencyTypes[exchangeType].symbol,
unit: exchangeType,
valRaw: exchangedAmt
};
} else if (exchangeType == "au") {
if (global.exchangeRates != null && global.goldExchangeRates != null) {
let dec = new Decimal(amount);
dec = dec.times(global.exchangeRates.usd).dividedBy(global.goldExchangeRates.usd);
let exchangedAmt = parseFloat(Math.round(dec * 100) / 100).toFixed(decimals);
return {
val: addThousandsSeparators(exchangedAmt),
unit: "oz",
symbol: "AU",
valRaw: exchangedAmt
};
}
}
return "";
}
function seededRandom(seed) {
let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
function seededRandomIntBetween(seed, min, max) {
let rand = seededRandom(seed);
return (min + (max - min) * rand);
}
function randomInt(min, max) {
return min + Math.floor(Math.random() * max);
}
function ellipsize(str, length, ending="…") {
if (str.length <= length) {
return str;
} else {
return str.substring(0, length - ending.length) + ending;
}
}
function ellipsizeMiddle(str, length, replacement="…", extraCharAtStart=true) {
if (str.length <= length) {
return str;
} else {
//"abcde"(3)->"a…e"
//"abcdef"(3)->"a…f"
//"abcdef"(5)->"ab…ef"
//"abcdef"(4)->"ab…f"
if ((length - replacement.length) % 2 == 0) {
return str.substring(0, (length - replacement.length) / 2) + replacement + str.slice(-(length - replacement.length) / 2);
} else {
if (extraCharAtStart) {
return str.substring(0, Math.ceil((length - replacement.length) / 2)) + replacement + str.slice(-Math.floor((length - replacement.length) / 2));
} else {
return str.substring(0, Math.floor((length - replacement.length) / 2)) + replacement + str.slice(-Math.ceil((length - replacement.length) / 2));
}
}
}
}
// options:
// - oneElement (default: false)
// - stripZeroes (default: true)
// - shortenDurationNames (default: true)
// - outputCommas (default: true)
function summarizeDuration(duration, options={}) {
let oneElement = "oneElement" in options ? options.oneElement : false;
let stripZeroes = "stripZeroes" in options ? options.stripZeroes : true;
let shortenDurationNames = "shortenDurationNames" in options ? options.shortenDurationNames : true;
let outputCommas = "outputCommas" in options ? options.outputCommas : true;
//console.log(JSON.stringify(options) + " - " + oneElement + " - " + stripZeroes + " - " + shortenDurationNames + " - " + outputCommas);
let formatParts = duration.format().split(",").map(x => x.trim());
let str = formatParts.join(", ");
if (oneElement) {
let parts = [duration.asYears(), duration.asMonths(), duration.asWeeks(), duration.asDays(), duration.asHours(), duration.asMinutes(), duration.asSeconds()];
let partNames = ["years", "months", "weeks", "days", "hours", "minutes", "seconds"];
for (let i = 0; i < parts.length; i++) {
if (parts[i] > 1) {
str = `${new Decimal(parts[i]).toDP(1)} ${partNames[i]}`;
break;
}
}
} else if (stripZeroes) {
// strip duration elements with zero magnitude (e.g. 11 months 0 days 12 hours)
formatParts = formatParts.map(x => { return x.startsWith("0 ") ? "" : x; }).filter(x => x.length > 0);
// hack: moment.js seems to have a bug where there can be formatted items that include "-0" magnitude elements
formatParts = formatParts.map(x => { return x.startsWith("-0 ") ? "" : x; }).filter(x => x.length > 0);
str = formatParts.join(", ");
}
if (shortenDurationNames) {
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");
}
if (!outputCommas) {
str = str.split(", ").join(" ");
}
return str;
}
function logMemoryUsage() {
let mbUsed = process.memoryUsage().heapUsed / 1024 / 1024;
mbUsed = Math.round(mbUsed * 100) / 100;
let mbTotal = process.memoryUsage().heapTotal / 1024 / 1024;
mbTotal = Math.round(mbTotal * 100) / 100;
//debugLog("memoryUsage: heapUsed=" + mbUsed + ", heapTotal=" + mbTotal + ", ratio=" + parseInt(mbUsed / mbTotal * 100));
}
function identifyMiner(coinbaseTx, blockHeight) {
if (coinbaseTx == null || coinbaseTx.vin == null || coinbaseTx.vin.length == 0) {
return null;
}
if (global.miningPoolsConfigs) {
for (let i = 0; i < global.miningPoolsConfigs.length; i++) {
let miningPoolsConfig = global.miningPoolsConfigs[i];
for (let payoutAddress in miningPoolsConfig.payout_addresses) {
if (miningPoolsConfig.payout_addresses.hasOwnProperty(payoutAddress)) {
if (coinbaseTx.vout && coinbaseTx.vout.length > 0) {
if (getVoutAddresses(coinbaseTx.vout[0]).includes(payoutAddress)) {
let minerInfo = miningPoolsConfig.payout_addresses[payoutAddress];
minerInfo.identifiedBy = "payout address " + payoutAddress;
return minerInfo;
}
}
}
}
for (let coinbaseTag in miningPoolsConfig.coinbase_tags) {
if (miningPoolsConfig.coinbase_tags.hasOwnProperty(coinbaseTag)) {
if (formatHex(coinbaseTx.vin[0].coinbase, "utf8").indexOf(coinbaseTag) != -1) {
let minerInfo = miningPoolsConfig.coinbase_tags[coinbaseTag];
minerInfo.identifiedBy = "coinbase tag '" + coinbaseTag + "'";
return minerInfo;
}
}
}
for (let blockHash in miningPoolsConfig.block_hashes) {
if (blockHash == coinbaseTx.blockhash) {
let minerInfo = miningPoolsConfig.block_hashes[blockHash];
minerInfo.identifiedBy = "known block hash '" + blockHash + "'";
return minerInfo;
}
}
if (global.activeBlockchain == "main" && miningPoolsConfig.block_heights) {
for (let minerName in miningPoolsConfig.block_heights) {
let minerInfo = miningPoolsConfig.block_heights[minerName];
minerInfo.name = minerName;
if (minerInfo.heights.includes(blockHeight)) {
minerInfo.identifiedBy = "known block height #" + blockHeight;
return minerInfo;
}
}
}
}
}
if (coinbaseTx.vout && coinbaseTx.vout.length > 0) {
for (let i = 0; i < coinbaseTx.vout.length; i++) {
const vout = coinbaseTx.vout[i];
const voutValue = new Decimal(vout.value);
if (voutValue > 0) {
const address = getVoutAddress(vout);
if (address) {
return {
name: address,
type: "address-only",
identifiedBy: "payout address " + address,
};
}
}
}
}
return null;
}
function getTxTotalInputOutputValues(tx, txInputs, blockHeight) {
let totalInputValue = new Decimal(0);
let totalOutputValue = new Decimal(0);
try {
if (txInputs) {
for (let i = 0; i < tx.vin.length; i++) {
if (tx.vin[i].coinbase) {
totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain)));
} else {
let txInput = txInputs[i];
if (txInput) {
try {
let vout = txInput;
if (vout.value) {
totalInputValue = totalInputValue.plus(new Decimal(vout.value));
}
} catch (err) {
logError("2397gs0gsse", err, {txid:tx.txid, vinIndex:i});
}
}
}
}
} else {
totalInputValue = null
}
for (let 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;
}
let blockReward = coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain);
let totalOutput = new Decimal(0);
for (let i = 0; i < coinbaseTx.vout.length; i++) {
let outputValue = coinbaseTx.vout[i].value;
if (outputValue > 0) {
totalOutput = totalOutput.plus(new Decimal(outputValue));
}
}
if (blockReward < 1e-8 || blockReward == null) {
return totalOutput;
} else {
return totalOutput.minus(new Decimal(blockReward));
}
}
function estimatedSupply(height) {
const checkpoint = coinConfig.utxoSetCheckpointsByNetwork[global.activeBlockchain];
let checkpointHeight = 0;
let checkpointSupply = new Decimal(50);
if (checkpoint && checkpoint.height <= height) {
//console.log("using checkpoint");
checkpointHeight = checkpoint.height;
checkpointSupply = new Decimal(checkpoint.total_amount);
}
let halvingBlockInterval = coinConfig.halvingBlockIntervalsByNetwork[global.activeBlockchain];
let supply = checkpointSupply;
let i = checkpointHeight;
while (i < height) {
let nextHalvingHeight = halvingBlockInterval * Math.floor(i / halvingBlockInterval) + halvingBlockInterval;
if (height < nextHalvingHeight) {
let heightDiff = height - i;
//console.log(`adding(${heightDiff}): ` + new Decimal(heightDiff).times(coinConfig.blockRewardFunction(i, global.activeBlockchain)));
return supply.plus(new Decimal(heightDiff).times(coinConfig.blockRewardFunction(i, global.activeBlockchain)));
}
let heightDiff = nextHalvingHeight - i;
supply = supply.plus(new Decimal(heightDiff).times(coinConfig.blockRewardFunction(i, global.activeBlockchain)));
i += heightDiff;
}
return supply;
}
async function refreshExchangeRates() {
if (!config.queryExchangeRates) {
return;
}
if (coins[config.coin].exchangeRateData) {
try {
const response = await axios.get(coins[config.coin].exchangeRateData.jsonUrl);
let exchangeRates = coins[config.coin].exchangeRateData.responseBodySelectorFunction(response.data);
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");
}
} catch (err) {
logError("39r7h2390fgewfgds", err);
}
}
if (coins[config.coin].goldExchangeRateData) {
if (process.env.NODE_ENV == "local") {
global.goldExchangeRates = {usd: 1731.2};
global.goldExchangeRatesUpdateTime = new Date();
debugLog("Using DEBUG gold exchange rates: " + JSON.stringify(global.goldExchangeRates) + " starting at " + global.goldExchangeRatesUpdateTime);
} else {
try {
const response = await axios.get(coins[config.coin].goldExchangeRateData.jsonUrl);
let exchangeRates = coins[config.coin].goldExchangeRateData.responseBodySelectorFunction(response.data);
if (exchangeRates != null) {
global.goldExchangeRates = exchangeRates;
global.goldExchangeRatesUpdateTime = new Date();
debugLog("Using gold exchange rates: " + JSON.stringify(global.goldExchangeRates) + " starting at " + global.goldExchangeRatesUpdateTime);
} else {
debugLog("Unable to get gold exchange rate data");
}
} catch (err) {
logError("34082yt78yewewe", err);
}
}
}
}
// Uses ipstack.com API
function geoLocateIpAddresses(ipAddresses, provider) {
return new Promise(function(resolve, reject) {
if (config.privacyMode || config.credentials.ipStackComApiAccessKey === undefined) {
resolve({});
return;
}
let ipDetails = {ips:ipAddresses, detailsByIp:{}};
let promises = [];
for (let i = 0; i < ipAddresses.length; i++) {
let ipStr = ipAddresses[i];
if (ipStr.endsWith(".onion")) {
// tor, no location possible
continue;
}
if (ipStr == "127.0.0.1") {
// skip
continue;
}
if (!ipStr.match(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/)) {
// non-IPv4, skip it
continue;
}
promises.push(new Promise(function(resolve2, reject2) {
ipCache.get(ipStr).then(async function(result) {
if (result.value == null) {
let apiUrl = "http://api.ipstack.com/" + result.key + "?access_key=" + config.credentials.ipStackComApiAccessKey;
try {
const response = await axios.get(apiUrl);
let ip = response.data.ip;
ipDetails.detailsByIp[ip] = response.data;
if (response.data.latitude && response.data.longitude) {
debugLog(`Successful IP-geo-lookup: ${ip} -> (${response.data.latitude}, ${response.data.longitude})`);
} else {
debugLog(`Unknown location for IP-geo-lookup: ${ip}`);
}
ipCache.set(ip, response.data, 1000 * 60 * 60 * 24 * 365);
resolve2();
} catch (err) {
debugLog("Failed IP-geo-lookup: " + result.key);
logError("39724gdge33a", err, {ip: result.key});
// we failed to get what we wanted, but there's no meaningful recourse,
// so we log the failure and continue without objection
resolve2();
}
} else {
ipDetails.detailsByIp[result.key] = result.value;
resolve2();
}
});
}));
}
Promise.all(promises).then(function(results) {
resolve(ipDetails);
}).catch(function(err) {
logError("80342hrf78wgehdf07gds", err);
reject(err);
});
});
}
function parseExponentStringDouble(val) {
let [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) {
try {
for (let i = 0; i < exponentScales.length; i++) {
let item = exponentScales[i];
let fraction = new Decimal(n / item.val);
if (Math.abs(fraction) >= 1) {
return [fraction.toDP(decimalPlaces), item];
}
}
return [new Decimal(n).toDP(decimalPlaces), {}];
} catch (err) {
logError("ru92huefhew", err, { n:n, decimalPlaces:decimalPlaces });
throw err;
}
}
function formatLargeNumberSignificant(n, significantDigits) {
try {
for (let i = 0; i < exponentScales.length; i++) {
let item = exponentScales[i];
let fraction = new Decimal(n / item.val);
if (Math.abs(fraction) >= 1) {
return [fraction.toDP(Math.max(0, significantDigits - `${Math.floor(fraction)}`.length)), item];
}
}
return [new Decimal(n).toDP(significantDigits), {}];
} catch (err) {
logError("38fhcdugdeogwe", err, { n:n, significantDigits:significantDigits });
throw err;
}
}
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
let 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")
let 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;
});
let 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) {
let 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 = {}, logStacktrace=true) {
debugErrorLog("Error " + errorId + ": " + err + ", json: " + JSON.stringify(err) + (optionalUserData != null ? (", userData: " + optionalUserData + " (json: " + JSON.stringify(optionalUserData) + ")") : ""));
if (err && err.stack && logStacktrace) {
debugErrorVerboseLog("Stack: " + err.stack);
}
if (!global.errorLog) {
global.errorLog = [];
}
if (!global.errorStats[errorId]) {
global.errorStats[errorId] = {
count: 0,
firstSeen: new Date().getTime(),
properties: {}
};
}
if (optionalUserData && err.message) {
optionalUserData.errorMsg = err.message;
}
if (optionalUserData) {
for (const [key, value] of Object.entries(optionalUserData)) {
if (!global.errorStats[errorId].properties[key]) {
global.errorStats[errorId].properties[key] = {};
}
if (!global.errorStats[errorId].properties[key][value]) {
global.errorStats[errorId].properties[key][value] = 0;
}
global.errorStats[errorId].properties[key][value]++;
}
}
statTracker.trackEvent(`errors.${errorId}`);
statTracker.trackEvent(`errors.*`);
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);
}
let returnVal = {errorId:errorId, error:err};
if (optionalUserData) {
returnVal.userData = optionalUserData;
}
return returnVal;
}
function buildQrCodeUrls(strings) {
return new Promise(function(resolve, reject) {
let promises = [];
let qrcodeUrls = {};
for (let 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) {
const map = {
"pubkey": "P2PK",
"multisig": "P2MS",
"pubkeyhash": "P2PKH",
"scripthash": "P2SH",
"witness_v0_keyhash": "P2WPKH",
"witness_v0_scripthash": "P2WSH",
"witness_v1_taproot": "P2TR",
"nonstandard": "nonstandard",
"nulldata": "nulldata"
};
if (map[outputType]) {
return map[outputType];
} else {
return "???";
}
}
function outputTypeName(outputType) {
const map = {
"pubkey": "Pay to Public Key",
"multisig": "Pay to MultiSig",
"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",
"witness_v1_taproot": "Witness, v1 Taproot",
"nonstandard": "Non-Standard",
"nulldata": "Null Data"
};
if (map[outputType]) {
return map[outputType];
} else {
return "???";
}
}
function asHash(value) {
return value.replace(/[^a-f0-9]/gi, "");
}
function asHashOrHeight(value) {
return +value || asHash(value);
}
function asAddress(value) {
return value.replace(/[^a-z0-9]/gi, "");
}
const arrayFromHexString = hexString =>
new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const getCrawlerFromUserAgentString = userAgentString => {
for (const [name, regex] of Object.entries(crawlerBotUserAgentStrings)) {
if (regex.test(userAgentString)) {
return name;
}
}
return null;
};
const safePromise = async (uid, promise) => {
try {
const response = await promise();
return response;
} catch (e) {
logError(uid, e);
}
};
const timePromise = async (name, promise, perfResults=null) => {
const startTime = startTimeNanos();
try {
const response = await promise();
const responseTimeMillis = dtMillis(startTime);
statTracker.trackPerformance(name, responseTimeMillis);
if (perfResults) {
perfResults[name] = Math.max(1, parseInt(responseTimeMillis));
}
return response;
} catch (e) {
const responseTimeMillis = dtMillis(startTime);
statTracker.trackPerformance(`${name}_error`, responseTimeMillis);
if (perfResults) {
perfResults[`${name}_error`] = Math.max(1, parseInt(responseTimeMillis));
}
throw e;
}
};
const timeFunction = (uid, f, perfResults=null) => {
const startTime = startTimeNanos();
f();
const responseTimeMillis = dtMillis(startTime);
statTracker.trackPerformance(uid, responseTimeMillis);
if (perfResults) {
perfResults[uid] = responseTimeMillis;
}
};
const fileCache = (cacheDir, cacheName, cacheVersion=1) => {
const filename = (version) => { return ((version > 1) ? [cacheName, `v${version}`].join("-") : cacheName) + ".json"; };
const filepath = `${cacheDir}/${filename(cacheVersion)}`;
if (cacheVersion > 1) {
// remove old versions
for (let i = 1; i < cacheVersion; i++) {
if (fs.existsSync(`${cacheDir}/${filename(i)}`)) {
fs.unlinkSync(`${cacheDir}/${filename(i)}`);
}
}
}
return {
tryLoadJson: () => {
if (fs.existsSync(filepath)) {
let rawData = fs.readFileSync(filepath);
try {
return JSON.parse(rawData);
} catch (e) {
logError("378y43edewe", e);
fs.unlinkSync(filepath);
return null;
}
}
return null;
},
writeJson: (obj) => {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
fs.writeFileSync(filepath, JSON.stringify(obj, null, 4));
}
};
};
const startTimeNanos = () => {
return process.hrtime.bigint();
};
const dtMillis = (startTimeNanos) => {
const dtNanos = process.hrtime.bigint() - startTimeNanos;
return parseInt(dtNanos) * 1e-6;
};
function objectProperties(obj) {
const props = [];
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
props.push(prop);
}
}
return props;
}
function objHasProperty(obj, name) {
return Object.prototype.hasOwnProperty.call(obj, name);
}
function iterateProperties(obj, action) {
for (const [key, value] of Object.entries(obj)) {
action([key, value]);
}
}
function stringifySimple(object) {
let simpleObject = {};
for (let prop in object) {
if (!object.hasOwnProperty(prop)) {
continue;
}
if (typeof(object[prop]) == 'object') {
continue;
}
if (typeof(object[prop]) == 'function') {
continue;
}
simpleObject[prop] = object[prop];
}
return JSON.stringify(simpleObject); // returns cleaned up JSON
}
function getVoutAddress(vout) {
if (vout && vout.scriptPubKey) {
if (vout.scriptPubKey.address) {
return vout.scriptPubKey.address;
} else if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.length > 0) {
return vout.scriptPubKey.addresses[0];
}
}
return null;
}
function getVoutAddresses(vout) {
if (vout && vout.scriptPubKey) {
if (vout.scriptPubKey.address) {
return [vout.scriptPubKey.address];
} else if (vout.scriptPubKey.addresses) {
return vout.scriptPubKey.addresses;
}
}
return [];
}
const xpubPrefixes = new Map([
['xpub', '0488b21e'],
['ypub', '049d7cb2'],
['Ypub', '0295b43f'],
['zpub', '04b24746'],
['Zpub', '02aa7ed3'],
['tpub', '043587cf'],
['upub', '044a5262'],
['Upub', '024289ef'],
['vpub', '045f1cf6'],
['Vpub', '02575483'],
]);
const bip32TestnetNetwork = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'tb',
bip32: {
public: 0x043587cf,
private: 0x04358394,
},
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xEF,
};
// ref: https://github.com/ExodusMovement/xpub-converter/blob/master/src/index.js
function xpubChangeVersionBytes(xpub, targetFormat) {
if (!xpubPrefixes.has(targetFormat)) {
throw new Error("Invalid target version");
}
// trim whitespace
xpub = xpub.trim();
let data = bs58check.decode(xpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from(xpubPrefixes.get(targetFormat), 'hex'), data]);
return bs58check.encode(data);
}
// HD wallet addresses
function bip32Addresses(extPubkey, addressType, account, limit=10, offset=0) {
let network = null;
if (!extPubkey.match(/^(xpub|ypub|zpub|Ypub|Zpub).*$/)) {
network = bip32TestnetNetwork;
}
let bip32object = bip32.fromBase58(extPubkey, network);
let addresses = [];
for (let i = offset; i < (offset + limit); i++) {
let bip32Child = bip32object.derive(account).derive(i);
let publicKey = bip32Child.publicKey;
if (addressType == "p2pkh") {
addresses.push(bitcoinjs.payments.p2pkh({ pubkey: publicKey, network: network }).address);
} else if (addressType == "p2sh(p2wpkh)") {
addresses.push(bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: publicKey, network: network })}).address);
} else if (addressType == "p2wpkh") {
addresses.push(bitcoinjs.payments.p2wpkh({ pubkey: publicKey, network: network }).address);
} else {
throw new Error(`Unknown address type: "${addressType}" (should be one of ["p2pkh", "p2sh(p2wpkh)", "p2wpkh"])`)
}
}
return addresses;
}
function difficultyAdjustmentEstimates(eraStartBlockHeader, currentBlockHeader) {
let difficultyPeriod = parseInt(Math.floor(currentBlockHeader.height / coinConfig.difficultyAdjustmentBlockCount));
let blocksUntilDifficultyAdjustment = ((difficultyPeriod + 1) * coinConfig.difficultyAdjustmentBlockCount) - currentBlockHeader.height;
let heightDiff = currentBlockHeader.height - eraStartBlockHeader.height;
let blockCount = heightDiff + 1;
let timeDiff = currentBlockHeader.mediantime - eraStartBlockHeader.mediantime;
let timePerBlock = timeDiff / heightDiff;
let timePerBlockDuration = moment.duration(timePerBlock * 1000);
let daysUntilAdjustment = new Decimal(blocksUntilDifficultyAdjustment).times(timePerBlock).dividedBy(60 * 60 * 24);
let hoursUntilAdjustment = new Decimal(blocksUntilDifficultyAdjustment).times(timePerBlock).dividedBy(60 * 60);
let duaDP1 = daysUntilAdjustment.toDP(1);
let daysUntilAdjustmentStr = daysUntilAdjustment > 1 ? `~${duaDP1} day${duaDP1 == "1" ? "" : "s"}` : "< 1 day";
let hoursUntilAdjustmentStr = hoursUntilAdjustment > 1 ? `~${hoursUntilAdjustment.toDP(0)} hr${hoursUntilAdjustment.toDP(1) == "1" ? "" : "s"}` : "< 1 hr";
let nowTime = new Date().getTime() / 1000;
let dt = nowTime - eraStartBlockHeader.time;
let timePerBlock2 = dt / heightDiff;
let predictedBlockCount = dt / coinConfig.targetBlockTimeSeconds;
let blockRatioPercent = new Decimal(blockCount / predictedBlockCount).times(100);
if (blockRatioPercent > 400) {
blockRatioPercent = new Decimal(400);
}
if (blockRatioPercent < 25) {
blockRatioPercent = new Decimal(25);
}
let diffAdjPercent = blockRatioPercent.minus(new Decimal(100));
let diffAdjText = `Blocks during the current difficulty epoch have taken this long, on average, to be mined. If this pace continues, then in ${blocksUntilDifficultyAdjustment.toLocaleString()} block${blocksUntilDifficultyAdjustment == 1 ? "" : "s"} (${daysUntilAdjustmentStr}) the difficulty will adjust upward: +${diffAdjPercent.toDP(1)}%`;
let diffAdjSign = "+";
let textColorClass = "text-success";
if (predictedBlockCount > blockCount) {
diffAdjPercent = new Decimal(100).minus(blockRatioPercent).times(-1);
diffAdjText = `Blocks during the current difficulty epoch have taken this long, on average, to be mined. If this pace continues, then in ${blocksUntilDifficultyAdjustment.toLocaleString()} block${blocksUntilDifficultyAdjustment == 1 ? "" : "s"} (${daysUntilAdjustmentStr}) the difficulty will adjust downward: -${diffAdjPercent.toDP(1)}%`;
diffAdjSign = "-";
textColorClass = "text-danger";
}
return {
estimateAvailable: blockCount > 30 && !isNaN(diffAdjPercent),
blockCount: blockCount,
blocksLeft: blocksUntilDifficultyAdjustment,
daysLeftStr: daysUntilAdjustmentStr,
timeLeftStr: (daysUntilAdjustment < 1 ? hoursUntilAdjustmentStr : daysUntilAdjustmentStr),
calculationBlockCount: heightDiff,
currentEpoch: difficultyPeriod,
delta: diffAdjPercent,
sign: diffAdjSign,
timePerBlock: timePerBlock,
firstBlockTime: eraStartBlockHeader.time,
nowTime: nowTime,
dt: dt,
predictedBlockCount: predictedBlockCount,
//nameDesc: `Estimate for the difficulty adjustment that will occur in ${blocksUntilDifficultyAdjustment.toLocaleString()} block${blocksUntilDifficultyAdjustment == 1 ? "" : "s"} (${daysUntilAdjustmentStr}). This is calculated using the average block time over the last ${heightDiff} block(s). This estimate becomes more reliable as the difficulty epoch nears its end.`,
};
}
function nextHalvingEstimates(eraStartBlockHeader, currentBlockHeader, difficultyAdjustmentDataArg=null) {
let blockCount = currentBlockHeader.height;
let halvingBlockInterval = coinConfig.halvingBlockIntervalsByNetwork[global.activeBlockchain];
let halvingCount = parseInt(blockCount / halvingBlockInterval);
let nextHalvingIndex = halvingCount + 1;
let targetBlockTimeSeconds = coinConfig.targetBlockTimeSeconds;
let nextHalvingBlock = (halvingBlockInterval * nextHalvingIndex);
let blocksUntilNextHalving = nextHalvingBlock - blockCount;
let terminalHalvingCount = coinConfig.terminalHalvingCountByNetwork[global.activeBlockchain];
if (nextHalvingIndex > terminalHalvingCount) {
halvingCount = terminalHalvingCount;
nextHalvingIndex = -1;
return {
halvingCount: terminalHalvingCount,
nextHalvingIndex: -1
};
}
let difficultyAdjustmentData = difficultyAdjustmentDataArg;
if (!difficultyAdjustmentData) {
difficultyAdjustmentData = difficultyAdjustmentEstimates(eraStartBlockHeader, currentBlockHeader);
}
let currDifficultyEraTimeDifferential = (coinConfig.targetBlockTimeSeconds - difficultyAdjustmentData.timePerBlock) * difficultyAdjustmentData.blocksLeft;
let secondsUntilNextHalving = blocksUntilNextHalving * targetBlockTimeSeconds - currDifficultyEraTimeDifferential;
let daysUntilNextHalving = secondsUntilNextHalving / 60 / 60 / 24;
let nextHalvingDate = new Date(new Date().getTime() + secondsUntilNextHalving * 1000);
return {
blockCount: blockCount,
halvingBlockInterval: halvingBlockInterval,
halvingCount: halvingCount,
nextHalvingIndex: nextHalvingIndex,
terminalHalvingCount: terminalHalvingCount,
nextHalvingBlock: nextHalvingBlock,
blocksUntilNextHalving: blocksUntilNextHalving,
targetBlockTimeSeconds: targetBlockTimeSeconds,
daysUntilNextHalving: daysUntilNextHalving,
nextHalvingDate: nextHalvingDate,
difficultyAdjustmentData: difficultyAdjustmentData
};
}
function tryParseAddress(address) {
let base58Error = null;
let bech32Error = null;
let bech32mError = null;
let parsedAddress = null;
let b58prefix = (global.activeBlockchain == "main" ? /^[13].*$/ : /^[2mn].*$/);
if (address.match(b58prefix)) {
try {
parsedAddress = bitcoinjs.address.fromBase58Check(address);
parsedAddress.hash = parsedAddress.hash.toString("hex");
return {
encoding: "base58",
parsedAddress: parsedAddress
};
} catch (err) {
base58Error = err;
}
}
try {
parsedAddress = bitcoinjs.address.fromBech32(address);
parsedAddress.data = parsedAddress.data.toString("hex");
return {
encoding: "bech32",
parsedAddress: parsedAddress
};
} catch (err) {
bech32Error = err;
}
try {
parsedAddress = bech32m.decode(address);
parsedAddress.words = Buffer.from(parsedAddress.words).toString("hex");
return {
encoding: "bech32m",
parsedAddress: parsedAddress
};
} catch (err) {
bech32mError = err;
}
let returnVal = {errors:[]};
if (base58Error) {
returnVal.errors.push(base58Error);
}
if (bech32Error) {
returnVal.errors.push(bech32Error);
}
if (bech32mError) {
returnVal.errors.push(bech32mError);
}
return returnVal;
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const awaitPromises = async (promises) => {
const promiseResults = await Promise.allSettled(promises);
promiseResults.forEach(x => {
if (x.status == "rejected") {
if (x.reason) {
logError("awaitPromises_rejected", x.reason);
}
}
});
return promiseResults;
};
const obfuscateProperties = (obj, properties) => {
if (process.env.BTCEXP_SKIP_LOG_OBFUSCATION) {
return obj;
}
let objCopy = Object.assign({}, obj);
properties.forEach(name => {
objCopy[name] = "*****";
});
return objCopy;
}
const perfLog = [];
let perfLogItemCount = 0;
const perfLogMaxItems = 100;
const perfLogNewItem = (tags) => {
const newItem = tags;
newItem.id = getRandomString(12, "aA#");
newItem.date = new Date();
newItem.results = {};
newItem.index = perfLogItemCount;
perfLogItemCount++;
perfLog.splice(0, 0, newItem);
while (perfLog.length > perfLogMaxItems) {
perfLog.splice(perfLog.length - 1, 1);
}
return {
perfId:newItem.id,
perfResults:newItem.results
};
};
module.exports = {
reflectPromise: reflectPromise,
redirectToConnectPageIfNeeded: redirectToConnectPageIfNeeded,
formatHex: formatHex,
splitArrayIntoChunks: splitArrayIntoChunks,
splitArrayIntoChunksByChunkCount: splitArrayIntoChunksByChunkCount,
getRandomString: getRandomString,
formatCurrencyAmount: formatCurrencyAmount,
formatCurrencyAmountWithForcedDecimalPlaces: formatCurrencyAmountWithForcedDecimalPlaces,
formatExchangedCurrency: formatExchangedCurrency,
getExchangedCurrencyFormatData: getExchangedCurrencyFormatData,
satoshisPerUnitOfLocalCurrency: satoshisPerUnitOfLocalCurrency,
addThousandsSeparators: addThousandsSeparators,
formatCurrencyAmountInSmallestUnits: formatCurrencyAmountInSmallestUnits,
seededRandom: seededRandom,
seededRandomIntBetween: seededRandomIntBetween,
randomInt: randomInt,
logMemoryUsage: logMemoryUsage,
identifyMiner: identifyMiner,
getBlockTotalFeesFromCoinbaseTxAndBlockHeight: getBlockTotalFeesFromCoinbaseTxAndBlockHeight,
estimatedSupply: estimatedSupply,
refreshExchangeRates: refreshExchangeRates,
parseExponentStringDouble: parseExponentStringDouble,
formatLargeNumber: formatLargeNumber,
formatLargeNumberSignificant: formatLargeNumberSignificant,
geoLocateIpAddresses: geoLocateIpAddresses,
getTxTotalInputOutputValues: getTxTotalInputOutputValues,
tryParseAddress: tryParseAddress,
rgbToHsl: rgbToHsl,
colorHexToRgb: colorHexToRgb,
colorHexToHsl: colorHexToHsl,
logError: logError,
buildQrCodeUrls: buildQrCodeUrls,
ellipsize: ellipsize,
ellipsizeMiddle: ellipsizeMiddle,
summarizeDuration: summarizeDuration,
outputTypeAbbreviation: outputTypeAbbreviation,
outputTypeName: outputTypeName,
asHash: asHash,
asHashOrHeight: asHashOrHeight,
asAddress: asAddress,
arrayFromHexString: arrayFromHexString,
getCrawlerFromUserAgentString: getCrawlerFromUserAgentString,
timePromise: timePromise,
timeFunction: timeFunction,
startTimeNanos: startTimeNanos,
dtMillis: dtMillis,
objectProperties: objectProperties,
objHasProperty: objHasProperty,
stringifySimple: stringifySimple,
safePromise: safePromise,
getVoutAddress: getVoutAddress,
getVoutAddresses: getVoutAddresses,
xpubChangeVersionBytes: xpubChangeVersionBytes,
bip32Addresses: bip32Addresses,
difficultyAdjustmentEstimates: difficultyAdjustmentEstimates,
nextHalvingEstimates: nextHalvingEstimates,
sleep: sleep,
obfuscateProperties: obfuscateProperties,
awaitPromises: awaitPromises,
perfLogNewItem: perfLogNewItem,
perfLog: perfLog,
fileCache: fileCache
};