Merge pull request #238 from pnagurny/feature/address-summary

Get address summary
This commit is contained in:
Braydon Fuller 2015-09-21 12:53:27 -04:00
commit 50ddd4b152
3 changed files with 257 additions and 3 deletions

View File

@ -33,6 +33,8 @@ var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
var testKey;
var client;
var outputForIsSpentTest1;
describe('Node Functionality', function() {
var regtest;
@ -264,7 +266,7 @@ describe('Node Functionality', function() {
throw err;
}
results.length.should.equal(1);
unspentOutput = results[0];
unspentOutput = outputForIsSpentTest1 = results[0];
done();
});
});
@ -293,6 +295,25 @@ describe('Node Functionality', function() {
done();
});
});
it('correctly give the summary for the address', function(done) {
var options = {
queryMempool: false
};
node.services.address.getAddressSummary(address, options, function(err, results) {
if (err) {
throw err;
}
results.totalReceived.should.equal(1000000000);
results.totalSpent.should.equal(0);
results.balance.should.equal(1000000000);
results.unconfirmedBalance.should.equal(1000000000);
results.appearances.should.equal(1);
results.unconfirmedAppearances.should.equal(0);
results.txids.length.should.equal(1);
done();
});
});
describe('History', function() {
this.timeout(20000);
@ -695,5 +716,36 @@ describe('Node Functionality', function() {
});
});
describe('isSpent', function() {
it('will return true if an input is spent in a confirmed transaction', function(done) {
var result = node.services.bitcoind.isSpent(outputForIsSpentTest1.txid, outputForIsSpentTest1.outputIndex);
result.should.equal(true);
done();
});
it('will incorrectly return false for an input that is spent in an unconfirmed transaction', function(done) {
node.services.address.getUnspentOutputs(address, false, function(err, results) {
if (err) {
throw err;
}
var unspentOutput = results[0];
var tx = new Transaction();
tx.from(unspentOutput);
tx.to(address, unspentOutput.satoshis - 1000);
tx.fee(1000);
tx.sign(testKey);
node.services.bitcoind.sendTransaction(tx.serialize());
setImmediate(function() {
var result = node.services.bitcoind.isSpent(unspentOutput.txid, unspentOutput.outputIndex);
result.should.equal(false);
done();
});
});
});
});
});
});

View File

@ -28,6 +28,7 @@ var AddressService = function(options) {
this.mempoolOutputIndex = {};
this.mempoolInputIndex = {};
this.mempoolSpentIndex = {};
};
@ -52,7 +53,8 @@ AddressService.prototype.getAPIMethods = function() {
['getOutputs', this, this.getOutputs, 2],
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
['isSpent', this, this.isSpent, 2],
['getAddressHistory', this, this.getAddressHistory, 2]
['getAddressHistory', this, this.getAddressHistory, 2],
['getAddressSummary', this, this.getAddressSummary, 1]
];
};
@ -176,6 +178,11 @@ AddressService.prototype.updateMempoolIndex = function(tx) {
for (var inputIndex = 0; inputIndex < inputLength; inputIndex++) {
var input = tx.inputs[inputIndex];
// Update spent index
var spentIndexKey = [input.prevTxId.toString('hex'), input.outputIndex].join('-');
this.mempoolSpentIndex[spentIndexKey] = true;
var address = input.script.toAddress(this.node.network);
if (!address) {
continue;
@ -197,6 +204,7 @@ AddressService.prototype.resetMempoolIndex = function(callback) {
var transactionBuffers = self.node.services.bitcoind.getMempoolTransactions();
this.mempoolInputIndex = {};
this.mempoolOutputIndex = {};
this.mempoolSpentIndex = {};
async.each(transactionBuffers, function(txBuffer, next) {
var tx = Transaction().fromBuffer(txBuffer);
self.updateMempoolIndex(tx);
@ -871,4 +879,121 @@ AddressService.prototype.getAddressHistory = function(addresses, options, callba
history.get(callback);
};
/**
* This will return an object with:
* balance - confirmed balance
* unconfirmedBalance - unconfirmed balance
* totalReceived - satoshis received
* totalSpent - satoshis spent
* appearances - number of times used in confirmed transactions
* unconfirmedAppearances - number of times used in unconfirmed transactions
* txids - list of txids (unless noTxList is set)
*
* @param {String} address
* @param {Object} options
* @param {Boolean} options.noTxList - if set, txid array will not be included
* @param {Function} callback
*/
AddressService.prototype.getAddressSummary = function(address, options, callback) {
var self = this;
var opt = {
queryMempool: true
};
var outputs;
var inputs;
var mempoolInputs;
async.parallel(
[
function(next) {
if(options.noTxList) {
setImmediate(next);
} else {
self.getInputs(address, opt, function(err, ins) {
inputs = ins;
next(err);
});
}
},
function(next) {
self.getOutputs(address, opt, function(err, outs) {
outputs = outs;
next(err);
});
}
],
function(err) {
if(err) {
return callback(err);
}
var totalReceived = 0;
var totalSpent = 0;
var balance = 0;
var unconfirmedBalance = 0;
var appearances = 0;
var unconfirmedAppearances = 0;
var txids = [];
for(var i = 0; i < outputs.length; i++) {
// Bitcoind's isSpent at the moment only works for confirmed transactions
var spentDB = self.node.services.bitcoind.isSpent(outputs[i].txid, outputs[i].outputIndex);
var spentIndexKey = [outputs[i].txid, outputs[i].outputIndex].join('-');
var spentMempool = self.mempoolSpentIndex[spentIndexKey];
txids.push(outputs[i]);
unconfirmedBalance += outputs[i].satoshis;
if(outputs[i].confirmations) {
totalReceived += outputs[i].satoshis;
balance += outputs[i].satoshis;
appearances++;
} else {
unconfirmedAppearances++;
}
if(spentDB || spentMempool) {
unconfirmedBalance -= outputs[i].satoshis;
if(spentDB) {
totalSpent += outputs[i].satoshis;
balance -= outputs[i].satoshis;
appearances++;
} else {
unconfirmedAppearances++;
}
}
}
var summary = {
totalReceived: totalReceived,
totalSpent: totalSpent,
balance: balance,
unconfirmedBalance: unconfirmedBalance,
appearances: appearances,
unconfirmedAppearances: unconfirmedAppearances
};
if(inputs) {
for(var i = 0; i < inputs.length; i++) {
txids.push(inputs[i]);
}
// sort by height
txids = txids.sort(function(a, b) {
return a.height > b.height ? 1 : -1;
}).map(function(obj) {
return obj.txid;
}).filter(function(value, index, self) {
return self.indexOf(value) === index;
});
summary.txids = txids;
}
callback(null, summary);
}
);
};
module.exports = AddressService;

View File

@ -31,7 +31,7 @@ describe('Address Service', function() {
it('should return the correct methods', function() {
var am = new AddressService({node: mocknode});
var methods = am.getAPIMethods();
methods.length.should.equal(5);
methods.length.should.equal(6);
});
});
@ -954,4 +954,81 @@ describe('Address Service', function() {
});
});
});
describe('#getAddressSummary', function() {
var node = {
services: {
bitcoind: {
isSpent: sinon.stub().returns(false),
on: sinon.spy()
}
}
};
var inputs = [
{
"txid": "9f183412de12a6c1943fc86c390174c1cde38d709217fdb59dcf540230fa58a6",
"height": -1,
"confirmations": 0,
"addresses": {
"mpkDdnLq26djg17s6cYknjnysAm3QwRzu2": {
"outputIndexes": [],
"inputIndexes": [
3
]
}
},
"address": "mpkDdnLq26djg17s6cYknjnysAm3QwRzu2"
}
];
var outputs = [
{
"address": "mpkDdnLq26djg17s6cYknjnysAm3QwRzu2",
"txid": "689e9f543fa4aa5b2daa3b5bb65f9a00ad5aa1a2e9e1fc4e11061d85f2aa9bc5",
"outputIndex": 0,
"height": 556351,
"satoshis": 3487110,
"script": "76a914653b58493c2208481e0902a8ffb97b8112b13fe188ac",
"confirmations": 13190
}
];
var as = new AddressService({node: node});
as.getInputs = sinon.stub().callsArgWith(2, null, inputs);
as.getOutputs = sinon.stub().callsArgWith(2, null, outputs);
as.mempoolSpentIndex = {
'689e9f543fa4aa5b2daa3b5bb65f9a00ad5aa1a2e9e1fc4e11061d85f2aa9bc5-0': true
};
it('should handle unconfirmed and confirmed outputs and inputs', function(done) {
as.getAddressSummary('mpkDdnLq26djg17s6cYknjnysAm3QwRzu2', {}, function(err, summary) {
should.not.exist(err);
summary.totalReceived.should.equal(3487110);
summary.totalSpent.should.equal(0);
summary.balance.should.equal(3487110);
summary.unconfirmedBalance.should.equal(0);
summary.appearances.should.equal(1);
summary.unconfirmedAppearances.should.equal(1);
summary.txids.should.deep.equal(
[
'9f183412de12a6c1943fc86c390174c1cde38d709217fdb59dcf540230fa58a6',
'689e9f543fa4aa5b2daa3b5bb65f9a00ad5aa1a2e9e1fc4e11061d85f2aa9bc5'
]
);
done();
});
});
it('noTxList should not include txids array', function(done) {
as.getAddressSummary('mpkDdnLq26djg17s6cYknjnysAm3QwRzu2', {noTxList: true}, function(err, summary) {
should.not.exist(err);
summary.totalReceived.should.equal(3487110);
summary.totalSpent.should.equal(0);
summary.balance.should.equal(3487110);
summary.unconfirmedBalance.should.equal(0);
summary.appearances.should.equal(1);
summary.unconfirmedAppearances.should.equal(1);
should.not.exist(summary.txids);
done();
});
});
});
});