diff --git a/app/api/addressApi.js b/app/api/addressApi.js index 10551aaba..6241c8091 100644 --- a/app/api/addressApi.js +++ b/app/api/addressApi.js @@ -33,6 +33,9 @@ const METHOD_MAPPING = { addressBalance : { "electrumx" : electrumAddressApi.getAddressBalance, "daemonRPC" : rpcApi.getAddressBalance + }, + addressDeltas : { + "daemonRPC" : rpcApi.getAddressDeltas, } } @@ -114,11 +117,24 @@ function executeMethod(method, ...args) { }); } -function getAddressDetails(address, scriptPubkey, sort, limit, offset) { - return executeMethod("addressDetails", address, scriptPubkey, sort, limit, offset); +function getAddressDetails(address, scriptPubkey, sort, limit, offset, assetName) { + return executeMethod("addressDetails", address, scriptPubkey, sort, limit, offset, assetName); +} + +function getAddressDeltas(address, scriptPubkey, sort, limit, offset, assetName) { + if(config.addressApi === "daemonRPC") { + scriptPubkey = null; + } + if(scriptPubkey) { + address = null; + } + return executeMethod("addressDeltas", address, scriptPubkey, sort, limit, offset, assetName); } function getAddressUTXOs(address, scriptPubkey) { + if(config.addressApi === "daemonRPC") { + scriptPubkey = null; + } if(scriptPubkey) { address = null; } @@ -126,6 +142,9 @@ function getAddressUTXOs(address, scriptPubkey) { } function getAddressBalance(address, scriptPubkey) { + if(config.addressApi === "daemonRPC") { + scriptPubkey = null; + } if(scriptPubkey) { address = null; } @@ -139,5 +158,6 @@ module.exports = { getCurrentAddressApiFeatureSupport: getCurrentAddressApiFeatureSupport, getAddressDetails: getAddressDetails, getAddressBalance : getAddressBalance, - getAddressUTXOs : getAddressUTXOs + getAddressUTXOs : getAddressUTXOs, + getAddressDeltas : getAddressDeltas }; diff --git a/app/api/coreApi.js b/app/api/coreApi.js index fbf5d5ba1..72940952f 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -9,69 +9,15 @@ var utils = require("../utils.js"); var config = require("../config.js"); var coins = require("../coins.js"); var redisCache = require("../redisCache.js"); +var Cache = require("./../cache.js"); var Decimal = require("decimal.js"); // choose one of the below: RPC to a node, or mock data while testing var rpcApi = require("./rpcApi.js"); -//var rpcApi = require("./mockApi.js"); - - -function onCacheEvent(cacheType, hitOrMiss, cacheKey) { - //debugLog(`cache.${cacheType}.${hitOrMiss}: ${cacheKey}`); -} - -function createMemoryLruCache(cacheObj) { - return { - get:function(key) { - return new Promise(function(resolve, reject) { - var val = cacheObj.get(key); - - if (val != null) { - onCacheEvent("memory", "hit", key); - - } else { - onCacheEvent("memory", "miss", key); - } - - resolve(cacheObj.get(key)); - }); - }, - set:function(key, obj, maxAge) { cacheObj.set(key, obj, maxAge); } - } -} - -var noopCache = { - get:function(key) { - return new Promise(function(resolve, reject) { - resolve(null); - }); - }, - set:function(key, obj, maxAge) {} -}; - -var miscCache = null; -var blockCache = null; -var txCache = null; - -if (config.noInmemoryRpcCache) { - miscCache = noopCache; - blockCache = noopCache; - txCache = noopCache; - -} else { - miscCache = createMemoryLruCache(new LRU(50)); - blockCache = createMemoryLruCache(new LRU(50)); - txCache = createMemoryLruCache(new LRU(200)); -} - -if (redisCache.active) { - miscCache = redisCache; - blockCache = redisCache; - txCache = redisCache; -} - - - +//var rpcApi = require("./mockApi.js") +var miscCache = new Cache(50); +var blockCache = new Cache(50); +var txCache = new Cache(200); function getGenesisBlockHash() { return coins[config.coin].genesisBlockHash; @@ -81,50 +27,6 @@ function getGenesisCoinbaseTransactionId() { return coins[config.coin].genesisCoinbaseTransactionId; } - - -function tryCacheThenRpcApi(cache, cacheKey, cacheMaxAge, rpcApiFunction, cacheConditionFunction) { - //debugLog("tryCache: " + cacheKey + ", " + cacheMaxAge); - if (cacheConditionFunction == null) { - cacheConditionFunction = function(obj) { - return true; - }; - } - - return new Promise(function(resolve, reject) { - var cacheResult = null; - - var finallyFunc = function() { - if (cacheResult != null) { - resolve(cacheResult); - - } else { - rpcApiFunction().then(function(rpcResult) { - if (rpcResult != null && cacheConditionFunction(rpcResult)) { - cache.set(cacheKey, rpcResult, cacheMaxAge); - } - - resolve(rpcResult); - - }).catch(function(err) { - reject(err); - }); - } - }; - - cache.get(cacheKey).then(function(result) { - cacheResult = result; - - finallyFunc(); - - }).catch(function(err) { - utils.logError("nds9fc2eg621tf3", err, {cacheKey:cacheKey}); - - finallyFunc(); - }); - }); -} - function shouldCacheTransaction(tx) { if (!tx.confirmations) { return false; @@ -141,50 +43,53 @@ function shouldCacheTransaction(tx) { return true; } - - function getBlockchainInfo() { - return tryCacheThenRpcApi(miscCache, "getBlockchainInfo", 10000, rpcApi.getBlockchainInfo); + return miscCache.tryCache("getBlockchainInfo", 10000, rpcApi.getBlockchainInfo); } function getBlockCount() { - return tryCacheThenRpcApi(miscCache, "getblockcount", 10000, rpcApi.getBlockCount); + return miscCache.tryCache("getblockcount", 10000, rpcApi.getBlockCount); } function getNetworkInfo() { - return tryCacheThenRpcApi(miscCache, "getNetworkInfo", 10000, rpcApi.getNetworkInfo); + return miscCache.tryCache("getNetworkInfo", 10000, rpcApi.getNetworkInfo); } function getNetTotals() { - return tryCacheThenRpcApi(miscCache, "getNetTotals", 10000, rpcApi.getNetTotals); + return miscCache.tryCache("getNetTotals", 10000, rpcApi.getNetTotals); } function getMempoolInfo() { - return tryCacheThenRpcApi(miscCache, "getMempoolInfo", 1000, rpcApi.getMempoolInfo); + return miscCache.tryCache("getMempoolInfo", 1000, rpcApi.getMempoolInfo); } function getMiningInfo() { - return tryCacheThenRpcApi(miscCache, "getMiningInfo", 30000, rpcApi.getMiningInfo); + return miscCache.tryCache("getMiningInfo", 30000, rpcApi.getMiningInfo); } function getUptimeSeconds() { - return tryCacheThenRpcApi(miscCache, "getUptimeSeconds", 1000, rpcApi.getUptimeSeconds); + return miscCache.tryCache("getUptimeSeconds", 1000, rpcApi.getUptimeSeconds); +} + +function getAddressDetails(address, scriptPubkey, sort, limit, offset, assetName) { + return miscCache.tryCache(`getAddressDetails-${address}-${assetName}-${sort}-${limit}-${offset}`, 300000, function() { + return rpcApi.getAddressDetails(address, scriptPubkey, sort, limit, offset, assetName); + }); } -function getAddressDetails(address, scriptPubkey, sort, limit, offset) { - return tryCacheThenRpcApi(miscCache, `getAddressDetails-${address}-${sort}-${limit}-${offset}`, 1200000, function() { - return rpcApi.getAddressDetails(address, scriptPubkey, sort, limit, offset); +function getAddressDeltas(address, scriptPubkey, sort, limit, offset, assetName) { + return miscCache.tryCache(`getAddressDeltas-${address}-${assetName}-${sort}`, 300000, function() { + return rpcApi.getAddressDeltas(address, scriptPubkey, sort, limit, offset, assetName); }); } function getAddressBalance(address, scriptPubkey) { - return tryCacheThenRpcApi(miscCache, "getAddressBalance-" + address, 1200000, function() { + return miscCache.tryCache("getAddressBalance-" + address, 300000, function() { return rpcApi.getAddressBalance(address, scriptPubkey); }); - } function getAddressUTXOs(address, scriptPubkey) { - return tryCacheThenRpcApi(miscCache, "getAddressUTXOs-" + address, 1200000, function() { + return miscCache.tryCache("getAddressUTXOs-" + address, 1200000, function() { return rpcApi.getAddressUTXOs(address, scriptPubkey); }); @@ -192,7 +97,7 @@ function getAddressUTXOs(address, scriptPubkey) { function getChainTxStats(blockCount) { - return tryCacheThenRpcApi(miscCache, "getChainTxStats-" + blockCount, 1200000, function() { + return miscCache.tryCache("getChainTxStats-" + blockCount, 1200000, function() { return rpcApi.getChainTxStats(blockCount); }); } @@ -267,7 +172,7 @@ function getTxCountStats(dataPtCount, blockStart, blockEnd) { function getPeerSummary() { return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "getpeerinfo", 1000, rpcApi.getPeerInfo).then(function(getpeerinfo) { + miscCache.tryCache("getpeerinfo", 1000, rpcApi.getPeerInfo).then(function(getpeerinfo) { var result = {}; result.getpeerinfo = getpeerinfo; @@ -348,7 +253,7 @@ function getPeerSummary() { function getMempoolDetails(start, count) { return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "getMempoolTxids", 1000, rpcApi.getMempoolTxids).then(function(resultTxids) { + miscCache.tryCache("getMempoolTxids", 1000, rpcApi.getMempoolTxids).then(function(resultTxids) { var txids = []; for (var i = start; (i < resultTxids.length && i < (start + count)); i++) { @@ -408,7 +313,7 @@ function getMempoolDetails(start, count) { function getMempoolStats() { return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "getRawMempool", 5000, rpcApi.getRawMempool).then(function(result) { + miscCache.tryCache("getRawMempool", 5000, rpcApi.getRawMempool).then(function(result) { var maxFee = 0; var maxFeePerByte = 0; var maxAge = 0; @@ -591,13 +496,13 @@ function getMempoolStats() { } function getBlockByHeight(blockHeight) { - return tryCacheThenRpcApi(blockCache, "getBlockByHeight-" + blockHeight, 3600000, function() { + return blockCache.tryCache("getBlockByHeight-" + blockHeight, 3600000, function() { return rpcApi.getBlockByHeight(blockHeight); }); } function getBlock(blockHeight) { - return tryCacheThenRpcApi(blockCache, "getBlock-" + blockHeight, 3600000, function() { + return blockCache.tryCache("getBlock-" + blockHeight, 3600000, function() { return rpcApi.getBlock(blockHeight); }); } @@ -619,7 +524,7 @@ function getBlocksByHeight(blockHeights) { } function getBlockByHash(blockHash) { - return tryCacheThenRpcApi(blockCache, "getBlockByHash-" + blockHash, 3600000, function() { + return blockCache.tryCache("getBlockByHash-" + blockHash, 3600000, function() { return rpcApi.getBlockByHash(blockHash); }); } @@ -651,7 +556,7 @@ function getRawTransaction(txid) { return rpcApi.getRawTransaction(txid); }; - return tryCacheThenRpcApi(txCache, "getRawTransaction-" + txid, 3600000, rpcApiFunction, shouldCacheTransaction); + return txCache.tryCache("getRawTransaction-" + txid, 3600000, rpcApiFunction, shouldCacheTransaction); } function getTxUtxos(tx) { @@ -673,7 +578,7 @@ function getTxUtxos(tx) { function getUtxo(txid, outputIndex) { return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "utxo-" + txid + "-" + outputIndex, 3600000, function() { + miscCache.tryCache("utxo-" + txid + "-" + outputIndex, 3600000, function() { return rpcApi.getUtxo(txid, outputIndex); }).then(function(result) { @@ -693,13 +598,13 @@ function getUtxo(txid, outputIndex) { } function getMempoolTxDetails(txid) { - return tryCacheThenRpcApi(miscCache, "mempoolTxDetails-" + txid, 3600000, function() { + return miscCache.tryCache("mempoolTxDetails-" + txid, 3600000, function() { return rpcApi.getMempoolTxDetails(txid); }); } function getAddress(address) { - return tryCacheThenRpcApi(miscCache, "getAddress-" + address, 3600000, function() { + return miscCache.tryCache("getAddress-" + address, 3600000, function() { return rpcApi.getAddress(address); }); } @@ -839,7 +744,7 @@ function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) { function getHelp() { return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "getHelp", 3600000, rpcApi.getHelp).then(function(helpContent) { + miscCache.tryCache("getHelp", 3600000, rpcApi.getHelp).then(function(helpContent) { var lines = helpContent.split("\n"); var sections = []; @@ -875,7 +780,7 @@ function getRpcMethodHelp(methodName) { }; return new Promise(function(resolve, reject) { - tryCacheThenRpcApi(miscCache, "getHelp-" + methodName, 3600000, rpcApiFunction).then(function(helpContent) { + miscCache.tryCache("getHelp-" + methodName, 3600000, rpcApiFunction).then(function(helpContent) { var output = {}; output.string = helpContent; @@ -952,7 +857,7 @@ function getRpcMethodHelp(methodName) { } function getSupply() { - return tryCacheThenRpcApi(miscCache, "getSupply", 1200000, function() { + return miscCache.tryCache("getSupply", 1200000, function() { return rpcApi.getSupply(); }); } @@ -999,5 +904,6 @@ module.exports = { getSupply : getSupply, getAddressDetails : getAddressDetails, getAddressUTXOs : getAddressUTXOs, - getAddressBalance : getAddressBalance + getAddressBalance : getAddressBalance, + getAddressDeltas : getAddressDeltas }; diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index d79f148bd..c636d02d3 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -65,31 +65,65 @@ function broadcast(rawtxhex) { return getRpcDataWithParams({method:"sendrawtransaction", parameters:[rawtxhex]}); } -function getAddressDetails(address, scriptPubkey, sort, limit, offset) { +function getAddressDeltas(address, scriptPubkey, sort, limit, offset, assetName = coins[config.coin].ticker) { return new Promise(function(resolve, reject) { - var promises = []; + var assetSupported = coins[config.coin].assetSupported ? true : false; + var promise; + if(assetSupported) { + promise = getRpcDataWithParams({method : "getaddressdeltas", parameters: [{addresses : [address], assetName : assetName}]}); + } else { + promise = getRpcDataWithParams({method : "getaddressdeltas", parameters: [{addresses : [address]}]}); + } + promise.then(addressDeltas => { + if (sort == "desc") { + addressDeltas.reverse(); + } + var end = Math.min(addressDeltas.length, limit + offset); + var result = { + txCount : addressDeltas.length, + txids : [], + blockHeightsByTxid : {} + } + addressDeltas = addressDeltas.slice(offset, end); + for (var i in addressDeltas) { + result.txids.push(addressDeltas[i].txid); + result.blockHeightsByTxid[addressDeltas[i].txid] = addressDeltas[i].height; + } + //console.log("getAddressDeltas ", result); + resolve({addressDeltas : result, errors : null}); + }).catch(reject); + }); +} +function getAddressDetails(address, scriptPubkey, sort, limit, offset, assetName = coins[config.coin].ticker) { + return new Promise(function(resolve, reject) { var txidData = null; var balanceData = null; var assetSupported = coins[config.coin].assetSupported ? true : false; // getBlockCount().then(currentHeight => { // promises.push(getRpcDataWithParams({method : "getaddresstxids", parameters: [{adddresses : [address]}]})); // }).catch(reject); - promises.push(getRpcDataWithParams({method : "getaddressdeltas", parameters: [{addresses : [address]}]})); + var promises = []; + if(assetSupported) { + promises.push(getRpcDataWithParams({method : "getaddressdeltas", parameters: [{addresses : [address], assetName : assetName}]})); + } else { + promises.push(getRpcDataWithParams({method : "getaddressdeltas", parameters: [{addresses : [address]}]})); + } promises.push(getRpcDataWithParams({method : "getaddressbalance", parameters: [{addresses : [address]},assetSupported]})); Promise.all(promises).then(function(results) { txidData = results[0]; if (sort == "desc") { txidData.reverse(); } - txidData = txidData.splice(0,100); + var end = Math.min(txidData.length, limit + offset); balanceData = results[1]; var addressDetails = { txCount : txidData.length, txids : [], blockHeightsByTxid : {} } - for (var i = offset; i < Math.min(txidData.length, limit + offset); i++) { + txidData = txidData.slice(offset, end); + for (var i in txidData) { addressDetails.txids.push(txidData[i].txid); addressDetails.blockHeightsByTxid[txidData[i].txid] = txidData[i].height; } @@ -429,5 +463,6 @@ module.exports = { broadcast : broadcast, getAddressDetails : getAddressDetails, getAddressUTXOs : getAddressUTXOs, - getAddressBalance : getAddressBalance + getAddressBalance : getAddressBalance, + getAddressDeltas : getAddressDeltas }; diff --git a/app/cache.js b/app/cache.js new file mode 100644 index 000000000..f1be9947f --- /dev/null +++ b/app/cache.js @@ -0,0 +1,99 @@ +const debug = require("debug"); +const debugLog = debug("btcexp:core"); +const LRU = require("lru-cache"); +const config = require("./config.js"); +const redisCache = require("./redisCache.js"); +class Cache { + constructor(maxCacheAmount) { + if (config.noInmemoryRpcCache) { + this.cacheObj = null; + } else if(redisCache.active) { + this.cacheObj = redisCache; + } else { + this.cacheObj = new LRU(maxCacheAmount); + } + } + + tryCache(cacheKey, cacheMaxAge, dataFunc, cacheConditionFunction = (obj) => {return true}) { + /*if (cacheConditionFunction == null) { + cacheConditionFunction = function(obj) { + return true; + }; + }*/ + var self = this; + return new Promise(function(resolve, reject) { + const finallyFunc = function() { + dataFunc().then(function(result) { + if (result != null && cacheConditionFunction(result)) { + // console.log("caching, key=%s,result=%O", cacheKey, result); + self.set(cacheKey, result, cacheMaxAge); + } + // console.log("null result ? %s----%O", cacheConditionFunction(result), result); + resolve(result); + + }).catch(function(err) { + console.log(err); + reject(err); + }); + }; + self.get(cacheKey).then(function(result) { + if(!result) { + finallyFunc(); + } else { + //console.log("cache hit %s=%O", cacheKey, result); + resolve(result); + } + }).catch(function(err) { + utils.logError("nds9fc2eg621tf3", err, {cacheKey:cacheKey}); + finallyFunc(); + }); + }); + } + onCacheEvent(cacheType, hitOrMiss, cacheKey) { + //debugLog(`cache.${cacheType}.${hitOrMiss}: ${cacheKey}`); + } + + LRUGet(key) { + var self = this; + return new Promise(function(resolve, reject) { + var val = self.cacheObj.get(key); + + if (val != null) { + self.onCacheEvent("memory", "hit", key); + + } else { + self.onCacheEvent("memory", "miss", key); + } + + resolve(self.cacheObj.get(key)); + }); + } + LRUSet(key, obj, maxAge) { + this.cacheObj.set(key, obj, maxAge); + } + + noopGet(key) { + return new Promise(function(resolve, reject) { + resolve(null); + }); + } + + get(key) { + if(this.cacheObj) { + if(redisCache.active) { + return redisCache.get(key); + } + return this.LRUGet(key); + } else { + return noopGet(key); + } + } + + set(key, obj, maxAge) { + if(this.cacheObj) { + this.cacheObj.set(key, obj, maxAge); + } + } +} + +module.exports = Cache; diff --git a/app/utils.js b/app/utils.js index e352b65b3..7e99e1f93 100644 --- a/app/utils.js +++ b/app/utils.js @@ -67,13 +67,13 @@ var ipCache = { function redirectToConnectPageIfNeeded(req, res) { if (!req.session.host) { req.session.redirectUrl = req.originalUrl; - + res.redirect("/"); res.end(); - + return true; } - + return false; } @@ -82,14 +82,14 @@ function hex2ascii(hex) { 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)); } @@ -99,28 +99,28 @@ function splitArrayIntoChunks(array, chunkSize) { 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; } @@ -148,7 +148,7 @@ function getCurrencyFormatInfo(formatType) { return null; } -function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedDecimalPlaces) { +function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, assetName, forcedDecimalPlaces) { var formatInfo = getCurrencyFormatInfo(formatType); if (formatInfo != null) { var dec = new Decimal(amount); @@ -161,11 +161,11 @@ function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedD if (forcedDecimalPlaces >= 0) { decimalPlaces = forcedDecimalPlaces; } - + //console.log("formatCurrencyAmountWithForcedDecimalPlaces assetName=", assetName); if (formatInfo.type == "native") { dec = dec.times(formatInfo.multiplier); - - return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + formatInfo.name; + var name = !assetName || assetName === coinConfig.ticker ? formatInfo.name : assetName; + return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + name; } else if (formatInfo.type == "exchanged") { if (global.exchangeRates != null && global.exchangeRates[formatInfo.multiplier] != null) { @@ -174,20 +174,20 @@ function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedD return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + formatInfo.name; } else { - return formatCurrencyAmountWithForcedDecimalPlaces(amount, coinConfig.defaultCurrencyUnit.name, forcedDecimalPlaces); + return formatCurrencyAmountWithForcedDecimalPlaces(amount, coinConfig.defaultCurrencyUnit.name, assetName, forcedDecimalPlaces); } } } - + return amount; } -function formatCurrencyAmount(amount, formatType) { - return formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, -1); +function formatCurrencyAmount(amount, formatType, assetName) { + return formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType,assetName, -1); } -function formatCurrencyAmountInSmallestUnits(amount, forcedDecimalPlaces) { - return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].baseCurrencyUnit.name, forcedDecimalPlaces); +function formatCurrencyAmountInSmallestUnits(amount, assetName, forcedDecimalPlaces) { + return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].baseCurrencyUnit.name, assetName, forcedDecimalPlaces); } // ref: https://stackoverflow.com/a/2901298/673828 @@ -241,7 +241,7 @@ 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]; @@ -275,10 +275,22 @@ function getMinerFromCoinbaseTx(tx) { return null; } -function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { +function getAssetValue(vout, assetName) { + if(assetName === coinConfig.ticker) { + return vout.value; + } + if(vout.scriptPubKey && vout.scriptPubKey.asset) { + return vout.scriptPubKey.asset.amount; + } + return 0; +} + +function getTxTotalInputOutputValues(tx, txInputs, blockHeight, assetName) { var totalInputValue = new Decimal(0); var totalOutputValue = new Decimal(0); - + if(!assetName) { + assetName = coinConfig.ticker; + } try { for (var i = 0; i < tx.vin.length; i++) { if (tx.vin[i].coinbase) { @@ -290,8 +302,9 @@ function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { if (txInput) { try { var vout = txInput.vout[tx.vin[i].vout]; - if (vout.value) { - totalInputValue = totalInputValue.plus(new Decimal(vout.value)); + var value = getAssetValue(vout, assetName); + if (value) { + totalInputValue = totalInputValue.plus(new Decimal(value)); } } catch (err) { logError("2397gs0gsse", err, {txid:tx.txid, vinIndex:i}); @@ -299,9 +312,10 @@ function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { } } } - + for (var i = 0; i < tx.vout.length; i++) { - totalOutputValue = totalOutputValue.plus(new Decimal(tx.vout[i].value)); + var value = getAssetValue(tx.vout[i], assetName); + totalOutputValue = totalOutputValue.plus(new Decimal(value)); } } catch (err) { logError("2308sh0sg44", err, {tx:tx, txInputs:txInputs, blockHeight:blockHeight}); @@ -369,12 +383,12 @@ function geoLocateIpAddresses(ipAddresses, provider) { 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) { @@ -422,7 +436,7 @@ function geoLocateIpAddresses(ipAddresses, provider) { function parseExponentStringDouble(val) { var [lead,decimal,pow] = val.toString().split(/e|\./); - return +pow <= 0 + 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))); } @@ -497,7 +511,7 @@ function logError(errorId, err, optionalUserData = null) { } debugErrorLog("Error " + errorId + ": " + err + ", json: " + JSON.stringify(err) + (optionalUserData != null ? (", userData: " + optionalUserData + " (json: " + JSON.stringify(optionalUserData) + ")") : "")); - + if (err && err.stack) { debugErrorLog("Stack: " + err.stack); } @@ -562,7 +576,7 @@ function getStatsSummary(json) { var price = `${formatExchangedCurrency(1.0, "btc", "฿", 8)}/${formatExchangedCurrency(1.0, "usd", "$", 6)}` mempoolBytesData[1].abbreviation = mempoolBytesData[1].abbreviation ? mempoolBytesData[1].abbreviation : ""; return { - hashrate : { + hashrate : { rate : hashrateData[0], unit : ` ${hashrateData[1].abbreviation}H = ${hashrateData[1].name}-hash (x10^${hashrateData[1].exponent})` }, diff --git a/package.json b/package.json index 9d6dc8b95..ca3ffee1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rpc-explorer", - "version": "1.2.0", + "version": "1.2.1", "description": "Explorer for Bitcoin and RPC-compatible blockchains", "private": false, "bin": "bin/cli.js", diff --git a/public/js/jquery-3.4.1.min.js b/public/js/jquery-3.4.1.min.js new file mode 100644 index 000000000..a1c07fd80 --- /dev/null +++ b/public/js/jquery-3.4.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0 0) { if(isHTML) { element.html(value); @@ -16,6 +16,27 @@ function updateElementAttr(id, attrName, value) { } } +function ajaxUpdate(uri, id) { + updateElementValue(id, `
+ Loading... +
`, true); + $.ajax({url: uri, success: function(html) { + updateElementValue(id, html, true); + } + }); +} + +function loadLazyContainers() { + var lazyElements = $(".lazyload"); + for(var i=0; i < lazyElements.length; i++) { + var ele = $(lazyElements[i]); + var loadUrl = ele.attr('loadurl'); + var paranetEle = ele.parent(); + console.log("loadUrl=",loadUrl); + ajaxUpdate(loadUrl, paranetEle); + } +} + function updateStats() { setInterval( function() { var checkEle = $("#hashrate"); @@ -36,4 +57,4 @@ function updateStats() { }}); } }, 180000); -} \ No newline at end of file +} diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index 3109622b4..d2ac1e9cf 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -22,6 +22,7 @@ var addressApi = require("./../app/api/addressApi.js"); var Session = require("./session.js"); const forceCsrf = csurf({ ignoreMethods: [] }); +const pug = require('pug'); var routing = function(path, method, sessionMethod, hashNext = true) { if(hashNext) { @@ -550,287 +551,14 @@ router.get("/tx/:transactionId", function(req, res, next) { }); }); -router.get("/address/:address", function(req, res, next) { - var limit = config.site.addressTxPageSize; - var offset = 0; - var sort = "desc"; - - - 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; - } - - - var address = req.params.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 = {}; - - try { - res.locals.addressObj = bitcoinjs.address.fromBase58Check(address); - - } catch (err) { - if (!err.toString().startsWith("Error: Non-base58 character")) { - res.locals.pageErrors.push(utils.logError("u3gr02gwef", err)); - } - - try { - res.locals.addressObj = bitcoinjs.address.fromBech32(address); - - } catch (err2) { - res.locals.pageErrors.push(utils.logError("u02qg02yqge", err)); - } - } - - if (global.miningPoolsConfigs) { - for (var i = 0; i < global.miningPoolsConfigs.length; i++) { - if (global.miningPoolsConfigs[i].payout_addresses[address]) { - res.locals.payoutAddressForMiner = global.miningPoolsConfigs[i].payout_addresses[address]; - } - } - } - - coreApi.getAddress(address).then(function(validateaddressResult) { - res.locals.result.validateaddress = validateaddressResult; - - var promises = []; - if (!res.locals.crawlerBot) { - var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey))); - addrScripthash = addrScripthash.match(/.{2}/g).reverse().join(""); - - res.locals.electrumScripthash = addrScripthash; - - promises.push(new Promise(function(resolve, reject) { - addressApi.getAddressDetails(address, validateaddressResult.scriptPubKey, sort, limit, offset).then(function(addressDetailsResult) { - var 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) { - var txids = addressDetails.txids; - - // if the active addressApi gives us blockHeightsByTxid, it saves us work, so try to use it - var blockHeightsByTxid = {}; - if (addressDetails.blockHeightsByTxid) { - blockHeightsByTxid = addressDetails.blockHeightsByTxid; - } - - res.locals.txids = txids; - - coreApi.getRawTransactionsWithInputs(txids).then(function(rawTxResult) { - 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 - var coinbaseTxs = []; - for (var i = 0; i < rawTxResult.transactions.length; i++) { - var tx = rawTxResult.transactions[i]; - - for (var 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); - } - } - } - } - - - var coinbaseTxBlockHashes = []; - var blockHashesByTxid = {}; - coinbaseTxs.forEach(function(tx) { - coinbaseTxBlockHashes.push(tx.blockhash); - blockHashesByTxid[tx.txid] = tx.blockhash; - }); - - var blockHeightsPromises = []; - if (coinbaseTxs.length > 0) { - // we need to query some blockHeights by hash for some coinbase txs - blockHeightsPromises.push(new Promise(function(resolve2, reject2) { - coreApi.getBlocksByHash(coinbaseTxBlockHashes).then(function(blocksByHashResult) { - for (var txid in blockHashesByTxid) { - if (blockHashesByTxid.hasOwnProperty(txid)) { - blockHeightsByTxid[txid] = blocksByHashResult[blockHashesByTxid[txid]].height; - } - } - - resolve2(); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("78ewrgwetg3", err)); - - reject2(err); - }); - })); - } - - Promise.all(blockHeightsPromises).then(function() { - var addrGainsByTx = {}; - var addrLossesByTx = {}; - - res.locals.addrGainsByTx = addrGainsByTx; - res.locals.addrLossesByTx = addrLossesByTx; - - var handledTxids = []; - - for (var i = 0; i < rawTxResult.transactions.length; i++) { - var tx = rawTxResult.transactions[i]; - var txInputs = rawTxResult.txInputsByTransaction[tx.txid]; - - if (handledTxids.includes(tx.txid)) { - continue; - } - - handledTxids.push(tx.txid); - - for (var j = 0; j < tx.vout.length; j++) { - if (tx.vout[j].value > 0 && tx.vout[j].scriptPubKey && tx.vout[j].scriptPubKey.addresses && tx.vout[j].scriptPubKey.addresses.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 (var j = 0; j < tx.vin.length; j++) { - var txInput = txInputs[j]; - var vinJ = tx.vin[j]; - - if (txInput != null) { - if (txInput.vout[vinJ.vout] && txInput.vout[vinJ.vout].scriptPubKey && txInput.vout[vinJ.vout].scriptPubKey.addresses && txInput.vout[vinJ.vout].scriptPubKey.addresses.includes(address)) { - if (addrLossesByTx[tx.txid] == null) { - addrLossesByTx[tx.txid] = new Decimal(0); - } - - addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(new Decimal(txInput.vout[vinJ.vout].value)); - } - } - } - - //debugLog("tx: " + JSON.stringify(tx)); - //debugLog("txInputs: " + JSON.stringify(txInputs)); - } - - res.locals.blockHeightsByTxid = blockHeightsByTxid; - - resolve(); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("230wefrhg0egt3", err)); - - reject(err); - }); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("asdgf07uh23", err)); - - reject(err); - }); - - } else { - // no addressDetails.txids available - resolve(); - } - } else { - // no addressDetails available - resolve(); - } - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("23t07ug2wghefud", err)); - - res.locals.addressApiError = err; - - reject(err); - }); - })); - - promises.push(new Promise(function(resolve, reject) { - coreApi.getBlockchainInfo().then(function(getblockchaininfo) { - res.locals.getblockchaininfo = getblockchaininfo; - - resolve(); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("132r80h32rh", err)); - - reject(err); - }); - })); - } - - promises.push(new Promise(function(resolve, reject) { - qrcode.toDataURL(address, function(err, url) { - if (err) { - res.locals.pageErrors.push(utils.logError("93ygfew0ygf2gf2", err)); - } - - res.locals.addressQrCodeUrl = url; - - resolve(); - }); - })); - - Promise.all(promises.map(utils.reflectPromise)).then(function() { - res.render("address"); - - next(); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("32197rgh327g2", err)); - - res.render("address"); - - next(); - }); - - }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("2108hs0gsdfe", err, {address:address})); - - res.locals.userMessage = "Failed to load address " + address + " (" + err + ")"; - - res.render("address"); +router.get("/addressview/:address", (req, res, next) => { + var session = new Session(req, res, next, config); + session.renderAddressView(coins[config.coin].assetSupported); +}); - next(); - }); +router.get("/address/:address", function(req, res, next) { + var session = new Session(req, res, next, config); + session.renderAddressPage(coins[config.coin].assetSupported); }); router.get("/rpc-terminal", function(req, res, next) { diff --git a/routes/session.js b/routes/session.js index 915c6e9c7..fb4a094d0 100644 --- a/routes/session.js +++ b/routes/session.js @@ -1,12 +1,26 @@ -var coreApi = require("./../app/api/coreApi.js"); -var utils = require("./../app/utils.js"); +const coreApi = require("./../app/api/coreApi.js"); +const addressApi = require("./../app/api/addressApi.js"); +const utils = require("./../app/utils.js"); +const debug = require("debug"); +const debugLog = debug("btcexp:core"); +const bitcoinjs = require('bitcoinjs-lib'); +const Cache = require("./../app/cache.js"); +const util = require('util'); +const moment = require('moment'); +const pug = require('pug'); +const sha256 = require("crypto-js/sha256"); +const hexEnc = require("crypto-js/enc-hex"); +const qrcode = require('qrcode'); +const Decimal = require("decimal.js"); +const htmlViewCache = new Cache(100); class Session { - constructor(req, res, next) { + constructor(req, res, next, config) { this.req = req; this.res = res; this.next = next; + this.config = config; } - + isRenderConnect() { if (this.req.session.host == null || this.req.session.host.trim() == "") { if (this.req.cookies['rpc-host']) { @@ -27,7 +41,344 @@ class Session { } return false; } - + + parseAddressRequest(assetSupported) { + var limit = this.config.site.addressTxPageSize; + var offset = 0; + var sort = "desc"; + var assetName; + if (this.req.query.limit) { + limit = parseInt(this.req.query.limit); + } + + if (this.req.query.offset) { + offset = parseInt(this.req.query.offset); + } + + if (this.req.query.sort) { + sort =this. req.query.sort; + } + + var address = this.req.params.address; + this.res.locals.address = address; + this.res.locals.limit = limit; + this.res.locals.offset = offset; + this.res.locals.sort = sort; + this.res.locals.transactions = []; + this.res.locals.addressApiSupport = addressApi.getCurrentAddressApiFeatureSupport(); + this.res.locals.result = {}; + try { + this.res.locals.addressObj = bitcoinjs.address.fromBase58Check(address); + } catch (err) { + if (!err.toString().startsWith("Error: Non-base58 character")) { + this.res.locals.pageErrors.push(utils.logError("u3gr02gwef", err)); + } + try { + this.res.locals.addressObj = bitcoinjs.address.fromBech32(address); + } catch (err2) { + this.res.locals.pageErrors.push(utils.logError("u02qg02yqge", err)); + } + } + if (global.miningPoolsConfigs) { + for (var i = 0; i < global.miningPoolsConfigs.length; i++) { + if (global.miningPoolsConfigs[i].payout_addresses[address]) { + this.res.locals.payoutAddressForMiner = global.miningPoolsConfigs[i].payout_addresses[address]; + } + } + } + if(assetSupported) { + if(this.req.query.assetName) { + assetName = this.req.query.assetName; + this.res.locals.paginationBaseUrl = `/addressview/${address}?assetName=${assetName}&sort=${sort}`; + this.res.locals.assetName = assetName; + this.res.locals.dynamicContainerId = `tab-${assetName}` + } else { + this.res.locals.paginationBaseUrl = `/addressview/${address}?assetName=${this.config.coin}&sort=${sort}`; + this.res.locals.dynamicContainerId = `tab-${this.config.coin}` + } + } else { + this.res.locals.paginationBaseUrl = `/addressview/${address}?sort=${sort}`; + this.res.locals.dynamicContainerId = "trans-history"; + } + } + getTransactionsDetail(txids, assetName) { + var self = this; + var result = this.res.locals; + return new Promise(function(resolve, reject) { + result = Object.assign(result, { + transactions : [], + txInputsByTransaction : null, + blockHeightsByTxid : {}, + }); + coreApi.getRawTransactionsWithInputs(txids).then(function(rawTxResult) { + result.transactions = rawTxResult.transactions; + result.txInputsByTransaction = rawTxResult.txInputsByTransaction; + // for coinbase txs, we need the block height in order to calculate subsidy to display + var coinbaseTxs = []; + for (var i = 0; i < rawTxResult.transactions.length; i++) { + var tx = rawTxResult.transactions[i]; + for (var 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 (!result.blockHeightsByTxid[tx.txid]) { + coinbaseTxs.push(tx); + } + } + } + } + var coinbaseTxBlockHashes = []; + var blockHashesByTxid = {}; + coinbaseTxs.forEach(function(tx) { + coinbaseTxBlockHashes.push(tx.blockhash); + blockHashesByTxid[tx.txid] = tx.blockhash; + }); + var blockHeightsPromises = []; + if (coinbaseTxs.length > 0) { + // we need to query some blockHeights by hash for some coinbase txs + blockHeightsPromises.push(new Promise(function(resolve2, reject2) { + coreApi.getBlocksByHash(coinbaseTxBlockHashes).then(function(blocksByHashResult) { + for (var txid in blockHashesByTxid) { + if (blockHashesByTxid.hasOwnProperty(txid)) { + result.blockHeightsByTxid[txid] = blocksByHashResult[blockHashesByTxid[txid]].height; + } + } + resolve2(); + }).catch(function(err) { + reject2({msg : utils.logError("78ewrgwetg3", err), err : err}); + }); + })); + } + + Promise.all(blockHeightsPromises).then(function() { + self.processRawTx(result, assetName, rawTxResult); + resolve(); + + }).catch(function(err) { + reject({msg : utils.logError("230wefrhg0egt3", err), err : err}); + }); + + }).catch(function(err) { + reject({msg : utils.logError("asdgf07uh23", err), err : err}); + }); + }); + } + + processRawTx(data, assetName, rawTxResult) { + var addrGainsByTx = {}; + var addrLossesByTx = {}; + data.addrGainsByTx = addrGainsByTx; + data.addrLossesByTx = addrLossesByTx; + var handledTxids = []; + for (var i = 0; i < rawTxResult.transactions.length; i++) { + var tx = rawTxResult.transactions[i]; + var txInputs = rawTxResult.txInputsByTransaction[tx.txid]; + + if (handledTxids.includes(tx.txid)) { + continue; + } + handledTxids.push(tx.txid); + for (var j = 0; j < tx.vout.length; j++) { + var isThiAddress; + var value = this.getVoutValue(tx.vout[j], data.address, assetName); + if (value) { + if (addrGainsByTx[tx.txid] == null) { + addrGainsByTx[tx.txid] = new Decimal(0); + } + addrGainsByTx[tx.txid] = addrGainsByTx[tx.txid].plus(value); + } + } + for (var j = 0; j < tx.vin.length; j++) { + var txInput = txInputs[j]; + var vinJ = tx.vin[j]; + + if (txInput != null) { + var value = this.getVoutValue(txInput.vout[vinJ.vout], data.address, assetName); + if (value) { + if (addrLossesByTx[tx.txid] == null) { + addrLossesByTx[tx.txid] = new Decimal(0); + } + + addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(value); + } + } + } + //debugLog("tx: " + JSON.stringify(tx)); + //debugLog("txInputs: " + JSON.stringify(txInputs)); + } + } + getVoutValue(vout, address, assetName) { + var value = null; + if(assetName === this.config.coin) { + if(vout && vout.value > 0 && vout.scriptPubKey && vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.includes(address)) { + value = new Decimal(vout.value); + //console.log("if " + value); + } + + } else { + //console.log(vout); + if(vout && vout.scriptPubKey && vout.scriptPubKey.asset && vout.scriptPubKey.asset.name === assetName && + vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.includes(address)) { + value = new Decimal(vout.scriptPubKey.asset.amount); + } + } + return value; + } + + getAddressMetaData(result) { + var assetName = result.assetName ? result.assetName : this.config.coin; + var address = result.address; + var self = this; + return new Promise(function(resolve, reject) { + coreApi.getAddress(address).then(function(validateaddressResult) { + result.result.validateaddress = validateaddressResult; + if (!result.crawlerBot) { + var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey))); + addrScripthash = addrScripthash.match(/.{2}/g).reverse().join(""); + result.electrumScripthash = addrScripthash; + addressApi.getAddressDeltas(address, validateaddressResult.scriptPubKey, result.sort, + result.limit, result.offset, assetName).then(addressResult => { + var addressDetails = addressResult.addressDeltas; + if (addressResult.errors) { + result.addressDetailsErrors = addressResult.errors; + } + if(addressDetails) { + result.addressDetails = addressDetails; + if (addressDetails.txCount == 0) { + // make sure txCount=0 pass the falsey check in the UI + addressDetails.txCount = "0"; + } + if (addressDetails.txids) { + var txids = addressDetails.txids; + // if the active addressApi gives us blockHeightsByTxid, it saves us work, so try to use it + result.blockHeightsByTxid = {}; + if (addressDetails.blockHeightsByTxid) { + result.blockHeightsByTxid = addressDetails.blockHeightsByTxid; + } + result.txids = txids; + self.getTransactionsDetail(txids, assetName).then(() => { + resolve(); + }).catch(reject); + + } else { + // no addressDetails.txids available + resolve(); + } + } else { + resolve(); + } + }).catch(err => { + result.pageErrors.push(utils.logError("23t07ug2wghefud", err)); + result.addressApiError = err; + reject(err); + }); + } + }); + }); + } + + compileView(query, viewUri, cacheKey, viewDataPromise) { + var self = this; + return htmlViewCache.tryCache(cacheKey, 300000, () => { + return new Promise((resolve, reject) => { + viewDataPromise(query).then(() => { + query.moment = moment; + query.utils = utils; + //console.log("query ", query); + var htmlView = pug.compileFile(`${__dirname}/../views/${viewUri}`)(query); + resolve(htmlView) + }).catch(err=> { + query.pageErrors.push(utils.logError("2108hs0gsdfe", err, {address:query.address})); + query.userMessage = "Failed to load address " + query.address + " (" + err + ")"; + self.res.send(query); + self.next(); + }); + }); + }); + } + + renderDynamicView(query, viewUri, cacheKey, viewDataPromise) { + var self = this; + this.compileView(query,viewUri, cacheKey, viewDataPromise).then((result) => { + if(result) { + self.res.send(result); + self.next(); + } else { + self.res.send("No Trnsactions"); + self.next(); + } + }).catch(err => { + query.pageErrors.push(utils.logError("2108hs0gsdfe", err, {address:query.address})); + query.userMessage = "Failed to load address " + address + " (" + err + ")"; + self.res.send(query); + self.next(); + }); + } + renderTransactions(assetSupported) { + var cacheKey; + var query = this.res.locals; + if(assetSupported) { + cacheKey = `address-transaction-view-${query.address}-${query.assetName}-${query.sort}-${query.limit}-${query.offset}` + } else { + cacheKey = `address-transaction-view-${query.address}-${query.sort}-${query.limit}-${query.offset}` + } + this.renderDynamicView(query, "includes/address-transaction.pug", cacheKey, this.getAddressMetaData.bind(this)); + } + + renderAddressView(assetSupported) { + this.parseAddressRequest(assetSupported); + this.renderTransactions(assetSupported); + } + + renderAddressPage(assetSupported) { + this.parseAddressRequest(assetSupported); + var self = this; + this.getAddressSummary().then(() => { + qrcode.toDataURL(this.res.locals.address, function(err, url) { + if (err) { + self.res.locals.pageErrors.push(utils.logError("93ygfew0ygf2gf2", err)); + } + self.res.locals.addressQrCodeUrl = url; + self.res.render("address"); + self.next(); + }); + }).catch(err => { + self.res.locals.pageErrors.push(utils.logError("2108hs0gsdfe", err, {address:self.res.locals.address})); + self.res.locals.userMessage = "Failed to load address " + self.res.locals.address + " (" + err + ")"; + self.res.render("address"); + self.next(); + }); + } + + getAddressSummary() { + var result = this.res.locals; + var assetName = result.assetName ? result.assetName : this.config.coin; + var address = result.address; + return new Promise(function(resolve, reject) { + coreApi.getAddress(address).then(function(validateaddressResult) { + result.result.validateaddress = validateaddressResult; + if (!result.crawlerBot) { + var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey))); + addrScripthash = addrScripthash.match(/.{2}/g).reverse().join(""); + + result.electrumScripthash = addrScripthash; + addressApi.getAddressBalance(address, addrScripthash).then(balData => { + result.addressDetails = {}; + if(balData.length) { + result.addressDetails.assets = {}; + for(var bIndex in balData) { + var bal = balData[bIndex]; + result.addressDetails.assets[bal.assetName] = bal.balance; + } + } else { + result.addressDetails.balanceSet = balData.balance; + } + resolve(); + }).catch(reject); + } + }); + }); + } + renderHome() { this.res.locals.homepage = true; var promises = []; @@ -97,7 +448,7 @@ class Session { self.next(); }); } - + getNetworkSummary() { var promises = []; @@ -154,4 +505,4 @@ class Session { } } -module.exports = Session; \ No newline at end of file +module.exports = Session; diff --git a/views/address.pug b/views/address.pug index 9c8604d66..7248311e3 100644 --- a/views/address.pug +++ b/views/address.pug @@ -182,12 +182,11 @@ block content include includes/value-display.pug if (addressDetails && addressDetails.assets) each bal, assetName in addressDetails.assets - if(bal != 0) - div(class="row") - div(class="summary-split-table-label") #{assetName} Balance - div(class="summary-split-table-content monospace") - - var value = new Decimal(bal).dividedBy(coinConfig.baseCurrencyUnit.multiplier); - span #{value} + div(class="row") + div(class="summary-split-table-label") #{assetName} Balance + div(class="summary-split-table-content monospace") + - var value = new Decimal(bal).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + span #{value} if (addressDetails && addressDetails.txCount) div(class="row") @@ -243,164 +242,30 @@ block content i(class="fas fa-check text-success") else i(class="fas fa-times text-warning") - - div(class="card mb-3 shadow-sm") - div(class="card-header clearfix") - div(class="float-left") - span(class="h6") - if (addressDetails && addressDetails.txCount) - if (addressDetails.txCount == 1) - span 1 Transaction - else - span #{addressDetails.txCount.toLocaleString()} Transactions - else - span Transactions - - if (config.addressApi) - if (config.addressApi == "electrumx") - small.text-muted.border-dotted.ml-2(title=`The list of transaction IDs for this address was queried from ElectrumX (using the configured server(s))` data-toggle="tooltip") Trust Note - else - small.text-muted.border-dotted.ml-2(title=`The list of transaction IDs for this address was queried from ${config.addressApi}` data-toggle="tooltip") Trust Note - - - if (!crawlerBot && txids && txids.length > 1 && addressApiSupport.sortDesc && addressApiSupport.sortAsc) - div(class="float-right") - a(href="#", class="pull-right dropdown-toggle", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") - if (sort == "desc") - span Newest First - else - span Oldest First - - div(class="dropdown-menu dropdown-menu-right") - a(href=("/address/" + address), class="dropdown-item") - if (sort == "desc") - i(class="fa fa-check") - span Newest First - a(href=("/address/" + address + "?sort=asc"), class="dropdown-item") - if (sort != "desc") - i(class="fa fa-check") - span Oldest First - - else if (txids && txids.length > 1 && addressApiSupport.sortDesc && !addressApiSupport.sortAsc) - div.float-right - span.text-muted Newest First - - div(class="card-body") - if (conflictedTxidResults) - div(class="alert alert-warning", style="padding-bottom: 0;") - div(class="float-left", style="width: 55px; height: 50px; font-size: 18px;") - i(class="fas fa-exclamation-triangle fa-2x", style="margin-top: 10px;") - h4(class="alert-heading h6 font-weight-bold") Trust Warning - p - span The transaction history for this address was requested from mulitple ElectrumX servers and the results did not match. The results below were obtained only from - span(class="font-weight-bold") #{electrumHistory.server} - - - if (true) - if (addressApiError && addressApiError.error && addressApiError.error.code && addressApiError.error.code == -32600) - span Failed to retrieve transaction history from ElectrumX. See - a(href="https://github.com/janoside/btc-rpc-explorer/issues/67") Issue #67 - span for more information. - - - else if (addressApiError && addressApiError.userText) - div.text-danger Error: #{addressApiError.userText} - - else if (addressDetailsErrors && addressDetailsErrors.length > 0) - each err in addressDetailsErrors - if (err.e && err.e.error && err.e.error.message == "history too large") - span Failed to retrieve transaction history from ElectrumX. See - a(href="https://github.com/janoside/btc-rpc-explorer/issues/67") Issue #67 - span for more information. - - else if (err == "No address API configured") - span No address API is configured. See - a(href="https://github.com/janoside/btc-rpc-explorer/blob/master/.env-sample") the example configuration file - span for help setting up an address API if desired. - - else if (transactions.length == 0) - span No transactions found - - each tx, txIndex in transactions - //pre - // code.json.bg-light #{JSON.stringify(tx, null, 4)} - div(class=("xcard bg-light rounded shadow-sm " + ((txIndex < (transactions.length - 1) || txids.length > limit) ? "mb-4" : ""))) - div(class="card-header monospace clearfix") - div(class="float-left", style="margin-right: 0px;") - if (sort == "desc") - span ##{(addressDetails.txCount - offset - txIndex).toLocaleString()} - else - span ##{(offset + txIndex + 1).toLocaleString()} - span – - - div(class="row") - div(class="col-md-6") - if (tx && tx.txid) - a(href=("/tx/" + tx.txid)) #{tx.txid} - - if (global.specialTransactions && global.specialTransactions[tx.txid]) - span - a(data-toggle="tooltip", title=(coinConfig.name + " Fun! See transaction for details")) - i(class="fas fa-certificate text-primary") - - div(class="col-md-6") - div(class="text-md-right") - if (tx.time) - span #{moment.utc(new Date(tx["time"] * 1000)).format("Y-MM-DD HH:mm:ss")} utc - - var timeAgoTime = tx.time; - include includes/time-ago.pug - - else - span(class="text-danger") Unconfirmed - - div(class="col") - if (addrGainsByTx[tx.txid]) - - var currencyValue = addrGainsByTx[tx.txid]; - span(class="text-success") + - include includes/value-display.pug - - if (addrLossesByTx[tx.txid]) - span / - - if (addrLossesByTx[tx.txid]) - - var currencyValue = addrLossesByTx[tx.txid]; - span(class="text-danger") - - include includes/value-display.pug - - div(class="card-body") - if (true) - - var txInputs = txInputsByTransaction[tx.txid]; - - var blockHeight = blockHeightsByTxid[tx.txid]; - - var txIOHighlightAddress = address; - - include includes/transaction-io-details.pug - - else - p Since this explorer is database-free, it doesn't natively support address transaction history. However, you can configure it to communicate with one or more ElectrumX servers to build and display this data. In doing so, you should be aware that you'll be trusting those ElectrumX servers. If you configure multiple servers the results obtained from each will be cross-referenced against the others. Communicating with ElectrumX servers will also impact your privacy since the servers will know what addresses you're interested in. If these tradeoffs are acceptable, you can see a list of public ElectrumX servers here: - a(href="https://uasf.saltylemon.org/electrum") https://uasf.saltylemon.org/electrum - - if (false) - pre - code.json.bg-light #{JSON.stringify(transactions, null, 4)} - - if (!crawlerBot && addressDetails && addressDetails.txCount > limit) - - var txCount = addressDetails.txCount; - - var pageNumber = offset / limit + 1; - - var pageCount = Math.floor(txCount / limit); - - if (pageCount * limit < txCount) { - - pageCount++; - - } - - var paginationUrlFunction = function(x) { - - return paginationBaseUrl + "&limit=" + limit + "&offset=" + ((x - 1) * limit); - - } - - hr.mt-3 - - include includes/pagination.pug - - - - + - var hasAsset = addressDetails && addressDetails.assets && Object.keys(addressDetails.assets).length > 0; + if (hasAsset) + ul(class='nav nav-tabs mb-3') + li(class="nav-item") + a(data-toggle="tab", href=`#tab-${coinConfig.ticker}`, class="nav-link active", role="tab", onclick=`ajaxUpdate("/addressview/${address}?assetName=${coinConfig.ticker}", "tab-${coinConfig.ticker}")`) #{coinConfig.ticker} + each bal, assetName in addressDetails.assets + if(assetName !== coinConfig.ticker) + - var hrefTab = `#tab-${assetName}` + li(class="nav-item") + a(data-toggle="tab", href=hrefTab, class="nav-link", role="tab", onclick=`ajaxUpdate("/addressview/${address}?assetName=${assetName}", "tab-${assetName}")`) #{assetName} + if (hasAsset) + div(class="tab-content") + div(id=`tab-${coinConfig.ticker}`, class="tab-pane active", role="tabpanel") + div(class="spinner-border lazyload", loadurl=`${paginationBaseUrl}`, role="status") + span(class="sr-only") Loading... + each bal, assetName in addressDetails.assets + if(assetName !== coinConfig.ticker) + div(id=`tab-${assetName}`, class="tab-pane", role="tabpanel") + div(class="spinner-border" role="status") + span(class="sr-only") Loading... + else + div(id="trans-history" class="card mb-3 shadow-sm") + div(class="spinner-border lazyload", loadurl=`${paginationBaseUrl}`, role="status") + span(class="sr-only") Loading... div(id="tab-json", class="tab-pane", role="tabpanel") div(class="highlight") h4 validateaddress diff --git a/views/includes/address-transaction.pug b/views/includes/address-transaction.pug new file mode 100644 index 000000000..c1145f7fd --- /dev/null +++ b/views/includes/address-transaction.pug @@ -0,0 +1,159 @@ +div(class="card mb-3 shadow-sm") + div(class="card-header clearfix") + div(class="float-left") + span(class="h6") + if (addressDetails && addressDetails.txCount) + if (addressDetails.txCount == 1) + span 1 Transaction + else + span #{addressDetails.txCount.toLocaleString()} Transactions + else + span Transactions + + if (config.addressApi) + if (config.addressApi == "electrumx") + small.text-muted.border-dotted.ml-2(title=`The list of transaction IDs for this address was queried from ElectrumX (using the configured server(s))` data-toggle="tooltip") Trust Note + else + small.text-muted.border-dotted.ml-2(title=`The list of transaction IDs for this address was queried from ${config.addressApi}` data-toggle="tooltip") Trust Note + + + if (!crawlerBot && txids && txids.length > 1 && addressApiSupport.sortDesc && addressApiSupport.sortAsc) + div(class="float-right") + a(href="#", class="pull-right dropdown-toggle", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") + if (sort == "desc") + span Newest First + else + span Oldest First + + div(class="dropdown-menu dropdown-menu-right") + a(href=("/address/" + address), class="dropdown-item") + if (sort == "desc") + i(class="fa fa-check") + span Newest First + a(href=("/address/" + address + "?sort=asc"), class="dropdown-item") + if (sort != "desc") + i(class="fa fa-check") + span Oldest First + + else if (txids && txids.length > 1 && addressApiSupport.sortDesc && !addressApiSupport.sortAsc) + div.float-right + span.text-muted Newest First + + div(class="card-body") + if (conflictedTxidResults) + div(class="alert alert-warning", style="padding-bottom: 0;") + div(class="float-left", style="width: 55px; height: 50px; font-size: 18px;") + i(class="fas fa-exclamation-triangle fa-2x", style="margin-top: 10px;") + h4(class="alert-heading h6 font-weight-bold") Trust Warning + p + span The transaction history for this address was requested from mulitple ElectrumX servers and the results did not match. The results below were obtained only from + span(class="font-weight-bold") #{electrumHistory.server} + + + if (true) + if (addressApiError && addressApiError.error && addressApiError.error.code && addressApiError.error.code == -32600) + span Failed to retrieve transaction history from ElectrumX. See + a(href="https://github.com/janoside/btc-rpc-explorer/issues/67") Issue #67 + span for more information. + + + else if (addressApiError && addressApiError.userText) + div.text-danger Error: #{addressApiError.userText} + + else if (addressDetailsErrors && addressDetailsErrors.length > 0) + each err in addressDetailsErrors + if (err.e && err.e.error && err.e.error.message == "history too large") + span Failed to retrieve transaction history from ElectrumX. See + a(href="https://github.com/janoside/btc-rpc-explorer/issues/67") Issue #67 + span for more information. + + else if (err == "No address API configured") + span No address API is configured. See + a(href="https://github.com/janoside/btc-rpc-explorer/blob/master/.env-sample") the example configuration file + span for help setting up an address API if desired. + + else if (transactions.length == 0) + span No transactions found + + each tx, txIndex in transactions + //pre + // code.json.bg-light #{JSON.stringify(tx, null, 4)} + div(class=("xcard bg-light rounded shadow-sm " + ((txIndex < (transactions.length - 1) || txids.length > limit) ? "mb-4" : ""))) + div(class="card-header monospace clearfix") + div(class="float-left", style="margin-right: 0px;") + if (sort == "desc") + span ##{(addressDetails.txCount - offset - txIndex).toLocaleString()} + else + span ##{(offset + txIndex + 1).toLocaleString()} + span – + + div(class="row") + div(class="col-md-6") + if (tx && tx.txid) + a(href=("/tx/" + tx.txid)) #{tx.txid} + + if (global.specialTransactions && global.specialTransactions[tx.txid]) + span + a(data-toggle="tooltip", title=(coinConfig.name + " Fun! See transaction for details")) + i(class="fas fa-certificate text-primary") + + div(class="col-md-6") + div(class="text-md-right") + if (tx.time) + span #{moment.utc(new Date(tx["time"] * 1000)).format("Y-MM-DD HH:mm:ss")} utc + - var timeAgoTime = tx.time; + include time-ago.pug + + else + span(class="text-danger") Unconfirmed + + div(class="col") + if (addrGainsByTx[tx.txid]) + - var currencyValue = addrGainsByTx[tx.txid]; + - var tokenName = assetName; + span(class="text-success") + + include value-display.pug + + if (addrLossesByTx[tx.txid]) + span / + + if (addrLossesByTx[tx.txid]) + - var currencyValue = addrLossesByTx[tx.txid]; + -var tokenName = assetName; + span(class="text-danger") - + include value-display.pug + + div(class="card-body") + if (true) + - var txInputs = txInputsByTransaction[tx.txid]; + - var blockHeight = blockHeightsByTxid[tx.txid]; + - var txIOHighlightAddress = address; + + include transaction-io-details.pug + + else + p Since this explorer is database-free, it doesn't natively support address transaction history. However, you can configure it to communicate with one or more ElectrumX servers to build and display this data. In doing so, you should be aware that you'll be trusting those ElectrumX servers. If you configure multiple servers the results obtained from each will be cross-referenced against the others. Communicating with ElectrumX servers will also impact your privacy since the servers will know what addresses you're interested in. If these tradeoffs are acceptable, you can see a list of public ElectrumX servers here: + a(href="https://uasf.saltylemon.org/electrum") https://uasf.saltylemon.org/electrum + + if (false) + pre + code.json.bg-light #{JSON.stringify(transactions, null, 4)} + + if (!crawlerBot && addressDetails && addressDetails.txCount > limit) + - var txCount = addressDetails.txCount; + - var pageNumber = offset / limit + 1; + - var pageCount = Math.floor(txCount / limit); + - if (pageCount * limit < txCount) { + - pageCount++; + - } + - var paginationUrlFunction = function(x) { + - var pageUrl = paginationBaseUrl + "&limit=" + limit + "&offset=" + ((x - 1) * limit); + - if(dynamicContainerId) { + - return `javascript:ajaxUpdate("${pageUrl}", "${dynamicContainerId}")` + -} + - return pageUrl; + - } + + hr.mt-3 + + include pagination.pug diff --git a/views/includes/pagination.pug b/views/includes/pagination.pug index 153810c6e..63d2c91d5 100644 --- a/views/includes/pagination.pug +++ b/views/includes/pagination.pug @@ -2,7 +2,7 @@ - for (var x = 1; x <= pageCount; x++) { - pageNumbers.push(x); - } - +- var jsfunction = assetName !== coinConfig.ticker ? "javascript:ajaxUpdate" : ""; nav(aria-label="Page navigation") ul(class="pagination pagination-lg justify-content-center flex-wrap") li(class="page-item", class=(pageNumber == 1 ? "disabled" : false)) @@ -20,7 +20,7 @@ nav(aria-label="Page navigation") else if (x == (pageCount - 1) && pageNumber < (pageCount - 5)) li(class="page-item disabled") a(class="page-link", href="javascript:void(0)") ... - + li(class="page-item", class=(pageNumber == pageCount ? "disabled" : false)) a(class="page-link", href=(pageNumber == pageCount ? "javascript:void(0)" : paginationUrlFunction(pageNumber + 1)), aria-label="Next") - span(aria-hidden="true") » \ No newline at end of file + span(aria-hidden="true") » diff --git a/views/includes/transaction-io-details.pug b/views/includes/transaction-io-details.pug index b14a14557..5e3fff6bd 100644 --- a/views/includes/transaction-io-details.pug +++ b/views/includes/transaction-io-details.pug @@ -1,7 +1,7 @@ - var fontawesomeInputName = "sign-in-alt"; - var fontawesomeOutputName = "sign-out-alt"; -- var totalIOValues = utils.getTxTotalInputOutputValues(tx, txInputs, blockHeight); +- var totalIOValues = utils.getTxTotalInputOutputValues(tx, txInputs, blockHeight, assetName); script. function showAllTxOutputs(link, txid) { @@ -48,25 +48,34 @@ div(class="row monospace") if (global.specialAddresses[vout.scriptPubKey.addresses[0]]) - var specialAddressInfo = global.specialAddresses[vout.scriptPubKey.addresses[0]]; if (specialAddressInfo.type == "minerPayout") - span + span a(data-toggle="tooltip", title=("Miner payout address: " + specialAddressInfo.minerInfo.name)) i(class="fas fa-certificate text-primary") else if (specialAddressInfo.type == "donation") - span + span a(data-toggle="tooltip", title=("Development donation address. All support is appreciated!")) i(class="fas fa-certificate text-primary") - span(class="small") via + span(class="small") via a(href=("/tx/" + txInput.txid + "#output-" + txVin.vout)) #{txInput.txid.substring(0, 20)}...[#{txVin.vout}] div(class="tx-io-value") if (txVin.coinbase) - var currencyValue = coinConfig.blockRewardFunction(blockHeight); + - var tokenName = config.coin; include ./value-display.pug - else - if (vout && vout.value) + else if(vout) + if (vout.scriptPubKey && vout.scriptPubKey.asset) + - var currencyValue = vout.scriptPubKey.asset.amount; + - var tokenName = vout.scriptPubKey.asset.name; + else - var currencyValue = vout.value; - include ./value-display.pug + - var tokenName = config.coin; + include ./value-display.pug + else + - var currencyValue = 0; + - var tokenName = config.coin; + include ./value-display.pug hr @@ -86,23 +95,22 @@ div(class="row monospace") div(class="tx-io-desc") span #{extraInputCount.toLocaleString()} more input(s) br - span(class="small") see + span(class="small") see a(href=("/tx/" + tx.txid)) transactions details div(class="tx-io-value") - var currencyValue = new Decimal(totalIOValues.output).minus(new Decimal(totalIOValues.input)); + - var tokenName = assetName; include ./value-display.pug hr div(class="row mb-5 mb-lg-2 pr-3") div(class="col") div(class="font-weight-bold text-left text-md-right") - span(class="d-block d-md-none") Total Input: + span(class="d-block d-md-none") Total Input: - var currencyValue = totalIOValues.input; + - var tokenName = assetName; include ./value-display.pug - - - div(class="col-lg-6") - var maxRegularRowCount = (txIOHighlightAddress != null ? config.site.addressPage.txOutputMaxDefaultDisplay : 10000000); @@ -152,22 +160,22 @@ div(class="row monospace") if (global.specialAddresses[vout.scriptPubKey.addresses[0]]) - var specialAddressInfo = global.specialAddresses[vout.scriptPubKey.addresses[0]]; if (specialAddressInfo.type == "minerPayout") - span + span a(data-toggle="tooltip", title=("Miner payout address: " + specialAddressInfo.minerInfo.name)) i(class="fas fa-certificate text-primary") else if (specialAddressInfo.type == "donation") - span + span a(data-toggle="tooltip", title=("Development donation address. All support is appreciated!")) i(class="fas fa-certificate text-primary") - + else if (vout.scriptPubKey.hex && vout.scriptPubKey.hex.startsWith('6a24aa21a9ed')) span(class="monospace") span(class="rounded bg-primary text-white px-2 py-1 mr-2") OP_RETURN - span(title="Segregated Witness", data-toggle="tooltip") SegWit - span committment + span(title="Segregated Witness", data-toggle="tooltip") SegWit + span committment - a(href="https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#commitment-structure", data-toggle="tooltip", title="View developer docs", target="_blank") + a(href="https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#commitment-structure", data-toggle="tooltip", title="View developer docs", target="_blank") i(class="fas fa-info-circle") else if (vout.scriptPubKey.asm && vout.scriptPubKey.asm.startsWith('OP_RETURN ')) @@ -179,13 +187,13 @@ div(class="row monospace") span(class="monospace") span(class="text-warning font-weight-bold") Unable to decode output: br - span(class="font-weight-bold") type: + span(class="font-weight-bold") type: span #{vout.scriptPubKey.type} br - span(class="font-weight-bold") asm: + span(class="font-weight-bold") asm: span #{vout.scriptPubKey.asm} br - span(class="font-weight-bold") decodedHex: + span(class="font-weight-bold") decodedHex: span #{utils.hex2ascii(vout.scriptPubKey.hex)} div(class="tx-io-value") @@ -195,8 +203,16 @@ div(class="row monospace") else if (utxos[voutIndex] == null) i.fas.fa-lock-open.text-secondary.mr-2(title="Spent output." data-toggle="tooltip") - - - var currencyValue = vout.value; + if(vout) + if (vout.scriptPubKey && vout.scriptPubKey.asset) + - var currencyValue = vout.scriptPubKey.asset.amount; + - var tokenName = vout.scriptPubKey.asset.name; + else + - var currencyValue = vout.value; + - var tokenName = config.coin; + else + - var currencyValue = 0; + - var tokenName = config.coin; include ./value-display.pug @@ -208,8 +224,7 @@ div(class="row monospace") div(class="row mb-2 pr-3") div(class="col") div(class="font-weight-bold text-left text-md-right") - span(class="d-block d-md-none") Total Output: + span(class="d-block d-md-none") Total Output: - var currencyValue = totalIOValues.output; + - var tokenName = assetName; include ./value-display.pug - - diff --git a/views/includes/transaction-table.pug b/views/includes/transaction-table.pug new file mode 100644 index 000000000..35fd48b80 --- /dev/null +++ b/views/includes/transaction-table.pug @@ -0,0 +1,37 @@ +div(class="card mb-4 shadow-sm") + div(class="card-header") + div(class="row") + div(class="col") + h2(class="h6 mb-0") Pending Transactions + if (getblockchaininfo.initialblockdownload) + small (#{(getblockchaininfo.headers - getblockchaininfo.blocks).toLocaleString()} behind) + + div(class="col") + span(style="float: right;") + a(href="/unconfirmed-tx") + span Browse Transactions » + + div(class="card-body") + div(class="table-responsive") + table(class="table table-striped mb-0") + thead + tr + th(class="data-header") txid + th(class="data-header") Created On (utc) + th(class="data-header text-right") Age + th(class="data-header text-right") Fee + th(class="data-header text-right") Size (bytes) + tbody + each tx, txIndex in pendingTransactions + if (tx) + tr + td(class="data-cell monospace") + a(href=("/tx/" + tx.txid)) #{tx.txid} + td(class="data-cell monospace") #{moment.utc(new Date(parseInt(block.time) * 1000)).format("Y-MM-DD HH:mm:ss")} + + - var timeAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(block.time) * 1000)))); + td(class="data-cell monospace text-right") #{timeAgo.format()} + td(class="data-cell monospace text-right") + - var currencyValue = new Decimal(block.totalFees).dividedBy(block.tx.length); + include ./value-display.pug + td(class="data-cell monospace text-right") #{block.size.toLocaleString()} \ No newline at end of file diff --git a/views/includes/value-display.pug b/views/includes/value-display.pug index b1ac85ee3..2e83c45b7 100644 --- a/views/includes/value-display.pug +++ b/views/includes/value-display.pug @@ -1,18 +1,18 @@ - var currencyFormatInfo = utils.getCurrencyFormatInfo(currencyFormatType); if (currencyValue > 0) - - var parts = utils.formatCurrencyAmount(currencyValue, currencyFormatType).split(" "); + - var parts = utils.formatCurrencyAmount(currencyValue, currencyFormatType, tokenName).split(" "); span.monospace #{parts[0]} if (currencyFormatInfo.type == "native") if (global.exchangeRates) - small.border-dotted.ml-1(data-toggle="tooltip", title=utils.formatExchangedCurrency(currencyValue, "usd")) #{parts[1]} + small.border-dotted.ml-1(data-toggle="tooltip", title=utils.formatExchangedCurrency(currencyValue, "usd", tokenName)) #{parts[1]} else small.ml-1 #{parts[1]} - + else if (currencyFormatInfo.type == "exchanged") - small.border-dotted.ml-1(data-toggle="tooltip", title=utils.formatCurrencyAmount(currencyValue, coinConfig.defaultCurrencyUnit.name)) #{parts[1]} - + small.border-dotted.ml-1(data-toggle="tooltip", title=utils.formatCurrencyAmount(currencyValue, coinConfig.defaultCurrencyUnit.name, tokenName)) #{parts[1]} + else - span.monospace 0 \ No newline at end of file + span.monospace 0 diff --git a/views/layout.pug b/views/layout.pug index d382a4f77..6781ef6c9 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -4,7 +4,7 @@ html(lang="en") meta(charset="utf-8") meta(name="csrf-token", content=csrfToken) meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no") - + link(rel="stylesheet", href="/css/fonts.css", integrity="sha384-XOmmu8j3C2MFUXRVGg8VWYNwlhEkSNb0rW/4e7bi3F56S6PejEmBUQDGZofQyjbL") link(rel="stylesheet", href="/css/highlight.min.css", integrity="sha384-zhIsEafzyQWHSoMCQ4BfT8ZlRXQyIFwAHAJn32PNdsb8n6tVysGZSLpEEIvCskw4") @@ -12,18 +12,18 @@ html(lang="en") if (session.uiTheme && session.uiTheme == "dark") link(rel="stylesheet", href="/css/bootstrap-dark.css", integrity="") link(rel="stylesheet", href="/css/dark-touchups.css", integrity="") - + else link(rel="stylesheet", href="/css/bootstrap.min.css", integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T") - + link(rel='stylesheet', href='/css/styling.css') link(rel="icon", type="image/png", href=("/img/logo/" + config.coin.toLowerCase() + ".png")) link(rel="shortcut icon" href=("/img/logo/" + config.coin.toLowerCase() + ".ico") type="image/x-icon") block headContent title Explorer - - utils.formatLargeNumber(10000000, 3); + - utils.formatLargeNumber(10000000, 3); body(class="bg-dark") nav(class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top") div(class="container") @@ -32,10 +32,10 @@ html(lang="en") if (coinConfig.logoUrl) img(src=coinConfig.logoUrl, class="header-image", alt="logo") span #{coinConfig.siteTitle} - + button(type="button", class="navbar-toggler navbar-toggler-right", data-toggle="collapse", data-target="#navbarNav", aria-label="collapse navigation") span(class="navbar-toggler-icon") - + div(class="collapse navbar-collapse", id="navbarNav") if (rpcClient) ul(class="navbar-nav mr-auto") @@ -46,7 +46,7 @@ html(lang="en") if (config.siteTools) li(class="nav-item dropdown") - a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") + a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") span Tools div(class="dropdown-menu shadow", aria-label="Tools Items") each item in config.siteTools @@ -58,7 +58,7 @@ html(lang="en") if (config.site.header.dropdowns) each dropdown, ddIndex in config.site.header.dropdowns li(class="nav-item dropdown") - a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") + a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") span #{dropdown.title} div(class="dropdown-menu shadow", aria-label=(dropdown.title + " Items")) each dropdownLink in dropdown.links @@ -67,7 +67,7 @@ html(lang="en") img(src=dropdownLink.imgUrl, style="width: 24px; height: 24px; margin-right: 8px;", alt=dropdownLink.name) span #{dropdownLink.name} - + form(method="post", action="/search", class="form-inline mr-3") input(type="hidden", name="_csrf", value=csrfToken) div(class="input-group input-group-sm") @@ -78,8 +78,8 @@ html(lang="en") ul(class="navbar-nav") li(class="nav-item dropdown") - a(class="nav-link dropdown-toggle", href="javascript:void(0)", id="navbarDropdown", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") - i(class="fas fa-cog mr-1") + a(class="nav-link dropdown-toggle", href="javascript:void(0)", id="navbarDropdown", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") + i(class="fas fa-cog mr-1") div(class="dropdown-menu dropdown-menu-right shadow", aria-labelledby="navbarDropdown") if (coinConfig.currencyUnits) span(class="dropdown-header") Currency Units @@ -100,7 +100,7 @@ html(lang="en") if (session.uiTheme == "dark") i(class="fas fa-check") span Dark - + if (host && port && !homepage && config.site.header.showToolsSubheader) div(id="sub-menu", class="container mb-2 pt-2 d-lg-block d-none border-top", style="") ul(class="nav") @@ -109,7 +109,7 @@ html(lang="en") li(class="nav-item") a(href=item.url, class="nav-link text-white px-2 text-decoration-underline") span #{item.name} - + - var bodyBgColor = "#ffffff;"; if (session.uiTheme && session.uiTheme == "dark") - bodyBgColor = "#0c0c0c;"; @@ -127,7 +127,7 @@ html(lang="en") if (userMessage) div(class="alert", class=(userMessageType ? ("alert-" + userMessageType) : "alert-warning"), role="alert") span !{userMessage} - + block content div(style="margin-bottom: 30px;") @@ -143,29 +143,29 @@ html(lang="en") dt Source dd a(href="https://github.com/npq7721/rpc-explorer") github.com/npq7721/rpc-explorer - + if (global.sourcecodeProjectMetadata) div(class="mt-2") a(href="https://github.com/npq7721/rpc-explorer", class="btn btn-primary btn-sm mr-3 mb-1 text-decoration-none") i(class="fas fa-star mr-2") - span(class="mr-2") Star + span(class="mr-2") Star span(class="badge bg-white text-dark") #{global.sourcecodeProjectMetadata.stargazers_count} a(href="https://github.com/npq7721/rpc-explorer/fork", class="btn btn-primary btn-sm mr-3 mb-1 text-decoration-none") i(class="fas fa-code-branch mr-2") - span(class="mr-2") Fork + span(class="mr-2") Fork span(class="badge bg-white text-dark") #{global.sourcecodeProjectMetadata.forks_count} - + dt App Details dd span version: #{global.appVersion} if (sourcecodeVersion) br - span commit: + span commit: a(href=("https://github.com/npq7721/rpc-explorer/commit/" + sourcecodeVersion)) #{sourcecodeVersion} - + br span released: #{sourcecodeDate} span(style="font-size: 0.9em;") ( @@ -179,7 +179,7 @@ html(lang="en") a(href=coinConfig.demoSiteUrl) #{coinConfig.demoSiteUrl} else a(href="https://btc-explorer.chaintools.io") https://btc-explorer.chaintools.io - + div(class="mt-2") - var demoSiteCoins = ["BTC"]; each demoSiteCoin in demoSiteCoins @@ -195,8 +195,9 @@ html(lang="en") button.btn.btn-primary(type="button", class="btn btn-primary", data-toggle="modal", data-target="#exampleModalCenter") i(class="fas fa-heart mr-2") span Support Project - - script(src="/js/jquery.min.js", integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=") + + //script(src="/js/jquery.min.js", integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=") + script(src="/js/jquery-3.4.1.min.js", integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=") script(src="/js/popper.min.js", integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut") script(src="/js/bootstrap.min.js", integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM") script(defer, src="/js/fontawesome.min.js", integrity="sha384-eVEQC9zshBn0rFj4+TU78eNA19HMNigMviK/PU/FFjLXqa/GKPgX58rvt5Z8PLs7") @@ -207,6 +208,7 @@ html(lang="en") $('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="popover"]').popover({html:true, container:"body"}); updateStats(); + loadLazyContainers(); }); hljs.initHighlightingOnLoad(); @@ -225,5 +227,5 @@ html(lang="en") gtag('config', '#{config.credentials.googleAnalyticsTrackingId}'); - + block endOfBody