Browse Source

Majority of the work on #8 to support querying N ElectrumX servers for address balance and address txid history

fix-133-memory-crash
Dan Janosik 6 years ago
parent
commit
1f38c5dd93
  1. 7
      app.js
  2. 44
      app/api/coreApi.js
  3. 98
      app/api/electrumApi.js
  4. 1
      package.json
  5. 146
      routes/baseActionsRouter.js
  6. 225
      views/address.pug
  7. 3
      views/includes/electrum-trust-note.pug

7
app.js

@ -22,6 +22,7 @@ var coins = require("./app/coins.js");
var request = require("request"); var request = require("request");
var qrcode = require("qrcode"); var qrcode = require("qrcode");
var fs = require('fs'); var fs = require('fs');
var electrumApi = require("./app/api/electrumApi.js");
var crawlerBotUserAgentStrings = [ "Googlebot", "Bingbot", "Slurp", "DuckDuckBot", "Baiduspider", "YandexBot", "Sogou", "Exabot", "facebot", "ia_archiver" ]; var crawlerBotUserAgentStrings = [ "Googlebot", "Bingbot", "Slurp", "DuckDuckBot", "Baiduspider", "YandexBot", "Sogou", "Exabot", "facebot", "ia_archiver" ];
@ -121,6 +122,12 @@ app.runOnStartup = function() {
}); });
} }
if (config.electrumXServers && config.electrumXServers.length > 0) {
electrumApi.connectToServers();
global.electrumApi = electrumApi;
}
if (global.coinConfig.miningPoolsConfigUrls) { if (global.coinConfig.miningPoolsConfigUrls) {
var promises = []; var promises = [];

44
app/api/coreApi.js

@ -558,6 +558,49 @@ function getRawTransactions(txids) {
}); });
} }
function getRawTransactionsWithInputs(txids) {
return new Promise(function(resolve, reject) {
getRawTransactions(txids).then(function(transactions) {
var maxInputsTracked = config.site.txMaxInput;
var vinTxids = [];
for (var i = 0; i < transactions.length; i++) {
var transaction = transactions[i];
if (transaction && transaction.vin) {
for (var j = 0; j < Math.min(maxInputsTracked, transaction.vin.length); j++) {
if (transaction.vin[j].txid) {
vinTxids.push(transaction.vin[j].txid);
}
}
}
}
var txInputsByTransaction = {};
getRawTransactions(vinTxids).then(function(vinTransactions) {
var vinTxById = {};
vinTransactions.forEach(function(tx) {
vinTxById[tx.txid] = tx;
});
transactions.forEach(function(tx) {
txInputsByTransaction[tx.txid] = {};
if (tx && tx.vin) {
for (var i = 0; i < Math.min(maxInputsTracked, tx.vin.length); i++) {
if (vinTxById[tx.vin[i].txid]) {
txInputsByTransaction[tx.txid][i] = vinTxById[tx.vin[i].txid];
}
}
}
resolve({ transactions:transactions, txInputsByTransaction:txInputsByTransaction });
});
});
});
});
}
function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) { function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
getBlockByHash(blockHash).then(function(block) { getBlockByHash(blockHash).then(function(block) {
@ -658,6 +701,7 @@ module.exports = {
getBlockByHashWithTransactions: getBlockByHashWithTransactions, getBlockByHashWithTransactions: getBlockByHashWithTransactions,
getRawTransaction: getRawTransaction, getRawTransaction: getRawTransaction,
getRawTransactions: getRawTransactions, getRawTransactions: getRawTransactions,
getRawTransactionsWithInputs: getRawTransactionsWithInputs,
getMempoolStats: getMempoolStats, getMempoolStats: getMempoolStats,
getUptimeSeconds: getUptimeSeconds, getUptimeSeconds: getUptimeSeconds,
getHelp: getHelp, getHelp: getHelp,

98
app/api/electrumApi.js

@ -0,0 +1,98 @@
var config = require("./../config.js");
const ElectrumClient = require('electrum-client');
var electrumClients = [];
function connectToServers() {
for (var i = 0; i < config.electrumXServers.length; i++) {
connectToServer(config.electrumXServers[i].host, config.electrumXServers[i].port);
}
}
function connectToServer(host, port) {
console.log("Connecting to ElectrumX Server: " + host + ":" + port);
var electrumClient = new ElectrumClient(port, host, 'tls');
electrumClient.connect().then(function() {
electrumClient.server_version("btc-rpc-explorer-1.1", "1.2").then(function(res) {
console.log("Connected to ElectrumX Server: " + host + ":" + port + ", versions: " + res);
});
});
electrumClients.push(electrumClient);
}
function runOnServer(electrumClient, f) {
return new Promise(function(resolve, reject) {
f(electrumClient).then(function(result) {
resolve(result);
}).catch(function(err) {
console.log("Error dif0e21qdh: " + JSON.stringify(err) + ", host=" + electrumClient.host + ", port=" + electrumClient.port);
});
});
}
function runOnAllServers(f) {
return new Promise(function(resolve, reject) {
var promises = [];
for (var i = 0; i < electrumClients.length; i++) {
promises.push(runOnServer(electrumClients[i], f));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
function getAddressTxids(addrScripthash) {
return new Promise(function(resolve, reject) {
runOnAllServers(function(electrumClient) {
return electrumClient.blockchainScripthash_getHistory(addrScripthash);
}).then(function(results) {
resolve(results[0]);
}).catch(function(err) {
reject(err);
});
});
}
function getAddressBalance(addrScripthash) {
return new Promise(function(resolve, reject) {
runOnAllServers(function(electrumClient) {
return electrumClient.blockchainScripthash_getBalance(addrScripthash);
}).then(function(results) {
var first = results[0];
var done = false;
for (var i = 1; i < results.length; i++) {
if (results[i].confirmed != first.confirmed) {
resolve({conflictedResults:results});
done = true;
}
}
if (!done) {
resolve(results[0]);
}
}).catch(function(err) {
reject(err);
});
});
}
module.exports = {
connectToServers: connectToServers,
getAddressTxids: getAddressTxids,
getAddressBalance: getAddressBalance
};

1
package.json

@ -27,6 +27,7 @@
"crypto-js": "3.1.9-1", "crypto-js": "3.1.9-1",
"debug": "~2.6.0", "debug": "~2.6.0",
"decimal.js": "7.2.3", "decimal.js": "7.2.3",
"electrum-client": "0.0.6",
"express": "~4.16.3", "express": "~4.16.3",
"express-session": "1.15.6", "express-session": "1.15.6",
"jstransformer-markdown-it": "^2.0.0", "jstransformer-markdown-it": "^2.0.0",

146
routes/baseActionsRouter.js

@ -5,6 +5,9 @@ var moment = require('moment');
var bitcoinCore = require("bitcoin-core"); var bitcoinCore = require("bitcoin-core");
var qrcode = require('qrcode'); var qrcode = require('qrcode');
var bitcoinjs = require('bitcoinjs-lib'); var bitcoinjs = require('bitcoinjs-lib');
var sha256 = require("crypto-js/sha256");
var hexEnc = require("crypto-js/enc-hex");
var Decimal = require("decimal.js");
var utils = require('./../app/utils.js'); var utils = require('./../app/utils.js');
var coins = require("./../app/coins.js"); var coins = require("./../app/coins.js");
@ -492,9 +495,37 @@ router.get("/tx/:transactionId", function(req, res) {
}); });
router.get("/address/:address", function(req, res) { router.get("/address/:address", function(req, res) {
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; var address = req.params.address;
res.locals.address = address; res.locals.address = address;
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.paginationBaseUrl = ("/address/" + address);
res.locals.result = {}; res.locals.result = {};
@ -518,18 +549,117 @@ router.get("/address/:address", function(req, res) {
} }
} }
coreApi.getAddress(address).then(function(result) { coreApi.getAddress(address).then(function(validateaddressResult) {
res.locals.result.validateaddress = result; res.locals.result.validateaddress = validateaddressResult;
qrcode.toDataURL(address, function(err, url) { var promises = [];
if (err) { if (global.electrumApi) {
console.log("Error 93ygfew0ygf2gf2: " + err); var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey)));
} addrScripthash = addrScripthash.match(/.{2}/g).reverse().join("");
promises.push(new Promise(function(resolve, reject) {
electrumApi.getAddressBalance(addrScripthash).then(function(result) {
res.locals.balance = result;
res.locals.electrumBalance = result;
res.locals.addressQrCodeUrl = url; resolve();
res.render("address"); }).catch(function(err) {
reject(err);
});
}));
promises.push(new Promise(function(resolve, reject) {
electrumApi.getAddressTxids(addrScripthash).then(function(result) {
res.locals.electrumHistory = result;
var txids = [];
var blockHeightsByTxid = {};
for (var i = 0; i < result.length; i++) {
txids.push(result[i].tx_hash);
blockHeightsByTxid[result[i].tx_hash] = result[i].height;
}
if (sort == "desc") {
txids = txids.reverse();
}
res.locals.txids = txids;
var pagedTxids = [];
for (var i = offset; i < (offset + limit); i++) {
pagedTxids.push(txids[i]);
}
coreApi.getRawTransactionsWithInputs(pagedTxids).then(function(rawTxResult) {
res.locals.transactions = rawTxResult.transactions;
res.locals.txInputsByTransaction = rawTxResult.txInputsByTransaction;
res.locals.blockHeightsByTxid = blockHeightsByTxid;
var addrGainsByTx = {};
var 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].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];
for (var k = 0; k < txInput.vout.length; k++) {
if (txInput.vout[k].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[k].value));
}
}
}
//console.log("tx: " + JSON.stringify(tx));
//console.log("txInputs: " + JSON.stringify(txInputs));
}
res.locals.addrGainsByTx = addrGainsByTx;
res.locals.addrLossesByTx = addrLossesByTx;
resolve();
});
}).catch(function(err) {
reject(err);
});
}));
}
Promise.all(promises).then(function() {
qrcode.toDataURL(address, function(err, url) {
if (err) {
console.log("Error 93ygfew0ygf2gf2: " + err);
}
res.locals.addressQrCodeUrl = url;
res.render("address");
});
}).catch(function(err) {
console.log(err);
}); });
}).catch(function(err) { }).catch(function(err) {
res.locals.userMessage = "Failed to load address " + address + " (" + err + ")"; res.locals.userMessage = "Failed to load address " + address + " (" + err + ")";

225
views/address.pug

@ -56,76 +56,203 @@ block content
div(class="card-header") div(class="card-header")
span(class="h6") Summary span(class="h6") Summary
div(class="card-body") div(class="card-body")
div(class="row")
div(class="col-md-6")
if (addressObj.hash) if (addressObj.hash)
div(class="row") div(class="row")
div(class="summary-table-label") Hash 160 div(class="summary-split-table-label") Hash 160
div(class="summary-table-content monospace") #{addressObj.hash.toString("hex")} div(class="summary-split-table-content monospace") #{addressObj.hash.toString("hex")}
if (result.validateaddress.scriptPubKey) if (result.validateaddress.scriptPubKey)
div(class="row") div(class="row")
div(class="summary-table-label") Script Public Key div(class="summary-split-table-label") Script Public Key
div(class="summary-table-content monospace") #{result.validateaddress.scriptPubKey} div(class="summary-split-table-content monospace") #{result.validateaddress.scriptPubKey}
if (addressObj.hasOwnProperty("version")) if (addressObj.hasOwnProperty("version"))
div(class="row") div(class="row")
div(class="summary-table-label") Version div(class="summary-split-table-label") Version
div(class="summary-table-content monospace") #{addressObj.version} div(class="summary-split-table-content monospace") #{addressObj.version}
if (result.validateaddress.hasOwnProperty("witness_version")) if (result.validateaddress.hasOwnProperty("witness_version"))
div(class="row") div(class="row")
div(class="summary-table-label") Witness Version div(class="summary-split-table-label") Witness Version
div(class="summary-table-content monospace") #{result.validateaddress.witness_version} div(class="summary-split-table-content monospace") #{result.validateaddress.witness_version}
if (result.validateaddress.witness_program) if (result.validateaddress.witness_program)
div(class="row") div(class="row")
div(class="summary-table-label") Witness Program div(class="summary-split-table-label") Witness Program
div(class="summary-table-content monospace") #{result.validateaddress.witness_program} div(class="summary-split-table-content monospace") #{result.validateaddress.witness_program}
div(class="row") if (balance)
div(class="summary-table-label") QR Code if (balance.conflictedResults)
div(class="summary-table-content monospace") div(class="row")
img(src=addressQrCodeUrl, alt=address, style="border: solid 1px #ccc;") div(class="summary-split-table-label") Balance
div(class="summary-split-table-content monospace")
span(class="text-danger") Conflicted ElectrumX Results
include includes/electrum-trust-note.pug
div(class="card mb-3") each item in balance.conflictedResults
div(class="card-header") - var currencyValue = item.confirmed / 100000000;
span(class="h6") Flags include includes/value-display.pug
div(class="card-body")
table(class="table table-responsive-sm text-center")
thead else
tr if (balance.confirmed)
th Is Valid? div(class="row")
th Is Script? div(class="summary-split-table-label") Balance
th Is Witness? div(class="summary-split-table-content monospace")
th Is Mine? - var currencyValue = balance.confirmed / 100000000;
th Is Watch-Only? include includes/value-display.pug
tbody include includes/electrum-trust-note.pug
tr
- var x = result.validateaddress; if (balance.unconfirmed)
- var flags = [x.isvalid, x.isscript, x.iswitness, x.ismine, x.iswatchonly]; div(class="row")
div(class="summary-split-table-label") Unconfirmed
each flag in flags div(class="summary-split-table-content monospace")
td - var currencyValue = balance.unconfirmed / 100000000;
if (flag) include includes/value-display.pug
include includes/electrum-trust-note.pug
if (electrumHistory)
div(class="row")
div(class="summary-split-table-label") Transactions
div(class="summary-split-table-content monospace") #{electrumHistory.length.toLocaleString()}
include includes/electrum-trust-note.pug
div(class="row")
div(class="summary-split-table-label") QR Code
div(class="summary-split-table-content monospace")
img(src=addressQrCodeUrl, alt=address, style="border: solid 1px #ccc;")
div(class="col-md-6")
- var x = result.validateaddress;
- var flagNames = ["Is Valid?", "Is Script?", "Is Witness?", "Is Mine?", "Is Watch-Only?"];
- var flags = [x.isvalid, x.isscript, x.iswitness, x.ismine, x.iswatchonly];
each flagName, index in flagNames
div(class="row")
div(class="summary-split-table-label") #{flagName}
div(class="summary-split-table-content monospace")
if (flags[index])
i(class="fas fa-check text-success") i(class="fas fa-check text-success")
else else
i(class="fas fa-times text-danger") i(class="fas fa-times text-danger")
if (false)
div(class="card mb-3")
div(class="card-header")
span(class="h6") Flags
div(class="card-body")
table(class="table table-responsive-sm text-center")
thead
tr
th Is Valid?
th Is Script?
th Is Witness?
th Is Mine?
th Is Watch-Only?
tbody
tr
- var x = result.validateaddress;
- var flags = [x.isvalid, x.isscript, x.iswitness, x.ismine, x.iswatchonly];
each flag in flags
td
if (flag)
i(class="fas fa-check text-success")
else
i(class="fas fa-times text-danger")
div(class="card") div(class="card")
div(class="card-header") div(class="card-header")
span(class="h6") Transactions span(class="h6") Transactions
if (transactions)
include includes/electrum-trust-note.pug
div(class="card-body") div(class="card-body")
table(class="table") if (transactions)
strong each tx, txIndex in transactions
p(class="text-warning") This is a work-in-progress //pre
p Since this app is database-free, displaying a list of transactions involving the current address is tricky. I'm actively researching the best way to implement this. // code #{JSON.stringify(tx, null, 4)}
div(class="xcard mb-3")
div(class="card-header monospace clearfix")
div(class="float-left", style="margin-right: 10px;")
span ##{(offset + txIndex + 1).toLocaleString()}
span &ndash;
div(class="float-left")
if (tx && tx.txid)
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
br
a(href=("/tx/" + tx.txid)) #{tx.txid}
br
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
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="card-body")
if (true)
- var txInputs = txInputsByTransaction[tx.txid];
- var blockHeight = blockHeightsByTxid[tx.txid];
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.stringify(transactions, null, 4)}
- var pageNumber = offset / limit + 1;
- var pageCount = Math.floor(txids.length / limit);
- if (pageCount * limit < txids.length) {
- pageCount++;
- }
- var paginationUrlFunction = function(x) {
- return paginationBaseUrl + "?limit=" + limit + "&offset=" + ((x - 1) * limit);
- }
hr
include includes/pagination.pug
a(href="https://github.com/janoside/btc-rpc-explorer/issues/8") Suggestions and/or pull requests are welcome!
div(id="tab-json", class="tab-pane", role="tabpanel") div(id="tab-json", class="tab-pane", role="tabpanel")
div(class="highlight") div(class="highlight")
h4 Node.ValidateAddress
pre pre
code(class="language-json", data-lang="json") #{JSON.stringify(result.validateaddress, null, 4)} code(class="language-json", data-lang="json") #{JSON.stringify(result.validateaddress, null, 4)}
h4 Electrum.Balance
pre
code(class="language-json", data-lang="json") #{JSON.stringify(electrumBalance, null, 4)}
h4 Electrum.History
pre
code(class="language-json", data-lang="json") #{JSON.stringify(electrumHistory, null, 4)}

3
views/includes/electrum-trust-note.pug

@ -0,0 +1,3 @@
span
span(data-toggle="tooltip", title="This data is at least partially generated from the ElectrumX servers currently configured: ")
i(class="fas fa-exclamation-triangle text-warning")
Loading…
Cancel
Save