Browse Source

Support for pluggable Address APIs (work on #118 and #119)

Move closer to the original vision of supporting any address-querying implementation desired. Current options include ElectrumX (as before) and now blockchain.com and blockcypher.com since they were easy to support and are publicly/easily available (though ridiculously neither is yet to support bc1 addresses).

- new env var option BTCEXP_ADDRESS_API, value can be electrumx, blockchain.com, blockcypher.com
- update ElectrumX client connect to request v1.4 of API as all clients are now supposed to do
- misc frontend improvements/cleanup for addresses
fix-133-memory-crash
Dan Janosik 6 years ago
parent
commit
3e8b092e86
No known key found for this signature in database GPG Key ID: C6F8CE9FFDB2CED2
  1. 12
      .env-sample
  2. 2
      README.md
  3. 41
      app.js
  4. 79
      app/api/addressApi.js
  5. 116
      app/api/blockchainAddressApi.js
  6. 60
      app/api/blockcypherAddressApi.js
  7. 122
      app/api/electrumAddressApi.js
  8. 3
      app/config.js
  9. 18
      app/utils.js
  10. 3
      bin/cli.js
  11. 165
      routes/baseActionsRouter.js
  12. 85
      views/address.pug

12
.env-sample

@ -14,10 +14,18 @@
#BTCEXP_BITCOIND_COOKIE=/path/to/bitcoind/.cookie
#BTCEXP_BITCOIND_RPC_TIMEOUT=5000
# Optional ElectrumX Servers, used to display address transaction histories
# Ref: https://uasf.saltylemon.org/electrum
# Select optional "address API" to display address tx lists and balances
# Options: electrumx, blockchain.com, blockcypher.com
# If electrumx set, the BTCEXP_ELECTRUMX_SERVERS variable must also be
# set. Neither blockchain.com or blockcypher.com support native-segwit bc1...
# addresses, but are convenient for users who don't run their own ElectrumX.
BTCEXP_ADDRESS_API=(electrumx|blockchain.com|blockcypher.com)
# Optional ElectrumX Servers. See BTCEXP_ADDRESS_API. This value is only
# used if BTCEXP_ADDRESS_API=electrumx
#BTCEXP_ELECTRUMX_SERVERS=tls://electrumx.server.com:50002,tcp://127.0.0.1:50001,...
# Optional InfluxDB Credentials (URI -OR- HOST/PORT/DBNAME/USER/PASS)
#BTCEXP_ENABLE_INFLUXDB=true
#BTCEXP_INFLUXDB_URI=influx://username:password@127.0.0.1:8086

2
README.md

@ -21,7 +21,7 @@ Live demos are available at:
* View transaction details, with navigation "backward" via spent transaction outputs
* View JSON content used to generate most pages
* Search by transaction ID, block hash/height, and address
* Optional transaction history for addresses by querying configurable ElectrumX servers
* Optional transaction history for addresses by querying from ElectrumX, blockchain.com, or blockcypher.com
* Mempool summary, with fee, size, and age breakdowns
* RPC command browser and terminal
* Currently supports BTC, LTC (support for any Bitcoin-RPC-protocol-compliant coin can be added easily)

41
app.js

@ -37,7 +37,8 @@ var coreApi = require("./app/api/coreApi.js");
var coins = require("./app/coins.js");
var request = require("request");
var qrcode = require("qrcode");
var electrumApi = require("./app/api/electrumApi.js");
var addressApi = require("./app/api/addressApi.js");
var electrumAddressApi = require("./app/api/electrumAddressApi.js");
var Influx = require("influx");
var coreApi = require("./app/api/coreApi.js");
var auth = require('./app/auth.js');
@ -357,17 +358,27 @@ app.runOnStartup = function() {
});
}
if (config.electrumXServers && config.electrumXServers.length > 0) {
electrumApi.connectToServers().then(function() {
console.log("Live with ElectrumX API.");
if (config.addressApi) {
var supportedAddressApis = addressApi.getSupportedAddressApis();
if (!supportedAddressApis.includes(config.addressApi)) {
utils.logError("32907ghsd0ge", `Unrecognized value for BTCEXP_ADDRESS_API: '${config.addressApi}'. Valid options are: ${supportedAddressApis}`);
}
global.electrumApi = electrumApi;
}).catch(function(err) {
console.log("Error 31207ugf4e0fed: " + err + ", while initializing ElectrumX API");
});
if (config.addressApi == "electrumx") {
if (config.electrumXServers && config.electrumXServers.length > 0) {
electrumAddressApi.connectToServers().then(function() {
global.electrumAddressApi = electrumAddressApi;
}).catch(function(err) {
utils.logError("31207ugf4e0fed", err, {electrumXServers:config.electrumXServers});
});
} else {
utils.logError("327hs0gde", "You must set the 'BTCEXP_ELECTRUMX_SERVERS' environment variable when BTCEXP_ADDRESS_API=electrumx.");
}
}
}
loadMiningPoolConfigs();
if (global.sourcecodeVersion == null && fs.existsSync('.git')) {
@ -470,18 +481,6 @@ app.use(function(req, res, next) {
}
}
// electrum trust warnings on address pages
if (!req.session.hideElectrumTrustWarnings) {
var cookieValue = req.cookies['user-setting-hideElectrumTrustWarnings'];
if (cookieValue) {
req.session.hideElectrumTrustWarnings = cookieValue;
} else {
req.session.hideElectrumTrustWarnings = "false";
}
}
res.locals.currencyFormatType = req.session.currencyFormatType;

79
app/api/addressApi.js

@ -0,0 +1,79 @@
var config = require("./../config.js");
var coins = require("../coins.js");
var utils = require("../utils.js");
var coinConfig = coins[config.coin];
var electrumAddressApi = require("./electrumAddressApi.js");
var blockchainAddressApi = require("./blockchainAddressApi.js");
var blockcypherAddressApi = require("./blockcypherAddressApi.js");
function getSupportedAddressApis() {
return ["blockchain.com", "blockcypher.com", "electrumx"];
}
function getCurrentAddressApiFeatureSupport() {
if (config.addressApi == "blockchain.com") {
return {
pageNumbers: true,
sortDesc: true,
sortAsc: true
};
} else if (config.addressApi == "blockcypher.com") {
return {
pageNumbers: true,
sortDesc: true,
sortAsc: false
};
} else if (config.addressApi == "electrumx") {
return {
pageNumbers: true,
sortDesc: true,
sortAsc: true
};
}
}
function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
return new Promise(function(resolve, reject) {
var promises = [];
if (config.addressApi == "blockchain.com") {
promises.push(blockchainAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
} else if (config.addressApi == "blockcypher.com") {
promises.push(blockcypherAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
} else if (config.addressApi == "electrumx") {
promises.push(electrumAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
} else {
promises.push(new Promise(function(resolve, reject) {
resolve({addressDetails:null, errors:["No address API configured"]});
}));
}
Promise.all(promises).then(function(results) {
if (results && results.length > 0) {
resolve(results[0]);
} else {
resolve(null);
}
}).catch(function(err) {
utils.logError("239x7rhsd0gs", err);
reject(err);
});
});
}
module.exports = {
getSupportedAddressApis: getSupportedAddressApis,
getCurrentAddressApiFeatureSupport: getCurrentAddressApiFeatureSupport,
getAddressDetails: getAddressDetails
};

116
app/api/blockchainAddressApi.js

@ -0,0 +1,116 @@
var request = require("request");
var utils = require("./../utils.js");
function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
return new Promise(function(resolve, reject) {
if (address.startsWith("bc1")) {
reject({userText:"blockchain.com API does not support bc1 (native Segwit) addresses"});
return;
}
if (sort == "asc") {
// need to query the total number of tx first, then build paging info from that value
var options = {
url: `https://blockchain.info/rawaddr/${address}?limit=1`,
headers: {
'User-Agent': 'request'
}
};
request(options, function(error, response, body) {
if (error == null && response && response.statusCode && response.statusCode == 200) {
var blockchainJson = JSON.parse(body);
var txCount = blockchainJson.n_tx;
var pageCount = parseInt(txCount / limit);
var lastPageSize = limit;
if (pageCount * limit < txCount) {
lastPageSize = txCount - pageCount * limit;
}
var dynamicOffset = txCount - limit - offset;
if (dynamicOffset < 0) {
limit += dynamicOffset;
dynamicOffset += limit;
}
getAddressDetailsSortDesc(address, limit, dynamicOffset).then(function(result) {
result.txids.reverse();
resolve({addressDetails:result});
}).catch(function(err) {
utils.logError("2308hsghse", err);
reject(err);
});
} else {
var fullError = {error:error, response:response, body:body};
utils.logError("we0f8hasd0fhas", fullError);
reject(fullError);
}
});
} else {
getAddressDetailsSortDesc(address, limit, offset).then(function(result) {
resolve({addressDetails:result});
}).catch(function(err) {
utils.logError("3208hwssse", err);
reject(err);
});
}
});
}
function getAddressDetailsSortDesc(address, limit, offset) {
return new Promise(function(resolve, reject) {
var options = {
url: `https://blockchain.info/rawaddr/${address}?limit=${limit}&offset=${offset}`,
headers: {
'User-Agent': 'request'
}
};
request(options, function(error, response, body) {
if (error == null && response && response.statusCode && response.statusCode == 200) {
var blockchainJson = JSON.parse(body);
var response = {};
response.txids = [];
response.blockHeightsByTxid = {};
blockchainJson.txs.forEach(function(tx) {
response.txids.push(tx.hash);
response.blockHeightsByTxid[tx.hash] = tx.block_height;
});
response.txCount = blockchainJson.n_tx;
response.hash160 = blockchainJson.hash160;
response.totalReceivedSat = blockchainJson.total_received;
response.totalSentSat = blockchainJson.total_sent;
response.balanceSat = blockchainJson.final_balance;
response.source = "blockchain.com";
resolve(response);
} else {
var fullError = {error:error, response:response, body:body};
utils.logError("32907shsghs", fullError);
reject(fullError);
}
});
});
}
module.exports = {
getAddressDetails: getAddressDetails
};

60
app/api/blockcypherAddressApi.js

@ -0,0 +1,60 @@
var request = require("request");
function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
return new Promise(function(resolve, reject) {
if (address.startsWith("bc1")) {
reject({userText:"blockcypher.com API does not support bc1 (native Segwit) addresses"});
return;
}
var limitOffset = limit + offset;
var options = {
url: `https://api.blockcypher.com/v1/btc/main/addrs/${address}?limit=${limitOffset}`,
headers: {
'User-Agent': 'request'
}
};
request(options, function(error, response, body) {
if (error == null && response && response.statusCode && response.statusCode == 200) {
var blockcypherJson = JSON.parse(body);
var response = {};
response.txids = [];
response.blockHeightsByTxid = {};
// blockcypher doesn't support offset for paging, so simulate up to the hard cap of 2,000
for (var i = offset; i < Math.min(blockcypherJson.txrefs.length, limitOffset); i++) {
var tx = blockcypherJson.txrefs[i];
response.txids.push(tx.tx_hash);
response.blockHeightsByTxid[tx.tx_hash] = tx.block_height;
}
response.txCount = blockcypherJson.n_tx;
response.totalReceivedSat = blockcypherJson.total_received;
response.totalSentSat = blockcypherJson.total_sent;
response.balanceSat = blockcypherJson.final_balance;
response.source = "blockcypher.com";
resolve({addressDetails:response});
} else {
var fullError = {error:error, response:response, body:body};
utils.logError("097wef0adsgadgs", fullError);
reject(fullError);
}
});
});
}
module.exports = {
getAddressDetails: getAddressDetails
};

122
app/api/electrumApi.js → app/api/electrumAddressApi.js

@ -1,7 +1,12 @@
var debug = require("debug");
var debugLog = debug("btcexp:electrumx");
var config = require("./../config.js");
var coins = require("../coins.js");
var utils = require("../utils.js");
var sha256 = require("crypto-js/sha256");
var hexEnc = require("crypto-js/enc-hex");
var coinConfig = coins[config.coin];
const ElectrumClient = require('electrum-client');
@ -21,52 +26,30 @@ function connectToServers() {
resolve();
}).catch(function(err) {
console.log("Error 120387rygxx231gwe40: " + err);
utils.logError("120387rygxx231gwe40", err);
reject(err);
});
});
}
function reconnectToServers() {
return new Promise(function(resolve, reject) {
for (var i = 0; i < electrumClients.length; i++) {
electrumClients[i].close();
}
electrumClients = [];
console.log("Reconnecting ElectrumX sockets...");
connectToServers().then(function() {
console.log("Done reconnecting ElectrumX sockets.");
resolve();
}).catch(function(err) {
console.log("Error 317fh29y7fg3333: " + err);
resolve();
});
});
}
function connectToServer(host, port, protocol) {
return new Promise(function(resolve, reject) {
console.log("Connecting to ElectrumX Server: " + host + ":" + port);
debugLog("Connecting to ElectrumX Server: " + host + ":" + port);
// default protocol is 'tcp' if port is 50001, which is the default unencrypted port for electrumx
var defaultProtocol = port === 50001 ? 'tcp' : 'tls';
var electrumClient = new ElectrumClient(port, host, protocol || defaultProtocol);
electrumClient.initElectrum({client:"btc-rpc-explorer-v1.1", version:"1.2"}).then(function(res) {
console.log("Connected to ElectrumX Server: " + host + ":" + port + ", versions: " + JSON.stringify(res));
electrumClient.initElectrum({client:"btc-rpc-explorer-v1.1", version:"1.4"}).then(function(res) {
debugLog("Connected to ElectrumX Server: " + host + ":" + port + ", versions: " + JSON.stringify(res));
electrumClients.push(electrumClient);
resolve();
}).catch(function(err) {
console.log("Error 137rg023xx7gerfwdd: " + err + ", when trying to connect to ElectrumX server at " + host + ":" + port);
utils.logError("137rg023xx7gerfwdd", err, {host:host, port:port, protocol:protocol});
reject(err);
});
@ -84,7 +67,7 @@ function runOnServer(electrumClient, f) {
reject({error:result.error, server:electrumClient.host});
}
}).catch(function(err) {
console.log("Error dif0e21qdh: " + err + ", host=" + electrumClient.host + ", port=" + electrumClient.port);
utils.logError("dif0e21qdh", err, {host:electrumClient.host, port:electrumClient.port});
reject(err);
});
@ -108,12 +91,85 @@ function runOnAllServers(f) {
});
}
function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
return new Promise(function(resolve, reject) {
var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubkey)));
addrScripthash = addrScripthash.match(/.{2}/g).reverse().join("");
var promises = [];
var txidData = null;
var balanceData = null;
promises.push(new Promise(function(resolve, reject) {
getAddressTxids(addrScripthash).then(function(result) {
txidData = result.result;
resolve();
}).catch(function(err) {
utils.logError("2397wgs0sgse", err);
reject(err);
});
}));
promises.push(new Promise(function(resolve, reject) {
getAddressBalance(addrScripthash).then(function(result) {
balanceData = result.result;
resolve();
}).catch(function(err) {
utils.logError("21307ws70sg", err);
reject(err);
});
}));
Promise.all(promises.map(utils.reflectPromise)).then(function(results) {
var addressDetails = {};
if (txidData) {
addressDetails.txCount = txidData.length;
addressDetails.txids = [];
addressDetails.blockHeightsByTxid = {};
if (sort == "desc") {
txidData.reverse();
}
for (var i = offset; i < Math.min(txidData.length, limit + offset); i++) {
addressDetails.txids.push(txidData[i].tx_hash);
addressDetails.blockHeightsByTxid[txidData[i].tx_hash] = txidData[i].height;
}
}
if (balanceData) {
addressDetails.balanceSat = balanceData.confirmed;
}
var errors = [];
results.forEach(function(x) {
if (x.status == "rejected") {
errors.push(x);
}
});
resolve({addressDetails:addressDetails, errors:errors});
});
});
}
function getAddressTxids(addrScripthash) {
return new Promise(function(resolve, reject) {
runOnAllServers(function(electrumClient) {
return electrumClient.blockchainScripthash_getHistory(addrScripthash);
}).then(function(results) {
debugLog(`getAddressTxids=${utils.ellipsize(JSON.stringify(results), 200)}`);
if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) {
for (var i = 0; i < results.length; i++) {
results[i].result.unshift({tx_hash:coinConfig.genesisCoinbaseTransactionId, height:0});
@ -146,6 +202,8 @@ function getAddressBalance(addrScripthash) {
return electrumClient.blockchainScripthash_getBalance(addrScripthash);
}).then(function(results) {
debugLog(`getAddressBalance=${JSON.stringify(results)}`);
if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) {
for (var i = 0; i < results.length; i++) {
var coinbaseBlockReward = coinConfig.blockRewardFunction(0);
@ -176,7 +234,5 @@ function getAddressBalance(addrScripthash) {
module.exports = {
connectToServers: connectToServers,
reconnectToServers: reconnectToServers,
getAddressTxids: getAddressTxids,
getAddressBalance: getAddressBalance
getAddressDetails: getAddressDetails
};

3
app/config.js

@ -124,13 +124,14 @@ module.exports = {
"walletpassphrasechange",
],
addressApi:process.env.BTCEXP_ADDRESS_API,
electrumXServers:electrumXServers,
redisUrl:process.env.BTCEXP_REDIS_URL,
site: {
blockTxPageSize:20,
addressTxPageSize:20,
addressTxPageSize:10,
txMaxInput:15,
browseBlocksPageSize:20,
addressPage:{

18
app/utils.js

@ -241,6 +241,15 @@ function logAppStats() {
}
}
function ellipsize(str, length) {
if (str.length <= length) {
return str;
} else {
return str.substring(0, length - 3) + "...";
}
}
function logMemoryUsage() {
var mbUsed = process.memoryUsage().heapUsed / 1024 / 1024;
mbUsed = Math.round(mbUsed * 100) / 100;
@ -511,6 +520,11 @@ function colorHexToHsl(hex) {
return rgbToHsl(rgb.r, rgb.g, rgb.b);
}
// https://stackoverflow.com/a/31424853/673828
const reflectPromise = p => p.then(v => ({v, status: "resolved" }),
e => ({e, status: "rejected" }));
function logError(errorId, err, optionalUserData = null) {
if (!global.errorLog) {
global.errorLog = [];
@ -580,6 +594,7 @@ function buildQrCodeUrl(str, results) {
module.exports = {
reflectPromise: reflectPromise,
redirectToConnectPageIfNeeded: redirectToConnectPageIfNeeded,
hex2ascii: hex2ascii,
splitArrayIntoChunks: splitArrayIntoChunks,
@ -605,5 +620,6 @@ module.exports = {
colorHexToRgb: colorHexToRgb,
colorHexToHsl: colorHexToHsl,
logError: logError,
buildQrCodeUrls: buildQrCodeUrls
buildQrCodeUrls: buildQrCodeUrls,
ellipsize: ellipsize
};

3
bin/cli.js

@ -17,7 +17,8 @@ const args = require('meow')(`
-u, --bitcoind-user <user> username for bitcoind rpc [default: none]
-w, --bitcoind-pass <pass> password for bitcoind rpc [default: none]
-E, --electrumx-servers <..> comma separated list of electrum servers to use for address queries [default: none]
--address-api <option> api to use for address queries (options: electrumx, blockchain.com, blockcypher.com) [default: none]
-E, --electrumx-servers <..> comma separated list of electrum servers to use for address queries; only used if --address-api=electrumx [default: none]
--rpc-allowall allow all rpc commands [default: false]
--rpc-blacklist <methods> comma separated list of rpc commands to block [default: see in config.js]

165
routes/baseActionsRouter.js

@ -14,6 +14,7 @@ var utils = require('./../app/utils.js');
var coins = require("./../app/coins.js");
var config = require("./../app/config.js");
var coreApi = require("./../app/api/coreApi.js");
var addressApi = require("./../app/api/addressApi.js");
const forceCsrf = csurf({ ignoreMethods: [] });
@ -599,8 +600,9 @@ router.get("/address/:address", function(req, res, next) {
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.sort = sort;
res.locals.paginationBaseUrl = ("/address/" + address + "?sort=" + sort);
res.locals.paginationBaseUrl = `/address/${address}?sort=${sort}`;
res.locals.transactions = [];
res.locals.addressApiSupport = addressApi.getCurrentAddressApiFeatureSupport();
res.locals.result = {};
@ -626,136 +628,107 @@ router.get("/address/:address", function(req, res, next) {
}
}
res.locals.advancedFunctionality = (global.electrumApi != null);
coreApi.getAddress(address).then(function(validateaddressResult) {
res.locals.result.validateaddress = validateaddressResult;
var promises = [];
if (global.electrumApi) {
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) {
electrumApi.getAddressBalance(addrScripthash).then(function(result) {
res.locals.balance = result;
addressApi.getAddressDetails(address, validateaddressResult.scriptPubKey, sort, limit, offset).then(function(addressDetailsResult) {
var addressDetails = addressDetailsResult.addressDetails;
res.locals.electrumBalance = result;
if (addressDetailsResult.errors) {
res.locals.addressDetailsErrors = addressDetailsResult.errors;
}
resolve();
if (addressDetails) {
res.locals.addressDetails = addressDetails;
}).catch(function(err) {
reject(err);
});
}));
if (addressDetails.balanceSat == 0) {
// make sure zero balances pass the falsey check in the UI
addressDetails.balanceSat = "0";
}
promises.push(new Promise(function(resolve, reject) {
electrumApi.getAddressTxids(addrScripthash).then(function(result) {
var txidResult = null;
if (addressDetails.txCount == 0) {
// make sure txCount=0 pass the falsey check in the UI
addressDetails.txCount = "0";
}
if (result.conflictedResults) {
res.locals.conflictedTxidResults = true;
if (addressDetails.txids) {
var txids = addressDetails.txids;
var blockHeightsByTxid = addressDetails.blockHeightsByTxid;
txidResult = result.conflictedResults[0];
res.locals.txids = txids;
coreApi.getRawTransactionsWithInputs(txids).then(function(rawTxResult) {
res.locals.transactions = rawTxResult.transactions;
res.locals.txInputsByTransaction = rawTxResult.txInputsByTransaction;
res.locals.blockHeightsByTxid = blockHeightsByTxid;
} else if (result.result != null) {
txidResult = result;
}
var addrGainsByTx = {};
var addrLossesByTx = {};
res.locals.electrumHistory = txidResult;
res.locals.addrGainsByTx = addrGainsByTx;
res.locals.addrLossesByTx = addrLossesByTx;
var txids = [];
var blockHeightsByTxid = {};
for (var i = 0; i < rawTxResult.transactions.length; i++) {
var tx = rawTxResult.transactions[i];
var txInputs = rawTxResult.txInputsByTransaction[tx.txid];
if (txidResult) {
for (var i = 0; i < txidResult.result.length; i++) {
txids.push(txidResult.result[i].tx_hash);
blockHeightsByTxid[txidResult.result[i].tx_hash] = txidResult.result[i].height;
}
}
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);
}
if (sort == "desc") {
txids = txids.reverse();
}
addrGainsByTx[tx.txid] = addrGainsByTx[tx.txid].plus(new Decimal(tx.vout[j].value));
}
}
res.locals.txids = txids;
for (var j = 0; j < tx.vin.length; j++) {
var txInput = txInputs[j];
var vinJ = tx.vin[j];
var pagedTxids = [];
for (var i = offset; i < (offset + limit); i++) {
if (txids.length > i) {
pagedTxids.push(txids[i]);
}
}
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);
}
if (txidResult && txidResult.result != null) {
// since we always request the first txid (to determine "first seen" info for the address),
// remove it for proper paging
pagedTxids.unshift(txidResult.result[0].tx_hash);
}
coreApi.getRawTransactionsWithInputs(pagedTxids).then(function(rawTxResult) {
// first result is always the earliest tx, but doesn't fit into the current paging;
// store it as firstSeenTransaction then remove from list
res.locals.firstSeenTransaction = rawTxResult.transactions[0];
rawTxResult.transactions.shift();
res.locals.transactions = rawTxResult.transactions;
res.locals.txInputsByTransaction = rawTxResult.txInputsByTransaction;
res.locals.blockHeightsByTxid = blockHeightsByTxid;
var addrGainsByTx = {};
var addrLossesByTx = {};
res.locals.addrGainsByTx = addrGainsByTx;
res.locals.addrLossesByTx = addrLossesByTx;
for (var i = 0; i < rawTxResult.transactions.length; i++) {
var tx = rawTxResult.transactions[i];
var txInputs = rawTxResult.txInputsByTransaction[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);
addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(new Decimal(txInput.vout[vinJ.vout].value));
}
}
}
addrGainsByTx[tx.txid] = addrGainsByTx[tx.txid].plus(new Decimal(tx.vout[j].value));
//console.log("tx: " + JSON.stringify(tx));
//console.log("txInputs: " + JSON.stringify(txInputs));
}
}
for (var j = 0; j < tx.vin.length; j++) {
var txInput = txInputs[j];
var vinJ = tx.vin[j];
resolve();
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);
}
}).catch(function(err) {
utils.logError("asdgf07uh23", err);
addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(new Decimal(txInput.vout[vinJ.vout].value));
}
}
}
reject(err);
});
//console.log("tx: " + JSON.stringify(tx));
//console.log("txInputs: " + JSON.stringify(txInputs));
} else {
// no addressDetails.txids available
resolve();
}
} else {
// no addressDetails available
resolve();
}).catch(function(err) {
console.log("Error asdgf07uh23: " + err + ", error json: " + JSON.stringify(err));
reject(err);
});
}
}).catch(function(err) {
res.locals.electrumHistoryError = err;
utils.logError("23t07ug2wghefud", err);
res.locals.addressApiError = err;
reject(err);
});
@ -787,7 +760,7 @@ router.get("/address/:address", function(req, res, next) {
});
}));
Promise.all(promises).then(function() {
Promise.all(promises.map(utils.reflectPromise)).then(function() {
res.render("address");
next();

85
views/address.pug

@ -87,18 +87,10 @@ block content
div(class="tab-content")
div(id="tab-details", class="tab-pane active", role="tabpanel")
if (config.electrumXServers && config.electrumXServers.length > 0)
if (false && config.electrumXServers && config.electrumXServers.length > 0)
if (session.hideElectrumTrustWarnings != "true")
div(class="alert alert-primary alert-dismissible clearfix shadow-sm", role="alert")
h4(class="alert-heading h6 font-weight-bold") Note
p Since this explorer is database-free, it doesn't natively support address balances and transaction histories. In order to provide this functionality, address balances and transaction history can be requested from a configurable set of ElectrumX servers. If multiple ElectrumX servers are configured, the results are cross-referenced and conflicts noted. For the transaction history displayed below, only the transaction identifiers from ElectrumX are used; the transaction details are requested via RPC from this app's primary node, as usual.
span(class="font-weight-bold") Configured ElectrumX Servers
ul
each server in config.electrumXServers
li
span #{server.host}
span : #{server.port}
span this explorer is database-free, it doesn't natively support address balances and transaction histories. In order to provide this functionality, address balances and transaction history can be requested from a configurable set of ElectrumX servers. If multiple ElectrumX servers are configured, the results are cross-referenced and conflicts noted. For the transaction history displayed below, only the transaction identifiers from ElectrumX are used; the transaction details are requested via RPC from this app's primary node, as usual.
a(href="/changeSetting?name=hideElectrumTrustWarnings&value=true", class="close", aria-label="Close", style="text-decoration: none;")
span(aria-hidden="true") &times;
@ -180,10 +172,17 @@ block content
- var currencyValue = balance.unconfirmed / coinConfig.baseCurrencyUnit.multiplier;
include includes/value-display.pug
if (electrumHistory)
if (addressDetails && addressDetails.balanceSat)
div(class="row")
div(class="summary-split-table-label") Balance
div(class="summary-split-table-content monospace")
- var currencyValue = new Decimal(addressDetails.balanceSat).dividedBy(coinConfig.baseCurrencyUnit.multiplier);
include includes/value-display.pug
if (addressDetails && addressDetails.txCount)
div(class="row")
div(class="summary-split-table-label") Transactions
div(class="summary-split-table-content monospace") #{electrumHistory.result.length.toLocaleString()}
div(class="summary-split-table-content monospace") #{addressDetails.txCount.toLocaleString()}
div(class="row")
div(class="summary-split-table-label") QR Code
@ -239,15 +238,22 @@ block content
div(class="card-header clearfix")
div(class="float-left")
span(class="h6")
if (txids)
if (txids.length == 1)
if (addressDetails && addressDetails.txCount)
if (addressDetails.txCount == 1)
span 1 Transaction
else
span #{txids.length.toLocaleString()} Transactions
span #{addressDetails.txCount.toLocaleString()} Transactions
else
span Transactions
if (!crawlerBot && txids && txids.length > 1)
if (config.addressApi)
if (config.addressApi == "electrumx")
small.text-muted.border-dotted.ml-2(title=`The list of transaction IDs for this address and the address balance were 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 and the address balance were 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")
@ -265,6 +271,10 @@ block content
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;")
@ -275,13 +285,29 @@ block content
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 (advancedFunctionality)
if (electrumHistoryError && electrumHistoryError.error && electrumHistoryError.error.code && electrumHistoryError.error.code == -32600)
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
@ -292,7 +318,7 @@ block content
div(class="card-header monospace clearfix")
div(class="float-left", style="margin-right: 0px;")
if (sort == "desc")
span ##{(txids.length - offset - txIndex).toLocaleString()}
span ##{(addressDetails.txCount - offset - txIndex).toLocaleString()}
else
span ##{(offset + txIndex + 1).toLocaleString()}
span &ndash;
@ -347,17 +373,18 @@ block content
pre
code(class="json bg-light") #{JSON.stringify(transactions, null, 4)}
if (!crawlerBot && txids && txids.length > limit)
if (!crawlerBot && addressDetails && addressDetails.txCount > limit)
- var txCount = addressDetails.txCount;
- var pageNumber = offset / limit + 1;
- var pageCount = Math.floor(txids.length / limit);
- if (pageCount * limit < txids.length) {
- var pageCount = Math.floor(txCount / limit);
- if (pageCount * limit < txCount) {
- pageCount++;
- }
- var paginationUrlFunction = function(x) {
- return paginationBaseUrl + "&limit=" + limit + "&offset=" + ((x - 1) * limit);
- }
hr
hr.mt-3
include includes/pagination.pug
@ -370,13 +397,9 @@ block content
pre
code(class="json bg-light", data-lang="json") #{JSON.stringify(result.validateaddress, null, 4)}
if (config.electrumXServers && config.electrumXServers.length > 0)
h4 ElectrumX-balance
pre
code(class="json bg-light", data-lang="json") #{JSON.stringify(electrumBalance, null, 4)}
h4 ElectrumX-history
if (addressDetails)
h4 addressDetails
pre
code(class="json bg-light", data-lang="json") #{JSON.stringify(electrumHistory, null, 4)}
code.json.bg-light #{JSON.stringify(addressDetails, null, 4)}

Loading…
Cancel
Save