├── .travis.yml ├── .npmignore ├── pools.json ├── .gitignore ├── lib ├── common.js ├── utils.js ├── messages.js ├── currency.js ├── service.js ├── status.js ├── ratelimiter.js ├── charts.js ├── addresses.js ├── transactions.js ├── blocks.js └── index.js ├── test ├── utils.js ├── messages.js ├── currency.js ├── status.js ├── ratelimiter.js ├── index.js ├── blocks.js ├── addresses.js └── transactions.js ├── .jshintrc ├── package.json └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'v0.12.7' 4 | - 'v4' 5 | install: 6 | - npm install 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | tags 11 | pids 12 | logs 13 | results 14 | build 15 | 16 | node_modules 17 | 18 | # extras 19 | *.swp 20 | *.swo 21 | *~ 22 | .project 23 | peerdb.json 24 | 25 | npm-debug.log 26 | .nodemonignore 27 | 28 | .DS_Store 29 | db/txs/* 30 | db/txs 31 | db/testnet/txs/* 32 | db/testnet/txs 33 | db/blocks/* 34 | db/blocks 35 | db/testnet/blocks/* 36 | db/testnet/blocks 37 | 38 | README.html 39 | k* 40 | public 41 | -------------------------------------------------------------------------------- /pools.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "poolName": "Bitfly", 4 | "url": "https://zcash.flypool.org/", 5 | "searchStrings": [ 6 | "/flypool/" 7 | ] 8 | }, 9 | { 10 | "poolName": "NaN", 11 | "url": "", 12 | "searchStrings": [ 13 | "/NaN" 14 | ] 15 | }, 16 | { 17 | "poolName": "Stratum", 18 | "url": "", 19 | "searchStrings": [ 20 | "/nodeStratum/" 21 | ] 22 | }, 23 | { 24 | "poolName": "Zmine", 25 | "url": "https://zmine.io/", 26 | "searchStrings": [ 27 | "{ZMINE.IO}" 28 | ] 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # from https://github.com/github/gitignore/blob/master/Node.gitignore 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | *.swp 11 | tags 12 | pids 13 | logs 14 | results 15 | build 16 | 17 | node_modules 18 | 19 | # extras 20 | *.swp 21 | *.swo 22 | *~ 23 | .project 24 | peerdb.json 25 | 26 | npm-debug.log 27 | .nodemonignore 28 | 29 | .DS_Store 30 | db/txs/* 31 | db/txs 32 | db/testnet/txs/* 33 | db/testnet/txs 34 | db/blocks/* 35 | db/blocks 36 | db/testnet/blocks/* 37 | db/testnet/blocks 38 | 39 | README.html 40 | public 41 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Common(options) { 4 | this.log = options.log; 5 | } 6 | 7 | Common.prototype.notReady = function (err, res, p) { 8 | res.status(503).send('Server not yet ready. Sync Percentage:' + p); 9 | }; 10 | 11 | Common.prototype.handleErrors = function (err, res) { 12 | if (err) { 13 | if (err.code) { 14 | res.status(400).send(err.message + '. Code:' + err.code); 15 | } else { 16 | this.log.error(err.stack); 17 | res.status(503).send(err.message); 18 | } 19 | } else { 20 | res.status(404).send('Not found'); 21 | } 22 | }; 23 | 24 | module.exports = Common; 25 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var Common = require('./common'); 6 | 7 | function UtilsController(node) { 8 | this.node = node; 9 | this.common = new Common({log: this.node.log}); 10 | } 11 | 12 | UtilsController.prototype.estimateFee = function(req, res) { 13 | var self = this; 14 | var args = req.query.nbBlocks || '2'; 15 | var nbBlocks = args.split(','); 16 | 17 | async.map(nbBlocks, function(n, next) { 18 | var num = parseInt(n); 19 | // Insight and Bitcoin JSON-RPC return bitcoin for this value (instead of satoshis). 20 | self.node.services.bitcoind.estimateFee(num, function(err, fee) { 21 | if (err) { 22 | return next(err); 23 | } 24 | next(null, [num, fee]); 25 | }); 26 | }, function(err, result) { 27 | if (err) { 28 | return self.common.handleErrors(err, res); 29 | } 30 | res.jsonp(_.zipObject(result)); 31 | }); 32 | 33 | }; 34 | 35 | module.exports = UtilsController; 36 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var should = require('should'); 5 | var UtilsController = require('../lib/utils'); 6 | 7 | describe('Utils', function() { 8 | describe('/utils/estimatefee', function() { 9 | it('should give the correct fee', function(done) { 10 | var node = { 11 | services: { 12 | bitcoind: { 13 | estimateFee: function(blocks, callback) { 14 | switch(blocks) { 15 | case 1: 16 | return callback(null, 1000 / 1e8); 17 | case 3: 18 | return callback(null, 3000 / 1e8); 19 | } 20 | } 21 | } 22 | } 23 | }; 24 | var utils = new UtilsController(node); 25 | 26 | var req = { 27 | query: { 28 | nbBlocks: '1, 3' 29 | } 30 | }; 31 | 32 | var res = { 33 | jsonp: function(fees) { 34 | should(fees).eql({1: 0.00001, 3: 0.00003}); 35 | done(); 36 | } 37 | }; 38 | utils.estimateFee(req, res); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib-zcash'); 4 | var _ = bitcore.deps._; 5 | var Message = require('bitcore-message-zcash'); 6 | var Common = require('./common'); 7 | 8 | function MessagesController(node) { 9 | this.node = node; 10 | this.common = new Common({log: this.node.log}); 11 | } 12 | 13 | MessagesController.prototype.verify = function(req, res) { 14 | var self = this; 15 | var address = req.body.address || req.query.address; 16 | var signature = req.body.signature || req.query.signature; 17 | var message = req.body.message || req.query.message; 18 | if(_.isUndefined(address) || _.isUndefined(signature) || _.isUndefined(message)) { 19 | return self.common.handleErrors({ 20 | message: 'Missing parameters (expected "address", "signature" and "message")', 21 | code: 1 22 | }, res); 23 | } 24 | var valid; 25 | try { 26 | valid = new Message(message).verify(address, signature); 27 | } catch(err) { 28 | return self.common.handleErrors({ 29 | message: 'Unexpected error: ' + err.message, 30 | code: 1 31 | }, res); 32 | } 33 | res.json({'result': valid}); 34 | }; 35 | 36 | module.exports = MessagesController; 37 | -------------------------------------------------------------------------------- /lib/currency.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | 5 | function CurrencyController(options) { 6 | this.node = options.node; 7 | var refresh = options.currencyRefresh || CurrencyController.DEFAULT_CURRENCY_DELAY; 8 | this.currencyDelay = refresh * 60000; 9 | this.bitstampRate = 0; // USD/BTC 10 | this.poloniexRate = 0; // BTC/ZEC 11 | this.timestamp = Date.now(); 12 | } 13 | 14 | CurrencyController.DEFAULT_CURRENCY_DELAY = 10; 15 | 16 | CurrencyController.prototype.index = function(req, res) { 17 | var self = this; 18 | var currentTime = Date.now(); 19 | if (self.bitstampRate === 0 || currentTime >= (self.timestamp + self.currencyDelay)) { 20 | self.timestamp = currentTime; 21 | request('https://www.bitstamp.net/api/ticker/', function(err, response, body) { 22 | if (err) { 23 | self.node.log.error(err); 24 | } 25 | if (!err && response.statusCode === 200) { 26 | self.bitstampRate = parseFloat(JSON.parse(body).last); 27 | } 28 | request('https://poloniex.com/public?command=returnTicker', function(err, response, body) { 29 | if (err) { 30 | self.node.log.error(err); 31 | } 32 | if (!err && response.statusCode === 200) { 33 | self.poloniexRate = parseFloat(JSON.parse(body).BTC_ZEC.last); 34 | } 35 | res.jsonp({ 36 | status: 200, 37 | data: { 38 | bitstamp: self.bitstampRate * self.poloniexRate 39 | } 40 | }); 41 | }); 42 | }); 43 | } else { 44 | res.jsonp({ 45 | status: 200, 46 | data: { 47 | bitstamp: self.bitstampRate * self.poloniexRate 48 | } 49 | }); 50 | } 51 | 52 | }; 53 | 54 | module.exports = CurrencyController; 55 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "esnext": true, // Allow ES.next specific features such as `const` and `let`. 5 | "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). 6 | "camelcase": false, // Permit only camelcase for `var` and `object indexes`. 7 | "curly": false, // Require {} for every new block or scope. 8 | "eqeqeq": true, // Require triple equals i.e. `===`. 9 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 10 | "latedef": true, // Prohibit variable use before definition. 11 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 12 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 13 | "quotmark": "single", // Define quotes to string values. 14 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 15 | "undef": true, // Require all non-global variables be declared before they are used. 16 | "unused": true, // Warn unused variables. 17 | "strict": true, // Require `use strict` pragma in every file. 18 | "trailing": true, // Prohibit trailing whitespaces. 19 | "smarttabs": false, // Suppresses warnings about mixed tabs and spaces 20 | "globals": { // Globals variables. 21 | "angular": true 22 | }, 23 | "predef": [ // Extra globals. 24 | "define", 25 | "require", 26 | "exports", 27 | "module", 28 | "describe", 29 | "before", 30 | "beforeEach", 31 | "after", 32 | "afterEach", 33 | "it", 34 | "inject", 35 | "$", 36 | "io", 37 | "app", 38 | "moment" 39 | ], 40 | "indent": false, // Specify indentation spacing 41 | "devel": true, // Allow development statements e.g. `console.log();`. 42 | "noempty": true // Prohibit use of empty blocks. 43 | } 44 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | var Service = function(options) { 7 | EventEmitter.call(this); 8 | 9 | this.node = options.node; 10 | this.name = options.name; 11 | }; 12 | 13 | util.inherits(Service, EventEmitter); 14 | 15 | /** 16 | * Describes the dependencies that should be loaded before this service. 17 | */ 18 | Service.dependencies = []; 19 | 20 | /** 21 | * blockHandler 22 | * @param {Block} block - the block being added or removed from the chain 23 | * @param {Boolean} add - whether the block is being added or removed 24 | * @param {Function} callback - call with the leveldb database operations to perform 25 | */ 26 | Service.prototype.blockHandler = function(block, add, callback) { 27 | // implement in the child class 28 | setImmediate(function() { 29 | callback(null, []); 30 | }); 31 | }; 32 | 33 | /** 34 | * the bus events available for subscription 35 | * @return {Array} an array of event info 36 | */ 37 | Service.prototype.getPublishEvents = function() { 38 | // Example: 39 | // return [ 40 | // ['eventname', this, this.subscribeEvent, this.unsubscribeEvent], 41 | // ]; 42 | return []; 43 | }; 44 | 45 | /** 46 | * the API methods to expose 47 | * @return {Array} return array of methods 48 | */ 49 | Service.prototype.getAPIMethods = function() { 50 | // Example: 51 | // return [ 52 | // ['getData', this, this.getData, 1] 53 | // ]; 54 | 55 | return []; 56 | }; 57 | 58 | // Example: 59 | // Service.prototype.getData = function(arg1, callback) { 60 | // 61 | // }; 62 | 63 | /** 64 | * Function which is called when module is first initialized 65 | */ 66 | Service.prototype.start = function(done) { 67 | setImmediate(done); 68 | }; 69 | 70 | /** 71 | * Function to be called when bitcore-node is stopped 72 | */ 73 | Service.prototype.stop = function(done) { 74 | setImmediate(done); 75 | }; 76 | 77 | /** 78 | * Setup express routes 79 | * @param {Express} app 80 | */ 81 | Service.prototype.setupRoutes = function(app) { 82 | // Setup express routes here 83 | }; 84 | 85 | Service.prototype.getRoutePrefix = function() { 86 | return this.name; 87 | }; 88 | 89 | 90 | 91 | module.exports = Service; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insight-api-zcash", 3 | "description": "A Zcash blockchain REST and web socket API service for Bitcore Node.", 4 | "version": "0.4.3", 5 | "repository": "git://github.com/zcash-hackworks/insight-api-zcash.git", 6 | "contributors": [ 7 | { 8 | "name": "Gustavo Cortez", 9 | "email": "cmgustavo83@gmail.com" 10 | }, 11 | { 12 | "name": "Ivan Socolsky", 13 | "email": "jungans@gmail.com" 14 | }, 15 | { 16 | "name": "Juan Ignacio Sosa Lopez", 17 | "email": "bechilandia@gmail.com" 18 | }, 19 | { 20 | "name": "Manuel Araoz", 21 | "email": "manuelaraoz@gmail.com" 22 | }, 23 | { 24 | "name": "Matias Alejo Garcia", 25 | "email": "ematiu@gmail.com" 26 | }, 27 | { 28 | "name": "Mario Colque", 29 | "email": "colquemario@gmail.com" 30 | }, 31 | { 32 | "name": "Patrick Nagurny", 33 | "email": "patrick@bitpay.com" 34 | }, 35 | { 36 | "name": "Braydon Fuller", 37 | "email": "braydon@bitpay.com" 38 | }, 39 | { 40 | "name": "Jack Grigg", 41 | "email": "jack@z.cash" 42 | }, 43 | { 44 | "name": "Simon Liu", 45 | "email": "simon@z.cash" 46 | } 47 | ], 48 | "bugs": { 49 | "url": "https://github.com/zcash-hackworks/insight-api-zcash/issues" 50 | }, 51 | "homepage": "https://github.com/zcash-hackworks/insight-api-zcash", 52 | "license": "MIT", 53 | "keywords": [ 54 | "insight", 55 | "insight api", 56 | "blockchain", 57 | "zcash api", 58 | "blockchain api", 59 | "json", 60 | "bitcore" 61 | ], 62 | "engines": { 63 | "node": ">=0.12.0" 64 | }, 65 | "scripts": { 66 | "test": "NODE_ENV=test mocha -R spec --recursive" 67 | }, 68 | "main": "lib", 69 | "bitcoreNode": "lib", 70 | "dependencies": { 71 | "async": "*", 72 | "bitcore-lib-zcash": "zcash-hackworks/bitcore-lib-zcash", 73 | "bitcore-message-zcash": "zcash-hackworks/bitcore-message-zcash", 74 | "body-parser": "^1.13.3", 75 | "compression": "^1.6.1", 76 | "lodash": "^2.4.1", 77 | "lru-cache": "^4.0.1", 78 | "morgan": "^1.7.0", 79 | "request": "^2.64.0" 80 | }, 81 | "devDependencies": { 82 | "chai": "^3.5.0", 83 | "mocha": "^2.4.5", 84 | "proxyquire": "^1.7.2", 85 | "should": "^8.3.1", 86 | "sinon": "^1.10.3" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var sinon = require('sinon'); 5 | var MessagesController = require('../lib/messages'); 6 | var bitcore = require('bitcore-lib-zcash'); 7 | var _ = require('lodash'); 8 | 9 | describe('Messages', function() { 10 | 11 | var privateKey = bitcore.PrivateKey.fromWIF('cQwApHAg8hw9AZuxiU4a7g9kFWdaemhPxVZXWiAKgJTx6dPP32fN'); 12 | var address = 'mswTKCE2tYSFvUNnNPBKZfeNmugYL1rZMx'; 13 | var badAddress = 'mswTKCE2tYSFvUNnNPBKZfeNmuhYL1rZMm'; 14 | var signature = 'IA4sIwhcLMPPsYtB8tN0PI+aQuwDyl+/4Ksa89llNSAeVaRdMyyIxpo1H5N3GHbPl9LQqZ7CvaokeQgsOkK9fn4='; 15 | var message = 'cellar door'; 16 | 17 | it('will verify a message (true)', function(done) { 18 | 19 | var controller = new MessagesController({node: {}}); 20 | 21 | var req = { 22 | body: { 23 | 'address': address, 24 | 'signature': signature, 25 | 'message': message 26 | }, 27 | query: {} 28 | }; 29 | var res = { 30 | json: function(data) { 31 | data.result.should.equal(true); 32 | done(); 33 | } 34 | }; 35 | 36 | controller.verify(req, res); 37 | }); 38 | 39 | it('will verify a message (false)', function(done) { 40 | 41 | var controller = new MessagesController({node: {}}); 42 | 43 | var req = { 44 | body: { 45 | 'address': address, 46 | 'signature': signature, 47 | 'message': 'wrong message' 48 | }, 49 | query: {} 50 | }; 51 | var res = { 52 | json: function(data) { 53 | data.result.should.equal(false); 54 | done(); 55 | } 56 | }; 57 | 58 | controller.verify(req, res); 59 | }); 60 | 61 | it('handle an error from message verification', function(done) { 62 | var controller = new MessagesController({node: {}}); 63 | var req = { 64 | body: { 65 | 'address': badAddress, 66 | 'signature': signature, 67 | 'message': message 68 | }, 69 | query: {} 70 | }; 71 | var send = sinon.stub(); 72 | var status = sinon.stub().returns({send: send}); 73 | var res = { 74 | status: status, 75 | }; 76 | controller.verify(req, res); 77 | status.args[0][0].should.equal(400); 78 | send.args[0][0].should.equal('Unexpected error: Checksum mismatch. Code:1'); 79 | done(); 80 | }); 81 | 82 | it('handle error with missing parameters', function(done) { 83 | var controller = new MessagesController({node: {}}); 84 | var req = { 85 | body: {}, 86 | query: {} 87 | }; 88 | var send = sinon.stub(); 89 | var status = sinon.stub().returns({send: send}); 90 | var res = { 91 | status: status 92 | }; 93 | controller.verify(req, res); 94 | status.args[0][0].should.equal(400); 95 | send.args[0][0].should.match(/^Missing parameters/); 96 | done(); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /test/currency.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire'); 6 | var CurrencyController = require('../lib/currency'); 7 | 8 | describe('Currency', function() { 9 | 10 | var bitstampData = { 11 | high: 239.44, 12 | last: 237.90, 13 | timestamp: 1443798711, 14 | bid: 237.61, 15 | vwap: 237.88, 16 | volume: 21463.27736401, 17 | low: 235.00, 18 | ask: 237.90 19 | }; 20 | 21 | it.skip('will make live request to bitstamp', function(done) { 22 | var currency = new CurrencyController({}); 23 | var req = {}; 24 | var res = { 25 | jsonp: function(response) { 26 | response.status.should.equal(200); 27 | should.exist(response.data.bitstamp); 28 | (typeof response.data.bitstamp).should.equal('number'); 29 | done(); 30 | } 31 | }; 32 | currency.index(req, res); 33 | }); 34 | 35 | it('will retrieve a fresh value', function(done) { 36 | var TestCurrencyController = proxyquire('../lib/currency', { 37 | request: sinon.stub().callsArgWith(1, null, {statusCode: 200}, JSON.stringify(bitstampData)) 38 | }); 39 | var node = { 40 | log: { 41 | error: sinon.stub() 42 | } 43 | }; 44 | var currency = new TestCurrencyController({node: node}); 45 | currency.bitstampRate = 220.20; 46 | currency.timestamp = Date.now() - 61000 * CurrencyController.DEFAULT_CURRENCY_DELAY; 47 | var req = {}; 48 | var res = { 49 | jsonp: function(response) { 50 | response.status.should.equal(200); 51 | should.exist(response.data.bitstamp); 52 | response.data.bitstamp.should.equal(237.90); 53 | done(); 54 | } 55 | }; 56 | currency.index(req, res); 57 | }); 58 | 59 | it('will log an error from request', function(done) { 60 | var TestCurrencyController = proxyquire('../lib/currency', { 61 | request: sinon.stub().callsArgWith(1, new Error('test')) 62 | }); 63 | var node = { 64 | log: { 65 | error: sinon.stub() 66 | } 67 | }; 68 | var currency = new TestCurrencyController({node: node}); 69 | currency.bitstampRate = 237.90; 70 | currency.timestamp = Date.now() - 65000 * CurrencyController.DEFAULT_CURRENCY_DELAY; 71 | var req = {}; 72 | var res = { 73 | jsonp: function(response) { 74 | response.status.should.equal(200); 75 | should.exist(response.data.bitstamp); 76 | response.data.bitstamp.should.equal(237.90); 77 | node.log.error.callCount.should.equal(1); 78 | done(); 79 | } 80 | }; 81 | currency.index(req, res); 82 | }); 83 | 84 | it('will retrieve a cached value', function(done) { 85 | var request = sinon.stub(); 86 | var TestCurrencyController = proxyquire('../lib/currency', { 87 | request: request 88 | }); 89 | var node = { 90 | log: { 91 | error: sinon.stub() 92 | } 93 | }; 94 | var currency = new TestCurrencyController({node: node}); 95 | currency.bitstampRate = 237.90; 96 | currency.timestamp = Date.now(); 97 | var req = {}; 98 | var res = { 99 | jsonp: function(response) { 100 | response.status.should.equal(200); 101 | should.exist(response.data.bitstamp); 102 | response.data.bitstamp.should.equal(237.90); 103 | request.callCount.should.equal(0); 104 | done(); 105 | } 106 | }; 107 | currency.index(req, res); 108 | }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /lib/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Common = require('./common'); 4 | 5 | function StatusController(node) { 6 | this.node = node; 7 | this.common = new Common({log: this.node.log}); 8 | } 9 | 10 | StatusController.prototype.show = function(req, res) { 11 | var self = this; 12 | var option = req.query.q; 13 | 14 | switch(option) { 15 | case 'getDifficulty': 16 | this.getDifficulty(function(err, result) { 17 | if (err) { 18 | return self.common.handleErrors(err, res); 19 | } 20 | res.jsonp(result); 21 | }); 22 | break; 23 | case 'getLastBlockHash': 24 | res.jsonp(this.getLastBlockHash()); 25 | break; 26 | case 'getBestBlockHash': 27 | this.getBestBlockHash(function(err, result) { 28 | if (err) { 29 | return self.common.handleErrors(err, res); 30 | } 31 | res.jsonp(result); 32 | }); 33 | break; 34 | case 'getInfo': 35 | default: 36 | this.getInfo(function(err, result) { 37 | if (err) { 38 | return self.common.handleErrors(err, res); 39 | } 40 | res.jsonp({ 41 | info: result 42 | }); 43 | }); 44 | } 45 | }; 46 | 47 | StatusController.prototype.getInfo = function(callback) { 48 | this.node.services.bitcoind.getInfo(function(err, result) { 49 | if (err) { 50 | return callback(err); 51 | } 52 | var info = { 53 | version: result.version, 54 | protocolversion: result.protocolVersion, 55 | blocks: result.blocks, 56 | timeoffset: result.timeOffset, 57 | connections: result.connections, 58 | proxy: result.proxy, 59 | difficulty: result.difficulty, 60 | testnet: result.testnet, 61 | relayfee: result.relayFee, 62 | errors: result.errors, 63 | network: result.network 64 | }; 65 | callback(null, info); 66 | }); 67 | }; 68 | 69 | StatusController.prototype.getLastBlockHash = function() { 70 | var hash = this.node.services.bitcoind.tiphash; 71 | return { 72 | syncTipHash: hash, 73 | lastblockhash: hash 74 | }; 75 | }; 76 | 77 | StatusController.prototype.getBestBlockHash = function(callback) { 78 | this.node.services.bitcoind.getBestBlockHash(function(err, hash) { 79 | if (err) { 80 | return callback(err); 81 | } 82 | callback(null, { 83 | bestblockhash: hash 84 | }); 85 | }); 86 | }; 87 | 88 | StatusController.prototype.getDifficulty = function(callback) { 89 | this.node.services.bitcoind.getInfo(function(err, info) { 90 | if (err) { 91 | return callback(err); 92 | } 93 | callback(null, { 94 | difficulty: info.difficulty 95 | }); 96 | }); 97 | }; 98 | 99 | StatusController.prototype.sync = function(req, res) { 100 | var self = this; 101 | var status = 'syncing'; 102 | 103 | this.node.services.bitcoind.isSynced(function(err, synced) { 104 | if (err) { 105 | return self.common.handleErrors(err, res); 106 | } 107 | if (synced) { 108 | status = 'finished'; 109 | } 110 | 111 | self.node.services.bitcoind.syncPercentage(function(err, percentage) { 112 | if (err) { 113 | return self.common.handleErrors(err, res); 114 | } 115 | var info = { 116 | status: status, 117 | blockChainHeight: self.node.services.bitcoind.height, 118 | syncPercentage: Math.round(percentage), 119 | height: self.node.services.bitcoind.height, 120 | error: null, 121 | type: 'bitcore node' 122 | }; 123 | 124 | res.jsonp(info); 125 | 126 | }); 127 | 128 | }); 129 | 130 | }; 131 | 132 | // Hard coded to make insight ui happy, but not applicable 133 | StatusController.prototype.peer = function(req, res) { 134 | res.jsonp({ 135 | connected: true, 136 | host: '127.0.0.1', 137 | port: null 138 | }); 139 | }; 140 | 141 | StatusController.prototype.version = function(req, res) { 142 | var pjson = require('../package.json'); 143 | res.jsonp({ 144 | version: pjson.version 145 | }); 146 | }; 147 | 148 | module.exports = StatusController; 149 | -------------------------------------------------------------------------------- /lib/ratelimiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var THREE_HOURS = 3 * 60 * 60 * 1000; 4 | 5 | /** 6 | * A rate limiter to be used as an express middleware. 7 | * 8 | * @param {Object} options 9 | * @param {Object} options.node - The bitcore node object 10 | * @param {Number} options.limit - Number of requests for normal rate limiter 11 | * @param {Number} options.interval - Interval of the normal rate limiter 12 | * @param {Array} options.whitelist - IP addresses that should have whitelist rate limiting 13 | * @param {Array} options.blacklist - IP addresses that should be blacklist rate limiting 14 | * @param {Number} options.whitelistLimit - Number of requests for whitelisted clients 15 | * @param {Number} options.whitelistInterval - Interval for whitelisted clients 16 | * @param {Number} options.blacklistLimit - Number of requests for blacklisted clients 17 | * @param {Number} options.blacklistInterval - Interval for blacklisted clients 18 | */ 19 | function RateLimiter(options) { 20 | if (!(this instanceof RateLimiter)) { 21 | return new RateLimiter(options); 22 | } 23 | 24 | if (!options){ 25 | options = {}; 26 | } 27 | 28 | this.node = options.node; 29 | this.clients = {}; 30 | this.whitelist = options.whitelist || []; 31 | this.blacklist = options.blacklist || []; 32 | 33 | this.config = { 34 | whitelist: { 35 | totalRequests: options.whitelistLimit || 3 * 60 * 60 * 10, // 108,000 36 | interval: options.whitelistInterval || THREE_HOURS 37 | }, 38 | blacklist: { 39 | totalRequests: options.blacklistLimit || 0, 40 | interval: options.blacklistInterval || THREE_HOURS 41 | }, 42 | normal: { 43 | totalRequests: options.limit || 3 * 60 * 60, // 10,800 44 | interval: options.interval || THREE_HOURS 45 | } 46 | }; 47 | 48 | } 49 | 50 | RateLimiter.prototype.middleware = function() { 51 | var self = this; 52 | return function(req, res, next) { 53 | self._middleware(req, res, next); 54 | }; 55 | }; 56 | 57 | RateLimiter.prototype._middleware = function(req, res, next) { 58 | 59 | var name = this.getClientName(req); 60 | var client = this.clients[name]; 61 | 62 | res.ratelimit = { 63 | clients: this.clients, 64 | exceeded: false 65 | }; 66 | 67 | if (!client) { 68 | client = this.addClient(name); 69 | } 70 | 71 | res.setHeader('X-RateLimit-Limit', this.config[client.type].totalRequests); 72 | res.setHeader('X-RateLimit-Remaining', this.config[client.type].totalRequests - client.visits); 73 | 74 | res.ratelimit.exceeded = this.exceeded(client); 75 | res.ratelimit.client = client; 76 | 77 | if (!this.exceeded(client)) { 78 | client.visits++; 79 | next(); 80 | } else { 81 | this.node.log.warn('Rate limited:', client); 82 | res.status(429).jsonp({ 83 | status: 429, 84 | error: 'Rate limit exceeded' 85 | }); 86 | } 87 | }; 88 | 89 | RateLimiter.prototype.exceeded = function(client) { 90 | if (this.config[client.type].totalRequests === -1) { 91 | return false; 92 | } else { 93 | return client.visits > this.config[client.type].totalRequests; 94 | } 95 | }; 96 | 97 | RateLimiter.prototype.getClientType = function(name) { 98 | if (this.whitelist.indexOf(name) > -1) { 99 | return 'whitelist'; 100 | } 101 | if (this.blacklist.indexOf(name) > -1) { 102 | return 'blacklist'; 103 | } 104 | return 'normal'; 105 | }; 106 | 107 | RateLimiter.prototype.getClientName = function(req) { 108 | var name = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress; 109 | return name; 110 | }; 111 | 112 | RateLimiter.prototype.addClient = function(name) { 113 | var self = this; 114 | 115 | var client = { 116 | name: name, 117 | type: this.getClientType(name), 118 | visits: 1 119 | }; 120 | 121 | var resetTime = this.config[client.type].interval; 122 | 123 | setTimeout(function() { 124 | delete self.clients[name]; 125 | }, resetTime).unref(); 126 | 127 | this.clients[name] = client; 128 | 129 | return client; 130 | 131 | }; 132 | 133 | module.exports = RateLimiter; 134 | -------------------------------------------------------------------------------- /test/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var should = require('should'); 5 | var StatusController = require('../lib/status'); 6 | 7 | describe('Status', function() { 8 | describe('/status', function() { 9 | var info = { 10 | version: 110000, 11 | protocolVersion: 70002, 12 | blocks: 548645, 13 | timeOffset: 0, 14 | connections: 8, 15 | difficulty: 21546.906405522557, 16 | testnet: true, 17 | relayFee: 1000, 18 | errors: '' 19 | }; 20 | 21 | var outSetInfo = { 22 | height: 151, 23 | bestblock: '20b6cc0600037171b8bb634bbd04ea754945be44db8d9199b74798f1abdb382d', 24 | transactions: 151, 25 | txouts: 151, 26 | bytes_serialized: 10431, 27 | hash_serialized: 'c165d5dcb22a897745ee2ee274b47133b995bbcf8dd4a7572fedad87541c7df8', 28 | total_amount: 750000000000 29 | }; 30 | 31 | var node = { 32 | services: { 33 | bitcoind: { 34 | getInfo: sinon.stub().callsArgWith(0, null, info), 35 | getBestBlockHash: sinon.stub().callsArgWith(0, null, outSetInfo.bestblock), 36 | tiphash: outSetInfo.bestblock 37 | } 38 | } 39 | }; 40 | 41 | var status = new StatusController(node); 42 | 43 | it('getInfo', function(done) { 44 | var req = { 45 | query: {} 46 | }; 47 | var res = { 48 | jsonp: function(data) { 49 | should.exist(data.info.version); 50 | should.exist(data.info.protocolversion); 51 | should.exist(data.info.blocks); 52 | should.exist(data.info.timeoffset); 53 | should.exist(data.info.connections); 54 | should.exist(data.info.difficulty); 55 | should.exist(data.info.testnet); 56 | should.exist(data.info.relayfee); 57 | done(); 58 | } 59 | }; 60 | 61 | status.show(req, res); 62 | }); 63 | 64 | it('getDifficulty', function(done) { 65 | var req = { 66 | query: { 67 | q: 'getDifficulty' 68 | } 69 | }; 70 | var res = { 71 | jsonp: function(data) { 72 | data.difficulty.should.equal(info.difficulty); 73 | done(); 74 | } 75 | }; 76 | 77 | status.show(req, res); 78 | }); 79 | 80 | it('getBestBlockHash', function(done) { 81 | var req = { 82 | query: { 83 | q: 'getBestBlockHash' 84 | } 85 | }; 86 | var res = { 87 | jsonp: function(data) { 88 | data.bestblockhash.should.equal(outSetInfo.bestblock); 89 | done(); 90 | } 91 | }; 92 | status.show(req, res); 93 | }); 94 | 95 | it('getLastBlockHash', function(done) { 96 | var req = { 97 | query: { 98 | q: 'getLastBlockHash' 99 | } 100 | }; 101 | var res = { 102 | jsonp: function(data) { 103 | data.syncTipHash.should.equal(outSetInfo.bestblock); 104 | data.lastblockhash.should.equal(outSetInfo.bestblock); 105 | done(); 106 | } 107 | }; 108 | status.show(req, res); 109 | }); 110 | 111 | }); 112 | 113 | describe('/sync', function() { 114 | it('should have correct data', function(done) { 115 | var node = { 116 | services: { 117 | bitcoind: { 118 | height: 500000, 119 | isSynced: sinon.stub().callsArgWith(0, null, true), 120 | syncPercentage: sinon.stub().callsArgWith(0, null, 99.99) 121 | } 122 | } 123 | }; 124 | 125 | var expected = { 126 | status: 'finished', 127 | blockChainHeight: 500000, 128 | syncPercentage: 100, 129 | height: 500000, 130 | error: null, 131 | type: 'bitcore node' 132 | }; 133 | 134 | var status = new StatusController(node); 135 | 136 | var req = {}; 137 | var res = { 138 | jsonp: function(data) { 139 | should(data).eql(expected); 140 | done(); 141 | } 142 | }; 143 | status.sync(req, res); 144 | }); 145 | }); 146 | 147 | describe('/peer', function() { 148 | it('should have correct data', function(done) { 149 | var node = {}; 150 | 151 | var expected = { 152 | connected: true, 153 | host: '127.0.0.1', 154 | port: null 155 | }; 156 | 157 | var req = {}; 158 | var res = { 159 | jsonp: function(data) { 160 | should(data).eql(expected); 161 | done(); 162 | } 163 | }; 164 | 165 | var status = new StatusController(node); 166 | 167 | status.peer(req, res); 168 | }); 169 | }); 170 | 171 | describe('/version', function() { 172 | it('should have correct data', function(done) { 173 | var node = {}; 174 | var expected = { 175 | version: require('../package.json').version 176 | }; 177 | 178 | var req = {}; 179 | var res = { 180 | jsonp: function(data) { 181 | should(data).eql(expected); 182 | done(); 183 | } 184 | }; 185 | 186 | var status = new StatusController(node); 187 | status.version(req, res); 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /lib/charts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var bitcore = require('bitcore-lib-zcash'); 5 | var Common = require('./common'); 6 | var LRU = require('lru-cache'); 7 | 8 | function ChartController(options) { 9 | var self = this; 10 | this.node = options.node; 11 | this.blocks = options.blocks; 12 | 13 | this.chartCache = LRU(options.chartCacheSize || ChartController.DEFAULT_CHART_CACHE_SIZE); 14 | 15 | this.common = new Common({log: this.node.log}); 16 | } 17 | 18 | ChartController.DEFAULT_CHART_CACHE_SIZE = 5; 19 | ChartController.CHARTS = { 20 | 'block-size': { 21 | name: 'Block Size' 22 | }, 23 | 'block-interval': { 24 | name: 'Block Interval' 25 | }, 26 | 'difficulty': { 27 | name: 'Difficulty' 28 | }, 29 | 'mining-revenue': { 30 | name: 'Mining revenue' 31 | } 32 | }; 33 | 34 | ChartController.prototype.list = function(req, res) { 35 | var data = { 36 | charts: ChartController.CHARTS 37 | }; 38 | res.jsonp(data); 39 | }; 40 | 41 | ChartController.prototype.chart = function(req, res, next) { 42 | var self = this; 43 | var chartType = req.params.chartType; 44 | if (!(chartType in ChartController.CHARTS)) { 45 | return self.common.handleErrors(null, res); 46 | } 47 | 48 | var cacheKey = chartType; 49 | var chartCached = self.chartCache.get(cacheKey); 50 | 51 | if (chartCached) { 52 | req.chart = chartCached; 53 | next(); 54 | } else { 55 | var dateStr; 56 | var todayStr = this.formatTimestamp(new Date()); 57 | dateStr = todayStr; 58 | var gte = Math.round((new Date(dateStr)).getTime() / 1000); 59 | var lte = parseInt(req.query.startTimestamp) || gte + 86400; 60 | 61 | self.node.services.bitcoind.getBlockHashesByTimestamp(lte, gte, function(err, hashes) { 62 | if (err) { 63 | return self.common.handleErrors(err, res); 64 | } 65 | 66 | async.mapSeries( 67 | hashes, 68 | function(hash, next) { 69 | var subReq = { 70 | params: { 71 | blockHash: hash 72 | } 73 | }; 74 | self.blocks.block(subReq, res, function() { 75 | next(null, subReq.block); 76 | }); 77 | }, 78 | function(err, blocks) { 79 | if (err) { 80 | return self.common.handleErrors(err, res); 81 | } 82 | self.generateChart(chartType, blocks, function(err, chart) { 83 | if (err) { 84 | return self.common.handleErrors(err, res); 85 | } 86 | self.chartCache.set(cacheKey, chart); 87 | req.chart = chart; 88 | next(); 89 | }); 90 | } 91 | ); 92 | }); 93 | } 94 | }; 95 | 96 | ChartController.prototype.generateChart = function(chartType, blocks, callback) { 97 | if (chartType == 'mining-revenue') { 98 | this._miningRevenueChart(blocks, callback); 99 | } else { 100 | this._simpleChart(chartType, blocks, callback); 101 | } 102 | }; 103 | 104 | ChartController.prototype._miningRevenueChart = function(blocks, callback) { 105 | var self = this; 106 | async.mapSeries( 107 | blocks, 108 | function(block, next) { 109 | async.reduce( 110 | block.tx, 111 | block.reward * 1e8, 112 | function(memo, txid, next2) { 113 | self.node.getDetailedTransaction(txid, function(err, tx) { 114 | next2(null, memo+tx.feeSatoshis); 115 | }); 116 | }, 117 | function(err, revenueSatoshis) { 118 | next(err, revenueSatoshis); 119 | } 120 | ); 121 | }, 122 | function(err, revenuesSat) { 123 | var chart = { 124 | name: ChartController.CHARTS['mining-revenue'].name, 125 | data: { 126 | x: 'height', 127 | json: { 128 | }, 129 | names: { 130 | } 131 | } 132 | }; 133 | 134 | chart.data.json.height = blocks.map(function(block, index) { 135 | return block.height; 136 | }); 137 | chart.data.names.height = 'Height'; 138 | 139 | chart.data.json.revenue = revenuesSat.map(function(revenueSatoshis, index) { 140 | return (revenueSatoshis / 1e8).toFixed(8); 141 | }); 142 | chart.data.names.revenue = 'Mining revenue'; 143 | 144 | callback(null, chart); 145 | } 146 | ); 147 | }; 148 | 149 | ChartController.prototype._simpleChart = function(chartType, blocks, callback) { 150 | var chart = { 151 | name: ChartController.CHARTS[chartType].name, 152 | data: { 153 | x: 'height', 154 | json: { 155 | }, 156 | names: { 157 | } 158 | } 159 | }; 160 | 161 | chart.data.json.height = blocks.map(function(block, index) { 162 | return block.height; 163 | }); 164 | chart.data.names.height = 'Height'; 165 | 166 | if (chartType == 'block-size') { 167 | chart.data.json.size = blocks.map(function(block, index) { 168 | return block.size; 169 | }); 170 | chart.data.names.size = 'Block size'; 171 | } else if (chartType == 'block-interval') { 172 | chart.data.json.height = chart.data.json.height.slice(1); 173 | chart.data.json.blockinterval = blocks.slice(1).map(function(block, index) { 174 | return block.time - blocks[index].time; 175 | }); 176 | chart.data.names.blockinterval = 'Block interval'; 177 | } else if (chartType == 'difficulty') { 178 | chart.data.json.difficulty = blocks.map(function(block, index) { 179 | return block.difficulty; 180 | }); 181 | chart.data.names.difficulty = 'Difficulty'; 182 | } 183 | 184 | callback(null, chart); 185 | }; 186 | 187 | ChartController.prototype.show = function(req, res) { 188 | if (req.chart) { 189 | res.jsonp(req.chart); 190 | } 191 | }; 192 | 193 | //helper to convert timestamps to yyyy-mm-dd format 194 | ChartController.prototype.formatTimestamp = function(date) { 195 | var yyyy = date.getUTCFullYear().toString(); 196 | var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based 197 | var dd = date.getUTCDate().toString(); 198 | 199 | return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding 200 | }; 201 | 202 | module.exports = ChartController; 203 | -------------------------------------------------------------------------------- /test/ratelimiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var sinon = require('sinon'); 5 | 6 | var RateLimiter = require('../lib/ratelimiter'); 7 | 8 | describe('RateLimiter', function() { 9 | 10 | describe('@constructor', function() { 11 | it('will instantiate without options', function() { 12 | var limiter = new RateLimiter(); 13 | should.exist(limiter); 14 | }); 15 | it('will instantiate without new', function() { 16 | /* jshint newcap:false */ 17 | var limiter = RateLimiter(); 18 | should.exist(limiter); 19 | }); 20 | it('will instantiate with options', function() { 21 | var whitelist = []; 22 | var blacklist = []; 23 | var node = {}; 24 | var limiter = new RateLimiter({ 25 | node: node, 26 | whitelist: whitelist, 27 | blacklist: blacklist, 28 | limit: 1, 29 | interval: 1, 30 | whitelistLimit: 1, 31 | whitelistInterval: 1, 32 | blacklistLimit: 1, 33 | blacklistInterval: 1 34 | }); 35 | should.exist(limiter); 36 | should.exist(limiter.config); 37 | should.exist(limiter.clients); 38 | should.exist(limiter.node); 39 | limiter.whitelist.should.equal(whitelist); 40 | limiter.blacklist.should.equal(blacklist); 41 | limiter.config.whitelist.totalRequests.should.equal(1); 42 | limiter.config.whitelist.interval.should.equal(1); 43 | limiter.config.blacklist.totalRequests.should.equal(1); 44 | limiter.config.blacklist.interval.should.equal(1); 45 | limiter.config.normal.interval.should.equal(1); 46 | limiter.config.normal.totalRequests.should.equal(1); 47 | }); 48 | }); 49 | 50 | describe('#middleware', function() { 51 | it('will set ratelimit headers', function(done) { 52 | var limiter = new RateLimiter(); 53 | var req = { 54 | headers: { 55 | 'cf-connecting-ip': '127.0.0.1' 56 | } 57 | }; 58 | var setHeader = sinon.stub(); 59 | var res = { 60 | setHeader: setHeader 61 | }; 62 | limiter.middleware()(req, res, function() { 63 | setHeader.callCount.should.equal(2); 64 | setHeader.args[0][0].should.equal('X-RateLimit-Limit'); 65 | setHeader.args[0][1].should.equal(10800); 66 | setHeader.args[1][0].should.equal('X-RateLimit-Remaining'); 67 | setHeader.args[1][1].should.equal(10799); 68 | done(); 69 | }); 70 | }); 71 | it('will give rate limit error', function() { 72 | var node = { 73 | log: { 74 | warn: sinon.stub() 75 | } 76 | }; 77 | var limiter = new RateLimiter({node: node}); 78 | limiter.exceeded = sinon.stub().returns(true); 79 | var req = { 80 | headers: { 81 | 'cf-connecting-ip': '127.0.0.1' 82 | } 83 | }; 84 | var jsonp = sinon.stub(); 85 | var status = sinon.stub().returns({ 86 | jsonp: jsonp 87 | }); 88 | var res = { 89 | status: status, 90 | setHeader: sinon.stub() 91 | }; 92 | limiter.middleware()(req, res); 93 | status.callCount.should.equal(1); 94 | status.args[0][0].should.equal(429); 95 | jsonp.callCount.should.equal(1); 96 | jsonp.args[0][0].should.eql({ 97 | status: 429, 98 | error: 'Rate limit exceeded' 99 | }); 100 | }); 101 | }); 102 | 103 | describe('#exceeded', function() { 104 | it('should not be exceeded', function() { 105 | var node = {}; 106 | var limiter = new RateLimiter({node: node}); 107 | var client = limiter.addClient('127.0.0.1'); 108 | var exceeded = limiter.exceeded(client); 109 | exceeded.should.equal(false); 110 | }); 111 | it('should be exceeded', function() { 112 | var node = {}; 113 | var limiter = new RateLimiter({node: node}); 114 | var client = limiter.addClient('127.0.0.1'); 115 | client.visits = 3 * 60 * 60 + 1; 116 | var exceeded = limiter.exceeded(client); 117 | exceeded.should.equal(true); 118 | }); 119 | it('should exclude whitelisted with no limit', function() { 120 | var node = {}; 121 | var limiter = new RateLimiter({ 122 | whitelist: [ 123 | '127.0.0.1' 124 | ], 125 | node: node, 126 | whitelistLimit: -1 127 | }); 128 | var client = limiter.addClient('127.0.0.1'); 129 | client.visits = Infinity; 130 | var exceeded = limiter.exceeded(client); 131 | exceeded.should.equal(false); 132 | }); 133 | }); 134 | 135 | describe('#getClientName', function() { 136 | it('should get client name from cloudflare header', function() { 137 | var node = {}; 138 | var limiter = new RateLimiter({node: node}); 139 | var req = { 140 | headers: { 141 | 'cf-connecting-ip': '127.0.0.1' 142 | } 143 | }; 144 | var name = limiter.getClientName(req); 145 | name.should.equal('127.0.0.1'); 146 | }); 147 | it('should get client name from x forwarded header', function() { 148 | var node = {}; 149 | var limiter = new RateLimiter({node: node}); 150 | var req = { 151 | headers: { 152 | 'x-forwarded-for': '127.0.0.1' 153 | } 154 | }; 155 | var name = limiter.getClientName(req); 156 | name.should.equal('127.0.0.1'); 157 | }); 158 | it('should get client name from connection remote address', function() { 159 | var node = {}; 160 | var limiter = new RateLimiter({node: node}); 161 | var req = { 162 | headers: {}, 163 | connection: { 164 | remoteAddress: '127.0.0.1' 165 | } 166 | }; 167 | var name = limiter.getClientName(req); 168 | name.should.equal('127.0.0.1'); 169 | }); 170 | }); 171 | 172 | describe('#addClient', function() { 173 | var sandbox = sinon.sandbox.create(); 174 | afterEach(function() { 175 | sandbox.restore(); 176 | }); 177 | it('will remove client after interval', function() { 178 | var THREE_HOURS_PLUS = 3 * 60 * 60 * 1000 + 1; 179 | var clock = sandbox.useFakeTimers(); 180 | var node = {}; 181 | var limiter = new RateLimiter({node: node}); 182 | limiter.addClient('127.0.0.1'); 183 | should.exist(limiter.clients['127.0.0.1']); 184 | clock.tick(THREE_HOURS_PLUS); 185 | should.not.exist(limiter.clients['127.0.0.1']); 186 | }); 187 | }); 188 | 189 | }); 190 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var sinon = require('sinon'); 5 | var InsightAPI = require('../lib/index'); 6 | 7 | describe('Index', function() { 8 | describe('@constructor', function() { 9 | it('will set rate limiter options', function() { 10 | var options = {}; 11 | var node = {}; 12 | var index = new InsightAPI({ 13 | rateLimiterOptions: options, 14 | node: node 15 | }); 16 | index.rateLimiterOptions.should.equal(options); 17 | }); 18 | it('will set disable rate limiter option', function() { 19 | var node = {}; 20 | var index = new InsightAPI({ 21 | disableRateLimiter: true, 22 | node: node 23 | }); 24 | index.disableRateLimiter.should.equal(true); 25 | }); 26 | }); 27 | describe('#_getRateLimiter', function() { 28 | it('will pass options to rate limiter', function() { 29 | var options = { 30 | whitelist: ['127.0.0.1'] 31 | }; 32 | var node = {}; 33 | var index = new InsightAPI({ 34 | rateLimiterOptions: options, 35 | node: node 36 | }); 37 | var limiter = index._getRateLimiter(); 38 | limiter.whitelist.should.eql(['127.0.0.1']); 39 | }); 40 | }); 41 | describe('#cache', function() { 42 | it('will set cache control header', function(done) { 43 | var node = { 44 | log: sinon.stub() 45 | }; 46 | var index = new InsightAPI({ 47 | enableCache: true, 48 | node: node 49 | }); 50 | var req = {}; 51 | var res = { 52 | header: sinon.stub() 53 | }; 54 | var middle = index.cache(10); 55 | middle(req, res, function() { 56 | res.header.callCount.should.equal(1); 57 | res.header.args[0][0].should.equal('Cache-Control'); 58 | res.header.args[0][1].should.equal('public, max-age=10'); 59 | done(); 60 | }); 61 | }); 62 | it('will NOT set cache control header', function(done) { 63 | var node = { 64 | log: sinon.stub() 65 | }; 66 | var index = new InsightAPI({ 67 | enableCache: false, 68 | node: node 69 | }); 70 | var req = {}; 71 | var res = { 72 | header: sinon.stub() 73 | }; 74 | var middle = index.cache(10); 75 | middle(req, res, function() { 76 | res.header.callCount.should.equal(0); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | describe('#cacheShort', function() { 82 | it('will set SHORT cache control header', function(done) { 83 | var node = { 84 | log: sinon.stub() 85 | }; 86 | var index = new InsightAPI({ 87 | enableCache: true, 88 | cacheShortSeconds: 35, 89 | node: node 90 | }); 91 | var req = {}; 92 | var res = { 93 | header: sinon.stub() 94 | }; 95 | var middle = index.cacheShort(); 96 | middle(req, res, function() { 97 | res.header.callCount.should.equal(1); 98 | res.header.args[0][0].should.equal('Cache-Control'); 99 | res.header.args[0][1].should.equal('public, max-age=35'); 100 | done(); 101 | }); 102 | }); 103 | it('will set SHORT DEFAULT cache control header', function(done) { 104 | var node = { 105 | log: sinon.stub() 106 | }; 107 | var index = new InsightAPI({ 108 | enableCache: true, 109 | node: node 110 | }); 111 | var req = {}; 112 | var res = { 113 | header: sinon.stub() 114 | }; 115 | var middle = index.cacheShort(); 116 | middle(req, res, function() { 117 | res.header.callCount.should.equal(1); 118 | res.header.args[0][0].should.equal('Cache-Control'); 119 | res.header.args[0][1].should.equal('public, max-age=30'); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | describe('#cacheLong', function() { 125 | it('will set LONG cache control header', function(done) { 126 | var node = { 127 | log: sinon.stub() 128 | }; 129 | var index = new InsightAPI({ 130 | enableCache: true, 131 | cacheLongSeconds: 86400000, 132 | node: node 133 | }); 134 | var req = {}; 135 | var res = { 136 | header: sinon.stub() 137 | }; 138 | var middle = index.cacheLong(); 139 | middle(req, res, function() { 140 | res.header.callCount.should.equal(1); 141 | res.header.args[0][0].should.equal('Cache-Control'); 142 | res.header.args[0][1].should.equal('public, max-age=86400000'); 143 | done(); 144 | }); 145 | }); 146 | it('will set LONG DEFAULT cache control header', function(done) { 147 | var node = { 148 | log: sinon.stub() 149 | }; 150 | var index = new InsightAPI({ 151 | enableCache: true, 152 | node: node 153 | }); 154 | var req = {}; 155 | var res = { 156 | header: sinon.stub() 157 | }; 158 | var middle = index.cacheLong(); 159 | middle(req, res, function() { 160 | res.header.callCount.should.equal(1); 161 | res.header.args[0][0].should.equal('Cache-Control'); 162 | res.header.args[0][1].should.equal('public, max-age=86400'); 163 | done(); 164 | }); 165 | }); 166 | }); 167 | describe('#setupRoutes', function() { 168 | it('will use rate limiter by default', function() { 169 | var node = {}; 170 | var index = new InsightAPI({ 171 | node: node 172 | }); 173 | var middlewareFunc = sinon.stub(); 174 | var middleware = sinon.stub().returns(middlewareFunc); 175 | var limiter = { 176 | middleware: middleware 177 | }; 178 | index._getRateLimiter = sinon.stub().returns(limiter); 179 | var use = sinon.stub(); 180 | var app = { 181 | use: use, 182 | get: sinon.stub(), 183 | param: sinon.stub(), 184 | post: sinon.stub() 185 | }; 186 | index.setupRoutes(app); 187 | use.callCount.should.be.above(0); 188 | use.args[0][0].should.equal(middlewareFunc); 189 | middleware.callCount.should.equal(1); 190 | }); 191 | it('will NOT use rate limiter if disabled', function() { 192 | var node = {}; 193 | var index = new InsightAPI({ 194 | node: node, 195 | disableRateLimiter: true 196 | }); 197 | index._getRateLimiter = sinon.stub(); 198 | var use = sinon.stub(); 199 | var app = { 200 | use: use, 201 | get: sinon.stub(), 202 | param: sinon.stub(), 203 | post: sinon.stub() 204 | }; 205 | index.setupRoutes(app); 206 | index._getRateLimiter.callCount.should.equal(0); 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /lib/addresses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib-zcash'); 4 | var async = require('async'); 5 | var TxController = require('./transactions'); 6 | var Common = require('./common'); 7 | 8 | function AddressController(node) { 9 | this.node = node; 10 | this.txController = new TxController(node); 11 | this.common = new Common({log: this.node.log}); 12 | } 13 | 14 | AddressController.prototype.show = function(req, res) { 15 | var self = this; 16 | var options = { 17 | noTxList: parseInt(req.query.noTxList) 18 | }; 19 | 20 | if (req.query.from && req.query.to) { 21 | options.from = parseInt(req.query.from); 22 | options.to = parseInt(req.query.to); 23 | } 24 | 25 | this.getAddressSummary(req.addr, options, function(err, data) { 26 | if(err) { 27 | return self.common.handleErrors(err, res); 28 | } 29 | 30 | res.jsonp(data); 31 | }); 32 | }; 33 | 34 | AddressController.prototype.balance = function(req, res) { 35 | this.addressSummarySubQuery(req, res, 'balanceSat'); 36 | }; 37 | 38 | AddressController.prototype.totalReceived = function(req, res) { 39 | this.addressSummarySubQuery(req, res, 'totalReceivedSat'); 40 | }; 41 | 42 | AddressController.prototype.totalSent = function(req, res) { 43 | this.addressSummarySubQuery(req, res, 'totalSentSat'); 44 | }; 45 | 46 | AddressController.prototype.unconfirmedBalance = function(req, res) { 47 | this.addressSummarySubQuery(req, res, 'unconfirmedBalanceSat'); 48 | }; 49 | 50 | AddressController.prototype.addressSummarySubQuery = function(req, res, param) { 51 | var self = this; 52 | this.getAddressSummary(req.addr, {}, function(err, data) { 53 | if(err) { 54 | return self.common.handleErrors(err, res); 55 | } 56 | 57 | res.jsonp(data[param]); 58 | }); 59 | }; 60 | 61 | AddressController.prototype.getAddressSummary = function(address, options, callback) { 62 | 63 | this.node.getAddressSummary(address, options, function(err, summary) { 64 | if(err) { 65 | return callback(err); 66 | } 67 | 68 | var transformed = { 69 | addrStr: address, 70 | balance: summary.balance / 1e8, 71 | balanceSat: summary.balance, 72 | totalReceived: summary.totalReceived / 1e8, 73 | totalReceivedSat: summary.totalReceived, 74 | totalSent: summary.totalSpent / 1e8, 75 | totalSentSat: summary.totalSpent, 76 | unconfirmedBalance: summary.unconfirmedBalance / 1e8, 77 | unconfirmedBalanceSat: summary.unconfirmedBalance, 78 | unconfirmedTxApperances: summary.unconfirmedAppearances, // misspelling - ew 79 | txApperances: summary.appearances, // yuck 80 | transactions: summary.txids 81 | }; 82 | 83 | callback(null, transformed); 84 | }); 85 | }; 86 | 87 | AddressController.prototype.checkAddr = function(req, res, next) { 88 | req.addr = req.params.addr; 89 | this.check(req, res, next, [req.addr]); 90 | }; 91 | 92 | AddressController.prototype.checkAddrs = function(req, res, next) { 93 | if(req.body.addrs) { 94 | req.addrs = req.body.addrs.split(','); 95 | } else { 96 | req.addrs = req.params.addrs.split(','); 97 | } 98 | 99 | this.check(req, res, next, req.addrs); 100 | }; 101 | 102 | AddressController.prototype.check = function(req, res, next, addresses) { 103 | var self = this; 104 | if(!addresses.length || !addresses[0]) { 105 | return self.common.handleErrors({ 106 | message: 'Must include address', 107 | code: 1 108 | }, res); 109 | } 110 | 111 | for(var i = 0; i < addresses.length; i++) { 112 | try { 113 | var a = new bitcore.Address(addresses[i]); 114 | } catch(e) { 115 | return self.common.handleErrors({ 116 | message: 'Invalid address: ' + e.message, 117 | code: 1 118 | }, res); 119 | } 120 | } 121 | 122 | next(); 123 | }; 124 | 125 | AddressController.prototype.utxo = function(req, res) { 126 | var self = this; 127 | 128 | this.node.getAddressUnspentOutputs(req.addr, {}, function(err, utxos) { 129 | if(err) { 130 | return self.common.handleErrors(err, res); 131 | } else if (!utxos.length) { 132 | return res.jsonp([]); 133 | } 134 | res.jsonp(utxos.map(self.transformUtxo.bind(self))); 135 | }); 136 | }; 137 | 138 | AddressController.prototype.multiutxo = function(req, res) { 139 | var self = this; 140 | this.node.getAddressUnspentOutputs(req.addrs, true, function(err, utxos) { 141 | if(err && err.code === -5) { 142 | return res.jsonp([]); 143 | } else if(err) { 144 | return self.common.handleErrors(err, res); 145 | } 146 | 147 | res.jsonp(utxos.map(self.transformUtxo.bind(self))); 148 | }); 149 | }; 150 | 151 | AddressController.prototype.transformUtxo = function(utxoArg) { 152 | var utxo = { 153 | address: utxoArg.address, 154 | txid: utxoArg.txid, 155 | vout: utxoArg.outputIndex, 156 | scriptPubKey: utxoArg.script, 157 | amount: utxoArg.satoshis / 1e8, 158 | satoshis: utxoArg.satoshis 159 | }; 160 | if (utxoArg.height && utxoArg.height > 0) { 161 | utxo.height = utxoArg.height; 162 | utxo.confirmations = this.node.services.bitcoind.height - utxoArg.height + 1; 163 | } else { 164 | utxo.confirmations = 0; 165 | } 166 | if (utxoArg.timestamp) { 167 | utxo.ts = utxoArg.timestamp; 168 | } 169 | return utxo; 170 | }; 171 | 172 | AddressController.prototype._getTransformOptions = function(req) { 173 | return { 174 | noAsm: parseInt(req.query.noAsm) ? true : false, 175 | noScriptSig: parseInt(req.query.noScriptSig) ? true : false, 176 | noSpent: parseInt(req.query.noSpent) ? true : false 177 | }; 178 | }; 179 | 180 | AddressController.prototype.multitxs = function(req, res, next) { 181 | var self = this; 182 | 183 | var options = { 184 | from: parseInt(req.query.from) || parseInt(req.body.from) || 0 185 | }; 186 | 187 | options.to = parseInt(req.query.to) || parseInt(req.body.to) || parseInt(options.from) + 10; 188 | 189 | self.node.getAddressHistory(req.addrs, options, function(err, result) { 190 | if(err) { 191 | return self.common.handleErrors(err, res); 192 | } 193 | 194 | var transformOptions = self._getTransformOptions(req); 195 | 196 | self.transformAddressHistoryForMultiTxs(result.items, transformOptions, function(err, items) { 197 | if (err) { 198 | return self.common.handleErrors(err, res); 199 | } 200 | res.jsonp({ 201 | totalItems: result.totalCount, 202 | from: options.from, 203 | to: Math.min(options.to, result.totalCount), 204 | items: items 205 | }); 206 | }); 207 | 208 | }); 209 | }; 210 | 211 | AddressController.prototype.transformAddressHistoryForMultiTxs = function(txinfos, options, callback) { 212 | var self = this; 213 | 214 | var items = txinfos.map(function(txinfo) { 215 | return txinfo.tx; 216 | }).filter(function(value, index, self) { 217 | return self.indexOf(value) === index; 218 | }); 219 | 220 | async.map( 221 | items, 222 | function(item, next) { 223 | self.txController.transformTransaction(item, options, next); 224 | }, 225 | callback 226 | ); 227 | }; 228 | 229 | 230 | 231 | module.exports = AddressController; 232 | -------------------------------------------------------------------------------- /test/blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var sinon = require('sinon'); 5 | var BlockController = require('../lib/blocks'); 6 | var bitcore = require('bitcore-lib-zcash'); 7 | var _ = require('lodash'); 8 | 9 | var blocks = require('./data/blocks.json'); 10 | 11 | var blockIndexes = { 12 | '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7': { 13 | hash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 14 | chainWork: '0000000000000000000000000000000000000000000000054626b1839ade284a', 15 | prevHash: '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4', 16 | nextHash: '000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d', 17 | confirmations: 119, 18 | height: 533974 19 | }, 20 | '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7': { 21 | hash: '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7', 22 | chainWork: '00000000000000000000000000000000000000000000000544ea52e1575ca753', 23 | prevHash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', 24 | confirmations: 119, 25 | height: 533951 26 | }, 27 | '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441': { 28 | hash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', 29 | chainWork: '00000000000000000000000000000000000000000000000544ea52e0575ba752', 30 | prevHash: '000000000001b9c41e6c4a7b81a068b50cf3f522ee4ac1e942e75ec16e090547', 31 | height: 533950 32 | }, 33 | '000000000000000004a118407a4e3556ae2d5e882017e7ce526659d8073f13a4': { 34 | hash: '000000000000000004a118407a4e3556ae2d5e882017e7ce526659d8073f13a4', 35 | prevHash: '00000000000000000a9d74a7b527f7b995fc21ceae5aa21087b443469351a362', 36 | height: 375493 37 | }, 38 | 533974: { 39 | hash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 40 | chainWork: '0000000000000000000000000000000000000000000000054626b1839ade284a', 41 | prevHash: '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4', 42 | height: 533974 43 | } 44 | }; 45 | 46 | describe('Blocks', function() { 47 | describe('/blocks/:blockHash route', function() { 48 | var insight = { 49 | 'hash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 50 | 'confirmations': 119, 51 | 'size': 1011, 52 | 'height': 533974, 53 | 'version': 536870919, 54 | 'merkleroot': 'b06437355844b8178173f3e18ca141472e4b0861daa81ef0f701cf9e51f0283e', 55 | 'tx': [ 56 | '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd', 57 | 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 58 | '2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1' 59 | ], 60 | 'time': 1440987503, 61 | 'nonce': 1868753784, 62 | 'bits': '1a0cf267', 63 | 'difficulty': 1295829.93087696, 64 | 'chainwork': '0000000000000000000000000000000000000000000000054626b1839ade284a', 65 | 'previousblockhash': '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4', 66 | 'nextblockhash': '000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d', 67 | 'reward': 12.5, 68 | 'isMainChain': true, 69 | 'poolInfo': {} 70 | }; 71 | 72 | var bitcoreBlock = bitcore.Block.fromBuffer(new Buffer(blocks['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'], 'hex')); 73 | 74 | var node = { 75 | log: sinon.stub(), 76 | getBlock: sinon.stub().callsArgWith(1, null, bitcoreBlock), 77 | services: { 78 | bitcoind: { 79 | getBlockHeader: sinon.stub().callsArgWith(1, null, blockIndexes['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7']), 80 | isMainChain: sinon.stub().returns(true), 81 | height: 534092 82 | } 83 | } 84 | }; 85 | 86 | it('block data should be correct', function(done) { 87 | var controller = new BlockController({node: node}); 88 | var hash = '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'; 89 | var req = { 90 | params: { 91 | blockHash: hash 92 | } 93 | }; 94 | var res = {}; 95 | var next = function() { 96 | should.exist(req.block); 97 | var block = req.block; 98 | should(block).eql(insight); 99 | done(); 100 | }; 101 | controller.block(req, res, next); 102 | }); 103 | 104 | it('block pool info should be correct', function(done) { 105 | var block = bitcore.Block.fromString(blocks['000000000000000004a118407a4e3556ae2d5e882017e7ce526659d8073f13a4']); 106 | var node = { 107 | log: sinon.stub(), 108 | getBlock: sinon.stub().callsArgWith(1, null, block), 109 | services: { 110 | bitcoind: { 111 | getBlockHeader: sinon.stub().callsArgWith(1, null, blockIndexes['000000000000000004a118407a4e3556ae2d5e882017e7ce526659d8073f13a4']), 112 | isMainChain: sinon.stub().returns(true), 113 | height: 534092 114 | } 115 | } 116 | }; 117 | var controller = new BlockController({node: node}); 118 | var req = { 119 | params: { 120 | blockHash: hash 121 | } 122 | }; 123 | var res = {}; 124 | var next = function() { 125 | should.exist(req.block); 126 | var block = req.block; 127 | req.block.poolInfo.poolName.should.equal('Discus Fish'); 128 | req.block.poolInfo.url.should.equal('http://f2pool.com/'); 129 | done(); 130 | }; 131 | 132 | var hash = '000000000000000004a118407a4e3556ae2d5e882017e7ce526659d8073f13a4'; 133 | 134 | controller.block(req, res, next); 135 | }); 136 | 137 | }); 138 | 139 | describe('/blocks route', function() { 140 | 141 | var insight = { 142 | 'blocks': [ 143 | { 144 | 'height': 533951, 145 | 'size': 206, 146 | 'hash': '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7', 147 | 'time': 1440978683, 148 | 'txlength': 1, 149 | 'poolInfo': { 150 | 'poolName': 'AntMiner', 151 | 'url': 'https://bitmaintech.com/' 152 | } 153 | }, 154 | { 155 | 'height': 533950, 156 | 'size': 206, 157 | 'hash': '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', 158 | 'time': 1440977479, 159 | 'txlength': 1, 160 | 'poolInfo': { 161 | 'poolName': 'AntMiner', 162 | 'url': 'https://bitmaintech.com/' 163 | } 164 | } 165 | ], 166 | 'length': 2, 167 | 'pagination': { 168 | 'current': '2015-08-30', 169 | 'currentTs': 1440979199, 170 | 'isToday': false, 171 | 'more': false, 172 | 'next': '2015-08-31', 173 | 'prev': '2015-08-29' 174 | } 175 | }; 176 | 177 | var stub = sinon.stub(); 178 | stub.onFirstCall().callsArgWith(1, null, new Buffer(blocks['000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7'], 'hex')); 179 | stub.onSecondCall().callsArgWith(1, null, new Buffer(blocks['00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441'], 'hex')); 180 | 181 | var hashes = [ 182 | '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', 183 | '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7' 184 | ]; 185 | var node = { 186 | log: sinon.stub(), 187 | services: { 188 | bitcoind: { 189 | getRawBlock: stub, 190 | getBlockHeader: function(hash, callback) { 191 | callback(null, blockIndexes[hash]); 192 | }, 193 | getBlockHashesByTimestamp: sinon.stub().callsArgWith(2, null, hashes) 194 | } 195 | } 196 | }; 197 | 198 | it('should have correct data', function(done) { 199 | var blocks = new BlockController({node: node}); 200 | 201 | var req = { 202 | query: { 203 | limit: 2, 204 | blockDate: '2015-08-30' 205 | } 206 | }; 207 | 208 | var res = { 209 | jsonp: function(data) { 210 | should(data).eql(insight); 211 | done(); 212 | } 213 | }; 214 | 215 | blocks.list(req, res); 216 | }); 217 | }); 218 | 219 | describe('/block-index/:height route', function() { 220 | var node = { 221 | log: sinon.stub(), 222 | services: { 223 | bitcoind: { 224 | getBlockHeader: function(height, callback) { 225 | callback(null, blockIndexes[height]); 226 | } 227 | } 228 | } 229 | }; 230 | 231 | it('should have correct data', function(done) { 232 | var blocks = new BlockController({node: node}); 233 | 234 | var insight = { 235 | 'blockHash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7' 236 | }; 237 | 238 | var height = 533974; 239 | 240 | var req = { 241 | params: { 242 | height: height 243 | } 244 | }; 245 | var res = { 246 | jsonp: function(data) { 247 | should(data).eql(insight); 248 | done(); 249 | } 250 | }; 251 | 252 | blocks.blockIndex(req, res); 253 | }); 254 | }); 255 | 256 | describe('#getBlockReward', function() { 257 | var node = { 258 | log: sinon.stub() 259 | }; 260 | var blocks = new BlockController({node: node}); 261 | 262 | it('should give a block reward of 50 * 1e8 for block before first halvening', function() { 263 | blocks.getBlockReward(100000).should.equal(50 * 1e8); 264 | }); 265 | 266 | it('should give a block reward of 25 * 1e8 for block between first and second halvenings', function() { 267 | blocks.getBlockReward(373011).should.equal(25 * 1e8); 268 | }); 269 | 270 | it('should give a block reward of 12.5 * 1e8 for block between second and third halvenings', function() { 271 | blocks.getBlockReward(500000).should.equal(12.5 * 1e8); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /lib/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib-zcash'); 4 | var _ = bitcore.deps._; 5 | var $ = bitcore.util.preconditions; 6 | var Common = require('./common'); 7 | var async = require('async'); 8 | 9 | var MAXINT = 0xffffffff; // Math.pow(2, 32) - 1; 10 | 11 | function TxController(node) { 12 | this.node = node; 13 | this.common = new Common({log: this.node.log}); 14 | } 15 | 16 | TxController.prototype.show = function(req, res) { 17 | if (req.transaction) { 18 | res.jsonp(req.transaction); 19 | } 20 | }; 21 | 22 | /** 23 | * Find transaction by hash ... 24 | */ 25 | TxController.prototype.transaction = function(req, res, next) { 26 | var self = this; 27 | var txid = req.params.txid; 28 | 29 | this.node.getDetailedTransaction(txid, function(err, transaction) { 30 | if (err && err.code === -5) { 31 | return self.common.handleErrors(null, res); 32 | } else if(err) { 33 | return self.common.handleErrors(err, res); 34 | } 35 | 36 | self.transformTransaction(transaction, function(err, transformedTransaction) { 37 | if (err) { 38 | return self.common.handleErrors(err, res); 39 | } 40 | req.transaction = transformedTransaction; 41 | next(); 42 | }); 43 | 44 | }); 45 | }; 46 | 47 | TxController.prototype.transformTransaction = function(transaction, options, callback) { 48 | if (_.isFunction(options)) { 49 | callback = options; 50 | options = {}; 51 | } 52 | $.checkArgument(_.isFunction(callback)); 53 | 54 | var confirmations = 0; 55 | if(transaction.height >= 0) { 56 | confirmations = this.node.services.bitcoind.height - transaction.height + 1; 57 | } 58 | 59 | var transformed = { 60 | txid: transaction.hash, 61 | version: transaction.version, 62 | locktime: transaction.locktime 63 | }; 64 | 65 | if(transaction.coinbase) { 66 | transformed.vin = [ 67 | { 68 | coinbase: transaction.inputs[0].script, 69 | sequence: transaction.inputs[0].sequence, 70 | n: 0 71 | } 72 | ]; 73 | } else { 74 | transformed.vin = transaction.inputs.map(this.transformInput.bind(this, options)); 75 | } 76 | 77 | transformed.vout = transaction.outputs.map(this.transformOutput.bind(this, options)); 78 | 79 | if (transformed.version >= 2) { 80 | transformed.vjoinsplit = transaction.joinSplits.map(this.transformJoinSplit.bind(this, options)); 81 | } 82 | 83 | transformed.blockhash = transaction.blockHash; 84 | transformed.blockheight = transaction.height; 85 | transformed.confirmations = confirmations; 86 | // TODO consider mempool txs with receivedTime? 87 | var time = transaction.blockTimestamp ? transaction.blockTimestamp : Math.round(Date.now() / 1000); 88 | transformed.time = time; 89 | if (transformed.confirmations) { 90 | transformed.blocktime = transformed.time; 91 | } 92 | 93 | if(transaction.coinbase) { 94 | transformed.isCoinBase = true; 95 | } 96 | 97 | transformed.valueOut = transaction.outputSatoshis / 1e8; 98 | transformed.size = transaction.hex.length / 2; // in bytes 99 | if (!transaction.coinbase) { 100 | transformed.valueIn = transaction.inputSatoshis / 1e8; 101 | transformed.fees = transaction.feeSatoshis / 1e8; 102 | } 103 | 104 | // Overwinter START 105 | transformed.fOverwintered = transaction.fOverwintered; 106 | if (transaction.fOverwintered) { 107 | transformed.nVersionGroupId = transaction.nVersionGroupId; 108 | transformed.nExpiryHeight = transaction.nExpiryHeight; 109 | } 110 | // Overwinter END 111 | 112 | // Sapling START 113 | if (transaction.fOverwintered && transaction.version >= 4) { 114 | transformed.valueBalance = transaction.valueBalance; 115 | transformed.spendDescs = transaction.spendDescs; 116 | transformed.outputDescs = transaction.outputDescs; 117 | if (transaction.bindingSig) { 118 | transformed.bindingSig = transaction.bindingSig; 119 | } 120 | } 121 | // Sapling END 122 | 123 | callback(null, transformed); 124 | }; 125 | 126 | TxController.prototype.transformInput = function(options, input, index) { 127 | // Input scripts are validated and can be assumed to be valid 128 | var transformed = { 129 | txid: input.prevTxId, 130 | vout: input.outputIndex, 131 | sequence: input.sequence, 132 | n: index 133 | }; 134 | 135 | if (!options.noScriptSig) { 136 | transformed.scriptSig = { 137 | hex: input.script 138 | }; 139 | if (!options.noAsm) { 140 | transformed.scriptSig.asm = input.scriptAsm; 141 | } 142 | } 143 | 144 | transformed.addr = input.address; 145 | transformed.valueSat = input.satoshis; 146 | transformed.value = input.satoshis / 1e8; 147 | transformed.doubleSpentTxID = null; // TODO 148 | //transformed.isConfirmed = null; // TODO 149 | //transformed.confirmations = null; // TODO 150 | //transformed.unconfirmedInput = null; // TODO 151 | 152 | return transformed; 153 | }; 154 | 155 | TxController.prototype.transformOutput = function(options, output, index) { 156 | var transformed = { 157 | value: (output.satoshis / 1e8).toFixed(8), 158 | n: index, 159 | scriptPubKey: { 160 | hex: output.script 161 | } 162 | }; 163 | 164 | if (!options.noAsm) { 165 | transformed.scriptPubKey.asm = output.scriptAsm; 166 | } 167 | 168 | if (!options.noSpent) { 169 | transformed.spentTxId = output.spentTxId || null; 170 | transformed.spentIndex = _.isUndefined(output.spentIndex) ? null : output.spentIndex; 171 | transformed.spentHeight = output.spentHeight || null; 172 | } 173 | 174 | if (output.address) { 175 | transformed.scriptPubKey.addresses = [output.address]; 176 | var address = bitcore.Address(output.address); //TODO return type from bitcore-node 177 | transformed.scriptPubKey.type = address.type; 178 | } 179 | return transformed; 180 | }; 181 | 182 | TxController.prototype.transformJoinSplit = function(options, jsdesc, index) { 183 | var transformed = { 184 | vpub_old: (jsdesc.oldZatoshis / 1e8).toFixed(8), 185 | vpub_new: (jsdesc.newZatoshis / 1e8).toFixed(8), 186 | n: index, 187 | }; 188 | return transformed; 189 | }; 190 | 191 | TxController.prototype.transformInvTransaction = function(transaction) { 192 | var self = this; 193 | 194 | var valueOut = 0; 195 | var vout = []; 196 | for (var i = 0; i < transaction.outputs.length; i++) { 197 | var output = transaction.outputs[i]; 198 | valueOut += output.satoshis; 199 | if (output.script) { 200 | var address = output.script.toAddress(self.node.network); 201 | if (address) { 202 | var obj = {}; 203 | obj[address.toString()] = output.satoshis; 204 | vout.push(obj); 205 | } 206 | } 207 | } 208 | 209 | var isRBF = _.any(_.pluck(transaction.inputs, 'sequenceNumber'), function(seq) { 210 | return seq < MAXINT - 1; 211 | }); 212 | 213 | var transformed = { 214 | txid: transaction.hash, 215 | valueOut: valueOut / 1e8, 216 | vout: vout, 217 | isRBF: isRBF, 218 | }; 219 | 220 | return transformed; 221 | }; 222 | 223 | TxController.prototype.rawTransaction = function(req, res, next) { 224 | var self = this; 225 | var txid = req.params.txid; 226 | 227 | this.node.getTransaction(txid, function(err, transaction) { 228 | if (err && err.code === -5) { 229 | return self.common.handleErrors(null, res); 230 | } else if(err) { 231 | return self.common.handleErrors(err, res); 232 | } 233 | 234 | req.rawTransaction = { 235 | 'rawtx': transaction.toBuffer().toString('hex') 236 | }; 237 | 238 | next(); 239 | }); 240 | }; 241 | 242 | TxController.prototype.showRaw = function(req, res) { 243 | if (req.rawTransaction) { 244 | res.jsonp(req.rawTransaction); 245 | } 246 | }; 247 | 248 | TxController.prototype.list = function(req, res) { 249 | var self = this; 250 | 251 | var blockHash = req.query.block; 252 | var address = req.query.address; 253 | var page = parseInt(req.query.pageNum) || 0; 254 | var pageLength = 10; 255 | var pagesTotal = 1; 256 | 257 | if(blockHash) { 258 | self.node.getBlockOverview(blockHash, function(err, block) { 259 | if(err && err.code === -5) { 260 | return self.common.handleErrors(null, res); 261 | } else if(err) { 262 | return self.common.handleErrors(err, res); 263 | } 264 | 265 | var totalTxs = block.txids.length; 266 | var txids; 267 | 268 | if(!_.isUndefined(page)) { 269 | var start = page * pageLength; 270 | txids = block.txids.slice(start, start + pageLength); 271 | pagesTotal = Math.ceil(totalTxs / pageLength); 272 | } else { 273 | txids = block.txids; 274 | } 275 | 276 | async.mapSeries(txids, function(txid, next) { 277 | self.node.getDetailedTransaction(txid, function(err, transaction) { 278 | if (err) { 279 | return next(err); 280 | } 281 | self.transformTransaction(transaction, next); 282 | }); 283 | }, function(err, transformed) { 284 | if(err) { 285 | return self.common.handleErrors(err, res); 286 | } 287 | 288 | res.jsonp({ 289 | pagesTotal: pagesTotal, 290 | txs: transformed 291 | }); 292 | }); 293 | 294 | }); 295 | } else if(address) { 296 | var options = { 297 | from: page * pageLength, 298 | to: (page + 1) * pageLength 299 | }; 300 | 301 | self.node.getAddressHistory(address, options, function(err, result) { 302 | if(err) { 303 | return self.common.handleErrors(err, res); 304 | } 305 | 306 | var txs = result.items.map(function(info) { 307 | return info.tx; 308 | }).filter(function(value, index, self) { 309 | return self.indexOf(value) === index; 310 | }); 311 | 312 | async.map( 313 | txs, 314 | function(tx, next) { 315 | self.transformTransaction(tx, next); 316 | }, 317 | function(err, transformed) { 318 | if (err) { 319 | return self.common.handleErrors(err, res); 320 | } 321 | res.jsonp({ 322 | pagesTotal: Math.ceil(result.totalCount / pageLength), 323 | txs: transformed 324 | }); 325 | } 326 | ); 327 | }); 328 | } else { 329 | return self.common.handleErrors(new Error('Block hash or address expected'), res); 330 | } 331 | }; 332 | 333 | TxController.prototype.send = function(req, res) { 334 | var self = this; 335 | this.node.sendTransaction(req.body.rawtx, function(err, txid) { 336 | if(err) { 337 | // TODO handle specific errors 338 | return self.common.handleErrors(err, res); 339 | } 340 | 341 | res.json({'txid': txid}); 342 | }); 343 | }; 344 | 345 | module.exports = TxController; 346 | -------------------------------------------------------------------------------- /lib/blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var bitcore = require('bitcore-lib-zcash'); 5 | var _ = bitcore.deps._; 6 | var pools = require('../pools.json'); 7 | var BN = bitcore.crypto.BN; 8 | var LRU = require('lru-cache'); 9 | var Common = require('./common'); 10 | 11 | function BlockController(options) { 12 | var self = this; 13 | this.node = options.node; 14 | 15 | this.blockSummaryCache = LRU(options.blockSummaryCacheSize || BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE); 16 | this.blockCacheConfirmations = 6; 17 | this.blockCache = LRU(options.blockCacheSize || BlockController.DEFAULT_BLOCK_CACHE_SIZE); 18 | 19 | this.poolStrings = {}; 20 | pools.forEach(function(pool) { 21 | pool.searchStrings.forEach(function(s) { 22 | self.poolStrings[s] = { 23 | poolName: pool.poolName, 24 | url: pool.url 25 | }; 26 | }); 27 | }); 28 | 29 | this.common = new Common({log: this.node.log}); 30 | } 31 | 32 | var BLOCK_LIMIT = 200; 33 | 34 | BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE = 1000000; 35 | BlockController.DEFAULT_BLOCK_CACHE_SIZE = 1000; 36 | 37 | function isHexadecimal(hash) { 38 | if (!_.isString(hash)) { 39 | return false; 40 | } 41 | return /^[0-9a-fA-F]+$/.test(hash); 42 | } 43 | 44 | BlockController.prototype.checkBlockHash = function(req, res, next) { 45 | var self = this; 46 | var hash = req.params.blockHash; 47 | if (hash.length < 64 || !isHexadecimal(hash)) { 48 | return self.common.handleErrors(null, res); 49 | } 50 | next(); 51 | }; 52 | 53 | /** 54 | * Find block by hash ... 55 | */ 56 | BlockController.prototype.block = function(req, res, next) { 57 | var self = this; 58 | var hash = req.params.blockHash; 59 | var blockCached = self.blockCache.get(hash); 60 | 61 | if (blockCached) { 62 | blockCached.confirmations = self.node.services.bitcoind.height - blockCached.height + 1; 63 | req.block = blockCached; 64 | next(); 65 | } else { 66 | self.node.getBlock(hash, function(err, block) { 67 | if((err && err.code === -5) || (err && err.code === -8)) { 68 | return self.common.handleErrors(null, res); 69 | } else if(err) { 70 | return self.common.handleErrors(err, res); 71 | } 72 | self.node.services.bitcoind.getBlockHeader(hash, function(err, info) { 73 | if (err) { 74 | return self.common.handleErrors(err, res); 75 | } 76 | var blockResult = self.transformBlock(block, info); 77 | if (blockResult.confirmations >= self.blockCacheConfirmations) { 78 | self.blockCache.set(hash, blockResult); 79 | } 80 | req.block = blockResult; 81 | next(); 82 | }); 83 | }); 84 | } 85 | }; 86 | 87 | /** 88 | * Find rawblock by hash and height... 89 | */ 90 | BlockController.prototype.rawBlock = function(req, res, next) { 91 | var self = this; 92 | var blockHash = req.params.blockHash; 93 | 94 | self.node.getRawBlock(blockHash, function(err, blockBuffer) { 95 | if((err && err.code === -5) || (err && err.code === -8)) { 96 | return self.common.handleErrors(null, res); 97 | } else if(err) { 98 | return self.common.handleErrors(err, res); 99 | } 100 | req.rawBlock = { 101 | rawblock: blockBuffer.toString('hex') 102 | }; 103 | next(); 104 | }); 105 | 106 | }; 107 | 108 | BlockController.prototype._normalizePrevHash = function(hash) { 109 | // TODO fix bitcore to give back null instead of null hash 110 | if (hash !== '0000000000000000000000000000000000000000000000000000000000000000') { 111 | return hash; 112 | } else { 113 | return null; 114 | } 115 | }; 116 | 117 | BlockController.prototype.transformBlock = function(block, info) { 118 | var blockObj = block.toObject(); 119 | var transactionIds = blockObj.transactions.map(function(tx) { 120 | return tx.hash; 121 | }); 122 | return { 123 | hash: block.hash, 124 | size: block.toBuffer().length, 125 | height: info.height, 126 | version: blockObj.header.version, 127 | merkleroot: blockObj.header.merkleRoot, 128 | tx: transactionIds, 129 | time: blockObj.header.time, 130 | nonce: blockObj.header.nonce, 131 | solution: blockObj.header.solution, 132 | bits: blockObj.header.bits.toString(16), 133 | difficulty: block.header.getDifficulty(), 134 | chainwork: info.chainWork, 135 | confirmations: info.confirmations, 136 | previousblockhash: this._normalizePrevHash(blockObj.header.prevHash), 137 | nextblockhash: info.nextHash, 138 | reward: this.getBlockReward(info.height) / 1e8, 139 | isMainChain: (info.confirmations !== -1), 140 | poolInfo: this.getPoolInfo(block) 141 | }; 142 | }; 143 | 144 | /** 145 | * Show block 146 | */ 147 | BlockController.prototype.show = function(req, res) { 148 | if (req.block) { 149 | res.jsonp(req.block); 150 | } 151 | }; 152 | 153 | BlockController.prototype.showRaw = function(req, res) { 154 | if (req.rawBlock) { 155 | res.jsonp(req.rawBlock); 156 | } 157 | }; 158 | 159 | BlockController.prototype.blockIndex = function(req, res) { 160 | var self = this; 161 | var height = req.params.height; 162 | this.node.services.bitcoind.getBlockHeader(parseInt(height), function(err, info) { 163 | if (err) { 164 | return self.common.handleErrors(err, res); 165 | } 166 | res.jsonp({ 167 | blockHash: info.hash 168 | }); 169 | }); 170 | }; 171 | 172 | BlockController.prototype._getBlockSummary = function(hash, moreTimestamp, next) { 173 | var self = this; 174 | 175 | function finish(result) { 176 | if (moreTimestamp > result.time) { 177 | moreTimestamp = result.time; 178 | } 179 | return next(null, result); 180 | } 181 | 182 | var summaryCache = self.blockSummaryCache.get(hash); 183 | 184 | if (summaryCache) { 185 | finish(summaryCache); 186 | } else { 187 | self.node.services.bitcoind.getRawBlock(hash, function(err, blockBuffer) { 188 | if (err) { 189 | return next(err); 190 | } 191 | 192 | var br = new bitcore.encoding.BufferReader(blockBuffer); 193 | 194 | // take a shortcut to get number of transactions and the blocksize. 195 | // Also reads the coinbase transaction and only that. 196 | // Old code parsed all transactions in every block _and_ then encoded 197 | // them all back together to get the binary size of the block. 198 | // FIXME: This code might still read the whole block. Fixing that 199 | // would require changes in bitcore-node. 200 | var header = bitcore.BlockHeader.fromBufferReader(br); 201 | var info = {}; 202 | var txlength = br.readVarintNum(); 203 | info.transactions = [bitcore.Transaction().fromBufferReader(br)]; 204 | 205 | self.node.services.bitcoind.getBlockHeader(hash, function(err, blockHeader) { 206 | if (err) { 207 | return next(err); 208 | } 209 | var height = blockHeader.height; 210 | 211 | var summary = { 212 | height: height, 213 | size: blockBuffer.length, 214 | hash: hash, 215 | time: header.time, 216 | txlength: txlength, 217 | poolInfo: self.getPoolInfo(info) 218 | }; 219 | 220 | var confirmations = self.node.services.bitcoind.height - height + 1; 221 | if (confirmations >= self.blockCacheConfirmations) { 222 | self.blockSummaryCache.set(hash, summary); 223 | } 224 | 225 | finish(summary); 226 | }); 227 | }); 228 | 229 | } 230 | }; 231 | 232 | // List blocks by date 233 | BlockController.prototype.list = function(req, res) { 234 | var self = this; 235 | 236 | var dateStr; 237 | var todayStr = this.formatTimestamp(new Date()); 238 | var isToday; 239 | 240 | if (req.query.blockDate) { 241 | dateStr = req.query.blockDate; 242 | var datePattern = /\d{4}-\d{2}-\d{2}/; 243 | if(!datePattern.test(dateStr)) { 244 | return self.common.handleErrors(new Error('Please use yyyy-mm-dd format'), res); 245 | } 246 | 247 | isToday = dateStr === todayStr; 248 | } else { 249 | dateStr = todayStr; 250 | isToday = true; 251 | } 252 | 253 | var gte = Math.round((new Date(dateStr)).getTime() / 1000); 254 | 255 | //pagination 256 | var lte = parseInt(req.query.startTimestamp) || gte + 86400; 257 | var prev = this.formatTimestamp(new Date((gte - 86400) * 1000)); 258 | var next = lte ? this.formatTimestamp(new Date(lte * 1000)) : null; 259 | var limit = parseInt(req.query.limit || BLOCK_LIMIT); 260 | var more = false; 261 | var moreTimestamp = lte; 262 | 263 | self.node.services.bitcoind.getBlockHashesByTimestamp(lte, gte, function(err, hashes) { 264 | if(err) { 265 | return self.common.handleErrors(err, res); 266 | } 267 | 268 | hashes.reverse(); 269 | 270 | if(hashes.length > limit) { 271 | more = true; 272 | hashes = hashes.slice(0, limit); 273 | } 274 | 275 | async.mapSeries( 276 | hashes, 277 | function(hash, next) { 278 | self._getBlockSummary(hash, moreTimestamp, next); 279 | }, 280 | function(err, blocks) { 281 | if(err) { 282 | return self.common.handleErrors(err, res); 283 | } 284 | 285 | blocks.sort(function(a, b) { 286 | return b.height - a.height; 287 | }); 288 | 289 | var data = { 290 | blocks: blocks, 291 | length: blocks.length, 292 | pagination: { 293 | next: next, 294 | prev: prev, 295 | currentTs: lte - 1, 296 | current: dateStr, 297 | isToday: isToday, 298 | more: more 299 | } 300 | }; 301 | 302 | if(more) { 303 | data.pagination.moreTs = moreTimestamp; 304 | } 305 | 306 | res.jsonp(data); 307 | } 308 | ); 309 | }); 310 | }; 311 | 312 | BlockController.prototype.getPoolInfo = function(block) { 313 | var coinbaseBuffer = block.transactions[0].inputs[0]._scriptBuffer; 314 | 315 | for(var k in this.poolStrings) { 316 | if (coinbaseBuffer.toString('utf-8').match(k)) { 317 | return this.poolStrings[k]; 318 | } 319 | } 320 | 321 | return {}; 322 | }; 323 | 324 | //helper to convert timestamps to yyyy-mm-dd format 325 | BlockController.prototype.formatTimestamp = function(date) { 326 | var yyyy = date.getUTCFullYear().toString(); 327 | var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based 328 | var dd = date.getUTCDate().toString(); 329 | 330 | return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding 331 | }; 332 | 333 | BlockController.prototype.getBlockReward = function(height) { 334 | var subsidy = new BN(12.5 * 1e8); 335 | 336 | // Mining slow start 337 | // The subsidy is ramped up linearly, skipping the middle payout of 338 | // MAX_SUBSIDY/2 to keep the monetary curve consistent with no slow start. 339 | if (height < (20000 / 2)) { 340 | subsidy /= 20000; 341 | subsidy *= height; 342 | return subsidy; 343 | } else if (height < 20000) { 344 | subsidy /= 20000; 345 | subsidy *= (height+1); 346 | return subsidy; 347 | } 348 | 349 | var halvings = Math.floor((height - (20000/2)) / 840000); 350 | // Force block reward to zero when right shift is undefined. 351 | if (halvings >= 64) { 352 | return 0; 353 | } 354 | 355 | // Subsidy is cut in half every 840,000 blocks which will occur approximately every 4 years. 356 | subsidy = subsidy.shrn(halvings); 357 | 358 | return parseInt(subsidy.toString(10)); 359 | }; 360 | 361 | module.exports = BlockController; 362 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Writable = require('stream').Writable; 4 | var bodyParser = require('body-parser'); 5 | var compression = require('compression'); 6 | var BaseService = require('./service'); 7 | var inherits = require('util').inherits; 8 | var BlockController = require('./blocks'); 9 | var TxController = require('./transactions'); 10 | var AddressController = require('./addresses'); 11 | var ChartController = require('./charts'); 12 | var StatusController = require('./status'); 13 | var MessagesController = require('./messages'); 14 | var UtilsController = require('./utils'); 15 | var CurrencyController = require('./currency'); 16 | var RateLimiter = require('./ratelimiter'); 17 | var morgan = require('morgan'); 18 | var bitcore = require('bitcore-lib-zcash'); 19 | var _ = bitcore.deps._; 20 | var $ = bitcore.util.preconditions; 21 | var Transaction = bitcore.Transaction; 22 | var EventEmitter = require('events').EventEmitter; 23 | 24 | /** 25 | * A service for Bitcore to enable HTTP routes to query information about the blockchain. 26 | * 27 | * @param {Object} options 28 | * @param {Boolean} options.enableCache - This will enable cache-control headers 29 | * @param {Number} options.cacheShortSeconds - The time to cache short lived cache responses. 30 | * @param {Number} options.cacheLongSeconds - The time to cache long lived cache responses. 31 | * @param {String} options.routePrefix - The URL route prefix 32 | */ 33 | var InsightAPI = function(options) { 34 | BaseService.call(this, options); 35 | 36 | // in minutes 37 | this.currencyRefresh = options.currencyRefresh || CurrencyController.DEFAULT_CURRENCY_DELAY; 38 | 39 | this.subscriptions = { 40 | inv: [] 41 | }; 42 | 43 | if (!_.isUndefined(options.enableCache)) { 44 | $.checkArgument(_.isBoolean(options.enableCache)); 45 | this.enableCache = options.enableCache; 46 | } 47 | this.cacheShortSeconds = options.cacheShortSeconds; 48 | this.cacheLongSeconds = options.cacheLongSeconds; 49 | 50 | this.rateLimiterOptions = options.rateLimiterOptions; 51 | this.disableRateLimiter = options.disableRateLimiter; 52 | 53 | this.blockSummaryCacheSize = options.blockSummaryCacheSize || BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE; 54 | this.blockCacheSize = options.blockCacheSize || BlockController.DEFAULT_BLOCK_CACHE_SIZE; 55 | 56 | if (!_.isUndefined(options.routePrefix)) { 57 | this.routePrefix = options.routePrefix; 58 | } else { 59 | this.routePrefix = this.name; 60 | } 61 | 62 | this.txController = new TxController(this.node); 63 | }; 64 | 65 | InsightAPI.dependencies = ['bitcoind', 'web']; 66 | 67 | inherits(InsightAPI, BaseService); 68 | 69 | InsightAPI.prototype.cache = function(maxAge) { 70 | var self = this; 71 | return function(req, res, next) { 72 | if (self.enableCache) { 73 | res.header('Cache-Control', 'public, max-age=' + maxAge); 74 | } 75 | next(); 76 | }; 77 | }; 78 | 79 | InsightAPI.prototype.cacheShort = function() { 80 | var seconds = this.cacheShortSeconds || 30; // thirty seconds 81 | return this.cache(seconds); 82 | }; 83 | 84 | InsightAPI.prototype.cacheLong = function() { 85 | var seconds = this.cacheLongSeconds || 86400; // one day 86 | return this.cache(seconds); 87 | }; 88 | 89 | InsightAPI.prototype.getRoutePrefix = function() { 90 | return this.routePrefix; 91 | }; 92 | 93 | InsightAPI.prototype.start = function(callback) { 94 | this.node.services.bitcoind.on('tx', this.transactionEventHandler.bind(this)); 95 | this.node.services.bitcoind.on('block', this.blockEventHandler.bind(this)); 96 | setImmediate(callback); 97 | }; 98 | 99 | InsightAPI.prototype.createLogInfoStream = function() { 100 | var self = this; 101 | 102 | function Log(options) { 103 | Writable.call(this, options); 104 | } 105 | inherits(Log, Writable); 106 | 107 | Log.prototype._write = function (chunk, enc, callback) { 108 | self.node.log.info(chunk.slice(0, chunk.length - 1)); // remove new line and pass to logger 109 | callback(); 110 | }; 111 | var stream = new Log(); 112 | 113 | return stream; 114 | }; 115 | 116 | InsightAPI.prototype.getRemoteAddress = function(req) { 117 | if (req.headers['cf-connecting-ip']) { 118 | return req.headers['cf-connecting-ip']; 119 | } 120 | return req.socket.remoteAddress; 121 | }; 122 | 123 | InsightAPI.prototype._getRateLimiter = function() { 124 | var rateLimiterOptions = _.isUndefined(this.rateLimiterOptions) ? {} : _.clone(this.rateLimiterOptions); 125 | rateLimiterOptions.node = this.node; 126 | var limiter = new RateLimiter(rateLimiterOptions); 127 | return limiter; 128 | }; 129 | 130 | InsightAPI.prototype.setupRoutes = function(app) { 131 | 132 | var self = this; 133 | 134 | //Enable rate limiter 135 | if (!this.disableRateLimiter) { 136 | var limiter = this._getRateLimiter(); 137 | app.use(limiter.middleware()); 138 | } 139 | 140 | //Setup logging 141 | morgan.token('remote-forward-addr', function(req){ 142 | return self.getRemoteAddress(req); 143 | }); 144 | var logFormat = ':remote-forward-addr ":method :url" :status :res[content-length] :response-time ":user-agent" '; 145 | var logStream = this.createLogInfoStream(); 146 | app.use(morgan(logFormat, {stream: logStream})); 147 | 148 | //Enable compression 149 | app.use(compression()); 150 | 151 | //Enable urlencoded data 152 | app.use(bodyParser.urlencoded({extended: true})); 153 | 154 | //Enable CORS 155 | app.use(function(req, res, next) { 156 | 157 | res.header('Access-Control-Allow-Origin', '*'); 158 | res.header('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, OPTIONS'); 159 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, cf-connecting-ip'); 160 | 161 | var method = req.method && req.method.toUpperCase && req.method.toUpperCase(); 162 | 163 | if (method === 'OPTIONS') { 164 | res.statusCode = 204; 165 | res.end(); 166 | } else { 167 | next(); 168 | } 169 | }); 170 | 171 | //Block routes 172 | var blockOptions = { 173 | node: this.node, 174 | blockSummaryCacheSize: this.blockSummaryCacheSize, 175 | blockCacheSize: this.blockCacheSize 176 | }; 177 | var blocks = new BlockController(blockOptions); 178 | app.get('/blocks', this.cacheShort(), blocks.list.bind(blocks)); 179 | 180 | app.get('/block/:blockHash', this.cacheShort(), blocks.checkBlockHash.bind(blocks), blocks.show.bind(blocks)); 181 | app.param('blockHash', blocks.block.bind(blocks)); 182 | 183 | app.get('/rawblock/:blockHash', this.cacheLong(), blocks.checkBlockHash.bind(blocks), blocks.showRaw.bind(blocks)); 184 | app.param('blockHash', blocks.rawBlock.bind(blocks)); 185 | 186 | app.get('/block-index/:height', this.cacheShort(), blocks.blockIndex.bind(blocks)); 187 | app.param('height', blocks.blockIndex.bind(blocks)); 188 | 189 | // Transaction routes 190 | var transactions = new TxController(this.node); 191 | app.get('/tx/:txid', this.cacheShort(), transactions.show.bind(transactions)); 192 | app.param('txid', transactions.transaction.bind(transactions)); 193 | app.get('/txs', this.cacheShort(), transactions.list.bind(transactions)); 194 | app.post('/tx/send', transactions.send.bind(transactions)); 195 | 196 | // Raw Routes 197 | app.get('/rawtx/:txid', this.cacheLong(), transactions.showRaw.bind(transactions)); 198 | app.param('txid', transactions.rawTransaction.bind(transactions)); 199 | 200 | // Address routes 201 | var addresses = new AddressController(this.node); 202 | app.get('/addr/:addr', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.show.bind(addresses)); 203 | app.get('/addr/:addr/utxo', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.utxo.bind(addresses)); 204 | app.get('/addrs/:addrs/utxo', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); 205 | app.post('/addrs/utxo', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); 206 | app.get('/addrs/:addrs/txs', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); 207 | app.post('/addrs/txs', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); 208 | 209 | // Address property routes 210 | app.get('/addr/:addr/balance', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.balance.bind(addresses)); 211 | app.get('/addr/:addr/totalReceived', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.totalReceived.bind(addresses)); 212 | app.get('/addr/:addr/totalSent', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.totalSent.bind(addresses)); 213 | app.get('/addr/:addr/unconfirmedBalance', this.cacheShort(), addresses.checkAddr.bind(addresses), addresses.unconfirmedBalance.bind(addresses)); 214 | 215 | // Chart routes 216 | var chartOptions = { 217 | node: this.node, 218 | blocks: blocks 219 | }; 220 | var charts = new ChartController(chartOptions); 221 | app.get('/charts', this.cacheShort(), charts.list.bind(charts)); 222 | app.get('/chart/:chartType', this.cacheShort(), charts.show.bind(charts)); 223 | app.param('chartType', charts.chart.bind(charts)); 224 | 225 | // Status route 226 | var status = new StatusController(this.node); 227 | app.get('/status', this.cacheShort(), status.show.bind(status)); 228 | app.get('/sync', this.cacheShort(), status.sync.bind(status)); 229 | app.get('/peer', this.cacheShort(), status.peer.bind(status)); 230 | app.get('/version', this.cacheShort(), status.version.bind(status)); 231 | 232 | // Address routes 233 | var messages = new MessagesController(this.node); 234 | app.get('/messages/verify', messages.verify.bind(messages)); 235 | app.post('/messages/verify', messages.verify.bind(messages)); 236 | 237 | // Utils route 238 | var utils = new UtilsController(this.node); 239 | app.get('/utils/estimatefee', utils.estimateFee.bind(utils)); 240 | 241 | // Currency 242 | var currency = new CurrencyController({ 243 | node: this.node, 244 | currencyRefresh: this.currencyRefresh 245 | }); 246 | app.get('/currency', currency.index.bind(currency)); 247 | 248 | // Not Found 249 | app.use(function(req, res) { 250 | res.status(404).jsonp({ 251 | status: 404, 252 | url: req.originalUrl, 253 | error: 'Not found' 254 | }); 255 | }); 256 | 257 | }; 258 | 259 | InsightAPI.prototype.getPublishEvents = function() { 260 | return [ 261 | { 262 | name: 'inv', 263 | scope: this, 264 | subscribe: this.subscribe.bind(this), 265 | unsubscribe: this.unsubscribe.bind(this), 266 | extraEvents: ['tx', 'block'] 267 | } 268 | ]; 269 | }; 270 | 271 | InsightAPI.prototype.blockEventHandler = function(hashBuffer) { 272 | // Notify inv subscribers 273 | for (var i = 0; i < this.subscriptions.inv.length; i++) { 274 | this.subscriptions.inv[i].emit('block', hashBuffer.toString('hex')); 275 | } 276 | }; 277 | InsightAPI.prototype.transactionEventHandler = function(txBuffer) { 278 | var tx = new Transaction().fromBuffer(txBuffer); 279 | var result = this.txController.transformInvTransaction(tx); 280 | 281 | for (var i = 0; i < this.subscriptions.inv.length; i++) { 282 | this.subscriptions.inv[i].emit('tx', result); 283 | } 284 | }; 285 | 286 | InsightAPI.prototype.subscribe = function(emitter) { 287 | $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); 288 | 289 | var emitters = this.subscriptions.inv; 290 | var index = emitters.indexOf(emitter); 291 | if(index === -1) { 292 | emitters.push(emitter); 293 | } 294 | }; 295 | 296 | InsightAPI.prototype.unsubscribe = function(emitter) { 297 | $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); 298 | 299 | var emitters = this.subscriptions.inv; 300 | var index = emitters.indexOf(emitter); 301 | if(index > -1) { 302 | emitters.splice(index, 1); 303 | } 304 | }; 305 | 306 | module.exports = InsightAPI; 307 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Insight API 2 | 3 | A Zcash blockchain REST and web socket API service for [Bitcore Node](https://github.com/bitpay/bitcore-node). 4 | 5 | This is a backend-only service. If you're looking for the web frontend application, take a look at https://github.com/bitpay/insight-ui. 6 | 7 | ## Getting Started 8 | 9 | ```bashl 10 | npm install -g bitcore-node@latest 11 | bitcore-node create mynode 12 | cd mynode 13 | bitcore-node install insight-api-zcash 14 | bitcore-node start 15 | ``` 16 | 17 | The API endpoints will be available by default at: `http://localhost:3001/insight-api/` 18 | 19 | ## Prerequisites 20 | 21 | - [Bitcore Node 3.x](https://github.com/bitpay/bitcore-node) 22 | 23 | **Note:** You can use an existing Zcash data directory, however `txindex`, `addressindex`, `timestampindex` and `spentindex` needs to be set to true in `zcash.conf`, as well as a few other additional fields. 24 | 25 | ## Notes on Upgrading from v0.3 26 | 27 | The unspent outputs format now has `satoshis` and `height`: 28 | ``` 29 | [ 30 | { 31 | "address":"mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", 32 | "txid":"d5f8a96faccf79d4c087fa217627bb1120e83f8ea1a7d84b1de4277ead9bbac1", 33 | "vout":0, 34 | "scriptPubKey":"76a91453c0307d6851aa0ce7825ba883c6bd9ad242b48688ac", 35 | "amount":0.000006, 36 | "satoshis":600, 37 | "confirmations":0, 38 | "ts":1461349425 39 | }, 40 | { 41 | "address": "mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", 42 | "txid": "bc9df3b92120feaee4edc80963d8ed59d6a78ea0defef3ec3cb374f2015bfc6e", 43 | "vout": 1, 44 | "scriptPubKey": "76a91453c0307d6851aa0ce7825ba883c6bd9ad242b48688ac", 45 | "amount": 0.12345678, 46 | "satoshis: 12345678, 47 | "confirmations": 1, 48 | "height": 300001 49 | } 50 | ] 51 | ``` 52 | The `timestamp` property will only be set for unconfirmed transactions and `height` can be used for determining block order. The `confirmationsFromCache` is nolonger set or necessary, confirmation count is only cached for the time between blocks. 53 | 54 | There is a new `GET` endpoint or raw blocks at `/rawblock/`: 55 | 56 | Response format: 57 | ``` 58 | { 59 | "rawblock": "blockhexstring..." 60 | } 61 | ``` 62 | 63 | There are a few changes to the `GET` endpoint for `/addr/[:address]`: 64 | 65 | - The list of txids in an address summary does not include orphaned transactions 66 | - The txids will be sorted in block order 67 | - The list of txids will be limited at 1000 txids 68 | - There are two new query options "from" and "to" for pagination of the txids (e.g. `/addr/[:address]?from=1000&to=2000`) 69 | 70 | Some additional general notes: 71 | - The transaction history for an address will be sorted in block order 72 | - The response for the `/sync` endpoint does not include `startTs` and `endTs` as the sync is no longer relevant as indexes are built in zcashd. 73 | - The endpoint for `/peer` is no longer relevant connection to zcashd is via ZMQ. 74 | - `/tx` endpoint results will now include block height, and spentTx related fields will be set to `null` if unspent. 75 | - `/block` endpoint results does not include `confirmations` and will include `poolInfo`. 76 | 77 | ## Notes on Upgrading from v0.2 78 | 79 | Some of the fields and methods are not supported: 80 | 81 | The `/tx/` endpoint JSON response will not include the following fields on the "vin" 82 | object: 83 | - `doubleSpentTxId` // double spends are not currently tracked 84 | - `isConfirmed` // confirmation of the previous output 85 | - `confirmations` // confirmations of the previous output 86 | - `unconfirmedInput` 87 | 88 | The `/tx/` endpoint JSON response will not include the following fields on the "vout" 89 | object. 90 | - `spentTs` 91 | 92 | The `/status?q=getTxOutSetInfo` method has also been removed due to the query being very slow and locking zcashd. 93 | 94 | Plug-in support for Insight API is also no longer available, as well as the endpoints: 95 | - `/email/retrieve` 96 | - `/rates/:code` 97 | 98 | Caching support has not yet been added in the v0.3 upgrade. 99 | 100 | ## Query Rate Limit 101 | 102 | To protect the server, insight-api has a built it query rate limiter. It can be configurable in `bitcore-node.json` with: 103 | ``` json 104 | "servicesConfig": { 105 | "insight-api": { 106 | "rateLimiterOptions": { 107 | "whitelist": ["::ffff:127.0.0.1"] 108 | } 109 | } 110 | } 111 | ``` 112 | With all the configuration options available: https://github.com/bitpay/insight-api/blob/master/lib/ratelimiter.js#L10-17 113 | 114 | Or disabled entirely with: 115 | ``` json 116 | "servicesConfig": { 117 | "insight-api": { 118 | "disableRateLimiter": true 119 | } 120 | } 121 | ``` 122 | 123 | 124 | ## API HTTP Endpoints 125 | 126 | ### Block 127 | ``` 128 | /insight-api/block/[:hash] 129 | /insight-api/block/00000000a967199a2fad0877433c93df785a8d8ce062e5f9b451cd1397bdbf62 130 | ``` 131 | 132 | ### Block Index 133 | Get block hash by height 134 | ``` 135 | /insight-api/block-index/[:height] 136 | /insight-api/block-index/0 137 | ``` 138 | This would return: 139 | ``` 140 | { 141 | "blockHash":"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 142 | } 143 | ``` 144 | which is the hash of the Genesis block (0 height) 145 | 146 | 147 | ### Raw Block 148 | ``` 149 | /insight-api/rawblock/[:blockHash] 150 | /insight-api/rawblock/[:blockHeight] 151 | ``` 152 | 153 | This would return: 154 | ``` 155 | { 156 | "rawblock":"blockhexstring..." 157 | } 158 | ``` 159 | 160 | ### Block Summaries 161 | 162 | Get block summaries by date: 163 | ``` 164 | /insight-api/blocks?limit=3&blockDate=2016-04-22 165 | ``` 166 | 167 | Example response: 168 | ``` 169 | { 170 | "blocks": [ 171 | { 172 | "height": 408495, 173 | "size": 989237, 174 | "hash": "00000000000000000108a1f4d4db839702d72f16561b1154600a26c453ecb378", 175 | "time": 1461360083, 176 | "txlength": 1695, 177 | "poolInfo": { 178 | "poolName": "BTCC Pool", 179 | "url": "https://pool.btcc.com/" 180 | } 181 | } 182 | ], 183 | "length": 1, 184 | "pagination": { 185 | "next": "2016-04-23", 186 | "prev": "2016-04-21", 187 | "currentTs": 1461369599, 188 | "current": "2016-04-22", 189 | "isToday": true, 190 | "more": true, 191 | "moreTs": 1461369600 192 | } 193 | } 194 | ``` 195 | 196 | ### Transaction 197 | ``` 198 | /insight-api/tx/[:txid] 199 | /insight-api/tx/525de308971eabd941b139f46c7198b5af9479325c2395db7f2fb5ae8562556c 200 | /insight-api/rawtx/[:rawid] 201 | /insight-api/rawtx/525de308971eabd941b139f46c7198b5af9479325c2395db7f2fb5ae8562556c 202 | ``` 203 | 204 | ### Address 205 | ``` 206 | /insight-api/addr/[:addr][?noTxList=1][&from=&to=] 207 | /insight-api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5?noTxList=1 208 | /insight-api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5?from=1000&to=2000 209 | ``` 210 | 211 | ### Address Properties 212 | ``` 213 | /insight-api/addr/[:addr]/balance 214 | /insight-api/addr/[:addr]/totalReceived 215 | /insight-api/addr/[:addr]/totalSent 216 | /insight-api/addr/[:addr]/unconfirmedBalance 217 | ``` 218 | The response contains the value in Satoshis. 219 | 220 | ### Unspent Outputs 221 | ``` 222 | /insight-api/addr/[:addr]/utxo 223 | ``` 224 | Sample return: 225 | ``` 226 | [ 227 | { 228 | "address":"mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", 229 | "txid":"d5f8a96faccf79d4c087fa217627bb1120e83f8ea1a7d84b1de4277ead9bbac1", 230 | "vout":0, 231 | "scriptPubKey":"76a91453c0307d6851aa0ce7825ba883c6bd9ad242b48688ac", 232 | "amount":0.000006, 233 | "satoshis":600, 234 | "confirmations":0, 235 | "ts":1461349425 236 | }, 237 | { 238 | "address": "mo9ncXisMeAoXwqcV5EWuyncbmCcQN4rVs", 239 | "txid": "bc9df3b92120feaee4edc80963d8ed59d6a78ea0defef3ec3cb374f2015bfc6e", 240 | "vout": 1, 241 | "scriptPubKey": "76a91453c0307d6851aa0ce7825ba883c6bd9ad242b48688ac", 242 | "amount": 0.12345678, 243 | "satoshis: 12345678, 244 | "confirmations": 1, 245 | "height": 300001 246 | } 247 | ] 248 | ``` 249 | 250 | ### Unspent Outputs for Multiple Addresses 251 | GET method: 252 | ``` 253 | /insight-api/addrs/[:addrs]/utxo 254 | /insight-api/addrs/2NF2baYuJAkCKo5onjUKEPdARQkZ6SYyKd5,2NAre8sX2povnjy4aeiHKeEh97Qhn97tB1f/utxo 255 | ``` 256 | 257 | POST method: 258 | ``` 259 | /insight-api/addrs/utxo 260 | ``` 261 | 262 | POST params: 263 | ``` 264 | addrs: 2NF2baYuJAkCKo5onjUKEPdARQkZ6SYyKd5,2NAre8sX2povnjy4aeiHKeEh97Qhn97tB1f 265 | ``` 266 | 267 | ### Transactions by Block 268 | ``` 269 | /insight-api/txs/?block=HASH 270 | /insight-api/txs/?block=00000000fa6cf7367e50ad14eb0ca4737131f256fc4c5841fd3c3f140140e6b6 271 | ``` 272 | ### Transactions by Address 273 | ``` 274 | /insight-api/txs/?address=ADDR 275 | /insight-api/txs/?address=mmhmMNfBiZZ37g1tgg2t8DDbNoEdqKVxAL 276 | ``` 277 | 278 | ### Transactions for Multiple Addresses 279 | GET method: 280 | ``` 281 | /insight-api/addrs/[:addrs]/txs[?from=&to=] 282 | /insight-api/addrs/2NF2baYuJAkCKo5onjUKEPdARQkZ6SYyKd5,2NAre8sX2povnjy4aeiHKeEh97Qhn97tB1f/txs?from=0&to=20 283 | ``` 284 | 285 | POST method: 286 | ``` 287 | /insight-api/addrs/txs 288 | ``` 289 | 290 | POST params: 291 | ``` 292 | addrs: 2NF2baYuJAkCKo5onjUKEPdARQkZ6SYyKd5,2NAre8sX2povnjy4aeiHKeEh97Qhn97tB1f 293 | from (optional): 0 294 | to (optional): 20 295 | noAsm (optional): 1 (will omit script asm from results) 296 | noScriptSig (optional): 1 (will omit the scriptSig from all inputs) 297 | noSpent (option): 1 (will omit spent information per output) 298 | ``` 299 | 300 | Sample output: 301 | ``` 302 | { totalItems: 100, 303 | from: 0, 304 | to: 20, 305 | items: 306 | [ { txid: '3e81723d069b12983b2ef694c9782d32fca26cc978de744acbc32c3d3496e915', 307 | version: 1, 308 | locktime: 0, 309 | vin: [Object], 310 | vout: [Object], 311 | blockhash: '00000000011a135e5277f5493c52c66829792392632b8b65429cf07ad3c47a6c', 312 | confirmations: 109367, 313 | time: 1393659685, 314 | blocktime: 1393659685, 315 | valueOut: 0.3453, 316 | size: 225, 317 | firstSeenTs: undefined, 318 | valueIn: 0.3454, 319 | fees: 0.0001 }, 320 | { ... }, 321 | { ... }, 322 | ... 323 | { ... } 324 | ] 325 | } 326 | ``` 327 | 328 | Note: if pagination params are not specified, the result is an array of transactions. 329 | 330 | ### Transaction Broadcasting 331 | POST method: 332 | ``` 333 | /insight-api/tx/send 334 | ``` 335 | POST params: 336 | ``` 337 | rawtx: "signed transaction as hex string" 338 | 339 | eg 340 | 341 | rawtx: 01000000017b1eabe0209b1fe794124575ef807057c77ada2138ae4fa8d6c4de0398a14f3f00000000494830450221008949f0cb400094ad2b5eb399d59d01c14d73d8fe6e96df1a7150deb388ab8935022079656090d7f6bac4c9a94e0aad311a4268e082a725f8aeae0573fb12ff866a5f01ffffffff01f0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000 342 | 343 | ``` 344 | POST response: 345 | ``` 346 | { 347 | txid: [:txid] 348 | } 349 | 350 | eg 351 | 352 | { 353 | txid: "c7736a0a0046d5a8cc61c8c3c2821d4d7517f5de2bc66a966011aaa79965ffba" 354 | } 355 | ``` 356 | 357 | ### Historic Blockchain Data Sync Status 358 | ``` 359 | /insight-api/sync 360 | ``` 361 | 362 | ### Live Network P2P Data Sync Status 363 | ``` 364 | /insight-api/peer 365 | ``` 366 | 367 | ### Status of the Zcash Network 368 | ``` 369 | /insight-api/status?q=xxx 370 | ``` 371 | 372 | Where "xxx" can be: 373 | 374 | * getInfo 375 | * getDifficulty 376 | * getBestBlockHash 377 | * getLastBlockHash 378 | 379 | 380 | ### Utility Methods 381 | ``` 382 | /insight-api/utils/estimatefee[?nbBlocks=2] 383 | ``` 384 | 385 | 386 | ## Web Socket API 387 | The web socket API is served using [socket.io](http://socket.io). 388 | 389 | The following are the events published by insight: 390 | 391 | `tx`: new transaction received from network. This event is published in the 'inv' room. Data will be a app/models/Transaction object. 392 | Sample output: 393 | ``` 394 | { 395 | "txid":"00c1b1acb310b87085c7deaaeba478cef5dc9519fab87a4d943ecbb39bd5b053", 396 | "processed":false 397 | ... 398 | } 399 | ``` 400 | 401 | 402 | `block`: new block received from network. This event is published in the `inv` room. Data will be a app/models/Block object. 403 | Sample output: 404 | ``` 405 | { 406 | "hash":"000000004a3d187c430cd6a5e988aca3b19e1f1d1727a50dead6c8ac26899b96", 407 | "time":1389789343, 408 | ... 409 | } 410 | ``` 411 | 412 | ``: new transaction concerning received from network. This event is published in the `` room. 413 | 414 | `status`: every 1% increment on the sync task, this event will be triggered. This event is published in the `sync` room. 415 | 416 | Sample output: 417 | ``` 418 | { 419 | blocksToSync: 164141, 420 | syncedBlocks: 475, 421 | upToExisting: true, 422 | scanningBackward: true, 423 | isEndGenesis: true, 424 | end: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", 425 | isStartGenesis: false, 426 | start: "000000009f929800556a8f3cfdbe57c187f2f679e351b12f7011bfc276c41b6d" 427 | } 428 | ``` 429 | 430 | ### Example Usage 431 | 432 | The following html page connects to the socket.io insight API and listens for new transactions. 433 | 434 | html 435 | ``` 436 | 437 | 438 | 439 | 452 | 453 | 454 | ``` 455 | 456 | ## License 457 | (The MIT License) 458 | 459 | Permission is hereby granted, free of charge, to any person obtaining 460 | a copy of this software and associated documentation files (the 461 | 'Software'), to deal in the Software without restriction, including 462 | without limitation the rights to use, copy, modify, merge, publish, 463 | distribute, sublicense, and/or sell copies of the Software, and to 464 | permit persons to whom the Software is furnished to do so, subject to 465 | the following conditions: 466 | 467 | The above copyright notice and this permission notice shall be 468 | included in all copies or substantial portions of the Software. 469 | 470 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 471 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 472 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 473 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 474 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 475 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 476 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 477 | -------------------------------------------------------------------------------- /test/addresses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var sinon = require('sinon'); 3 | var should = require('should'); 4 | var AddressController = require('../lib/addresses'); 5 | var _ = require('lodash'); 6 | var bitcore = require('bitcore-lib-zcash'); 7 | 8 | var txinfos = { 9 | totalCount: 2, 10 | items: [ 11 | { 12 | 'address': 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 13 | 'satoshis': 2782729129, 14 | 'height': 534105, 15 | 'confirmations': 123, 16 | 'timestamp': 1441068774, 17 | 'fees': 35436, 18 | 'outputIndexes': [ 19 | 0 20 | ], 21 | 'inputIndexes': [], 22 | 'tx': { 23 | 'hash': 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 24 | 'version': 1, 25 | 'inputs': [ 26 | { 27 | 'prevTxId': 'ea5e5bafbf29cdf6f6097ab344128477e67889d4d6203cb43594836daa6cc425', 28 | 'outputIndex': 1, 29 | 'sequenceNumber': 4294967294, 30 | 'script': '483045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a6012103acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6', 31 | 'scriptString': '72 0x3045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a601 33 0x03acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6', 32 | 'output': { 33 | 'satoshis': 2796764565, 34 | 'script': '76a91488b1fe8aec5ae4358a11447a2f22b2781faedb9b88ac' 35 | } 36 | } 37 | ], 38 | 'outputs': [ 39 | { 40 | 'satoshis': 2782729129, 41 | 'script': '76a9143583efb5e64a4668c6c54bb5fcc30af4417b4f2d88ac' 42 | }, 43 | { 44 | 'satoshis': 14000000, 45 | 'script': '76a9149713201957f42379e574d7c70d506ee49c2c8ad688ac' 46 | } 47 | ], 48 | 'nLockTime': 534089 49 | } 50 | }, 51 | { 52 | 'address': 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 53 | 'satoshis': -2782729129, 54 | 'height': 534110, 55 | 'confirmations': 118, 56 | 'timestamp': 1441072817, 57 | 'fees': 35437, 58 | 'outputIndexes': [], 59 | 'inputIndexes': [ 60 | '0' 61 | ], 62 | 'tx': { 63 | 'hash': '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3', 64 | 'version': 1, 65 | 'inputs': [ 66 | { 67 | 'prevTxId': 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 68 | 'outputIndex': 0, 69 | 'sequenceNumber': 4294967294, 70 | 'script': '47304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d5640121034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24', 71 | 'scriptString': '71 0x304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d56401 33 0x034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24', 72 | 'output': { 73 | 'satoshis': 2782729129, 74 | 'script': '76a9143583efb5e64a4668c6c54bb5fcc30af4417b4f2d88ac' 75 | } 76 | } 77 | ], 78 | 'outputs': [ 79 | { 80 | 'satoshis': 2764693692, 81 | 'script': '76a91456e446bc3489543d8324c6d0271524c0bd0506dd88ac' 82 | }, 83 | { 84 | 'satoshis': 18000000, 85 | 'script': '76a914011d2963b619186a318f768dddfd98cd553912a088ac' 86 | } 87 | ], 88 | 'nLockTime': 534099 89 | } 90 | } 91 | ] 92 | }; 93 | 94 | var tx = { 95 | height: 534181, 96 | blockTimestamp: 1441116143, 97 | blockHash: '0000000000000041ddc94ecf4f86a456a83b2e320c36c6f0c13ff92c7e75f013', 98 | hex: '0100000002f379708395d0a0357514205a3758a0317926428356e54a09089852fc6f7297ea010000008a473044022054233934268b30be779fad874ef42e8db928ba27a1b612d5f111b3ee95eb271c022024272bbaf2dcc4050bd3b9dfa3c93884f6ba6ad7d257598b8245abb65b5ab1e40141040682fdb281a8533e21e13dfd1fcfa424912a85b6cdc4136b5842c85de05ac1f0e4a013f20702adeb53329de13b2ef388e5ed6244676f4f1ee4ee685ab607964dffffffffb758ffd4c31693d9620f326385404530a079d5e60a90b94e46d3c2dbc29c0a98020000008a473044022044938ac3f8fcb8da29011df6397ed28cc7e894cdc35d596d4f3623bd8c7e465f022014829c6e0bd7ee97a1bcfef6b85c5fd232653f289394fc6ce6ebb41c73403f1b014104d9ccf88efc6e5be3151fae5e848efd94c91d75e7bf621f9f724a8caff51415338525d3239fae6b93826edf759dd562f77693e55dfa852ffd96a92d683db590f2ffffffff03605b0300000000001976a914b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d88ac40992d03000000001976a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac256c0400000000001976a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac00000000', 99 | hash: '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 100 | version: 1, 101 | inputSatoshis: 53839829, 102 | outputSatoshis: 53829829, 103 | feeSatoshis: 10000, 104 | inputs: [ 105 | { 106 | address: 'moFfnRwt77pApKnnU6m5uocFaa43aAYpt5', 107 | prevTxId: 'ea97726ffc529808094ae5568342267931a058375a20147535a0d095837079f3', 108 | outputIndex: 1, 109 | sequence: 4294967295, 110 | script: '473044022054233934268b30be779fad874ef42e8db928ba27a1b612d5f111b3ee95eb271c022024272bbaf2dcc4050bd3b9dfa3c93884f6ba6ad7d257598b8245abb65b5ab1e40141040682fdb281a8533e21e13dfd1fcfa424912a85b6cdc4136b5842c85de05ac1f0e4a013f20702adeb53329de13b2ef388e5ed6244676f4f1ee4ee685ab607964d', 111 | scriptAsm: '3044022054233934268b30be779fad874ef42e8db928ba27a1b612d5f111b3ee95eb271c022024272bbaf2dcc4050bd3b9dfa3c93884f6ba6ad7d257598b8245abb65b5ab1e401 040682fdb281a8533e21e13dfd1fcfa424912a85b6cdc4136b5842c85de05ac1f0e4a013f20702adeb53329de13b2ef388e5ed6244676f4f1ee4ee685ab607964d', 112 | satoshis: 53540000, 113 | }, 114 | { 115 | address: 'n1XJBAyU4hNR4xRtY3UxnmAteoJX83p5qv', 116 | prevTxId: '980a9cc2dbc2d3464eb9900ae6d579a03045408563320f62d99316c3d4ff58b7', 117 | outputIndex: 2, 118 | sequence: 4294967295, 119 | script: '473044022044938ac3f8fcb8da29011df6397ed28cc7e894cdc35d596d4f3623bd8c7e465f022014829c6e0bd7ee97a1bcfef6b85c5fd232653f289394fc6ce6ebb41c73403f1b014104d9ccf88efc6e5be3151fae5e848efd94c91d75e7bf621f9f724a8caff51415338525d3239fae6b93826edf759dd562f77693e55dfa852ffd96a92d683db590f2', 120 | scriptAsm: '3044022044938ac3f8fcb8da29011df6397ed28cc7e894cdc35d596d4f3623bd8c7e465f022014829c6e0bd7ee97a1bcfef6b85c5fd232653f289394fc6ce6ebb41c73403f1b01 04d9ccf88efc6e5be3151fae5e848efd94c91d75e7bf621f9f724a8caff51415338525d3239fae6b93826edf759dd562f77693e55dfa852ffd96a92d683db590f2', 121 | satoshis: 299829, 122 | } 123 | ], 124 | outputs: [ 125 | { 126 | satoshis: 220000, 127 | script: '76a914b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d88ac', 128 | scriptAsm: 'OP_DUP OP_HASH160 b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d OP_EQUALVERIFY OP_CHECKSIG', 129 | address: 'mxT2KzTUQvsaYYothDtjcdvyAdaHA9ofMp' 130 | }, 131 | { 132 | satoshis: 53320000, 133 | address: 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK', 134 | script: '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 135 | scriptAsm: 'OP_DUP OP_HASH160 d2ec20bb8e5f25a52f730384b803d95683250e0b OP_EQUALVERIFY OP_CHECKSIG' 136 | }, 137 | { 138 | address: 'moZY18rGNmh4YCPeugtGW46AkkWMQttBUD', 139 | satoshis: 289829, 140 | script: '76a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac', 141 | scriptAsm: 'OP_DUP OP_HASH160 583df9fa56ad961051e00ca93e68dfaf1eab9ec5 OP_EQUALVERIFY OP_CHECKSIG' 142 | } 143 | ], 144 | locktime: 0 145 | }; 146 | 147 | var txinfos2 = { 148 | totalCount: 1, 149 | items: [ 150 | { 151 | tx: tx 152 | } 153 | ] 154 | }; 155 | 156 | var utxos = [ 157 | { 158 | 'address': 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK', 159 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 160 | 'outputIndex': 1, 161 | 'timestamp': 1441116143, 162 | 'satoshis': 53320000, 163 | 'script': '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 164 | 'height': 534181, 165 | 'confirmations': 50 166 | }, 167 | { 168 | 'address': 'moZY18rGNmh4YCPeugtGW46AkkWMQttBUD', 169 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 170 | 'outputIndex': 2, 171 | 'timestamp': 1441116143, 172 | 'satoshis': 289829, 173 | 'script': '76a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac', 174 | 'height': 534181, 175 | 'confirmations': 50 176 | } 177 | ]; 178 | 179 | describe('Addresses', function() { 180 | var summary = { 181 | balance: 0, 182 | totalReceived: 2782729129, 183 | totalSpent: 2782729129, 184 | unconfirmedBalance: 0, 185 | appearances: 2, 186 | unconfirmedAppearances: 0, 187 | txids: [ 188 | 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 189 | '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3' 190 | ] 191 | }; 192 | describe('/addr/:addr', function() { 193 | var node = { 194 | getAddressSummary: sinon.stub().callsArgWith(2, null, summary) 195 | }; 196 | 197 | var addresses = new AddressController(node); 198 | var req = { 199 | addr: 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 200 | query: {} 201 | }; 202 | 203 | it('should have correct data', function(done) { 204 | var insight = { 205 | 'addrStr': 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 206 | 'balance': 0, 207 | 'balanceSat': 0, 208 | 'totalReceived': 27.82729129, 209 | 'totalReceivedSat': 2782729129, 210 | 'totalSent': 27.82729129, 211 | 'totalSentSat': 2782729129, 212 | 'unconfirmedBalance': 0, 213 | 'unconfirmedBalanceSat': 0, 214 | 'unconfirmedTxApperances': 0, 215 | 'txApperances': 2, 216 | 'transactions': [ 217 | 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 218 | '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3' 219 | ] 220 | }; 221 | 222 | var res = { 223 | jsonp: function(data) { 224 | should(data).eql(insight); 225 | done(); 226 | } 227 | }; 228 | addresses.show(req, res); 229 | }); 230 | 231 | it('handle error', function() { 232 | var testnode = {}; 233 | testnode.log = {}; 234 | testnode.log.error = sinon.stub(); 235 | var controller = new AddressController(testnode); 236 | controller.getAddressSummary = sinon.stub().callsArgWith(2, new Error('test')); 237 | var req = { 238 | query: { 239 | noTxList: 1 240 | }, 241 | addr: 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er' 242 | }; 243 | var send = sinon.stub(); 244 | var status = sinon.stub().returns({send: send}); 245 | var res = { 246 | status: status 247 | }; 248 | controller.show(req, res); 249 | send.callCount.should.equal(1); 250 | status.callCount.should.equal(1); 251 | status.args[0][0].should.equal(503); 252 | send.args[0][0].should.equal('test'); 253 | }); 254 | 255 | it('/balance', function(done) { 256 | var insight = 0; 257 | 258 | var res = { 259 | jsonp: function(data) { 260 | should(data).eql(insight); 261 | done(); 262 | } 263 | }; 264 | addresses.balance(req, res); 265 | }); 266 | 267 | it('/totalReceived', function(done) { 268 | var insight = 2782729129; 269 | 270 | var res = { 271 | jsonp: function(data) { 272 | should(data).eql(insight); 273 | done(); 274 | } 275 | }; 276 | 277 | addresses.totalReceived(req, res); 278 | }); 279 | 280 | it('/totalSent', function(done) { 281 | var insight = 2782729129; 282 | 283 | var res = { 284 | jsonp: function(data) { 285 | should(data).eql(insight); 286 | done(); 287 | } 288 | }; 289 | 290 | addresses.totalSent(req, res); 291 | }); 292 | 293 | it('/unconfirmedBalance', function(done) { 294 | var insight = 0; 295 | 296 | var res = { 297 | jsonp: function(data) { 298 | should(data).eql(insight); 299 | done(); 300 | } 301 | }; 302 | 303 | addresses.unconfirmedBalance(req, res); 304 | }); 305 | }); 306 | 307 | describe('/addr/:addr/utxo', function() { 308 | it('should have correct data', function(done) { 309 | var insight = [ 310 | { 311 | 'address': 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK', 312 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 313 | 'vout': 1, 314 | 'ts': 1441116143, 315 | 'scriptPubKey': '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 316 | 'amount': 0.5332, 317 | 'confirmations': 50, 318 | 'height': 534181, 319 | 'satoshis': 53320000, 320 | 'confirmationsFromCache': true 321 | } 322 | ]; 323 | 324 | var todos = [ 325 | { 326 | confirmationsFromCache: true 327 | } 328 | ]; 329 | 330 | var node = { 331 | services: { 332 | bitcoind: { 333 | height: 534230 334 | } 335 | }, 336 | getAddressUnspentOutputs: sinon.stub().callsArgWith(2, null, utxos.slice(0, 1)) 337 | }; 338 | 339 | var addresses = new AddressController(node); 340 | 341 | var req = { 342 | addr: 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK' 343 | }; 344 | 345 | var res = { 346 | jsonp: function(data) { 347 | var merged = _.merge(data, todos); 348 | should(merged).eql(insight); 349 | done(); 350 | } 351 | }; 352 | 353 | addresses.utxo(req, res); 354 | }); 355 | }); 356 | 357 | describe('/addrs/:addrs/utxo', function() { 358 | it('should have the correct data', function(done) { 359 | var insight = [ 360 | { 361 | 'address': 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK', 362 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 363 | 'vout': 1, 364 | 'ts': 1441116143, 365 | 'scriptPubKey': '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 366 | 'amount': 0.5332, 367 | 'height': 534181, 368 | 'satoshis': 53320000, 369 | 'confirmations': 50, 370 | 'confirmationsFromCache': true 371 | }, 372 | { 373 | 'address': 'moZY18rGNmh4YCPeugtGW46AkkWMQttBUD', 374 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 375 | 'vout': 2, 376 | 'ts': 1441116143, 377 | 'scriptPubKey': '76a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac', 378 | 'amount': 0.00289829, 379 | 'height': 534181, 380 | 'satoshis': 289829, 381 | 'confirmations': 50, 382 | 'confirmationsFromCache': true 383 | } 384 | ]; 385 | 386 | var todos = [ 387 | { 388 | confirmationsFromCache: true 389 | }, { 390 | confirmationsFromCache: true 391 | } 392 | ]; 393 | 394 | var node = { 395 | services: { 396 | bitcoind: { 397 | height: 534230 398 | } 399 | }, 400 | getAddressUnspentOutputs: sinon.stub().callsArgWith(2, null, utxos) 401 | }; 402 | 403 | var addresses = new AddressController(node); 404 | 405 | var req = { 406 | addrs: 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK,moZY18rGNmh4YCPeugtGW46AkkWMQttBUD' 407 | }; 408 | 409 | var res = { 410 | jsonp: function(data) { 411 | var merged = _.merge(data, todos); 412 | should(merged).eql(insight); 413 | done(); 414 | } 415 | }; 416 | 417 | addresses.multiutxo(req, res); 418 | }); 419 | }); 420 | 421 | describe('/addrs/:addrs/txs', function() { 422 | it('should have correct data', function(done) { 423 | var insight = { 424 | 'totalItems': 1, 425 | 'from': 0, 426 | 'to': 1, 427 | 'items': [ 428 | { 429 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 430 | 'version': 1, 431 | 'locktime': 0, 432 | 'vin': [ 433 | { 434 | 'txid': 'ea97726ffc529808094ae5568342267931a058375a20147535a0d095837079f3', 435 | 'vout': 1, 436 | 'scriptSig': { 437 | 'asm': '3044022054233934268b30be779fad874ef42e8db928ba27a1b612d5f111b3ee95eb271c022024272bbaf2dcc4050bd3b9dfa3c93884f6ba6ad7d257598b8245abb65b5ab1e401 040682fdb281a8533e21e13dfd1fcfa424912a85b6cdc4136b5842c85de05ac1f0e4a013f20702adeb53329de13b2ef388e5ed6244676f4f1ee4ee685ab607964d', 438 | 'hex': '473044022054233934268b30be779fad874ef42e8db928ba27a1b612d5f111b3ee95eb271c022024272bbaf2dcc4050bd3b9dfa3c93884f6ba6ad7d257598b8245abb65b5ab1e40141040682fdb281a8533e21e13dfd1fcfa424912a85b6cdc4136b5842c85de05ac1f0e4a013f20702adeb53329de13b2ef388e5ed6244676f4f1ee4ee685ab607964d' 439 | }, 440 | 'sequence': 4294967295, 441 | 'n': 0, 442 | 'addr': 'moFfnRwt77pApKnnU6m5uocFaa43aAYpt5', 443 | 'valueSat': 53540000, 444 | 'value': 0.5354, 445 | 'doubleSpentTxID': null 446 | }, 447 | { 448 | 'txid': '980a9cc2dbc2d3464eb9900ae6d579a03045408563320f62d99316c3d4ff58b7', 449 | 'vout': 2, 450 | 'scriptSig': { 451 | 'asm': '3044022044938ac3f8fcb8da29011df6397ed28cc7e894cdc35d596d4f3623bd8c7e465f022014829c6e0bd7ee97a1bcfef6b85c5fd232653f289394fc6ce6ebb41c73403f1b01 04d9ccf88efc6e5be3151fae5e848efd94c91d75e7bf621f9f724a8caff51415338525d3239fae6b93826edf759dd562f77693e55dfa852ffd96a92d683db590f2', 452 | 'hex': '473044022044938ac3f8fcb8da29011df6397ed28cc7e894cdc35d596d4f3623bd8c7e465f022014829c6e0bd7ee97a1bcfef6b85c5fd232653f289394fc6ce6ebb41c73403f1b014104d9ccf88efc6e5be3151fae5e848efd94c91d75e7bf621f9f724a8caff51415338525d3239fae6b93826edf759dd562f77693e55dfa852ffd96a92d683db590f2' 453 | }, 454 | 'sequence': 4294967295, 455 | 'n': 1, 456 | 'addr': 'n1XJBAyU4hNR4xRtY3UxnmAteoJX83p5qv', 457 | 'valueSat': 299829, 458 | 'value': 0.00299829, 459 | 'doubleSpentTxID': null 460 | } 461 | ], 462 | 'vout': [ 463 | { 464 | 'value': '0.00220000', 465 | 'n': 0, 466 | 'scriptPubKey': { 467 | 'asm': 'OP_DUP OP_HASH160 b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d OP_EQUALVERIFY OP_CHECKSIG', 468 | 'hex': '76a914b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d88ac', 469 | 'reqSigs': 1, 470 | 'type': 'pubkeyhash', 471 | 'addresses': [ 472 | 'mxT2KzTUQvsaYYothDtjcdvyAdaHA9ofMp' 473 | ] 474 | }, 475 | 'spentHeight': null, 476 | 'spentIndex': null, 477 | 'spentTxId': null 478 | }, 479 | { 480 | 'value': '0.53320000', 481 | 'n': 1, 482 | 'scriptPubKey': { 483 | 'asm': 'OP_DUP OP_HASH160 d2ec20bb8e5f25a52f730384b803d95683250e0b OP_EQUALVERIFY OP_CHECKSIG', 484 | 'hex': '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 485 | 'reqSigs': 1, 486 | 'type': 'pubkeyhash', 487 | 'addresses': [ 488 | 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK' 489 | ], 490 | }, 491 | 'spentHeight': null, 492 | 'spentIndex': null, 493 | 'spentTxId': null 494 | }, 495 | { 496 | 'value': '0.00289829', 497 | 'n': 2, 498 | 'scriptPubKey': { 499 | 'asm': 'OP_DUP OP_HASH160 583df9fa56ad961051e00ca93e68dfaf1eab9ec5 OP_EQUALVERIFY OP_CHECKSIG', 500 | 'hex': '76a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac', 501 | 'reqSigs': 1, 502 | 'type': 'pubkeyhash', 503 | 'addresses': [ 504 | 'moZY18rGNmh4YCPeugtGW46AkkWMQttBUD' 505 | ] 506 | }, 507 | 'spentHeight': null, 508 | 'spentIndex': null, 509 | 'spentTxId': null 510 | } 511 | ], 512 | 'blockhash': '0000000000000041ddc94ecf4f86a456a83b2e320c36c6f0c13ff92c7e75f013', 513 | 'blockheight': 534181, 514 | 'confirmations': 52, 515 | 'time': 1441116143, 516 | 'blocktime': 1441116143, 517 | 'valueOut': 0.53829829, 518 | 'size': 470, 519 | 'valueIn': 0.53839829, 520 | 'fees': 0.0001, 521 | 'firstSeenTs': 1441108193 522 | } 523 | ] 524 | }; 525 | 526 | var todos = { 527 | 'items': [ 528 | { 529 | 'vout': [ 530 | { 531 | 'scriptPubKey': { 532 | 'reqSigs': 1, 533 | } 534 | }, 535 | { 536 | 'scriptPubKey': { 537 | 'reqSigs': 1, 538 | } 539 | }, 540 | { 541 | 'scriptPubKey': { 542 | 'reqSigs': 1, 543 | } 544 | } 545 | ], 546 | 'firstSeenTs': 1441108193 547 | } 548 | ] 549 | }; 550 | 551 | var node = { 552 | getAddressHistory: sinon.stub().callsArgWith(2, null, txinfos2), 553 | services: { 554 | bitcoind: { 555 | height: 534232 556 | } 557 | }, 558 | network: 'testnet' 559 | }; 560 | 561 | var addresses = new AddressController(node); 562 | 563 | var req = { 564 | addrs: 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK,moZY18rGNmh4YCPeugtGW46AkkWMQttBUD', 565 | query: {}, 566 | body: {} 567 | }; 568 | 569 | var res = { 570 | jsonp: function(data) { 571 | var merged = _.merge(data, todos); 572 | should(merged).eql(insight); 573 | done(); 574 | } 575 | }; 576 | 577 | addresses.multitxs(req, res); 578 | }); 579 | it('should have trimmed data', function(done) { 580 | var insight = { 581 | 'totalItems': 1, 582 | 'from': 0, 583 | 'to': 1, 584 | 'items': [ 585 | { 586 | 'txid': '63b68becb0e514b32317f4b29a5cf0627d4087e54ac17f686fcb1d9a27680f73', 587 | 'version': 1, 588 | 'locktime': 0, 589 | 'vin': [ 590 | { 591 | 'txid': 'ea97726ffc529808094ae5568342267931a058375a20147535a0d095837079f3', 592 | 'vout': 1, 593 | 'sequence': 4294967295, 594 | 'n': 0, 595 | 'addr': 'moFfnRwt77pApKnnU6m5uocFaa43aAYpt5', 596 | 'valueSat': 53540000, 597 | 'value': 0.5354, 598 | 'doubleSpentTxID': null 599 | }, 600 | { 601 | 'txid': '980a9cc2dbc2d3464eb9900ae6d579a03045408563320f62d99316c3d4ff58b7', 602 | 'vout': 2, 603 | 'sequence': 4294967295, 604 | 'n': 1, 605 | 'addr': 'n1XJBAyU4hNR4xRtY3UxnmAteoJX83p5qv', 606 | 'valueSat': 299829, 607 | 'value': 0.00299829, 608 | 'doubleSpentTxID': null 609 | } 610 | ], 611 | 'vout': [ 612 | { 613 | 'value': '0.00220000', 614 | 'n': 0, 615 | 'scriptPubKey': { 616 | 'hex': '76a914b9bbd76588d9e4e09f0369a9aa0b2749a11c4e8d88ac', 617 | 'reqSigs': 1, 618 | 'type': 'pubkeyhash', 619 | 'addresses': [ 620 | 'mxT2KzTUQvsaYYothDtjcdvyAdaHA9ofMp' 621 | ] 622 | } 623 | }, 624 | { 625 | 'value': '0.53320000', 626 | 'n': 1, 627 | 'scriptPubKey': { 628 | 'hex': '76a914d2ec20bb8e5f25a52f730384b803d95683250e0b88ac', 629 | 'reqSigs': 1, 630 | 'type': 'pubkeyhash', 631 | 'addresses': [ 632 | 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK' 633 | ], 634 | } 635 | }, 636 | { 637 | 'value': '0.00289829', 638 | 'n': 2, 639 | 'scriptPubKey': { 640 | 'hex': '76a914583df9fa56ad961051e00ca93e68dfaf1eab9ec588ac', 641 | 'reqSigs': 1, 642 | 'type': 'pubkeyhash', 643 | 'addresses': [ 644 | 'moZY18rGNmh4YCPeugtGW46AkkWMQttBUD' 645 | ] 646 | } 647 | } 648 | ], 649 | 'blockhash': '0000000000000041ddc94ecf4f86a456a83b2e320c36c6f0c13ff92c7e75f013', 650 | 'blockheight': 534181, 651 | 'confirmations': 52, 652 | 'time': 1441116143, 653 | 'blocktime': 1441116143, 654 | 'valueOut': 0.53829829, 655 | 'size': 470, 656 | 'valueIn': 0.53839829, 657 | 'fees': 0.0001, 658 | 'firstSeenTs': 1441108193 659 | } 660 | ] 661 | }; 662 | 663 | var todos = { 664 | 'items': [ 665 | { 666 | 'vout': [ 667 | { 668 | 'scriptPubKey': { 669 | 'reqSigs': 1, 670 | } 671 | }, 672 | { 673 | 'scriptPubKey': { 674 | 'reqSigs': 1, 675 | } 676 | }, 677 | { 678 | 'scriptPubKey': { 679 | 'reqSigs': 1, 680 | } 681 | } 682 | ], 683 | 'firstSeenTs': 1441108193 684 | } 685 | ] 686 | }; 687 | 688 | var node = { 689 | getAddressHistory: sinon.stub().callsArgWith(2, null, txinfos2), 690 | services: { 691 | bitcoind: { 692 | height: 534232 693 | } 694 | }, 695 | network: 'testnet' 696 | }; 697 | 698 | var addresses = new AddressController(node); 699 | 700 | var req = { 701 | addrs: 'mzkD4nmQ8ixqxySdBgsXTpgvAMK5iRZpNK,moZY18rGNmh4YCPeugtGW46AkkWMQttBUD', 702 | query: {noSpent: '1', noScriptSig: '1', noAsm: '1'}, 703 | body: {} 704 | }; 705 | 706 | var res = { 707 | jsonp: function(data) { 708 | var merged = _.merge(data, todos); 709 | should(merged).eql(insight); 710 | done(); 711 | } 712 | }; 713 | 714 | addresses.multitxs(req, res); 715 | }); 716 | }); 717 | describe('#_getTransformOptions', function() { 718 | it('will return false with value of string "0"', function() { 719 | var node = {}; 720 | var addresses = new AddressController(node); 721 | var req = { 722 | query: { 723 | noAsm: '0', 724 | noScriptSig: '0', 725 | noSpent: '0' 726 | } 727 | }; 728 | var options = addresses._getTransformOptions(req); 729 | options.should.eql({ 730 | noAsm: false, 731 | noScriptSig: false, 732 | noSpent: false 733 | }); 734 | }); 735 | it('will return true with value of string "1"', function() { 736 | var node = {}; 737 | var addresses = new AddressController(node); 738 | var req = { 739 | query: { 740 | noAsm: '1', 741 | noScriptSig: '1', 742 | noSpent: '1' 743 | } 744 | }; 745 | var options = addresses._getTransformOptions(req); 746 | options.should.eql({ 747 | noAsm: true, 748 | noScriptSig: true, 749 | noSpent: true 750 | }); 751 | }); 752 | it('will return true with value of number "1"', function() { 753 | var node = {}; 754 | var addresses = new AddressController(node); 755 | var req = { 756 | query: { 757 | noAsm: 1, 758 | noScriptSig: 1, 759 | noSpent: 1 760 | } 761 | }; 762 | var options = addresses._getTransformOptions(req); 763 | options.should.eql({ 764 | noAsm: true, 765 | noScriptSig: true, 766 | noSpent: true 767 | }); 768 | }); 769 | }); 770 | }); 771 | -------------------------------------------------------------------------------- /test/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var should = require('should'); 3 | var sinon = require('sinon'); 4 | var bitcore = require('bitcore-lib-zcash'); 5 | var TxController = require('../lib/transactions'); 6 | var _ = require('lodash'); 7 | 8 | describe('Transactions', function() { 9 | describe('/tx/:txid', function() { 10 | it('should have correct data', function(done) { 11 | var insight = { 12 | 'txid': 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 13 | 'version': 1, 14 | 'locktime': 0, 15 | 'vin': [ 16 | { 17 | 'txid': '87c9b0f27571fff14b8c2d69e55614eacedd0f59fcc490b721320f9dae145aad', 18 | 'vout': 0, 19 | 'scriptSig': { 20 | 'asm': '30450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c01 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 21 | 'hex': '4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307' 22 | }, 23 | 'sequence': 4294967295, 24 | 'n': 0, 25 | 'addr': 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 26 | 'valueSat': 18535505, 27 | 'value': 0.18535505, 28 | 'doubleSpentTxID': null, 29 | 'isConfirmed': true, 30 | 'confirmations': 242, 31 | 'unconfirmedInput': false 32 | }, 33 | { 34 | 'txid': 'd8a10aaedf3dd33b5ddf8979273f3dbf61e4638d1aa6a93c59ea22bc65ac2196', 35 | 'vout': 0, 36 | 'scriptSig': { 37 | 'asm': '30440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb3101 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 38 | 'hex': '4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307' 39 | }, 40 | 'sequence': 4294967295, 41 | 'n': 1, 42 | 'addr': 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 43 | 'valueSat': 16419885, 44 | 'value': 0.16419885, 45 | 'doubleSpentTxID': null, 46 | 'isConfirmed': true, 47 | 'confirmations': 242, 48 | 'unconfirmedInput': false 49 | } 50 | ], 51 | 'vout': [ 52 | { 53 | 'value': '0.21247964', 54 | 'n': 0, 55 | 'scriptPubKey': { 56 | 'asm': 'OP_DUP OP_HASH160 4b7b335f978f130269fe661423258ae9642df8a1 OP_EQUALVERIFY OP_CHECKSIG', 57 | 'hex': '76a9144b7b335f978f130269fe661423258ae9642df8a188ac', 58 | 'reqSigs': 1, 59 | 'type': 'pubkeyhash', 60 | 'addresses': [ 61 | 'mnQ4ZaGessNgdxmWPxbTHcfx4b8R6eUr1X' 62 | ] 63 | }, 64 | 'spentTxId': null, 65 | 'spentIndex': null, 66 | 'spentHeight': null 67 | }, 68 | { 69 | 'value': '0.13677426', 70 | 'n': 1, 71 | 'scriptPubKey': { 72 | 'asm': 'OP_DUP OP_HASH160 6efcf883b4b6f9997be9a0600f6c095fe2bd2d92 OP_EQUALVERIFY OP_CHECKSIG', 73 | 'hex': '76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac', 74 | 'reqSigs': 1, 75 | 'type': 'pubkeyhash', 76 | 'addresses': [ 77 | 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f' 78 | ] 79 | }, 80 | 'spentTxId': '614fe1708825f9c21732394e4784cc6808ac1d8b939736bfdead970567561eec', 81 | 'spentIndex': 1, 82 | 'spentHeight': 10, 83 | 'spentTs': 1440997099 84 | } 85 | ], 86 | 'blockhash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 87 | 'blockheight': 533974, 88 | 'confirmations': 230, 89 | 'time': 1440987503, 90 | 'blocktime': 1440987503, 91 | 'valueOut': 0.3492539, 92 | 'size': 437, 93 | 'valueIn': 0.3495539, 94 | 'fees': 0.0003 95 | }; 96 | 97 | var spentTxId = '614fe1708825f9c21732394e4784cc6808ac1d8b939736bfdead970567561eec'; 98 | var spentIndex = 1; 99 | var detailedTransaction = { 100 | hex: '7b5485d3628922f004f470f497f6a83f6df4df347e1bce15831a964623f8072b565f7c7bc5dcbc717c6e2a2301a2f6b4a19e65042ad88c9f5d037628de38603c4f137f625e135691e2bd0169cab74e1368abe858f3c3d116e9d13c4c85ead129d9edf0245a3fb1b35561bd230607dca0dcaf3cffc735a3982d8384a1ecc5d622a7bb4db8b5d47d061701978b1f45e2e39946d66c3394f8a20b8ac8c931a6786f761da2d0f3fa2c7c93edee9f2a94de7c47510498767c3d87afe68815bd6058710bf5d8c850a5d20fc217943d9c00da58a4908d92a0912578247746f2086e54cb7b81b6a9e3cc1741457e956d41bdeaae06c441db96ec39a2d17147dd8f468eeaeaaa78dc2e53d66188a791c46b2a4965639ad72a2b90ee52786e36db1a8cf924346b105a40b41a3027dae657782ef7e8b56d6da86062184cb5366d4886cd2ce27471d9d62d1df447f2e5a9641e1f8d1f2b628054d3bd915bf7932bcec6f2dd4965e2406b1dba445b5493ee475757de332618220318dd806b880a7364370c5c0c3b736a653f97b2901fdb5cf4b5b2230b09b2d7bd324a392633d51c598765f9bd286421239a1f25db34a9a61f645eb601e59f10fc1b', 101 | hash: 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 102 | version: 1, 103 | blockHash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 104 | height: 533974, 105 | blockTimestamp: 1440987503, 106 | inputSatoshis: 34955390, 107 | outputSatoshis: 34925390, 108 | feeSatoshis: 30000, 109 | inputs: [ 110 | { 111 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 112 | prevTxId: '87c9b0f27571fff14b8c2d69e55614eacedd0f59fcc490b721320f9dae145aad', 113 | outputIndex: 0, 114 | sequence: 4294967295, 115 | script: '4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 116 | scriptAsm: '30450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c01 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 117 | satoshis: 18535505, 118 | }, 119 | { 120 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 121 | prevTxId: 'd8a10aaedf3dd33b5ddf8979273f3dbf61e4638d1aa6a93c59ea22bc65ac2196', 122 | outputIndex: 0, 123 | sequence: 4294967295, 124 | script: '4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 125 | scriptAsm: '30440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb3101 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 126 | satoshis: 16419885, 127 | } 128 | ], 129 | outputs: [ 130 | { 131 | satoshis: 21247964, 132 | script: '76a9144b7b335f978f130269fe661423258ae9642df8a188ac', 133 | scriptAsm: 'OP_DUP OP_HASH160 4b7b335f978f130269fe661423258ae9642df8a1 OP_EQUALVERIFY OP_CHECKSIG', 134 | address: 'mnQ4ZaGessNgdxmWPxbTHcfx4b8R6eUr1X' 135 | }, 136 | { 137 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 138 | satoshis: 13677426, 139 | scriptAsm: 'OP_DUP OP_HASH160 6efcf883b4b6f9997be9a0600f6c095fe2bd2d92 OP_EQUALVERIFY OP_CHECKSIG', 140 | script: '76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac', 141 | spentTxId: spentTxId, 142 | spentIndex: spentIndex, 143 | spentHeight: 10 144 | } 145 | ], 146 | locktime: 0 147 | }; 148 | 149 | var todos = { 150 | vin: [ 151 | { 152 | isConfirmed: true, 153 | confirmations: 242, 154 | unconfirmedInput: false 155 | }, 156 | { 157 | isConfirmed: true, 158 | confirmations: 242, 159 | unconfirmedInput: false 160 | } 161 | ], 162 | vout: [ 163 | { 164 | scriptPubKey: { 165 | reqSigs: 1 166 | } 167 | }, 168 | { 169 | scriptPubKey: { 170 | reqSigs: 1 171 | }, 172 | spentTs: 1440997099 173 | } 174 | ] 175 | }; 176 | 177 | var node = { 178 | getDetailedTransaction: sinon.stub().callsArgWith(1, null, detailedTransaction), 179 | services: { 180 | bitcoind: { 181 | height: 534203 182 | }, 183 | }, 184 | network: 'testnet' 185 | }; 186 | 187 | var transactions = new TxController(node); 188 | var txid = 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0'; 189 | var req = { 190 | params: { 191 | txid: txid 192 | } 193 | }; 194 | var res = {}; 195 | var next = function() { 196 | var merged = _.merge(req.transaction, todos); 197 | should(merged).eql(insight); 198 | done(); 199 | }; 200 | 201 | transactions.transaction(req, res, next); 202 | }); 203 | }); 204 | 205 | describe('/txs', function() { 206 | var sandbox = sinon.sandbox.create(); 207 | afterEach(function() { 208 | sandbox.restore(); 209 | }); 210 | it('by block hash', function(done) { 211 | var blockOverview = { 212 | hash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 213 | height: 533974, 214 | chainWork: '0000000000000000000000000000000000000000000000054626b1839ade284a', 215 | prevHash: '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4', 216 | txids: [ 217 | '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd', 218 | 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 219 | '2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1' 220 | ] 221 | }; 222 | 223 | var transactionDetails = { 224 | '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd': { 225 | hex: '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2303d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000ffffffff01c018824a000000001976a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac00000000', 226 | coinbase: true, 227 | version: 1, 228 | blockTimestamp: 1440987503, 229 | blockHash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 230 | height: 533974, 231 | inputSatoshis: 0, 232 | outputSatoshis: 1250040000, 233 | feeSatoshis: 0, 234 | locktime: 0, 235 | hash: '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd', 236 | inputs: [ 237 | { 238 | script: '03d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000', 239 | sequence: 4294967295 240 | } 241 | ], 242 | outputs: [ 243 | { 244 | address: 'mq4oDPjmNWnBxbzx7qouzhpCSTMePUtYDF', 245 | script: '76a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac', 246 | scriptAsm: 'OP_DUP OP_HASH160 68bedce8982d25c3b6b03f6238cbad00378b8ead OP_EQUALVERIFY OP_CHECKSIG', 247 | satoshis: 1250040000 248 | } 249 | ] 250 | }, 251 | 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0': { 252 | hex: '0100000002ad5a14ae9d0f3221b790c4fc590fddceea1456e5692d8c4bf1ff7175f2b0c987000000008b4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff9621ac65bc22ea593ca9a61a8d63e461bf3d3f277989df5d3bd33ddfae0aa1d8000000008a4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff02dc374401000000001976a9144b7b335f978f130269fe661423258ae9642df8a188ac72b3d000000000001976a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac00000000', 253 | inputSatoshis: 34955390, 254 | outputSatoshis: 34925390, 255 | feeSatoshis: 30000, 256 | version: 1, 257 | hash: 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 258 | blockTimestamp: 1440987503, 259 | height: 533974, 260 | blockHash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 261 | locktime: 0, 262 | inputs: [ 263 | { 264 | satoshis: 18535505, 265 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 266 | script: '4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 267 | scriptAsm: '30450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c01 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 268 | prevTxId: '87c9b0f27571fff14b8c2d69e55614eacedd0f59fcc490b721320f9dae145aad', 269 | outputIndex: 0, 270 | sequence: 4294967295 271 | }, 272 | { 273 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 274 | script: '4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 275 | scriptAsm: '30440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb3101 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 276 | satoshis: 16419885, 277 | sequence: 4294967295, 278 | prevTxId: 'd8a10aaedf3dd33b5ddf8979273f3dbf61e4638d1aa6a93c59ea22bc65ac2196', 279 | outputIndex: 0 280 | } 281 | ], 282 | outputs: [ 283 | { 284 | address: 'mnQ4ZaGessNgdxmWPxbTHcfx4b8R6eUr1X', 285 | script: '76a9144b7b335f978f130269fe661423258ae9642df8a188ac', 286 | scriptAsm: 'OP_DUP OP_HASH160 4b7b335f978f130269fe661423258ae9642df8a1 OP_EQUALVERIFY OP_CHECKSIG', 287 | satoshis: 21247964 288 | }, 289 | { 290 | script: '76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac', 291 | scriptAsm: 'OP_DUP OP_HASH160 6efcf883b4b6f9997be9a0600f6c095fe2bd2d92 OP_EQUALVERIFY OP_CHECKSIG', 292 | address: 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 293 | satoshis: 13677426, 294 | spentTxId: '614fe1708825f9c21732394e4784cc6808ac1d8b939736bfdead970567561eec', 295 | spentIndex: 1, 296 | spentHeight: 200 297 | } 298 | ] 299 | }, 300 | '2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1': { 301 | hex: '0100000002060d3cb6dfb7ffe85e2908010fea63190c9707e96fc7448128eb895b5e222771030000006b483045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901210346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909dfeffffff7b2d8a8263cffbdb722e2a5c74166e6f2258634e277c0b08f51b578b667e2fba000000006a473044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c012103371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eeefeffffff02209a1d00000000001976a9148e451eec7ca0a1764b4ab119274efdd2727b3c8588ac40420f00000000001976a914d0fce8f064cd1059a6a11501dd66fe42368572b088accb250800', 302 | blockHash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 303 | blockTimestamp: 1440987503, 304 | height: 533974, 305 | locktime: 533963, 306 | inputSatoshis: 2950000, 307 | outputSatoshis: 2940000, 308 | feeSatoshis: 10000, 309 | version: 1, 310 | hash: '2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1', 311 | inputs: [ 312 | { 313 | address: 'mgZK8zpudWoAaAwpLQSgc9t9PJJyEBpBdJ', 314 | satoshis: 990000, 315 | script: '483045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901210346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909d', 316 | scriptAsm: '3045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901 0346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909d', 317 | sequence: 4294967294, 318 | outputIndex: 3, 319 | prevTxId: '7127225e5b89eb288144c76fe907970c1963ea0f0108295ee8ffb7dfb63c0d06' 320 | }, 321 | { 322 | address: 'n4oM7bPuC4ZPdCEDvtw9xGYQC7jmi5S6F4', 323 | satoshis: 1960000, 324 | script: '473044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c012103371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eee', 325 | scriptAsm: '3044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c01 03371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eee', 326 | prevTxId: 'ba2f7e668b571bf5080b7c274e6358226f6e16745c2a2e72dbfbcf63828a2d7b', 327 | sequence: 4294967294, 328 | outputIndex : 0 329 | } 330 | ], 331 | outputs: [ 332 | { 333 | spentTxId: '9a213b879da9073a9a30606f9046f35f36f268cbf03f6242993a97c4c07c00b9', 334 | spentIndex: 1, 335 | spentHeight: 200, 336 | satoshis: 1940000, 337 | script: '76a9148e451eec7ca0a1764b4ab119274efdd2727b3c8588ac', 338 | scriptAsm: 'OP_DUP OP_HASH160 8e451eec7ca0a1764b4ab119274efdd2727b3c85 OP_EQUALVERIFY OP_CHECKSIG', 339 | address: 'mtVD3tdifBNujYzZ5N7PgXfKk4Bc85tDKA' 340 | }, 341 | { 342 | spentTxId: '418d3eb60275957b3456b96902e908abf962e71be4c4f09486564254664951bc', 343 | spentIndex: 34, 344 | spentHeight: 200, 345 | script: '76a914d0fce8f064cd1059a6a11501dd66fe42368572b088ac', 346 | scriptAsm: 'OP_DUP OP_HASH160 d0fce8f064cd1059a6a11501dd66fe42368572b0 OP_EQUALVERIFY OP_CHECKSIG', 347 | address: 'mzZypShcs1B35udnkqeYeJy8rUdgHDDvKG', 348 | satoshis: 1000000 349 | } 350 | ] 351 | } 352 | }; 353 | 354 | var node = { 355 | getBlockOverview: sinon.stub().callsArgWith(1, null, blockOverview), 356 | getDetailedTransaction: function(txid, callback) { 357 | callback(null, transactionDetails[txid]); 358 | }, 359 | services: { 360 | bitcoind: { 361 | height: 534209 362 | } 363 | }, 364 | network: 'testnet' 365 | }; 366 | 367 | var transactions = new TxController(node); 368 | 369 | var insight = { 370 | 'pagesTotal': 1, 371 | 'txs': [ 372 | { 373 | 'txid': '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd', 374 | 'version': 1, 375 | 'locktime': 0, 376 | 'vin': [ 377 | { 378 | 'coinbase': '03d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000', 379 | 'sequence': 4294967295, 380 | 'n': 0 381 | } 382 | ], 383 | 'vout': [ 384 | { 385 | 'value': '12.50040000', 386 | 'n': 0, 387 | 'scriptPubKey': { 388 | 'asm': 'OP_DUP OP_HASH160 68bedce8982d25c3b6b03f6238cbad00378b8ead OP_EQUALVERIFY OP_CHECKSIG', 389 | 'hex': '76a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac', 390 | 'reqSigs': 1, 391 | 'type': 'pubkeyhash', 392 | 'addresses': [ 393 | 'mq4oDPjmNWnBxbzx7qouzhpCSTMePUtYDF' 394 | ] 395 | }, 396 | 'spentTxId': null, 397 | 'spentIndex': null, 398 | 'spentHeight': null 399 | } 400 | ], 401 | 'blockhash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 402 | 'blockheight': 533974, 403 | 'confirmations': 236, 404 | 'time': 1440987503, 405 | 'blocktime': 1440987503, 406 | 'isCoinBase': true, 407 | 'valueOut': 12.5004, 408 | 'size': 120 409 | }, 410 | { 411 | 'txid': 'b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0', 412 | 'version': 1, 413 | 'locktime': 0, 414 | 'vin': [ 415 | { 416 | 'txid': '87c9b0f27571fff14b8c2d69e55614eacedd0f59fcc490b721320f9dae145aad', 417 | 'vout': 0, 418 | 'scriptSig': { 419 | 'asm': '30450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c01 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 420 | 'hex': '4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307' 421 | }, 422 | 'sequence': 4294967295, 423 | 'n': 0, 424 | 'addr': 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 425 | 'valueSat': 18535505, 426 | 'value': 0.18535505, 427 | 'doubleSpentTxID': null 428 | }, 429 | { 430 | 'txid': 'd8a10aaedf3dd33b5ddf8979273f3dbf61e4638d1aa6a93c59ea22bc65ac2196', 431 | 'vout': 0, 432 | 'scriptSig': { 433 | 'asm': '30440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb3101 04eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307', 434 | 'hex': '4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307' 435 | }, 436 | 'sequence': 4294967295, 437 | 'n': 1, 438 | 'addr': 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f', 439 | 'valueSat': 16419885, 440 | 'value': 0.16419885, 441 | 'doubleSpentTxID': null 442 | } 443 | ], 444 | 'vout': [ 445 | { 446 | 'value': '0.21247964', 447 | 'n': 0, 448 | 'scriptPubKey': { 449 | 'asm': 'OP_DUP OP_HASH160 4b7b335f978f130269fe661423258ae9642df8a1 OP_EQUALVERIFY OP_CHECKSIG', 450 | 'hex': '76a9144b7b335f978f130269fe661423258ae9642df8a188ac', 451 | 'reqSigs': 1, 452 | 'type': 'pubkeyhash', 453 | 'addresses': [ 454 | 'mnQ4ZaGessNgdxmWPxbTHcfx4b8R6eUr1X' 455 | ] 456 | }, 457 | 'spentTxId': null, 458 | 'spentIndex': null, 459 | 'spentHeight': null 460 | }, 461 | { 462 | 'value': '0.13677426', 463 | 'n': 1, 464 | 'scriptPubKey': { 465 | 'asm': 'OP_DUP OP_HASH160 6efcf883b4b6f9997be9a0600f6c095fe2bd2d92 OP_EQUALVERIFY OP_CHECKSIG', 466 | 'hex': '76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac', 467 | 'reqSigs': 1, 468 | 'type': 'pubkeyhash', 469 | 'addresses': [ 470 | 'mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f' 471 | ] 472 | }, 473 | 'spentTxId': '614fe1708825f9c21732394e4784cc6808ac1d8b939736bfdead970567561eec', 474 | 'spentIndex': 1, 475 | 'spentHeight': 200, 476 | 'spentTs': 1440997099 477 | } 478 | ], 479 | 'blockhash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 480 | 'blockheight': 533974, 481 | 'confirmations': 236, 482 | 'time': 1440987503, 483 | 'blocktime': 1440987503, 484 | 'valueOut': 0.3492539, 485 | 'size': 437, 486 | 'valueIn': 0.3495539, 487 | 'fees': 0.0003 488 | }, 489 | { 490 | 'txid': '2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1', 491 | 'version': 1, 492 | 'locktime': 533963, 493 | 'vin': [ 494 | { 495 | 'txid': '7127225e5b89eb288144c76fe907970c1963ea0f0108295ee8ffb7dfb63c0d06', 496 | 'vout': 3, 497 | 'scriptSig': { 498 | 'asm': '3045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901 0346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909d', 499 | 'hex': '483045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901210346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909d' 500 | }, 501 | 'sequence': 4294967294, 502 | 'n': 0, 503 | 'addr': 'mgZK8zpudWoAaAwpLQSgc9t9PJJyEBpBdJ', 504 | 'valueSat': 990000, 505 | 'value': 0.0099, 506 | 'doubleSpentTxID': null 507 | }, 508 | { 509 | 'txid': 'ba2f7e668b571bf5080b7c274e6358226f6e16745c2a2e72dbfbcf63828a2d7b', 510 | 'vout': 0, 511 | 'scriptSig': { 512 | 'asm': '3044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c01 03371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eee', 513 | 'hex': '473044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c012103371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eee' 514 | }, 515 | 'sequence': 4294967294, 516 | 'n': 1, 517 | 'addr': 'n4oM7bPuC4ZPdCEDvtw9xGYQC7jmi5S6F4', 518 | 'valueSat': 1960000, 519 | 'value': 0.0196, 520 | 'doubleSpentTxID': null 521 | } 522 | ], 523 | 'vout': [ 524 | { 525 | 'value': '0.01940000', 526 | 'n': 0, 527 | 'scriptPubKey': { 528 | 'asm': 'OP_DUP OP_HASH160 8e451eec7ca0a1764b4ab119274efdd2727b3c85 OP_EQUALVERIFY OP_CHECKSIG', 529 | 'hex': '76a9148e451eec7ca0a1764b4ab119274efdd2727b3c8588ac', 530 | 'reqSigs': 1, 531 | 'type': 'pubkeyhash', 532 | 'addresses': [ 533 | 'mtVD3tdifBNujYzZ5N7PgXfKk4Bc85tDKA' 534 | ] 535 | }, 536 | 'spentTxId': '9a213b879da9073a9a30606f9046f35f36f268cbf03f6242993a97c4c07c00b9', 537 | 'spentIndex': 1, 538 | 'spentHeight': 200, 539 | 'spentTs': 1440992946 540 | }, 541 | { 542 | 'value': '0.01000000', 543 | 'n': 1, 544 | 'scriptPubKey': { 545 | 'asm': 'OP_DUP OP_HASH160 d0fce8f064cd1059a6a11501dd66fe42368572b0 OP_EQUALVERIFY OP_CHECKSIG', 546 | 'hex': '76a914d0fce8f064cd1059a6a11501dd66fe42368572b088ac', 547 | 'reqSigs': 1, 548 | 'type': 'pubkeyhash', 549 | 'addresses': [ 550 | 'mzZypShcs1B35udnkqeYeJy8rUdgHDDvKG' 551 | ] 552 | }, 553 | 'spentTxId': '418d3eb60275957b3456b96902e908abf962e71be4c4f09486564254664951bc', 554 | 'spentIndex': 34, 555 | 'spentHeight': 200, 556 | 'spentTs': 1440999118 557 | } 558 | ], 559 | 'blockhash': '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', 560 | 'blockheight': 533974, 561 | 'confirmations': 236, 562 | 'time': 1440987503, 563 | 'blocktime': 1440987503, 564 | 'valueOut': 0.0294, 565 | 'size': 373, 566 | 'valueIn': 0.0295, 567 | 'fees': 0.0001 568 | } 569 | ] 570 | }; 571 | 572 | var todos = { 573 | txs: [ 574 | { 575 | vout: [ 576 | { 577 | scriptPubKey: { 578 | reqSigs: 1 579 | } 580 | } 581 | ] 582 | }, 583 | { 584 | vin: [ 585 | ], 586 | vout: [ 587 | { 588 | scriptPubKey: { 589 | reqSigs: 1 590 | } 591 | }, 592 | { 593 | scriptPubKey: { 594 | reqSigs: 1 595 | }, 596 | spentTs: 1440997099 597 | } 598 | ] 599 | }, 600 | { 601 | vin: [ 602 | ], 603 | vout: [ 604 | { 605 | scriptPubKey: { 606 | reqSigs: 1 607 | }, 608 | spentTs: 1440992946 609 | }, 610 | { 611 | scriptPubKey: { 612 | reqSigs: 1 613 | }, 614 | spentTs: 1440999118 615 | } 616 | ] 617 | } 618 | ] 619 | }; 620 | 621 | var req = { 622 | query: { 623 | block: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7' 624 | } 625 | }; 626 | 627 | var res = { 628 | jsonp: function(data) { 629 | _.merge(data, todos); 630 | should(data).eql(insight); 631 | done(); 632 | } 633 | }; 634 | 635 | transactions.list(req, res); 636 | }); 637 | it('by address', function(done) { 638 | 639 | var txinfos = [ 640 | { 641 | tx: { 642 | hex: '010000000125c46caa6d839435b43c20d6d48978e677841244b37a09f6f6cd29bfaf5b5eea010000006b483045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a6012103acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6feffffff02a913dda5000000001976a9143583efb5e64a4668c6c54bb5fcc30af4417b4f2d88ac809fd500000000001976a9149713201957f42379e574d7c70d506ee49c2c8ad688ac49260800', 643 | hash: 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 644 | version: 1, 645 | inputs: [ 646 | { 647 | prevTxId: 'ea5e5bafbf29cdf6f6097ab344128477e67889d4d6203cb43594836daa6cc425', 648 | outputIndex: 1, 649 | sequence: 4294967294, 650 | script: '483045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a6012103acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6', 651 | scriptAsm: '3045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a601 03acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6', 652 | satoshis: 2796764565, 653 | address: 'msyjRQQ88MabQmyafpKCjBHUwuJ49tVjcb' 654 | } 655 | ], 656 | outputs: [ 657 | { 658 | satoshis: 2782729129, 659 | address: 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 660 | script: '76a9143583efb5e64a4668c6c54bb5fcc30af4417b4f2d88ac', 661 | scriptAsm: 'OP_DUP OP_HASH160 3583efb5e64a4668c6c54bb5fcc30af4417b4f2d OP_EQUALVERIFY OP_CHECKSIG' 662 | }, 663 | { 664 | satoshis: 14000000, 665 | address: 'muHmEsjhjmATf9i3T9gHyeQoce9LXe2dWz', 666 | script: '76a9149713201957f42379e574d7c70d506ee49c2c8ad688ac', 667 | scriptAsm: 'OP_DUP OP_HASH160 9713201957f42379e574d7c70d506ee49c2c8ad6 OP_EQUALVERIFY OP_CHECKSIG' 668 | } 669 | ], 670 | inputSatoshis: 2796764565, 671 | outputSatoshis: 2796729129, 672 | feeSatoshis: 35436, 673 | locktime: 534089 674 | } 675 | }, 676 | { 677 | tx: { 678 | hex: '0100000001c7f1230d689647ccbff2aae4ddeeccf26aa8836fea709552c9fa0962b9c30ebb000000006a47304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d5640121034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24feffffff02bce0c9a4000000001976a91456e446bc3489543d8324c6d0271524c0bd0506dd88ac80a81201000000001976a914011d2963b619186a318f768dddfd98cd553912a088ac53260800', 679 | hash: '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3', 680 | version: 1, 681 | inputs: [ 682 | { 683 | prevTxId: 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 684 | outputIndex: 0, 685 | sequence: 4294967294, 686 | script: '47304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d5640121034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24', 687 | scriptAsm: '304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d56401 034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24', 688 | satoshis: 2782729129, 689 | address: 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er' 690 | } 691 | ], 692 | outputs: [ 693 | { 694 | satoshis: 2764693692, 695 | address: 'moSPsU4p2C2gssiniJ1JNH4fB9xs633tLv', 696 | script: '76a91456e446bc3489543d8324c6d0271524c0bd0506dd88ac', 697 | scriptAsm: 'OP_DUP OP_HASH160 56e446bc3489543d8324c6d0271524c0bd0506dd OP_EQUALVERIFY OP_CHECKSIG' 698 | }, 699 | { 700 | satoshis: 18000000, 701 | scriptAsm: 'OP_DUP OP_HASH160 011d2963b619186a318f768dddfd98cd553912a0 OP_EQUALVERIFY OP_CHECKSIG', 702 | script: '76a914011d2963b619186a318f768dddfd98cd553912a088ac', 703 | address: 'mfcquSAitCkUKXaYRZTRZQDfUegnL3kDew', 704 | spentTxId: '71a9e60c0341c9c258367f1a6d4253276f16e207bf84f41ff7412d8958a81bed' 705 | } 706 | ], 707 | inputSatoshis: 2782729129, 708 | outputSatoshis: 2782693692, 709 | feeSatoshis: 35437, 710 | locktime: 534099 711 | } 712 | } 713 | ]; 714 | 715 | var historyResult = { 716 | totalCount: txinfos.length, 717 | items: txinfos 718 | }; 719 | 720 | txinfos[0].tx.blockHash = '00000000000001001aba15de213648f370607fb048288dd27b96f7e833a73520'; 721 | txinfos[0].tx.blockTimestamp = 1441068774; 722 | txinfos[0].tx.height = 534105; 723 | 724 | txinfos[1].tx.blockHash = '0000000000000a3acc1f7fe72917eb48bb319ed96c125a6dfcc0ba6acab3c4d0'; 725 | txinfos[1].tx.blockTimestamp = 1441072817; 726 | txinfos[1].tx.height = 534110; 727 | 728 | txinfos[0].tx.outputs[0].spentTxId = '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3'; 729 | txinfos[0].tx.outputs[0].spentIndex = 0; 730 | txinfos[0].tx.outputs[0].spentHeight = 199; 731 | 732 | txinfos[1].tx.outputs[0].spentTxId = '661194e5533a395ce9076f292b7e0fb28fe94cd8832a81b4aa0517ff58c1ddd2'; 733 | txinfos[1].tx.outputs[0].spentIndex = 0; 734 | txinfos[1].tx.outputs[0].spentHeight = 134; 735 | 736 | txinfos[1].tx.outputs[1].spentTxId = '71a9e60c0341c9c258367f1a6d4253276f16e207bf84f41ff7412d8958a81bed'; 737 | txinfos[1].tx.outputs[1].spentIndex = 0; 738 | txinfos[1].tx.outputs[1].spentHeight = 112; 739 | 740 | var node = { 741 | getAddressHistory: sinon.stub().callsArgWith(2, null, historyResult), 742 | services: { 743 | bitcoind: { 744 | height: 534223 745 | } 746 | }, 747 | network: 'testnet' 748 | }; 749 | 750 | var insight = { 751 | 'pagesTotal': 1, 752 | 'txs': [ 753 | { 754 | 'txid': 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 755 | 'version': 1, 756 | 'locktime': 534089, 757 | 'vin': [ 758 | { 759 | 'txid': 'ea5e5bafbf29cdf6f6097ab344128477e67889d4d6203cb43594836daa6cc425', 760 | 'vout': 1, 761 | 'scriptSig': { 762 | 'asm': '3045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a601 03acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6', 763 | 'hex': '483045022100f4d169783bef70e3943d2a617cce55d9fe4e33fc6f9880b8277265e2f619a97002201238648abcdf52960500664e969046d41755f7fc371971ebc78002fc418465a6012103acdcd31d51272403ce0829447e59e2ac9e08ed0bf92011cbf7420addf24534e6' 764 | }, 765 | 'sequence': 4294967294, 766 | 'n': 0, 767 | 'addr': 'msyjRQQ88MabQmyafpKCjBHUwuJ49tVjcb', 768 | 'valueSat': 2796764565, 769 | 'value': 27.96764565, 770 | 'doubleSpentTxID': null 771 | } 772 | ], 773 | 'vout': [ 774 | { 775 | 'value': '27.82729129', 776 | 'n': 0, 777 | 'scriptPubKey': { 778 | 'asm': 'OP_DUP OP_HASH160 3583efb5e64a4668c6c54bb5fcc30af4417b4f2d OP_EQUALVERIFY OP_CHECKSIG', 779 | 'hex': '76a9143583efb5e64a4668c6c54bb5fcc30af4417b4f2d88ac', 780 | 'reqSigs': 1, 781 | 'type': 'pubkeyhash', 782 | 'addresses': [ 783 | 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er' 784 | ] 785 | }, 786 | 'spentTxId': '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3', 787 | 'spentIndex': 0, 788 | 'spentHeight': 199, 789 | 'spentTs': 1441072817 790 | }, 791 | { 792 | 'value': '0.14000000', 793 | 'n': 1, 794 | 'scriptPubKey': { 795 | 'asm': 'OP_DUP OP_HASH160 9713201957f42379e574d7c70d506ee49c2c8ad6 OP_EQUALVERIFY OP_CHECKSIG', 796 | 'hex': '76a9149713201957f42379e574d7c70d506ee49c2c8ad688ac', 797 | 'reqSigs': 1, 798 | 'type': 'pubkeyhash', 799 | 'addresses': [ 800 | 'muHmEsjhjmATf9i3T9gHyeQoce9LXe2dWz' 801 | ] 802 | }, 803 | 'spentTxId': null, 804 | 'spentIndex': null, 805 | 'spentHeight': null 806 | } 807 | ], 808 | 'blockhash': '00000000000001001aba15de213648f370607fb048288dd27b96f7e833a73520', 809 | 'blockheight': 534105, 810 | 'confirmations': 119, 811 | 'time': 1441068774, 812 | 'blocktime': 1441068774, 813 | 'valueOut': 27.96729129, 814 | 'size': 226, 815 | 'valueIn': 27.96764565, 816 | 'fees': 0.00035436 817 | }, 818 | { 819 | 'txid': '01f700df84c466f2a389440e5eeacdc47d04f380c39e5d19dce2ce91a11ecba3', 820 | 'version': 1, 821 | 'locktime': 534099, 822 | 'vin': [ 823 | { 824 | 'txid': 'bb0ec3b96209fac9529570ea6f83a86af2cceedde4aaf2bfcc4796680d23f1c7', 825 | 'vout': 0, 826 | 'scriptSig': { 827 | 'asm': '304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d56401 034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24', 828 | 'hex': '47304402201ee69281db6b95bb1aa3074059b67581635b719e8f64e4c2694db6ec56ad9447022011e91528996ea459b1fb2c0b59363fecbefe4bc2ca90f7b2382bdaa358f2d5640121034cc057b12a68ee79df998004b9a1341bbb18b17ea4939bebaa3bac001e940f24' 829 | }, 830 | 'sequence': 4294967294, 831 | 'n': 0, 832 | 'addr': 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er', 833 | 'valueSat': 2782729129, 834 | 'value': 27.82729129, 835 | 'doubleSpentTxID': null 836 | } 837 | ], 838 | 'vout': [ 839 | { 840 | 'value': '27.64693692', 841 | 'n': 0, 842 | 'scriptPubKey': { 843 | 'asm': 'OP_DUP OP_HASH160 56e446bc3489543d8324c6d0271524c0bd0506dd OP_EQUALVERIFY OP_CHECKSIG', 844 | 'hex': '76a91456e446bc3489543d8324c6d0271524c0bd0506dd88ac', 845 | 'reqSigs': 1, 846 | 'type': 'pubkeyhash', 847 | 'addresses': [ 848 | 'moSPsU4p2C2gssiniJ1JNH4fB9xs633tLv' 849 | ] 850 | }, 851 | 'spentTxId': '661194e5533a395ce9076f292b7e0fb28fe94cd8832a81b4aa0517ff58c1ddd2', 852 | 'spentIndex': 0, 853 | 'spentHeight': 134, 854 | 'spentTs': 1441077236 855 | }, 856 | { 857 | 'value': '0.18000000', 858 | 'n': 1, 859 | 'scriptPubKey': { 860 | 'asm': 'OP_DUP OP_HASH160 011d2963b619186a318f768dddfd98cd553912a0 OP_EQUALVERIFY OP_CHECKSIG', 861 | 'hex': '76a914011d2963b619186a318f768dddfd98cd553912a088ac', 862 | 'reqSigs': 1, 863 | 'type': 'pubkeyhash', 864 | 'addresses': [ 865 | 'mfcquSAitCkUKXaYRZTRZQDfUegnL3kDew' 866 | ] 867 | }, 868 | 'spentTxId': '71a9e60c0341c9c258367f1a6d4253276f16e207bf84f41ff7412d8958a81bed', 869 | 'spentIndex': 0, 870 | 'spentHeight': 112, 871 | 'spentTs': 1441069523 872 | } 873 | ], 874 | 'blockhash': '0000000000000a3acc1f7fe72917eb48bb319ed96c125a6dfcc0ba6acab3c4d0', 875 | 'blockheight': 534110, 876 | 'confirmations': 114, 877 | 'time': 1441072817, 878 | 'blocktime': 1441072817, 879 | 'valueOut': 27.82693692, 880 | 'size': 225, 881 | 'valueIn': 27.82729129, 882 | 'fees': 0.00035437 883 | } 884 | ] 885 | }; 886 | 887 | var todos = { 888 | 'txs': [ 889 | { 890 | 'vin': [ 891 | ], 892 | 'vout': [ 893 | { 894 | 'scriptPubKey': { 895 | 'reqSigs': 1 896 | }, 897 | 'spentTs': 1441072817 898 | }, 899 | { 900 | 'scriptPubKey': { 901 | 'reqSigs': 1 902 | } 903 | } 904 | ] 905 | }, 906 | { 907 | 'vin': [ 908 | ], 909 | 'vout': [ 910 | { 911 | 'scriptPubKey': { 912 | 'reqSigs': 1 913 | }, 914 | 'spentTs': 1441077236 915 | }, 916 | { 917 | 'scriptPubKey': { 918 | 'reqSigs': 1 919 | }, 920 | 'spentTs': 1441069523 921 | } 922 | ] 923 | } 924 | ] 925 | }; 926 | 927 | var req = { 928 | query: { 929 | address: 'mkPvAKZ2rar6qeG3KjBtJHHMSP1wFZH7Er' 930 | } 931 | }; 932 | 933 | var res = { 934 | jsonp: function(data) { 935 | var merged = _.merge(data, todos); 936 | should(merged).eql(insight); 937 | done(); 938 | } 939 | }; 940 | 941 | var transactions = new TxController(node); 942 | transactions.list(req, res); 943 | }); 944 | }); 945 | 946 | describe('/rawtx/:txid', function() { 947 | it('should give the correct data', function(done) { 948 | var hex = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2303d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000ffffffff01c018824a000000001976a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac00000000'; 949 | 950 | var node = { 951 | getTransaction: sinon.stub().callsArgWith(1, null, bitcore.Transaction().fromBuffer(new Buffer(hex, 'hex'))) 952 | }; 953 | 954 | var transactions = new TxController(node); 955 | 956 | var res = {}; 957 | var req = { 958 | params: { 959 | txid: txid 960 | } 961 | }; 962 | var next = function() { 963 | should(req.rawTransaction.rawtx).eql(hex); 964 | done(); 965 | }; 966 | var txid = '25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd'; 967 | transactions.rawTransaction(req, res, next); 968 | }); 969 | }); 970 | 971 | describe('#transformInvTransaction', function() { 972 | it('should give the correct data', function() { 973 | var insight = { 974 | 'txid': 'a15a7c257af596704390d345ff3ea2eed4cd02ce8bfb8afb700bff82257e49fb', 975 | 'valueOut': 0.02038504, 976 | 'vout': [ 977 | { 978 | '3DQYCLG6rZdtV2Xw8y4YtozZjNHYoKsLuo': 45000 979 | }, 980 | { 981 | '12WvZmssxT85f81dD6wcmWznxbnFkEpNMS': 1993504 982 | } 983 | ], 984 | 'isRBF': false 985 | }; 986 | 987 | var rawTx = '01000000011760bc271a397bfb65b7506d430d96ebb1faff467ed957516238a9670e806a86010000006b483045022100f0056ae68a34cdb4194d424bd727c18f82653bca2a198e0d55ab6b4ee88bbdb902202a5745af4f72a5dbdca1e3d683af4667728a8b20e8001e0f8308a4d329ce3f96012102f3af6e66b61c9d99c74d9a9c3c1bec014a8c05d28bf339c8f5f395b5ce319e7dffffffff02c8af00000000000017a9148083b541ea15f1d18c5ca5e1fd47f9035cce24ed87206b1e00000000001976a91410a0e70cd91a45e0e6e409e227ab285bd61592b188ac00000000'; 988 | var tx = bitcore.Transaction().fromBuffer(new Buffer(rawTx, 'hex')); 989 | 990 | var node = { 991 | network: bitcore.Networks.livenet 992 | }; 993 | 994 | var transactions = new TxController(node); 995 | 996 | var result = transactions.transformInvTransaction(tx); 997 | should(result).eql(insight); 998 | }); 999 | it('will not include null values in vout array', function() { 1000 | var insight = { 1001 | 'txid': '716d54157c31e52c820494c6c2b8af1b64352049f4dcc80632aa15742a7f82c4', 1002 | 'valueOut': 12.5002, 1003 | 'vout': [ 1004 | { 1005 | 'n4eY3qiP9pi32MWC6FcJFHciSsfNiYFYgR': 12.5002 * 1e8 1006 | } 1007 | ], 1008 | 'isRBF': false 1009 | }; 1010 | 1011 | var rawTx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0403ebc108ffffffff04a0ca814a000000001976a914fdb9fb622b0db8d9121475a983288a0876f4de4888ac0000000000000000226a200000000000000000000000000000000000000000000000000000ffff0000000000000000000000001b6a1976a914fdb9fb622b0db8d9121475a983288a0876f4de4888ac0000000000000000326a303a791c8e85200500d89769b4f958e4db6b3ec388ddaa30233c4517d942d440c24ae903bff40d97ca06465fcf2714000000000000'; 1012 | var tx = bitcore.Transaction().fromBuffer(new Buffer(rawTx, 'hex')); 1013 | 1014 | var node = { 1015 | network: bitcore.Networks.testnet 1016 | }; 1017 | 1018 | var transactions = new TxController(node); 1019 | 1020 | var result = transactions.transformInvTransaction(tx); 1021 | should(result).eql(insight); 1022 | }); 1023 | it('should detect RBF txs', function() { 1024 | var testCases = [ 1025 | { 1026 | rawTx: '01000000017fa897c3556271c34cb28c03c196c2d912093264c9d293cb4980a2635474467d010000000f5355540b6f93598893578893588851ffffffff01501e0000000000001976a914aa2482ce71d219018ef334f6cc551ee88abd920888ac00000000', 1027 | expected: false, 1028 | }, { 1029 | rawTx: '01000000017fa897c3556271c34cb28c03c196c2d912093264c9d293cb4980a2635474467d010000000f5355540b6f935988935788935888510000000001501e0000000000001976a914aa2482ce71d219018ef334f6cc551ee88abd920888ac00000000', 1030 | expected: true, 1031 | }, 1032 | ]; 1033 | 1034 | var node = { 1035 | network: bitcore.Networks.livenet 1036 | }; 1037 | 1038 | var transactions = new TxController(node); 1039 | 1040 | _.each(testCases, function(tc) { 1041 | var tx = bitcore.Transaction().fromBuffer(new Buffer(tc.rawTx, 'hex')); 1042 | var result = transactions.transformInvTransaction(tx); 1043 | should.exist(result.isRBF); 1044 | result.isRBF.should.equal(tc.expected); 1045 | }); 1046 | }); 1047 | 1048 | }); 1049 | }); 1050 | --------------------------------------------------------------------------------