From 191cb0e0d12a954a5ad915010829361681392775 Mon Sep 17 00:00:00 2001 From: eordano Date: Wed, 25 Feb 2015 14:36:30 -0300 Subject: [PATCH 1/3] Reorg db and mock services --- config/default.yml | 8 ++ lib/{databases/block.js => BlockDb.js} | 0 .../transaction.js => TransactionDb.js} | 7 -- lib/indexes.js | 0 lib/services/block.js | 115 ++++++++++++++++++ lib/services/transaction.js | 51 ++++++++ package.json | 2 + test/services/block.js | 79 ++++++++++++ test/services/transaction.js | 53 ++++++++ 9 files changed, 308 insertions(+), 7 deletions(-) rename lib/{databases/block.js => BlockDb.js} (100%) rename lib/{databases/transaction.js => TransactionDb.js} (99%) create mode 100644 lib/indexes.js create mode 100644 lib/services/block.js create mode 100644 lib/services/transaction.js create mode 100644 test/services/block.js create mode 100644 test/services/transaction.js diff --git a/config/default.yml b/config/default.yml index a5c41ca3..1d2c9ff1 100644 --- a/config/default.yml +++ b/config/default.yml @@ -3,3 +3,11 @@ BitcoreNode: network: livenet host: localhost port: 8333 +LevelUp: ./db +RPC: + username: username + password: password + protocol: http + network: livenet + 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..b5973af8 --- /dev/null +++ b/lib/services/block.js @@ -0,0 +1,115 @@ +'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 $ = 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 + }); +}; + +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 Promise.resolve(BlockService.blockRPCtoBitcore(blockData)); + + }).catch(function(err) { + console.log(err); + return Promise.reject(err); + }); +}; + +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(function(err) { + console.log(err); + return Promise.reject(err); + }); +}; + +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(function(err) { + console.log(err); + return Promise.reject(err); + }); +}; + + +module.exports = BlockService; diff --git a/lib/services/transaction.js b/lib/services/transaction.js new file mode 100644 index 00000000..f62e4aae --- /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 rpcResponse.result; +}; + +TransactionService.prototype.getTransaction = function(transactionId) { + + var self = this; + + return Promise.try(function() { + return self.rpc.getRawTransactionAsync(transactionId); + }).then(function(rawTransaction) { + return new bitcore.Transaction(TransactionService.transactionRPCtoBitcore(rawTransaction)); + }); +}; + +module.exports = TransactionService; diff --git a/package.json b/package.json index 9ffc3755..22c8bb61 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,11 @@ "morgan": "^1.5.1", "preconditions": "^1.0.7", "request": "^2.48.0", + "sequelize": "^2.0.3", "socket.io": "1.0.6", "socket.io-client": "1.0.6", "soop": "=0.1.5", + "sqlite3": "^3.0.5", "winston": "*", "xmlhttprequest": "~1.6.0" }, diff --git a/test/services/block.js b/test/services/block.js new file mode 100644 index 00000000..cfa36c31 --- /dev/null +++ b/test/services/block.js @@ -0,0 +1,79 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require('chai').should(); + +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' + }); + blockService.should.exist(); + 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..e6eb543f --- /dev/null +++ b/test/services/transaction.js @@ -0,0 +1,53 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require('chai').should(); +var Sequelize = require('sequelize'); + +var bitcore = require('bitcore'); + +var TransactionService = require('../../lib/services/transaction'); + +describe('TransactionService', function() { + + var service = new TransactionService(); + var schema = sinon.stub(); + + var rawTransaction = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; + var transactionId = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'; + + schema.Transaction = {}; + schema.Transaction.find = sinon.stub(); + + var transactionResult = sinon.stub(); + transactionResult.getDataValue = function() { return rawTransaction; }; + + it('initializes correctly', function() { + (new TransactionService()).should.exist(); + }); + + describe('get', function() { + it('allows the user to fetch a transaction using its hash', function(callback) { + schema.Transaction.find.onFirstCall().returns({ + then: function(f) { + return { + then: function(g) { + return g(f(transactionResult)); + } + }; + } + }); + + service.getTransaction(schema, transactionId).then(function(transaction) { + transaction.should.be.an.instanceof(bitcore.Transaction); + transaction.toString().should.equal(rawTransaction); + callback(); + }); + }); + it('fails on a non-string argument', function() { + (function() { + return service.getTransaction(); + }).should.throw(bitcore.errors.InvalidArgument); + }); + }); +}); From c846ccdeebd7a05cf813ed7b3851a2b42514e44a Mon Sep 17 00:00:00 2001 From: eordano Date: Wed, 11 Mar 2015 10:59:16 -0300 Subject: [PATCH 2/3] Sort dependencies --- lib/services/block.js | 25 +++++++--------- package.json | 25 ++++------------ test/services/block.js | 3 +- test/services/transaction.js | 57 ++++++++++++++++++------------------ 4 files changed, 46 insertions(+), 64 deletions(-) diff --git a/lib/services/block.js b/lib/services/block.js index b5973af8..8ea51b65 100644 --- a/lib/services/block.js +++ b/lib/services/block.js @@ -7,6 +7,8 @@ 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._; @@ -46,6 +48,11 @@ BlockService.blockRPCtoBitcore = function(blockData, 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'); @@ -66,12 +73,9 @@ BlockService.prototype.getBlock = function(blockHash) { }).then(function(transactions) { blockData.transactions = transactions; - return Promise.resolve(BlockService.blockRPCtoBitcore(blockData)); + return BlockService.blockRPCtoBitcore(blockData); - }).catch(function(err) { - console.log(err); - return Promise.reject(err); - }); + }).catch(blockNotFound); }; BlockService.prototype.getBlockByHeight = function(height) { @@ -87,10 +91,7 @@ BlockService.prototype.getBlockByHeight = function(height) { return self.getBlock(blockHash); - }).catch(function(err) { - console.log(err); - return Promise.reject(err); - }); + }).catch(blockNotFound); }; BlockService.prototype.getLatest = function() { @@ -105,11 +106,7 @@ BlockService.prototype.getLatest = function() { return self.getBlock(blockHash); - }).catch(function(err) { - console.log(err); - return Promise.reject(err); - }); + }).catch(blockNotFound); }; - module.exports = BlockService; diff --git a/package.json b/package.json index 22c8bb61..32484510 100644 --- a/package.json +++ b/package.json @@ -45,45 +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", - "sequelize": "^2.0.3", "socket.io": "1.0.6", - "socket.io-client": "1.0.6", - "soop": "=0.1.5", - "sqlite3": "^3.0.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 index cfa36c31..ee96cbd1 100644 --- a/test/services/block.js +++ b/test/services/block.js @@ -2,6 +2,7 @@ var sinon = require('sinon'); var should = require('chai').should(); +var Promise = require('bluebird'); var bitcore = require('bitcore'); @@ -18,7 +19,7 @@ describe('BlockService', function() { rpc: 'rpc', transactionService: 'txService' }); - blockService.should.exist(); + blockService.should.exist; blockService.database.should.equal(database); blockService.rpc.should.equal(rpc); blockService.transactionService.should.equal(txService); diff --git a/test/services/transaction.js b/test/services/transaction.js index e6eb543f..7a66d0d4 100644 --- a/test/services/transaction.js +++ b/test/services/transaction.js @@ -2,7 +2,7 @@ var sinon = require('sinon'); var should = require('chai').should(); -var Sequelize = require('sequelize'); +var Promise = require('bluebird'); var bitcore = require('bitcore'); @@ -10,44 +10,43 @@ var TransactionService = require('../../lib/services/transaction'); describe('TransactionService', function() { - var service = new TransactionService(); - var schema = sinon.stub(); - var rawTransaction = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; var transactionId = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'; - schema.Transaction = {}; - schema.Transaction.find = sinon.stub(); - - var transactionResult = sinon.stub(); - transactionResult.getDataValue = function() { return rawTransaction; }; - it('initializes correctly', function() { - (new TransactionService()).should.exist(); + var database = 'mock'; + var rpc = 'mock'; + var service = new TransactionService({ + database: database, + rpc: rpc + }); + service.should.exist; }); describe('get', function() { - it('allows the user to fetch a transaction using its hash', function(callback) { - schema.Transaction.find.onFirstCall().returns({ - then: function(f) { - return { - then: function(g) { - return g(f(transactionResult)); - } - }; - } - }); - service.getTransaction(schema, transactionId).then(function(transaction) { - transaction.should.be.an.instanceof(bitcore.Transaction); - transaction.toString().should.equal(rawTransaction); + 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(); }); }); - it('fails on a non-string argument', function() { - (function() { - return service.getTransaction(); - }).should.throw(bitcore.errors.InvalidArgument); - }); }); }); From 808b191648247c1ad05314e757a475bf1c09f5e6 Mon Sep 17 00:00:00 2001 From: eordano Date: Wed, 11 Mar 2015 12:50:41 -0300 Subject: [PATCH 3/3] Minor fixes (address concerns on PR #35) --- config/default.yml | 1 - lib/services/transaction.js | 4 ++-- test/services/block.js | 2 +- test/services/transaction.js | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/default.yml b/config/default.yml index 1d2c9ff1..73a9cd23 100644 --- a/config/default.yml +++ b/config/default.yml @@ -8,6 +8,5 @@ RPC: username: username password: password protocol: http - network: livenet host: 127.0.0.1 port: 8332 diff --git a/lib/services/transaction.js b/lib/services/transaction.js index f62e4aae..ce438b73 100644 --- a/lib/services/transaction.js +++ b/lib/services/transaction.js @@ -34,7 +34,7 @@ TransactionService.transactionRPCtoBitcore = function(rpcResponse) { if (rpcResponse.error) { throw new bitcore.Error(rpcResponse.error); } - return rpcResponse.result; + return new bitcore.Transaction(rpcResponse.result); }; TransactionService.prototype.getTransaction = function(transactionId) { @@ -44,7 +44,7 @@ TransactionService.prototype.getTransaction = function(transactionId) { return Promise.try(function() { return self.rpc.getRawTransactionAsync(transactionId); }).then(function(rawTransaction) { - return new bitcore.Transaction(TransactionService.transactionRPCtoBitcore(rawTransaction)); + return TransactionService.transactionRPCtoBitcore(rawTransaction); }); }; diff --git a/test/services/block.js b/test/services/block.js index ee96cbd1..a85957a5 100644 --- a/test/services/block.js +++ b/test/services/block.js @@ -19,7 +19,7 @@ describe('BlockService', function() { rpc: 'rpc', transactionService: 'txService' }); - blockService.should.exist; + should.exist(blockService); blockService.database.should.equal(database); blockService.rpc.should.equal(rpc); blockService.transactionService.should.equal(txService); diff --git a/test/services/transaction.js b/test/services/transaction.js index 7a66d0d4..7e7fcff8 100644 --- a/test/services/transaction.js +++ b/test/services/transaction.js @@ -20,7 +20,7 @@ describe('TransactionService', function() { database: database, rpc: rpc }); - service.should.exist; + should.exist(service); }); describe('get', function() {