diff --git a/bitcore-node/addresses.js b/bitcore-node/addresses.js new file mode 100644 index 0000000..13454a1 --- /dev/null +++ b/bitcore-node/addresses.js @@ -0,0 +1,213 @@ +'use strict'; + +var common = require('./common'); +var bitcore = require('bitcore'); +var TxController = require('./transactions'); + +function AddressController(node) { + this.node = node; + this.txController = new TxController(node); +} + +AddressController.prototype.show = function(req, res, next) { + this.getAddressHistory(req.addr, function(err, data) { + if(err) { + return common.handleErrors(err, res); + } + + res.jsonp(data); + }); +}; + +AddressController.prototype.balance = function(req, res, next) { + this.addressHistorySubQuery(req, res, next, 'balanceSat'); +}; + +AddressController.prototype.totalReceived = function(req, res, next) { + this.addressHistorySubQuery(req, res, next, 'totalReceivedSat'); +}; + +AddressController.prototype.totalSent = function(req, res, next) { + this.addressHistorySubQuery(req, res, next, 'totalSentSat'); +}; + +AddressController.prototype.unconfirmedBalance = function(req, res, next) { + this.addressHistorySubQuery(req, res, next, 'unconfirmedBalanceSat'); +}; + +AddressController.prototype.addressHistorySubQuery = function(req, res, next, param) { + this.getAddressHistory(req.addr, function(err, data) { + if(err) { + return common.handleErrors(err, res); + } + + res.jsonp(data[param]); + }); +}; + +AddressController.prototype.getAddressHistory = function(address, callback) { + var self = this; + + this.node.getAddressHistory(address, true, function(err, txinfos) { + if(err) { + return callback(err); + } + + callback(null, self.transformAddressHistory(txinfos, address)); + }); +}; + +AddressController.prototype.checkAddr = function(req, res, next) { + req.addr = req.params.addr; + this.check(req, res, next, [req.addr]); +}; + +AddressController.prototype.checkAddrs = function(req, res, next) { + if(req.body.addrs) { + req.addrs = req.body.addrs.split(','); + } else { + req.addrs = req.params.addrs.split(','); + } + + this.check(req, res, next, req.addrs); +} + +AddressController.prototype.check = function(req, res, next, addresses) { + if(!addresses.length || !addresses[0]) { + return common.handleErrors({ + message: 'Must include address', + code: 1 + }, res); + } + + for(var i = 0; i < addresses.length; i++) { + try { + var a = new bitcore.Address(addresses[i]); + } catch(e) { + return common.handleErrors({ + message: 'Invalid address: ' + e.message, + code: 1 + }, res); + } + } + + next(); +} + +AddressController.prototype.transformAddressHistory = function(txinfos, address) { + var transactions = txinfos.map(function(info) { + return info.tx.hash; + }).filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + + var balance = 0; + var appearances = 0; + var totalReceived = 0; + var totalSent = 0; + var unconfirmedBalance = 0; + var unconfirmedAppearances = 0; + + for(var i = 0; i < txinfos.length; i++) { + if(txinfos[i].satoshis > 0) { + totalReceived += txinfos[i].satoshis; + } else { + totalSent += -txinfos[i].satoshis; + } + + if(txinfos[i].confirmations) { + balance += txinfos[i].satoshis; + unconfirmedBalance += txinfos[i].satoshis; + appearances++; + } else { + unconfirmedBalance += txinfos[i].satoshis; + unconfirmedAppearances++; + } + } + + return { + addrStr: address, + balance: balance / 1e8, + balanceSat: balance, + totalReceived: totalReceived / 1e8, + totalReceivedSat: totalReceived, + totalSent: totalSent / 1e8, + totalSentSat: totalSent, + unconfirmedBalance: unconfirmedBalance / 1e8, + unconfirmedBalanceSat: unconfirmedBalance, + unconfirmedTxApperances: unconfirmedAppearances, // misspelling - ew + txApperances: appearances, // yuck + transactions: transactions + }; +}; + +AddressController.prototype.utxo = function(req, res, next) { + var self = this; + + this.node.getUnspentOutputs(req.addr, true, function(err, utxos) { + if(err && err instanceof self.node.errors.NoOutputs) { + return res.jsonp([]); + } else if(err) { + return common.handleErrors(err, res); + } + + res.jsonp(utxos.map(self.transformUtxo.bind(self))); + }); +}; + +AddressController.prototype.multiutxo = function(req, res, next) { + var self = this; + + this.node.getUnspentOutputs(req.addrs, true, function(err, utxos) { + if(err && err instanceof self.node.errors.NoOutputs) { + return res.jsonp([]); + } else if(err) { + return common.handleErrors(err, res); + } + + res.jsonp(utxos.map(self.transformUtxo.bind(self))); + }); +}; + +AddressController.prototype.transformUtxo = function(utxo) { + return { + address: utxo.address, + txid: utxo.txid, + vout: utxo.outputIndex, + ts: utxo.timestamp ? Math.round(utxo.timestamp / 1000) : Math.round(Date.now() / 1000), + scriptPubKey: utxo.script, + amount: utxo.satoshis / 1e8, + confirmations: utxo.confirmations + }; +}; + +AddressController.prototype.multitxs = function(req, res, next) { + var self = this; + + this.node.getAddressHistory(req.addrs, true, function(err, txinfos) { + if(err) { + return common.handleErrors(err, res); + } + + res.jsonp(self.transformAddressHistoryForMultiTxs(txinfos)); + }); +}; + +AddressController.prototype.transformAddressHistoryForMultiTxs = function(txinfos) { + var self = this; + + var transformed = { + totalItems: txinfos.length, + from: 0, + to: txinfos.length, + items: txinfos.map(function(txinfo) { + return self.txController.transformTransaction(txinfo.tx); + }).reverse() + }; + + return transformed; +}; + + + +module.exports = AddressController; \ No newline at end of file diff --git a/bitcore-node/blocks.js b/bitcore-node/blocks.js new file mode 100644 index 0000000..7b862ae --- /dev/null +++ b/bitcore-node/blocks.js @@ -0,0 +1,128 @@ +'use strict'; + +var common = require('./common'); +var async = require('async'); +var bitcore = require('bitcore'); +var BufferUtil = bitcore.util.buffer; + +function BlockController(node) { + this.node = node; +} + +var BLOCK_LIMIT = 200; + +/** + * Find block by hash ... + */ +BlockController.prototype.block = function(req, res, next, hash) { + var self = this; + + this.node.getBlock(hash, function(err, block) { + if(err && err.message === 'Block not found.') { + // TODO libbitcoind should pass an instance of errors.Block.NotFound + return common.handleErrors(null, res); + } else if(err) { + return common.handleErrors(err, res); + } + + var info = self.node.services.bitcoind.getBlockIndex(hash); + + req.block = self.transformBlock(block, info); + next(); + }); +}; + +BlockController.prototype.transformBlock = function(block, info) { + var blockObj = block.toObject(); + var transactionIds = blockObj.transactions.map(function(tx) { + return tx.hash + }); + return { + hash: block.hash, + confirmations: this.node.services.db.tip.__height - info.height + 1, + size: block.toBuffer().length, + height: info.height, + version: blockObj.header.version, + merkleroot: blockObj.header.merkleRoot, + tx: transactionIds, + time: blockObj.header.time, + nonce: blockObj.header.nonce, + bits: blockObj.header.bits.toString(16), + difficulty: 0, // placeholder + chainwork: info.chainWork, + previousblockhash: blockObj.header.prevHash, + nextblockhash: null, // placeholder + reward: 0, // First output of first transaction gives us the reward + fees. How to isolate just reward? + isMainChain: true // placeholder + } +}; + +/** + * Show block + */ +BlockController.prototype.show = function(req, res) { + if (req.block) { + res.jsonp(req.block); + } +}; + +BlockController.prototype.blockIndex = function(req, res, next, height) { + var info = this.node.services.bitcoind.getBlockIndex(parseInt(height)); + if(!info) { + return common.handleErrors(null, res); + } + + res.jsonp({ + blockHash: info.hash + }); +}; + +// List blocks by date +BlockController.prototype.list = function(req, res) { + var self = this; + + // TODO actually get blocks by date instead of just the last blocks + // TODO pagination + + var limit = parseInt(req.query.limit || BLOCK_LIMIT); + + var blocks = []; + var lastHash = this.node.services.db.tip.hash; + + async.timesSeries(limit, function(n, next) { + self.node.getBlock(lastHash, function(err, block) { + if(err) { + return next(err); + } + + var info = self.node.services.bitcoind.getBlockIndex(block.hash); + block.__height = info.height; + blocks.push(block); + lastHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); + next(); + }); + }, function(err) { + if(err) { + return common.handleErrors(err, res); + } + + var data = { + blocks: blocks.map(function(block) { + return { + height: block.__height, + size: block.toBuffer().length, + hash: block.hash, + time: block.header.time, + txlength: block.transactions.length, + poolInfo: {} + }; + }), + length: blocks.length, + pagination: {} + }; + + res.jsonp(data); + }); +}; + +module.exports = BlockController; \ No newline at end of file diff --git a/bitcore-node/common.js b/bitcore-node/common.js new file mode 100644 index 0000000..d7fb9af --- /dev/null +++ b/bitcore-node/common.js @@ -0,0 +1,19 @@ +'use strict'; + +exports.notReady = function (err, res, p) { + res.status(503).send('Server not yet ready. Sync Percentage:' + p); +}; + +exports.handleErrors = function (err, res) { + if (err) { + if (err.code) { + res.status(400).send(err.message + '. Code:' + err.code); + } + else { + res.status(503).send(err.message); + } + } + else { + res.status(404).send('Not found'); + } +}; diff --git a/bitcore-node/index.js b/bitcore-node/index.js new file mode 100644 index 0000000..50a3782 --- /dev/null +++ b/bitcore-node/index.js @@ -0,0 +1,97 @@ +'use strict'; + +var BaseService = require('./service'); +var inherits = require('util').inherits; +var BlockController = require('./blocks'); +var TxController = require('./transactions'); +var AddressController = require('./addresses'); +var StatusController = require('./status'); + +var InsightAPI = function(options) { + BaseService.call(this, options); +}; + +InsightAPI.dependencies = ['address', 'web']; + +inherits(InsightAPI, BaseService); + +InsightAPI.prototype.setupRoutes = function(app) { + var apiPrefix = '/api'; + + //Block routes + var blocks = new BlockController(this.node); + app.get(apiPrefix + '/blocks', blocks.list.bind(blocks)); + + + app.get(apiPrefix + '/block/:blockHash', blocks.show.bind(blocks)); + app.param('blockHash', blocks.block.bind(blocks)); + + app.get(apiPrefix + '/block-index/:height', blocks.blockIndex.bind(blocks)); + app.param('height', blocks.blockIndex.bind(blocks)); + + + // Transaction routes + var transactions = new TxController(this.node); + app.get(apiPrefix + '/tx/:txid', transactions.show.bind(transactions)); + app.param('txid', transactions.transaction.bind(transactions)); + app.get(apiPrefix + '/txs', transactions.list.bind(transactions)); + app.post(apiPrefix + '/tx/send', transactions.send.bind(transactions)); + + // Raw Routes + app.get(apiPrefix + '/rawtx/:txid', transactions.showRaw.bind(transactions)); + app.param('txid', transactions.rawTransaction.bind(transactions)); + + // Address routes + var addresses = new AddressController(this.node); + app.get(apiPrefix + '/addr/:addr', addresses.checkAddr.bind(addresses), addresses.show.bind(addresses)); + app.get(apiPrefix + '/addr/:addr/utxo', addresses.checkAddr.bind(addresses), addresses.utxo.bind(addresses)); + app.get(apiPrefix + '/addrs/:addrs/utxo', addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); + app.post(apiPrefix + '/addrs/utxo', addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); + app.get(apiPrefix + '/addrs/:addrs/txs', addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); + app.post(apiPrefix + '/addrs/txs', addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); + + // Address property routes + app.get(apiPrefix + '/addr/:addr/balance', addresses.checkAddr.bind(addresses), addresses.balance.bind(addresses)); + app.get(apiPrefix + '/addr/:addr/totalReceived', addresses.checkAddr.bind(addresses), addresses.totalReceived.bind(addresses)); + app.get(apiPrefix + '/addr/:addr/totalSent', addresses.checkAddr.bind(addresses), addresses.totalSent.bind(addresses)); + app.get(apiPrefix + '/addr/:addr/unconfirmedBalance', addresses.checkAddr.bind(addresses), addresses.unconfirmedBalance.bind(addresses)); + + // Status route + var status = new StatusController(this.node); + app.get(apiPrefix + '/status', status.show.bind(status)); + app.get(apiPrefix + '/sync', status.sync.bind(status)); + app.get(apiPrefix + '/peer', status.peer.bind(status)); + app.get(apiPrefix + '/version', status.version.bind(status)); + + + // Utils route + /*var utils = require('../app/controllers/utils'); + app.get(apiPrefix + '/utils/estimatefee', utils.estimateFee); + + // Currency + var currency = require('../app/controllers/currency'); + app.get(apiPrefix + '/currency', currency.index); + + // Email store plugin + if (config.enableEmailstore) { + var emailPlugin = require('../plugins/emailstore'); + app.get(apiPrefix + '/email/retrieve', emailPlugin.retrieve); + } + + // Currency rates plugin + if (config.enableCurrencyRates) { + var currencyRatesPlugin = require('../plugins/currencyrates'); + app.get(apiPrefix + '/rates/:code', currencyRatesPlugin.getRate); + } + + // Address routes + var messages = require('../app/controllers/messages'); + app.get(apiPrefix + '/messages/verify', messages.verify); + app.post(apiPrefix + '/messages/verify', messages.verify); + + //Home route + var index = require('../app/controllers/index'); + app.get('*', index.render);*/ +}; + +module.exports = InsightAPI; \ No newline at end of file diff --git a/bitcore-node/service.js b/bitcore-node/service.js new file mode 100644 index 0000000..cc47ce1 --- /dev/null +++ b/bitcore-node/service.js @@ -0,0 +1,91 @@ +'use strict'; + +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + +var Service = function(options) { + EventEmitter.call(this); + + this.node = options.node; + this.name = options.name; +}; + +util.inherits(Service, EventEmitter); + +/** + * Describes the dependencies that should be loaded before this service. + */ +Service.dependencies = []; + +/** + * blockHandler + * @param {Block} block - the block being added or removed from the chain + * @param {Boolean} add - whether the block is being added or removed + * @param {Function} callback - call with the leveldb database operations to perform + */ +Service.prototype.blockHandler = function(block, add, callback) { + // implement in the child class + setImmediate(function() { + callback(null, []); + }); +}; + +/** + * the bus events available for subscription + * @return {Array} an array of event info + */ +Service.prototype.getPublishEvents = function() { + // Example: + // return [ + // ['eventname', this, this.subscribeEvent, this.unsubscribeEvent], + // ]; + return []; +}; + +/** + * the API methods to expose + * @return {Array} return array of methods + */ +Service.prototype.getAPIMethods = function() { + // Example: + // return [ + // ['getData', this, this.getData, 1] + // ]; + + return []; +}; + +// Example: +// Service.prototype.getData = function(arg1, callback) { +// +// }; + +/** + * Function which is called when module is first initialized + */ +Service.prototype.start = function(done) { + setImmediate(done); +}; + +/** + * Function to be called when bitcore-node is stopped + */ +Service.prototype.stop = function(done) { + setImmediate(done); +}; + +/** + * Setup express routes + * @param {Express} app + */ +Service.prototype.setupRoutes = function(app) { + // Setup express routes here +}; + +Service.prototype.getRoutePrefix = function() { + return this.name; +}; + + + +module.exports = Service; diff --git a/bitcore-node/status.js b/bitcore-node/status.js new file mode 100644 index 0000000..ef62339 --- /dev/null +++ b/bitcore-node/status.js @@ -0,0 +1,75 @@ +'use strict'; + +function StatusController(node) { + this.node = node; +} + +StatusController.prototype.show = function(req, res) { + + var option = req.query.q; + + switch(option) { + case 'getDifficulty': + res.jsonp(this.getDifficulty()); + break; + case 'getTxOutSetInfo': + // TODO + // break; + case 'getLastBlockHash': + // TODO + // break; + case 'getBestBlockHash': + // TODO + // break; + case 'getInfo': + default: + res.jsonp(this.getInfo()); + } +}; + +StatusController.prototype.getInfo = function() { + return this.node.services.bitcoind.getInfo(); +}; + +StatusController.prototype.getDifficulty = function() { + var info = this.node.services.bitcoind.getInfo(); + return { + difficulty: info.difficulty + }; +}; + +StatusController.prototype.sync = function(req, res) { + var status = 'syncing'; + if(this.node.services.bitcoind.isSynced() && this.node.services.db.tip.__height === this.node.services.bitcoind.height) { + status = 'finished'; + } + + var info = { + status: status, + blockChainHeight: this.node.services.bitcoind.height, + syncPercentage: this.node.services.db.tip.__height / this.node.services.bitcoind.height * 100, + height: this.node.services.db.tip.__height, + error: null, + type: 'bitcore node' + }; + + res.jsonp(info); +}; + +// Hard coded to make insight ui happy, but not applicable +StatusController.prototype.peer = function(req, res) { + res.jsonp({ + connected: true, + host: '127.0.0.1', + port: null + }); +}; + +StatusController.prototype.version = function(req, res) { + var pjson = require('../../../package.json'); + res.json({ + version: pjson.version + }); +} + +module.exports = StatusController; \ No newline at end of file diff --git a/bitcore-node/transactions.js b/bitcore-node/transactions.js new file mode 100644 index 0000000..1344952 --- /dev/null +++ b/bitcore-node/transactions.js @@ -0,0 +1,251 @@ +'use strict'; + +var bitcore = require('bitcore'); +var common = require('./common'); +var async = require('async'); + +function TxController(node) { + this.node = node; +} + +TxController.prototype.show = function(req, res) { + if (req.transaction) { + res.jsonp(req.transaction); + } +}; + +/** + * Find transaction by hash ... + */ +TxController.prototype.transaction = function(req, res, next, txid) { + var self = this; + + this.node.getTransactionWithBlockInfo(txid, true, function(err, transaction) { + if (err && err instanceof self.node.errors.Transaction.NotFound) { + return common.handleErrors(null, res); + } else if(err) { + return common.handleErrors(err, res); + } + + transaction.populateInputs(self.node.services.db, [], function(err) { + if(err) { + return res.send({ + error: err.toString() + }) + } + + req.transaction = self.transformTransaction(transaction); + next(); + }); + }); +}; + +TxController.prototype.transformTransaction = function(transaction) { + var txObj = transaction.toObject(); + + var confirmations = 0; + if(transaction.__height >= 0) { + confirmations = this.node.services.db.tip.__height - transaction.__height + 1; + } + + var transformed = { + txid: txObj.hash, + version: txObj.version, + locktime: txObj.nLockTime + }; + + if(transaction.isCoinbase()) { + transformed.vin = [ + { + coinbase: txObj.inputs[0].script, + sequence: txObj.inputs[0].sequenceNumber, + n: 0 + } + ]; + } else { + transformed.vin = txObj.inputs.map(this.transformInput.bind(this)); + } + + transformed.vout = txObj.outputs.map(this.transformOutput.bind(this)); + + transformed.blockhash = transaction.__blockHash; + transformed.confirmations = confirmations; + transformed.time = transaction.__timestamp ? transaction.__timestamp : Date.now(); // can we get this from bitcoind? + transformed.blocktime = transformed.time; + + if(transaction.isCoinbase()) { + transformed.isCoinBase = true; + } + + transformed.valueOut = transaction.outputAmount / 1e8; + transformed.size = transaction.toBuffer().length; + if(transaction.hasAllUtxoInfo()) { + transformed.valueIn = transaction.inputAmount / 1e8; + transformed.fees = transaction.getFee() / 1e8; + } + + return transformed; +}; + +TxController.prototype.transformInput = function(input, index) { + var transformed = { + txid: input.prevTxId, + vout: input.outputIndex, + scriptSig: { + asm: null, // TODO + hex: input.script + }, + sequence: input.sequenceNumber, + n: index + }; + + if(input.output) { + transformed.addr = bitcore.Script(input.output.script).toAddress(this.node.network).toString(); + transformed.valueSat = input.output.satoshis; + transformed.value = input.output.satoshis / 1e8; + transformed.doubleSpentTxID = null; // TODO + transformed.isConfirmed = null; // TODO + transformed.confirmations = null; // TODO + transformed.unconfirmedInput = null; // TODO + } + + return transformed; +}; + +TxController.prototype.transformOutput = function(output, index) { + var transformed = { + value: (output.satoshis / 1e8).toFixed(8), + n: index, + scriptPubKey: { + asm: null, // TODO + hex: output.script, + reqSigs: null, // TODO + type: null // TODO + }, + spentTxId: null, // TODO + spentIndex: null, // TODO + spentTs: null // TODO + }; + + var address = bitcore.Script(output.script).toAddress(this.node.network).toString(); + if(address !== 'false') { + transformed.scriptPubKey.addresses = [address]; + } + + return transformed; +}; + +TxController.prototype.rawTransaction = function(req, res, next, txid) { + this.node.getTransaction(txid, true, function(err, transaction) { + if (err && err instanceof self.node.errors.Transaction.NotFound) { + return common.handleErrors(null, res); + } else if(err) { + return common.handleErrors(err, res); + } + + req.rawTransaction = { + 'rawtx': transaction.toBuffer().toString('hex') + }; + + next(); + }); +}; + +TxController.prototype.showRaw = function(req, res) { + if (req.rawTransaction) { + res.jsonp(req.rawTransaction); + } +}; + +TxController.prototype.list = function(req, res) { + var self = this; + + var blockHash = req.query.block; + var address = req.query.address; + var page = req.query.pageNum; + var pageLength = 10; + var pagesTotal = 1; + + if(blockHash) { + self.node.getBlock(blockHash, function(err, block) { + if(err && err.message === 'Block not found.') { + return common.handleErrors(null, res); + } else if(err) { + return common.handleErrors(err, res); + } + + var blockInfo = self.node.services.bitcoind.getBlockIndex(block.hash); + var txs = block.transactions; + var totalTxs = txs.length; + + if(page) { + txs = txs.splice(page * pageLength, pageLength); + pagesTotal = Math.ceil(totalTxs / pageLength); + } + + async.mapSeries(txs, function(tx, next) { + tx.__blockHash = block.hash; + tx.__height = blockInfo.height; + tx.__timestamp = block.header.time; + + tx.populateInputs(self.node.services.db, [], function(err) { + if(err) { + return next(err); + } + + next(null, self.transformTransaction(tx)); + }); + }, function(err, transformed) { + if(err) { + return common.handleErrors(err, res); + } + + res.jsonp({ + pagesTotal: pagesTotal, + txs: transformed + }); + }); + }); + } else if(address) { + self.node.getAddressHistory(address, true, function(err, txinfos) { + if(err) { + return common.handleErrors(err, res); + } + + var txs = txinfos.map(function(info) { + return info.tx; + }).filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + + var totalTxs = txs.length; + + if(page) { + txs = txs.splice(page * pageLength, pageLength); + pagesTotal = Math.ceil(totalTxs / pageLength); + } + + var transformed = txs.map(self.transformTransaction.bind(self)); + + res.jsonp({ + pagesTotal: pagesTotal, + txs: transformed + }); + }); + } else { + return common.handleErrors(new Error('Block hash or address expected'), res); + } +}; + +TxController.prototype.send = function(req, res) { + this.node.sendTransaction(req.body.rawtx, function(err, txid) { + if(err) { + // TODO handle specific errors + return common.handleErrors(err, res); + } + + res.json({'txid': txid}); + }); +}; + +module.exports = TxController; \ No newline at end of file diff --git a/package.json b/package.json index 0b06e98..8f3ddc8 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "scripts": { "start": "node insight.js" }, + "bitcoreNode": "bitcore-node", "dependencies": { "async": "*", "base58-native": "0.1.2", diff --git a/test/bitcore-node/blocks.js b/test/bitcore-node/blocks.js new file mode 100644 index 0000000..bfb481b --- /dev/null +++ b/test/bitcore-node/blocks.js @@ -0,0 +1,165 @@ +'use strict'; + +var should = require('should'); +var sinon = require('sinon'); +var BlockController = require('../../bitcore-node/blocks'); +var bitcore = require('bitcore'); +var _ = require('lodash'); + +var blocks = { + '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7': '07000020a491892cca9f143f7f00b8d65bbce0204bb32e17e914325fa5010000000000003e28f0519ecf01f7f01ea8da61084b2e4741a18ce1f3738117b84458353764b06fb9e35567f20c1a78eb626f0301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2303d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000ffffffff01c018824a000000001976a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac000000000100000002ad5a14ae9d0f3221b790c4fc590fddceea1456e5692d8c4bf1ff7175f2b0c987000000008b4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff9621ac65bc22ea593ca9a61a8d63e461bf3d3f277989df5d3bd33ddfae0aa1d8000000008a4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff02dc374401000000001976a9144b7b335f978f130269fe661423258ae9642df8a188ac72b3d000000000001976a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac000000000100000002060d3cb6dfb7ffe85e2908010fea63190c9707e96fc7448128eb895b5e222771030000006b483045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901210346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909dfeffffff7b2d8a8263cffbdb722e2a5c74166e6f2258634e277c0b08f51b578b667e2fba000000006a473044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c012103371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eeefeffffff02209a1d00000000001976a9148e451eec7ca0a1764b4ab119274efdd2727b3c8588ac40420f00000000001976a914d0fce8f064cd1059a6a11501dd66fe42368572b088accb250800', + '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7': '0300000041c49521c6106debe1221077ca9e71850e3c328037e5e98fbd0600000000000082b3a56da828618b1b7b103763e3c21c38c9536ce185ddd4aebdcd627645a878fb96e355ffff001d192907730101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2803bf25081b4d696e656420627920416e74506f6f6c202bde53072055e396fb010300000099050000ffffffff01807c814a000000001976a9144c5220c58749e5474ec4e5bbb0c2cc0342c6f44488ac00000000', + '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441': '030000004705096ec15ee742e9c14aee22f5f30cb568a0817b4a6c1ec4b9010000000000afb4442dbd55dcc8c86c94dc84ca0d130aeade144623fc0e47849343087067204792e35567f20c1a4b773d0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2803be25081b4d696e656420627920416e74506f6f6c202bde53072055e392470100000000aba10000ffffffff01807c814a000000001976a9144c5220c58749e5474ec4e5bbb0c2cc0342c6f44488ac00000000' +}; + +var blockIndexes = { + '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7': { + hash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7', + chainWork: '0000000000000000000000000000000000000000000000054626b1839ade284a', + prevHash: '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4', + height: 533974 + }, + '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7': { + hash: '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7', + chainWork: '00000000000000000000000000000000000000000000000544ea52e1575ca753', + prevHash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', + height: 533951 + }, + '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441': { + hash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441', + chainWork: '00000000000000000000000000000000000000000000000544ea52e0575ba752', + prevHash: '000000000001b9c41e6c4a7b81a068b50cf3f522ee4ac1e942e75ec16e090547', + height: 533950 + } +}; + +describe('Blocks', function() { + describe('/blocks/:blockHash route', function() { + var insight = { + "hash": "0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7", + "confirmations": 119, + "size": 1011, + "height": 533974, + "version": 536870919, + "merkleroot": "b06437355844b8178173f3e18ca141472e4b0861daa81ef0f701cf9e51f0283e", + "tx": [ + "25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd", + "b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0", + "2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1" + ], + "time": 1440987503, + "nonce": 1868753784, + "bits": "1a0cf267", + "difficulty": 1295829.93087696, + "chainwork": "0000000000000000000000000000000000000000000000054626b1839ade284a", + "previousblockhash": "00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4", + "nextblockhash": "000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d", + "reward": 12.5, + "isMainChain": true + }; + + var bitcoreBlock = bitcore.Block.fromBuffer(new Buffer(blocks['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'], 'hex')); + + var todos = { + difficulty: 1295829.93087696, + nextblockhash: '000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d', + reward: 12.5 + }; + + var node = { + getBlock: sinon.stub().callsArgWith(1, null, bitcoreBlock), + services: { + bitcoind: { + getBlockIndex: sinon.stub().returns(blockIndexes['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7']) + }, + db: { + tip: { + __height: 534092 + } + } + } + }; + + + it('block data should be correct', function(done) { + var blocks = new BlockController(node); + var req = {}; + var res = {}; + var next = function() { + should.exist(req.block); + var block = _.extend(req.block, todos); + should(block).eql(insight); + done(); + }; + + var hash = '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'; + + blocks.block(req, res, next, hash); + }); + }); + + describe('/blocks route', function() { + + var insight = { + "blocks": [ + { + "height": 533951, + "size": 206, + "hash": "000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7", + "time": 1440978683, + "txlength": 1, + "poolInfo": {} + }, + { + "height": 533950, + "size": 206, + "hash": "00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441", + "time": 1440977479, + "txlength": 1, + "poolInfo": {} + } + ], + "length": 2, + "pagination": {} + }; + + var stub = sinon.stub(); + stub.onFirstCall().callsArgWith(1, null, bitcore.Block.fromBuffer(blocks['000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7'], 'hex')); + stub.onSecondCall().callsArgWith(1, null, bitcore.Block.fromBuffer(blocks['00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441'], 'hex')) + + var node = { + getBlock: stub, + services: { + bitcoind: { + getBlockIndex: function(hash) { + return blockIndexes[hash]; + } + }, + db: { + tip: { + hash: '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7' + } + } + } + }; + + it('should have correct data', function(done) { + var blocks = new BlockController(node); + + var req = { + query: { + limit: 2 + } + }; + + var res = { + jsonp: function(data) { + should(data).eql(insight); + done(); + } + }; + + blocks.list(req, res); + }); + }); +}); \ No newline at end of file