diff --git a/app/rpcApi.js b/app/rpcApi.js index 7bd3661..78ac81a 100644 --- a/app/rpcApi.js +++ b/app/rpcApi.js @@ -315,6 +315,22 @@ function getRawTransaction(txid) { }); } +function getAddress(address) { + return new Promise(function(resolve, reject) { + client.command('validateaddress', address, function(err, result, resHeaders) { + if (err) { + console.log("Error 9234ygf0weg: " + err); + + reject(err); + + return; + } + + resolve(result); + }); + }); +} + function getRawTransactions(txids) { console.log("getRawTransactions: " + txids); @@ -590,5 +606,6 @@ module.exports = { getUptimeSeconds: getUptimeSeconds, getHelp: getHelp, getRpcMethodHelp: getRpcMethodHelp, - getHistoricalData: getHistoricalData + getHistoricalData: getHistoricalData, + getAddress: getAddress }; \ No newline at end of file diff --git a/package.json b/package.json index 5e3a418..f75efdc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "body-parser": "~1.18.2", "cookie-parser": "~1.4.3", "bitcoin-core": "2.0.0", + "bitcoinjs-lib": "3.3.2", "crypto-js": "3.1.9-1", "debug": "~2.6.0", "decimal.js":"7.2.3", diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index f52abd6..ea191e0 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -7,6 +7,7 @@ var env = require("./../app/env"); var bitcoinCore = require("bitcoin-core"); var rpcApi = require("./../app/rpcApi"); var qrcode = require('qrcode'); +var bitcoinjs = require('bitcoinjs-lib'); router.get("/", function(req, res) { if (req.session.host == null || req.session.host.trim() == "") { @@ -229,6 +230,7 @@ router.post("/search", function(req, res) { } var query = req.body.query.toLowerCase().trim(); + var rawCaseQuery = req.body.query.trim(); req.session.query = req.body.query; @@ -247,6 +249,14 @@ router.post("/search", function(req, res) { return; } + rpcApi.getAddress(rawCaseQuery).then(function(validateaddress) { + if (validateaddress && validateaddress.isvalid) { + res.redirect("/address/" + rawCaseQuery); + + return; + } + }); + req.session.userMessage = "No results found for query: " + query; res.redirect("/"); @@ -289,14 +299,18 @@ router.post("/search", function(req, res) { res.redirect("/"); }); } else { - req.session.userMessage = "Invalid query: " + query; + rpcApi.getAddress(rawCaseQuery).then(function(validateaddress) { + if (validateaddress && validateaddress.isvalid) { + res.redirect("/address/" + rawCaseQuery); - res.redirect("/"); + return; + } - return; - } + req.session.userMessage = "No results found for query: " + rawCaseQuery; - + res.redirect("/"); + }); + } }); router.get("/block-height/:blockHeight", function(req, res) { @@ -412,6 +426,39 @@ router.get("/tx/:transactionId", function(req, res) { }); }); +router.get("/address/:address", function(req, res) { + var address = req.params.address; + + res.locals.address = address; + + res.locals.result = {}; + + if (address.startsWith("1") || address.startsWith("3")) { + res.locals.addressObj = bitcoinjs.address.fromBase58Check(address); + + } else { + res.locals.addressObj = bitcoinjs.address.fromBech32(address); + } + + rpcApi.getAddress(address).then(function(result) { + res.locals.result.validateaddress = result; + + qrcode.toDataURL(address, function(err, url) { + if (err) { + console.log("Error 93ygfew0ygf2gf2: " + err); + } + + res.locals.addressQrCodeUrl = url; + + res.render("address"); + }); + }).catch(function(err) { + res.locals.userMessage = "Failed to load address " + address + " (" + err + ")"; + + res.render("address"); + }); +}); + router.get("/rpc-terminal", function(req, res) { if (!env.demoSite) { var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; diff --git a/views/address.pug b/views/address.pug new file mode 100644 index 0000000..bac052c --- /dev/null +++ b/views/address.pug @@ -0,0 +1,104 @@ +extends layout + +block headContent + title Address #{address} + +block content + if (result && result.validateaddress) + if (!result.validateaddress.isvalid) + h1(class="h2 text-danger") Invalid Address + br + small(class="monospace") #{address} + else + h1(class="h2") Address + br + small(class="monospace") #{address} + + hr + + if (false) + pre + code #{JSON.stringify(addressObj, null, 4)} + + ul(class='nav nav-tabs mb-3') + li(class="nav-item") + a(data-toggle="tab", href="#tab-summary", class="nav-link active", role="tab") Summary + li(class="nav-item") + a(data-toggle="tab", href="#tab-raw", class="nav-link", role="tab") Raw + + + div(class="tab-content") + div(id="tab-summary", class="tab-pane active", role="tabpanel") + table(class="table") + if (addressObj.hash) + tr + th(class="table-active properties-header") Hash 160 + td(class="monospace") #{addressObj.hash.toString("hex")} + + if (result.validateaddress.scriptPubKey) + tr + th(class="table-active properties-header") Script Public Key + td(class="monospace") #{result.validateaddress.scriptPubKey} + + if (addressObj.hasOwnProperty("version")) + tr + th(class="table-active properties-header") Version + td(class="monospace") #{addressObj.version} + + if (result.validateaddress.hasOwnProperty("witness_version")) + tr + th(class="table-active properties-header") Witness Version + td(class="monospace") #{result.validateaddress.witness_version} + + if (result.validateaddress.witness_program) + tr + th(class="table-active properties-header") Witness Program + td(class="monospace") #{result.validateaddress.witness_program} + + tr + th(class="table-active properties-header") QR Code + td(class="monospace") + img(src=addressQrCodeUrl, alt=address) + + div(class="card mb-3") + div(class="card-header") + span(class="h6") Flags + div(class="card-body") + table(class="table 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 + 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. + + a(href="https://github.com/janoside/btc-rpc-explorer/issues/8") Suggestions and/or pull requests are welcome! + + + div(id="tab-raw", class="tab-pane", role="tabpanel") + div(class="highlight") + pre + code(class="language-json", data-lang="json") #{JSON.stringify(result.validateaddress, null, 4)} + + diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index 3a0ae93..b118ae7 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -159,9 +159,11 @@ div(class="tab-content") td if (vout.scriptPubKey && vout.scriptPubKey.addresses) - div(class="monospace", style="word-break: break-word;") #{vout.scriptPubKey.addresses[0]} - span(class="monospace text-muted") via tx - a(href=("/tx/" + txInput.txid + "#output-" + tx.vin[txInputIndex].vout), class="monospace") #{txInput.txid.substring(0, 14)}..., Output ##{tx.vin[txInputIndex].vout + 1} + div(class="monospace", style="word-break: break-word;") + a(href=("/address/" + vout.scriptPubKey.addresses[0])) #{vout.scriptPubKey.addresses[0]} + + span(class="monospace") via + a(href=("/tx/" + txInput.txid + "#output-" + tx.vin[txInputIndex].vout), class="monospace") tx:#{txInput.txid.substring(0, 14)}...[#{tx.vin[txInputIndex].vout}] td if (vout.value) - totalInputValue = totalInputValue.plus(new Decimal(vout.value)); @@ -203,7 +205,7 @@ div(class="tab-content") td if (vout.scriptPubKey) if (vout.scriptPubKey.addresses) - a(id="output-" + voutIndex) + a(id=("output-" + voutIndex), href=("/address/" + vout.scriptPubKey.addresses[0])) div(class="monospace", style="word-break: break-word;") #{vout.scriptPubKey.addresses[0]} else if (vout.scriptPubKey.hex && vout.scriptPubKey.hex.startsWith('6a24aa21a9ed')) diff --git a/views/layout.pug b/views/layout.pug index 2a0d783..5e1bfff 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -60,7 +60,7 @@ html form(method="post", action="/search", class="form-inline") div(class="input-group input-group-sm") - input(type="text", class="form-control form-control-sm", name="query", placeholder="block height, block hash, txid", value=(query), style="width: 300px;") + input(type="text", class="form-control form-control-sm", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 300px;") div(class="input-group-append") button(type="submit", class="btn btn-primary") i(class="fas fa-search") diff --git a/views/transaction.pug b/views/transaction.pug index b4e0dae..a5656e9 100644 --- a/views/transaction.pug +++ b/views/transaction.pug @@ -183,9 +183,10 @@ block content td if (vout.scriptPubKey && vout.scriptPubKey.addresses) - div(class="monospace", style="word-break: break-word;") #{vout.scriptPubKey.addresses[0]} - span(class="monospace text-muted") via tx - a(href=("/tx/" + txInput.txid + "#output-" + result.getrawtransaction.vin[txInputIndex].vout), class="monospace") #{txInput.txid.substring(0, 14)}..., Output ##{result.getrawtransaction.vin[txInputIndex].vout + 1} + div(class="monospace", style="word-break: break-word;") + a(href=("/address/" + vout.scriptPubKey.addresses[0])) #{vout.scriptPubKey.addresses[0]} + span(class="monospace") via + a(href=("/tx/" + txInput.txid + "#output-" + result.getrawtransaction.vin[txInputIndex].vout), class="monospace") #{txInput.txid.substring(0, 14)}...[#{result.getrawtransaction.vin[txInputIndex].vout}] td if (vout.value) - var currencyValue = vout.value; @@ -215,7 +216,7 @@ block content td if (vout.scriptPubKey) if (vout.scriptPubKey.addresses) - a(id="output-" + voutIndex) + a(id=("output-" + voutIndex), href=("/address/" + vout.scriptPubKey.addresses[0])) div(class="monospace", style="word-break: break-word;") #{vout.scriptPubKey.addresses[0]} else if (vout.scriptPubKey.hex && vout.scriptPubKey.hex.startsWith('6a24aa21a9ed'))