Merge pull request #238 from pnagurny/feature/address-summary
Get address summary
This commit is contained in:
commit
50ddd4b152
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue