Merge pull request #53 from pnagurny/feature/get-txs-address

getAddressHistory
This commit is contained in:
Braydon Fuller 2015-07-31 11:20:49 -04:00
commit 47219a745a
8 changed files with 549 additions and 13 deletions

View File

@ -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();
});
});
});
});

View File

@ -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);
};

View File

@ -80,6 +80,20 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) {
});
};
DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
this.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) {
if(err) {
return callback(err);
}
var tx = Transaction().fromBuffer(obj.buffer);
tx.__height = obj.height;
tx.__timestamp = obj.timestamp;
callback(null, tx);
});
}
DB.prototype.validateBlockData = function(block, callback) {
// bitcoind does the validation
setImmediate(callback);

View File

@ -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() {
@ -31,7 +33,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],
['getAddressHistory', this, this.getAddressHistory, 2]
];
};
@ -116,6 +119,14 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
continue;
}
for(var j = 0; j < inputs.length; j++) {
var input = inputs[j].toObject();
operations.push({
type: action,
key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
value: [txid, j].join(':')
});
}
}
setImmediate(function() {
@ -281,4 +292,115 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) {
});
};
module.exports = AddressModule;
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, value) {
if(err) {
return callback(err);
}
value = value.split(':');
var info = {
txid: value[0],
inputIndex: value[1]
};
callback(null, info);
});
};
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);
}
async.eachSeries(
outputs,
function(output, next) {
getTransactionInfo(output.txid, function(err, txinfo) {
if(err) {
return next(err);
}
txinfo.outputIndexes.push(output.outputIndex);
txinfo.satoshis += output.satoshis;
self.getSpendInfoForOutput(output.txid, output.outputIndex, function(err, spendInfo) {
if(err instanceof levelup.errors.NotFoundError) {
return next();
} else if(err) {
return next(err);
}
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) {
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);
}
);
});
};
module.exports = AddressModule;

View File

@ -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++) {

View File

@ -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<Function> 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<Function> callback = Local<Function>::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<Function> 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<async_tx_data*>(req->data);
uint256 hash = uint256S(data->txid);
uint256 blockHash;
CTransaction ctx;
if (data->queryMempool) {
LOCK(mempool.cs);
map<uint256, CTxMemPoolEntry>::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<async_tx_data*>(req->data);
CTransaction ctx = data->ctx;
Local<Function> cb = data->callback.Get(isolate);
Local<Object> obj = NanNew<Object>();
if (data->err_msg != "") {
Local<Value> err = Exception::Error(NanNew<String>(data->err_msg));
const unsigned argc = 1;
Local<Value> 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<Value> rawNodeBuffer = node::Buffer::New(isolate, stx.c_str(), stx.size());
obj->Set(NanNew<String>("height"), NanNew<Number>(data->height));
obj->Set(NanNew<String>("timestamp"), NanNew<Number>(data->nTime));
obj->Set(NanNew<String>("buffer"), rawNodeBuffer);
const unsigned argc = 2;
Local<Value> argv[argc] = {
Local<Value>::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<Object> 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);

View File

@ -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(4);
methods.length.should.equal(5);
});
});
@ -111,21 +113,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, 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);
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, 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(':'));
done();
});
});
@ -481,4 +497,210 @@ describe('AddressModule', function() {
});
});
describe('#getSpendInfoForOutput', function() {
it('should call store.get the right values', function(done) {
var db = {
store: {
get: sinon.stub().callsArgWith(1, null, 'spendtxid:1')
}
};
var am = new AddressModule({db: db});
am.getSpendInfoForOutput('txid', 3, function(err, info) {
should.not.exist(err);
info.txid.should.equal('spendtxid');
info.inputIndex.should.equal('1');
db.store.get.args[0][0].should.equal('sp-txid-3');
done();
});
});
});
describe('#getAddressHistory', function() {
var incoming = [
{
txid: 'tx1',
outputIndex: 0,
spentTx: 'tx2',
inputIndex: 0,
height: 1,
timestamp: 1438289011844,
satoshis: 5000
},
{
txid: 'tx3',
outputIndex: 1,
height: 3,
timestamp: 1438289031844,
satoshis: 2000
},
{
txid: 'tx4',
outputIndex: 2,
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 = {
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, 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 transaction history for an address', function(done) {
am.getAddressHistory('address', true, function(err, history) {
should.not.exist(err);
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();
});
});
});
});

View File

@ -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);