diff --git a/config/default.yml b/config/default.yml index a5c41ca3..73a9cd23 100644 --- a/config/default.yml +++ b/config/default.yml @@ -3,3 +3,10 @@ BitcoreNode: network: livenet host: localhost port: 8333 +LevelUp: ./db +RPC: + username: username + password: password + protocol: http + host: 127.0.0.1 + port: 8332 diff --git a/lib/databases/block.js b/lib/BlockDb.js similarity index 100% rename from lib/databases/block.js rename to lib/BlockDb.js diff --git a/lib/databases/transaction.js b/lib/TransactionDb.js similarity index 99% rename from lib/databases/transaction.js rename to lib/TransactionDb.js index 370d3ea6..bc8cdddc 100644 --- a/lib/databases/transaction.js +++ b/lib/TransactionDb.js @@ -1,10 +1,3 @@ -'use strict'; - -var imports = require('soop').imports(); - - - -// to show tx outs var OUTS_PREFIX = 'txo-'; //txo-- => [addr, btc_sat] var SPENT_PREFIX = 'txs-'; //txs---- = ts diff --git a/lib/indexes.js b/lib/indexes.js new file mode 100644 index 00000000..e69de29b diff --git a/lib/services/block.js b/lib/services/block.js new file mode 100644 index 00000000..8ea51b65 --- /dev/null +++ b/lib/services/block.js @@ -0,0 +1,112 @@ +'use strict'; + +var LevelUp = require('levelup'); +var Promise = require('bluebird'); +var RPC = require('bitcoind-rpc'); +var TransactionService = require('./transaction'); +var bitcore = require('bitcore'); +var config = require('config'); + +var BitcoreNode = require('../../'); + +var $ = bitcore.util.preconditions; +var JSUtil = bitcore.util.js; +var _ = bitcore.deps._; + +var LATEST_BLOCK = 'latest-block'; + +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'))); + this.transactionService = opts.transactionService || new TransactionService({ + database: this.database, + rpc: this.rpc + }); +} + +BlockService.blockRPCtoBitcore = function(blockData, transactions) { + $.checkArgument(_.all(transactions, function(transaction) { + return transaction instanceof bitcore.Transaction; + }), 'All transactions must be instances of bitcore.Transaction'); + return new bitcore.Block({ + header: new bitcore.BlockHeader({ + version: blockData.version, + prevHash: bitcore.util.buffer.reverse( + new bitcore.deps.Buffer(blockData.previousblockhash, 'hex') + ), + time: blockData.time, + nonce: blockData.nonce, + bits: new bitcore.deps.bnjs( + new bitcore.deps.Buffer(blockData.bits, 'hex') + ), + merkleRoot: bitcore.util.buffer.reverse( + new bitcore.deps.Buffer(blockData.merkleRoot, 'hex') + ) + }), + transactions: transactions + }); +}; + +var blockNotFound = function(err) { + console.log(err); + return Promise.reject(new BitcoreNode.errors.Blocks.NotFound()); +}; + +BlockService.prototype.getBlock = function(blockHash) { + $.checkArgument(JSUtil.isHexa(blockHash), 'Block hash must be hexa'); + + var blockData; + var self = this; + + return Promise.try(function() { + + return self.rpc.getBlockAsync(blockHash); + + }).then(function(block) { + + blockData = block.result; + return Promise.all(blockData.tx.map(function(txId) { + return self.transactionService.getTransaction(txId); + })); + + }).then(function(transactions) { + + blockData.transactions = transactions; + return BlockService.blockRPCtoBitcore(blockData); + + }).catch(blockNotFound); +}; + +BlockService.prototype.getBlockByHeight = function(height) { + + $.checkArgument(_.isNumber(height), 'Block height must be a number'); + var self = this; + + return Promise.try(function() { + + return this.rpc.getBlockHash(height); + + }).then(function(blockHash) { + + return self.getBlock(blockHash); + + }).catch(blockNotFound); +}; + +BlockService.prototype.getLatest = function() { + + var self = this; + + return Promise.try(function() { + + return self.database.getAsync(LATEST_BLOCK); + + }).then(function(blockHash) { + + return self.getBlock(blockHash); + + }).catch(blockNotFound); +}; + +module.exports = BlockService; diff --git a/lib/services/transaction.js b/lib/services/transaction.js new file mode 100644 index 00000000..ce438b73 --- /dev/null +++ b/lib/services/transaction.js @@ -0,0 +1,51 @@ +/** + * @file service/transaction.js + * + * This implementation stores a set of indexes so quick queries are possible. + * An "index" for the purposes of this explanation is a structure for a set + * of keys to the LevelDB key/value store so that both the key and values can be + * sequentially accesed, which is a fast operation on LevelDB. + * + * Map of transaction to related addresses: + * * address-
--- -> true (unspent) + * -> + * * output-- -> { script, amount, spendTxId, spendIndex } + * * input-- -> { script, amount, prevTxId, outputIndex, output } + * + */ +'use strict'; + +var RPC = require('bitcoind-rpc'); +var LevelUp = require('levelup'); +var Promise = require('bluebird'); +var bitcore = require('bitcore'); +var config = require('config'); + +var _ = bitcore.deps._; +var $ = bitcore.util.preconditions; + +function TransactionService (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'))); +} + +TransactionService.transactionRPCtoBitcore = function(rpcResponse) { + if (rpcResponse.error) { + throw new bitcore.Error(rpcResponse.error); + } + return new bitcore.Transaction(rpcResponse.result); +}; + +TransactionService.prototype.getTransaction = function(transactionId) { + + var self = this; + + return Promise.try(function() { + return self.rpc.getRawTransactionAsync(transactionId); + }).then(function(rawTransaction) { + return TransactionService.transactionRPCtoBitcore(rawTransaction); + }); +}; + +module.exports = TransactionService; diff --git a/package.json b/package.json index 9ffc3755..32484510 100644 --- a/package.json +++ b/package.json @@ -45,43 +45,30 @@ }, "dependencies": { "async": "0.9.0", - "bignum": "*", + "bitcoind-rpc": "^0.2.1", "bitcore": "bitpay/bitcore", "bitcore-p2p": "bitpay/bitcore-p2p", "bluebird": "^2.9.12", "body-parser": "^1.12.0", "bufferput": "bitpay/node-bufferput", "buffertools": "*", - "commander": "^2.3.0", - "compression": "^1.4.1", "config": "^1.12.0", - "cors": "^2.5.3", - "cron": "^1.0.4", "eventemitter2": "^0.4.14", "express": "4.11.1", "glob": "*", - "js-yaml": "^3.2.7", - "leveldown": "~0.10.0", "levelup": "~0.19.0", - "lodash": "^2.4.1", - "microtime": "^0.6.0", - "mkdirp": "^0.5.0", "moment": "~2.5.0", "morgan": "^1.5.1", - "preconditions": "^1.0.7", "request": "^2.48.0", "socket.io": "1.0.6", - "socket.io-client": "1.0.6", - "soop": "=0.1.5", - "winston": "*", - "xmlhttprequest": "~1.6.0" + "winston": "*" }, "devDependencies": { "bitcore-build": "bitpay/bitcore-build", - "chai": "*", + "chai": "^2.1.1", "gulp": "^3.8.10", - "should": "^2.1.1", - "sinon": "^1.10.3", + "should": "^5.1.0", + "sinon": "^1.13.0", "supertest": "^0.15.0" } } diff --git a/test/services/block.js b/test/services/block.js new file mode 100644 index 00000000..a85957a5 --- /dev/null +++ b/test/services/block.js @@ -0,0 +1,80 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require('chai').should(); +var Promise = require('bluebird'); + +var bitcore = require('bitcore'); + +var BlockService = require('../../lib/services/block'); + +describe('BlockService', function() { + + it('initializes correctly', function() { + var database = 'database'; + var rpc = 'rpc'; + var txService = 'txService'; + var blockService = new BlockService({ + database: database, + rpc: 'rpc', + transactionService: 'txService' + }); + should.exist(blockService); + blockService.database.should.equal(database); + blockService.rpc.should.equal(rpc); + blockService.transactionService.should.equal(txService); + }); + + describe('getBlock', function() { + + var mockRpc, transactionMock, database, blockService; + + beforeEach(function() { + database = sinon.mock(); + mockRpc = sinon.mock(); + transactionMock = sinon.mock(); + + mockRpc.getBlockAsync = function(block) { + return Promise.resolve({ + result: { + hash: '000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd', + confirmations: 347064, + size: 215, + height: 2, + version: 1, + merkleRoot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5', + tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ], + time: 1231469744, + nonce: 1639830024, + bits: '1d00ffff', + previousblockhash: '00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048' + } + }); + }; + + transactionMock.getTransaction = function(txId) { + return Promise.resolve( + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010bffffffff0100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac00000000' + ); + }; + + blockService = new BlockService({ + rpc: mockRpc, + transactionService: transactionMock, + database: database + }); + }); + + it('retrieves correctly a block, uses RPC', function(callback) { + + var hash = '000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd'; + + blockService.getBlock(hash).then(function(block) { + block.hash.should.equal(hash); + callback(); + }); + + }); + + }); +}); diff --git a/test/services/transaction.js b/test/services/transaction.js new file mode 100644 index 00000000..7e7fcff8 --- /dev/null +++ b/test/services/transaction.js @@ -0,0 +1,52 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require('chai').should(); +var Promise = require('bluebird'); + +var bitcore = require('bitcore'); + +var TransactionService = require('../../lib/services/transaction'); + +describe('TransactionService', function() { + + var rawTransaction = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; + var transactionId = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'; + + it('initializes correctly', function() { + var database = 'mock'; + var rpc = 'mock'; + var service = new TransactionService({ + database: database, + rpc: rpc + }); + should.exist(service); + }); + + describe('get', function() { + + var database, rpc, service; + + beforeEach(function() { + database = sinon.mock(); + rpc = sinon.mock(); + rpc.getRawTransactionAsync = function(transaction) { + return Promise.resolve({ + result: rawTransaction + }); + }; + service = new TransactionService({ + rpc: rpc, + database: database + }); + }); + + it('allows the user to fetch a transaction using its hash', function(callback) { + + service.getTransaction(transactionId).then(function(transaction) { + transaction.hash.should.equal(transactionId); + callback(); + }); + }); + }); +});