// Source: public/src/js/app.js angular.module('insight',[ 'ngAnimate', 'ngResource', 'ngRoute', 'ngProgress', 'ui.bootstrap', 'ui.route', 'monospaced.qrcode', 'insight.system', 'insight.socket', 'insight.blocks', 'insight.transactions', 'insight.address', 'insight.search', 'insight.status', 'insight.connection', 'insight.currency' ]); angular.module('insight.system', []); angular.module('insight.socket', []); angular.module('insight.blocks', []); angular.module('insight.transactions', []); angular.module('insight.address', []); angular.module('insight.search', []); angular.module('insight.status', []); angular.module('insight.connection', []); angular.module('insight.currency', []); // Source: public/src/js/controllers/address.js angular.module('insight.address').controller('AddressController', function($scope, $rootScope, $routeParams, $location, Global, Address, getSocket) { $scope.global = Global; $scope.findOne = function() { $rootScope.currentAddr = $routeParams.addrStr; Address.get({ addrStr: $routeParams.addrStr }, function(address) { $rootScope.titleDetail = address.addrStr.substring(0, 7) + '...'; $rootScope.flashMessage = null; $scope.address = address; }, function(e) { if (e.status === 400) { $rootScope.flashMessage = 'Invalid Address: ' + $routeParams.addrStr; } else if (e.status === 503) { $rootScope.flashMessage = 'Backend Error. ' + e.data; } else { $rootScope.flashMessage = 'Address Not Found'; } $location.path('/'); }); }; var socket = getSocket($scope); socket.on('connect', function() { socket.emit('subscribe', $routeParams.addrStr); socket.on($routeParams.addrStr, function(tx) { console.log('AddressTx event received ' + tx); $rootScope.$broadcast('tx', tx); }); }); $scope.params = $routeParams; }); // Source: public/src/js/controllers/blocks.js angular.module('insight.blocks').controller('BlocksController', function($scope, $rootScope, $routeParams, $location, Global, Block, Blocks, BlockByHeight) { $scope.global = Global; $scope.loading = false; if ($routeParams.blockHeight) { BlockByHeight.get({ blockHeight: $routeParams.blockHeight }, function(hash) { $location.path('/block/' + hash.blockHash); }, function() { $rootScope.flashMessage = 'Bad Request'; $location.path('/'); }); } //Datepicker var _formatTimestamp = function (date) { var yyyy = date.getUTCFullYear().toString(); var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based var dd = date.getUTCDate().toString(); return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding }; $scope.$watch('dt', function(newValue, oldValue) { if (newValue !== oldValue) { $location.path('/blocks-date/' + _formatTimestamp(newValue)); } }); $scope.openCalendar = function($event) { $event.preventDefault(); $event.stopPropagation(); $scope.opened = true; }; $scope.humanSince = function(time) { var m = moment.unix(time).startOf('day'); var b = moment().startOf('day'); return m.max().from(b); }; $scope.list = function() { $scope.loading = true; if ($routeParams.blockDate) { $rootScope.titleDetail = 'on ' + $routeParams.blockDate; } Blocks.get({ blockDate: $routeParams.blockDate }, function(res) { $scope.loading = false; $scope.blocks = res.blocks; $scope.pagination = res.pagination; }); }; $scope.findOne = function() { $scope.loading = true; Block.get({ blockHash: $routeParams.blockHash }, function(block) { $rootScope.titleDetail = block.height; $rootScope.flashMessage = null; $scope.loading = false; $scope.block = block; }, function(e) { if (e.status === 400) { $rootScope.flashMessage = 'Invalid Transaction ID: ' + $routeParams.txId; } else if (e.status === 503) { $rootScope.flashMessage = 'Backend Error. ' + e.data; } else { $rootScope.flashMessage = 'Block Not Found'; } $location.path('/'); }); }; $scope.params = $routeParams; }); // Source: public/src/js/controllers/connection.js angular.module('insight.connection').controller('ConnectionController', function($scope, $window, Status, getSocket, PeerSync) { // Set initial values $scope.apiOnline = true; $scope.serverOnline = true; $scope.clienteOnline = true; var socket = getSocket($scope); // Check for the node server connection socket.on('connect', function() { $scope.serverOnline = true; socket.on('disconnect', function() { $scope.serverOnline = false; }); }); // Check for the api connection $scope.getConnStatus = function() { PeerSync.get({}, function(peer) { $scope.apiOnline = peer.connected; $scope.host = peer.host; $scope.port = peer.port; }, function() { $scope.apiOnline = false; }); }; socket.emit('subscribe', 'sync'); socket.on('status', function(sync) { $scope.sync = sync; $scope.apiOnline = (sync.status !== 'aborted' && sync.status !== 'error'); }); // Check for the client conneciton $window.addEventListener('offline', function() { $scope.$apply(function() { $scope.clienteOnline = false; }); }, true); $window.addEventListener('online', function() { $scope.$apply(function() { $scope.clienteOnline = true; }); }, true); }); // Source: public/src/js/controllers/currency.js angular.module('insight.currency').controller('CurrencyController', function($scope, $rootScope, Currency) { var _roundFloat = function(x, n) { if(!parseInt(n, 10) || !parseFloat(x)) n = 0; return Math.round(x * Math.pow(10, n)) / Math.pow(10, n); }; $rootScope.currency.getConvertion = function(value) { if (typeof value !== 'undefined' && value !== null) { var response; if (this.symbol === 'USD') { response = _roundFloat((value * this.factor), 2); } else if (this.symbol === 'mBTC') { this.factor = 1000; response = _roundFloat((value * this.factor), 5); } else { this.factor = 1; response = value; } return response + ' ' + this.symbol; } return 'value error'; }; $scope.setCurrency = function(currency) { $rootScope.currency.symbol = currency; if (currency === 'USD') { Currency.get({}, function(res) { $rootScope.currency.factor = $rootScope.currency.bitstamp = res.data.bitstamp; }); } else if (currency === 'mBTC') { $rootScope.currency.factor = 1000; } else { $rootScope.currency.factor = 1; } }; // Get initial value Currency.get({}, function(res) { $rootScope.currency.bitstamp = res.data.bitstamp; }); }); // Source: public/src/js/controllers/footer.js angular.module('insight.system').controller('FooterController', function($scope, Version) { var _getVersion = function() { Version.get({}, function(res) { $scope.version = res.version; }); }; $scope.version = _getVersion(); }); // Source: public/src/js/controllers/header.js angular.module('insight.system').controller('HeaderController', function($scope, $rootScope, getSocket, Global, Block) { $scope.global = Global; $rootScope.currency = { factor: 1, bitstamp: 0, symbol: 'BTC' }; $scope.menu = [{ 'title': 'Blocks', 'link': 'blocks' }, { 'title': 'Status', 'link': 'status' }]; var _getBlock = function(hash) { Block.get({ blockHash: hash }, function(res) { $scope.totalBlocks = res.height; }); }; var socket = getSocket($scope); socket.on('connect', function() { socket.emit('subscribe', 'inv'); socket.on('block', function(block) { var blockHash = block.toString(); _getBlock(blockHash); }); }); $rootScope.isCollapsed = true; }); // Source: public/src/js/controllers/index.js var TRANSACTION_DISPLAYED = 10; var BLOCKS_DISPLAYED = 5; angular.module('insight.system').controller('IndexController', function($scope, Global, getSocket, Blocks) { $scope.global = Global; var _getBlocks = function() { Blocks.get({ limit: BLOCKS_DISPLAYED }, function(res) { $scope.blocks = res.blocks; $scope.blocksLength = res.lenght; }); }; var socket = getSocket($scope); socket.on('connect', function() { socket.emit('subscribe', 'inv'); socket.on('tx', function(tx) { $scope.txs.unshift(tx); if (parseInt($scope.txs.length, 10) >= parseInt(TRANSACTION_DISPLAYED, 10)) { $scope.txs = $scope.txs.splice(0, TRANSACTION_DISPLAYED); } }); socket.on('block', function() { _getBlocks(); }); }); $scope.humanSince = function(time) { var m = moment.unix(time); return m.max().fromNow(); }; $scope.index = function() { _getBlocks(); }; $scope.txs = []; $scope.blocks = []; }); // Source: public/src/js/controllers/search.js angular.module('insight.search').controller('SearchController', function($scope, $routeParams, $location, $timeout, Global, Block, Transaction, Address, BlockByHeight) { $scope.global = Global; $scope.loading = false; var _badQuery = function() { $scope.badQuery = true; $timeout(function() { $scope.badQuery = false; }, 2000); }; var _resetSearch = function() { $scope.q = ''; $scope.loading = false; }; $scope.search = function() { var q = $scope.q; $scope.badQuery = false; $scope.loading = true; Block.get({ blockHash: q }, function() { _resetSearch(); $location.path('block/' + q); }, function() { //block not found, search on TX Transaction.get({ txId: q }, function() { _resetSearch(); $location.path('tx/' + q); }, function() { //tx not found, search on Address Address.get({ addrStr: q }, function() { _resetSearch(); $location.path('address/' + q); }, function() { // block by height not found if (isFinite(q)) { // ensure that q is a finite number. A logical height value. BlockByHeight.get({ blockHeight: q }, function(hash) { _resetSearch(); $location.path('/block/' + hash.blockHash); }, function() { //not found, fail :( _badQuery(); }); } else { $scope.loading = false; _badQuery(); } }); }); }); }; }); // Source: public/src/js/controllers/status.js angular.module('insight.status').controller('StatusController', function($scope, $routeParams, $location, Global, Status, Sync, getSocket) { $scope.global = Global; $scope.getStatus = function(q) { Status.get({ q: 'get' + q }, function(d) { $scope.loaded = 1; angular.extend($scope, d); }, function(e) { $scope.error = 'API ERROR: ' + e.data; }); }; $scope.humanSince = function(time) { var m = moment.unix(time / 1000); return m.max().fromNow(); }; var _onSyncUpdate = function(sync) { $scope.sync = sync; }; $scope.getSync = function() { Sync.get({}, function(sync) { _onSyncUpdate(sync); }, function(e) { var err = 'Could not get sync information' + e.toString(); $scope.sync = { error: err }; }); }; var socket = getSocket($scope); socket.on('connect', function() { socket.emit('subscribe', 'sync'); socket.on('status', function(sync) { _onSyncUpdate(sync); }); }); }); // Source: public/src/js/controllers/transactions.js angular.module('insight.transactions').controller('transactionsController', function($scope, $rootScope, $routeParams, $location, Global, Transaction, TransactionsByBlock, TransactionsByAddress) { $scope.global = Global; $scope.loading = false; $scope.loadedBy = null; var pageNum = 0; var pagesTotal = 1; var COIN = 100000000; var _aggregateItems = function(items) { if (!items) return []; var l = items.length; var ret = []; var tmp = {}; var u = 0; for(var i=0; i < l; i++) { var notAddr = false; // non standard input if (items[i].scriptSig && !items[i].addr) { items[i].addr = 'Unparsed address [' + u++ + ']'; items[i].notAddr = true; notAddr = true; } // non standard output if (items[i].scriptPubKey && !items[i].scriptPubKey.addresses) { items[i].scriptPubKey.addresses = ['Unparsed address [' + u++ + ']']; items[i].notAddr = true; notAddr = true; } // multiple addr at output if (items[i].scriptPubKey && items[i].scriptPubKey.addresses.length > 1) { items[i].addr = items[i].scriptPubKey.addresses.join(','); ret.push(items[i]); continue; } var addr = items[i].addr || (items[i].scriptPubKey && items[i].scriptPubKey.addresses[0]); if (!tmp[addr]) { tmp[addr] = {}; tmp[addr].valueSat = 0; tmp[addr].count = 0; tmp[addr].addr = addr; tmp[addr].items = []; } tmp[addr].isSpent = items[i].spentTxId; tmp[addr].doubleSpentTxID = tmp[addr].doubleSpentTxID || items[i].doubleSpentTxID; tmp[addr].doubleSpentIndex = tmp[addr].doubleSpentIndex || items[i].doubleSpentIndex; tmp[addr].unconfirmedInput += items[i].unconfirmedInput; tmp[addr].dbError = tmp[addr].dbError || items[i].dbError; tmp[addr].valueSat += items[i].value * COIN; tmp[addr].items.push(items[i]); tmp[addr].notAddr = notAddr; tmp[addr].count++; } angular.forEach(tmp, function(v) { v.value = parseInt(v.valueSat) / COIN; ret.push(v); }); return ret; }; var _processTX = function(tx) { tx.vinSimple = _aggregateItems(tx.vin); tx.voutSimple = _aggregateItems(tx.vout); }; var _paginate = function(data) { $scope.loading = false; pagesTotal = data.pagesTotal; pageNum += 1; data.txs.forEach(function(tx) { _processTX(tx); $scope.txs.push(tx); }); }; var _byBlock = function() { TransactionsByBlock.get({ block: $routeParams.blockHash, pageNum: pageNum }, function(data) { _paginate(data); }); }; var _byAddress = function () { TransactionsByAddress.get({ address: $routeParams.addrStr, pageNum: pageNum }, function(data) { _paginate(data); }); }; var _findTx = function(txid) { Transaction.get({ txId: txid }, function(tx) { $rootScope.titleDetail = tx.txid.substring(0,7) + '...'; $rootScope.flashMessage = null; $scope.tx = tx; _processTX(tx); $scope.txs.unshift(tx); }, function(e) { if (e.status === 400) { $rootScope.flashMessage = 'Invalid Transaction ID: ' + $routeParams.txId; } else if (e.status === 503) { $rootScope.flashMessage = 'Backend Error. ' + e.data; } else { $rootScope.flashMessage = 'Transaction Not Found'; } $location.path('/'); }); }; $scope.findThis = function() { _findTx($routeParams.txId); }; //Initial load $scope.load = function(from) { $scope.loadedBy = from; $scope.loadMore(); }; //Load more transactions for pagination $scope.loadMore = function() { if (pageNum < pagesTotal && !$scope.loading) { $scope.loading = true; if ($scope.loadedBy === 'address') { _byAddress(); } else { _byBlock(); } } }; //Init without txs $scope.txs = []; $scope.$on('tx', function(event, txid) { _findTx(txid); }); }); // Source: public/src/js/services/address.js angular.module('insight.address').factory('Address', function($resource) { return $resource('/api/addr/:addrStr', { addrStr: '@addStr' }, { get: { method: 'GET', interceptor: { response: function (res) { return res.data; }, responseError: function (res) { if (res.status === 404) { return res; } } } } }); }); // Source: public/src/js/services/blocks.js angular.module('insight.blocks') .factory('Block', function($resource) { return $resource('/api/block/:blockHash', { blockHash: '@blockHash' }, { get: { method: 'GET', interceptor: { response: function (res) { return res.data; }, responseError: function (res) { if (res.status === 404) { return res; } } } } }); }) .factory('Blocks', function($resource) { return $resource('/api/blocks'); }) .factory('BlockByHeight', function($resource) { return $resource('/api/block-index/:blockHeight'); }); // Source: public/src/js/services/currency.js angular.module('insight.currency').factory('Currency', function($resource) { return $resource('/api/currency'); }); // Source: public/src/js/services/global.js //Global service for global variables angular.module('insight.system') .factory('Global',[ function() { } ]) .factory('Version', function($resource) { return $resource('/api/version'); }); // Source: public/src/js/services/socket.js var ScopedSocket = function(socket, $rootScope) { this.socket = socket; this.$rootScope = $rootScope; this.listeners = []; }; ScopedSocket.prototype.removeAllListeners = function(opts) { if (!opts) opts = {}; for (var i = 0; i < this.listeners.length; i++) { var details = this.listeners[i]; if (opts.skipConnect && details.event === 'connect') { continue; } this.socket.removeListener(details.event, details.fn); } this.listeners = []; }; ScopedSocket.prototype.on = function(event, callback) { var socket = this.socket; var $rootScope = this.$rootScope; var wrapped_callback = function() { var args = arguments; $rootScope.$apply(function() { callback.apply(socket, args); }); }; socket.on(event, wrapped_callback); this.listeners.push({ event: event, fn: wrapped_callback }); }; ScopedSocket.prototype.emit = function(event, data, callback) { var socket = this.socket; var $rootScope = this.$rootScope; socket.emit(event, data, function() { var args = arguments; $rootScope.$apply(function() { if (callback) { callback.apply(socket, args); } }); }); }; angular.module('insight.socket').factory('getSocket', function($rootScope) { var socket = io.connect(null, { 'reconnect': true, 'reconnection delay': 500, }); return function(scope) { var scopedSocket = new ScopedSocket(socket, $rootScope); scope.$on('$destroy', function() { scopedSocket.removeAllListeners(); }); socket.on('connect', function() { scopedSocket.removeAllListeners({ skipConnect: true }); }); return scopedSocket; }; }); // Source: public/src/js/services/status.js angular.module('insight.status') .factory('Status', function($resource) { return $resource('/api/status', { q: '@q' }); }) .factory('Sync', function($resource) { return $resource('/api/sync'); }) .factory('PeerSync', function($resource) { return $resource('/api/peer'); }); // Source: public/src/js/services/transactions.js angular.module('insight.transactions') .factory('Transaction', function($resource) { return $resource('/api/tx/:txId', { txId: '@txId' }, { get: { method: 'GET', interceptor: { response: function (res) { return res.data; }, responseError: function (res) { if (res.status === 404) { return res; } } } } }); }) .factory('TransactionsByBlock', function($resource) { return $resource('/api/txs', { block: '@block' }); }) .factory('TransactionsByAddress', function($resource) { return $resource('/api/txs', { address: '@address' }); }) .factory('Transactions', function($resource) { return $resource('/api/txs'); }); // Source: public/src/js/directives.js var ZeroClipboard = window.ZeroClipboard; angular.module('insight') .directive('whenScrolled', function($window) { return { restric: 'A', link: function(scope, elm, attr) { var pageHeight, clientHeight, scrollPos; $window = angular.element($window); var handler = function() { pageHeight = window.document.documentElement.scrollHeight; clientHeight = window.document.documentElement.clientHeight; scrollPos = window.pageYOffset; if (pageHeight - (scrollPos + clientHeight) === 0) { scope.$apply(attr.whenScrolled); } }; $window.on('scroll', handler); scope.$on('$destroy', function() { return $window.off('scroll', handler); }); } }; }) .directive('clipCopy', function() { ZeroClipboard.config({ moviePath: '/lib/zeroclipboard/ZeroClipboard.swf', trustedDomains: ['*'], allowScriptAccess: 'always', forceHandCursor: true }); return { restric: 'A', scope: { clipCopy: '=clipCopy' }, template: '