Browse Source

More work on #8 - showing balances and transaction history for addresses

- in tx history, request/display all inputs to get correct gain/loss values
- cross referencing for txid history from electrum
- include genesis coinbase values for genesis coinbase output address (electrum ignores the genesis coinbase TX and +50 value, but for consistency with the rest of this tool they're included)
- banner describing the electrum trust model
- ui tweaks including showing gain/loss for each address tx history item
fix-133-memory-crash
Dan Janosik 6 years ago
parent
commit
8bd07537b4
  1. 12
      app.js
  2. 10
      app/api/coreApi.js
  3. 40
      app/api/electrumApi.js
  4. 59
      app/coins/btc.js
  5. 58
      app/coins/ltc.js
  6. 4
      app/config.js
  7. 43
      routes/baseActionsRouter.js
  8. 160
      views/address.pug

12
app.js

@ -245,6 +245,18 @@ 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;

10
app/api/coreApi.js

@ -558,10 +558,18 @@ function getRawTransactions(txids) {
});
}
function getRawTransactionsWithInputs(txids) {
function getRawTransactionsWithInputs(txids, maxInputs=-1) {
return new Promise(function(resolve, reject) {
getRawTransactions(txids).then(function(transactions) {
var maxInputsTracked = config.site.txMaxInput;
if (maxInputs <= 0) {
maxInputsTracked = 1000000;
} else if (maxInputs > 0) {
maxInputsTracked = maxInputs;
}
var vinTxids = [];
for (var i = 0; i < transactions.length; i++) {
var transaction = transactions[i];

40
app/api/electrumApi.js

@ -1,4 +1,8 @@
var config = require("./../config.js");
var coins = require("../coins.js");
var utils = require("../utils.js");
var coinConfig = coins[config.coin];
const ElectrumClient = require('electrum-client');
@ -17,16 +21,16 @@ function connectToServer(host, port) {
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);
});
});
electrumClients.push(electrumClient);
}
function runOnServer(electrumClient, f) {
return new Promise(function(resolve, reject) {
f(electrumClient).then(function(result) {
resolve(result);
resolve({result:result, server:electrumClient.host});
}).catch(function(err) {
console.log("Error dif0e21qdh: " + JSON.stringify(err) + ", host=" + electrumClient.host + ", port=" + electrumClient.port);
@ -57,8 +61,26 @@ function getAddressTxids(addrScripthash) {
return electrumClient.blockchainScripthash_getHistory(addrScripthash);
}).then(function(results) {
resolve(results[0]);
if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) {
for (var i = 0; i < results.length; i++) {
results[i].result.unshift({tx_hash:coinConfig.genesisCoinbaseTransactionId, height:0});
}
}
var first = results[0];
var done = false;
for (var i = 1; i < results.length; i++) {
if (results[i].length != first.length) {
resolve({conflictedResults:results});
done = true;
}
}
if (!done) {
resolve(results[0]);
}
}).catch(function(err) {
reject(err);
});
@ -71,12 +93,21 @@ function getAddressBalance(addrScripthash) {
return electrumClient.blockchainScripthash_getBalance(addrScripthash);
}).then(function(results) {
if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) {
for (var i = 0; i < results.length; i++) {
var coinbaseBlockReward = coinConfig.blockRewardFunction(0);
results[i].result.confirmed += (coinbaseBlockReward * coinConfig.baseCurrencyUnit.multiplier);
}
}
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;
}
}
@ -84,7 +115,6 @@ function getAddressBalance(addrScripthash) {
if (!done) {
resolve(results[0]);
}
}).catch(function(err) {
reject(err);
});

59
app/coins/btc.js

@ -1,6 +1,34 @@
var Decimal = require("decimal.js");
Decimal8 = Decimal.clone({ precision:8, rounding:8 });
var btcCurrencyUnits = [
{
name:"BTC",
multiplier:1,
default:true,
values:["", "btc", "BTC"],
decimalPlaces:8
},
{
name:"mBTC",
multiplier:1000,
values:["mbtc"],
decimalPlaces:5
},
{
name:"bits",
multiplier:1000000,
values:["bits"],
decimalPlaces:2
},
{
name:"sat",
multiplier:100000000,
values:["sat", "satoshi"],
decimalPlaces:0
}
];
module.exports = {
name:"Bitcoin",
logoUrl:"/img/logo/btc.svg",
@ -14,33 +42,9 @@ module.exports = {
"https://raw.githubusercontent.com/btccom/Blockchain-Known-Pools/master/pools.json"
],
maxBlockWeight: 4000000,
currencyUnits:[
{
name:"BTC",
multiplier:1,
default:true,
values:["", "btc", "BTC"],
decimalPlaces:8
},
{
name:"mBTC",
multiplier:1000,
values:["mbtc"],
decimalPlaces:5
},
{
name:"bits",
multiplier:1000000,
values:["bits"],
decimalPlaces:2
},
{
name:"sat",
multiplier:100000000,
values:["sat", "satoshi"],
decimalPlaces:0
}
],
currencyUnits:btcCurrencyUnits,
currencyUnitsByName:{"BTC":btcCurrencyUnits[0], "mBTC":btcCurrencyUnits[1], "bits":btcCurrencyUnits[2], "sat":btcCurrencyUnits[3]},
baseCurrencyUnit:btcCurrencyUnits[3],
feeSatoshiPerByteBucketMaxima: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 75, 100, 150],
genesisBlockHash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
genesisCoinbaseTransactionId: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
@ -77,6 +81,7 @@ module.exports = {
"time": 1230988505,
"blocktime": 1230988505
},
genesisCoinbaseOutputAddressScripthash:"8b01df4e368ea28f8dc0423bcf7a4923e3a12d307c875e47a0cfbf90b5c39161",
historicalData: [
{
type: "blockheight",

58
app/coins/ltc.js

@ -1,6 +1,34 @@
var Decimal = require("decimal.js");
Decimal8 = Decimal.clone({ precision:8, rounding:8 });
var ltcCurrencyUnits = [
{
name:"LTC",
multiplier:1,
default:true,
values:["", "ltc", "LTC"],
decimalPlaces:8
},
{
name:"lite",
multiplier:1000,
values:["lite"],
decimalPlaces:5
},
{
name:"photon",
multiplier:1000000,
values:["photon"],
decimalPlaces:2
},
{
name:"litoshi",
multiplier:100000000,
values:["litoshi", "lit"],
decimalPlaces:0
}
];
module.exports = {
name:"Litecoin",
logoUrl:"/img/logo/ltc.svg",
@ -12,33 +40,9 @@ module.exports = {
"https://raw.githubusercontent.com/hashstream/pools/master/pools.json",
],
maxBlockWeight: 4000000,
currencyUnits:[
{
name:"LTC",
multiplier:1,
default:true,
values:["", "ltc", "LTC"],
decimalPlaces:8
},
{
name:"lite",
multiplier:1000,
values:["lite"],
decimalPlaces:5
},
{
name:"photon",
multiplier:1000000,
values:["photon"],
decimalPlaces:2
},
{
name:"litoshi",
multiplier:100000000,
values:["litoshi", "lit"],
decimalPlaces:0
}
],
currencyUnits:ltcCurrencyUnits,
currencyUnitsByName:{"LTC":ltcCurrencyUnits[0], "lite":ltcCurrencyUnits[1], "photon":ltcCurrencyUnits[2], "litoshi":ltcCurrencyUnits[3]},
baseCurrencyUnit:ltcCurrencyUnits[3],
feeSatoshiPerByteBucketMaxima: [5, 10, 25, 50, 100, 150, 200, 250],
genesisBlockHash: "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2",
genesisCoinbaseTransactionId: "97ddfbbae6be97fd6cdf3e7ca13232a3afff2353e29badfab7f73011edd4ced9",

4
app/config.js

@ -32,6 +32,10 @@ module.exports = {
"pruneblockchain"
],
// https://uasf.saltylemon.org/electrum
electrumXServers:[
],
site: {
blockTxPageSize:20,
addressTxPageSize:20,

43
routes/baseActionsRouter.js

@ -525,7 +525,8 @@ router.get("/address/:address", function(req, res) {
res.locals.address = address;
res.locals.limit = limit;
res.locals.offset = offset;
res.locals.paginationBaseUrl = ("/address/" + address);
res.locals.sort = sort;
res.locals.paginationBaseUrl = ("/address/" + address + "?sort=" + sort);
res.locals.result = {};
@ -557,6 +558,8 @@ router.get("/address/:address", function(req, res) {
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;
@ -572,14 +575,25 @@ router.get("/address/:address", function(req, res) {
promises.push(new Promise(function(resolve, reject) {
electrumApi.getAddressTxids(addrScripthash).then(function(result) {
res.locals.electrumHistory = result;
var txidResult = null;
if (result.conflictedResults) {
res.locals.conflictedTxidResults = true;
txidResult = result.conflictedResults[0];
} else {
txidResult = result;
}
res.locals.electrumHistory = txidResult;
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;
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;
}
if (sort == "desc") {
@ -606,7 +620,7 @@ router.get("/address/:address", function(req, res) {
var txInputs = rawTxResult.txInputsByTransaction[tx.txid];
for (var j = 0; j < tx.vout.length; j++) {
if (tx.vout[j].scriptPubKey.addresses.includes(address)) {
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);
}
@ -618,16 +632,17 @@ router.get("/address/:address", function(req, res) {
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);
}
if (txInput != null) {
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));
addrLossesByTx[tx.txid] = addrLossesByTx[tx.txid].plus(new Decimal(txInput.vout[k].value));
}
}
}
}
//console.log("tx: " + JSON.stringify(tx));
@ -658,6 +673,8 @@ router.get("/address/:address", function(req, res) {
});
}).catch(function(err) {
console.log(err);
res.render("address");
});
}).catch(function(err) {

160
views/address.pug

@ -52,6 +52,22 @@ block content
div(class="tab-content")
div(id="tab-details", class="tab-pane active", role="tabpanel")
if (config.electrumXServers && config.electrumXServers.length > 0)
if (session.hideElectrumTrustWarnings != "true")
div(class="alert alert-primary alert-dismissible clearfix", 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}
a(href="/changeSetting?name=hideElectrumTrustWarnings&value=true", class="close", aria-label="Close", style="text-decoration: none;")
span(aria-hidden="true") &times;
div(class="card mb-3")
div(class="card-header")
span(class="h6") Summary
@ -90,10 +106,9 @@ block content
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
each item in balance.conflictedResults
- var currencyValue = item.confirmed / 100000000;
- var currencyValue = item.confirmed / coinConfig.baseCurrencyUnit.multiplier;
include includes/value-display.pug
@ -101,23 +116,20 @@ block content
div(class="row")
div(class="summary-split-table-label") Balance
div(class="summary-split-table-content monospace")
- var currencyValue = balance.confirmed / 100000000;
- var currencyValue = balance.result.confirmed / coinConfig.baseCurrencyUnit.multiplier;
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;
- var currencyValue = balance.unconfirmed / coinConfig.baseCurrencyUnit.multiplier;
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="summary-split-table-content monospace") #{electrumHistory.result.length.toLocaleString()}
div(class="row")
div(class="summary-split-table-label") QR Code
@ -125,6 +137,11 @@ block content
img(src=addressQrCodeUrl, alt=address, style="border: solid 1px #ccc;")
div(class="col-md-6")
if (electrumScripthash)
div(class="row")
div(class="summary-split-table-label") Scripthash
div(class="summary-split-table-content monospace") #{electrumScripthash}
- 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];
@ -164,34 +181,79 @@ block content
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-header clearfix")
div(class="float-left")
span(class="h6")
if (txids)
if (txids.length == 1)
span 1 Transaction
else
span #{txids.length.toLocaleString()} Transactions
else
span Transactions
if (!crawlerBot && txids && txids.length > 0)
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
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 (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
else
span(class="text-danger") Unconfirmed
br
a(href=("/tx/" + tx.txid)) #{tx.txid}
br
div(class="float-left", style="margin-right: 0px;")
if (sort == "desc")
span ##{(txids.length - offset - txIndex).toLocaleString()}
else
span ##{(offset + txIndex + 1).toLocaleString()}
span &ndash;
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") +
@ -204,11 +266,6 @@ block content
- 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)
@ -216,6 +273,7 @@ block content
- 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
@ -224,18 +282,19 @@ block content
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
if (!crawlerBot && txids && txids.length > limit)
- 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
include includes/pagination.pug
@ -246,12 +305,13 @@ block content
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)}
if (config.electrumXServers && config.electrumXServers.length > 0)
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)}
h4 Electrum.History
pre
code(class="language-json", data-lang="json") #{JSON.stringify(electrumHistory, null, 4)}

Loading…
Cancel
Save