Skip to content
Browse files

Initial import.

  • Loading branch information...
0 parents commit eee886820ea3df18a10a174e54a3a046151ac527 @justmoon justmoon committed
Showing with 649 additions and 0 deletions.
  1. +21 −0 .livereload
  2. +315 −0 app.js
  3. +50 −0 public/stylesheets/style.css
  4. +21 −0 test/app.test.js
  5. +65 −0 views/address.ejs
  6. +73 −0 views/block.ejs
  7. +23 −0 views/index.ejs
  8. +11 −0 views/layout.ejs
  9. +70 −0 views/transaction.ejs
21 .livereload
@@ -0,0 +1,21 @@
+# Lines starting with pound sign (#) are ignored.
+
+# additional extensions to monitor
+config.exts << 'ejs'
+
+# exclude files with NAMES matching this mask
+#config.exclusions << '~*'
+# exclude files with PATHS matching this mask (if the mask contains a slash)
+#config.exclusions << '/excluded_dir/*'
+# exclude files with PATHS matching this REGEXP
+#config.exclusions << /somedir.*(ab){2,4}.(css|js)$/
+
+# reload the whole page when .js changes
+#config.apply_js_live = false
+# reload the whole page when .css changes
+#config.apply_css_live = false
+# reload the whole page when images (png, jpg, gif) change
+#config.apply_images_live = false
+
+# wait 100ms for more changes before reloading a page
+#config.grace_period = 0.1
315 app.js
@@ -0,0 +1,315 @@
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express');
+var winston = require('winston');
+var bitcoin = require('bitcoin-p2p');
+var bigint = require('bigint');
+
+global.Util = bitcoin.Util;
+global.bigint = bitcoin.bigint;
+
+var app = module.exports = express.createServer();
+
+var storage = new bitcoin.Storage('mongodb://localhost/bitcoin');
+var node = new bitcoin.Node();
+var chain = node.getBlockChain();
+
+node.cfg.network.bootstrap = [];
+node.addPeer('localhost');
+
+node.start();
+
+// Configuration
+
+app.configure(function(){
+ app.set('views', __dirname + '/views');
+ app.set('view engine', 'ejs');
+ app.use(express.bodyParser());
+ app.use(express.methodOverride());
+ app.use(app.router);
+ app.use(express.static(__dirname + '/public'));
+});
+
+app.configure('development', function(){
+ app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
+});
+
+app.configure('production', function(){
+ app.use(express.errorHandler());
+});
+
+function getOutpoints(txs, callback) {
+ // If we got only one tx, wrap it so we can use the same code afterwards
+ if (txs.hash) txs = [txs];
+
+ var txList = [];
+ txs.forEach(function (tx) {
+ tx.ins.forEach(function (txin) {
+ txList.push(txin.outpoint.hash);
+ });
+ });
+ storage.Transaction.find({hash: {"$in": txList}}, function (err, result) {
+ if (err) return callback(err);
+
+ var txIndex = {};
+ result.forEach(function (tx) {
+ txIndex[tx.hash.toString('base64')] = tx;
+ });
+
+ txs.forEach(function (tx, i) {
+ tx.totalIn = bigint(0);
+ tx.totalOut = bigint(0);
+ tx.ins.forEach(function (txin, j) {
+ var op = txin.outpoint;
+ var srctx = txIndex[op.hash.toString('base64')];
+ if (srctx) {
+ txin.source = srctx.outs[op.index];
+ tx.totalIn = tx.totalIn.add(Util.valueToBigInt(txin.source.value));
+ }
+ });
+ tx.outs.forEach(function (txout) {
+ tx.totalOut = tx.totalOut.add(Util.valueToBigInt(txout.value));
+ });
+ if (!tx.isCoinBase()) tx.fee = tx.totalIn.sub(tx.totalOut);
+ });
+ callback(null);
+ });
+}
+
+// Params
+
+app.param('blockHash', function (req, res, next, hash){
+ hash = new Buffer(hash, 'hex').reverse();
+ chain.getBlockByHash(hash, function (err, block) {
+ if (err) return next(err);
+
+ chain.getBlockByPrev(hash, function (err, nextBlock) {
+ if (err) return next(err);
+
+ storage.Transaction.find({block: block._id}, function (err, txs) {
+ if (err) return next(err);
+
+ getOutpoints(txs, function (err) {
+ if (err) return next(err);
+
+ var totalFee = bigint(0);
+ var totalOut = bigint(0);
+ txs.forEach(function (tx) {
+ tx.outs.forEach(function (txout) {
+ totalOut = totalOut.add(Util.valueToBigInt(txout.value));
+ });
+ if (tx.fee) totalFee = totalFee.add(tx.fee);
+ });
+ block.totalFee = totalFee;
+ block.totalOut = totalOut;
+
+ req.block = block;
+ req.nextBlock = nextBlock;
+ req.block.txs = txs;
+
+ next();
+ });
+ });
+ });
+ });
+});
+
+app.param('txHash', function (req, res, next, hash){
+ hash = new Buffer(hash, 'hex').reverse();
+ storage.Transaction.findOne({hash: hash}, function (err, tx) {
+ if (err) return next(err);
+ req.tx = tx;
+
+ storage.Block.findOne({_id: tx.block}, function (err, block) {
+ if (err) return next(err);
+ req.block = block;
+
+ getOutpoints(tx, function (err) {
+ if (err) return next(err);
+
+ next();
+ });
+ });
+ });
+});
+
+app.param('addrBase58', function (req, res, next, addr){
+ var pubKeyHash = Util.addressToPubKeyHash(addr);
+ req.pubKeyHash = pubKeyHash;
+ storage.Account.findOne({pubKeyHash: pubKeyHash.toString('base64')}, function (err, account) {
+ if (err) return next(err);
+
+ if (!account) return next(new Error("Address "+addr+" not found!"));
+
+ var txList = [];
+ account.txs.forEach(function (txRef) {
+ txList.push(txRef.tx);
+ });
+
+ storage.Transaction.find({hash: {$in: txList}}).exec(function (err, txs) {
+ if (err) return next(err);
+
+ var blockIds = [];
+ txs.forEach(function (tx) {
+ if (blockIds.indexOf(tx.block) == -1) blockIds.push(tx.block);
+ });
+
+ storage.Block.find({_id: {$in: blockIds}}, function (err, blocks) {
+ if (err) return next(err);
+
+ getOutpoints(txs, function (err) {
+ if (err) return next(err);
+
+ var blkObj = {};
+ blocks.forEach(function (block) {
+ blkObj[block._id.toString()] = block;
+ });
+ var txsObj = {};
+ txs.forEach(function (tx) {
+ tx.blockObj = blkObj[tx.block.toString()];
+ txsObj[tx.hash.toString('base64')] = tx;
+ });
+ req.txsObj = txsObj;
+
+ var receivedCount = 0;
+ var receivedAmount = bigint(0);
+ var sentCount = 0;
+ var sentAmount = bigint(0);
+
+ txOutsObj = {};
+ account.txs.forEach(function (txRef, index) {
+ var tx = txsObj[txRef.tx];
+ for (var i = 0; i < tx.outs.length; i++) {
+ var txout = tx.outs[i];
+ var script = txout.getScript();
+
+ var outPubKey = script.simpleOutPubKeyHash();
+
+ if (outPubKey && pubKeyHash.compare(outPubKey) == 0) {
+ receivedCount++;
+ var outIndex =
+ tx.hash.toString('base64')+":"+
+ i;
+ txOutsObj[outIndex] = txout;
+
+ receivedAmount = receivedAmount.add(Util.valueToBigInt(txout.value));
+
+ tx.myOut = txout;
+ }
+ };
+ });
+
+ txs.forEach(function (tx, index) {
+ if (tx.isCoinBase()) return;
+
+ tx.ins.forEach(function (txin, j) {
+ var script = txin.getScript();
+
+ var inPubKey = Util.sha256ripe160(script.simpleInPubKey());
+
+ if (inPubKey && pubKeyHash.compare(inPubKey) == 0) {
+ sentCount++;
+ var outIndex =
+ txin.outpoint.hash.toString('base64')+":"+
+ txin.outpoint.index;
+
+ if (!txOutsObj[outIndex]) {
+ winston.warn('Outgoing transaction is missing matching incoming transaction.');
+ return;
+ }
+ txOutsObj[outIndex].spent = {
+ txin: txin,
+ tx: tx
+ };
+
+ sentAmount = sentAmount.add(Util.valueToBigInt(txin.source.value));
+
+ tx.myIn = txin;
+ }
+ });
+ });
+
+ // Calculate the current available balance
+ var totalAvailable = bigint(0);
+ for (var i in txOutsObj) {
+ if (!txOutsObj[i].spent) {
+ totalAvailable = totalAvailable.add(Util.valueToBigInt(txOutsObj[i].value));
+ }
+ }
+
+ // Bring txs into correct order
+ txs = account.txs.map(function (txRef) {
+ return txsObj[txRef.tx];
+ });
+
+ account.totalAvailable = totalAvailable;
+ account.receivedCount = receivedCount;
+ account.receivedAmount = receivedAmount;
+ account.sentCount = sentCount;
+ account.sentAmount = sentAmount;
+
+ req.account = account;
+ req.txs = txs;
+ req.txOutsObj = txOutsObj;
+ next();
+ });
+ });
+ });
+ });
+});
+
+// Routes
+
+app.get('/', function(req, res){
+ storage.Block.find().sort('height', -1).limit(15).exec(function (err, rows) {
+ if (err) return next(err);
+ res.render('index', {
+ title: 'Home - Bitcoin Explorer',
+ latestBlocks: rows
+ });
+ });
+});
+
+app.get('/block/:blockHash', function (req, res) {
+ res.render('block', {
+ title: 'Block '+req.block.height+' - Bitcoin Explorer',
+ block: req.block,
+ nextBlock: req.nextBlock,
+ totalAmount: req.totalAmount,
+ hexDifficulty: bigint(req.block.bits).toString(16)
+ });
+});
+
+app.get('/tx/:txHash', function (req, res) {
+ var totalOut = bigint(0);
+ req.tx.outs.forEach(function (txout) {
+ totalOut = totalOut.add(Util.valueToBigInt(txout.value));
+ });
+ res.render('transaction', {
+ title: 'Tx '+Util.formatHashAlt(req.tx.hash)+'... - Bitcoin Explorer',
+ tx: req.tx,
+ block: req.block,
+ totalOut: totalOut
+ });
+});
+
+app.get('/address/:addrBase58', function (req, res) {
+ res.render('address', {
+ title: 'Address '+(req.params.addrBase58)+' - Bitcoin Explorer',
+ address: req.params.addrBase58,
+ pubKeyHash: req.pubKeyHash,
+ account: req.account,
+ txs: req.txs,
+ txOutsObj: req.txOutsObj
+ });
+});
+
+// Only listen on $ node app.js
+
+if (!module.parent) {
+ app.listen(3000);
+ winston.info("Express server listening on port " + app.address().port);
+}
50 public/stylesheets/style.css
@@ -0,0 +1,50 @@
+body {
+ padding: 50px;
+ font: 15px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+ color: #00B7FF;
+}
+
+.mono {
+ font-family: 'Anonymous Pro', monospace;
+}
+
+dl.props {
+ border: 3px solid #ccc;
+ padding: 0.5em;
+}
+
+dl.props dt {
+ float: left;
+ clear: left;
+ text-align: right;
+ font-weight: bold;
+ color: #999;
+ width: 9em;
+}
+
+dl.props dd {
+ padding: 0 0 0.5em 0;
+ margin-left: 9.3em;
+}
+
+table {}
+
+table td {
+ border: 0;
+ margin: 0;
+ padding: 0 0.8em 0 0;
+ vertical-align: top;
+}
+
+table thead td {
+ font-weight: bold;
+ white-space: nowrap;
+}
+
+table.txs tbody td {
+ border-top: 1px solid #ccc;
+ padding: 0.5em 0;
+}
21 test/app.test.js
@@ -0,0 +1,21 @@
+
+// Run $ expresso
+
+/**
+ * Module dependencies.
+ */
+
+var app = require('../app')
+ , assert = require('assert');
+
+
+module.exports = {
+ 'GET /': function(){
+ assert.response(app,
+ { url: '/' },
+ { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }},
+ function(res){
+ assert.includes(res.body, '<title>Express</title>');
+ });
+ }
+};
65 views/address.ejs
@@ -0,0 +1,65 @@
+<h1>Address <%=address%></h1>
+<dl class="props">
+ <dt>First Seen</dt>
+ <dd><a href="../../block/<%=Util.formatHashFull(txs[0].blockObj.hash)%>"><%=txs[0].blockObj.height%></a> (<%=new Date(txs[0].blockObj.timestamp*1000).toUTCString()%>)</dd>
+ <dt>Hash160</dt>
+ <dd><span class="mono"><%=account.pubKeyHash.toHex()%></span></dd>
+ <dt>Received</dt>
+ <dd><%=Util.formatValue(account.receivedAmount)%> BTC (<%=account.receivedCount%> transaction<%=account.receivedCount == 1 ? "" : "s"%>)</dd>
+ <dt>Sent</dt>
+ <dd><%=Util.formatValue(account.sentAmount)%> BTC (<%=account.sentCount%> transaction<%=account.sentCount == 1 ? "" : "s"%>)</dd>
+ <dt>Balance available</dt>
+ <dd><%=Util.formatValue(account.totalAvailable)%> BTC</dd>
+</dl>
+
+<h2>Outputs</h2>
+<table>
+ <thead>
+ <tr>
+ <td></td>
+ <td>Transaction</td>
+ <td>Block</td>
+ <td>Amount</td>
+ <td>Type</td>
+ <td>From/To</td>
+ <td>Status</td>
+ </tr>
+ </thead>
+ <tbody>
+<% txs.forEach(function (tx) { %>
+ <tr>
+ <td style="text-align: right"><span class="mono"><%=tx.myOut ? "+" : "-"%></span></td>
+ <td><span class="mono"><a href="../../tx/<%=Util.formatHashFull(new Buffer(tx.hash, 'base64'))%>"><%=Util.formatHashAlt(new Buffer(tx.hash, 'base64'))%>&hellip;</a></span></td>
+ <td style="white-space: nowrap"><a href="../../block/<%=Util.formatHashFull(tx.blockObj.hash)%>"><%=tx.blockObj.height%></a> (<%=new Date(tx.blockObj.timestamp*1000).toUTCString()%>)</td>
+ <td><%=Util.formatValue(tx.myOut ? tx.myOut.value : tx.myIn.source.value)%></td>
+ <% if (tx.myOut) { %>
+ <td><%=tx.myOut.getScript().getOutType()%></td>
+ <td>
+ <% tx.ins.forEach(function (txin) { %>
+ <% if (txin.isCoinBase()) { %>
+ Generation
+ <% } else { %>
+ <a href="../../address/<%=Util.pubKeyHashToAddress(Util.sha256ripe160(txin.getScript().simpleInPubKey()))%>"><%=Util.pubKeyHashToAddress(Util.sha256ripe160(txin.getScript().simpleInPubKey()))%></a>
+ <% } %><br>
+ <% }); %>
+ </td>
+ <td>
+ <% if (tx.myOut && tx.myOut.spent) { %>
+ Spent in <span class="mono"><a href="../../tx/<%=Util.formatHashFull(new Buffer(tx.myOut.spent.tx.hash, 'base64'))%>"><%=Util.formatHashAlt(new Buffer(tx.myOut.spent.tx.hash, 'base64'))%>&hellip;</a></span>
+ <% } else { %>
+ Available
+ <% } %>
+ </td>
+ <% } else { %>
+ <td><%=tx.myIn.getScript().getInType()%></td>
+ <td>
+ <% tx.outs.forEach(function (txout) { %>
+ <a href="../../address/<%=Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash())%>"><%=Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash())%></a><br>
+ <% }); %>
+ </td>
+ <td></td>
+ <% } %>
+ </tr>
+<% }); %>
+ </tbody>
+</table>
73 views/block.ejs
@@ -0,0 +1,73 @@
+<h1>Block <%=block.height%></h1>
+<dl class="props">
+ <dt>Hash</dt>
+ <dd><span class="mono"><%=Util.formatHashFull(block.hash)%></span></dd>
+ <dt>Previous Block</dt>
+ <dd><span class="mono"><a href="<%=Util.formatHashFull(block.prev_hash)%>"><%=Util.formatHashFull(block.prev_hash)%></a></span></dd>
+<% if (nextBlock) { %>
+ <dt>Next Block</dt>
+ <dd><span class="mono"><a href="<%=Util.formatHashFull(nextBlock.hash)%>"><%=Util.formatHashFull(nextBlock.hash)%></a></span></dd>
+<% } %>
+ <dt>Time</dt>
+ <dd><%=new Date(block.timestamp*1000).toUTCString()%></dd>
+ <dt>Difficulty</dt>
+ <dd><%=Util.calcDifficulty(parseInt(block.bits))%> (Bits: <%=hexDifficulty%>)</dd>
+ <dt>Transactions</dt>
+ <dd><%=block.txs.length%></dd>
+ <dt>Total</dt>
+ <dd><%=Util.formatValue(block.totalOut)%> BTC</dd>
+ <dt>Size</dt>
+ <dd><%=block.size%> bytes</dd>
+ <dt>Merkle root</dt>
+ <dd><span class="mono"><%=Util.formatHashFull(block.merkle_root)%></span></dd>
+ <dt>Nonce</dt>
+ <dd><%=block.nonce%></dd>
+
+<h2>Transactions</h2>
+<table class="txs">
+ <thead>
+ <tr>
+ <td>Transaction</td>
+ <td>Fee</td>
+ <td>Size</td>
+ <td>From</td>
+ <td colspan="2">To</td>
+ </tr>
+ </thead>
+ <tbody>
+<% block.txs.forEach(function (tx) { %>
+ <tr>
+ <td><span class="mono"><a href="../../tx/<%=Util.formatHashFull(tx.getHash())%>"><%=Util.formatHashAlt(tx.getHash())%></a></span></td>
+ <td><%=tx.fee ? Util.formatValue(tx.fee) : ""%></td>
+ <td><%=tx.serialize().length%></td>
+ <td style="white-space: nowrap;">
+ <% tx.ins.forEach(function (txin) { %>
+ <% if (txin.isCoinBase()) { %>
+ Generation: 50 + <%=Util.formatValue(block.totalFee)%> total fees
+ <% } else { %>
+ <%=Util.formatValue(txin.source.value)%>
+ <%
+ var addr = Util.pubKeyHashToAddress(Util.sha256ripe160(txin.getScript().simpleInPubKey()));
+ if (addr.length) { %>
+ <a href="../../address/<%=addr%>"><%=addr%></a>
+ <% } %>
+ <% } %>
+ <br>
+ <% }); %>
+ </td>
+ <td style="white-space: nowrap;">
+ <% tx.outs.forEach(function (txout) { %>
+ <%=Util.formatValue(txout.value)%>
+ <%
+ var addr = Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash());
+ if (addr.length) { %>
+ <a href="../../address/<%=addr%>"><%=addr%></a>
+ <% } %>
+ <br>
+ <% }); %>
+ </td>
+ </tr>
+<% }); %>
+ </tbody>
+</table>
+</dl>
23 views/index.ejs
@@ -0,0 +1,23 @@
+<h1>Bitcoin Explorer</h1>
+<p>Bitcoin Explorer is a sample application for the Bitcoin Node.js client library <a href="https://github.com/justmoon/node-bitcoin-p2p">node-bitcoin-p2p</a>. It is modeled after the popular <a href="http://blockexplorer.com/">Block Explorer</a> website by theymos.</p>
+<h2>Latest Blocks</h2>
+<table>
+ <thead>
+ <tr>
+ <td>Number</td>
+ <td>Hash</td>
+ <td>Time</td>
+ <td>Size</td>
+ </tr>
+ </thead>
+ <tbody>
+ <% latestBlocks.forEach(function (block) { %>
+ <tr>
+ <td><a href="block/<%=Util.formatHashFull(block.hash)%>"><%=block.height-1%></a></td>
+ <td><span class="mono"><a href="block/<%=Util.formatHashFull(block.hash)%>"><%=Util.formatHashAlt(block.hash)%>&hellip;</a></span></td>
+ <td><%=new Date(block.timestamp*1000).toUTCString()%></td>
+ <td><%=block.size%></td>
+ </tr>
+ <% }); %>
+ </tbody>
+</table>
11 views/layout.ejs
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= title %></title>
+ <link href="http://fonts.googleapis.com/css?family=Anonymous+Pro:regular,bold" rel="stylesheet" type="text/css">
+ <link rel="stylesheet" href="/stylesheets/style.css" />
+ </head>
+ <body>
+ <%- body %>
+ </body>
+</html>
70 views/transaction.ejs
@@ -0,0 +1,70 @@
+<h1>Transaction</h1>
+<dl class="props">
+ <dt>Hash</dt>
+ <dd><span class="mono"><%=Util.formatHashFull(tx.hash)%></span></dd>
+ <dt>Block</dt>
+ <dd><a href="../../block/<%=Util.formatHashFull(block.hash)%>"><%=block.height-1%></a> (<%=new Date(block.timestamp*1000).toUTCString()%>)</dd>
+ <dt>Inputs #</dt>
+ <dd><%=tx.ins.length%></dd>
+ <dt>Total (in)</dt>
+ <dd><%=Util.formatValue(tx.totalIn.add(block.getBlockValue()))%> BTC</dd></dd>
+ <dt>Outputs #</dt>
+ <dd><%=tx.outs.length%></dd>
+ <dt>Total (out)</dt>
+ <dd><%=Util.formatValue(totalOut)%> BTC</dd>
+ <dt>Size</dt>
+ <dd><%=tx.serialize().length%> bytes</dd>
+</dl>
+<h2>Inputs</h2>
+<table>
+ <thead>
+ <tr>
+ <td>Previous output (index)</td>
+ <td>Amount</td>
+ <td>From address</td>
+ <td>Type</td>
+ <td>Script</td>
+ </tr>
+ </thead>
+ <tbody>
+ <% tx.ins.forEach(function (txin) { %>
+ <tr>
+ <% if (txin.isCoinBase()) { %>
+ <td>N/A</td>
+ <td><%=Util.formatValue(block.getBlockValue())%> + fees</td>
+ <td>N/A</td>
+ <td>Generation</td>
+ <% } else { %>
+ <td><a class="mono" href="../../tx/<%=Util.formatHashFull(txin.outpoint.hash)%>"><%=Util.formatHashAlt(txin.outpoint.hash)%>&hellip;</a> (<%=txin.outpoint.index%>)</td>
+ <td>N/A</td>
+ <td><a href="../../address/<%=Util.pubKeyHashToAddress(Util.sha256ripe160(txin.getScript().simpleInPubKey()))%>"><%=Util.pubKeyHashToAddress(Util.sha256ripe160(txin.getScript().simpleInPubKey()))%></a></td>
+ <td><%=txin.getScript().getInType()%></td>
+ <% } %>
+ <td><span class="mono"><%=txin.getScript().getStringContent()%></span></td>
+ </tr>
+ <% }); %>
+ </tbody>
+</table>
+<h2>Outputs</h2>
+<table>
+ <thead>
+ <tr>
+ <td>Index</td>
+ <td>Amount</td>
+ <td>To address</td>
+ <td>Type</td>
+ <td>Script</td>
+ </tr>
+ </thead>
+ <tbody>
+ <% tx.outs.forEach(function (txout, i) { %>
+ <tr>
+ <td><%=i%></td>
+ <td><%=Util.formatValue(txout.value)%></td>
+ <td><a href="../../address/<%=Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash())%>"><%=Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash())%></a></td>
+ <td><%=txout.getScript().getOutType()%></td>
+ <td><span class="mono"><%=txout.getScript().getStringContent()%></span></td>
+ </tr>
+ <% }); %>
+ </tbody>
+</table>

0 comments on commit eee8868

Please sign in to comment.
Something went wrong with that request. Please try again.