diff --git a/.gitignore b/.gitignore index 15093bfa..a477a2de 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ report *~ .idea .project -peerdb.json npm-debug.log .nodemonignore @@ -38,6 +37,10 @@ db/blocks/* db/blocks db/testnet/blocks/* db/testnet/blocks +db/* +db-test/ README.html public + +blocks diff --git a/api/controllers/blocks.js b/api/controllers/blocks.js index c7478c90..01aab381 100644 --- a/api/controllers/blocks.js +++ b/api/controllers/blocks.js @@ -23,7 +23,7 @@ Blocks.setNode = function(aNode) { * Finds a block by its hash */ Blocks.blockHashParam = function(req, res, next, blockHash) { - node.getBlock(blockHash) + node.blockService.getBlock(blockHash) .then(function(block) { req.block = block; }) @@ -38,7 +38,7 @@ Blocks.blockHashParam = function(req, res, next, blockHash) { */ Blocks.heightParam = function(req, res, next, height) { height = parseInt(height); - node.getBlock(height) + node.blockService.getBlockByHeight(height) .then(function(block) { req.block = block; }) @@ -83,7 +83,7 @@ Blocks.list = function(req, res) { }; Blocks.getLatest = function(req, res) { - node.getLatestBlock() + node.blockService.getLatest() .then(function(block) { req.block = block; Blocks.get(req, res); diff --git a/api/controllers/transactions.js b/api/controllers/transactions.js index 388f297a..2dd73599 100644 --- a/api/controllers/transactions.js +++ b/api/controllers/transactions.js @@ -25,7 +25,7 @@ Transactions.setNode = function(aNode) { * Finds a transaction by its hash */ Transactions.txHashParam = function(req, res, next, txHash) { - node.getTransaction(txHash) + node.transactionService.getTransaction(txHash) .then(function(tx) { req.tx = tx; }) diff --git a/api/test/http.js b/api/test/http.js index e4d2951d..2bcac25c 100644 --- a/api/test/http.js +++ b/api/test/http.js @@ -11,6 +11,9 @@ describe('BitcoreHTTP', function() { // mocks var opts = { + BitcoreNode: { + database: {} + }, port: 1234 }; var nodeMock; @@ -23,7 +26,7 @@ describe('BitcoreHTTP', function() { should.exist(http); }); it('from create', function() { - var http = new BitcoreHTTP.create(); + var http = new BitcoreHTTP.create(opts); should.exist(http); }); }); diff --git a/api/test/v1/blocks.js b/api/test/v1/blocks.js index b25acfa7..5cf29082 100644 --- a/api/test/v1/blocks.js +++ b/api/test/v1/blocks.js @@ -25,26 +25,28 @@ describe('BitcoreHTTP v1 blocks routes', function() { return mockBlocks[hash]; }; var last3 = _.keys(mockBlocks).splice(-3).map(blockForHash); - var some2 = _.keys(mockBlocks).splice(2,2).map(blockForHash); + var some2 = _.keys(mockBlocks).splice(2, 2).map(blockForHash); var nodeMock, app, agent; var blockList = _.values(mockBlocks); beforeEach(function() { nodeMock = new EventEmitter(); - nodeMock.getBlock = function(blockHash) { - var block; - if (typeof blockHash === 'number') { - var height = blockHash; - block = mockBlocks[_.keys(mockBlocks)[height - 100000]]; - } else { - block = mockBlocks[blockHash]; - } + nodeMock.blockService = {}; + nodeMock.blockService.resolveBlock = function(block, blockHash) { if (_.isUndefined(block)) { return Promise.reject(new BitcoreNode.errors.Blocks.NotFound(blockHash)); } return Promise.resolve(block); + }; + nodeMock.blockService.getBlockByHeight = function(height) { + var block = mockBlocks[_.keys(mockBlocks)[height - 100000]]; + return this.resolveBlock(block, height); + }; + nodeMock.blockService.getBlock = function(blockHash) { + var block = mockBlocks[blockHash]; + return this.resolveBlock(block, blockHash); }; - nodeMock.getLatestBlock = function() { + nodeMock.blockService.getLatest = function() { return Promise.resolve(lastBlock); }; nodeMock.listBlocks = function(from, to, offset, limit) { diff --git a/api/test/v1/transactions.js b/api/test/v1/transactions.js index 287573f3..bb30b3b0 100644 --- a/api/test/v1/transactions.js +++ b/api/test/v1/transactions.js @@ -23,7 +23,8 @@ describe('BitcoreHTTP v1 transactions routes', function() { var nodeMock, app, agent; beforeEach(function() { nodeMock = new EventEmitter(); - nodeMock.getTransaction = function(txHash) { + nodeMock.transactionService = {}; + nodeMock.transactionService.getTransaction = function(txHash) { var tx = mockTransactions[txHash]; if (_.isUndefined(tx)) { return Promise.reject(new BitcoreNode.errors.Transactions.NotFound(txHash)); diff --git a/config/default.yml b/config/default.yml index 5b0d42ac..48ddb954 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,12 +1,15 @@ BitcoreNode: + LevelUp: ./db + network: livenet NetworkMonitor: - network: livenet host: localhost port: 8333 -Reporter: simple # none, simple, matrix -LevelUp: ./db +Reporter: none # none, simple, matrix +BitcoreHTTP: + host: localhost + port: 8080 RPC: - user: username + user: user pass: password protocol: http host: 127.0.0.1 diff --git a/http/controllers/addresses.js b/http/controllers/addresses.js deleted file mode 100644 index 30cfca3c..00000000 --- a/http/controllers/addresses.js +++ /dev/null @@ -1,206 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var Address = require('../models/Address'); -var common = require('./common'); -var async = require('async'); - -var tDb = require('../../lib/TransactionDb').default(); - -var getAddr = function(req, res, next) { - var a; - try { - var addr = req.param('addr'); - a = new Address(addr); - } catch (e) { - common.handleErrors({ - message: 'Invalid address:' + e.message, - code: 1 - }, res, next); - return null; - } - return a; -}; - -var getAddrs = function(req, res, next) { - var as = []; - try { - var addrStrs = req.param('addrs'); - var s = addrStrs.split(','); - if (s.length === 0) return as; - for (var i = 0; i < s.length; i++) { - var a = new Address(s[i]); - as.push(a); - } - } catch (e) { - common.handleErrors({ - message: 'Invalid address:' + e.message, - code: 1 - }, res, next); - return null; - } - return as; -}; - -exports.show = function(req, res, next) { - var a = getAddr(req, res, next); - - if (a) { - a.update(function(err) { - if (err) { - return common.handleErrors(err, res); - } else { - return res.jsonp(a.getObj()); - } - }, {txLimit: req.query.noTxList?0:-1, ignoreCache: req.param('noCache')}); - } -}; - - - -exports.utxo = function(req, res, next) { - var a = getAddr(req, res, next); - if (a) { - a.update(function(err) { - if (err) - return common.handleErrors(err, res); - else { - return res.jsonp(a.unspent); - } - }, {onlyUnspent:1, ignoreCache: req.param('noCache')}); - } -}; - -exports.multiutxo = function(req, res, next) { - var as = getAddrs(req, res, next); - if (as) { - var utxos = []; - async.each(as, function(a, callback) { - a.update(function(err) { - if (err) callback(err); - utxos = utxos.concat(a.unspent); - callback(); - }, {onlyUnspent:1, ignoreCache: req.param('noCache')}); - }, function(err) { // finished callback - if (err) return common.handleErrors(err, res); - res.jsonp(utxos); - }); - } -}; - -exports.multitxs = function(req, res, next) { - - function processTxs(txs, from, to, cb) { - txs = _.uniq(_.flatten(txs), 'txid'); - var nbTxs = txs.length; - var paginated = !_.isUndefined(from) || !_.isUndefined(to); - - if (paginated) { - txs.sort(function(a, b) { - return (b.ts || b.ts) - (a.ts || a.ts); - }); - var start = Math.max(from || 0, 0); - var end = Math.min(to || txs.length, txs.length); - txs = txs.slice(start, end); - } - - var txIndex = {}; - _.each(txs, function (tx) { txIndex[tx.txid] = tx; }); - - async.each(txs, function (tx, callback) { - tDb.fromIdWithInfo(tx.txid, function(err, tx) { - if (err) console.log(err); - if (tx && tx.info) { - txIndex[tx.txid].info = tx.info; - } - callback(); - }); - }, function (err) { - if (err) return cb(err); - - var transactions = _.pluck(txs, 'info'); - if (paginated) { - transactions = { - totalItems: nbTxs, - from: +from, - to: +to, - items: transactions, - }; - } - return cb(null, transactions); - }); - }; - - var from = req.param('from'); - var to = req.param('to'); - - var as = getAddrs(req, res, next); - if (as) { - var txs = []; - async.eachLimit(as, 10, function(a, callback) { - a.update(function(err) { - if (err) callback(err); - txs.push(a.transactions); - callback(); - }, {ignoreCache: req.param('noCache'), includeTxInfo: true}); - }, function(err) { // finished callback - if (err) return common.handleErrors(err, res); - processTxs(txs, from, to, function (err, transactions) { - if (err) return common.handleErrors(err, res); - res.jsonp(transactions); - }); - }); - } -}; - -exports.balance = function(req, res, next) { - var a = getAddr(req, res, next); - if (a) - a.update(function(err) { - if (err) { - return common.handleErrors(err, res); - } else { - return res.jsonp(a.balanceSat); - } - }, {ignoreCache: req.param('noCache')}); -}; - -exports.totalReceived = function(req, res, next) { - var a = getAddr(req, res, next); - if (a) - a.update(function(err) { - if (err) { - return common.handleErrors(err, res); - } else { - return res.jsonp(a.totalReceivedSat); - } - }, {ignoreCache: req.param('noCache')}); -}; - -exports.totalSent = function(req, res, next) { - var a = getAddr(req, res, next); - if (a) - a.update(function(err) { - if (err) { - return common.handleErrors(err, res); - } else { - return res.jsonp(a.totalSentSat); - } - }, {ignoreCache: req.param('noCache')}); -}; - -exports.unconfirmedBalance = function(req, res, next) { - var a = getAddr(req, res, next); - if (a) - a.update(function(err) { - if (err) { - return common.handleErrors(err, res); - } else { - return res.jsonp(a.unconfirmedBalanceSat); - } - }, {ignoreCache: req.param('noCache')}); -}; diff --git a/http/controllers/blocks.js b/http/controllers/blocks.js deleted file mode 100644 index 2de390d4..00000000 --- a/http/controllers/blocks.js +++ /dev/null @@ -1,176 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var common = require('./common'), - async = require('async'), - BlockDb = require('../../lib/BlockDb'), - TransactionDb = require('../../lib/TransactionDb'); - -var bdb = new BlockDb(); -var tdb = new TransactionDb(); - -/** - * Find block by hash ... - */ -exports.block = function(req, res, next, hash) { - bdb.fromHashWithInfo(hash, function(err, block) { - if (err || !block) - return common.handleErrors(err, res, next); - else { - tdb.getPoolInfo(block.info.tx[0], function(info) { - block.info.poolInfo = info; - req.block = block.info; - return next(); - }); - } - }); -}; - - -/** - * Show block - */ -exports.show = function(req, res) { - if (req.block) { - res.jsonp(req.block); - } -}; - -/** - * Show block by Height - */ -exports.blockindex = function(req, res, next, height) { - bdb.blockIndex(height, function(err, hashStr) { - if (err) { - console.log(err); - res.status(400).send('Bad Request'); // TODO - } else { - res.jsonp(hashStr); - } - }); -}; - -var getBlock = function(blockhash, cb) { - bdb.fromHashWithInfo(blockhash, function(err, block) { - if (err) { - console.log(err); - return cb(err); - } - - // TODO - if (!block.info) { - console.log('Could not get %s from RPC. Orphan? Error?', blockhash); //TODO - // Probably orphan - block.info = { - hash: blockhash, - isOrphan: 1, - }; - } - - tdb.getPoolInfo(block.info.tx[0], function(info) { - block.info.poolInfo = info; - return cb(err, block.info); - }); - - }); -}; - -/** - * List of blocks by date - */ - -var DFLT_LIMIT=200; - // in testnet, this number is much bigger, we dont support - // exploring blocks by date. - -exports.list = function(req, res) { - var isToday = false; - - //helper to convert timestamps to yyyy-mm-dd format - var formatTimestamp = function(date) { - var yyyy = date.getUTCFullYear().toString(); - var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based - var dd = date.getUTCDate().toString(); - - return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding - }; - - var dateStr; - var todayStr = formatTimestamp(new Date()); - - if (req.query.blockDate) { - // TODO: Validate format yyyy-mm-dd - dateStr = req.query.blockDate; - isToday = dateStr === todayStr; - } else { - dateStr = todayStr; - isToday = true; - } - var gte = Math.round((new Date(dateStr)).getTime() / 1000); - - //pagination - var lte = parseInt(req.query.startTimestamp) || gte + 86400; - var prev = formatTimestamp(new Date((gte - 86400) * 1000)); - var next = lte ? formatTimestamp(new Date(lte * 1000)) :null; - var limit = parseInt(req.query.limit || DFLT_LIMIT) + 1; - var more; - - bdb.getBlocksByDate(gte, lte, limit, function(err, blockList) { - - if (err) { - res.status(500).send(err); - } else { - var l = blockList.length; - - if (l===limit) { - more = true; - blockList.pop; - } - - var moreTs=lte; - async.mapSeries(blockList, - function(b, cb) { - getBlock(b.hash, function(err, info) { - if (err) { - console.log(err); - return cb(err); - } - if (b.ts < moreTs) moreTs = b.ts; - return cb(err, { - height: info.height, - size: info.size, - hash: b.hash, - time: b.ts || info.time, - txlength: info.tx.length, - poolInfo: info.poolInfo - }); - }); - }, function(err, allblocks) { - - // sort blocks by height - allblocks.sort( - function compare(a,b) { - if (a.height < b.height) return 1; - if (a.height > b.height) return -1; - return 0; - }); - - res.jsonp({ - blocks: allblocks, - length: allblocks.length, - pagination: { - next: next, - prev: prev, - currentTs: lte - 1, - current: dateStr, - isToday: isToday, - more: more, - moreTs: moreTs, - } - }); - }); - } - }); -}; diff --git a/http/controllers/common.js b/http/controllers/common.js deleted file mode 100644 index b44756bf..00000000 --- a/http/controllers/common.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - - -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/http/controllers/currency.js b/http/controllers/currency.js deleted file mode 100644 index 244ef327..00000000 --- a/http/controllers/currency.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -var config = require('../../config/config'); - -// Set the initial vars -var timestamp = +new Date(), - delay = config.currencyRefresh * 60000, - bitstampRate = 0; - -exports.index = function(req, res) { - - var _xhr = function() { - if (typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest !== null) { - return new XMLHttpRequest(); - } else if (typeof require !== 'undefined' && require !== null) { - var XMLhttprequest = require('xmlhttprequest').XMLHttpRequest; - return new XMLhttprequest(); - } - }; - - var _request = function(url, cb) { - var request; - request = _xhr(); - request.open('GET', url, true); - request.onreadystatechange = function() { - if (request.readyState === 4) { - if (request.status === 200) { - return cb(false, request.responseText); - } - - return cb(true, { - status: request.status, - message: 'Request error' - }); - } - }; - - return request.send(null); - }; - - // Init - var currentTime = +new Date(); - if (bitstampRate === 0 || currentTime >= (timestamp + delay)) { - timestamp = currentTime; - - _request('https://www.bitstamp.net/api/ticker/', function(err, data) { - if (!err) bitstampRate = parseFloat(JSON.parse(data).last); - - res.jsonp({ - status: 200, - data: { bitstamp: bitstampRate } - }); - }); - } else { - res.jsonp({ - status: 200, - data: { bitstamp: bitstampRate } - }); - } -}; diff --git a/http/controllers/index.js b/http/controllers/index.js deleted file mode 100644 index d5f51bb3..00000000 --- a/http/controllers/index.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -var config = require('../../config/config'); - -var _getVersion = function() { - var pjson = require('../../package.json'); - return pjson.version; -}; - -exports.render = function(req, res) { - - if (config.publicPath) { - return res.sendfile(config.publicPath + '/index.html'); - } - else { - var version = _getVersion(); - res.send('bitcore-node API v' + version); - } -}; - -exports.version = function(req, res) { - var version = _getVersion(); - res.json({ - version: version - }); -}; diff --git a/http/controllers/messages.js b/http/controllers/messages.js deleted file mode 100644 index 5c2fbfc6..00000000 --- a/http/controllers/messages.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var common = require('./common'); -var Rpc = require('../../lib/Rpc'); - - -exports.verify = function(req, res) { - var address = req.param('address'), - signature = req.param('signature'), - message = req.param('message'); - - if(typeof(address) == 'undefined' - || typeof(signature) == 'undefined' - || typeof(message) == 'undefined') { - return common.handleErrors({ - message: 'Missing parameters (expected "address", "signature" and "message")', - code: 1 - }, res); - } - - Rpc.verifyMessage(address, signature, message, function(err, result) { - if (err) { - return common.handleErrors(err, res); - } - res.json({'result' : result}); - }); -}; diff --git a/http/controllers/status.js b/http/controllers/status.js deleted file mode 100644 index 322135fb..00000000 --- a/http/controllers/status.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Status = require('../models/Status'), - common = require('./common'); - -/** - * Status - */ -exports.show = function(req, res) { - - if (! req.query.q) { - res.status(400).send('Bad Request'); - } - else { - var option = req.query.q; - var statusObject = new Status(); - - var returnJsonp = function (err) { - if (err || ! statusObject) - return common.handleErrors(err, res); - else { - res.jsonp(statusObject); - } - }; - - switch(option) { - case 'getInfo': - statusObject.getInfo(returnJsonp); - break; - case 'getDifficulty': - statusObject.getDifficulty(returnJsonp); - break; - case 'getTxOutSetInfo': - statusObject.getTxOutSetInfo(returnJsonp); - break; - case 'getLastBlockHash': - statusObject.getLastBlockHash(returnJsonp); - break; - case 'getBestBlockHash': - statusObject.getBestBlockHash(returnJsonp); - break; - default: - res.status(400).send('Bad Request'); - } - } -}; - -exports.sync = function(req, res) { - if (req.historicSync) - res.jsonp(req.historicSync.info()); -}; - -exports.peer = function(req, res) { - if (req.peerSync) { - var info = req.peerSync.info(); - res.jsonp(info); - } -}; diff --git a/http/controllers/transactions.js b/http/controllers/transactions.js deleted file mode 100644 index ed01c329..00000000 --- a/http/controllers/transactions.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var Address = require('../models/Address'); -var async = require('async'); -var common = require('./common'); -var util = require('util'); - -var Rpc = require('../../lib/Rpc'); - -var tDb = require('../../lib/TransactionDb').default(); -var bdb = require('../../lib/BlockDb').default(); - -exports.send = function(req, res) { - Rpc.sendRawTransaction(req.body.rawtx, function(err, txid) { - if (err) { - var message; - if(err.code == -25) { - message = util.format( - 'Generic error %s (code %s)', - err.message, err.code); - } else if(err.code == -26) { - message = util.format( - 'Transaction rejected by network (code %s). Reason: %s', - err.code, err.message); - } else { - message = util.format('%s (code %s)', err.message, err.code); - } - return res.status(400).send(message); - } - res.json({'txid' : txid}); - }); -}; - - -/** - * Find transaction by hash ... - */ -exports.transaction = function(req, res, next, txid) { - - tDb.fromIdWithInfo(txid, function(err, tx) { - if (err || ! tx) - return common.handleErrors(err, res); - else { - req.transaction = tx.info; - return next(); - } - }); -}; - - -/** - * Show transaction - */ -exports.show = function(req, res) { - - if (req.transaction) { - res.jsonp(req.transaction); - } -}; - - -var getTransaction = function(txid, cb) { - - tDb.fromIdWithInfo(txid, function(err, tx) { - if (err) console.log(err); - - if (!tx || !tx.info) { - console.log('[transactions.js.48]:: TXid %s not found in RPC. CHECK THIS.', txid); - return ({ txid: txid }); - } - - return cb(null, tx.info); - }); -}; - - -/** - * List of transaction - */ -exports.list = function(req, res, next) { - var bId = req.query.block; - var addrStr = req.query.address; - var page = req.query.pageNum; - var pageLength = 10; - var pagesTotal = 1; - var txLength; - var txs; - - if (bId) { - bdb.fromHashWithInfo(bId, function(err, block) { - if (err) { - console.log(err); - return res.status(500).send('Internal Server Error'); - } - - if (! block) { - return res.status(404).send('Not found'); - } - - txLength = block.info.tx.length; - - if (page) { - var spliceInit = page * pageLength; - txs = block.info.tx.splice(spliceInit, pageLength); - pagesTotal = Math.ceil(txLength / pageLength); - } - else { - txs = block.info.tx; - } - - async.mapSeries(txs, getTransaction, function(err, results) { - if (err) { - console.log(err); - res.status(404).send('TX not found'); - } - - res.jsonp({ - pagesTotal: pagesTotal, - txs: results - }); - }); - }); - } - else if (addrStr) { - var a = new Address(addrStr); - - a.update(function(err) { - if (err && !a.totalReceivedSat) { - console.log(err); - res.status(404).send('Invalid address'); - return next(); - } - - txLength = a.transactions.length; - - if (page) { - var spliceInit = page * pageLength; - txs = a.transactions.splice(spliceInit, pageLength); - pagesTotal = Math.ceil(txLength / pageLength); - } - else { - txs = a.transactions; - } - - async.mapSeries(txs, getTransaction, function(err, results) { - if (err) { - console.log(err); - res.status(404).send('TX not found'); - } - - res.jsonp({ - pagesTotal: pagesTotal, - txs: results - }); - }); - }); - } - else { - res.jsonp({ - txs: [] - }); - } -}; diff --git a/http/express.js b/http/express.js deleted file mode 100644 index 8de62c81..00000000 --- a/http/express.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var express = require('express'); -var config = require('./config'); -var path = require('path'); -var logger = require('../lib/logger').logger; - -module.exports = function(app, historicSync, peerSync) { - - - //custom middleware - var setHistoric = function(req, res, next) { - req.historicSync = historicSync; - next(); - }; - - var setPeer = function(req, res, next) { - req.peerSync = peerSync; - next(); - }; - - app.set('showStackError', true); - app.set('json spaces', 0); - - app.enable('jsonp callback'); - app.use(config.apiPrefix + '/sync', setHistoric); - app.use(config.apiPrefix + '/peer', setPeer); - app.use(express.logger('dev')); - app.use(express.json()); - app.use(express.urlencoded()); - app.use(express.methodOverride()); - app.use(express.compress()); - - if (config.enableEmailstore) { - var allowCopayCrossDomain = function(req, res, next) { - if ('OPTIONS' == req.method) { - res.send(200); - res.end(); - return; - } - next(); - } - app.use(allowCopayCrossDomain); - } - - if (config.publicPath) { - var staticPath = path.normalize(config.rootPath + '/../' + config.publicPath); - //IMPORTANT: for html5mode, this line must to be before app.router - app.use(express.static(staticPath)); - } - - app.use(function(req, res, next) { - app.locals.config = config; - next(); - }); - - //routes should be at the last - app.use(app.router); - - //Assume 404 since no middleware responded - app.use(function(req, res) { - res.status(404).jsonp({ - status: 404, - url: req.originalUrl, - error: 'Not found' - }); - }); -}; diff --git a/http/headers.js b/http/headers.js deleted file mode 100644 index 8cd9e73c..00000000 --- a/http/headers.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -var logger = require('../lib/logger').logger; - -module.exports = function(app) { - - app.use(function(req, res, next) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization'); - res.setHeader('Access-Control-Expose-Headers', 'X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining'); - next(); - }); -}; diff --git a/http/models/Address.js b/http/models/Address.js deleted file mode 100644 index 1cfe7e76..00000000 --- a/http/models/Address.js +++ /dev/null @@ -1,211 +0,0 @@ -'use strict'; - -var imports = require('soop').imports(); -var async = require('async'); -var bitcore = require('bitcore'); -var BitcoreAddress = bitcore.Address; -var BitcoreTransaction = bitcore.Transaction; -var BitcoreUtil = bitcore.util; -var Parser = bitcore.BinaryParser; -var Buffer = bitcore.Buffer; -var TransactionDb = imports.TransactionDb || require('../../lib/TransactionDb').default(); -var BlockDb = imports.BlockDb || require('../../lib/BlockDb').default(); -var config = require('../../config/config'); -var CONCURRENCY = 5; - -function Address(addrStr) { - this.balanceSat = 0; - this.totalReceivedSat = 0; - this.totalSentSat = 0; - - this.unconfirmedBalanceSat = 0; - - this.txApperances = 0; - this.unconfirmedTxApperances= 0; - this.seen = {}; - - // TODO store only txids? +index? +all? - this.transactions = []; - this.unspent = []; - - var a = new BitcoreAddress(addrStr); - a.validate(); - this.addrStr = addrStr; - - Object.defineProperty(this, 'totalSent', { - get: function() { - return parseFloat(this.totalSentSat) / parseFloat(BitcoreUtil.COIN); - }, - set: function(i) { - this.totalSentSat = i * BitcoreUtil.COIN; - }, - enumerable: 1, - }); - - Object.defineProperty(this, 'balance', { - get: function() { - return parseFloat(this.balanceSat) / parseFloat(BitcoreUtil.COIN); - }, - set: function(i) { - this.balance = i * BitcoreUtil.COIN; - }, - enumerable: 1, - }); - - Object.defineProperty(this, 'totalReceived', { - get: function() { - return parseFloat(this.totalReceivedSat) / parseFloat(BitcoreUtil.COIN); - }, - set: function(i) { - this.totalReceived = i * BitcoreUtil.COIN; - }, - enumerable: 1, - }); - - - Object.defineProperty(this, 'unconfirmedBalance', { - get: function() { - return parseFloat(this.unconfirmedBalanceSat) / parseFloat(BitcoreUtil.COIN); - }, - set: function(i) { - this.unconfirmedBalanceSat = i * BitcoreUtil.COIN; - }, - enumerable: 1, - }); - -} - -Address.prototype.getObj = function() { - // Normalize json address - return { - 'addrStr': this.addrStr, - 'balance': this.balance, - 'balanceSat': this.balanceSat, - 'totalReceived': this.totalReceived, - 'totalReceivedSat': this.totalReceivedSat, - 'totalSent': this.totalSent, - 'totalSentSat': this.totalSentSat, - 'unconfirmedBalance': this.unconfirmedBalance, - 'unconfirmedBalanceSat': this.unconfirmedBalanceSat, - 'unconfirmedTxApperances': this.unconfirmedTxApperances, - 'txApperances': this.txApperances, - 'transactions': this.transactions - }; -}; - -Address.prototype._addTxItem = function(txItem, txList, includeInfo) { - function addTx(data) { - if (!txList) return; - if (includeInfo) { - txList.push(data); - } else { - txList.push(data.txid); - } - }; - - var add=0, addSpend=0; - var v = txItem.value_sat; - var seen = this.seen; - - // Founding tx - if (!seen[txItem.txid]) { - seen[txItem.txid] = 1; - add = 1; - - addTx({ txid: txItem.txid, ts: txItem.ts }); - } - - // Spent tx - if (txItem.spentTxId && !seen[txItem.spentTxId] ) { - addTx({ txid: txItem.spentTxId, ts: txItem.spentTs }); - seen[txItem.spentTxId]=1; - addSpend=1; - } - if (txItem.isConfirmed) { - this.txApperances += add; - this.totalReceivedSat += v; - if (! txItem.spentTxId ) { - //unspent - this.balanceSat += v; - } - else if(!txItem.spentIsConfirmed) { - // unspent - this.balanceSat += v; - this.unconfirmedBalanceSat -= v; - this.unconfirmedTxApperances += addSpend; - } - else { - // spent - this.totalSentSat += v; - this.txApperances += addSpend; - } - } - else { - this.unconfirmedBalanceSat += v; - this.unconfirmedTxApperances += add; - } -}; - -// opts are -// .onlyUnspent -// .txLimit (=0 -> no txs, => -1 no limit) -// .includeTxInfo -// -Address.prototype.update = function(next, opts) { - var self = this; - if (!self.addrStr) return next(); - opts = opts || {}; - - if (! ('ignoreCache' in opts) ) - opts.ignoreCache = config.ignoreCache; - - // should collect txList from address? - var txList = opts.txLimit === 0 ? null: []; - - var tDb = TransactionDb; - var bDb = BlockDb; - tDb.fromAddr(self.addrStr, opts, function(err,txOut){ - if (err) return next(err); - - bDb.fillConfirmations(txOut, function(err) { - if (err) return next(err); - - tDb.cacheConfirmations(txOut, function(err) { -// console.log('[Address.js.161:txOut:]',txOut); //TODO - if (err) return next(err); - if (opts.onlyUnspent) { - txOut = txOut.filter(function(x){ - return !x.spentTxId; - }); - tDb.fillScriptPubKey(txOut, function() { - self.unspent = txOut.map(function(x){ - return { - address: self.addrStr, - txid: x.txid, - vout: x.index, - ts: x.ts, - scriptPubKey: x.scriptPubKey, - amount: x.value_sat / BitcoreUtil.COIN, - confirmations: x.isConfirmedCached ? (config.safeConfirmations) : x.confirmations, - confirmationsFromCache: !!x.isConfirmedCached, - }; - }); - return next(); - }); - } - else { - txOut.forEach(function(txItem){ - self._addTxItem(txItem, txList, opts.includeTxInfo); - }); - if (txList) - self.transactions = txList; - - return next(); - } - }); - }); - }); -}; - -module.exports = require('soop')(Address); - diff --git a/http/models/Status.js b/http/models/Status.js deleted file mode 100644 index ffb744ad..00000000 --- a/http/models/Status.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; -//var imports = require('soop').imports(); - -var async = require('async'); -var bitcore = require('bitcore'); -var RpcClient = bitcore.RpcClient; -var config = require('../../config/config'); -var rpc = new RpcClient(config.bitcoind); -var bDb = require('../../lib/BlockDb').default(); - -function Status() {} - -Status.prototype.getInfo = function(next) { - var that = this; - async.series([ - function (cb) { - rpc.getInfo(function(err, info){ - if (err) return cb(err); - - that.info = info.result; - return cb(); - }); - }, - ], function (err) { - return next(err); - }); -}; - -Status.prototype.getDifficulty = function(next) { - var that = this; - async.series([ - function (cb) { - rpc.getDifficulty(function(err, df){ - if (err) return cb(err); - - that.difficulty = df.result; - return cb(); - }); - } - ], function (err) { - return next(err); - }); -}; - -Status.prototype.getTxOutSetInfo = function(next) { - var that = this; - async.series([ - function (cb) { - rpc.getTxOutSetInfo(function(err, txout){ - if (err) return cb(err); - - that.txoutsetinfo = txout.result; - return cb(); - }); - } - ], function (err) { - return next(err); - }); -}; - -Status.prototype.getBestBlockHash = function(next) { - var that = this; - async.series([ - function (cb) { - rpc.getBestBlockHash(function(err, bbh){ - if (err) return cb(err); - - that.bestblockhash = bbh.result; - return cb(); - }); - }, - - ], function (err) { - return next(err); - }); -}; - -Status.prototype.getLastBlockHash = function(next) { - var that = this; - bDb.getTip(function(err,tip) { - that.syncTipHash = tip; - async.waterfall( - [ - function(callback){ - rpc.getBlockCount(function(err, bc){ - if (err) return callback(err); - callback(null, bc.result); - }); - }, - function(bc, callback){ - rpc.getBlockHash(bc, function(err, bh){ - if (err) return callback(err); - callback(null, bh.result); - }); - } - ], - function (err, result) { - that.lastblockhash = result; - return next(); - } - ); - }); -}; - -module.exports = require('soop')(Status); diff --git a/http/routes.js b/http/routes.js deleted file mode 100644 index eeedf7de..00000000 --- a/http/routes.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var config = require('./config'); - -module.exports = function(app) { - - var apiPrefix = config.apiPrefix; - - //Block routes - var blocks = require('../app/controllers/blocks'); - app.get(apiPrefix + '/blocks', blocks.list); - - - app.get(apiPrefix + '/block/:blockHash', blocks.show); - app.param('blockHash', blocks.block); - - app.get(apiPrefix + '/block-index/:height', blocks.blockindex); - app.param('height', blocks.blockindex); - - // Transaction routes - var transactions = require('../app/controllers/transactions'); - app.get(apiPrefix + '/tx/:txid', transactions.show); - app.param('txid', transactions.transaction); - app.get(apiPrefix + '/txs', transactions.list); - app.post(apiPrefix + '/tx/send', transactions.send); - - // Address routes - var addresses = require('../app/controllers/addresses'); - app.get(apiPrefix + '/addr/:addr', addresses.show); - app.get(apiPrefix + '/addr/:addr/utxo', addresses.utxo); - app.get(apiPrefix + '/addrs/:addrs/utxo', addresses.multiutxo); - app.post(apiPrefix + '/addrs/utxo', addresses.multiutxo); - app.get(apiPrefix + '/addrs/:addrs/txs', addresses.multitxs); - app.post(apiPrefix + '/addrs/txs', addresses.multitxs); - - // Address property routes - app.get(apiPrefix + '/addr/:addr/balance', addresses.balance); - app.get(apiPrefix + '/addr/:addr/totalReceived', addresses.totalReceived); - app.get(apiPrefix + '/addr/:addr/totalSent', addresses.totalSent); - app.get(apiPrefix + '/addr/:addr/unconfirmedBalance', addresses.unconfirmedBalance); - - // Status route - var st = require('../app/controllers/status'); - app.get(apiPrefix + '/status', st.show); - - app.get(apiPrefix + '/sync', st.sync); - app.get(apiPrefix + '/peer', st.peer); - - // Currency - var currency = require('../app/controllers/currency'); - app.get(apiPrefix + '/currency', currency.index); - - // 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(apiPrefix + '/version', index.version); - app.get('*', index.render); -}; diff --git a/index.js b/index.js index aae7b25e..8ffbaab9 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,16 @@ var BitcoreNode = require('./lib/node'); var reporters = require('./lib/reporters'); +var bitcore = require('bitcore'); +var Promise = require('bluebird'); +Promise.longStackTraces(); + +BitcoreNode.errors = require('./lib/errors'); if (require.main === module) { var config = require('config'); + bitcore.Networks.defaultNetwork = bitcore.Networks.get(config.get('BitcoreNode').network); + var node = BitcoreNode.create(config.get('BitcoreNode')); node.start(); node.on('error', function(err) { @@ -14,6 +21,10 @@ if (require.main === module) { console.log('Error: ', err); } }); + process.on('SIGINT', function() { + node.stop(); + process.exit(); + }); var reporterName = config.get('Reporter'); var reporter = reporters[reporterName]; @@ -24,7 +35,4 @@ if (require.main === module) { node.on('Transaction', reporter); } - -BitcoreNode.errors = require('./lib/errors'); - module.exports = BitcoreNode; diff --git a/lib/Rpc.js b/lib/Rpc.js deleted file mode 100644 index f81f3721..00000000 --- a/lib/Rpc.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -var imports = require('soop').imports(); - -var bitcore = require('bitcore'), - RpcClient = bitcore.RpcClient, - BitcoreBlock = bitcore.Block, - util = require('util'), - config = require('../config/config'); - -var bitcoreRpc = imports.bitcoreRpc || new RpcClient(config.bitcoind); - -function Rpc() { -} - -Rpc._parseTxResult = function(info) { - var b = new Buffer(info.hex,'hex'); - - // remove fields we dont need, to speed and adapt the information - delete info.hex; - - // Inputs => add index + coinBase flag - var n =0; - info.vin.forEach(function(i) { - i.n = n++; - if (i.coinbase) info.isCoinBase = true; - }); - - // Outputs => add total - var valueOutSat = 0; - info.vout.forEach( function(o) { - o.value = o.value.toFixed(8); - valueOutSat += o.value * bitcore.util.COIN; - }); - info.valueOut = valueOutSat.toFixed(0) / bitcore.util.COIN; - info.size = b.length; - - return info; -}; - - -Rpc.errMsg = function(err) { - var e = err; - e.message += util.format(' [Host: %s:%d User:%s Using password:%s]', - bitcoreRpc.host, - bitcoreRpc.port, - bitcoreRpc.user, - bitcoreRpc.pass?'yes':'no' - ); - return e; -}; - -Rpc.getTxInfo = function(txid, doNotParse, cb) { - var self = this; - - if (typeof doNotParse === 'function') { - cb = doNotParse; - doNotParse = false; - } - - bitcoreRpc.getRawTransaction(txid, 1, function(err, txInfo) { - // Not found? - if (err && err.code === -5) return cb(); - if (err) return cb(self.errMsg(err)); - - var info = doNotParse ? txInfo.result : self._parseTxResult(txInfo.result); - return cb(null,info); - }); -}; - - -Rpc.blockIndex = function(height, cb) { - var self = this; - - bitcoreRpc.getBlockHash(height, function(err, bh){ - if (err) return cb(self.errMsg(err)); - cb(null, { blockHash: bh.result }); - }); -}; - -Rpc.getBlock = function(hash, cb) { - var self = this; - - bitcoreRpc.getBlock(hash, function(err,info) { - // Not found? - if (err && err.code === -5) return cb(); - if (err) return cb(self.errMsg(err)); - - - if (info.result.height) - info.result.reward = BitcoreBlock.getBlockValue(info.result.height) / bitcore.util.COIN ; - - return cb(err,info.result); - }); -}; - -Rpc.sendRawTransaction = function(rawtx, cb) { - bitcoreRpc.sendRawTransaction(rawtx, function(err, txid) { - if (err) return cb(err); - - return cb(err, txid.result); - }); -}; - -Rpc.verifyMessage = function(address, signature, message, cb) { - var self = this; - bitcoreRpc.verifyMessage(address, signature, message, function(err, message) { - if (err && (err.code === -3 || err.code === -5)) - return cb(err); // -3 = invalid address, -5 = malformed base64 / etc. - if (err) - return cb(self.errMsg(err)); - - return cb(err, message.result); - }); -}; - -module.exports = require('soop')(Rpc); - - diff --git a/lib/blockchain.js b/lib/blockchain.js new file mode 100644 index 00000000..c40de043 --- /dev/null +++ b/lib/blockchain.js @@ -0,0 +1,166 @@ +'use strict'; + +var bitcore = require('bitcore'); +var $ = bitcore.util.preconditions; +var _ = bitcore.deps._; + +var NULL = '0000000000000000000000000000000000000000000000000000000000000000'; + +function BlockChain() { + this.tip = NULL; + this.work = {}; + this.work[NULL] = 0; + this.height = {}; + this.height[NULL] = -1; + this.hashByHeight = { '-1': NULL }; + this.next = {}; + this.prev = {}; +} + +BlockChain.NULL = NULL; + +BlockChain.fromObject = function(obj) { + var blockchain = new BlockChain(); + blockchain.tip = obj.tip; + blockchain.work = obj.work; + blockchain.hashByHeight = obj.hashByHeight; + blockchain.height = obj.height; + blockchain.next = obj.next; + blockchain.prev = obj.prev; + return blockchain; +}; + +var getWork = function(bits) { + var bytes = ((bits >>> 24) & 0xff) >>> 0; + return ((bits & 0xffffff) << (8 * (bytes - 3))) >>> 0; +}; + +BlockChain.prototype.addData = function(block) { + $.checkArgument(block instanceof bitcore.Block, 'Argument is not a Block instance'); + + var prevHash = bitcore.util.buffer.reverse(block.header.prevHash).toString('hex'); + + this.work[block.hash] = this.work[prevHash] + getWork(block.header.bits); + this.prev[block.hash] = prevHash; +}; + +BlockChain.prototype.proposeNewBlock = function(block) { + $.checkArgument(block instanceof bitcore.Block, 'Argument is not a Block instance'); + var prevHash = bitcore.util.buffer.reverse(block.header.prevHash).toString('hex'); + + if (_.isUndefined(this.work[prevHash])) { + throw new Error('No previous data to estimate work'); + } + this.addData(block); + + if (this.work[block.hash] > this.work[this.tip]) { + + var toUnconfirm = []; + var toConfirm = []; + var commonAncestor; + + var pointer = block.hash; + while (_.isUndefined(this.height[pointer])) { + toConfirm.push(pointer); + pointer = this.prev[pointer]; + } + commonAncestor = pointer; + + pointer = this.tip; + while (pointer !== commonAncestor) { + toUnconfirm.push(pointer); + pointer = this.prev[pointer]; + } + + toConfirm.reverse(); + + var self = this; + toUnconfirm.map(function(hash) { + self.unconfirm(hash); + }); + toConfirm.map(function(hash) { + self.confirm(hash); + }); + return { + unconfirmed: toUnconfirm, + confirmed: toConfirm + }; + } + return { + unconfirmed: [], + confirmed: [] + }; +}; + +BlockChain.prototype.confirm = function(hash) { + var prevHash = this.prev[hash]; + $.checkState(prevHash === this.tip); + + this.tip = hash; + var height = this.height[prevHash] + 1; + this.next[prevHash] = hash; + this.hashByHeight[height] = hash; + this.height[hash] = height; +}; + +BlockChain.prototype.unconfirm = function(hash) { + var prevHash = this.prev[hash]; + $.checkState(hash === this.tip); + + this.tip = prevHash; + var height = this.height[hash]; + delete this.next[prevHash]; + delete this.hashByHeight[height]; + delete this.height[hash]; +}; + +BlockChain.prototype.getBlockLocator = function() { + $.checkState(this.tip); + $.checkState(!_.isUndefined(this.height[this.tip])); + + var result = []; + var currentHeight = this.height[this.tip]; + var exponentialBackOff = 1; + for (var i = 0; i < 10; i++) { + if (currentHeight >= 0) { + result.push(this.hashByHeight[currentHeight--]); + } + } + while (currentHeight > 0) { + result.push(this.hashByHeight[currentHeight]); + currentHeight -= exponentialBackOff; + exponentialBackOff *= 2; + } + return result; +}; + +BlockChain.prototype.hasData = function(hash) { + return !_.isUndefined(this.work[hash]); +}; + +BlockChain.prototype.prune = function() { + var self = this; + _.each(this.prev, function(key, value) { + if (!self.height[key]) { + delete this.prev[key]; + delete this.work[key]; + } + }); +}; + +BlockChain.prototype.toObject = function() { + return { + tip: this.tip, + work: this.work, + next: this.next, + hashByHeight: this.hashByHeight, + height: this.height, + prev: this.prev + }; +}; + +BlockChain.prototype.toJSON = function() { + return JSON.stringify(this.toObject()); +}; + +module.exports = BlockChain; diff --git a/lib/data/genesis.js b/lib/data/genesis.js new file mode 100644 index 00000000..46bf2f59 --- /dev/null +++ b/lib/data/genesis.js @@ -0,0 +1,13 @@ +module.exports = { + livenet: new Buffer('010000000000000000000000000000000000000000000000000000000000' + + '0000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a5132' + + '3a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c01010000000100000000' + + '00000000000000000000000000000000000000000000000000000000ffff' + + 'ffff4d04ffff001d0104455468652054696d65732030332f4a616e2f3230' + + '3039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f' + + '6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01' + + '000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a6' + + '7962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b' + + '8d578a4c702b6bf11d5fac00000000', 'hex'), + testnet: new Buffer('') +} diff --git a/lib/eventbus.js b/lib/eventbus.js index 2859d9a8..750c41b2 100644 --- a/lib/eventbus.js +++ b/lib/eventbus.js @@ -37,13 +37,26 @@ EventBus.prototype.process = function(e) { ); }); }; - var eventsEmitted = processEvent(e) + + var whenPreviousFinishes = Promise.resolve(); + if (this.previous && !this.previous.isFulfilled()) { + //console.log('setting new task with other running, lets queue'); + whenPreviousFinishes = this.previous; + } + + var current = whenPreviousFinishes + .then(function() { + //console.log('ok, lets go with the new block'); + return processEvent(e); + }) .then(function() { done.forEach(function(event) { self.emit(event.name || event.constructor.name, event); }); }); - return eventsEmitted; + this.previous = current; + return current; + }; diff --git a/lib/networkmonitor.js b/lib/networkmonitor.js index 84e48dfc..9e87523c 100644 --- a/lib/networkmonitor.js +++ b/lib/networkmonitor.js @@ -6,6 +6,7 @@ var EventEmitter = require('eventemitter2').EventEmitter2; var bitcore = require('bitcore'); var Networks = bitcore.Networks; var $ = bitcore.util.preconditions; +var _ = bitcore.deps._; var p2p = require('bitcore-p2p'); var Peer = p2p.Peer; var messages = new p2p.Messages(); @@ -21,10 +22,13 @@ util.inherits(NetworkMonitor, EventEmitter); NetworkMonitor.create = function(eventBus, opts) { opts = opts || {}; - var network = Networks.get(opts.network) || Networks.defaultNetwork; var host = opts.host || 'localhost'; var port = opts.port || Networks.defaultNetwork.port; - var peer = new Peer(host, port, network); + var peer = new Peer({ + host: host, + port: port, + network: Networks.defaultNetwork + }); return new NetworkMonitor(eventBus, peer); }; @@ -35,24 +39,56 @@ NetworkMonitor.prototype.setupPeer = function(peer) { self.emit('ready'); }); peer.on('inv', function(m) { + self.emit('inv', m.inventory); // TODO only ask for data if tx or block is unknown peer.sendMessage(messages.GetData(m.inventory)); }); peer.on('tx', function(m) { - self.bus.process(m.transaction); + self.bus.process(m.transaction) + .catch(function(err) { + self.abort(err); + }); }); peer.on('block', function(m) { - self.bus.process(m.block); + self.bus.process(m.block) + .catch(function(err) { + self.abort(err); + }); }); peer.on('error', function(err) { self.emit('error', err); + self.abort(err); + }); + peer.on('disconnect', function() { + self.emit('disconnect'); }); }; +NetworkMonitor.prototype.requestBlocks = function(locator) { + $.checkArgument(_.isArray(locator) && + _.isUndefined(locator[0]) || + _.isString(locator[0]), 'start must be a block hash string array'); + this.peer.sendMessage(messages.GetBlocks({ + starts: locator, + //stop: '000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9' // TODO: remove this!!! + })); +}; + NetworkMonitor.prototype.start = function() { this.peer.connect(); }; +NetworkMonitor.prototype.stop = function(reason) { + this.peer.disconnect(); + console.log('Stopping network, reason:', reason); +}; + +NetworkMonitor.prototype.abort = function(reason) { + this.peer.disconnect(); + if (reason) { + throw reason; + } +}; module.exports = NetworkMonitor; diff --git a/lib/node.js b/lib/node.js index 6e67fc83..984ab2b6 100644 --- a/lib/node.js +++ b/lib/node.js @@ -4,36 +4,175 @@ var util = require('util'); var EventEmitter = require('eventemitter2').EventEmitter2; var bitcore = require('bitcore'); +var _ = bitcore.deps._; var $ = bitcore.util.preconditions; +var Promise = require('bluebird'); +var RPC = require('bitcoind-rpc'); var NetworkMonitor = require('./networkmonitor'); var EventBus = require('./eventbus'); -var BitcoreNode = function(bus, nm) { - $.checkArgument(bus); - $.checkArgument(nm); - var self = this; - this.bus = bus; - this.nm = nm; +var LevelUp = require('levelup'); +var BlockService = require('./services/block'); +var TransactionService = require('./services/transaction'); +var AddressService = require('./services/address'); - this.bus.onAny(function(value) { - self.emit(this.event, value); - }); - this.nm.on('error', function(err) { - self.emit('error', err); - }); +var BlockChain = require('./blockchain'); +var genesisBlocks = require('./data/genesis'); + +var BitcoreNode = function(bus, networkMonitor, blockService, transactionService, addressService) { + $.checkArgument(bus, 'bus is required'); + $.checkArgument(networkMonitor, 'networkMonitor is required'); + $.checkArgument(blockService, 'blockService is required'); + $.checkArgument(transactionService, 'transactionService is required'); + $.checkArgument(addressService, 'addressService is required'); + this.bus = bus; + this.networkMonitor = networkMonitor; + + this.tip = null; + + this.addressService = addressService; + this.transactionService = transactionService; + this.blockService = blockService; + + this.blockCache = {}; + this.inventory = {}; // blockHash -> bool (has data) + this.initialize(); }; util.inherits(BitcoreNode, EventEmitter); BitcoreNode.create = function(opts) { opts = opts || {}; + var bus = new EventBus(); - var nm = NetworkMonitor.create(bus, opts.NetworkMonitor); - return new BitcoreNode(bus, nm); + + var networkMonitor = NetworkMonitor.create(bus, opts.NetworkMonitor); + + var database = opts.database || Promise.promisifyAll( + new LevelUp(opts.LevelUp || './db') + ); + var rpc = opts.rpc || Promise.promisifyAll(new RPC(opts.RPC)); + + var transactionService = opts.transactionService || new TransactionService({ + rpc: rpc, + database: database + }); + var blockService = opts.blockService || new BlockService({ + rpc: rpc, + database: database, + transactionService: transactionService + }); + var addressService = opts.addressService || new AddressService({ + rpc: rpc, + database: database, + transactionService: transactionService, + blockService: blockService + }); + return new BitcoreNode(bus, networkMonitor, blockService, transactionService, addressService); +}; + + +BitcoreNode.prototype.initialize = function() { + var self = this; + + setInterval(function() { + if (!self.blockchain) { + // not ready yet + return; + } + var tipHash = self.blockchain.tip; + var block = self.blockCache[tipHash]; + console.log('block', block.id, 'height', block.height); + }, 5 * 1000); + + this.bus.register(bitcore.Block, function(block) { + + var prevHash = bitcore.util.buffer.reverse(block.header.prevHash).toString('hex'); + self.blockCache[block.hash] = block; + self.inventory[block.hash] = true; + if (!self.blockchain.hasData(prevHash)) { + self.requestFromTip(); + return; + } + var blockchainChanges = self.blockchain.proposeNewBlock(block); + + // Annotate block with extra data from the chain + block.height = self.blockchain.height[block.id]; + block.work = self.blockchain.work[block.id]; + + return Promise.each(blockchainChanges.unconfirmed, function(hash) { + return self.blockService.unconfirm(self.blockCache[hash]); + }) + .then(function() { + return Promise.all(blockchainChanges.confirmed.map(function(hash) { + return self.blockService.confirm(self.blockCache[hash]); + })); + }) + .then(function() { + var deleteHeight = block.height - 100; + if (deleteHeight > 0) { + var deleteHash = self.blockchain.hashByHeight[deleteHeight]; + delete self.blockCache[deleteHash]; + } + }) + .then(function() { + // TODO: include this + if (false && _.size(self.inventory) && _.all(_.values(self.inventory))) { + self.inventory = {}; + self.requestFromTip(); + } + }) + .catch(function(error) { + self.stop(error); + }); + }); + + this.bus.onAny(function(value) { + self.emit(this.event, value); + }); + this.networkMonitor.on('error', function(err) { + self.emit('error', err); + }); + this.networkMonitor.on('disconnect', function() { + console.log('network monitor disconnected'); + }); }; BitcoreNode.prototype.start = function() { - this.nm.start(); + var self = this; + var genesis = bitcore.Block.fromBuffer(genesisBlocks[bitcore.Networks.defaultNetwork.name]); + + this.blockService.getBlockchain().then(function(blockchain) { + if (!blockchain) { + console.log('nothing'); + self.blockchain = new BlockChain(); + self.bus.process(genesis); + } else { + self.blockchain = blockchain; + } + self.sync(); + self.networkMonitor.start(); + }); + this.networkMonitor.on('stop', function() { + self.blockService.saveBlockchain(self.blockchain); + }); +}; + +BitcoreNode.prototype.stop = function(reason) { + this.networkMonitor.abort(reason); +}; + +BitcoreNode.prototype.requestFromTip = function() { + var locator = this.blockchain.getBlockLocator(); + console.log('requesting blocks, locator size:', locator.length); + this.networkMonitor.requestBlocks(locator); +}; + +BitcoreNode.prototype.sync = function() { + var self = this; + this.networkMonitor.on('ready', function() { + self.requestFromTip(); + }); }; module.exports = BitcoreNode; diff --git a/lib/services/address.js b/lib/services/address.js index 47c37896..76e6092e 100644 --- a/lib/services/address.js +++ b/lib/services/address.js @@ -3,6 +3,7 @@ var Promise = require('bluebird'); var bitcore = require('bitcore'); var TransactionService = require('./transaction'); +var RPC = require('bitcoind-rpc'); var _ = bitcore.deps._; var NULLTXHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex'); diff --git a/lib/services/block.js b/lib/services/block.js index 712e6f37..e360ef1b 100644 --- a/lib/services/block.js +++ b/lib/services/block.js @@ -1,20 +1,20 @@ 'use strict'; var LevelUp = require('levelup'); -var LevelLock = require('level-lock'); var Promise = require('bluebird'); var RPC = require('bitcoind-rpc'); var TransactionService = require('./transaction'); var bitcore = require('bitcore'); +var Transaction = bitcore.Transaction; var config = require('config'); -var BitcoreNode = require('../../'); +var errors = require('../errors'); +var BlockChain = require('../blockchain'); var $ = bitcore.util.preconditions; var JSUtil = bitcore.util.js; var _ = bitcore.deps._; -var LOCK = 'lock-'; var NULLBLOCKHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex'); var GENESISPARENT = { height: -1, @@ -36,22 +36,24 @@ var helper = function(index) { }; var Index = { - timestamp: 'bts-', // bts- -> hash for the block that was mined at this TS - prev: 'prev-', // prev- -> parent hash - next: 'nxt-', // nxt- -> hash for the next block in the main chain that is a child - height: 'bh-', // bh- -> height (-1 means disconnected) - tip: 'tip' // tip -> { hash: hex, height: int }, the latest tip + timestamp: 'bts-', // bts- -> hash for the block that was mined at this TS + prev: 'prev-', // prev- -> parent hash + next: 'nxt-', // nxt- -> hash for the next block in the main chain that is a child + height: 'bh-', // bh- -> height (-1 means disconnected) + tip: 'tip', // tip -> { hash: hex, height: int }, the latest tip + work: 'wk-' // wk- -> amount of work for block }; _.extend(Index, { getNextBlock: helper(Index.next), getPreviousBlock: helper(Index.prev), getBlockHeight: helper(Index.height), + getBlockWork: helper(Index.work), getBlockByTs: function(block) { return Index.timestamp + block.header.time; } }); -function BlockService (opts) { +function BlockService(opts) { opts = _.extend({}, opts); this.database = opts.database || Promise.promisifyAll(new LevelUp(config.get('LevelUp'))); this.rpc = opts.rpc || Promise.promisifyAll(new RPC(config.get('RPC'))); @@ -61,21 +63,6 @@ function BlockService (opts) { }); } -BlockService.prototype.writeLock = function() { - var self = this; - return new Promise(function(resolve, reject) { - if (self.lock) { - return reject(); - } else { - self.lock = true; - return resolve(); - } - }); -}; - -BlockService.prototype.unlock = function() { - this.lock = false; -}; /** * Transforms data as received from an RPC result structure for `getblock`, @@ -88,15 +75,13 @@ BlockService.prototype.unlock = function() { * @param {Number} blockData.time a 32 bit number with the timestamp when this block was created * @param {Number} blockData.nonce a 32 bit number with a random number * @param {string} blockData.bits a 32 bit "varint" encoded number with the length of the block - * @param {string} blockData.merkleRoot an hex string of length 64 with the hash of the block + * @param {string} blockData.merkleroot an hex string of length 64 with the hash of the block * @param {Array} transactions an array of bitcore.Transaction objects, in the order that forms the * merkle root hash * @return {bitcore.Block} */ -BlockService.blockRPCtoBitcore = function(blockData, transactions) { - $.checkArgument(_.all(transactions, function(transaction) { - return transaction instanceof bitcore.Transaction; - }), 'All transactions must be instances of bitcore.Transaction'); +BlockService.blockRPCtoBitcore = function(blockData) { + $.checkArgument(blockData, 'blockData is required'); var block = new bitcore.Block({ header: new bitcore.BlockHeader({ version: blockData.version, @@ -107,13 +92,13 @@ BlockService.blockRPCtoBitcore = function(blockData, transactions) { time: blockData.time, nonce: blockData.nonce, bits: new bitcore.deps.bnjs( - new bitcore.deps.Buffer(blockData.bits, 'hex') + new bitcore.deps.Buffer(blockData.bits, 'hex') ), merkleRoot: bitcore.util.buffer.reverse( new bitcore.deps.Buffer(blockData.merkleroot, 'hex') ) }), - transactions: transactions + transactions: blockData.transactions }); block.height = blockData.height; return block; @@ -126,8 +111,7 @@ BlockService.blockRPCtoBitcore = function(blockData, transactions) { * @return {Promise} a promise that will always be rejected */ var blockNotFound = function(err) { - console.log(err, err.stack); - return Promise.reject(new BitcoreNode.errors.Blocks.NotFound()); + throw new errors.Blocks.NotFound(err); }; /** @@ -136,7 +120,7 @@ var blockNotFound = function(err) { * @param {string} blockHash the hash of the block to be fetched * @return {Promise} */ -BlockService.prototype.getBlock = function(blockHash) { +BlockService.prototype.getBlock = function(blockHash, opts) { $.checkArgument( JSUtil.isHexa(blockHash) || bitcore.util.buffer.isBuffer(blockHash), 'Block hash must be a buffer or hexa' @@ -144,27 +128,33 @@ BlockService.prototype.getBlock = function(blockHash) { if (bitcore.util.buffer.isBuffer(blockHash)) { blockHash = bitcore.util.buffer.reverse(blockHash).toString('hex'); } + opts = opts || {}; var blockData; var self = this; return Promise.try(function() { + return self.rpc.getBlockAsync(blockHash); + }) + .catch(blockNotFound) + .then(function(block) { - return self.rpc.getBlockAsync(blockHash); + blockData = block.result; - }).then(function(block) { + if (opts.withoutTransactions) { + return []; + } - blockData = block.result; - return Promise.all(blockData.tx.map(function(txId) { - return self.transactionService.getTransaction(txId); - })); + return Promise.all(blockData.tx.map(function(txId) { + return self.transactionService.getTransaction(txId); + })); - }).then(function(transactions) { + }).then(function(transactions) { - blockData.transactions = transactions; - return BlockService.blockRPCtoBitcore(blockData); + blockData.transactions = transactions; + return BlockService.blockRPCtoBitcore(blockData); - }).catch(blockNotFound); + }); }; /** @@ -180,13 +170,16 @@ BlockService.prototype.getBlockByHeight = function(height) { return Promise.try(function() { - return self.rpc.getBlockHash(height); + return self.rpc.getBlockHashAsync(height); - }).then(function(blockHash) { + }) + .catch(blockNotFound) + .then(function(result) { - return self.getBlock(blockHash); + var blockHash = result.result; + return self.getBlock(blockHash); - }).catch(blockNotFound); + }); }; /** @@ -206,96 +199,133 @@ BlockService.prototype.getLatest = function() { return self.getBlock(blockHash); - }).catch(blockNotFound); + }).catch(LevelUp.errors.NotFoundError, function() { + return null; + }); }; +/** + * Handle a block from the network + * + * @param {bitcore.Block} block + * @return a list of events back to the event bus + */ +BlockService.prototype.onBlock = function(block) { + var events = []; + return this.save(block) + .then(function(block) { + console.log('block', block.id, 'saved with height', block.height); + block.transactions.forEach(function(tx) { + events.push(tx); + }); + return events; + }); +}; + + /** * Set a block as the current tip of the blockchain * * @param {bitcore.Block} block + * @param {Array=} ops * @return {Promise} a promise of the same block, for chaining */ -BlockService.prototype._confirmBlock = function(block) { +BlockService.prototype.confirm = function(block, ops) { $.checkArgument(block instanceof bitcore.Block); var self = this; - var ops = []; + ops = ops || []; - return this.writeLock().then(function() { + //console.log(0); + return Promise.try(function() { + //console.log(1); + self._setNextBlock(ops, block.header.prevHash, block); - return self._setNextBlock(ops, block.header.prevHash, block); + //console.log(3); + self._setBlockHeight(ops, block); - }).then(function() { + //console.log(3); + self._setBlockWork(ops, block); - if (block.header.prevHash.toString('hex') !== NULLBLOCKHASH) { - return self.getBlock(block.header.prevHash); - } else { - return GENESISPARENT; - } + //console.log(4); + self._setBlockByTs(ops, block); - }).then(function(parent) { + self._setTip(ops, block); - return self._setBlockHeight(ops, block, parent.height + 1); + //console.log(5); + return Promise.all(block.transactions.map(function(transaction) { + return self.transactionService._confirmTransaction(ops, block, transaction); + })); - }).then(function() { - - return self._setBlockByTs(ops, block); - - }).then(function() { - - return Promise.all(block.transactions.map(function(transaction) { - return self.transactionService._confirmTransaction(ops, block, transaction); - })); - - }).then(function() { - - return self.database.batchAsync(ops) - - }).then(function() { - return self.unlock(); - }); + }) + .then(function() { + //console.log(6); + return self.database.batchAsync(ops); + }) + .then(function() { + //console.log(7); + return block; + }); }; BlockService.prototype._setNextBlock = function(ops, prevBlockHash, block) { if (bitcore.util.buffer.isBuffer(prevBlockHash)) { prevBlockHash = bitcore.util.buffer.reverse(prevBlockHash).toString('hex'); } - return Promise.try(function() { - ops.push({ - type: 'put', - key: Index.getNextBlock(prevBlockHash), - value: block.hash - }); - ops.push({ - type: 'put', - key: Index.getPreviousBlock(block.hash), - value: prevBlockHash.toString('hex') - }); + ops.push({ + type: 'put', + key: Index.getNextBlock(prevBlockHash), + value: block.hash + }); + ops.push({ + type: 'put', + key: Index.getPreviousBlock(block.hash), + value: prevBlockHash }); }; -BlockService.prototype._setBlockHeight = function(ops, block, height) { - return Promise.try(function() { - ops.push({ - type: 'put', - key: Index.getBlockHeight(block), - value: height - }); - return ops; +BlockService.prototype._setBlockHeight = function(ops, block) { + ops.push({ + type: 'put', + key: Index.getBlockHeight(block), + value: block.height + }); +}; + +BlockService.prototype._setTip = function(ops, block) { + ops.push({ + type: 'put', + key: Index.tip, + value: block.hash + }); +}; + +BlockService.prototype._setBlockWork = function(ops, block) { + ops.push({ + type: 'put', + key: Index.getBlockWork(block), + value: block.work }); }; BlockService.prototype._setBlockByTs = function(ops, block) { + + // TODO: uncomment this + /* var self = this; - var key = Index.timestamp + block.time; + var key = Index.timestamp + block.header.time; + console.log('key', key); return Promise.try(function() { + console.log('a'); return self.database.getAsync(key); - }).then(function(result) { + }) + .then(function(result) { + console.log('b'); if (result === block.hash) { return Promise.resolve(); } else { @@ -303,7 +333,10 @@ BlockService.prototype._setBlockByTs = function(ops, block) { throw new Error('Found blocks that have same timestamp'); } - }).error(function(err) { + }) + .error(function(err) { + console.log('err', err); + // TODO: Check if err is not found return ops.push({ type: 'put', @@ -311,6 +344,64 @@ BlockService.prototype._setBlockByTs = function(ops, block) { value: block.hash }); }); + */ +}; + +/** + * Unconfirm a block + * + * @param {bitcore.Block} block + * @param {Array=} ops + * @return {Promise} a promise of the same block, for chaining + */ +BlockService.prototype.unconfirm = function(block, ops) { + + ops = ops || []; + + return Promise.try(function() { + + self._removeNextBlock(ops, block.header.prevHash, block); + + self._unsetBlockHeight(ops, block, block.height); + + self._dropBlockByTs(ops, block); + + return Promise.all(block.transactions.map(function(transaction) { + return self.transactionService._unconfirmTransaction(ops, block, transaction); + })); + + }).then(function() { + + return self.database.batchAsync(ops); + + }); +}; + +BlockService.prototype._removeNextBlock = function(ops, prevHash, block) { + + if (bitcore.util.buffer.isBuffer(prevBlockHash)) { + prevBlockHash = bitcore.util.buffer.reverse(prevBlockHash).toString('hex'); + } + + ops.push({ + type: 'del', + key: Index.getNextBlock(prevBlockHash) + }); + ops.push({ + type: 'del', + key: Index.getPreviousBlock(block.hash) + }); +}; + +BlockService.prototype._unsetBlockHeight = function(ops, block, height) { + ops.push({ + type: 'del', + key: Index.getBlockHeight(block) + }); +}; + +BlockService.prototype._dropBlockByTs = function(ops, block) { + // TODO }; /** @@ -346,4 +437,57 @@ BlockService.prototype.getBlockForTransaction = function(transaction) { }); }; +BlockService.prototype.getBlockchain = function() { + var self = this; + + var blockchain = new BlockChain(); + + var fetchBlock = function(blockHash) { + return Promise.all([ + self.database.getAsync(Index.getPreviousBlock(blockHash)).then(function(prevHash) { + blockchain.prev[blockHash] = prevHash; + blockchain.next[prevHash] = blockHash; + }), + self.database.getAsync(Index.getBlockHeight(blockHash)).then(function(height) { + blockchain.height[blockHash] = +height; + blockchain.hashByHeight[height] = blockHash; + }), + self.database.getAsync(Index.getBlockWork(blockHash)).then(function(work) { + blockchain.work[blockHash] = work; + }) + ]).then(function() { + return blockHash; + }); + }; + + var fetchUnlessGenesis = function(blockHash) { + return fetchBlock(blockHash).then(function() { + if (blockchain.prev[blockHash] === BlockChain.NULL) { + return; + } else { + return fetchUnlessGenesis(blockchain.prev[blockHash]); + } + }); + }; + + return self.database.getAsync(Index.tip) + .catch(function(err) { + if (err.notFound) { + return undefined; + } + throw err; + }) + .then(function(tip) { + if (!tip) { + console.log('No tip found'); + return; + } + console.log('Tip is', tip); + blockchain.tip = tip; + return fetchUnlessGenesis(tip).then(function() { + return blockchain; + }); + }); +}; + module.exports = BlockService; diff --git a/lib/services/transaction.js b/lib/services/transaction.js index 375b5d85..a704bc12 100644 --- a/lib/services/transaction.js +++ b/lib/services/transaction.js @@ -179,6 +179,7 @@ TransactionService.prototype._getAddressForInput = function(input) { hash, bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash ); } else if (script.isPublicKeyIn()) { + /* return self.getTransaction(input.prevTxId.toString('hex')).then(function(transaction) { var outputScript = transaction.outputs[input.outputIndex].script; if (outputScript.isPublicKeyOut()) { @@ -189,6 +190,7 @@ TransactionService.prototype._getAddressForInput = function(input) { } return; }); + */ } else { return new bitcore.Script(script.chunks[script.chunks.length - 1]).toAddress(); } @@ -208,4 +210,18 @@ TransactionService.prototype._confirmTransaction = function(ops, block, transact )); }; +TransactionService.prototype._unconfirmTransaction = function(ops, block, transaction) { + var self = this; + ops.push({ + type: 'del', + key: Index.getBlockForTransaction(transaction), + value: block.id + }); + return Promise.all( + _.map(transaction.outputs, self._unconfirmOutput(ops, block, transaction)) + .concat( + _.map(transaction.inputs, self._unconfirmInput(ops, block, transaction)) + )); +}; + module.exports = TransactionService; diff --git a/package.json b/package.json index ecce833d..a5cfec7d 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "async": "0.9.0", "bitcoind-rpc": "^0.2.1", "bitcore": "bitpay/bitcore", - "bitcore-p2p": "bitpay/bitcore-p2p", + "bitcore-p2p": "^0.11.0", "bluebird": "^2.9.12", "body-parser": "^1.12.0", "bufferput": "bitpay/node-bufferput", @@ -59,8 +59,9 @@ "express": "4.11.1", "glob": "*", "js-yaml": "^3.2.7", - "level-lock": "^1.0.1", - "levelup": "~0.19.0", + "leveldown": "~1.0.0", + "levelup": "^0.19.0", + "memdown": "^1.0.0", "moment": "~2.5.0", "morgan": "^1.5.1", "request": "^2.48.0", diff --git a/test/networkmonitor.js b/test/networkmonitor.js index 732f8310..40c9d560 100644 --- a/test/networkmonitor.js +++ b/test/networkmonitor.js @@ -38,6 +38,9 @@ describe('NetworkMonitor', function() { block: mockBlock }); }; + peerMock.disconnect = function() { + + }; }); it('instantiates correctly from constructor', function() { @@ -62,7 +65,10 @@ describe('NetworkMonitor', function() { it('broadcasts errors in underlying peer', function(cb) { var nm = new NetworkMonitor(busMock, peerMock); - nm.on('error', cb); + nm.on('error', function() { + console.log('under'); + cb(); + }); nm.start(); peerMock.emit('error'); }); diff --git a/test/node.js b/test/node.js index fe9ca6e8..8170c1f7 100644 --- a/test/node.js +++ b/test/node.js @@ -13,36 +13,50 @@ Promise.longStackTraces(); describe('BitcoreNode', function() { // mocks - var busMock, nmMock; + var node, busMock, nmMock, bsMock, tsMock, asMock, chainMock; beforeEach(function() { busMock = new EventBus(); nmMock = new EventEmitter(); nmMock.start = function() {}; + chainMock = {}; + bsMock = {}; + bsMock.getBlockchain = function() { + return Promise.resolve(chainMock); + }; + tsMock = {}; + asMock = {}; + node = new BitcoreNode(busMock, nmMock, bsMock, tsMock, asMock); }); describe('instantiates', function() { it('from constructor', function() { - var node = new BitcoreNode(busMock, nmMock); - should.exist(node); + var n = new BitcoreNode(busMock, nmMock, bsMock, tsMock, asMock); + should.exist(n); }); it('from create', function() { - var node = BitcoreNode.create(); + var dbMock = {}; + var rpcMock = {}; + var opts = { + database: dbMock, + rpc: rpcMock, + blockService: bsMock, + transactionService: tsMock + }; + var node = BitcoreNode.create(opts); should.exist(node); }); }); it('starts', function() { - var node = new BitcoreNode(busMock, nmMock); + node.start(); node.start.bind(node).should.not.throw(); }); it('broadcasts errors from network monitor', function(cb) { - var node = new BitcoreNode(busMock, nmMock); node.on('error', cb); nmMock.emit('error'); }); it('exposes all events from the event bus', function(cb) { - var node = new BitcoreNode(busMock, nmMock); node.on('foo', cb); busMock.emit('foo'); }); diff --git a/test/services/block.js b/test/services/block.js index f3706012..b204d0cb 100644 --- a/test/services/block.js +++ b/test/services/block.js @@ -28,7 +28,7 @@ describe('BlockService', function() { describe('getBlock', function() { var mockRpc, transactionMock, database, blockService; - + beforeEach(function() { database = sinon.mock(); mockRpc = sinon.mock(); @@ -43,7 +43,7 @@ describe('BlockService', function() { height: 2, version: 1, merkleroot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5', - tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ], + tx: ['9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5'], time: 1231469744, nonce: 1639830024, bits: '1d00ffff', @@ -63,7 +63,7 @@ describe('BlockService', function() { transactionService: transactionMock, database: database }); - }); + }); it('retrieves correctly a block, uses RPC', function(callback) { @@ -87,10 +87,19 @@ describe('BlockService', function() { return arg(); } }; + var work = 1000; + var work169 = 169; + var work170 = 170; var genesisBlock = require('../data/genesis'); + genesisBlock.work = work; + genesisBlock.height = 1; var block169 = require('../data/169'); + block169.work = work169; + block169.height = 169; var block170 = require('../data/170'); - + block170.work = work170; + block170.height = 170; + beforeEach(function() { database = sinon.mock(); mockRpc = sinon.mock(); @@ -102,64 +111,66 @@ describe('BlockService', function() { database: database }); blockService.writeLock = sinon.mock(); - }); + }); it('makes the expected calls when confirming the genesis block', function(callback) { database.batchAsync = function(ops) { - ops.should.deep.equal([ - { type: 'put', - key: 'nxt-0000000000000000000000000000000000000000000000000000000000000000', - value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' }, - { type: 'put', - key: 'prev-000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', - value: '0000000000000000000000000000000000000000000000000000000000000000' }, - { type: 'put', - key: 'bh-000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', - value: 0 }, - { type: 'put', - key: 'bts-1231006505', - value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' } - ]); - return thenCaller; - }; - blockService.unlock = callback; - blockService.writeLock.onFirstCall().returns(thenCaller); - blockService.getBlock = sinon.mock(); - database.getAsync = function() { - return Promise.reject({notFound: true}); + var expectedOps = [{ + type: 'put', + key: 'nxt-0000000000000000000000000000000000000000000000000000000000000000', + value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' + }, { + type: 'put', + key: 'prev-000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', + value: '0000000000000000000000000000000000000000000000000000000000000000' + }, { + type: 'put', + key: 'bh-000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', + value: 0 + }, { + type: 'put', + key: 'wk-000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', + value: work + }, { + type: 'put', + key: 'tip', + value: genesisBlock.id + }]; + ops.should.deep.equal(expectedOps); + return callback(); }; transactionMock._confirmTransaction = sinon.mock(); - blockService._confirmBlock(genesisBlock); + blockService.confirm(genesisBlock); }); it('makes the expected calls when confirming the block #170', function(callback) { database.batchAsync = function(ops) { - ops.should.deep.equal([ - { type: 'put', - key: 'nxt-000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55', - value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' }, - { type: 'put', - key: 'prev-00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee', - value: '000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55' }, - { type: 'put', - key: 'bh-00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee', - value: 170 }, - { type: 'put', - key: 'bts-1231731025', - value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' } - ]); - return thenCaller; + ops.should.deep.equal([{ + type: 'put', + key: 'nxt-000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55', + value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' + }, { + type: 'put', + key: 'prev-00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee', + value: '000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55' + }, { + type: 'put', + key: 'bh-00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee', + value: 170 + }, { + type: 'put', + key: 'wk-00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee', + value: work170 + }, { + type: 'put', + key: 'tip', + value: block170.id + }]); + return callback(); }; - blockService.unlock = callback; blockService.writeLock.onFirstCall().returns(thenCaller); - blockService.getBlock = function() { - return Promise.resolve(block169); - }; - database.getAsync = function() { - return Promise.reject({notFound: true}); - }; transactionMock._confirmTransaction = sinon.spy(); - blockService._confirmBlock(block170); + blockService.confirm(block170); }); }); }); diff --git a/test/services/transaction.js b/test/services/transaction.js index 07561780..ac07a991 100644 --- a/test/services/transaction.js +++ b/test/services/transaction.js @@ -147,7 +147,9 @@ describe('TransactionService', function() { sequenceNumber: 4294967295, script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901', heightConfirmed: 170 } }, - { type: 'put', + ]); + /* TODO: This should work if address spent is accepted for public key. Add test for P2PKH if not accepted + * { type: 'put', key: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0', value: { heightSpent: 170, @@ -156,7 +158,7 @@ describe('TransactionService', function() { spendInput: { prevTxId: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9', outputIndex: 0, sequenceNumber: 4294967295, - script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901' }}}]); + script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901' }}}]);*/ callback(); }); });