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 testKey;
|
||||||
var client;
|
var client;
|
||||||
|
|
||||||
|
var outputForIsSpentTest1;
|
||||||
|
|
||||||
describe('Node Functionality', function() {
|
describe('Node Functionality', function() {
|
||||||
|
|
||||||
var regtest;
|
var regtest;
|
||||||
@ -264,7 +266,7 @@ describe('Node Functionality', function() {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
results.length.should.equal(1);
|
results.length.should.equal(1);
|
||||||
unspentOutput = results[0];
|
unspentOutput = outputForIsSpentTest1 = results[0];
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -293,6 +295,25 @@ describe('Node Functionality', function() {
|
|||||||
done();
|
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() {
|
describe('History', function() {
|
||||||
|
|
||||||
this.timeout(20000);
|
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.mempoolOutputIndex = {};
|
||||||
this.mempoolInputIndex = {};
|
this.mempoolInputIndex = {};
|
||||||
|
this.mempoolSpentIndex = {};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,7 +53,8 @@ AddressService.prototype.getAPIMethods = function() {
|
|||||||
['getOutputs', this, this.getOutputs, 2],
|
['getOutputs', this, this.getOutputs, 2],
|
||||||
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
||||||
['isSpent', this, this.isSpent, 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++) {
|
for (var inputIndex = 0; inputIndex < inputLength; inputIndex++) {
|
||||||
|
|
||||||
var input = tx.inputs[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);
|
var address = input.script.toAddress(this.node.network);
|
||||||
if (!address) {
|
if (!address) {
|
||||||
continue;
|
continue;
|
||||||
@ -197,6 +204,7 @@ AddressService.prototype.resetMempoolIndex = function(callback) {
|
|||||||
var transactionBuffers = self.node.services.bitcoind.getMempoolTransactions();
|
var transactionBuffers = self.node.services.bitcoind.getMempoolTransactions();
|
||||||
this.mempoolInputIndex = {};
|
this.mempoolInputIndex = {};
|
||||||
this.mempoolOutputIndex = {};
|
this.mempoolOutputIndex = {};
|
||||||
|
this.mempoolSpentIndex = {};
|
||||||
async.each(transactionBuffers, function(txBuffer, next) {
|
async.each(transactionBuffers, function(txBuffer, next) {
|
||||||
var tx = Transaction().fromBuffer(txBuffer);
|
var tx = Transaction().fromBuffer(txBuffer);
|
||||||
self.updateMempoolIndex(tx);
|
self.updateMempoolIndex(tx);
|
||||||
@ -871,4 +879,121 @@ AddressService.prototype.getAddressHistory = function(addresses, options, callba
|
|||||||
history.get(callback);
|
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;
|
module.exports = AddressService;
|
||||||
|
|||||||
@ -31,7 +31,7 @@ describe('Address Service', function() {
|
|||||||
it('should return the correct methods', function() {
|
it('should return the correct methods', function() {
|
||||||
var am = new AddressService({node: mocknode});
|
var am = new AddressService({node: mocknode});
|
||||||
var methods = am.getAPIMethods();
|
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
Block a user