Skip to content

Instantly share code, notes, and snippets.

@npedrini
Last active January 8, 2020 20:00
Show Gist options
  • Save npedrini/6030317 to your computer and use it in GitHub Desktop.
Save npedrini/6030317 to your computer and use it in GitHub Desktop.
Bitcoin transaction monitor

This visualization monitors Bitcoin transactions and trades (on the Mt. Gox exchange) in real time. It is based on bitcoinmonitor, except that it uses websockets instead of polling.

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<!-- vendor css -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="//cdnjs.cloudflare.com/ajax/libs/noUiSlider/3.1.1/nouislider.fox.min.css" rel="stylesheet" media="screen">
<!-- vendor js -->
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.2.2/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/noUiSlider/3.1.1/jquery.nouislider.min.js"></script>
<script type="text/javascript">
$(document).ready
(
function()
{
// init slider
$("#slider").noUiSlider
(
{
range:[MIN_MINUTES,MAX_MINUTES],
start:3,
handles: 1,
slide:function(){ onUpdateTimespan($(this).val().toFixed(0)); }
}
)
.css("margin-left",margin.left+"px");
initWebSocket();
initChart();
}
);
function initWebSocket()
{
// init blockchain websocket (activity, blocks)
var blockchain = new WebSocket('ws://ws.blockchain.info/inv');
blockchain.onerror = function (error){ console.log('connection.onerror',error); };
blockchain.onopen = function ()
{
blockchain.send( JSON.stringify( {"op":"unconfirmed_sub"} ) ); // subscribe to uncofirmed activity
blockchain.send( JSON.stringify( {"op":"blocks_sub"} ) ); // subscribe to new blocks
};
blockchain.onmessage = function (message)
{
var response = JSON.parse(message.data);
var date = new Date(0);
date.setUTCSeconds( response.x.time );
if( response.op == "utx")
{
var amount = 0;
for(var i=0;i<response.x.out.length;i++)
amount += response.x.out[i].value;
// amount is in satoshi
// 1 BTC = 100,000,000 Satoshi (https://en.bitcoin.it/wiki/activity)
response.amount = amount / 100000000;
response.type = TYPE_TRANSACTION;
response.index = index++;
}
else if( response.op == "block" )
{
response.type = TYPE_BLOCK;
response.amount = Math.round( response.x.height / 10000 );
}
if( DEBUG )
console.log( response.op, response );
response.date = date;
activity.push( response )
refresh();
};
// init mtgox websocket (trades)
var mtgox = new WebSocket('wss://websocket.mtgox.com/mtgox?currency=USD');
mtgox.onerror = function (error) { console.log('connection.onerror',error); };
mtgox.onmessage = function (message)
{
var response = JSON.parse(message.data);
if( response.trade ) // is it a trade
{
var date = new Date(0);
date.setUTCSeconds( response.trade.date );
response.type = TYPE_TRADE;
response.amount = response.trade.amount_int / 100000000;
response.date = date;
activity.push( response );
if( DEBUG ) console.log( response );
refresh();
}
}
}
function initChart()
{
svg = d3.select("#chart").append("svg")
.attr('class', 'chart')
.attr("width",width)
.attr("height",height)
.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xaxis = svg.append("g")
.attr("class","axis xaxis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom) + ")")
.call(xAxis);
var yaxis = svg.append("g")
.attr("class","axis yaxis").call(yAxis);
/*
xaxis.append("text")
.attr("class","axisLabel")
.attr("text-anchor","middle")
.attr("transform", "translate(" + (width-margin.left-margin.right)/2 + "," + 40 + ")")
.text("Transaction Time");
yaxis.append("text")
.attr("class","axisLabel")
.attr("text-anchor","middle")
.attr("transform", "rotate(-90 0 0),translate(-" + (height - margin.top - margin.bottom)/2 + ",-" + (margin.left*.75) + ")")
.text("Transaction Amount (BTC)");
*/
onUpdateTimespan();
start();
}
function refresh()
{
var dateMax = new Date();
dateMax.setUTCMilliseconds( dateMax.getUTCMilliseconds() - (MAX_MINUTES * MINUTE) );
// remove undisplayable data points
for(var i=activity.length-1;i>0;i--)
if( activity[i].date.getTime() < dateMax.getTime() )
activity.splice(i,1);
var data = activity.slice(0);
// remove undisplayable data points
for(var i=data.length-1;i>0;i--)
if( data[i].date.getTime() < x.domain()[0].getTime() )
data.splice(i,1);
var maxDisplayable = new Date();
maxDisplayable.setUTCMilliseconds( maxDisplayable.getUTCMilliseconds() - (MAX_MINUTES * MINUTE) );
var dots = svg.selectAll(".node")
.data( data, function(d) { return d.index; } );
dots.enter()
.append("g")
.attr("id",function(d){ return "node" + d.index; } )
.attr("class",nodeClass)
.attr("transform", function(d) { return "translate(" + x(d.date) + "," + y(d.amount) + ")"; })
.attr("title",tooltip)
.on("mouseover",mouseover)
.on("mouseout",mouseout)
.append("circle")
.attr("r",6)
.attr("fill","#000");
dots.exit().remove();
svg.select(".yaxis").transition().duration(250).call(yAxis);
$('g.node').tooltip( {container: "body", html:true, trigger: "manual"} );
}
function onUpdateTimespan(value)
{
value = value || minutesDisplayed;
minutesDisplayed = value;
$("#slider-value").text("about " + minutesDisplayed + " minutes");
updateXAxis();
refresh();
}
function updateXAxis()
{
var today = new Date();
x.domain( [ new Date(today.getTime() - MINUTE * minutesDisplayed), today ] );
svg.select(".xaxis").transition().duration().call(xAxis);
svg.selectAll("g.node").transition().duration(0).attr("transform", function(d) { return "translate(" + x(d.date) + "," + y(d.amount) + ")"; });
}
function tooltip(d)
{
if( d.type == TYPE_TRANSACTION )
{
var inputs = [];
for(var i=0;i<d.x.out.length;i++)
inputs.push( d.x.out[i].value/100000000 );
return "Transfer of " + d.amount + " BTC " + (inputs.length ? "(" + inputs.join(" BTC + ") + " BTC)" : "");
}
else if( d.type == "trade" )
{
return "Trade: " + d.amount + " BTC @ " + d.trade.price + " " + d.trade.price_currency;
}
else if( d.type == "block" )
{
return "New block found (" + d.amount + " generated)";
}
}
function nodeClass(d)
{
return "node " + d.type;
}
function mouseover(d)
{
d3.select("#node" + d.index + " text").attr("display","block");
$("#node" + d.index).tooltip("show");
stop();
}
function mouseout(d)
{
d3.select("#node" + d.index+ " text").attr("display","none");
$("#node" + d.index).tooltip("hide");
start();
}
function start()
{
updateInterval = setInterval( updateXAxis, 100 );
}
function stop()
{
clearInterval( updateInterval );
}
// constants
var TYPE_TRANSACTION = "transaction";
var TYPE_TRADE = "trade";
var TYPE_BLOCK = "block";
var MINUTE = 1000*60;
var HOUR = MINUTE*60;
var MIN_MINUTES = 1;
var MAX_MINUTES = 10;
var DEBUG = false;
var minutesDisplayed = 3;
var numberFormat = d3.format(",f");
var currencyFormatter = d3.format(",.2f");
var timeFormat = d3.time.format.utc("%H:%M:%S UTC");
var svg,updateInterval;
var margin = {top: 20, right: 20, bottom: 50, left: 100};
var width = 700,height=400,chartHeight=height-margin.top-margin.bottom,chartWidth=width-margin.left-margin.right;
var yMax = 200;
var h = height-margin.bottom-margin.top;
var x = d3.time.scale().range([0, width - margin.left - margin.right]);
var y = d3.scale.pow().exponent(.5).domain([0,100,1000]).range([h,h/2,0]);
var xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(6).tickPadding(10).tickSize(-chartHeight,0,-chartHeight).tickFormat(timeFormat);
var yAxis = d3.svg.axis().scale(y).orient("left").tickFormat(function(d) { return currencyFormatter(d) + " BTC"; }).tickPadding(10).tickSize(-chartWidth,0,-chartWidth);/*.tickFormat(logFormat);*/
var activity = [];
var index = 0;
</script>
<style>
body{ margin: 20px; }
#chart { margin-top: 50px; }
#footer { margin-top: 50px; font-size: .8em; }
.chart { font-family: arial,sans-serif; font-size: .8em; }
.axisLabel { font-size: 1.2em; font-weight: bold; fill: #545454; }
path.domain { fill: none; stroke: #666; stroke-width: 1; }
line { stroke: #ccc; stroke-width: .5; stroke-linecap: "square" }
.transaction circle, .block circle, .trade circle { fill: #fff; fill-opacity: 1; stroke-width:2px; }
.transaction circle { stroke: #afd8fb; }
.block circle { stroke: #edc300; }
.trade circle { stroke: #cb4c48; }
/* boostrap overrides */
.popover { font-size: .7em; max-width: 500px; }
.popover .header { font-size: 1.1em; margin-bottom: 10px; line-height: 1.3em; }
#slider { display: inline-block; }
#slider-value { margin-left: 50px; font-size: .9em; }
</style>
</head>
<body>
<div id="chart"></div>
<div>
<div id="slider" class="noUiSlider"></div>
<span id="slider-value"></span>
</div>
<div id="footer">
Data from <a href="http://blockchain.info/api/api_websocket" target="_blank">blockchain</a>, inspired by <a href="http://bitcoinmonitor.com" target="_blank">bitcoinmonitor</a>
</div>
</body>
</html>
@asd2791
Copy link

asd2791 commented Jan 16, 2018

Is there any license for this code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment