Merge pull request #223 from braydonf/history
Pagination for Address History
This commit is contained in:
commit
696759849f
|
@ -24,6 +24,7 @@ var should = chai.should();
|
|||
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var index = require('..');
|
||||
var Transaction = index.Transaction;
|
||||
var BitcoreNode = index.Node;
|
||||
var AddressService = index.services.Address;
|
||||
var BitcoinService = index.services.Bitcoin;
|
||||
|
@ -34,6 +35,8 @@ var client;
|
|||
|
||||
describe('Node Functionality', function() {
|
||||
|
||||
var regtest;
|
||||
|
||||
before(function(done) {
|
||||
this.timeout(30000);
|
||||
|
||||
|
@ -51,6 +54,7 @@ describe('Node Functionality', function() {
|
|||
port: 18444,
|
||||
dnsSeeds: [ ]
|
||||
});
|
||||
regtest = bitcore.Networks.get('regtest');
|
||||
|
||||
var datadir = __dirname + '/data';
|
||||
|
||||
|
@ -238,4 +242,407 @@ describe('Node Functionality', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Address Functionality', function() {
|
||||
var address;
|
||||
var unspentOutput;
|
||||
before(function() {
|
||||
address = testKey.toAddress().toString();
|
||||
});
|
||||
it('should be able to get the balance of the test address', function(done) {
|
||||
node.services.address.getBalance(address, false, function(err, balance) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
balance.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get unspent outputs for address', function(done) {
|
||||
node.services.address.getUnspentOutputs(address, false, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
unspentOutput = results[0];
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('correctly give the history for the address', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 10,
|
||||
queryMempool: false
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
var info = results[0];
|
||||
should.exist(info.addresses[address]);
|
||||
info.addresses[address].outputIndexes.length.should.equal(1);
|
||||
info.addresses[address].outputIndexes[0].should.be.within(0, 1);
|
||||
info.addresses[address].inputIndexes.should.deep.equal([]);
|
||||
info.satoshis.should.equal(10 * 1e8);
|
||||
info.confirmations.should.equal(3);
|
||||
info.timestamp.should.be.a('number');
|
||||
info.fees.should.be.within(190, 193);
|
||||
info.tx.should.be.an.instanceof(Transaction);
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('History', function() {
|
||||
|
||||
this.timeout(20000);
|
||||
|
||||
var testKey2;
|
||||
var address2;
|
||||
var testKey3;
|
||||
var address3;
|
||||
var testKey4;
|
||||
var address4;
|
||||
var testKey5;
|
||||
var address5;
|
||||
var testKey6;
|
||||
var address6;
|
||||
|
||||
before(function(done) {
|
||||
/* jshint maxstatements: 50 */
|
||||
|
||||
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
|
||||
address2 = testKey2.toAddress().toString();
|
||||
|
||||
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
|
||||
address3 = testKey3.toAddress().toString();
|
||||
|
||||
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
|
||||
address4 = testKey4.toAddress().toString();
|
||||
|
||||
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
|
||||
address5 = testKey5.toAddress().toString();
|
||||
|
||||
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
|
||||
address6 = testKey6.toAddress().toString();
|
||||
|
||||
var tx = new Transaction();
|
||||
tx.from(unspentOutput);
|
||||
tx.to(address, 1 * 1e8);
|
||||
tx.to(address, 2 * 1e8);
|
||||
tx.to(address, 0.5 * 1e8);
|
||||
tx.to(address, 3 * 1e8);
|
||||
tx.fee(10000);
|
||||
tx.change(address);
|
||||
tx.sign(testKey);
|
||||
|
||||
node.services.bitcoind.sendTransaction(tx.serialize());
|
||||
|
||||
function mineBlock(next) {
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
node.once('synced', function() {
|
||||
node.services.address.getUnspentOutputs(address, false, function(err, results) {
|
||||
/* jshint maxstatements: 50 */
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(5);
|
||||
|
||||
async.series([
|
||||
function(next) {
|
||||
var tx2 = new Transaction();
|
||||
tx2.from(results[0]);
|
||||
tx2.to(address2, results[0].satoshis - 10000);
|
||||
tx2.change(address);
|
||||
tx2.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx2.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx3 = new Transaction();
|
||||
tx3.from(results[1]);
|
||||
tx3.to(address3, results[1].satoshis - 10000);
|
||||
tx3.change(address);
|
||||
tx3.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx3.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx4 = new Transaction();
|
||||
tx4.from(results[2]);
|
||||
tx4.to(address4, results[2].satoshis - 10000);
|
||||
tx4.change(address);
|
||||
tx4.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx4.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx5 = new Transaction();
|
||||
tx5.from(results[3]);
|
||||
tx5.from(results[4]);
|
||||
tx5.to(address5, results[3].satoshis - 10000);
|
||||
tx5.to(address6, results[4].satoshis - 10000);
|
||||
tx5.change(address);
|
||||
tx5.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx5.serialize());
|
||||
mineBlock(next);
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
node.once('synced', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('five addresses', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(4);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(156);
|
||||
should.exist(history[1].addresses[address4]);
|
||||
history[2].height.should.equal(155);
|
||||
should.exist(history[2].addresses[address3]);
|
||||
history[3].height.should.equal(154);
|
||||
should.exist(history[3].addresses[address2]);
|
||||
history[3].satoshis.should.equal(99990000);
|
||||
history[3].confirmations.should.equal(4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (limited by height)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
start: 157,
|
||||
end: 156
|
||||
};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(2);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(156);
|
||||
should.exist(history[1].addresses[address4]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (limited by height 155 to 154)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
start: 155,
|
||||
end: 154
|
||||
};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(2);
|
||||
history[0].height.should.equal(155);
|
||||
history[1].height.should.equal(154);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (paginated by index)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 3
|
||||
};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(3);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(156);
|
||||
should.exist(history[1].addresses[address4]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('one address with sending and receiving', function(done) {
|
||||
var addresses = [
|
||||
address
|
||||
];
|
||||
var options = {};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(6);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].addresses[address].inputIndexes.should.deep.equal([0, 1]);
|
||||
history[0].addresses[address].outputIndexes.should.deep.equal([2]);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(156);
|
||||
history[2].height.should.equal(155);
|
||||
history[3].height.should.equal(154);
|
||||
history[4].height.should.equal(153);
|
||||
history[4].satoshis.should.equal(-10000);
|
||||
history[4].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[4].addresses[address].inputIndexes.should.deep.equal([0]);
|
||||
history[5].height.should.equal(150);
|
||||
history[5].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('total transaction count (sending and receiving)', function(done) {
|
||||
var addresses = [
|
||||
address
|
||||
];
|
||||
var options = {};
|
||||
node.services.address.getAddressHistoryCount(addresses, options, function(err, count) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
count.should.equal(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', function() {
|
||||
it('from 0 to 1', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 1
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(157);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 1 to 2', function(done) {
|
||||
var options = {
|
||||
from: 1,
|
||||
to: 2
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(156);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 2 to 3', function(done) {
|
||||
var options = {
|
||||
from: 2,
|
||||
to: 3
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(155);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 3 to 4', function(done) {
|
||||
var options = {
|
||||
from: 3,
|
||||
to: 4
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(154);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 4 to 5', function(done) {
|
||||
var options = {
|
||||
from: 4,
|
||||
to: 5
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(153);
|
||||
history[0].satoshis.should.equal(-10000);
|
||||
history[0].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[0].addresses[address].inputIndexes.should.deep.equal([0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 5 to 6', function(done) {
|
||||
var options = {
|
||||
from: 5,
|
||||
to: 6
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(150);
|
||||
history[0].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,586 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var BaseService = require('../service');
|
||||
var inherits = require('util').inherits;
|
||||
var async = require('async');
|
||||
var index = require('../');
|
||||
var log = index.log;
|
||||
var levelup = require('levelup');
|
||||
var errors = index.errors;
|
||||
var bitcore = require('bitcore');
|
||||
var $ = bitcore.util.preconditions;
|
||||
var _ = bitcore.deps._;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var PublicKey = bitcore.PublicKey;
|
||||
var Address = bitcore.Address;
|
||||
|
||||
var AddressService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
|
||||
this.subscriptions = {};
|
||||
this.subscriptions['address/transaction'] = {};
|
||||
this.subscriptions['address/balance'] = {};
|
||||
|
||||
this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this));
|
||||
|
||||
};
|
||||
|
||||
inherits(AddressService, BaseService);
|
||||
|
||||
AddressService.dependencies = [
|
||||
'bitcoind',
|
||||
'db'
|
||||
];
|
||||
|
||||
AddressService.PREFIXES = {
|
||||
OUTPUTS: 'outs',
|
||||
SPENTS: 'sp'
|
||||
};
|
||||
|
||||
AddressService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getBalance', this, this.getBalance, 2],
|
||||
['getOutputs', this, this.getOutputs, 2],
|
||||
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
||||
['isSpent', this, this.isSpent, 2],
|
||||
['getAddressHistory', this, this.getAddressHistory, 2]
|
||||
];
|
||||
};
|
||||
|
||||
AddressService.prototype.getPublishEvents = function() {
|
||||
return [
|
||||
{
|
||||
name: 'address/transaction',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'address/transaction'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'address/transaction')
|
||||
},
|
||||
{
|
||||
name: 'address/balance',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'address/balance'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'address/balance')
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Will process each output of a transaction from the daemon "tx" event, and construct
|
||||
* an object with the data for the message to be relayed to any subscribers for an address.
|
||||
*
|
||||
* @param {Object} messages - An object to collect messages
|
||||
* @param {Transaction} tx - Instance of the transaction
|
||||
* @param {Number} outputIndex - The index of the output in the transaction
|
||||
* @param {Boolean} rejected - If the transaction was rejected by the mempool
|
||||
*/
|
||||
AddressService.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) {
|
||||
var script = tx.outputs[outputIndex].script;
|
||||
|
||||
// If the script is invalid skip
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the address for the output
|
||||
var address = script.toAddress(this.node.network);
|
||||
if (!address && script.isPublicKeyOut()) {
|
||||
var pubkey = script.chunks[0].buf;
|
||||
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network);
|
||||
} else if (!address){
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect data to publish to address subscribers
|
||||
if (messages[address]) {
|
||||
messages[address].outputIndexes.push(outputIndex);
|
||||
} else {
|
||||
messages[address] = {
|
||||
tx: tx,
|
||||
outputIndexes: [outputIndex],
|
||||
address: address.toString(),
|
||||
rejected: rejected
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will handle data from the daemon "tx" event, go through each of the outputs
|
||||
* and send messages to any subscribers for a particular address.
|
||||
*
|
||||
* @param {Object} txInfo - The data from the daemon.on('tx') event
|
||||
* @param {Buffer} txInfo.buffer - The transaction buffer
|
||||
* @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool
|
||||
* @param {String} txInfo.hash - The hash of the transaction
|
||||
*/
|
||||
AddressService.prototype.transactionHandler = function(txInfo) {
|
||||
|
||||
// Basic transaction format is handled by the daemon
|
||||
// and we can safely assume the buffer is properly formatted.
|
||||
var tx = bitcore.Transaction().fromBuffer(txInfo.buffer);
|
||||
|
||||
var messages = {};
|
||||
|
||||
var outputsLength = tx.outputs.length;
|
||||
for (var i = 0; i < outputsLength; i++) {
|
||||
this.transactionOutputHandler(messages, tx, i, !txInfo.mempool);
|
||||
}
|
||||
|
||||
for (var key in messages) {
|
||||
this.transactionEventHandler(messages[key]);
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
||||
var txs = block.transactions;
|
||||
|
||||
var action = 'put';
|
||||
if (!addOutput) {
|
||||
action = 'del';
|
||||
}
|
||||
|
||||
var operations = [];
|
||||
|
||||
var transactionLength = txs.length;
|
||||
for (var i = 0; i < transactionLength; i++) {
|
||||
|
||||
var tx = txs[i];
|
||||
var txid = tx.id;
|
||||
var inputs = tx.inputs;
|
||||
var outputs = tx.outputs;
|
||||
|
||||
// Subscription messages
|
||||
var txmessages = {};
|
||||
|
||||
var outputLength = outputs.length;
|
||||
for (var j = 0; j < outputLength; j++) {
|
||||
var output = outputs[j];
|
||||
|
||||
var script = output.script;
|
||||
|
||||
if(!script) {
|
||||
log.debug('Invalid script');
|
||||
continue;
|
||||
}
|
||||
|
||||
var address = script.toAddress(this.node.network);
|
||||
if (!address && script.isPublicKeyOut()) {
|
||||
var pubkey = script.chunks[0].buf;
|
||||
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network);
|
||||
} else if (!address){
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputIndex = j;
|
||||
// TODO: expose block timestamp as a date object in bitcore?
|
||||
var timestamp = block.header.timestamp;
|
||||
var height = block.__height;
|
||||
|
||||
var addressStr = address.toString();
|
||||
var scriptHex = output._scriptBuffer.toString('hex');
|
||||
|
||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-');
|
||||
var value = [output.satoshis, scriptHex, height].join(':');
|
||||
|
||||
operations.push({
|
||||
type: action,
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
|
||||
// Collect data for subscribers
|
||||
if (txmessages[addressStr]) {
|
||||
txmessages[addressStr].outputIndexes.push(outputIndex);
|
||||
} else {
|
||||
txmessages[addressStr] = {
|
||||
tx: tx,
|
||||
height: block.__height,
|
||||
outputIndexes: [outputIndex],
|
||||
address: addressStr,
|
||||
timestamp: block.header.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
this.balanceEventHandler(block, address);
|
||||
|
||||
}
|
||||
|
||||
// Publish events to any subscribers for this transaction
|
||||
for (var addressKey in txmessages) {
|
||||
this.transactionEventHandler(txmessages[addressKey]);
|
||||
}
|
||||
|
||||
if(tx.isCoinbase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(var k = 0; k < inputs.length; k++) {
|
||||
var input = inputs[k].toObject();
|
||||
operations.push({
|
||||
type: action,
|
||||
key: [AddressService.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
|
||||
value: [txid, k].join(':')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setImmediate(function() {
|
||||
callback(null, operations);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Transaction} obj.tx - The transaction
|
||||
* @param {String} [obj.address] - The address for the subscription
|
||||
* @param {Array} [obj.outputIndexes] - Indexes of the inputs that includes the address
|
||||
* @param {Array} [obj.inputIndexes] - Indexes of the outputs that includes the address
|
||||
* @param {Date} [obj.timestamp] - The time of the block the transaction was included
|
||||
* @param {Number} [obj.height] - The height of the block the transaction was included
|
||||
* @param {Boolean} [obj.rejected] - If the transaction was not accepted in the mempool
|
||||
*/
|
||||
AddressService.prototype.transactionEventHandler = function(obj) {
|
||||
if(this.subscriptions['address/transaction'][obj.address]) {
|
||||
var emitters = this.subscriptions['address/transaction'][obj.address];
|
||||
for(var i = 0; i < emitters.length; i++) {
|
||||
emitters[i].emit('address/transaction', obj);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.balanceEventHandler = function(block, address) {
|
||||
if(this.subscriptions['address/balance'][address]) {
|
||||
var emitters = this.subscriptions['address/balance'][address];
|
||||
this.getBalance(address, true, function(err, balance) {
|
||||
if(err) {
|
||||
return this.emit(err);
|
||||
}
|
||||
|
||||
for(var i = 0; i < emitters.length; i++) {
|
||||
emitters[i].emit('address/balance', address, balance, block);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.subscribe = function(name, emitter, addresses) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
$.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses');
|
||||
|
||||
for(var i = 0; i < addresses.length; i++) {
|
||||
if(!this.subscriptions[name][addresses[i]]) {
|
||||
this.subscriptions[name][addresses[i]] = [];
|
||||
}
|
||||
this.subscriptions[name][addresses[i]].push(emitter);
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.unsubscribe = function(name, emitter, addresses) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
$.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined');
|
||||
|
||||
if(!addresses) {
|
||||
return this.unsubscribeAll(name, emitter);
|
||||
}
|
||||
|
||||
for(var i = 0; i < addresses.length; i++) {
|
||||
if(this.subscriptions[name][addresses[i]]) {
|
||||
var emitters = this.subscriptions[name][addresses[i]];
|
||||
var index = emitters.indexOf(emitter);
|
||||
if(index > -1) {
|
||||
emitters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.unsubscribeAll = function(name, emitter) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
|
||||
for(var address in this.subscriptions[name]) {
|
||||
var emitters = this.subscriptions[name][address];
|
||||
var index = emitters.indexOf(emitter);
|
||||
if(index > -1) {
|
||||
emitters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
|
||||
this.getUnspentOutputs(address, queryMempool, function(err, outputs) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var satoshis = outputs.map(function(output) {
|
||||
return output.satoshis;
|
||||
});
|
||||
|
||||
var sum = satoshis.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
|
||||
return callback(null, sum);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getOutputs = function(addressStr, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
var outputs = [];
|
||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-');
|
||||
|
||||
var stream = this.node.services.db.store.createReadStream({
|
||||
start: key,
|
||||
end: key + '~'
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
|
||||
var key = data.key.split('-');
|
||||
var value = data.value.split(':');
|
||||
|
||||
var output = {
|
||||
address: addressStr,
|
||||
txid: key[3],
|
||||
outputIndex: Number(key[4]),
|
||||
timestamp: Number(key[2]),
|
||||
satoshis: Number(value[0]),
|
||||
script: value[1],
|
||||
blockHeight: Number(value[2]),
|
||||
confirmations: self.node.services.db.tip.__height - Number(value[2]) + 1
|
||||
};
|
||||
|
||||
outputs.push(output);
|
||||
|
||||
});
|
||||
|
||||
var error;
|
||||
|
||||
stream.on('error', function(streamError) {
|
||||
if (streamError) {
|
||||
error = streamError;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', function() {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(queryMempool) {
|
||||
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
|
||||
}
|
||||
|
||||
callback(null, outputs);
|
||||
});
|
||||
|
||||
return stream;
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
if(!Array.isArray(addresses)) {
|
||||
addresses = [addresses];
|
||||
}
|
||||
|
||||
var utxos = [];
|
||||
|
||||
async.eachSeries(addresses, function(address, next) {
|
||||
self.getUnspentOutputsForAddress(address, queryMempool, function(err, unspents) {
|
||||
if(err && err instanceof errors.NoOutputs) {
|
||||
return next();
|
||||
} else if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
utxos = utxos.concat(unspents);
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
callback(err, utxos);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.getOutputs(address, queryMempool, function(err, outputs) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
} else if(!outputs.length) {
|
||||
return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []);
|
||||
}
|
||||
|
||||
var isUnspent = function(output, callback) {
|
||||
self.isUnspent(output, queryMempool, callback);
|
||||
};
|
||||
|
||||
async.filter(outputs, isUnspent, function(results) {
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.isUnspent = function(output, queryMempool, callback) {
|
||||
this.isSpent(output, queryMempool, function(spent) {
|
||||
callback(!spent);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.isSpent = function(output, queryMempool, callback) {
|
||||
var self = this;
|
||||
var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid;
|
||||
|
||||
setImmediate(function() {
|
||||
callback(self.node.services.bitcoind.isSpent(txid, output.outputIndex));
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) {
|
||||
var self = this;
|
||||
|
||||
var key = [AddressService.PREFIXES.SPENTS, txid, outputIndex].join('-');
|
||||
this.node.services.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);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressHistory = function(addresses, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
if(!Array.isArray(addresses)) {
|
||||
addresses = [addresses];
|
||||
}
|
||||
|
||||
var history = [];
|
||||
|
||||
async.eachSeries(addresses, function(address, next) {
|
||||
self.getAddressHistoryForAddress(address, queryMempool, function(err, h) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
history = history.concat(h);
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
callback(err, history);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
var txinfos = {};
|
||||
|
||||
function getTransactionInfo(txid, callback) {
|
||||
if(txinfos[txid]) {
|
||||
return callback(null, txinfos[txid]);
|
||||
}
|
||||
|
||||
self.node.services.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
transaction.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var confirmations = 0;
|
||||
if(transaction.__height >= 0) {
|
||||
confirmations = self.node.services.db.tip.__height - transaction.__height;
|
||||
confirmations = self.node.services.db.tip.__height - transaction.__height + 1;
|
||||
}
|
||||
|
||||
txinfos[transaction.hash] = {
|
||||
address: address,
|
||||
satoshis: 0,
|
||||
height: transaction.__height,
|
||||
confirmations: confirmations,
|
||||
timestamp: transaction.__timestamp,
|
||||
// TODO bitcore should return null instead of throwing error on coinbase
|
||||
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
||||
outputIndexes: [],
|
||||
inputIndexes: [],
|
||||
tx: 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.tx.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 = AddressService;
|
|
@ -0,0 +1,257 @@
|
|||
'use strict';
|
||||
|
||||
var bitcore = require('bitcore');
|
||||
var async = require('async');
|
||||
var _ = bitcore.deps._;
|
||||
|
||||
/**
|
||||
* This represents an instance that keeps track of data over a series of
|
||||
* asynchronous I/O calls to get the transaction history for a group of
|
||||
* addresses. History can be queried by start and end block heights to limit large sets
|
||||
* of results (uses leveldb key streaming). See AddressService.prototype.getAddressHistory
|
||||
* for complete documentation about options.
|
||||
*/
|
||||
function AddressHistory(args) {
|
||||
this.node = args.node;
|
||||
this.options = args.options;
|
||||
|
||||
if(Array.isArray(args.addresses)) {
|
||||
this.addresses = args.addresses;
|
||||
} else {
|
||||
this.addresses = [args.addresses];
|
||||
}
|
||||
this.transactionInfo = [];
|
||||
this.combinedArray = [];
|
||||
this.detailedArray = [];
|
||||
}
|
||||
|
||||
AddressHistory.MAX_ADDRESS_QUERIES = 20;
|
||||
|
||||
AddressHistory.prototype.getCount = function(callback) {
|
||||
var self = this;
|
||||
|
||||
async.eachLimit(
|
||||
self.addresses,
|
||||
AddressHistory.MAX_ADDRESS_QUERIES,
|
||||
function(address, next) {
|
||||
self.getTransactionInfo(address, next);
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.combineTransactionInfo();
|
||||
callback(null, self.combinedArray.length);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
AddressHistory.prototype.get = function(callback) {
|
||||
var self = this;
|
||||
|
||||
// TODO check for mempool inputs and outputs by a group of addresses, currently
|
||||
// each address individually loops through the mempool and does not check input scripts.
|
||||
|
||||
async.eachLimit(
|
||||
self.addresses,
|
||||
AddressHistory.MAX_ADDRESS_QUERIES,
|
||||
function(address, next) {
|
||||
self.getTransactionInfo(address, next);
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.combineTransactionInfo();
|
||||
self.sortAndPaginateCombinedArray();
|
||||
|
||||
async.eachSeries(
|
||||
self.combinedArray,
|
||||
function(txInfo, next) {
|
||||
self.getDetailedInfo(txInfo, next);
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, self.detailedArray);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getTransactionInfo = function(address, next) {
|
||||
var self = this;
|
||||
|
||||
var args = {
|
||||
start: self.options.start,
|
||||
end: self.options.end,
|
||||
queryMempool: _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool
|
||||
};
|
||||
|
||||
var outputs;
|
||||
var inputs;
|
||||
|
||||
async.parallel([
|
||||
function(done) {
|
||||
self.node.services.address.getOutputs(address, args, function(err, result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
outputs = result;
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
self.node.services.address.getInputs(address, args, function(err, result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
inputs = result;
|
||||
done();
|
||||
});
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
self.transactionInfo = self.transactionInfo.concat(outputs, inputs);
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function combines results from getInputs and getOutputs by
|
||||
* combining inputIndexes and outputIndexes for address transaction
|
||||
* matching combinations.
|
||||
*/
|
||||
AddressHistory.prototype.combineTransactionInfo = function() {
|
||||
var combinedArrayMap = {};
|
||||
this.combinedArray = [];
|
||||
var l = this.transactionInfo.length;
|
||||
for(var i = 0; i < l; i++) {
|
||||
var item = this.transactionInfo[i];
|
||||
var mapKey = item.txid;
|
||||
if (combinedArrayMap[mapKey] >= 0) {
|
||||
var combined = this.combinedArray[combinedArrayMap[mapKey]];
|
||||
if (!combined.addresses[item.address]) {
|
||||
combined.addresses[item.address] = {
|
||||
outputIndexes: [],
|
||||
inputIndexes: []
|
||||
};
|
||||
}
|
||||
if (item.outputIndex >= 0) {
|
||||
combined.satoshis += item.satoshis;
|
||||
combined.addresses[item.address].outputIndexes.push(item.outputIndex);
|
||||
} else if (item.inputIndex >= 0) {
|
||||
combined.addresses[item.address].inputIndexes.push(item.inputIndex);
|
||||
}
|
||||
} else {
|
||||
item.addresses = {};
|
||||
item.addresses[item.address] = {
|
||||
outputIndexes: [],
|
||||
inputIndexes: []
|
||||
};
|
||||
if (item.outputIndex >= 0) {
|
||||
item.addresses[item.address].outputIndexes.push(item.outputIndex);
|
||||
} else if (item.inputIndex >= 0) {
|
||||
item.addresses[item.address].inputIndexes.push(item.inputIndex);
|
||||
}
|
||||
delete item.outputIndex;
|
||||
delete item.inputIndex;
|
||||
delete item.address;
|
||||
this.combinedArray.push(item);
|
||||
combinedArrayMap[mapKey] = this.combinedArray.length - 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.prototype.sortAndPaginateCombinedArray = function() {
|
||||
this.combinedArray.sort(AddressHistory.sortByHeight);
|
||||
if (!_.isUndefined(this.options.from) && !_.isUndefined(this.options.to)) {
|
||||
this.combinedArray = this.combinedArray.slice(this.options.from, this.options.to);
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.sortByHeight = function(a, b) {
|
||||
if (a.height < 0 && b.height < 0) {
|
||||
// Both are from the mempool, compare timestamps
|
||||
if (a.timestamp === b.timestamp) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.timestamp < b.timestamp ? 1 : -1;
|
||||
}
|
||||
} else if (a.height < 0 && b.height > 0) {
|
||||
// A is from the mempool and B is in a block
|
||||
return -1;
|
||||
} else if (a.height > 0 && b.height < 0) {
|
||||
// A is in a block and B is in the mempool
|
||||
return 1;
|
||||
} else if (a.height === b.height) {
|
||||
// The heights are equal
|
||||
return 0;
|
||||
} else {
|
||||
// Otherwise compare heights
|
||||
return a.height < b.height ? 1 : -1;
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getDetailedInfo = function(txInfo, next) {
|
||||
var self = this;
|
||||
var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool;
|
||||
|
||||
self.node.services.db.getTransactionWithBlockInfo(
|
||||
txInfo.txid,
|
||||
queryMempool,
|
||||
function(err, transaction) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
transaction.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.detailedArray.push({
|
||||
addresses: txInfo.addresses,
|
||||
satoshis: self.getSatoshisDetail(transaction, txInfo),
|
||||
height: transaction.__height,
|
||||
confirmations: self.getConfirmationsDetail(transaction),
|
||||
timestamp: transaction.__timestamp,
|
||||
// TODO bitcore should return null instead of throwing error on coinbase
|
||||
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
||||
tx: transaction
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getConfirmationsDetail = function(transaction) {
|
||||
var confirmations = 0;
|
||||
if (transaction.__height >= 0) {
|
||||
confirmations = this.node.services.db.tip.__height - transaction.__height + 1;
|
||||
}
|
||||
return confirmations;
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getSatoshisDetail = function(transaction, txInfo) {
|
||||
var satoshis = txInfo.satoshis || 0;
|
||||
|
||||
for(var address in txInfo.addresses) {
|
||||
if (txInfo.addresses[address].inputIndexes.length >= 0) {
|
||||
for(var j = 0; j < txInfo.addresses[address].inputIndexes.length; j++) {
|
||||
satoshis -= transaction.inputs[txInfo.addresses[address].inputIndexes[j]].output.satoshis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return satoshis;
|
||||
};
|
||||
|
||||
module.exports = AddressHistory;
|
|
@ -0,0 +1,775 @@
|
|||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var async = require('async');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var errors = index.errors;
|
||||
var bitcore = require('bitcore');
|
||||
var $ = bitcore.util.preconditions;
|
||||
var _ = bitcore.deps._;
|
||||
var Hash = bitcore.crypto.Hash;
|
||||
var BufferReader = bitcore.encoding.BufferReader;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var PublicKey = bitcore.PublicKey;
|
||||
var Address = bitcore.Address;
|
||||
var AddressHistory = require('./history');
|
||||
|
||||
var AddressService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
|
||||
this.subscriptions = {};
|
||||
this.subscriptions['address/transaction'] = {};
|
||||
this.subscriptions['address/balance'] = {};
|
||||
|
||||
this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this));
|
||||
|
||||
};
|
||||
|
||||
inherits(AddressService, BaseService);
|
||||
|
||||
AddressService.dependencies = [
|
||||
'bitcoind',
|
||||
'db'
|
||||
];
|
||||
|
||||
AddressService.PREFIXES = {
|
||||
OUTPUTS: new Buffer('32', 'hex'),
|
||||
SPENTS: new Buffer('33', 'hex')
|
||||
};
|
||||
|
||||
AddressService.SPACER_MIN = new Buffer('00', 'hex');
|
||||
AddressService.SPACER_MAX = new Buffer('ff', 'hex');
|
||||
|
||||
AddressService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getBalance', this, this.getBalance, 2],
|
||||
['getOutputs', this, this.getOutputs, 2],
|
||||
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
||||
['isSpent', this, this.isSpent, 2],
|
||||
['getAddressHistory', this, this.getAddressHistory, 2],
|
||||
['getAddressHistoryCount', this, this.getAddressHistoryCount, 2]
|
||||
];
|
||||
};
|
||||
|
||||
AddressService.prototype.getPublishEvents = function() {
|
||||
return [
|
||||
{
|
||||
name: 'address/transaction',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'address/transaction'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'address/transaction')
|
||||
},
|
||||
{
|
||||
name: 'address/balance',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'address/balance'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'address/balance')
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Will process each output of a transaction from the daemon "tx" event, and construct
|
||||
* an object with the data for the message to be relayed to any subscribers for an address.
|
||||
*
|
||||
* @param {Object} messages - An object to collect messages
|
||||
* @param {Transaction} tx - Instance of the transaction
|
||||
* @param {Number} outputIndex - The index of the output in the transaction
|
||||
* @param {Boolean} rejected - If the transaction was rejected by the mempool
|
||||
*/
|
||||
AddressService.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) {
|
||||
var script = tx.outputs[outputIndex].script;
|
||||
|
||||
// If the script is invalid skip
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
|
||||
var addressInfo = this._extractAddressInfoFromScript(script);
|
||||
if (!addressInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
addressInfo.hashHex = addressInfo.hashBuffer.toString('hex');
|
||||
|
||||
// Collect data to publish to address subscribers
|
||||
if (messages[addressInfo.hashHex]) {
|
||||
messages[addressInfo.hashHex].outputIndexes.push(outputIndex);
|
||||
} else {
|
||||
messages[addressInfo.hashHex] = {
|
||||
tx: tx,
|
||||
outputIndexes: [outputIndex],
|
||||
addressInfo: addressInfo,
|
||||
rejected: rejected
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will handle data from the daemon "tx" event, go through each of the outputs
|
||||
* and send messages to any subscribers for a particular address.
|
||||
*
|
||||
* @param {Object} txInfo - The data from the daemon.on('tx') event
|
||||
* @param {Buffer} txInfo.buffer - The transaction buffer
|
||||
* @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool
|
||||
* @param {String} txInfo.hash - The hash of the transaction
|
||||
*/
|
||||
AddressService.prototype.transactionHandler = function(txInfo) {
|
||||
|
||||
// Basic transaction format is handled by the daemon
|
||||
// and we can safely assume the buffer is properly formatted.
|
||||
var tx = bitcore.Transaction().fromBuffer(txInfo.buffer);
|
||||
|
||||
var messages = {};
|
||||
|
||||
var outputsLength = tx.outputs.length;
|
||||
for (var i = 0; i < outputsLength; i++) {
|
||||
this.transactionOutputHandler(messages, tx, i, !txInfo.mempool);
|
||||
}
|
||||
|
||||
for (var key in messages) {
|
||||
this.transactionEventHandler(messages[key]);
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype._extractAddressInfoFromScript = function(script) {
|
||||
var hashBuffer;
|
||||
var addressType;
|
||||
if (script.isPublicKeyHashOut()) {
|
||||
hashBuffer = script.chunks[2].buf;
|
||||
addressType = Address.PayToPublicKeyHash;
|
||||
} else if (script.isScriptHashOut()) {
|
||||
hashBuffer = script.chunks[1].buf;
|
||||
addressType = Address.PayToScriptHash;
|
||||
} else if (script.isPublicKeyOut()) {
|
||||
var pubkey = script.chunks[0].buf;
|
||||
var address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network);
|
||||
hashBuffer = address.hashBuffer;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
hashBuffer: hashBuffer,
|
||||
addressType: addressType
|
||||
};
|
||||
};
|
||||
|
||||
AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
||||
var txs = block.transactions;
|
||||
var height = block.__height;
|
||||
|
||||
var action = 'put';
|
||||
if (!addOutput) {
|
||||
action = 'del';
|
||||
}
|
||||
|
||||
var operations = [];
|
||||
|
||||
var transactionLength = txs.length;
|
||||
for (var i = 0; i < transactionLength; i++) {
|
||||
|
||||
var tx = txs[i];
|
||||
var txid = tx.id;
|
||||
var inputs = tx.inputs;
|
||||
var outputs = tx.outputs;
|
||||
|
||||
// Subscription messages
|
||||
var txmessages = {};
|
||||
|
||||
var outputLength = outputs.length;
|
||||
for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) {
|
||||
var output = outputs[outputIndex];
|
||||
|
||||
var script = output.script;
|
||||
|
||||
if(!script) {
|
||||
log.debug('Invalid script');
|
||||
continue;
|
||||
}
|
||||
|
||||
var addressInfo = this._extractAddressInfoFromScript(script);
|
||||
if (!addressInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need to use the height for indexes (and not the timestamp) because the
|
||||
// the timestamp has unreliable sequential ordering. The next block
|
||||
// can have a time that is previous to the previous block (however not
|
||||
// less than the mean of the 11 previous blocks) and not greater than 2
|
||||
// hours in the future.
|
||||
var key = this._encodeOutputKey(addressInfo.hashBuffer, height, txid, outputIndex);
|
||||
var value = this._encodeOutputValue(output.satoshis, output._scriptBuffer);
|
||||
operations.push({
|
||||
type: action,
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
|
||||
addressInfo.hashHex = addressInfo.hashBuffer.toString('hex');
|
||||
|
||||
// Collect data for subscribers
|
||||
if (txmessages[addressInfo.hashHex]) {
|
||||
txmessages[addressInfo.hashHex].outputIndexes.push(outputIndex);
|
||||
} else {
|
||||
txmessages[addressInfo.hashHex] = {
|
||||
tx: tx,
|
||||
height: height,
|
||||
outputIndexes: [outputIndex],
|
||||
addressInfo: addressInfo,
|
||||
timestamp: block.header.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
this.balanceEventHandler(block, addressInfo);
|
||||
|
||||
}
|
||||
|
||||
// Publish events to any subscribers for this transaction
|
||||
for (var addressKey in txmessages) {
|
||||
this.transactionEventHandler(txmessages[addressKey]);
|
||||
}
|
||||
|
||||
if(tx.isCoinbase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
|
||||
|
||||
var input = inputs[inputIndex];
|
||||
var inputHash;
|
||||
|
||||
if (input.script.isPublicKeyHashIn()) {
|
||||
inputHash = Hash.sha256ripemd160(input.script.chunks[1].buf);
|
||||
} else if (input.script.isScriptHashIn()) {
|
||||
inputHash = Hash.sha256ripemd160(input.script.chunks[input.script.chunks.length - 1].buf);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// To be able to query inputs by address and spent height
|
||||
var inputKey = this._encodeInputKey(inputHash, height, input.prevTxId, input.outputIndex);
|
||||
var inputValue = this._encodeInputValue(txid, inputIndex);
|
||||
|
||||
operations.push({
|
||||
type: action,
|
||||
key: inputKey,
|
||||
value: inputValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setImmediate(function() {
|
||||
callback(null, operations);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype._encodeOutputKey = function(hashBuffer, height, txid, outputIndex) {
|
||||
var heightBuffer = new Buffer(4);
|
||||
heightBuffer.writeUInt32BE(height);
|
||||
var outputIndexBuffer = new Buffer(4);
|
||||
outputIndexBuffer.writeUInt32BE(outputIndex);
|
||||
var key = Buffer.concat([
|
||||
AddressService.PREFIXES.OUTPUTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
heightBuffer,
|
||||
new Buffer(txid, 'hex'), //TODO get buffer directly from tx
|
||||
outputIndexBuffer
|
||||
]);
|
||||
return key;
|
||||
};
|
||||
|
||||
AddressService.prototype._decodeOutputKey = function(buffer) {
|
||||
var reader = new BufferReader(buffer);
|
||||
var prefix = reader.read(1);
|
||||
var hashBuffer = reader.read(20);
|
||||
var spacer = reader.read(1);
|
||||
var height = reader.readUInt32BE();
|
||||
var txid = reader.read(32);
|
||||
var outputIndex = reader.readUInt32BE();
|
||||
return {
|
||||
prefix: prefix,
|
||||
hashBuffer: hashBuffer,
|
||||
height: height,
|
||||
txid: txid,
|
||||
outputIndex: outputIndex
|
||||
};
|
||||
};
|
||||
|
||||
AddressService.prototype._encodeOutputValue = function(satoshis, scriptBuffer) {
|
||||
var satoshisBuffer = new Buffer(8);
|
||||
satoshisBuffer.writeDoubleBE(satoshis);
|
||||
return Buffer.concat([satoshisBuffer, scriptBuffer]);
|
||||
};
|
||||
|
||||
AddressService.prototype._decodeOutputValue = function(buffer) {
|
||||
var satoshis = buffer.readDoubleBE(0);
|
||||
var scriptBuffer = buffer.slice(8, buffer.length);
|
||||
return {
|
||||
satoshis: satoshis,
|
||||
scriptBuffer: scriptBuffer
|
||||
};
|
||||
};
|
||||
|
||||
AddressService.prototype._encodeInputKey = function(hashBuffer, height, prevTxIdBuffer, outputIndex) {
|
||||
var heightBuffer = new Buffer(4);
|
||||
heightBuffer.writeUInt32BE(height);
|
||||
var outputIndexBuffer = new Buffer(4);
|
||||
outputIndexBuffer.writeUInt32BE(outputIndex);
|
||||
return Buffer.concat([
|
||||
AddressService.PREFIXES.SPENTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
heightBuffer,
|
||||
prevTxIdBuffer,
|
||||
outputIndexBuffer
|
||||
]);
|
||||
};
|
||||
|
||||
AddressService.prototype._decodeInputKey = function(buffer) {
|
||||
var reader = new BufferReader(buffer);
|
||||
var prefix = reader.read(1);
|
||||
var hashBuffer = reader.read(20);
|
||||
var spacer = reader.read(1);
|
||||
var height = reader.readUInt32BE();
|
||||
var prevTxId = reader.read(32);
|
||||
var outputIndex = reader.readUInt32BE();
|
||||
return {
|
||||
prefix: prefix,
|
||||
hashBuffer: hashBuffer,
|
||||
height: height,
|
||||
prevTxId: prevTxId,
|
||||
outputIndex: outputIndex
|
||||
};
|
||||
};
|
||||
|
||||
AddressService.prototype._encodeInputValue = function(txid, inputIndex) {
|
||||
var inputIndexBuffer = new Buffer(4);
|
||||
inputIndexBuffer.writeUInt32BE(inputIndex);
|
||||
return Buffer.concat([
|
||||
new Buffer(txid, 'hex'),
|
||||
inputIndexBuffer
|
||||
]);
|
||||
};
|
||||
|
||||
AddressService.prototype._decodeInputValue = function(buffer) {
|
||||
var txid = buffer.slice(0, 32);
|
||||
var inputIndex = buffer.readUInt32BE(32);
|
||||
return {
|
||||
txid: txid,
|
||||
inputIndex: inputIndex
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Transaction} obj.tx - The transaction
|
||||
* @param {Object} obj.addressInfo
|
||||
* @param {String} obj.addressInfo.hashHex - The hex string of address hash for the subscription
|
||||
* @param {String} obj.addressInfo.hashBuffer - The address hash buffer
|
||||
* @param {String} obj.addressInfo.addressType - The address type
|
||||
* @param {Array} obj.outputIndexes - Indexes of the inputs that includes the address
|
||||
* @param {Array} obj.inputIndexes - Indexes of the outputs that includes the address
|
||||
* @param {Date} obj.timestamp - The time of the block the transaction was included
|
||||
* @param {Number} obj.height - The height of the block the transaction was included
|
||||
* @param {Boolean} obj.rejected - If the transaction was not accepted in the mempool
|
||||
*/
|
||||
AddressService.prototype.transactionEventHandler = function(obj) {
|
||||
if(this.subscriptions['address/transaction'][obj.addressInfo.hashHex]) {
|
||||
var emitters = this.subscriptions['address/transaction'][obj.addressInfo.hashHex];
|
||||
var address = new Address({
|
||||
hashBuffer: obj.addressInfo.hashBuffer,
|
||||
network: this.node.network,
|
||||
type: obj.addressInfo.addressType
|
||||
});
|
||||
for(var i = 0; i < emitters.length; i++) {
|
||||
emitters[i].emit('address/transaction', {
|
||||
rejected: obj.rejected,
|
||||
height: obj.height,
|
||||
timestamp: obj.timestamp,
|
||||
inputIndexes: obj.inputIndexes,
|
||||
outputIndexes: obj.outputIndexes,
|
||||
address: address,
|
||||
tx: obj.tx
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Block} block
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.hashHex
|
||||
* @param {Buffer} obj.hashBuffer
|
||||
* @param {String} obj.addressType
|
||||
*/
|
||||
AddressService.prototype.balanceEventHandler = function(block, obj) {
|
||||
if(this.subscriptions['address/balance'][obj.hashHex]) {
|
||||
var emitters = this.subscriptions['address/balance'][obj.hashHex];
|
||||
var address = new Address({
|
||||
hashBuffer: obj.hashBuffer,
|
||||
network: this.node.network,
|
||||
type: obj.addressType
|
||||
});
|
||||
this.getBalance(address, true, function(err, balance) {
|
||||
if(err) {
|
||||
return this.emit(err);
|
||||
}
|
||||
for(var i = 0; i < emitters.length; i++) {
|
||||
emitters[i].emit('address/balance', address, balance, block);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.subscribe = function(name, emitter, addresses) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
$.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses');
|
||||
|
||||
for(var i = 0; i < addresses.length; i++) {
|
||||
var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex');
|
||||
if(!this.subscriptions[name][hashHex]) {
|
||||
this.subscriptions[name][hashHex] = [];
|
||||
}
|
||||
this.subscriptions[name][hashHex].push(emitter);
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.unsubscribe = function(name, emitter, addresses) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
$.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined');
|
||||
|
||||
if(!addresses) {
|
||||
return this.unsubscribeAll(name, emitter);
|
||||
}
|
||||
|
||||
for(var i = 0; i < addresses.length; i++) {
|
||||
var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex');
|
||||
if(this.subscriptions[name][hashHex]) {
|
||||
var emitters = this.subscriptions[name][hashHex];
|
||||
var index = emitters.indexOf(emitter);
|
||||
if(index > -1) {
|
||||
emitters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.unsubscribeAll = function(name, emitter) {
|
||||
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
|
||||
|
||||
for(var hashHex in this.subscriptions[name]) {
|
||||
var emitters = this.subscriptions[name][hashHex];
|
||||
var index = emitters.indexOf(emitter);
|
||||
if(index > -1) {
|
||||
emitters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
|
||||
this.getUnspentOutputs(address, queryMempool, function(err, outputs) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var satoshis = outputs.map(function(output) {
|
||||
return output.satoshis;
|
||||
});
|
||||
|
||||
var sum = satoshis.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
|
||||
return callback(null, sum);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} addressStr - The relevant address
|
||||
* @param {Object} options - Additional options for query the outputs
|
||||
* @param {Number} [options.start] - The relevant start block height
|
||||
* @param {Number} [options.end] - The relevant end block height
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getInputs = function(addressStr, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var inputs = [];
|
||||
var stream;
|
||||
|
||||
var hashBuffer = bitcore.Address(addressStr).hashBuffer;
|
||||
|
||||
if (options.start && options.end) {
|
||||
|
||||
var endBuffer = new Buffer(4);
|
||||
endBuffer.writeUInt32BE(options.end);
|
||||
|
||||
var startBuffer = new Buffer(4);
|
||||
startBuffer.writeUInt32BE(options.start + 1);
|
||||
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
gte: Buffer.concat([
|
||||
AddressService.PREFIXES.SPENTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
endBuffer
|
||||
]),
|
||||
lte: Buffer.concat([
|
||||
AddressService.PREFIXES.SPENTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
startBuffer
|
||||
]),
|
||||
valueEncoding: 'binary',
|
||||
keyEncoding: 'binary'
|
||||
});
|
||||
} else {
|
||||
var allKey = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer]);
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
gte: Buffer.concat([allKey, AddressService.SPACER_MIN]),
|
||||
lte: Buffer.concat([allKey, AddressService.SPACER_MAX]),
|
||||
valueEncoding: 'binary',
|
||||
keyEncoding: 'binary'
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('data', function(data) {
|
||||
|
||||
var key = self._decodeInputKey(data.key);
|
||||
var value = self._decodeInputValue(data.value);
|
||||
|
||||
var input = {
|
||||
address: addressStr,
|
||||
txid: value.txid.toString('hex'),
|
||||
inputIndex: value.inputIndex,
|
||||
height: key.height,
|
||||
confirmations: self.node.services.db.tip.__height - key.height + 1
|
||||
};
|
||||
|
||||
inputs.push(input);
|
||||
|
||||
});
|
||||
|
||||
var error;
|
||||
|
||||
stream.on('error', function(streamError) {
|
||||
if (streamError) {
|
||||
error = streamError;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', function() {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// TODO include results from mempool
|
||||
|
||||
callback(null, inputs);
|
||||
|
||||
});
|
||||
|
||||
return stream;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} addressStr - The relevant address
|
||||
* @param {Object} options - Additional options for query the outputs
|
||||
* @param {Number} [options.start] - The relevant start block height
|
||||
* @param {Number} [options.end] - The relevant end block height
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getOutputs = function(addressStr, options, callback) {
|
||||
var self = this;
|
||||
$.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.');
|
||||
$.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.');
|
||||
|
||||
var hashBuffer = bitcore.Address(addressStr).hashBuffer;
|
||||
|
||||
var outputs = [];
|
||||
var stream;
|
||||
|
||||
if (options.start && options.end) {
|
||||
|
||||
var startBuffer = new Buffer(4);
|
||||
startBuffer.writeUInt32BE(options.start + 1);
|
||||
var endBuffer = new Buffer(4);
|
||||
endBuffer.writeUInt32BE(options.end);
|
||||
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
gte: Buffer.concat([
|
||||
AddressService.PREFIXES.OUTPUTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
endBuffer
|
||||
]),
|
||||
lte: Buffer.concat([
|
||||
AddressService.PREFIXES.OUTPUTS,
|
||||
hashBuffer,
|
||||
AddressService.SPACER_MIN,
|
||||
startBuffer
|
||||
]),
|
||||
valueEncoding: 'binary',
|
||||
keyEncoding: 'binary'
|
||||
});
|
||||
} else {
|
||||
var allKey = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer]);
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
gte: Buffer.concat([allKey, AddressService.SPACER_MIN]),
|
||||
lte: Buffer.concat([allKey, AddressService.SPACER_MAX]),
|
||||
valueEncoding: 'binary',
|
||||
keyEncoding: 'binary'
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('data', function(data) {
|
||||
|
||||
var key = self._decodeOutputKey(data.key);
|
||||
var value = self._decodeOutputValue(data.value);
|
||||
|
||||
var output = {
|
||||
address: addressStr,
|
||||
txid: key.txid.toString('hex'),
|
||||
outputIndex: key.outputIndex,
|
||||
height: key.height,
|
||||
satoshis: value.satoshis,
|
||||
script: value.scriptBuffer.toString('hex'),
|
||||
confirmations: self.node.services.db.tip.__height - key.height + 1
|
||||
};
|
||||
|
||||
outputs.push(output);
|
||||
|
||||
});
|
||||
|
||||
var error;
|
||||
|
||||
stream.on('error', function(streamError) {
|
||||
if (streamError) {
|
||||
error = streamError;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', function() {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(options.queryMempool) {
|
||||
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
|
||||
}
|
||||
callback(null, outputs);
|
||||
});
|
||||
|
||||
return stream;
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
if(!Array.isArray(addresses)) {
|
||||
addresses = [addresses];
|
||||
}
|
||||
|
||||
var utxos = [];
|
||||
|
||||
async.eachSeries(addresses, function(address, next) {
|
||||
self.getUnspentOutputsForAddress(address, queryMempool, function(err, unspents) {
|
||||
if(err && err instanceof errors.NoOutputs) {
|
||||
return next();
|
||||
} else if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
utxos = utxos.concat(unspents);
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
callback(err, utxos);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.getOutputs(address, {queryMempool: queryMempool}, function(err, outputs) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
} else if(!outputs.length) {
|
||||
return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []);
|
||||
}
|
||||
|
||||
var isUnspent = function(output, callback) {
|
||||
self.isUnspent(output, queryMempool, callback);
|
||||
};
|
||||
|
||||
async.filter(outputs, isUnspent, function(results) {
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.isUnspent = function(output, queryMempool, callback) {
|
||||
this.isSpent(output, queryMempool, function(spent) {
|
||||
callback(!spent);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.isSpent = function(output, queryMempool, callback) {
|
||||
var self = this;
|
||||
var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid;
|
||||
|
||||
setImmediate(function() {
|
||||
callback(self.node.services.bitcoind.isSpent(txid, output.outputIndex));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This will give the total count of transactions for a single or several addresses
|
||||
* limited by a range of block heights.
|
||||
* @param {Array} addresses - An array of addresses
|
||||
* @param {Object} options - The options to limit the query
|
||||
* @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height).
|
||||
* @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive).
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the query
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getAddressHistoryCount = function(addresses, options, callback) {
|
||||
var history = new AddressHistory({
|
||||
node: this.node,
|
||||
options: options,
|
||||
addresses: addresses
|
||||
});
|
||||
history.getCount(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will give the history for many addresses limited by a range of block heights (to limit
|
||||
* the database lookup times) and/or paginated to limit the results length.
|
||||
* @param {Array} addresses - An array of addresses
|
||||
* @param {Object} options - The options to limit the query
|
||||
* @param {Number} [options.from] - The pagination "from" index
|
||||
* @param {Number} [options.to] - The pagination "to" index
|
||||
* @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height).
|
||||
* @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive).
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the query
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||
var history = new AddressHistory({
|
||||
node: this.node,
|
||||
options: options,
|
||||
addresses: addresses
|
||||
});
|
||||
history.get(callback);
|
||||
};
|
||||
|
||||
module.exports = AddressService;
|
|
@ -0,0 +1,714 @@
|
|||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var Transaction = require('../../../lib/transaction');
|
||||
var AddressHistory = require('../../../lib/services/address/history');
|
||||
|
||||
describe('Address Service History', function() {
|
||||
|
||||
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
|
||||
|
||||
describe('@constructor', function() {
|
||||
it('will construct a new instance', function() {
|
||||
var node = {};
|
||||
var options = {};
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: node,
|
||||
options: options,
|
||||
addresses: addresses
|
||||
});
|
||||
history.should.be.instanceof(AddressHistory);
|
||||
history.node.should.equal(node);
|
||||
history.options.should.equal(options);
|
||||
history.addresses.should.equal(addresses);
|
||||
history.transactionInfo.should.deep.equal([]);
|
||||
history.combinedArray.should.deep.equal([]);
|
||||
history.detailedArray.should.deep.equal([]);
|
||||
});
|
||||
it('will set addresses an array if only sent a string', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: address
|
||||
});
|
||||
history.addresses.should.deep.equal([address]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCount', function() {
|
||||
it('will complete the async each limit series', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
history.getTransactionInfo = sinon.stub().callsArg(1);
|
||||
history.combineTransactionInfo = sinon.stub();
|
||||
history.get(function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.getTransactionInfo.callCount.should.equal(1);
|
||||
history.combineTransactionInfo.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('handle an error from getTransactionInfo', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
history.get(function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', function() {
|
||||
it('will complete the async each limit series', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.detailedArray = expected;
|
||||
history.combinedArray = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArg(1);
|
||||
history.combineTransactionInfo = sinon.stub();
|
||||
history.sortAndPaginateCombinedArray = sinon.stub();
|
||||
history.getDetailedInfo = sinon.stub().callsArg(1);
|
||||
history.sortTransactionsIntoArray = sinon.stub();
|
||||
history.get(function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.getTransactionInfo.callCount.should.equal(1);
|
||||
history.getDetailedInfo.callCount.should.equal(1);
|
||||
history.combineTransactionInfo.callCount.should.equal(1);
|
||||
history.sortAndPaginateCombinedArray.callCount.should.equal(1);
|
||||
results.should.equal(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('handle an error from getDetailedInfo', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArg(1);
|
||||
history.paginateSortedArray = sinon.stub();
|
||||
history.getDetailedInfo = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
history.get(function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('handle an error from getTransactionInfo', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
history.get(function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getTransactionInfo', function() {
|
||||
it('will handle an error from getInputs', function(done) {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, new Error('test'))
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will handle an error from getOutputs', function(done) {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, new Error('test')),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will call getOutputs and getInputs with the correct options', function() {
|
||||
var startTimestamp = 1438289011844;
|
||||
var endTimestamp = 1438289012412;
|
||||
var expectedArgs = {
|
||||
start: new Date(startTimestamp * 1000),
|
||||
end: new Date(endTimestamp * 1000),
|
||||
queryMempool: true
|
||||
};
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
start: new Date(startTimestamp * 1000),
|
||||
end: new Date(endTimestamp * 1000),
|
||||
queryMempool: true
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.node.services.address.getOutputs.args[0][1].should.deep.equal(expectedArgs);
|
||||
history.node.services.address.getInputs.args[0][1].should.deep.equal(expectedArgs);
|
||||
});
|
||||
});
|
||||
it('will handle empty results from getOutputs and getInputs', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.transactionInfo.length.should.equal(1);
|
||||
history.node.services.address.getOutputs.args[0][0].should.equal(address);
|
||||
});
|
||||
});
|
||||
it('will concatenate outputs and inputs', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, [{}]),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [{}])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.transactionInfo.length.should.equal(3);
|
||||
history.node.services.address.getOutputs.args[0][0].should.equal(address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@sortByHeight', function() {
|
||||
it('will sort latest to oldest using height', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
height: 276328
|
||||
},
|
||||
{
|
||||
height: 273845,
|
||||
},
|
||||
{
|
||||
height: 555655
|
||||
},
|
||||
{
|
||||
height: 325496
|
||||
},
|
||||
{
|
||||
height: 329186
|
||||
},
|
||||
{
|
||||
height: 534195
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].height.should.equal(555655);
|
||||
transactionInfo[1].height.should.equal(534195);
|
||||
transactionInfo[2].height.should.equal(329186);
|
||||
transactionInfo[3].height.should.equal(325496);
|
||||
transactionInfo[4].height.should.equal(276328);
|
||||
transactionInfo[5].height.should.equal(273845);
|
||||
});
|
||||
it('mempool and tip with time in the future', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
timestamp: 1442050424328,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442050424429,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: 15
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].height.should.equal(-1);
|
||||
transactionInfo[0].timestamp.should.equal(1442050424429);
|
||||
transactionInfo[1].height.should.equal(-1);
|
||||
transactionInfo[1].timestamp.should.equal(1442050424328);
|
||||
transactionInfo[2].height.should.equal(15);
|
||||
transactionInfo[3].height.should.equal(14);
|
||||
});
|
||||
it('tip with time in the future and mempool', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
timestamp: 1442050424328,
|
||||
height: -1
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].height.should.equal(-1);
|
||||
transactionInfo[1].height.should.equal(14);
|
||||
});
|
||||
it('many transactions in the mempool', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
timestamp: 1442259670462,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259785114,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259759896,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259692601,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259692601,
|
||||
height: 100
|
||||
},
|
||||
{
|
||||
timestamp: 1442259749463,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259737719,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442259773138,
|
||||
height: -1,
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].timestamp.should.equal(1442259785114);
|
||||
transactionInfo[1].timestamp.should.equal(1442259773138);
|
||||
transactionInfo[2].timestamp.should.equal(1442259759896);
|
||||
transactionInfo[3].timestamp.should.equal(1442259749463);
|
||||
transactionInfo[4].timestamp.should.equal(1442259737719);
|
||||
transactionInfo[5].timestamp.should.equal(1442259692601);
|
||||
transactionInfo[6].timestamp.should.equal(1442259670462);
|
||||
transactionInfo[7].height.should.equal(100);
|
||||
});
|
||||
it('mempool and mempool', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
timestamp: 1442050424328,
|
||||
height: -1
|
||||
},
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: -1,
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].timestamp.should.equal(1442050425439);
|
||||
transactionInfo[1].timestamp.should.equal(1442050424328);
|
||||
});
|
||||
it('mempool and mempool with the same timestamp', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: -1,
|
||||
txid: '1',
|
||||
},
|
||||
{
|
||||
timestamp: 1442050425439,
|
||||
height: -1,
|
||||
txid: '2'
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].txid.should.equal('1');
|
||||
transactionInfo[1].txid.should.equal('2');
|
||||
});
|
||||
it('matching block heights', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
height: 325496,
|
||||
txid: '1',
|
||||
},
|
||||
{
|
||||
height: 325496,
|
||||
txid: '2'
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].txid.should.equal('1');
|
||||
transactionInfo[1].txid.should.equal('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sortAndPaginateCombinedArray', function() {
|
||||
it('from 0 to 2', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 2
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.combinedArray = [
|
||||
{
|
||||
height: 13
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.sortAndPaginateCombinedArray();
|
||||
history.combinedArray.length.should.equal(2);
|
||||
history.combinedArray[0].height.should.equal(14);
|
||||
history.combinedArray[1].height.should.equal(13);
|
||||
});
|
||||
it('from 0 to 4 (exceeds length)', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 4
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.combinedArray = [
|
||||
{
|
||||
height: 13
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.sortAndPaginateCombinedArray();
|
||||
history.combinedArray.length.should.equal(3);
|
||||
history.combinedArray[0].height.should.equal(14);
|
||||
history.combinedArray[1].height.should.equal(13);
|
||||
history.combinedArray[2].height.should.equal(12);
|
||||
});
|
||||
it('from 0 to 1', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 1
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.combinedArray = [
|
||||
{
|
||||
height: 13
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.sortAndPaginateCombinedArray();
|
||||
history.combinedArray.length.should.equal(1);
|
||||
history.combinedArray[0].height.should.equal(14);
|
||||
});
|
||||
it('from 2 to 3', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 2,
|
||||
to: 3
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.combinedArray = [
|
||||
{
|
||||
height: 13
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.sortAndPaginateCombinedArray();
|
||||
history.combinedArray.length.should.equal(1);
|
||||
history.combinedArray[0].height.should.equal(12);
|
||||
});
|
||||
it('from 10 to 20 (out of range)', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 10,
|
||||
to: 20
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.combinedArray = [
|
||||
{
|
||||
height: 13
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.sortAndPaginateCombinedArray();
|
||||
history.combinedArray.length.should.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDetailedInfo', function() {
|
||||
it('will add additional information to existing this.transactions', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub()
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.node.services.db.getTransactionsWithBlockInfo.callCount.should.equal(0);
|
||||
});
|
||||
});
|
||||
it('will handle error from getTransactionFromBlock', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('test')),
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
err.message.should.equal('test');
|
||||
});
|
||||
});
|
||||
it('will handle error from populateInputs', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, {
|
||||
populateInputs: sinon.stub().callsArgWith(2, new Error('test'))
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
err.message.should.equal('test');
|
||||
});
|
||||
});
|
||||
it('will set this.transactions with correct information', function() {
|
||||
// block #314159
|
||||
// txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e
|
||||
// outputIndex 1
|
||||
var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo';
|
||||
var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000';
|
||||
var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000';
|
||||
var transaction = new Transaction();
|
||||
var previousTransaction = new Transaction();
|
||||
previousTransaction.fromString(previousTxString);
|
||||
var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
transaction.fromString(txString);
|
||||
var txid = transaction.hash;
|
||||
transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45';
|
||||
transaction.__height = 314159;
|
||||
transaction.__timestamp = 1407292005;
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
tip: {
|
||||
__height: 314159
|
||||
},
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, transaction),
|
||||
getTransaction: function(prevTxid, queryMempool, callback) {
|
||||
prevTxid.should.equal(previousTransactionTxid);
|
||||
setImmediate(function() {
|
||||
callback(null, previousTransaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
var transactionInfo = {
|
||||
addresses: {},
|
||||
txid: txid,
|
||||
timestamp: 1407292005,
|
||||
satoshis: 48020000,
|
||||
address: txAddress
|
||||
};
|
||||
transactionInfo.addresses[txAddress] = {};
|
||||
transactionInfo.addresses[txAddress].outputIndexes = [1];
|
||||
transactionInfo.addresses[txAddress].inputIndexes = [];
|
||||
history.getDetailedInfo(transactionInfo, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var info = history.detailedArray[0];
|
||||
info.addresses[txAddress].should.deep.equal({
|
||||
outputIndexes: [1],
|
||||
inputIndexes: []
|
||||
});
|
||||
info.satoshis.should.equal(48020000);
|
||||
info.height.should.equal(314159);
|
||||
info.confirmations.should.equal(1);
|
||||
info.timestamp.should.equal(1407292005);
|
||||
info.fees.should.equal(20000);
|
||||
info.tx.should.equal(transaction);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#getConfirmationsDetail', function() {
|
||||
it('the correct confirmations when included in the tip', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
tip: {
|
||||
__height: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
var transaction = {
|
||||
__height: 100
|
||||
};
|
||||
history.getConfirmationsDetail(transaction).should.equal(1);
|
||||
});
|
||||
});
|
||||
describe('#getSatoshisDetail', function() {
|
||||
it('subtract inputIndexes satoshis without outputIndexes', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
var transaction = {
|
||||
inputs: [
|
||||
{
|
||||
output: {
|
||||
satoshis: 10000
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
var txInfo = {
|
||||
addresses: {}
|
||||
};
|
||||
txInfo.addresses[address] = {};
|
||||
txInfo.addresses[address].inputIndexes = [0];
|
||||
history.getSatoshisDetail(transaction, txInfo).should.equal(-10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var bitcorenode = require('../../');
|
||||
var proxyquire = require('proxyquire');
|
||||
var bitcorenode = require('../../../');
|
||||
var AddressService = bitcorenode.services.Address;
|
||||
var blockData = require('../data/livenet-345003.json');
|
||||
var blockData = require('../../data/livenet-345003.json');
|
||||
var bitcore = require('bitcore');
|
||||
var Networks = bitcore.Networks;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var errors = bitcorenode.errors;
|
||||
var levelup = require('levelup');
|
||||
|
||||
var mockdb = {
|
||||
};
|
||||
|
@ -29,7 +29,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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,13 +74,15 @@ describe('Address Service', function() {
|
|||
var am = new AddressService({node: mocknode});
|
||||
am.node.network = Networks.livenet;
|
||||
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
|
||||
var hashHex = bitcore.Address(address).hashBuffer.toString('hex');
|
||||
var messages = {};
|
||||
am.transactionOutputHandler(messages, tx, 0, true);
|
||||
should.exist(messages[address]);
|
||||
var message = messages[address];
|
||||
should.exist(messages[hashHex]);
|
||||
var message = messages[hashHex];
|
||||
message.tx.should.equal(tx);
|
||||
message.outputIndexes.should.deep.equal([0]);
|
||||
message.address.should.equal(address);
|
||||
message.addressInfo.hashBuffer.toString('hex').should.equal(hashHex);
|
||||
message.addressInfo.hashHex.should.equal(hashHex);
|
||||
message.rejected.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
@ -106,52 +108,6 @@ describe('Address Service', function() {
|
|||
var am;
|
||||
var testBlock = bitcore.Block.fromString(blockData);
|
||||
|
||||
var data = [
|
||||
{
|
||||
key: {
|
||||
address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw',
|
||||
timestamp: 1424836934,
|
||||
txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923',
|
||||
outputIndex: 0
|
||||
},
|
||||
value: {
|
||||
satoshis: 2502227470,
|
||||
script: '76a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac',
|
||||
blockHeight: 345003
|
||||
}
|
||||
},
|
||||
{
|
||||
key: {
|
||||
prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9',
|
||||
prevOutputIndex: 32
|
||||
},
|
||||
value: {
|
||||
txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca',
|
||||
inputIndex: 0,
|
||||
timestamp: 1424836934
|
||||
}
|
||||
},
|
||||
{
|
||||
key: {
|
||||
address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm',
|
||||
timestamp: 1424836934,
|
||||
txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d',
|
||||
outputIndex: 1
|
||||
},
|
||||
value: {
|
||||
satoshis: 3100000,
|
||||
script: '76a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac',
|
||||
blockHeight: 345003
|
||||
}
|
||||
}
|
||||
];
|
||||
var key0 = data[0].key;
|
||||
var value0 = data[0].value;
|
||||
var key3 = data[1].key;
|
||||
var value3 = data[1].value;
|
||||
var key64 = data[2].key;
|
||||
var value64 = data[2].value;
|
||||
|
||||
before(function() {
|
||||
am = new AddressService({node: mocknode});
|
||||
am.node.network = Networks.livenet;
|
||||
|
@ -170,17 +126,14 @@ describe('Address Service', function() {
|
|||
should.not.exist(err);
|
||||
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[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
|
||||
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
|
||||
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[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
|
||||
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
|
||||
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(':'));
|
||||
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
|
||||
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -196,14 +149,14 @@ describe('Address Service', function() {
|
|||
should.not.exist(err);
|
||||
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[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
|
||||
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
|
||||
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[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
|
||||
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
|
||||
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(':'));
|
||||
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
|
||||
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -276,16 +229,16 @@ describe('Address Service', function() {
|
|||
it('will emit a transaction if there is a subscriber', function(done) {
|
||||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
am.subscriptions['address/transaction'] = {
|
||||
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter]
|
||||
};
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
am.subscriptions['address/transaction'] = {};
|
||||
am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter];
|
||||
var block = {
|
||||
__height: 0,
|
||||
timestamp: new Date()
|
||||
};
|
||||
var tx = {};
|
||||
emitter.on('address/transaction', function(obj) {
|
||||
obj.address.should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
obj.address.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
obj.tx.should.equal(tx);
|
||||
obj.timestamp.should.equal(block.timestamp);
|
||||
obj.height.should.equal(block.__height);
|
||||
|
@ -293,7 +246,11 @@ describe('Address Service', function() {
|
|||
done();
|
||||
});
|
||||
am.transactionEventHandler({
|
||||
address: '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N',
|
||||
addressInfo: {
|
||||
hashHex: address.hashBuffer.toString('hex'),
|
||||
hashBuffer: address.hashBuffer,
|
||||
addressType: address.type
|
||||
},
|
||||
height: block.__height,
|
||||
timestamp: block.timestamp,
|
||||
outputIndexes: [1],
|
||||
|
@ -306,19 +263,22 @@ describe('Address Service', function() {
|
|||
it('will emit a balance if there is a subscriber', function(done) {
|
||||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
am.subscriptions['address/balance'] = {
|
||||
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter]
|
||||
};
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter];
|
||||
var block = {};
|
||||
var balance = 1000;
|
||||
am.getBalance = sinon.stub().callsArgWith(2, null, balance);
|
||||
emitter.on('address/balance', function(address, bal, b) {
|
||||
address.should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
emitter.on('address/balance', function(a, bal, b) {
|
||||
a.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
bal.should.equal(balance);
|
||||
b.should.equal(block);
|
||||
done();
|
||||
});
|
||||
am.balanceEventHandler(block, '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
am.balanceEventHandler(block, {
|
||||
hashHex: address.hashBuffer.toString('hex'),
|
||||
hashBuffer: address.hashBuffer,
|
||||
addressType: address.type
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -327,34 +287,40 @@ describe('Address Service', function() {
|
|||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
|
||||
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
var name = 'address/transaction';
|
||||
am.subscribe(name, emitter, [address]);
|
||||
am.subscriptions['address/transaction'][address].should.deep.equal([emitter]);
|
||||
am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter]);
|
||||
|
||||
var address2 = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W');
|
||||
am.subscribe(name, emitter, [address2]);
|
||||
am.subscriptions['address/transaction'][address2].should.deep.equal([emitter]);
|
||||
am.subscriptions['address/transaction'][address2.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter]);
|
||||
|
||||
var emitter2 = new EventEmitter();
|
||||
am.subscribe(name, emitter2, [address]);
|
||||
am.subscriptions['address/transaction'][address].should.deep.equal([emitter, emitter2]);
|
||||
am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter, emitter2]);
|
||||
});
|
||||
it('will add an emitter to the subscribers array (balance)', function() {
|
||||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
var name = 'address/balance';
|
||||
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
am.subscribe(name, emitter, [address]);
|
||||
am.subscriptions['address/balance'][address].should.deep.equal([emitter]);
|
||||
am.subscriptions['address/balance'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter]);
|
||||
|
||||
var address2 = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W');
|
||||
am.subscribe(name, emitter, [address2]);
|
||||
am.subscriptions['address/balance'][address2].should.deep.equal([emitter]);
|
||||
am.subscriptions['address/balance'][address2.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter]);
|
||||
|
||||
var emitter2 = new EventEmitter();
|
||||
am.subscribe(name, emitter2, [address]);
|
||||
am.subscriptions['address/balance'][address].should.deep.equal([emitter, emitter2]);
|
||||
am.subscriptions['address/balance'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter, emitter2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -363,35 +329,37 @@ describe('Address Service', function() {
|
|||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
var emitter2 = new EventEmitter();
|
||||
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
|
||||
am.subscriptions['address/transaction'][address] = [emitter, emitter2];
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter, emitter2];
|
||||
var name = 'address/transaction';
|
||||
am.unsubscribe(name, emitter, [address]);
|
||||
am.subscriptions['address/transaction'][address].should.deep.equal([emitter2]);
|
||||
am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter2]);
|
||||
});
|
||||
it('will remove emitter from subscribers array (balance)', function() {
|
||||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
var emitter2 = new EventEmitter();
|
||||
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
|
||||
var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
var name = 'address/balance';
|
||||
am.subscriptions['address/balance'][address] = [emitter, emitter2];
|
||||
am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter, emitter2];
|
||||
am.unsubscribe(name, emitter, [address]);
|
||||
am.subscriptions['address/balance'][address].should.deep.equal([emitter2]);
|
||||
am.subscriptions['address/balance'][address.hashBuffer.toString('hex')]
|
||||
.should.deep.equal([emitter2]);
|
||||
});
|
||||
it('should unsubscribe from all addresses if no addresses are specified', function() {
|
||||
var am = new AddressService({node: mocknode});
|
||||
var emitter = new EventEmitter();
|
||||
var emitter2 = new EventEmitter();
|
||||
am.subscriptions['address/balance'] = {
|
||||
'1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter, emitter2],
|
||||
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2, emitter]
|
||||
};
|
||||
var address1 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W');
|
||||
var hashHex1 = address1.hashBuffer.toString('hex');
|
||||
var address2 = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N');
|
||||
var hashHex2 = address2.hashBuffer.toString('hex');
|
||||
am.subscriptions['address/balance'][hashHex1] = [emitter, emitter2];
|
||||
am.subscriptions['address/balance'][hashHex2] = [emitter2, emitter];
|
||||
am.unsubscribe('address/balance', emitter);
|
||||
am.subscriptions['address/balance'].should.deep.equal({
|
||||
'1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter2],
|
||||
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2]
|
||||
});
|
||||
am.subscriptions['address/balance'][hashHex1].should.deep.equal([emitter2]);
|
||||
am.subscriptions['address/balance'][hashHex2].should.deep.equal([emitter2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -421,9 +389,10 @@ describe('Address Service', function() {
|
|||
|
||||
});
|
||||
|
||||
describe('#getOutputs', function() {
|
||||
describe('#getInputs', function() {
|
||||
var am;
|
||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var hashBuffer = bitcore.Address(address).hashBuffer;
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
|
@ -437,11 +406,170 @@ describe('Address Service', function() {
|
|||
}
|
||||
}
|
||||
};
|
||||
before(function() {
|
||||
am = new AddressService({node: testnode});
|
||||
});
|
||||
|
||||
it('will get inputs for an address and timestamp', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
start: 15,
|
||||
end: 12,
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('000000000c', 'hex')]);
|
||||
ops.gte.toString('hex').should.equal(gte.toString('hex'));
|
||||
var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('0000000010', 'hex')]);
|
||||
ops.lte.toString('hex').should.equal(lte.toString('hex'));
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolInputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getInputs(address, args, function(err, inputs) {
|
||||
should.not.exist(err);
|
||||
inputs.length.should.equal(1);
|
||||
inputs[0].address.should.equal(address);
|
||||
inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||
inputs[0].inputIndex.should.equal(0);
|
||||
inputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
|
||||
value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
it('should get inputs for address', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('00', 'hex')]);
|
||||
ops.gte.toString('hex').should.equal(gte.toString('hex'));
|
||||
var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('ff', 'hex')]);
|
||||
ops.lte.toString('hex').should.equal(lte.toString('hex'));
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolInputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getInputs(address, args, function(err, inputs) {
|
||||
should.not.exist(err);
|
||||
inputs.length.should.equal(1);
|
||||
inputs[0].address.should.equal(address);
|
||||
inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||
inputs[0].inputIndex.should.equal(0);
|
||||
inputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
|
||||
value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
it('should give an error if the readstream has an error', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
am.node.services.db.store = {
|
||||
createReadStream: sinon.stub().returns(testStream)
|
||||
};
|
||||
|
||||
am.getOutputs(address, {}, function(err, outputs) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('readstreamerror');
|
||||
done();
|
||||
});
|
||||
|
||||
testStream.emit('error', new Error('readstreamerror'));
|
||||
setImmediate(function() {
|
||||
testStream.emit('close');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#getOutputs', function() {
|
||||
var am;
|
||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var hashBuffer = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W').hashBuffer;
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var options = {
|
||||
queryMempool: true
|
||||
};
|
||||
|
||||
before(function() {
|
||||
am = new AddressService({node: testnode});
|
||||
});
|
||||
|
||||
it('will get outputs for an address and timestamp', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
start: 15,
|
||||
end: 12,
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
var gte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, new Buffer('000000000c', 'hex')]);
|
||||
ops.gte.toString('hex').should.equal(gte.toString('hex'));
|
||||
var lte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, new Buffer('0000000010', 'hex')]);
|
||||
ops.lte.toString('hex').should.equal(lte.toString('hex'));
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolOutputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getOutputs(address, args, function(err, outputs) {
|
||||
should.not.exist(err);
|
||||
outputs.length.should.equal(1);
|
||||
outputs[0].address.should.equal(address);
|
||||
outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87');
|
||||
outputs[0].outputIndex.should.equal(1);
|
||||
outputs[0].satoshis.should.equal(4527773864);
|
||||
outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
|
||||
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
|
||||
it('should get outputs for an address', function(done) {
|
||||
var readStream1 = new EventEmitter();
|
||||
am.node.services.db.store = {
|
||||
|
@ -452,7 +580,7 @@ describe('Address Service', function() {
|
|||
address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W',
|
||||
txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371',
|
||||
satoshis: 307627737,
|
||||
script: 'OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG',
|
||||
script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac',
|
||||
blockHeight: 352532
|
||||
}
|
||||
];
|
||||
|
@ -460,38 +588,36 @@ describe('Address Service', function() {
|
|||
getMempoolOutputs: sinon.stub().returns(mempoolOutputs)
|
||||
};
|
||||
|
||||
am.getOutputs(address, true, function(err, outputs) {
|
||||
am.getOutputs(address, options, function(err, outputs) {
|
||||
should.not.exist(err);
|
||||
outputs.length.should.equal(3);
|
||||
outputs[0].address.should.equal(address);
|
||||
outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87');
|
||||
outputs[0].outputIndex.should.equal(1);
|
||||
outputs[0].satoshis.should.equal(4527773864);
|
||||
outputs[0].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
|
||||
outputs[0].blockHeight.should.equal(345000);
|
||||
outputs[0].timestamp.should.equal(1424835319000);
|
||||
outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[0].height.should.equal(345000);
|
||||
outputs[1].address.should.equal(address);
|
||||
outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||
outputs[1].outputIndex.should.equal(2);
|
||||
outputs[1].satoshis.should.equal(10000);
|
||||
outputs[1].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
|
||||
outputs[1].blockHeight.should.equal(345004);
|
||||
outputs[1].timestamp.should.equal(1424837300000);
|
||||
outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[1].height.should.equal(345004);
|
||||
outputs[2].address.should.equal(address);
|
||||
outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371');
|
||||
outputs[2].script.should.equal('OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG');
|
||||
outputs[2].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac');
|
||||
outputs[2].blockHeight.should.equal(352532);
|
||||
done();
|
||||
});
|
||||
|
||||
var data1 = {
|
||||
key: ['outs', address, 1424835319000, '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||
value: ['4527773864', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345000'].join(':')
|
||||
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
|
||||
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
|
||||
};
|
||||
|
||||
var data2 = {
|
||||
key: ['outs', address, 1424837300000, '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
|
||||
value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':')
|
||||
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'),
|
||||
value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
|
||||
};
|
||||
|
||||
readStream1.emit('data', data1);
|
||||
|
@ -505,14 +631,14 @@ describe('Address Service', function() {
|
|||
createReadStream: sinon.stub().returns(readStream2)
|
||||
};
|
||||
|
||||
am.getOutputs(address, true, function(err, outputs) {
|
||||
am.getOutputs(address, options, function(err, outputs) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('readstreamerror');
|
||||
done();
|
||||
});
|
||||
|
||||
readStream2.emit('error', new Error('readstreamerror'));
|
||||
process.nextTick(function() {
|
||||
setImmediate(function() {
|
||||
readStream2.emit('close');
|
||||
});
|
||||
});
|
||||
|
@ -731,245 +857,39 @@ describe('Address Service', 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 testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var am = new AddressService({node: testnode});
|
||||
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');
|
||||
describe('#getAddressHistoryCount', function() {
|
||||
it('will call getCount on address history instance', function(done) {
|
||||
function TestAddressHistory(args) {
|
||||
args.node.should.equal(mocknode);
|
||||
args.addresses.should.deep.equal([]);
|
||||
args.options.should.deep.equal({});
|
||||
}
|
||||
TestAddressHistory.prototype.getCount = sinon.stub().callsArg(0);
|
||||
var TestAddressService = proxyquire('../../../lib/services/address', {
|
||||
'./history': TestAddressHistory
|
||||
});
|
||||
var am = new TestAddressService({node: mocknode});
|
||||
am.getAddressHistoryCount([], {}, function(err, history) {
|
||||
TestAddressHistory.prototype.getCount.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAddressHistory', function() {
|
||||
var incoming = [
|
||||
{
|
||||
txid: 'tx1',
|
||||
outputIndex: 0,
|
||||
spentTx: 'tx2',
|
||||
inputIndex: 0,
|
||||
height: 1,
|
||||
timestamp: 1438289011844,
|
||||
satoshis: 5000,
|
||||
getFee: sinon.stub().throws(new Error('inputs not populated')),
|
||||
isCoinbase: sinon.stub().returns(true)
|
||||
},
|
||||
{
|
||||
txid: 'tx3',
|
||||
outputIndex: 1,
|
||||
height: 3,
|
||||
timestamp: 1438289031844,
|
||||
satoshis: 2000,
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
{
|
||||
txid: 'tx4',
|
||||
outputIndex: 2,
|
||||
spentTx: 'tx5',
|
||||
inputIndex: 1,
|
||||
height: 4,
|
||||
timestamp: 1438289041844,
|
||||
satoshis: 3000,
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
];
|
||||
|
||||
var outgoing = [
|
||||
{
|
||||
txid: 'tx2',
|
||||
height: 2,
|
||||
timestamp: 1438289021844,
|
||||
inputs: [
|
||||
{
|
||||
output: {
|
||||
satoshis: 5000
|
||||
}
|
||||
}
|
||||
],
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
{
|
||||
txid: 'tx5',
|
||||
height: 5,
|
||||
timestamp: 1438289051844,
|
||||
inputs: [
|
||||
{},
|
||||
{
|
||||
output: {
|
||||
satoshis: 3000
|
||||
}
|
||||
}
|
||||
],
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
it('will call get on address history instance', function(done) {
|
||||
function TestAddressHistory(args) {
|
||||
args.node.should.equal(mocknode);
|
||||
args.addresses.should.deep.equal([]);
|
||||
args.options.should.deep.equal({});
|
||||
}
|
||||
];
|
||||
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
},
|
||||
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;
|
||||
transaction.getFee = incoming[i].getFee;
|
||||
transaction.isCoinbase = incoming[i].isCoinbase;
|
||||
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;
|
||||
transaction.getFee = outgoing[i].getFee;
|
||||
transaction.isCoinbase = outgoing[i].isCoinbase;
|
||||
return callback(null, transaction);
|
||||
}
|
||||
}
|
||||
callback(new Error('tx ' + txid + ' not found'));
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var am = new AddressService({node: testnode});
|
||||
|
||||
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].tx.hash.should.equal('tx1');
|
||||
history[0].satoshis.should.equal(5000);
|
||||
history[0].height.should.equal(1);
|
||||
history[0].timestamp.should.equal(1438289011844);
|
||||
should.equal(history[0].fees, null);
|
||||
history[1].tx.hash.should.equal('tx2');
|
||||
history[1].satoshis.should.equal(-5000);
|
||||
history[1].height.should.equal(2);
|
||||
history[1].timestamp.should.equal(1438289021844);
|
||||
history[1].fees.should.equal(1000);
|
||||
history[2].tx.hash.should.equal('tx3');
|
||||
history[2].satoshis.should.equal(2000);
|
||||
history[2].height.should.equal(3);
|
||||
history[2].timestamp.should.equal(1438289031844);
|
||||
history[2].fees.should.equal(1000);
|
||||
history[3].tx.hash.should.equal('tx4');
|
||||
history[3].satoshis.should.equal(3000);
|
||||
history[3].height.should.equal(4);
|
||||
history[3].timestamp.should.equal(1438289041844);
|
||||
history[3].fees.should.equal(1000);
|
||||
history[4].tx.hash.should.equal('tx5');
|
||||
history[4].satoshis.should.equal(-3000);
|
||||
history[4].height.should.equal(5);
|
||||
history[4].timestamp.should.equal(1438289051844);
|
||||
history[4].fees.should.equal(1000);
|
||||
done();
|
||||
TestAddressHistory.prototype.get = sinon.stub().callsArg(0);
|
||||
var TestAddressService = proxyquire('../../../lib/services/address', {
|
||||
'./history': TestAddressHistory
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
var am = new TestAddressService({node: mocknode});
|
||||
am.getAddressHistory([], {}, function(err, history) {
|
||||
TestAddressHistory.prototype.get.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue