btc-rpc-explorer
Version:
Open-source, self-hosted Bitcoin explorer
1,727 lines (1,243 loc) • 68.7 kB
JavaScript
"use strict";
const debug = require("debug");
const debugLog = debug("btcexp:router");
const express = require('express');
const csrfApi = require('csurf');
const router = express.Router();
const util = require('util');
const moment = require('moment');
const qrcode = require('qrcode');
const bitcoinjs = require('bitcoinjs-lib');
const bip32 = require('bip32');
const bs58check = require('bs58check');
const { bech32, bech32m } = require("bech32");
const sha256 = require("crypto-js/sha256");
const hexEnc = require("crypto-js/enc-hex");
const Decimal = require("decimal.js");
const semver = require("semver");
const markdown = require("markdown-it")();
const asyncHandler = require("express-async-handler");
const utils = require('./../app/utils.js');
const coins = require("./../app/coins.js");
const config = require("./../app/config.js");
const coreApi = require("./../app/api/coreApi.js");
const addressApi = require("./../app/api/addressApi.js");
const rpcApi = require("./../app/api/rpcApi.js");
const btcQuotes = require("./../app/coins/btcQuotes.js");
const forceCsrf = csrfApi({ ignoreMethods: [] });
let noTxIndexMsg = "\n\nYour node does not have **txindex** enabled. Without it, you can only lookup wallet, mempool, and recently confirmed transactions by their **txid**. Searching for non-wallet transactions that were confirmed more than "+config.noTxIndexSearchDepth+" blocks ago is only possible if the confirmed block height is available.";
router.get("/", asyncHandler(async (req, res, next) => {
try {
if (req.session.host == null || req.session.host.trim() == "") {
if (req.cookies['rpc-host']) {
res.locals.host = req.cookies['rpc-host'];
}
if (req.cookies['rpc-port']) {
res.locals.port = req.cookies['rpc-port'];
}
if (req.cookies['rpc-username']) {
res.locals.username = req.cookies['rpc-username'];
}
res.render("connect");
res.end();
return;
}
const { perfId, perfResults } = utils.perfLogNewItem({action:"homepage"});
res.locals.perfId = perfId;
res.locals.homepage = true;
// don't need timestamp on homepage "blocks-list", this flag disables
res.locals.hideTimestampColumn = true;
// variables used by blocks-list.pug
res.locals.offset = 0;
res.locals.sort = "desc";
let feeConfTargets = [1, 6, 144, 1008];
res.locals.feeConfTargets = feeConfTargets;
let promises = [];
promises.push(utils.timePromise("homepage.getMempoolInfo", async () => {
res.locals.mempoolInfo = await coreApi.getMempoolInfo();
}, perfResults));
promises.push(utils.timePromise("homepage.getMiningInfo", async () => {
res.locals.miningInfo = await coreApi.getMiningInfo();
}, perfResults));
promises.push(utils.timePromise("homepage.getSmartFeeEstimates", async () => {
const rawSmartFeeEstimates = await coreApi.getSmartFeeEstimates("CONSERVATIVE", feeConfTargets);
let smartFeeEstimates = {};
for (let i = 0; i < feeConfTargets.length; i++) {
let rawSmartFeeEstimate = rawSmartFeeEstimates[i];
if (rawSmartFeeEstimate.errors) {
smartFeeEstimates[feeConfTargets[i]] = "?";
} else {
smartFeeEstimates[feeConfTargets[i]] = parseInt(new Decimal(rawSmartFeeEstimate.feerate).times(coinConfig.baseCurrencyUnit.multiplier).dividedBy(1000));
}
}
res.locals.smartFeeEstimates = smartFeeEstimates;
}, perfResults));
promises.push(utils.timePromise("homepage.getNetworkHashrate", async () => {
res.locals.hashrate7d = await coreApi.getNetworkHashrate(1008);
}, perfResults));
promises.push(utils.timePromise("homepage.getNetworkHashrate", async () => {
res.locals.hashrate30d = await coreApi.getNetworkHashrate(4320);
}, perfResults));
const getblockchaininfo = await utils.timePromise("homepage.getBlockchainInfo", async () => {
return await coreApi.getBlockchainInfo();
}, perfResults);
res.locals.getblockchaininfo = getblockchaininfo;
res.locals.difficultyPeriod = parseInt(Math.floor(getblockchaininfo.blocks / coinConfig.difficultyAdjustmentBlockCount));
let blockHeights = [];
if (getblockchaininfo.blocks) {
// +1 to page size here so we have the next block to calculate T.T.M.
for (let i = 0; i < (config.site.homepage.recentBlocksCount + 1); i++) {
blockHeights.push(getblockchaininfo.blocks - i);
}
} else if (global.activeBlockchain == "regtest") {
// hack: default regtest node returns getblockchaininfo.blocks=0, despite
// having a genesis block; hack this to display the genesis block
blockHeights.push(0);
}
promises.push(utils.timePromise("homepage.getBlocksStatsByHeight", async () => {
const rawblockstats = await coreApi.getBlocksStatsByHeight(blockHeights);
if (rawblockstats && rawblockstats.length > 0 && rawblockstats[0] != null) {
res.locals.blockstatsByHeight = {};
for (let i = 0; i < rawblockstats.length; i++) {
let blockstats = rawblockstats[i];
res.locals.blockstatsByHeight[blockstats.height] = blockstats;
}
}
}, perfResults));
promises.push(utils.timePromise("homepage.getBlockHeaderByHeight", async () => {
let h = coinConfig.difficultyAdjustmentBlockCount * res.locals.difficultyPeriod;
res.locals.difficultyPeriodFirstBlockHeader = await coreApi.getBlockHeaderByHeight(h);
}, perfResults));
promises.push(utils.timePromise("homepage.getBlocksByHeight", async () => {
const latestBlocks = await coreApi.getBlocksByHeight(blockHeights);
res.locals.latestBlocks = latestBlocks;
res.locals.blocksUntilDifficultyAdjustment = ((res.locals.difficultyPeriod + 1) * coinConfig.difficultyAdjustmentBlockCount) - latestBlocks[0].height;
}));
let targetBlocksPerDay = 24 * 60 * 60 / global.coinConfig.targetBlockTimeSeconds;
res.locals.targetBlocksPerDay = targetBlocksPerDay;
if (false && getblockchaininfo.chain !== 'regtest') {
/*promises.push(new Promise(async (resolve, reject) => {
res.locals.txStats = await utils.timePromise("homepage.getTxStats", coreApi.getTxStats(targetBlocksPerDay / 4, -targetBlocksPerDay, "latest"));
resolve();
}));*/
let chainTxStatsIntervals = [ [targetBlocksPerDay, "24 hours"], [7 * targetBlocksPerDay, "7 days"], [30 * targetBlocksPerDay, "30 days"] ]
.filter(dat => dat[0] <= getblockchaininfo.blocks);
res.locals.chainTxStats = {};
for (let i = 0; i < chainTxStatsIntervals.length; i++) {
promises.push(utils.timePromise(`homepage.getChainTxStats.${chainTxStatsIntervals[i][0]}`, async () => {
res.locals.chainTxStats[chainTxStatsIntervals[i][0]] = await coreApi.getChainTxStats(chainTxStatsIntervals[i][0]);
}, perfResults));
}
chainTxStatsIntervals.push([-1, "All time"]);
res.locals.chainTxStatsIntervals = chainTxStatsIntervals;
promises.push(utils.timePromise("homepage.getChainTxStats.allTime", async () => {
res.locals.chainTxStats[-1] = await coreApi.getChainTxStats(getblockchaininfo.blocks - 1);
}, perfResults));
}
/*promises.push(utils.timePromise("homepage.getblocktemplate", async () => {
let nextBlockEstimate = await utils.timePromise("homepage.getNextBlockEstimate", async () => {
return await coreApi.getNextBlockEstimate();
}, perfResults);
res.locals.nextBlockTemplate = nextBlockEstimate.blockTemplate;
res.locals.nextBlockFeeRateGroups = nextBlockEstimate.feeRateGroups;
res.locals.nextBlockMinFeeRate = nextBlockEstimate.minFeeRate;
res.locals.nextBlockMaxFeeRate = nextBlockEstimate.maxFeeRate;
res.locals.nextBlockMinFeeTxid = nextBlockEstimate.minFeeTxid;
res.locals.nextBlockMaxFeeTxid = nextBlockEstimate.maxFeeTxid;
res.locals.nextBlockTotalFees = nextBlockEstimate.totalFees;
}, perfResults));*/
await utils.awaitPromises(promises);
let eraStartBlockHeader = res.locals.difficultyPeriodFirstBlockHeader;
let currentBlock = res.locals.latestBlocks[0];
res.locals.difficultyAdjustmentData = utils.difficultyAdjustmentEstimates(eraStartBlockHeader, currentBlock);
res.locals.nextHalvingData = utils.nextHalvingEstimates(
res.locals.difficultyPeriodFirstBlockHeader,
res.locals.latestBlocks[0],
res.locals.difficultyAdjustmentData);
res.locals.perfResults = perfResults;
await utils.timePromise("homepage.render", async () => {
res.render("index");
}, perfResults);
next();
} catch (err) {
utils.logError("238023hw87gddd", err);
res.locals.userMessage = "Error building page: " + err;
await utils.timePromise("homepage.render", async () => {
res.render("index");
});
next();
}
}));
router.get("/node-details", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"node-details"});
res.locals.perfId = perfId;
const promises = [];
promises.push(utils.timePromise("node-details.getBlockchainInfo", async () => {
res.locals.getblockchaininfo = await coreApi.getBlockchainInfo();
}, perfResults));
promises.push(utils.timePromise("node-details.getDeploymentInfo", async () => {
res.locals.getdeploymentinfo = await coreApi.getDeploymentInfo();
}, perfResults));
promises.push(utils.timePromise("node-details.getNetworkInfo", async () => {
res.locals.getnetworkinfo = await coreApi.getNetworkInfo();
}, perfResults));
promises.push(utils.timePromise("node-details.getUptimeSeconds", async () => {
res.locals.uptimeSeconds = await coreApi.getUptimeSeconds();
}, perfResults));
promises.push(utils.timePromise("node-details.getNetTotals", async () => {
res.locals.getnettotals = await coreApi.getNetTotals();
}, perfResults));
await utils.awaitPromises(promises);
res.locals.perfResults = perfResults;
await utils.timePromise("node-details.render", async () => {
res.render("node-details");
}, perfResults);
next();
} catch (err) {
utils.logError("32978efegdde", err);
res.locals.userMessage = "Error building page: " + err;
await utils.timePromise("node-details.render", async () => {
res.render("node-details");
});
next();
}
}));
router.get("/mempool-summary", asyncHandler(async (req, res, next) => {
try {
res.locals.satoshiPerByteBucketMaxima = coinConfig.feeSatoshiPerByteBucketMaxima;
await utils.timePromise("mempool-summary/render", async () => {
res.render("mempool-summary");
});
next();
} catch (err) {
utils.logError("390824yw7e332", err);
res.locals.userMessage = "Error building page: " + err;
res.render("mempool-summary");
next();
}
}));
router.get("/peers", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"peers"});
res.locals.perfId = perfId;
const promises = [];
promises.push(utils.timePromise("peers.getPeerSummary", async () => {
res.locals.peerSummary = await coreApi.getPeerSummary();
}, perfResults));
await utils.awaitPromises(promises);
let peerSummary = res.locals.peerSummary;
let peerIps = [];
for (let i = 0; i < peerSummary.getpeerinfo.length; i++) {
let ipWithPort = peerSummary.getpeerinfo[i].addr;
if (ipWithPort.lastIndexOf(":") >= 0) {
let ip = ipWithPort.substring(0, ipWithPort.lastIndexOf(":"));
if (ip.trim().length > 0) {
peerIps.push(ip.trim());
}
}
}
if (peerIps.length > 0) {
res.locals.peerIpSummary = await utils.timePromise("peers.geoLocateIpAddresses", async () => {
return await utils.geoLocateIpAddresses(peerIps)
}, perfResults);
res.locals.mapBoxComApiAccessKey = config.credentials.mapBoxComApiAccessKey;
}
await utils.timePromise("peers.render", async () => {
res.render("peers");
}, perfResults);
next();
} catch (err) {
utils.logError("394rhweghe", err);
res.locals.userMessage = "Error: " + err;
await utils.timePromise("peers.render", async () => {
res.render("peers");
});
next();
}
}));
router.post("/connect", function(req, res, next) {
let host = req.body.host;
let port = req.body.port;
let username = req.body.username;
let password = req.body.password;
res.cookie('rpc-host', host);
res.cookie('rpc-port', port);
res.cookie('rpc-username', username);
req.session.host = host;
req.session.port = port;
req.session.username = username;
let newClient = new bitcoinCore({
host: host,
port: port,
username: username,
password: password,
timeout: 30000
});
debugLog("created new rpc client: " + newClient);
global.rpcClient = newClient;
req.session.userMessage = "<span class='font-weight-bold'>Connected via RPC</span>: " + username + " @ " + host + ":" + port;
req.session.userMessageType = "success";
res.redirect("/");
});
router.get("/disconnect", function(req, res, next) {
res.cookie('rpc-host', "");
res.cookie('rpc-port', "");
res.cookie('rpc-username', "");
req.session.host = "";
req.session.port = "";
req.session.username = "";
debugLog("destroyed rpc client.");
global.rpcClient = null;
req.session.userMessage = "Disconnected from node.";
req.session.userMessageType = "success";
res.redirect("/");
});
router.get("/changeSetting", function(req, res, next) {
if (req.query.name) {
if (!req.session.userSettings) {
req.session.userSettings = Object.create(null);
}
if (typeof req.query.name !== "string" || typeof req.query.value !== "string") {
res.redirect(req.headers.referer);
return;
}
if (req.query.name == "userTzOffset") {
if (parseFloat(req.query.value) == NaN) {
res.redirect(req.headers.referer);
return;
}
}
req.session.userSettings[req.query.name.toString()] = req.query.value.toString();
let userSettings = JSON.parse(req.cookies["user-settings"] || "{}");
userSettings[req.query.name] = req.query.value;
res.cookie("user-settings", JSON.stringify(userSettings));
}
res.redirect(req.headers.referer);
});
router.get("/session-data", function(req, res, next) {
if (req.query.action && req.query.data) {
let action = req.query.action;
let data = req.query.data;
if (action == "add-rpc-favorite") {
if (!req.session.favoriteRpcCommands) {
req.session.favoriteRpcCommands = [];
}
if (!req.session.favoriteRpcCommands.includes(data)) {
req.session.favoriteRpcCommands.push(data);
}
req.session.favoriteRpcCommands.sort();
}
if (action == "remove-rpc-favorite") {
if (!req.session.favoriteRpcCommands) {
req.session.favoriteRpcCommands = [];
}
if (req.session.favoriteRpcCommands.includes(data)) {
req.session.favoriteRpcCommands.splice(req.session.favoriteRpcCommands.indexOf(data), 1);
}
}
}
res.redirect(req.headers.referer);
});
router.get("/user-settings", asyncHandler(async (req, res, next) => {
await utils.timePromise("user-settings.render", async () => {
res.render("user-settings");
});
next();
}));
router.get("/blocks", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"blocks"});
res.locals.perfId = perfId;
let limit = config.site.browseBlocksPageSize;
let offset = 0;
let sort = "desc";
if (req.query.limit) {
limit = parseInt(req.query.limit);
}
if (req.query.offset) {
offset = parseInt(req.query.offset);
}
if (req.query.sort) {
sort = req.query.sort;
}
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.sort = sort;
res.locals.paginationBaseUrl = "./blocks";
// if pruning is active, global.pruneHeight is used when displaying this page
// global.pruneHeight is updated whenever we send a getblockchaininfo RPC to the node
let getblockchaininfo = await utils.timePromise("blocks.geoLocateIpAddresses", coreApi.getBlockchainInfo, perfResults);
res.locals.blockCount = getblockchaininfo.blocks;
res.locals.blockOffset = offset;
let blockHeights = [];
if (sort == "desc") {
for (let i = (getblockchaininfo.blocks - offset); i > (getblockchaininfo.blocks - offset - limit - 1); i--) {
if (i >= 0) {
blockHeights.push(i);
}
}
} else {
for (let i = offset - 1; i < (offset + limit); i++) {
if (i >= 0) {
blockHeights.push(i);
}
}
}
blockHeights = blockHeights.filter((h) => {
return h >= 0 && h <= getblockchaininfo.blocks;
});
let promises = [];
promises.push(utils.timePromise("blocks.getBlocksByHeight", async () => {
res.locals.blocks = await coreApi.getBlocksByHeight(blockHeights);
}, perfResults));
promises.push(utils.timePromise("blocks.getBlocksByHeight", async () => {
try {
let rawblockstats = await coreApi.getBlocksStatsByHeight(blockHeights);
if (rawblockstats != null && rawblockstats.length > 0 && rawblockstats[0] != null) {
res.locals.blockstatsByHeight = {};
for (let i = 0; i < rawblockstats.length; i++) {
let blockstats = rawblockstats[i];
res.locals.blockstatsByHeight[blockstats.height] = blockstats;
}
}
} catch (err) {
if (!global.prunedBlockchain) {
throw err;
} else {
// failure may be due to pruning, let it pass
// TODO: be more discerning here...consider throwing something
}
}
}, perfResults));
await utils.awaitPromises(promises);
await utils.timePromise("blocks.render", async () => {
res.render("blocks");
}, perfResults);
next();
} catch (err) {
res.locals.pageErrors.push(utils.logError("32974hrbfbvc", err));
res.locals.userMessage = "Error: " + err;
await utils.timePromise("blocks.render", async () => {
res.render("blocks");
});
next();
}
}));
router.get("/mining-summary", asyncHandler(async (req, res, next) => {
try {
let getblockchaininfo = await utils.timePromise("mining-summary.getBlockchainInfo", coreApi.getBlockchainInfo);
res.locals.currentBlockHeight = getblockchaininfo.blocks;
await utils.timePromise("mining-summary.render", async () => {
res.render("mining-summary");
});
next();
} catch (err) {
res.locals.pageErrors.push(utils.logError("39342heuges", err));
res.locals.userMessage = "Error: " + err;
res.render("mining-summary");
next();
}
}));
router.get("/xyzpub/:extendedPubkey", asyncHandler(async (req, res, next) => {
try {
const extendedPubkey = req.params.extendedPubkey;
res.locals.extendedPubkey = extendedPubkey;
let limit = 20;
if (req.query.limit) {
limit = parseInt(req.query.limit);
}
res.locals.limit = limit;
let offset = 0;
if (req.query.offset) {
offset = parseInt(req.query.offset);
}
res.locals.offset = offset;
res.locals.paginationBaseUrl = `./xyzpub/${extendedPubkey}`;
res.locals.metaTitle = `Extended Public Key: ${utils.ellipsizeMiddle(extendedPubkey, 24)}`;
res.locals.relatedKeys = [];
const xpub_tpub = global.activeBlockchain == "main" ? "xpub" : "tpub";
const ypub_upub = global.activeBlockchain == "main" ? "ypub" : "upub";
const zpub_vpub = global.activeBlockchain == "main" ? "zpub" : "vpub";
res.locals.pubkeyType = "Unknown";
res.locals.bip32Path = "Unknown";
res.locals.pubkeyTypeDesc = null;
res.locals.keyType = extendedPubkey.substring(0, 4);
// if xpub/ypub/zpub convert to address under path m/0/0
if (extendedPubkey.match(/^(xpub|tpub).*$/)) {
res.locals.pubkeyType = "P2PKH";
res.locals.pubkeyTypeDesc = "Pay to Public Key Hash";
res.locals.bip32Path = "m/44'/0'";
let xpub = extendedPubkey;
if (!extendedPubkey.startsWith(xpub_tpub)) {
xpub = utils.xpubChangeVersionBytes(extendedPubkey, xpub_tpub);
}
res.locals.receiveAddresses = utils.bip32Addresses(extendedPubkey, "p2pkh", 0, limit, offset);
res.locals.changeAddresses = utils.bip32Addresses(extendedPubkey, "p2pkh", 1, limit, offset);
if (!extendedPubkey.startsWith(xpub_tpub)) {
res.locals.relatedKeys.push({
keyType: xpub_tpub,
key: utils.xpubChangeVersionBytes(xpub, xpub_tpub),
bip32Path: "m/44'/0'",
outputType: "P2PKH",
firstAddresses: utils.bip32Addresses(xpub, "p2pkh", 0, 3, 0)
});
}
res.locals.relatedKeys.push({
keyType: xpub_tpub,
key: extendedPubkey,
bip32Path: "m/44'/0'",
outputType: "P2PKH",
firstAddresses: utils.bip32Addresses(xpub, "p2pkh", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: ypub_upub,
key: utils.xpubChangeVersionBytes(xpub, ypub_upub),
bip32Path: "m/49'/0'",
outputType: "P2WPKH in P2SH",
firstAddresses: utils.bip32Addresses(xpub, "p2sh(p2wpkh)", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: zpub_vpub,
key: utils.xpubChangeVersionBytes(xpub, zpub_vpub),
bip32Path: "m/84'/0'",
outputType: "P2WPKH",
firstAddresses: utils.bip32Addresses(xpub, "p2wpkh", 0, 3, 0)
});
} else if (extendedPubkey.match(/^(ypub|upub).*$/)) {
res.locals.pubkeyType = "P2WPKH in P2SH";
res.locals.pubkeyTypeDesc = "Pay to Witness Public Key Hash (P2WPKH) wrapped inside Pay to Script Hash (P2SH), aka Wrapped Segwit";
res.locals.bip32Path = "m/49'/0'";
const xpub = utils.xpubChangeVersionBytes(extendedPubkey, xpub_tpub);
res.locals.receiveAddresses = utils.bip32Addresses(xpub, "p2sh(p2wpkh)", 0, limit, offset);
res.locals.changeAddresses = utils.bip32Addresses(xpub, "p2sh(p2wpkh)", 1, limit, offset);
res.locals.relatedKeys.push({
keyType: xpub_tpub,
key: xpub,
bip32Path: "m/44'/0'",
outputType: "P2PKH",
firstAddresses: utils.bip32Addresses(xpub, "p2pkh", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: ypub_upub,
key: extendedPubkey,
bip32Path: "m/49'/0'",
outputType: "P2WPKH in P2SH",
firstAddresses: utils.bip32Addresses(xpub, "p2sh(p2wpkh)", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: zpub_vpub,
key: utils.xpubChangeVersionBytes(xpub, zpub_vpub),
bip32Path: "m/84'/0'",
outputType: "P2WPKH",
firstAddresses: utils.bip32Addresses(xpub, "p2wpkh", 0, 3, 0)
});
} else if (extendedPubkey.match(/^(zpub|vpub).*$/)) {
res.locals.pubkeyType = "P2WPKH";
res.locals.pubkeyTypeDesc = "Pay to Witness Public Key Hash, aka Native Segwit";
res.locals.bip32Path = "m/84'/0'";
const xpub = utils.xpubChangeVersionBytes(extendedPubkey, xpub_tpub);
res.locals.receiveAddresses = utils.bip32Addresses(xpub, "p2wpkh", 0, limit, offset);
res.locals.changeAddresses = utils.bip32Addresses(xpub, "p2wpkh", 1, limit, offset);
res.locals.relatedKeys.push({
keyType: xpub_tpub,
key: xpub,
bip32Path: "m/44'/0'",
outputType: "P2PKH",
firstAddresses: utils.bip32Addresses(xpub, "p2pkh", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: ypub_upub,
key: utils.xpubChangeVersionBytes(xpub, ypub_upub),
bip32Path: "m/49'/0'",
outputType: "P2WPKH in P2SH",
firstAddresses: utils.bip32Addresses(xpub, "p2sh(p2wpkh)", 0, 3, 0)
});
res.locals.relatedKeys.push({
keyType: zpub_vpub,
key: extendedPubkey,
bip32Path: "m/84'/0'",
outputType: "P2WPKH",
firstAddresses: utils.bip32Addresses(xpub, "p2wpkh", 0, 3, 0)
});
} else if (extendedPubkey.startsWith("Ypub")) {
res.locals.pubkeyType = "Multi-Sig P2WSH in P2SH";
res.locals.bip32Path = "-";
} else if (extendedPubkey.startsWith("Zpub")) {
res.locals.pubkeyType = "Multi-Sig P2WSH";
res.locals.bip32Path = "-";
}
// Cumulate balanceSat of all addresses
res.locals.balanceSat = 0;
// Loop over the 2 types addresses (first receive and then change)
let allAddresses = [res.locals.receiveAddresses, res.locals.changeAddresses];
res.locals.receiveAddresses = [];
res.locals.changeAddresses = [];
for (let i = 0; i < allAddresses.length; i++) {
// Duplicate addresses and change them to addressDetails objects with 3 properties (address, balanceSat, txCount)
let addresses = [...allAddresses[i]];
for (let j = 0; j < addresses.length; j++) {
const address = addresses[j];
const validateaddressResult = await coreApi.getAddress(address);
// No need to paginate request => use a high limit value
const addressDetailsResult = await addressApi.getAddressDetails(address, validateaddressResult.scriptPubKey, "desc", 100, 0);
// In case of errors, we just skip this address result
if (Array.isArray(addressDetailsResult.errors) && addressDetailsResult.errors.length == 0) {
res.locals.balanceSat += addressDetailsResult.addressDetails.balanceSat;
const addressDetails = { ...addressDetailsResult.addressDetails, address};
if (i == 0)
res.locals.receiveAddresses.push(addressDetails);
else
res.locals.changeAddresses.push(addressDetails);
}
}
}
await utils.timePromise("extended-public-key.render", async () => {
res.render("extended-public-key");
});
next();
} catch (err) {
res.locals.pageErrors.push(utils.logError("23r08uyhe7ege", err));
res.locals.userMessage = "Error: " + err;
await utils.timePromise("extended-public-key.render", async () => {
res.render("extended-public-key");
});
next();
}
}));
router.get("/block-stats", asyncHandler(async (req, res, next) => {
if (semver.lt(global.btcNodeSemver, rpcApi.minRpcVersions.getblockstats)) {
res.locals.rpcApiUnsupportedError = {rpc:"getblockstats", version:rpcApi.minRpcVersions.getblockstats};
}
try {
const getblockchaininfo = await coreApi.getBlockchainInfo();
res.locals.currentBlockHeight = getblockchaininfo.blocks;
await utils.timePromise("block-stats.render", async () => {
res.render("block-stats");
});
next();
} catch(err) {
res.locals.userMessage = "Error: " + err;
await utils.timePromise("block-stats.render", async () => {
res.render("block-stats");
});
next();
};
}));
router.get("/mining-template", asyncHandler(async (req, res, next) => {
// url changed
res.redirect("./next-block");
}));
router.get("/next-block", asyncHandler(async (req, res, next) => {
const blockTemplate = await coreApi.getBlockTemplate();
res.locals.minFeeRate = 1000000;
res.locals.maxFeeRate = -1;
res.locals.medianFeeRate = -1;
const parentTxIndexes = new Set();
blockTemplate.transactions.forEach(tx => {
if (tx.depends && tx.depends.length > 0) {
tx.depends.forEach(index => {
parentTxIndexes.add(index);
});
}
});
let txIndex = 1;
let feeRates = [];
blockTemplate.transactions.forEach(tx => {
let feeRate = tx.fee / tx.weight * 4;
if (tx.depends && tx.depends.length > 0) {
let totalFee = tx.fee;
let totalWeight = tx.weight;
tx.depends.forEach(index => {
totalFee += blockTemplate.transactions[index - 1].fee;
totalWeight += blockTemplate.transactions[index - 1].weight;
});
tx.avgFeeRate = totalFee / totalWeight * 4;
}
// txs that are ancestors should not be included in min/max
// calculations since their native fee rate is different than
// their effective fee rate (which takes descendant fee rates
// into account)
if (!parentTxIndexes.has(txIndex) && (!tx.depends || tx.depends.length == 0)) {
feeRates.push(feeRate);
if (feeRate > res.locals.maxFeeRate) {
res.locals.maxFeeRate = feeRate;
}
if (feeRate < res.locals.minFeeRate) {
res.locals.minFeeRate = feeRate;
}
}
txIndex++;
});
if (feeRates.length > 0) {
res.locals.medianFeeRate = feeRates[Math.floor(feeRates.length / 2)];
}
res.locals.blockTemplate = blockTemplate;
await utils.timePromise("next-block.render", async () => {
res.render("next-block");
});
next();
}));
router.get("/search", function(req, res, next) {
res.render("search");
next();
});
router.post("/search", function(req, res, next) {
if (!req.body.query) {
req.session.userMessage = "Enter a block height, block hash, or transaction id.";
res.redirect("./");
return;
}
let query = req.body.query.toLowerCase().trim();
let rawCaseQuery = req.body.query.trim();
req.session.query = req.body.query;
// xpub/ypub/zpub -> redirect: /xyzpub/XXX
if (rawCaseQuery.match(/^(xpub|ypub|zpub|Ypub|Zpub).*$/)) {
res.redirect(`./xyzpub/${rawCaseQuery}`);
return;
}
// tpub/upub/vpub -> redirect: /xyzpub/XXX
if (rawCaseQuery.match(/^(tpub|upub|vpub|Upub|Vpub).*$/)) {
res.redirect(`./xyzpub/${rawCaseQuery}`);
return;
}
// Support txid@height lookups
if (/^[a-f0-9]{64}@\d+$/.test(query)) {
return res.redirect("./tx/" + query);
}
let parseAddressData = utils.tryParseAddress(rawCaseQuery);
if (false) {
if (parseAddressData.errors) {
parseAddressData.errors.forEach(err => {
utils.logError("19238rfehdusd", err, {address:query});
});
}
}
if (parseAddressData.parsedAddress) {
res.redirect("./address/" + rawCaseQuery);
} else if (query.length == 64) {
coreApi.getRawTransaction(query).then(function(tx) {
res.redirect("./tx/" + query);
}).catch(function(err) {
coreApi.getBlockByHash(query).then(function(blockByHash) {
res.redirect("./block/" + query);
}).catch(function(err) {
req.session.userMessage = "No results found for query: " + query;
if (!global.txindexAvailable) {
req.session.userMessage += noTxIndexMsg;
}
res.redirect("./");
});
});
} else if (!isNaN(query)) {
coreApi.getBlockByHeight(parseInt(query)).then(function(blockByHeight) {
res.redirect("./block-height/" + query);
}).catch(function(err) {
req.session.userMessage = "No results found for query: " + query;
res.redirect("./");
});
} else {
req.session.userMessage = "No results found for query: " + rawCaseQuery;
res.redirect("./");
}
});
router.get("/block-height/:blockHeight", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"block-height"});
res.locals.perfId = perfId;
let blockHeight = parseInt(req.params.blockHeight);
res.locals.blockHeight = blockHeight;
res.locals.result = {};
let limit = config.site.blockTxPageSize;
let offset = 0;
res.locals.maxTxOutputDisplayCount = 15;
if (req.query.limit) {
limit = parseInt(req.query.limit);
// for demo sites, limit page sizes
if (config.demoSite && limit > config.site.blockTxPageSize) {
limit = config.site.blockTxPageSize;
res.locals.userMessage = "Transaction page size limited to " + config.site.blockTxPageSize + ". If this is your site, you can change or disable this limit in the site config.";
}
}
if (req.query.offset) {
offset = parseInt(req.query.offset);
}
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.paginationBaseUrl = "./block-height/" + blockHeight;
const result = await utils.timePromise("block-height.getBlockByHeight", async () => {
return await coreApi.getBlockByHeight(blockHeight);
}, perfResults);
res.locals.result.getblockbyheight = result;
let promises = [];
promises.push(utils.timePromise("block-height.getBlockByHashWithTransactions", async () => {
const blockWithTransactions = await coreApi.getBlockByHashWithTransactions(result.hash, limit, offset);
res.locals.result.getblock = blockWithTransactions.getblock;
res.locals.result.transactions = blockWithTransactions.transactions;
res.locals.result.txInputsByTransaction = blockWithTransactions.txInputsByTransaction;
}, perfResults));
promises.push(utils.timePromise("block-height.getBlockStats", async () => {
try {
const blockStats = await coreApi.getBlockStats(result.hash);
res.locals.result.blockstats = blockStats;
} catch (err) {
if (global.prunedBlockchain) {
// unavailable, likely due to pruning
debugLog('Failed loading block stats', err);
res.locals.result.blockstats = null;
} else {
throw err;
}
}
}, perfResults));
await utils.awaitPromises(promises);
if (global.specialBlocks && global.specialBlocks[res.locals.result.getblock.hash]) {
let funInfo = global.specialBlocks[res.locals.result.getblock.hash];
res.locals.metaTitle = funInfo.summary;
if (funInfo.alertBodyHtml) {
res.locals.metaDesc = funInfo.alertBodyHtml.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, "");
} else {
res.locals.metaDesc = "";
}
} else {
res.locals.metaTitle = `Bitcoin Block #${blockHeight.toLocaleString()}`;
res.locals.metaDesc = "";
}
await utils.timePromise("block-height.render", async () => {
res.render("block");
}, perfResults);
next();
} catch (err) {
res.locals.userMessageMarkdown = `Failed loading block: height=**${blockHeight}**`;
res.locals.pageErrors.push(utils.logError("389wer07eghdd", err));
await utils.timePromise("block-height.render", async () => {
res.render("block");
});
next();
}
}));
router.get("/block/:blockHash", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"block"});
res.locals.perfId = perfId;
let blockHash = utils.asHash(req.params.blockHash);
res.locals.blockHash = blockHash;
res.locals.result = {};
let limit = config.site.blockTxPageSize;
let offset = 0;
res.locals.maxTxOutputDisplayCount = 15;
if (req.query.limit) {
limit = parseInt(req.query.limit);
// for demo sites, limit page sizes
if (config.demoSite && limit > config.site.blockTxPageSize) {
limit = config.site.blockTxPageSize;
res.locals.userMessage = "Transaction page size limited to " + config.site.blockTxPageSize + ". If this is your site, you can change or disable this limit in the site config.";
}
}
if (req.query.offset) {
offset = parseInt(req.query.offset);
}
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.paginationBaseUrl = "./block/" + blockHash;
let promises = [];
promises.push(utils.timePromise("block.getBlockByHashWithTransactions", async () => {
const blockWithTransactions = await coreApi.getBlockByHashWithTransactions(blockHash, limit, offset);
res.locals.result.getblock = blockWithTransactions.getblock;
res.locals.result.transactions = blockWithTransactions.transactions;
res.locals.result.txInputsByTransaction = blockWithTransactions.txInputsByTransaction;
}, perfResults));
promises.push(utils.timePromise("block.getBlockStats", async () => {
try {
const blockStats = await coreApi.getBlockStats(blockHash);
res.locals.result.blockstats = blockStats;
} catch (err) {
if (global.prunedBlockchain) {
// unavailable, likely due to pruning
debugLog('Failed loading block stats, likely due to pruning', err);
} else {
throw err;
}
}
}, perfResults));
await utils.awaitPromises(promises);
if (global.specialBlocks && global.specialBlocks[res.locals.result.getblock.hash]) {
let funInfo = global.specialBlocks[res.locals.result.getblock.hash];
res.locals.metaTitle = funInfo.summary;
if (funInfo.alertBodyHtml) {
res.locals.metaDesc = funInfo.alertBodyHtml.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, "");
} else {
res.locals.metaDesc = "";
}
} else {
res.locals.metaTitle = `Bitcoin Block ${utils.ellipsizeMiddle(res.locals.result.getblock.hash, 16)}`;
res.locals.metaDesc = "";
}
await utils.timePromise("block.render", async () => {
res.render("block");
}, perfResults);
next();
} catch (err) {
res.locals.userMessageMarkdown = `Failed to load block: **${blockHash}**`;
res.locals.pageErrors.push(utils.logError("32824yhr2973t3d", err));
await utils.timePromise("block.render", async () => {
res.render("block");
});
next();
}
}));
router.get("/predicted-blocks", asyncHandler(async (req, res, next) => {
try {
res.locals.satoshiPerByteBucketMaxima = coinConfig.feeSatoshiPerByteBucketMaxima;
res.render("predicted-blocks");
next();
} catch (err) {
utils.logError("2083ryw0efghsu", err);
res.locals.userMessage = "Error building page: " + err;
res.render("predicted-blocks");
next();
}
}));
router.get("/predicted-blocks-old", asyncHandler(async (req, res, next) => {
try {
const mempoolTxids = await utils.timePromise("predicted-blocks.getAllMempoolTxids", coreApi.getAllMempoolTxids());
let mempoolTxSummaries = await coreApi.getMempoolTxSummaries(mempoolTxids, Math.random().toString(36).substr(2, 5), (x) => {});
const blockTemplate = {weight: 0, totalFees: new Decimal(0), vB: 0, txCount:0, txids: []};
const blocks = [];
mempoolTxSummaries.sort((a, b) => {
let aFeeRate = (a.f + a.af) / (a.w + a.asz * 4);
let bFeeRate = (b.f + b.af) / (b.w + b.asz * 4);
if (aFeeRate > bFeeRate) {
return -1;
} else if (aFeeRate < bFeeRate) {
return 1;
} else {
return a.key.localeCompare(b.key);
}
});
res.locals.topTxs = mempoolTxSummaries.slice(0, 20);
let currentBlock = Object.assign({}, blockTemplate);
for (let i = 0; i < mempoolTxSummaries.length; i++) {
const tx = mempoolTxSummaries[i];
tx.frw = tx.f / tx.w;
tx.fr = tx.f / tx.sz;
if ((currentBlock.weight + tx.w) > coinConfig.maxBlockWeight) {
// this tx doesn't fit in the current block we're building
// so let's finish this one up and add it to the list
currentBlock.avgFee = currentBlock.totalFees.dividedBy(currentBlock.txCount);
currentBlock.avgFeeRate = currentBlock.totalFees.dividedBy(currentBlock.vB);
blocks.push(currentBlock);
// ...and start a new block
currentBlock = Object.assign({}, blockTemplate);
console.log(JSON.stringify(currentBlock));
}
currentBlock.txCount++;
currentBlock.weight += tx.w;
currentBlock.totalFees = currentBlock.totalFees.plus(new Decimal(tx.f));
currentBlock.vB += tx.sz;
//currentBlock.txids.push(tx.key);
}
res.locals.projectedBlocks = blocks;
res.render("predicted-blocks");
next();
} catch (err) {
res.locals.pageErrors.push(utils.logError("234efuewgew", err));
res.render("predicted-blocks");
next();
}
}));
router.get("/block-analysis/:blockHashOrHeight", function(req, res, next) {
let blockHashOrHeight = utils.asHashOrHeight(req.params.blockHashOrHeight);
let goWithBlockHash = function(blockHash) {
res.locals.blockHash = blockHash;
res.locals.result = {};
let txResults = [];
let promises = [];
res.locals.result = {};
coreApi.getBlockByHash(blockHash).then(function(block) {
res.locals.block = block;
res.locals.result.getblock = block;
res.render("block-analysis");
next();
}).catch(function(err) {
res.locals.pageErrors.push(utils.logError("943h84ehedr", err));
res.render("block-analysis");
next();
});
};
if (!isNaN(blockHashOrHeight)) {
coreApi.getBlockByHeight(parseInt(blockHashOrHeight)).then(function(blockByHeight) {
goWithBlockHash(blockByHeight.hash);
});
} else {
goWithBlockHash(blockHashOrHeight);
}
});
router.get("/block-analysis", function(req, res, next) {
res.render("block-analysis-search");
next();
});
router.get("/tx/:transactionId@:blockHeight", asyncHandler(async (req, res, next) => {
req.query.blockHeight = req.params.blockHeight;
req.url = "/tx/" + req.params.transactionId;
next();
}));
router.get("/tx/:transactionId", asyncHandler(async (req, res, next) => {
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"transaction"});
res.locals.perfId = perfId;
let txid = utils.asHash(req.params.transactionId);
let output = -1;
if (req.query.output) {
output = parseInt(req.query.output);
}
res.locals.txid = txid;
res.locals.output = output;
res.locals.maxTxOutputDisplayCount = 40;
const promises = [];
if (req.query.blockHeight) {
res.locals.blockHeight = parseInt(req.query.blockHeight);
}
res.locals.result = {};
let txInputLimit = (res.locals.crawlerBot) ? 3 : -1;
let txPromise = req.query.blockHeight ?
async () => {
const block = await coreApi.getBlockByHeight(parseInt(req.query.blockHeight));
res.locals.block = block;
return await coreApi.getRawTransactionsWithInputs([txid], txInputLimit, block.hash);
}
:
async () => {
return await coreApi.getRawTransactionsWithInputs([txid], txInputLimit);
};
const rawTxResult = await utils.timePromise("tx.getRawTransactionsWithInputs", txPromise, perfResults);
let tx = rawTxResult.transactions[0];
res.locals.tx = tx;
res.locals.isCoinbaseTx = tx.vin[0].coinbase;
res.locals.result.getrawtransaction = tx;
res.locals.result.txInputs = rawTxResult.txInputsByTransaction[txid] || {};
promises.push(utils.timePromise("tx.getTxUtxos", async () => {
res.locals.utxos = await coreApi.getTxUtxos(tx);
}, perfResults));
if (tx.confirmations == null) {
promises.push(utils.timePromise("tx.getMempoolTxDetails", async () => {
res.locals.mempoolDetails = await coreApi.getMempoolTxDetails(txid, true);
}, perfResults));
} else {
promises.push(utils.timePromise("tx.getblockheader", async () => {
let rpcResult = await rpcApi.getRpcDataWithParams({method:'getblockheader', parameters:[tx.blockhash]});
res.locals.result.getblock = rpcResult;
}, perfResults));
}
await utils.awaitPromises(promises);
if (global.specialTransactions && global.specialTransactions[txid]) {
let funInfo = global.specialTransactions[txid];
res.locals.metaTitle = funInfo.summary;
if (funInfo.alertBodyHtml) {
res.locals.metaDesc = funInfo.alertBodyHtml.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, "");
} else {
res.locals.metaDesc = "";
}
} else {
res.locals.metaTitle = `Bitcoin Transaction ${utils.ellipsizeMiddle(txid, 16)}`;
res.locals.metaDesc = "";
}
res.locals.perfResults = perfResults;
await utils.timePromise("tx.render", async () => {
res.render("transaction");
}, perfResults);
next();
} catch (err) {
if (global.prunedBlockchain && res.locals.blockHeight && res.locals.blockHeight < global.pruneHeight) {
// Failure to load tx here is expected and a full description of the situation is given to the user
// in the UI. No need to also show an error userMessage here.
} else if (!global.txindexAvailable) {
res.locals.noTxIndexMsg = noTxIndexMsg;
// As above, failure to load the tx is expected here and good user feedback is given in the UI.
// No need for error userMessage.
} else {
res.locals.userMessageMarkdown = `Failed to load transaction: txid=**${txid}**`;
}
utils.logError("1237y4ewssgt", err);
await utils.timePromise("tx.render", async () => {
res.render("transaction");
});
next();
}
}));
router.get("/address/:address", asyncHandler(async (req, res, next) => {
let address = utils.asAddress(req.params.address);
try {
const { perfId, perfResults } = utils.perfLogNewItem({action:"address"});
res.locals.perfId = perfId;
let limit = config.site.addressTxPageSize;
let offset = 0;
let sort = "desc";
res.locals.maxTxOutputDisplayCount = config.site.addressPage.txOutputMaxDefaultDisplay;
if (req.query.limit) {
limit = parseInt(req.query.limit);
// for demo sites, limit page sizes
if (config.demoSite && limit > config.site.addressTxPageSize) {
limit = config.site.addressTxPageSize;
res.locals.userMessage = "Transaction page size limited to " + config.site.addressTxPageSize + ". If this is your site, you can change or disable this limit in the site config.";
}
}
if (req.query.offset) {
offset = parseInt(req.query.offset);
}
if (req.query.sort) {
sort = req.query.sort;
}
res.locals.metaTitle = `Bitcoin Address ${address}`;
res.locals.address = address;
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.sort = sort;
res.locals.paginationBaseUrl = `./address/${address}?sort=${sort}`;
res.locals.transactions = [];
res.locals.addressApiSupport = addressApi.getCurrentAddressApiFeatureSupport();
res.locals.result = {};
let parseAddressData = utils.tryParseAddress(address);
if (parseAddressData.parsedAddress) {
//console.log("address.parse: " + JSON.stringify(parseAddressData));
res.locals.addressObj = parseAddressData.parsedAddress;
res.locals.addressEncoding = parseAddressData.encoding;
} else if (parseAddressData.errors) {
parseAddressData.errors.forEach(err => {
res.locals.pageErrors.push(utils.logError("ParseAddressError", err));
});
}
if (global.miningPoolsConfigs) {
for (let i = 0; i < global.miningPoolsConfigs.length; i++) {
if (global.miningPoolsConfigs[i].payout_addresses[address]) {
res.locals.payoutAddressForMiner = global.miningPoolsConfigs[i].payout_addresses[address];
}
}
}
const validateaddressResult = await coreApi.getAddress(address);
res.locals.result.validateaddress = validateaddressResult;
let promises = [];
if (!res.locals.crawlerBot) {
let addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey)));
addrScripthash = addrScripthash.match(/.{2}/g).reverse().join("");
res.locals.electrumScripthash = addrScripthash;
promises.push(utils.timePromise("address.getAddressDetails", async () => {
const addressDetailsResult = await addressApi.getAddressDetails(address, validateaddressResult.scriptPubKey, sort, limit, offset);
let addressDetails = addressDetailsResult.addressDetails;
if (addressDetailsResult.errors) {
res.locals.addressDetailsErrors = addressDetailsResult.errors;
}
if (addressDetails) {
res.locals.addressDetails = addressDetails;
if (addressDetails.balanceSat == 0) {
// make sure zero balances pass the falsey check in the UI
addressDetails.balanceSat = "0";
}
if (addressDetails.txCount == 0) {
// make sure txCount=0 pass the falsey check in the UI
addressDetails.txCount = "0";
}
if (addressDetails.txids) {
let txids = addressDetails.txids;
// if the active addressApi gives us blockHeightsByTxid, it saves us work, so try to use it
let blockHeightsByTxid = {};
if (addressDetails.blockHeightsByTxid) {
blockHeightsByTxid = addressDetails.blockHeightsByTxid;
}
res.locals.txids = txids;
const rawTxResult = await (global.txindexAvailable
? coreApi.getRawTransactionsWithInputs(txids, 5)
: coreApi.getRawTransactionsByHeights(txids, blockHeightsByTxid)
.then(transactions => ({ transactions, txInputsByTransaction: {} }))
);
res.locals.transactions = rawTxResult.transactions;
res.locals.txInputsByTransaction = rawTxResult.txInputsByTransaction;
// for coinbase txs, we need the block height in order to calculate subsidy to display
let coinbaseTxs = [];
for (let i = 0; i < rawTxResult.transactions.length; i++) {
let tx = rawTxResult.transactions[i];
for (let j = 0; j < tx.vin.length; j++) {
if (tx.vin[j].coinbase) {
// addressApi sometimes has blockHeightByTxid already available, otherwise we need to query for it
if (!blockHeightsByTxid[tx.txid]) {
coinbaseTxs.push(tx);
}
}
}
}
let coinbaseTxBlockHashes = [];
let blockHashesByTxid = {};
coinbaseTxs.forEach(function(tx) {
coinbaseTxBlockHashes.push(tx.blockhash);
blockHashesByTxid[tx.txid] = tx.blockhash;
});
let blockHeightsPromises = [];
if (coinbaseTxs.length > 0) {
// we need to query some blockHeights by hash for some coinbase txs
blockHeightsPromises.push(utils.timePromise("address.getBlocksByHash", async () => {
const blocksByHashResult = await coreApi.getBlocksByHash(coinbaseTxBlockHashes);
for (let txid in blockHashesByTxid) {
if (blockHashesByTxid.hasOwnProperty(txid)) {
blockHeightsByTxid[txid] = blocksByHashResult[blockHashesByTxid[txid]].height;
}
}
}, perfResults));
}
await utils.awaitPromises(blockHeightsPromises);
let addrGainsByTx = {};
let addrLossesByTx = {};
res.locals.addrGainsByTx = addrGainsByTx;
res.locals.addrLossesByTx = addrLossesByTx;
let handledTxids = [];
for (let i = 0; i < rawTxResult.transactions.length; i++) {
let tx = rawTxResult.transactions[i];
let txInputs = rawTxResult.txInputsByTransaction[tx.txid] || {};
if (handledTxids.includes(tx.txid)) {
continue;
}
handledTxids.push(tx.txid);
for (let j = 0; j < tx.vout.length; j++) {
if (tx.vout[j].value > 0 && tx.vout[j].scriptPubKey) {
if (utils.getVoutAddresses(tx.vout[j]).includes(address)) {
if (addrGainsByTx[tx.txid] == null) {
addrGainsByTx[tx.txid] = new Decimal(0);
}
addrGainsByTx[tx.txid] = addrGainsByTx[tx.txid].plus(new Decimal(tx.vout[j].value));
}
}
}
for (let j = 0; j < tx.vin.length; j++) {
let txInput = txInputs[j];
let vinJ = tx.vin[j];
if (txInput != null) {
if (txInput && txInput.scriptPubKey) {
if (utils.getVoutAddresses(txInput).includes(address)) {
if (addrLossesByTx[tx.txid] == null) {
addrLossesByTx[tx.txid] = new Decimal(0);
}
addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(new Decimal(txInput.value));
}
}
}
}
//debugLog("tx: " + JSON.stringify(tx));
//debugLog("txInputs: " + JSON.stringify(txInputs));
}
res.locals.blockHeightsByTxid = blockHeightsByTxid;
}
}
}, perfResults));
promises.push(utils.timePromise("address.getBlockchainInfo", async () => {
res.locals.getblockchaininfo = await coreApi.getBlockchainInfo();
}, perfResults));
}
promises.push(utils.timePromise("address.qrcode.toDataURL", async () => {
try {
const url = await qrcode.toDataURL(address);
res.locals.addressQrCodeUrl = url;
} catch(err) {
res.locals.pageErrors.push(utils.logError("93ygfew0ygf2gf2", err));
}
}, perfResults));
await utils.awaitPromises(promises);
await utils.timePromise("address.render", async ()