From d5801c9172a3f779cfc04ddcff2bb24aa3a37680 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 24 Jul 2015 09:21:32 -0600 Subject: [PATCH 1/8] getTransactionsForAddress() without mempool --- lib/modules/address.js | 56 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 89e62874..69da0c19 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -31,7 +31,8 @@ AddressModule.prototype.getAPIMethods = function() { ['getBalance', this, this.getBalance, 2], ['getOutputs', this, this.getOutputs, 2], ['getUnspentOutputs', this, this.getUnspentOutputs, 2], - ['isSpent', this, this.isSpent, 2] + ['isSpent', this, this.isSpent, 2], + ['getTransactionsForAddress', this, this.getTransactionsForAddress, 2] ]; }; @@ -281,4 +282,55 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) { }); }; -module.exports = AddressModule; +AddressModule.prototype.getTransactionsForAddress = function(address, queryMempool, callback) { + var self = this; + + var txids = []; + var key = [AddressModule.PREFIXES.OUTPUTS, address].join('-'); + + var stream = this.db.store.createReadStream({ + start: key, + end: key + '~' + }); + + stream.on('data', function(data) { + var key = data.key.split('-'); + txids.push(key[3]); + }); + + var error; + + stream.on('error', function(streamError) { + if (streamError) { + error = streamError; + } + }); + + stream.on('close', function() { + if (error) { + return callback(error); + } + + async.map( + txids, + function(txid, next) { + self.db.getTransaction(txid, next); + }, + function(err, transactions) { + if(err) { + return callback(err); + } + + if(queryMempool) { + // TODO + } + + callback(null, transactions); + }); + }); + + return stream; + +}; + +module.exports = AddressModule; \ No newline at end of file From ac774ba9e89f352dddacb17682e7f5f51d884eaa Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 24 Jul 2015 12:34:33 -0600 Subject: [PATCH 2/8] find transactions which spend from the address --- lib/modules/address.js | 95 +++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 69da0c19..4148aa97 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -5,6 +5,7 @@ var inherits = require('util').inherits; var async = require('async'); var chainlib = require('chainlib'); var log = chainlib.log; +var levelup = chainlib.deps.levelup; var errors = chainlib.errors; var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; @@ -23,7 +24,8 @@ var AddressModule = function(options) { inherits(AddressModule, BaseModule); AddressModule.PREFIXES = { - OUTPUTS: 'outs' + OUTPUTS: 'outs', + SPENTS: 'sp' }; AddressModule.prototype.getAPIMethods = function() { @@ -117,6 +119,13 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { continue; } + for(var j = 0; j < inputs.length; j++) { + operations.push({ + type: action, + key: [AddressModule.PREFIXES.SPENTS, inputs[j].prevTxId, inputs[j].outputIndex].join('-'), + value: txid + }); + } } setImmediate(function() { @@ -282,55 +291,55 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) { }); }; +AddressModule.prototype.getSpendTxForOutput = function(txid, outputIndex, queryMempool, callback) { + var self = this; + + var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-'); + this.db.store.get(key, function(err, spentTxId) { + if(err) { + return callback(err); + } + + self.db.getTransaction(spentTxId, queryMempool, callback); + }); +}; + AddressModule.prototype.getTransactionsForAddress = function(address, queryMempool, callback) { var self = this; - var txids = []; - var key = [AddressModule.PREFIXES.OUTPUTS, address].join('-'); - - var stream = this.db.store.createReadStream({ - start: key, - end: key + '~' - }); - - stream.on('data', function(data) { - var key = data.key.split('-'); - txids.push(key[3]); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); + this.getOutputs(address, queryMempool, function(err, outputs) { + if(err) { + return callback(err); } - async.map( - txids, - function(txid, next) { - self.db.getTransaction(txid, next); + var transactions = []; + + async.eachSeries( + outputs, + function(output, next) { + self.db.getTransaction(output.txid, queryMempool, function(err, tx) { + if(err) { + return next(err); + } + + transactions.push(tx); + + self.db.getSpendTxForOutput(output.txid, output.outputIndex, queryMempool, function(err, tx) { + if(err instanceof levelup.errors.NotFoundError) { + return next(); + } else if(err) { + return next(err); + } + + transactions.push(tx); + next(); + }); + }); }, - function(err, transactions) { - if(err) { - return callback(err); - } - - if(queryMempool) { - // TODO - } - - callback(null, transactions); - }); + function(err) { + callback(err, transactions); + }); }); - - return stream; - }; module.exports = AddressModule; \ No newline at end of file From bede8a96ea038696a2cb81fd6adcce2249c769df Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 24 Jul 2015 13:43:17 -0600 Subject: [PATCH 3/8] convert input to object --- lib/modules/address.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 4148aa97..b2661653 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -34,7 +34,8 @@ AddressModule.prototype.getAPIMethods = function() { ['getOutputs', this, this.getOutputs, 2], ['getUnspentOutputs', this, this.getUnspentOutputs, 2], ['isSpent', this, this.isSpent, 2], - ['getTransactionsForAddress', this, this.getTransactionsForAddress, 2] + ['getTransactionsForAddress', this, this.getTransactionsForAddress, 2], + ['getSpendTxForOutput', this, this.getSpendTxForOutput, 3] ]; }; @@ -120,9 +121,10 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { } for(var j = 0; j < inputs.length; j++) { + var input = inputs[j].toObject(); operations.push({ type: action, - key: [AddressModule.PREFIXES.SPENTS, inputs[j].prevTxId, inputs[j].outputIndex].join('-'), + key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'), value: txid }); } @@ -324,7 +326,7 @@ AddressModule.prototype.getTransactionsForAddress = function(address, queryMempo transactions.push(tx); - self.db.getSpendTxForOutput(output.txid, output.outputIndex, queryMempool, function(err, tx) { + self.getSpendTxForOutput(output.txid, output.outputIndex, queryMempool, function(err, tx) { if(err instanceof levelup.errors.NotFoundError) { return next(); } else if(err) { From 073353f895f932eeb1c9c9626d1de6a188787854 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 24 Jul 2015 14:18:51 -0600 Subject: [PATCH 4/8] write tests --- lib/modules/address.js | 4 +- test/modules/address.unit.js | 85 ++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index b2661653..ef7cce1e 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -297,12 +297,12 @@ AddressModule.prototype.getSpendTxForOutput = function(txid, outputIndex, queryM var self = this; var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-'); - this.db.store.get(key, function(err, spentTxId) { + this.db.store.get(key, function(err, spendTxId) { if(err) { return callback(err); } - self.db.getTransaction(spentTxId, queryMempool, callback); + self.db.getTransaction(spendTxId, queryMempool, callback); }); }; diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 1bd55439..40d5fa68 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -15,7 +15,7 @@ describe('AddressModule', function() { it('should return the correct methods', function() { var am = new AddressModule({}); var methods = am.getAPIMethods(); - methods.length.should.equal(4); + methods.length.should.equal(6); }); }); @@ -111,21 +111,35 @@ describe('AddressModule', function() { it('should create the correct operations when updating/adding outputs', function(done) { am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) { should.not.exist(err); - operations.length.should.equal(11); + operations.length.should.equal(81); operations[0].type.should.equal('put'); var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'); operations[0].key.should.equal(expected0); operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); + operations[3].type.should.equal('put'); + var expected3 = ['sp', key3.prevTxId, key3.prevOutputIndex].join('-'); + operations[3].key.should.equal(expected3); + operations[3].value.should.equal([value3.txid].join(':')); + operations[64].type.should.equal('put'); + var expected64 = ['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-'); + operations[64].key.should.equal(expected64); + operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':')); done(); }); }); it('should create the correct operations when removing outputs', function(done) { am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { should.not.exist(err); - operations.length.should.equal(11); + operations.length.should.equal(81); operations[0].type.should.equal('del'); operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-')); operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); + operations[3].type.should.equal('del'); + operations[3].key.should.equal(['sp', key3.prevTxId, key3.prevOutputIndex].join('-')); + operations[3].value.should.equal([value3.txid].join(':')); + operations[64].type.should.equal('del'); + operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-')); + operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':')); done(); }); }); @@ -481,4 +495,69 @@ describe('AddressModule', function() { }); }); + describe('#getSpendTxForOutput', function() { + it('should call store.get and db.getTransaction with the right values', function(done) { + var db = { + store: { + get: sinon.stub().callsArgWith(1, null, 'spendtxid') + }, + getTransaction: sinon.stub().callsArgWith(2, null, 'spendtx') + }; + var am = new AddressModule({db: db}); + am.getSpendTxForOutput('txid', 'outputindex', true, function(err, tx) { + should.not.exist(err); + tx.should.equal('spendtx'); + db.store.get.args[0][0].should.equal('sp-txid-outputindex'); + db.getTransaction.args[0][0].should.equal('spendtxid'); + db.getTransaction.args[0][1].should.equal(true); + done(); + }); + }); + }); + + describe('#getTransactionsForAddress', function() { + var outputs = [ + { + txid: 'tx1', + outputIndex: 0, + spentTx: 'tx2' + }, + { + txid: 'tx3', + outputIndex: 1 + }, + { + txid: 'tx4', + outputIndex: 2, + spentTx: 'tx5' + } + ]; + + var db = { + getTransaction: function(txid, queryMempool, callback) { + callback(null, txid); + } + }; + var am = new AddressModule({db: db}); + + am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); + am.getSpendTxForOutput = function(txid, outputIndex, queryMempool, callback) { + for(var i = 0; i < outputs.length; i++) { + if(outputs[i].txid === txid && outputs[i].outputIndex === outputIndex && outputs[i].spentTx) { + return callback(null, outputs[i].spentTx); + } + } + + callback(new levelup.errors.NotFoundError()); + }; + + it('should give transactions containing address as an output and an input', function(done) { + am.getTransactionsForAddress('address', true, function(err, txs) { + should.not.exist(err); + txs.should.deep.equal(['tx1', 'tx2', 'tx3', 'tx4', 'tx5']); + done(); + }); + }); + }); + }); From 7e8d17ae13de1452d5a10e90194ca4262a0d1a78 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Wed, 29 Jul 2015 10:24:29 -0400 Subject: [PATCH 5/8] refactor into getAddressHistory --- lib/db.js | 15 +++++++ lib/modules/address.js | 92 ++++++++++++++++++++++++++++++++++-------- lib/transaction.js | 6 ++- 3 files changed, 96 insertions(+), 17 deletions(-) diff --git a/lib/db.js b/lib/db.js index 7d7b6298..621520fe 100644 --- a/lib/db.js +++ b/lib/db.js @@ -80,6 +80,21 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { }); }; +DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { + this.getTransaction(txid, queryMempool, callback); + /*this.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { + if(err) { + return callback(err); + } + + var tx = Transaction().fromBuffer(obj.buffer); + tx.__blockHeight = obj.blockHeight; + tx.__timestamp = obj.timestamp; + + callback(null, tx); + });*/ +} + DB.prototype.validateBlockData = function(block, callback) { // bitcoind does the validation setImmediate(callback); diff --git a/lib/modules/address.js b/lib/modules/address.js index ef7cce1e..81d5d727 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -34,8 +34,7 @@ AddressModule.prototype.getAPIMethods = function() { ['getOutputs', this, this.getOutputs, 2], ['getUnspentOutputs', this, this.getUnspentOutputs, 2], ['isSpent', this, this.isSpent, 2], - ['getTransactionsForAddress', this, this.getTransactionsForAddress, 2], - ['getSpendTxForOutput', this, this.getSpendTxForOutput, 3] + ['getAddressHistory', this, this.getAddressHistory, 2] ]; }; @@ -125,7 +124,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { operations.push({ type: action, key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'), - value: txid + value: [txid, j].join(':') }); } } @@ -293,54 +292,115 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) { }); }; -AddressModule.prototype.getSpendTxForOutput = function(txid, outputIndex, queryMempool, callback) { +AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) { var self = this; var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-'); - this.db.store.get(key, function(err, spendTxId) { + this.db.store.get(key, function(err, value) { if(err) { return callback(err); } - self.db.getTransaction(spendTxId, queryMempool, callback); + value = value.split(':'); + + var info = { + txid: value[0], + inputIndex: value[1] + }; + + callback(null, info); }); }; -AddressModule.prototype.getTransactionsForAddress = function(address, queryMempool, callback) { +AddressModule.prototype.getAddressHistory = function(address, queryMempool, callback) { var self = this; + var txinfos = {}; + + function getTransactionInfo(txid, callback) { + if(txinfos[txid]) { + return callback(null, txinfos[txid]); + } + + self.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) { + if(err) { + return callback(err); + } + + transaction.populateInputs(self.db, [], function(err) { + if(err) { + return callback(err); + } + + txinfos[transaction.hash] = { + satoshis: 0, + height: transaction.__height, + timestamp: transaction.__timestamp, + outputIndexes: [], + inputIndexes: [], + transaction: transaction + }; + + callback(null, txinfos[transaction.hash]); + }); + }); + } + + this.getOutputs(address, queryMempool, function(err, outputs) { if(err) { return callback(err); } - var transactions = []; - async.eachSeries( outputs, function(output, next) { - self.db.getTransaction(output.txid, queryMempool, function(err, tx) { + getTransactionInfo(output.txid, function(err, txinfo) { if(err) { return next(err); } - transactions.push(tx); + txinfo.outputIndexes.push(output.outputIndex); + txinfo.satoshis += output.satoshis; - self.getSpendTxForOutput(output.txid, output.outputIndex, queryMempool, function(err, tx) { + self.getSpendInfoForOutput(output.txid, output.outputIndex, function(err, spendInfo) { if(err instanceof levelup.errors.NotFoundError) { return next(); } else if(err) { return next(err); } - transactions.push(tx); - next(); + getTransactionInfo(spendInfo.txid, function(err, txinfo) { + if(err) { + return next(err); + } + + txinfo.inputIndexes.push(spendInfo.inputIndex); + txinfo.satoshis -= txinfo.transaction.inputs[spendInfo.inputIndex].output.satoshis; + next(); + }); }); }); }, function(err) { - callback(err, transactions); - }); + if(err) { + return callback(err); + } + + // convert to array + var history = []; + for(var txid in txinfos) { + history.push(txinfos[txid]); + } + + // sort by height + history.sort(function(a, b) { + return a.height < b.height; + }); + + callback(null, history); + } + ); }); }; diff --git a/lib/transaction.js b/lib/transaction.js index 76893db7..f2431409 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -54,6 +54,10 @@ Transaction.prototype._validateInputs = function(db, poolTransactions, callback) Transaction.prototype.populateInputs = function(db, poolTransactions, callback) { var self = this; + if(this.isCoinbase()) { + return setImmediate(callback); + } + async.each( this.inputs, function(input, next) { @@ -68,7 +72,7 @@ Transaction.prototype._populateInput = function(db, input, poolTransactions, cal return callback(new Error('Input is expected to have prevTxId as a buffer')); } var txid = input.prevTxId.toString('hex'); - db.getTransactionFromDB(txid, function(err, prevTx) { + db.getTransaction(txid, false, function(err, prevTx) { if(err instanceof levelup.errors.NotFoundError) { // Check the pool for transaction for(var i = 0; i < poolTransactions.length; i++) { From 4a4e71797a6130caef0e5fceaa87bfbcf3003496 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Wed, 29 Jul 2015 17:13:51 -0400 Subject: [PATCH 6/8] getTransactionWithBlockInfo --- integration/regtest.js | 12 +++ lib/daemon.js | 4 + lib/db.js | 7 +- src/bitcoindjs.cc | 161 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 178 insertions(+), 6 deletions(-) diff --git a/integration/regtest.js b/integration/regtest.js index d96f2228..116e2789 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -304,4 +304,16 @@ describe('Daemon Binding Functionality', function() { }); + describe('get transaction with block info', function() { + it('should include tx buffer, height and timestamp', function(done) { + bitcoind.getTransactionWithBlockInfo(utxo.txid, true, function(err, data) { + should.not.exist(err); + data.height.should.equal(151); + should.exist(data.timestamp); + should.exist(data.buffer); + done(); + }); + }); + }); + }); diff --git a/lib/daemon.js b/lib/daemon.js index 0133bfb2..3f9f985d 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -362,6 +362,10 @@ Daemon.prototype.getTransactionWithBlock = function(txid, blockhash, callback) { }); }; +Daemon.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { + return bitcoindjs.getTransactionWithBlockInfo(txid, queryMempool, callback); +}; + Daemon.prototype.getMempoolOutputs = function(address) { return bitcoindjs.getMempoolOutputs(address); }; diff --git a/lib/db.js b/lib/db.js index 621520fe..dd0d89b8 100644 --- a/lib/db.js +++ b/lib/db.js @@ -81,18 +81,17 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { }; DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - this.getTransaction(txid, queryMempool, callback); - /*this.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { + this.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { if(err) { return callback(err); } var tx = Transaction().fromBuffer(obj.buffer); - tx.__blockHeight = obj.blockHeight; + tx.__height = obj.height; tx.__timestamp = obj.timestamp; callback(null, tx); - });*/ + }); } DB.prototype.validateBlockData = function(block, callback) { diff --git a/src/bitcoindjs.cc b/src/bitcoindjs.cc index 172286f9..fb153016 100644 --- a/src/bitcoindjs.cc +++ b/src/bitcoindjs.cc @@ -74,6 +74,12 @@ async_get_tx(uv_work_t *req); static void async_get_tx_after(uv_work_t *req); +static void +async_get_tx_and_info(uv_work_t *req); + +static void +async_get_tx_and_info_after(uv_work_t *req); + static bool process_messages(CNode* pfrom); @@ -156,6 +162,8 @@ struct async_tx_data { std::string err_msg; std::string txid; std::string blockhash; + uint32_t nTime; + int64_t height; bool queryMempool; CTransaction ctx; Eternal callback; @@ -1018,7 +1026,7 @@ async_get_block_after(uv_work_t *req) { /** * GetTransaction() - * bitcoind.getTransaction(txid, callback) + * bitcoind.getTransaction(txid, queryMempool, callback) * Read any transaction from disk asynchronously. */ @@ -1030,7 +1038,7 @@ NAN_METHOD(GetTransaction) { || !args[1]->IsBoolean() || !args[2]->IsFunction()) { return NanThrowError( - "Usage: bitcoindjs.getTransaction(txid, callback)"); + "Usage: daemon.getTransaction(txid, queryMempool, callback)"); } String::Utf8Value txid_(args[0]->ToString()); @@ -1145,6 +1153,154 @@ async_get_tx_after(uv_work_t *req) { delete req; } +/** + * GetTransactionWithBlockInfo() + * bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback) + * Read any transaction from disk asynchronously with block timestamp and height. + */ + +NAN_METHOD(GetTransactionWithBlockInfo) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope scope(isolate); + if (args.Length() < 3 + || !args[0]->IsString() + || !args[1]->IsBoolean() + || !args[2]->IsFunction()) { + return NanThrowError( + "Usage: bitcoindjs.getTransactionWithBlockInfo(txid, queryMempool, callback)"); + } + + String::Utf8Value txid_(args[0]->ToString()); + bool queryMempool = args[1]->BooleanValue(); + Local callback = Local::Cast(args[2]); + + async_tx_data *data = new async_tx_data(); + + data->err_msg = std::string(""); + data->txid = std::string(""); + + std::string txid = std::string(*txid_); + + data->txid = txid; + data->queryMempool = queryMempool; + Eternal eternal(isolate, callback); + data->callback = eternal; + + uv_work_t *req = new uv_work_t(); + req->data = data; + + int status = uv_queue_work(uv_default_loop(), + req, async_get_tx_and_info, + (uv_after_work_cb)async_get_tx_and_info_after); + + assert(status == 0); + + NanReturnValue(Undefined(isolate)); +} + +static void +async_get_tx_and_info(uv_work_t *req) { + async_tx_data* data = static_cast(req->data); + + uint256 hash = uint256S(data->txid); + uint256 blockHash; + CTransaction ctx; + + if (data->queryMempool) { + LOCK(mempool.cs); + map::const_iterator i = mempool.mapTx.find(hash); + if (i != mempool.mapTx.end()) { + data->ctx = i->second.GetTx(); + data->nTime = i->second.GetTime(); + data->height = -1; + return; + } + } + + CDiskTxPos postx; + if (pblocktree->ReadTxIndex(hash, postx)) { + + CAutoFile file(OpenBlockFile(postx, true), SER_DISK, CLIENT_VERSION); + + if (file.IsNull()) { + data->err_msg = std::string("%s: OpenBlockFile failed", __func__); + return; + } + + CBlockHeader blockHeader; + + try { + // Read header first to get block timestamp and hash + file >> blockHeader; + blockHash = blockHeader.GetHash(); + data->nTime = blockHeader.nTime; + fseek(file.Get(), postx.nTxOffset, SEEK_CUR); + file >> ctx; + data->ctx = ctx; + } catch (const std::exception& e) { + data->err_msg = std::string("Deserialize or I/O error - %s", __func__); + return; + } + + // get block height + CBlockIndex* blockIndex; + + if (mapBlockIndex.count(blockHash) == 0) { + data->height = -1; + } else { + blockIndex = mapBlockIndex[blockHash]; + data->height = blockIndex->nHeight; + } + + } + +} + +static void +async_get_tx_and_info_after(uv_work_t *req) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope scope(isolate); + async_tx_data* data = static_cast(req->data); + + CTransaction ctx = data->ctx; + Local cb = data->callback.Get(isolate); + Local obj = NanNew(); + + if (data->err_msg != "") { + Local err = Exception::Error(NanNew(data->err_msg)); + const unsigned argc = 1; + Local argv[argc] = { err }; + TryCatch try_catch; + cb->Call(isolate->GetCurrentContext()->Global(), argc, argv); + if (try_catch.HasCaught()) { + node::FatalException(try_catch); + } + } else { + + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << ctx; + std::string stx = ssTx.str(); + Local rawNodeBuffer = node::Buffer::New(isolate, stx.c_str(), stx.size()); + + obj->Set(NanNew("height"), NanNew(data->height)); + obj->Set(NanNew("timestamp"), NanNew(data->nTime)); + obj->Set(NanNew("buffer"), rawNodeBuffer); + + const unsigned argc = 2; + Local argv[argc] = { + Local::New(isolate, NanNull()), + obj + }; + TryCatch try_catch; + cb->Call(isolate->GetCurrentContext()->Global(), argc, argv); + if (try_catch.HasCaught()) { + node::FatalException(try_catch); + } + } + delete data; + delete req; +} + /** * IsSpent() * bitcoindjs.isSpent() @@ -1465,6 +1621,7 @@ init(Handle target) { NODE_SET_METHOD(target, "stopped", IsStopped); NODE_SET_METHOD(target, "getBlock", GetBlock); NODE_SET_METHOD(target, "getTransaction", GetTransaction); + NODE_SET_METHOD(target, "getTransactionWithBlockInfo", GetTransactionWithBlockInfo); NODE_SET_METHOD(target, "getInfo", GetInfo); NODE_SET_METHOD(target, "isSpent", IsSpent); NODE_SET_METHOD(target, "getBlockIndex", GetBlockIndex); From 2a55c900cef15176cbb92756c7709501c56854ca Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 30 Jul 2015 17:45:55 -0400 Subject: [PATCH 7/8] update tests --- lib/modules/address.js | 3 +- test/modules/address.unit.js | 200 ++++++++++++++++++++++++++++++----- test/transaction.unit.js | 9 +- 3 files changed, 178 insertions(+), 34 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 81d5d727..c4a582f8 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -346,7 +346,6 @@ AddressModule.prototype.getAddressHistory = function(address, queryMempool, call }); } - this.getOutputs(address, queryMempool, function(err, outputs) { if(err) { return callback(err); @@ -395,7 +394,7 @@ AddressModule.prototype.getAddressHistory = function(address, queryMempool, call // sort by height history.sort(function(a, b) { - return a.height < b.height; + return a.height > b.height; }); callback(null, history); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 40d5fa68..a4b2d0ab 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -8,6 +8,8 @@ var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var EventEmitter = require('events').EventEmitter; var errors = bitcoindjs.errors; +var chainlib = require('chainlib'); +var levelup = chainlib.deps.levelup; describe('AddressModule', function() { @@ -15,7 +17,7 @@ describe('AddressModule', function() { it('should return the correct methods', function() { var am = new AddressModule({}); var methods = am.getAPIMethods(); - methods.length.should.equal(6); + methods.length.should.equal(5); }); }); @@ -119,7 +121,7 @@ describe('AddressModule', function() { operations[3].type.should.equal('put'); var expected3 = ['sp', key3.prevTxId, key3.prevOutputIndex].join('-'); operations[3].key.should.equal(expected3); - operations[3].value.should.equal([value3.txid].join(':')); + operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':')); operations[64].type.should.equal('put'); var expected64 = ['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-'); operations[64].key.should.equal(expected64); @@ -136,7 +138,7 @@ describe('AddressModule', function() { operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); operations[3].type.should.equal('del'); operations[3].key.should.equal(['sp', key3.prevTxId, key3.prevOutputIndex].join('-')); - operations[3].value.should.equal([value3.txid].join(':')); + operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':')); operations[64].type.should.equal('del'); operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-')); operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':')); @@ -495,66 +497,208 @@ describe('AddressModule', function() { }); }); - describe('#getSpendTxForOutput', function() { - it('should call store.get and db.getTransaction with the right values', function(done) { + describe('#getSpendInfoForOutput', function() { + it('should call store.get the right values', function(done) { var db = { store: { - get: sinon.stub().callsArgWith(1, null, 'spendtxid') - }, - getTransaction: sinon.stub().callsArgWith(2, null, 'spendtx') + get: sinon.stub().callsArgWith(1, null, 'spendtxid:1') + } }; var am = new AddressModule({db: db}); - am.getSpendTxForOutput('txid', 'outputindex', true, function(err, tx) { + am.getSpendInfoForOutput('txid', 3, function(err, info) { should.not.exist(err); - tx.should.equal('spendtx'); - db.store.get.args[0][0].should.equal('sp-txid-outputindex'); - db.getTransaction.args[0][0].should.equal('spendtxid'); - db.getTransaction.args[0][1].should.equal(true); + info.txid.should.equal('spendtxid'); + info.inputIndex.should.equal('1'); + db.store.get.args[0][0].should.equal('sp-txid-3'); done(); }); }); }); - describe('#getTransactionsForAddress', function() { - var outputs = [ + describe('#getAddressHistory', function() { + var incoming = [ { txid: 'tx1', outputIndex: 0, - spentTx: 'tx2' + spentTx: 'tx2', + inputIndex: 0, + height: 1, + timestamp: 1438289011844, + satoshis: 5000 }, { txid: 'tx3', - outputIndex: 1 + outputIndex: 1, + height: 3, + timestamp: 1438289031844, + satoshis: 2000 }, { txid: 'tx4', outputIndex: 2, - spentTx: 'tx5' + spentTx: 'tx5', + inputIndex: 1, + height: 4, + timestamp: 1438289041844, + satoshis: 3000 + }, + ]; + + var outgoing = [ + { + txid: 'tx2', + height: 2, + timestamp: 1438289021844, + inputs: [ + { + output: { + satoshis: 5000 + } + } + ] + }, + { + txid: 'tx5', + height: 5, + timestamp: 1438289051844, + inputs: [ + {}, + { + output: { + satoshis: 3000 + } + } + ] } ]; var db = { - getTransaction: function(txid, queryMempool, callback) { - callback(null, txid); + getTransactionWithBlockInfo: function(txid, queryMempool, callback) { + var transaction = { + populateInputs: sinon.stub().callsArg(2) + }; + for(var i = 0; i < incoming.length; i++) { + if(incoming[i].txid === txid) { + if(incoming[i].error) { + return callback(new Error(incoming[i].error)); + } + transaction.hash = txid; + transaction.__height = incoming[i].height; + transaction.__timestamp = incoming[i].timestamp; + return callback(null, transaction); + } + } + + for(var i = 0; i < outgoing.length; i++) { + if(outgoing[i].txid === txid) { + if(outgoing[i].error) { + return callback(new Error(outgoing[i].error)); + } + transaction.hash = txid; + transaction.__height = outgoing[i].height; + transaction.__timestamp = outgoing[i].timestamp; + transaction.inputs = outgoing[i].inputs; + return callback(null, transaction); + } + } + callback(new Error('tx ' + txid + ' not found')); } }; var am = new AddressModule({db: db}); - am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.getSpendTxForOutput = function(txid, outputIndex, queryMempool, callback) { - for(var i = 0; i < outputs.length; i++) { - if(outputs[i].txid === txid && outputs[i].outputIndex === outputIndex && outputs[i].spentTx) { - return callback(null, outputs[i].spentTx); + am.getOutputs = sinon.stub().callsArgWith(2, null, incoming); + am.getSpendInfoForOutput = function(txid, outputIndex, callback) { + for(var i = 0; i < incoming.length; i++) { + if(incoming[i].txid === txid && incoming[i].outputIndex === outputIndex && incoming[i].spentTx) { + if(incoming[i].spendError) { + return callback(new Error(incoming[i].spendError)); + } + return callback(null, { + txid: incoming[i].spentTx, + inputIndex: incoming[i].inputIndex + }); } } callback(new levelup.errors.NotFoundError()); }; - it('should give transactions containing address as an output and an input', function(done) { - am.getTransactionsForAddress('address', true, function(err, txs) { + it('should give transaction history for an address', function(done) { + am.getAddressHistory('address', true, function(err, history) { should.not.exist(err); - txs.should.deep.equal(['tx1', 'tx2', 'tx3', 'tx4', 'tx5']); + console.log(history); + history[0].transaction.hash.should.equal('tx1'); + history[0].satoshis.should.equal(5000); + history[0].height.should.equal(1); + history[0].timestamp.should.equal(1438289011844); + history[1].transaction.hash.should.equal('tx2'); + history[1].satoshis.should.equal(-5000); + history[1].height.should.equal(2); + history[1].timestamp.should.equal(1438289021844); + history[2].transaction.hash.should.equal('tx3'); + history[2].satoshis.should.equal(2000); + history[2].height.should.equal(3); + history[2].timestamp.should.equal(1438289031844); + history[3].transaction.hash.should.equal('tx4'); + history[3].satoshis.should.equal(3000); + history[3].height.should.equal(4); + history[3].timestamp.should.equal(1438289041844); + history[4].transaction.hash.should.equal('tx5'); + history[4].satoshis.should.equal(-3000); + history[4].height.should.equal(5); + history[4].timestamp.should.equal(1438289051844); + done(); + }); + }); + + it('should give an error if the second getTransactionInfo gives an error', function(done) { + outgoing[0].error = 'txinfo2err'; + am.getAddressHistory('address', true, function(err, history) { + should.exist(err); + err.message.should.equal('txinfo2err'); + outgoing[0].error = null; + done(); + }); + }); + + it('should give an error if getSpendInfoForOutput gives an error', function(done) { + incoming[0].spendError = 'spenderr'; + am.getAddressHistory('address', true, function(err, history) { + should.exist(err); + err.message.should.equal('spenderr'); + incoming[0].spendError = null; + done(); + }); + }); + + it('should give an error if the first getTransactionInfo gives an error', function(done) { + incoming[1].error = 'txinfo1err'; + am.getAddressHistory('address', true, function(err, history) { + should.exist(err); + err.message.should.equal('txinfo1err'); + incoming[1].error = null; + done(); + }); + }); + + it('should give an error if populateInputs gives an error', function(done) { + var populateStub = sinon.stub().callsArgWith(2, new Error('populateerr')); + sinon.stub(db, 'getTransactionWithBlockInfo').callsArgWith(2, null, { + populateInputs: populateStub + }); + am.getAddressHistory('address', true, function(err, history) { + should.exist(err); + err.message.should.equal('populateerr'); + db.getTransactionWithBlockInfo.restore(); + done(); + }); + }); + + it('should give an error if getOutputs gives an error', function(done) { + am.getOutputs = sinon.stub().callsArgWith(2, new Error('getoutputserr')); + am.getAddressHistory('address', true, function(err, history) { + should.exist(err); + err.message.should.equal('getoutputserr'); done(); }); }); diff --git a/test/transaction.unit.js b/test/transaction.unit.js index cb3d56ab..9f02ba74 100644 --- a/test/transaction.unit.js +++ b/test/transaction.unit.js @@ -106,6 +106,7 @@ describe('Bitcoin Transaction', function() { describe('#populateInputs', function() { it('will call _populateInput with transactions', function() { var tx = new Transaction(); + tx.isCoinbase = sinon.stub().returns(false); tx._populateInput = sinon.stub().callsArg(3); tx.inputs = ['input']; var transactions = []; @@ -138,7 +139,7 @@ describe('Bitcoin Transaction', function() { it('if an error happened it should pass it along', function(done) { var tx = new Transaction(); var db = { - getTransactionFromDB: sinon.stub().callsArgWith(1, new Error('error')) + getTransaction: sinon.stub().callsArgWith(2, new Error('error')) }; tx._populateInput(db, input, [], function(err) { should.exist(err); @@ -149,7 +150,7 @@ describe('Bitcoin Transaction', function() { it('should return an error if the transaction for the input does not exist', function(done) { var tx = new Transaction(); var db = { - getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError()) + getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) }; tx._populateInput(db, input, [], function(err) { should.exist(err); @@ -160,7 +161,7 @@ describe('Bitcoin Transaction', function() { it('should look through poolTransactions if database does not have transaction', function(done) { var tx = new Transaction(); var db = { - getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError()) + getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) }; var transactions = [ { @@ -179,7 +180,7 @@ describe('Bitcoin Transaction', function() { prevTx.outputs = ['output']; var tx = new Transaction(); var db = { - getTransactionFromDB: sinon.stub().callsArgWith(1, null, prevTx) + getTransaction: sinon.stub().callsArgWith(2, null, prevTx) }; tx._populateInput(db, input, [], function(err) { should.not.exist(err); From afee19e1f7866ee7999e56b042c6fe9981624fba Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 31 Jul 2015 10:54:44 -0400 Subject: [PATCH 8/8] remove console.log --- test/modules/address.unit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index a4b2d0ab..71c32478 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -626,7 +626,6 @@ describe('AddressModule', function() { it('should give transaction history for an address', function(done) { am.getAddressHistory('address', true, function(err, history) { should.not.exist(err); - console.log(history); history[0].transaction.hash.should.equal('tx1'); history[0].satoshis.should.equal(5000); history[0].height.should.equal(1);