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. 148
      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 qrcode = require("qrcode");
var fs = require('fs');
var electrumApi = require("./app/api/electrumApi.js");
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) {
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) {
return new Promise(function(resolve, reject) {
getBlockByHash(blockHash).then(function(block) {
@ -658,6 +701,7 @@ module.exports = {
getBlockByHashWithTransactions: getBlockByHashWithTransactions,
getRawTransaction: getRawTransaction,
getRawTransactions: getRawTransactions,
getRawTransactionsWithInputs: getRawTransactionsWithInputs,
getMempoolStats: getMempoolStats,
getUptimeSeconds: getUptimeSeconds,
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",
"debug": "~2.6.0",
"decimal.js": "7.2.3",
"electrum-client": "0.0.6",
"express": "~4.16.3",
"express-session": "1.15.6",
"jstransformer-markdown-it": "^2.0.0",

148
routes/baseActionsRouter.js

@ -5,6 +5,9 @@ var moment = require('moment');
var bitcoinCore = require("bitcoin-core");
var qrcode = require('qrcode');
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 coins = require("./../app/coins.js");
@ -492,9 +495,37 @@ router.get("/tx/:transactionId", 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;
res.locals.address = address;
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.paginationBaseUrl = ("/address/" + address);
res.locals.result = {};
@ -517,19 +548,118 @@ router.get("/address/:address", function(req, res) {
res.locals.payoutAddressForMiner = global.miningPoolsConfigs[i].payout_addresses[address];
}
}
coreApi.getAddress(address).then(function(result) {
res.locals.result.validateaddress = result;
qrcode.toDataURL(address, function(err, url) {
if (err) {
console.log("Error 93ygfew0ygf2gf2: " + err);
}
coreApi.getAddress(address).then(function(validateaddressResult) {
res.locals.result.validateaddress = validateaddressResult;
var promises = [];
if (global.electrumApi) {
var addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(validateaddressResult.scriptPubKey)));
addrScripthash = addrScripthash.match(/.{2}/g).reverse().join("");
res.locals.addressQrCodeUrl = url;
promises.push(new Promise(function(resolve, reject) {
electrumApi.getAddressBalance(addrScripthash).then(function(result) {
res.locals.balance = result;
res.locals.electrumBalance = result;
resolve();
}).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;
}
res.render("address");
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) {
res.locals.userMessage = "Failed to load address " + address + " (" + err + ")";

225
views/address.pug

@ -56,76 +56,203 @@ block content
div(class="card-header")
span(class="h6") Summary
div(class="card-body")
div(class="row")
div(class="col-md-6")
if (addressObj.hash)
div(class="row")
div(class="summary-table-label") Hash 160
div(class="summary-table-content monospace") #{addressObj.hash.toString("hex")}
if (addressObj.hash)
div(class="row")
div(class="summary-split-table-label") Hash 160
div(class="summary-split-table-content monospace") #{addressObj.hash.toString("hex")}
if (result.validateaddress.scriptPubKey)
div(class="row")
div(class="summary-table-label") Script Public Key
div(class="summary-table-content monospace") #{result.validateaddress.scriptPubKey}
if (result.validateaddress.scriptPubKey)
div(class="row")
div(class="summary-split-table-label") Script Public Key
div(class="summary-split-table-content monospace") #{result.validateaddress.scriptPubKey}
if (addressObj.hasOwnProperty("version"))
div(class="row")
div(class="summary-table-label") Version
div(class="summary-table-content monospace") #{addressObj.version}
if (addressObj.hasOwnProperty("version"))
div(class="row")
div(class="summary-split-table-label") Version
div(class="summary-split-table-content monospace") #{addressObj.version}
if (result.validateaddress.hasOwnProperty("witness_version"))
div(class="row")
div(class="summary-table-label") Witness Version
div(class="summary-table-content monospace") #{result.validateaddress.witness_version}
if (result.validateaddress.hasOwnProperty("witness_version"))
div(class="row")
div(class="summary-split-table-label") Witness Version
div(class="summary-split-table-content monospace") #{result.validateaddress.witness_version}
if (result.validateaddress.witness_program)
div(class="row")
div(class="summary-table-label") Witness Program
div(class="summary-table-content monospace") #{result.validateaddress.witness_program}
if (result.validateaddress.witness_program)
div(class="row")
div(class="summary-split-table-label") Witness Program
div(class="summary-split-table-content monospace") #{result.validateaddress.witness_program}
div(class="row")
div(class="summary-table-label") QR Code
div(class="summary-table-content monospace")
img(src=addressQrCodeUrl, alt=address, style="border: solid 1px #ccc;")
if (balance)
if (balance.conflictedResults)
div(class="row")
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")
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)
each item in balance.conflictedResults
- var currencyValue = item.confirmed / 100000000;
include includes/value-display.pug
else
if (balance.confirmed)
div(class="row")
div(class="summary-split-table-label") Balance
div(class="summary-split-table-content monospace")
- var currencyValue = balance.confirmed / 100000000;
include includes/value-display.pug
include includes/electrum-trust-note.pug
if (balance.unconfirmed)
div(class="row")
div(class="summary-split-table-label") Unconfirmed
div(class="summary-split-table-content monospace")
- var currencyValue = balance.unconfirmed / 100000000;
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")
else
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-header")
span(class="h6") Transactions
if (transactions)
include includes/electrum-trust-note.pug
div(class="card-body")
table(class="table")
strong
p(class="text-warning") This is a work-in-progress
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.
if (transactions)
each tx, txIndex in transactions
//pre
// 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
a(href="https://github.com/janoside/btc-rpc-explorer/issues/8") Suggestions and/or pull requests are welcome!
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
div(id="tab-json", class="tab-pane", role="tabpanel")
div(class="highlight")
h4 Node.ValidateAddress
pre
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