'use strict'; var should = require('chai').should(); var sinon = require('sinon'); var stream = require('stream'); var levelup = require('levelup'); var proxyquire = require('proxyquire'); var bitcorenode = require('../../../'); var AddressService = bitcorenode.services.Address; var blockData = require('../../data/livenet-345003.json'); var bitcore = require('bitcore-lib'); var _ = bitcore.deps._; var memdown = require('memdown'); var leveldown = require('leveldown'); var Networks = bitcore.Networks; var EventEmitter = require('events').EventEmitter; var errors = bitcorenode.errors; var Transaction = require('../../../lib/transaction'); var txData = require('../../data/transaction.json'); var index = require('../../../lib'); var log = index.log; var constants = require('../../../lib/services/address/constants'); var encoding = require('../../../lib/services/address/encoding'); var mockdb = { }; var mocknode = { network: Networks.testnet, datadir: 'testdir', db: mockdb, services: { bitcoind: { on: sinon.stub() } } }; describe('Address Service', function() { var txBuf = new Buffer(txData[0], 'hex'); describe('@constructor', function() { it('config to use memdown for mempool index', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.levelupStore.should.equal(memdown); }); it('config to use leveldown for mempool index', function() { var am = new AddressService({ node: mocknode }); am.levelupStore.should.equal(leveldown); }); }); describe('#start', function() { it('will flush existing mempool', function(done) { var leveldownmock = { destroy: sinon.stub().callsArgWith(1, null) }; var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(true) }, 'leveldown': leveldownmock, 'levelup': sinon.stub().callsArgWith(2, null), 'mkdirp': sinon.stub().callsArgWith(1, null) }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function() { leveldownmock.destroy.callCount.should.equal(1); leveldownmock.destroy.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); done(); }); }); it('will mkdirp if directory does not exist', function(done) { var leveldownmock = { destroy: sinon.stub().callsArgWith(1, null) }; var mkdirpmock = sinon.stub().callsArgWith(1, null); var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(false) }, 'leveldown': leveldownmock, 'levelup': sinon.stub().callsArgWith(2, null), 'mkdirp': mkdirpmock }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function() { mkdirpmock.callCount.should.equal(1); mkdirpmock.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); done(); }); }); it('start levelup db for mempool', function(done) { var levelupStub = sinon.stub().callsArg(2); var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(true) }, 'leveldown': { destroy: sinon.stub().callsArgWith(1, null) }, 'levelup': levelupStub, 'mkdirp': sinon.stub().callsArgWith(1, null) }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function() { levelupStub.callCount.should.equal(1); var dbPath1 = levelupStub.args[0][0]; dbPath1.should.equal('testdir/testnet3/bitcore-addressmempool.db'); var options = levelupStub.args[0][1]; options.db.should.equal(memdown); options.keyEncoding.should.equal('binary'); options.valueEncoding.should.equal('binary'); options.fillCache.should.equal(false); done(); }); }); it('handle error from mkdirp', function(done) { var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(false) }, 'leveldown': { destroy: sinon.stub().callsArgWith(1, null) }, 'levelup': sinon.stub().callsArgWith(2, null), 'mkdirp': sinon.stub().callsArgWith(1, new Error('testerror')) }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function(err) { err.message.should.equal('testerror'); done(); }); }); it('handle error from levelup', function(done) { var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(false) }, 'leveldown': { destroy: sinon.stub().callsArgWith(1, null) }, 'levelup': sinon.stub().callsArgWith(2, new Error('leveltesterror')), 'mkdirp': sinon.stub().callsArgWith(1, null) }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function(err) { err.message.should.equal('leveltesterror'); done(); }); }); it('handle error from leveldown.destroy', function(done) { var TestAddressService = proxyquire('../../../lib/services/address', { 'fs': { existsSync: sinon.stub().returns(true) }, 'leveldown': { destroy: sinon.stub().callsArgWith(1, new Error('destroy')) }, 'levelup': sinon.stub().callsArgWith(2, null), 'mkdirp': sinon.stub().callsArgWith(1, null) }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.start(function(err) { err.message.should.equal('destroy'); done(); }); }); }); describe('#stop', function() { it('will close mempool levelup', function(done) { var testnode = { network: Networks.testnet, datadir: 'testdir', db: mockdb, services: { bitcoind: { on: sinon.stub(), removeListener: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.mempoolIndex = {}; am.mempoolIndex.close = sinon.stub().callsArg(0); am.stop(function() { am.mempoolIndex.close.callCount.should.equal(1); am.node.services.bitcoind.removeListener.callCount.should.equal(2); done(); }); }); }); describe('#_setMempoolIndexPath', function() { it('should set the database path', function() { var testnode = { network: Networks.livenet, datadir: process.env.HOME + '/.bitcoin', services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am._setMempoolIndexPath(); am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-addressmempool.db'); }); it('should load the db for testnet', function() { var testnode = { network: Networks.testnet, datadir: process.env.HOME + '/.bitcoin', services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am._setMempoolIndexPath(); am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-addressmempool.db'); }); it('error with unknown network', function() { var testnode = { network: 'unknown', datadir: process.env.HOME + '/.bitcoin', services: { bitcoind: { on: sinon.stub() } } }; (function() { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }).should.throw('Unknown network'); }); it('should load the db with regtest', function() { // Switch to use regtest Networks.remove(Networks.testnet); Networks.add({ name: 'regtest', alias: 'regtest', pubkeyhash: 0x6f, privatekey: 0xef, scripthash: 0xc4, xpubkey: 0x043587cf, xprivkey: 0x04358394, networkMagic: 0xfabfb5da, port: 18444, dnsSeeds: [ ] }); var regtest = Networks.get('regtest'); var testnode = { network: regtest, datadir: process.env.HOME + '/.bitcoin', services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-addressmempool.db'); Networks.remove(regtest); }); }); describe('#getAPIMethods', function() { it('should return the correct methods', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var methods = am.getAPIMethods(); methods.length.should.equal(7); }); }); describe('#getPublishEvents', function() { it('will return an array of publish event objects', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.subscribe = sinon.spy(); am.unsubscribe = sinon.spy(); var events = am.getPublishEvents(); var callCount = 0; function testName(event, name) { event.name.should.equal(name); event.scope.should.equal(am); var emitter = new EventEmitter(); var addresses = []; event.subscribe(emitter, addresses); am.subscribe.callCount.should.equal(callCount + 1); am.subscribe.args[callCount][0].should.equal(name); am.subscribe.args[callCount][1].should.equal(emitter); am.subscribe.args[callCount][2].should.equal(addresses); am.subscribe.thisValues[callCount].should.equal(am); event.unsubscribe(emitter, addresses); am.unsubscribe.callCount.should.equal(callCount + 1); am.unsubscribe.args[callCount][0].should.equal(name); am.unsubscribe.args[callCount][1].should.equal(emitter); am.unsubscribe.args[callCount][2].should.equal(addresses); am.unsubscribe.thisValues[callCount].should.equal(am); callCount++; } events.forEach(function(event) { testName(event, event.name); }); }); }); describe('#transactionOutputHandler', function() { it('create a message for an address', function() { var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); var tx = bitcore.Transaction().fromBuffer(txBuf); var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.node.network = Networks.livenet; var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var addrObj = bitcore.Address(address); var hashHex = addrObj.hashBuffer.toString('hex'); var hashType = addrObj.type; var messages = {}; am.transactionOutputHandler(messages, tx, 0, true); should.exist(messages[hashHex]); var message = messages[hashHex]; message.tx.should.equal(tx); message.outputIndexes.should.deep.equal([0]); message.addressInfo.hashBuffer.toString('hex').should.equal(hashHex); message.addressInfo.addressType.should.equal(hashType); message.addressInfo.hashHex.should.equal(hashHex); message.rejected.should.equal(true); }); }); describe('#transactionHandler', function() { it('will pass outputs to transactionOutputHandler and call transactionEventHandler and balanceEventHandler', function(done) { var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); var am1 = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var message = {}; am1.transactionOutputHandler = function(messages) { messages[address] = message; }; am1.transactionEventHandler = sinon.stub(); am1.balanceEventHandler = sinon.stub(); am1.transactionHandler({ buffer: txBuf }, function(err) { if (err) { throw err; } am1.transactionEventHandler.callCount.should.equal(1); am1.balanceEventHandler.callCount.should.equal(1); done(); }); }); }); describe('#blockHandler', function() { var am; var testBlock = bitcore.Block.fromString(blockData); before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.node.network = Networks.livenet; }); it('should create the correct operations when updating/adding outputs', function(done) { var block = { __height: 345003, header: { timestamp: 1424836934 }, transactions: testBlock.transactions.slice(0, 8) }; am.blockHandler(block, true, function(err, operations) { should.not.exist(err); operations.length.should.equal(151); operations[0].type.should.equal('put'); operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); operations[3].type.should.equal('put'); operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[4].type.should.equal('put'); operations[4].key.toString('hex').should.equal('053d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[4].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[121].type.should.equal('put'); operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); done(); }); }); it('should create the correct operations when removing outputs', function(done) { var block = { __height: 345003, header: { timestamp: 1424836934 }, transactions: testBlock.transactions.slice(0, 8) }; am.blockHandler(block, false, function(err, operations) { should.not.exist(err); operations.length.should.equal(151); operations[0].type.should.equal('del'); operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); operations[3].type.should.equal('del'); operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[121].type.should.equal('del'); operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); done(); }); }); it('should continue if output script is null', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode, }); var block = { __height: 345003, header: { timestamp: 1424836934 }, transactions: [ { id: '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', inputs: [], outputs: [ { script: null, satoshis: 1000, } ], isCoinbase: sinon.stub().returns(false) } ] }; am.blockHandler(block, false, function(err, operations) { should.not.exist(err); operations.length.should.equal(0); done(); }); }); it('will call event handlers', function() { var testBlock = bitcore.Block.fromString(blockData); var db = {}; var testnode = { datadir: 'testdir', db: db, network: Networks.testnet, services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.transactionEventHandler = sinon.spy(); am.balanceEventHandler = sinon.spy(); var block = { __height: 345003, header: { timestamp: 1424836934 }, transactions: testBlock.transactions.slice(0, 8) }; am.blockHandler( block, true, function(err) { if (err) { throw err; } am.transactionEventHandler.callCount.should.equal(11); am.balanceEventHandler.callCount.should.equal(11); } ); }); }); describe('#transactionEventHandler', function() { it('will emit a transaction if there is a subscriber', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); 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.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); obj.tx.should.equal(tx); obj.timestamp.should.equal(block.timestamp); obj.height.should.equal(block.__height); obj.outputIndexes.should.deep.equal([1]); done(); }); am.transactionEventHandler({ addressInfo: { hashHex: address.hashBuffer.toString('hex'), hashBuffer: address.hashBuffer, addressType: address.type }, height: block.__height, timestamp: block.timestamp, outputIndexes: [1], tx: tx }); }); }); describe('#balanceEventHandler', function() { it('will emit a balance if there is a subscriber', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); 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(a, bal, b) { a.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); bal.should.equal(balance); b.should.equal(block); done(); }); am.balanceEventHandler(block, { hashHex: address.hashBuffer.toString('hex'), hashBuffer: address.hashBuffer, addressType: address.type }); }); }); describe('#subscribe', function() { it('will add emitters to the subscribers array (transaction)', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); var name = 'address/transaction'; am.subscribe(name, emitter, [address]); am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] .should.deep.equal([emitter]); var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); am.subscribe(name, emitter, [address2]); 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.hashBuffer.toString('hex')] .should.deep.equal([emitter, emitter2]); }); it('will add an emitter to the subscribers array (balance)', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); var name = 'address/balance'; var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); am.subscribe(name, emitter, [address]); am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] .should.deep.equal([emitter]); var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); am.subscribe(name, emitter, [address2]); 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.hashBuffer.toString('hex')] .should.deep.equal([emitter, emitter2]); }); }); describe('#unsubscribe', function() { it('will remove emitter from subscribers array (transaction)', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); 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.hashBuffer.toString('hex')] .should.deep.equal([emitter2]); }); it('will remove emitter from subscribers array (balance)', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); var name = 'address/balance'; am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter, emitter2]; am.unsubscribe(name, emitter, [address]); 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({ mempoolMemoryIndex: true, node: mocknode }); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); 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'][hashHex1].should.deep.equal([emitter2]); am.subscriptions['address/balance'][hashHex2].should.deep.equal([emitter2]); }); }); describe('#getBalance', function() { it('should sum up the unspent outputs', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); var outputs = [ {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} ]; am.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs); am.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) { should.not.exist(err); balance.should.equal(6000); done(); }); }); it('will handle error from unspent outputs', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); am.getBalance('someaddress', false, function(err) { should.exist(err); err.message.should.equal('error'); done(); }); }); }); describe('#createInputsStream', function() { it('transform stream from buffer into object', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { tip: { __height: 157 } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; addressService.createInputsDBStream = sinon.stub().returns(streamStub); var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createInputsStream(address, {}); testStream.once('data', function(data) { data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); data.hashType.should.equal('pubkeyhash'); data.txid.should.equal('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df72'); data.inputIndex.should.equal(1); data.height.should.equal(157); data.confirmations.should.equal(1); done(); }); streamStub.emit('data', { key: new Buffer('030b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d786cfeae288d74aaf9f51f215f9882e7bd7bc18af7a550683c4d7c6962f6372900000004', 'hex'), value: new Buffer('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df7200000001', 'hex') }); streamStub.emit('end'); }); }); describe('#createInputsDBStream', function() { it('will stream all keys', function() { var streamStub = sinon.stub().returns({}); var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { store: { createReadStream: streamStub } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = {}; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createInputsDBStream(address, options); should.exist(testStream); streamStub.callCount.should.equal(1); var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; // The expected "lt" value should be one value above the start value, due // to the keys having additional data following it and can't be "equal". var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); }); it('will stream keys based on a range of block heights', function() { var streamStub = sinon.stub().returns({}); var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { store: { createReadStream: streamStub } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = { start: 1, end: 0 }; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createInputsDBStream(address, options); should.exist(testStream); streamStub.callCount.should.equal(1); var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; // The expected "lt" value should be one value above the start value, due // to the keys having additional data following it and can't be "equal". var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); }); }); describe('#getInputs', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 } }; var testnode = { network: Networks.livenet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }); it('will add mempool inputs on close', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var db = { store: { createReadStream: sinon.stub().returns(testStream) }, tip: { __height: 10 } }; var testnode = { network: Networks.livenet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var args = { start: 15, end: 12, queryMempool: true }; am._getInputsMempool = sinon.stub().callsArgWith(3, null, { address: address, hashType: 'pubkeyhash', height: -1, confirmations: 0 }); am.getInputs(address, args, function(err, inputs) { should.not.exist(err); inputs.length.should.equal(1); inputs[0].address.should.equal(address); inputs[0].height.should.equal(-1); done(); }); testStream.push(null); }); it('will get inputs for an address and timestamp', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var args = { start: 15, end: 12, queryMempool: true }; var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gt.toString('hex').should.equal(gt.toString('hex')); var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lt.toString('hex').should.equal(lt.toString('hex')); createReadStreamCallCount++; return testStream; } }; am.node.services.bitcoind = { getMempoolInputs: sinon.stub().returns([]) }; am._getInputsMempool = sinon.stub().callsArgWith(3, null, []); 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('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') }; testStream.emit('data', data); testStream.push(null); }); it('should get inputs for address', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var args = { queryMempool: true }; var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('0000000000', 'hex')]); ops.gt.toString('hex').should.equal(gt.toString('hex')); var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('ffffffffff', 'hex')]); ops.lt.toString('hex').should.equal(lt.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('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') }; testStream.emit('data', data); testStream.push(null); }); it('should give an error if the readstream has an error', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; am.node.services.db.store = { createReadStream: sinon.stub().returns(testStream) }; am.getInputs(address, {}, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); }); testStream.emit('error', new Error('readstreamerror')); setImmediate(function() { testStream.push(null); }); }); }); describe('#_getInputsMempool', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 } }; var testnode = { network: Networks.testnet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }); it('it will handle error', function(done) { var testStream = new EventEmitter(); am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); }); testStream.emit('error', new Error('readstreamerror')); setImmediate(function() { testStream.emit('close'); }); }); it('it will parse data', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); var nowTime = new Date().getTime(); am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, inputs) { should.not.exist(err); inputs.length.should.equal(1); var input = inputs[0]; input.address.should.equal(address); input.txid.should.equal(txid); input.hashType.should.equal('pubkeyhash'); input.hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); input.inputIndex.should.equal(5); input.height.should.equal(-1); input.confirmations.should.equal(0); input.timestamp.should.equal(nowTime); done(); }); var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; var inputIndex = 5; var inputIndexBuffer = new Buffer(4); var timestampBuffer = new Buffer(new Array(8)); timestampBuffer.writeDoubleBE(nowTime); inputIndexBuffer.writeUInt32BE(inputIndex); var valueData = Buffer.concat([ new Buffer(txid, 'hex'), inputIndexBuffer, timestampBuffer ]); // Note: key is not used currently testStream.emit('data', { value: valueData }); testStream.emit('close'); }); }); describe('#_getSpentMempool', function() { it('will decode data from the database', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.mempoolIndex = {}; var mempoolValue = Buffer.concat([ new Buffer('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b', 'hex'), new Buffer('00000003', 'hex') ]); am.mempoolIndex.get = sinon.stub().callsArgWith(1, null, mempoolValue); var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); var outputIndex = 1; var outputIndexBuffer = new Buffer('00000001', 'hex'); var expectedKey = Buffer.concat([ new Buffer('03', 'hex'), prevTxIdBuffer, outputIndexBuffer ]).toString('hex'); am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err, value) { if (err) { throw err; } am.mempoolIndex.get.args[0][0].toString('hex').should.equal(expectedKey); value.inputTxId.should.equal('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b'); value.inputIndex.should.equal(3); }); }); it('handle error from levelup', function() { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.mempoolIndex = {}; am.mempoolIndex.get = sinon.stub().callsArgWith(1, new Error('test')); var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); var outputIndex = 1; am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err) { err.message.should.equal('test'); }); }); }); describe('#createOutputsStream', function() { it('transform stream from buffer into object', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { tip: { __height: 157 } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; addressService.createOutputsDBStream = sinon.stub().returns(streamStub); var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createOutputsStream(address, {}); testStream.once('data', function(data) { data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); data.hashType.should.equal('pubkeyhash'); data.txid.should.equal('4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f9'); data.outputIndex.should.equal(2); data.height.should.equal(157); data.satoshis.should.equal(10000); data.script.toString('hex').should.equal('76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'); data.confirmations.should.equal(1); done(); }); streamStub.emit('data', { key: new Buffer('020b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f900000002', 'hex'), value: new Buffer('40c388000000000076a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', 'hex') }); streamStub.emit('end'); }); }); describe('#createOutputsDBStream', function() { it('will stream all keys', function() { var streamStub = sinon.stub().returns({}); var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { store: { createReadStream: streamStub } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = {}; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createOutputsDBStream(address, options); should.exist(testStream); streamStub.callCount.should.equal(1); var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; // The expected "lt" value should be one value above the start value, due // to the keys having additional data following it and can't be "equal". var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); }); it('will stream keys based on a range of block heights', function() { var streamStub = sinon.stub().returns({}); var testnode = { services: { bitcoind: { on: sinon.stub() }, db: { store: { createReadStream: streamStub } } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = { start: 1, end: 0 }; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var testStream = addressService.createOutputsDBStream(address, options); should.exist(testStream); streamStub.callCount.should.equal(1); var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; // The expected "lt" value should be one value above the start value, due // to the keys having additional data following it and can't be "equal". var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); }); }); describe('#getOutputs', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W').hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 } }; var testnode = { network: Networks.livenet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; var options = { queryMempool: true }; before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }); it('will get outputs for an address and timestamp', function(done) { var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var args = { start: 15, end: 12, queryMempool: true }; var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gt.toString('hex').should.equal(gt.toString('hex')); var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lt.toString('hex').should.equal(lt.toString('hex')); createReadStreamCallCount++; return testStream; } }; am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); 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].hashType.should.equal('pubkeyhash'); outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); 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('02038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; testStream.emit('data', data); testStream.push(null); }); it('should get outputs for an address', function(done) { var readStream1 = new stream.Readable(); readStream1._read = function() { /* do nothing */ }; am.node.services.db.store = { createReadStream: sinon.stub().returns(readStream1) }; am._getOutputsMempool = sinon.stub().callsArgWith(3, null, [ { address: address, height: -1, hashType: 'pubkeyhash', confirmations: 0, txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', satoshis: 307627737, script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac', outputIndex: 0 } ]); am.getOutputs(address, options, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(3); outputs[0].address.should.equal(address); outputs[0].hashType.should.equal('pubkeyhash'); 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(345000); outputs[1].address.should.equal(address); outputs[1].hashType.should.equal('pubkeyhash'); outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); outputs[1].outputIndex.should.equal(2); outputs[1].satoshis.should.equal(10000); outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); outputs[1].height.should.equal(345004); outputs[2].address.should.equal(address); outputs[2].hashType.should.equal('pubkeyhash'); outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); outputs[2].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac'); outputs[2].height.should.equal(-1); outputs[2].confirmations.should.equal(0); done(); }); var data1 = { key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; var data2 = { key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'), value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; readStream1.emit('data', data1); readStream1.emit('data', data2); readStream1.push(null); }); it('should give an error if the readstream has an error', function(done) { var readStream2 = new stream.Readable(); readStream2._read = function() { /* do nothing */ }; am.node.services.db.store = { createReadStream: sinon.stub().returns(readStream2) }; am.getOutputs(address, options, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); }); readStream2.emit('error', new Error('readstreamerror')); setImmediate(function() { readStream2.push(null); }); }); it('should print outputs for a p2sh address', function(done) { // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. // See https://github.com/bitpay/bitcore-node/issues/377 var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; var hashBuffer = bitcore.Address(address).hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var args = { start: 15, end: 12, queryMempool: true }; var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gt.toString('hex').should.equal(gt.toString('hex')); var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lt.toString('hex').should.equal(lt.toString('hex')); createReadStreamCallCount++; return testStream; } }; am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); 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].hashType.should.equal('scripthash'); outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); outputs[0].outputIndex.should.equal(1); outputs[0].satoshis.should.equal(4527773864); outputs[0].script.should.equal('a914038a213afdfc551fc658e9a2a58a86e98d69b68787'); outputs[0].height.should.equal(15); done(); }); createReadStreamCallCount.should.equal(1); var data = { // note '68702', '02' meaning p2sh redeemScript, not p2pkh // value is also the p2sh script, not p2pkh key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68702000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('41f0de058a800000a914038a213afdfc551fc658e9a2a58a86e98d69b68787', 'hex') }; testStream.emit('data', data); testStream.push(null); }); it('should not print outputs for a p2pkh address, if the output was sent to a p2sh redeemScript', function(done) { // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. // See https://github.com/bitpay/bitcore-node/issues/377 var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; var hashBuffer = bitcore.Address(address).hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; var testStream = new stream.Readable(); testStream._read = function() { /* do nothing */ }; var args = { start: 15, end: 12, queryMempool: true }; var createReadStreamCallCount = 0; // Verifying that the db query is looking for a redeemScript, *not* a p2pkh am.node.services.db.store = { createReadStream: function(ops) { var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gt.toString('hex').should.equal(gt.toString('hex')); var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lt.toString('hex').should.equal(lt.toString('hex')); createReadStreamCallCount++; return testStream; } }; am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); am.getOutputs(address, args, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(0); done(); }); createReadStreamCallCount.should.equal(1); testStream.push(null); }); }); describe('#_getOutputsMempool', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 } }; var testnode = { network: Networks.testnet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }); it('it will handle error', function(done) { var testStream = new EventEmitter(); am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); }); testStream.emit('error', new Error('readstreamerror')); setImmediate(function() { testStream.emit('close'); }); }); it('it will parse data', function(done) { var testStream = new EventEmitter(); am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { if (err) { throw err; } outputs.length.should.equal(1); var output = outputs[0]; output.address.should.equal(address); output.hashType.should.equal('pubkeyhash'); output.txid.should.equal(txid); output.outputIndex.should.equal(outputIndex); output.height.should.equal(-1); output.satoshis.should.equal(3); output.script.should.equal('ac'); output.timestamp.should.equal(1452696715750); output.confirmations.should.equal(0); done(); }); var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; var txidBuffer = new Buffer(txid, 'hex'); var outputIndex = 3; var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); var keyData = Buffer.concat([ constants.MEMPREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, txidBuffer, outputIndexBuffer ]); var valueData = Buffer.concat([ new Buffer('4008000000000000', 'hex'), new Buffer('427523b78c1e6000', 'hex'), new Buffer('ac', 'hex') ]); // Note: key is not used currently testStream.emit('data', { key: keyData, value: valueData }); setImmediate(function() { testStream.emit('close'); }); }); }); describe('#getUnspentOutputs', function() { it('should concatenate utxos for multiple addresses, even those with none found', function(done) { var addresses = { 'addr1': ['utxo1', 'utxo2'], 'addr2': new errors.NoOutputs(), 'addr3': ['utxo3'] }; var db = {}; var testnode = { network: Networks.testnet, datadir: 'testdir', services: { db: db, bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { return callback(result); } else { return callback(null, result); } }; am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { should.not.exist(err); utxos.should.deep.equal(['utxo1', 'utxo2', 'utxo3']); done(); }); }); it('should give an error if an error occurred', function(done) { var addresses = { 'addr1': ['utxo1', 'utxo2'], 'addr2': new Error('weird error'), 'addr3': ['utxo3'] }; var db = {}; var testnode = { network: Networks.testnet, datadir: 'testdir', db: db, services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { return callback(result); } else { return callback(null, result); } }; am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { should.exist(err); err.message.should.equal('weird error'); done(); }); }); it('should also work for a single address', function(done) { var addresses = { 'addr1': ['utxo1', 'utxo2'], 'addr2': new Error('weird error'), 'addr3': ['utxo3'] }; var db = {}; var testnode = { network: Networks.testnet, datadir: 'testdir', db: db, services: { bitcoind: { on: sinon.stub() } } }; var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { return callback(result); } else { return callback(null, result); } }; am.getUnspentOutputs('addr1', true, function(err, utxos) { should.not.exist(err); utxos.should.deep.equal(['utxo1', 'utxo2']); done(); }); }); }); describe('#getUnspentOutputsForAddress', function() { it('should filter out spent outputs', function(done) { var outputs = [ { satoshis: 1000, spent: false, }, { satoshis: 2000, spent: true }, { satoshis: 3000, spent: false } ]; var i = 0; var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); am.isUnspent = function(output, options, callback) { callback(!outputs[i].spent); i++; }; am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(2); outputs[0].satoshis.should.equal(1000); outputs[1].satoshis.should.equal(3000); done(); }); }); it('should handle an error from getOutputs', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); err.message.should.equal('error'); done(); }); }); it('should handle when there are no outputs', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); am.getOutputs = sinon.stub().callsArgWith(2, null, []); am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); err.should.be.instanceof(errors.NoOutputs); outputs.length.should.equal(0); done(); }); }); }); describe('#isUnspent', function() { var am; before(function() { am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); }); it('should give true when isSpent() gives false', function(done) { am.isSpent = sinon.stub().callsArgWith(2, false); am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { unspent.should.equal(true); done(); }); }); it('should give false when isSpent() gives true', function(done) { am.isSpent = sinon.stub().callsArgWith(2, true); am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {},function(unspent) { unspent.should.equal(false); done(); }); }); it('should give false when isSpent() returns an error', function(done) { am.isSpent = sinon.stub().callsArgWith(2, new Error('error')); am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { unspent.should.equal(false); done(); }); }); }); describe('#isSpent', function() { var db = {}; var testnode = { network: Networks.testnet, datadir: 'testdir', db: db, services: { bitcoind: { on: sinon.stub() } } }; it('should give true if bitcoind.isSpent gives true (with output info)', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var isSpent = sinon.stub().returns(true); am.node.services.bitcoind = { isSpent: isSpent, on: sinon.stub() }; var output = { txid: '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57', outputIndex: 3 }; am.isSpent(output, {}, function(spent) { isSpent.callCount.should.equal(1); isSpent.args[0][0].should.equal(output.txid); isSpent.args[0][1].should.equal(output.outputIndex); spent.should.equal(true); done(); }); }); it('should give true if bitcoind.isSpent gives true (with input)', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var isSpent = sinon.stub().returns(true); am.node.services.bitcoind = { isSpent: isSpent, on: sinon.stub() }; var txid = '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57'; var output = { prevTxId: new Buffer(txid, 'hex'), outputIndex: 4 }; am.isSpent(output, {}, function(spent) { isSpent.callCount.should.equal(1); isSpent.args[0][0].should.equal(txid); isSpent.args[0][1].should.equal(output.outputIndex); spent.should.equal(true); done(); }); }); it('should give true if bitcoind.isSpent is false and mempoolSpentIndex is true', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.node.services.bitcoind = { isSpent: sinon.stub().returns(false), on: sinon.stub() }; var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; var outputIndex = 0; var output = { prevTxId: new Buffer(txid, 'hex'), outputIndex: outputIndex }; var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); var spentKey = Buffer.concat([ new Buffer(txid, 'hex'), outputIndexBuffer ]).toString('binary'); am.mempoolSpentIndex[spentKey] = true; am.isSpent(output, {queryMempool: true}, function(spent) { spent.should.equal(true); done(); }); }); it('should give false if spent in mempool with queryMempool set to false', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.node.services.bitcoind = { isSpent: sinon.stub().returns(false), on: sinon.stub() }; var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; var outputIndex = 0; var output = { prevTxId: new Buffer(txid, 'hex'), outputIndex: outputIndex }; var spentKey = [txid, outputIndex].join('-'); am.mempoolSpentIndex[spentKey] = new Buffer(5); am.isSpent(output, {queryMempool: false}, function(spent) { spent.should.equal(false); done(); }); }); it('default to querying the mempool', function(done) { var am = new AddressService({ mempoolMemoryIndex: true, node: testnode }); am.node.services.bitcoind = { isSpent: sinon.stub().returns(false), on: sinon.stub() }; var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); var outputIndex = 0; var output = { prevTxId: txidBuffer, outputIndex: outputIndex }; var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); var spentKey = Buffer.concat([ txidBuffer, outputIndexBuffer ]).toString('binary'); am.mempoolSpentIndex[spentKey] = true; am.isSpent(output, {}, function(spent) { spent.should.equal(true); done(); }); }); }); describe('#getAddressHistory', function() { 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({}); } TestAddressHistory.prototype.get = sinon.stub().callsArg(0); var TestAddressService = proxyquire('../../../lib/services/address', { './history': TestAddressHistory }); var am = new TestAddressService({ mempoolMemoryIndex: true, node: mocknode }); am.getAddressHistory([], {}, function(err, history) { TestAddressHistory.prototype.get.callCount.should.equal(1); done(); }); }); }); describe('#updateMempoolIndex/#removeMempoolIndex', function() { var am; var tx = Transaction().fromBuffer(txBuf); var clock; beforeEach(function() { am = new AddressService({ mempoolMemoryIndex: true, node: mocknode }); clock = sinon.useFakeTimers(); }); afterEach(function() { clock.restore(); }); it('will update the input and output indexes', function() { am.mempoolIndex = {}; am.mempoolIndex.batch = function(operations, callback) { callback.should.be.a('function'); Object.keys(am.mempoolSpentIndex).length.should.equal(14); Object.keys(am.mempoolAddressIndex).length.should.equal(5); _.values(am.mempoolAddressIndex).should.deep.equal([1,1,12,1,1]); for (var i = 0; i < operations.length; i++) { operations[i].type.should.equal('put'); } var nowTime = new Date().getTime(); var nowTimeBuffer = new Buffer(8); nowTimeBuffer.writeDoubleBE(nowTime); var expectedValue = '45202ffdeb8344af4dec07cddf0478485dc65cc7d08303e45959630c89b51ea200000002' + nowTimeBuffer.toString('hex'); operations[7].value.toString('hex').should.equal(expectedValue); var matches = 0; for (var j = 0; j < operations.length; j++) { var match = Buffer.concat([ constants.MEMPREFIXES.SPENTS, bitcore.Address('1JT7KDYwT9JY9o2vyqcKNSJgTWeKfV3ui8').hashBuffer ]).toString('hex'); if (operations[j].key.slice(0, 21).toString('hex') === match) { matches++; } } matches.should.equal(12); }; am.updateMempoolIndex(tx, true); }); it('will remove the input and output indexes', function() { am.mempoolIndex = {}; am.mempoolIndex.batch = function(operations, callback) { callback.should.be.a('function'); Object.keys(am.mempoolSpentIndex).length.should.equal(0); for (var i = 0; i < operations.length; i++) { operations[i].type.should.equal('del'); } Object.keys(am.mempoolAddressIndex).length.should.equal(0); }; am.updateMempoolIndex(tx, false); }); }); describe('#getAddressSummary', function() { var clock; beforeEach(function() { clock = sinon.useFakeTimers(); sinon.stub(log, 'warn'); }); afterEach(function() { clock.restore(); log.warn.restore(); }); it('will handle error from _getAddressConfirmedSummary', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var options = {}; addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, new Error('test')); addressService.getAddressSummary(address, options, function(err) { should.exist(err); err.message.should.equal('test'); done(); }); }); it('will handle error from _getAddressMempoolSummary', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var options = {}; addressService._getAddressConfirmedSummary = sinon.stub().callsArg(2); addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(2, new Error('test2')); addressService.getAddressSummary(address, options, function(err) { should.exist(err); err.message.should.equal('test2'); done(); }); }); it('will pass cache and summary between functions correctly', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var options = {}; var cache = {}; var summary = {}; addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); addressService.getAddressSummary(address, options, function(err, sum) { addressService._getAddressConfirmedSummary.callCount.should.equal(1); addressService._getAddressMempoolSummary.callCount.should.equal(1); addressService._getAddressMempoolSummary.args[0][2].should.equal(cache); addressService._setAndSortTxidsFromAppearanceIds.callCount.should.equal(1); addressService._setAndSortTxidsFromAppearanceIds.args[0][0].should.equal(cache); addressService._transformAddressSummaryFromResult.callCount.should.equal(1); addressService._transformAddressSummaryFromResult.args[0][0].should.equal(cache); sum.should.equal(summary); done(); }); }); it('will log if there is a slow query', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var addressService = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var options = {}; var cache = {}; var summary = {}; addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); addressService.getAddressSummary(address, options, function() { log.warn.callCount.should.equal(1); done(); }); clock.tick(6000); }); }); describe('#_getAddressConfirmedSummary', function() { it('will pass arguments correctly', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var options = {}; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = {}; as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, result); as._getAddressConfirmedSummary(address, options, function(err) { if (err) { return done(err); } var expectedResult = { appearanceIds: {}, totalReceived: 0, balance: 0, unconfirmedAppearanceIds: {}, unconfirmedBalance: 0 }; as._getAddressConfirmedInputsSummary.args[0][0].should.equal(address); as._getAddressConfirmedInputsSummary.args[0][1].should.deep.equal(expectedResult); as._getAddressConfirmedInputsSummary.args[0][2].should.deep.equal(options); as._getAddressConfirmedOutputsSummary.args[0][0].should.equal(address); as._getAddressConfirmedOutputsSummary.args[0][1].should.deep.equal(result); as._getAddressConfirmedOutputsSummary.args[0][2].should.equal(options); done(); }); }); it('will pass error correctly (inputs)', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var options = {}; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = {}; as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, new Error('test')); as._getAddressConfirmedSummary(address, options, function(err) { should.exist(err); err.message.should.equal('test'); done(); }); }); it('will pass error correctly (outputs)', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var options = {}; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = {}; as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, new Error('test')); as._getAddressConfirmedSummary(address, options, function(err) { should.exist(err); err.message.should.equal('test'); done(); }); }); }); describe('#_getAddressConfirmedInputsSummary', function() { it('will stream inputs and collect txids', function(done) { var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = { appearanceIds: {} }; var options = {}; var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); as.createInputsStream = sinon.stub().returns(streamStub); as._getAddressConfirmedInputsSummary(address, result, options, function(err, result) { if (err) { return done(err); } result.appearanceIds[txid].should.equal(10); done(); }); streamStub.emit('data', { txid: txid, height: 10 }); streamStub.push(null); }); it('handle stream error', function(done) { var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var cache = {}; var options = {}; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); as.createInputsStream = sinon.stub().returns(streamStub); as._getAddressConfirmedInputsSummary(address, cache, options, function(err, cache) { should.exist(err); err.message.should.equal('test'); done(); }); streamStub.emit('error', new Error('test')); streamStub.push(null); }); }); describe('#_getAddressConfirmedOutputsSummary', function() { it('will stream inputs and collect txids', function(done) { var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; var testnode = { services: { bitcoind: { on: sinon.stub(), isSpent: sinon.stub().returns(false) } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = { appearanceIds: {}, unconfirmedAppearanceIds: {}, balance: 0, totalReceived: 0, unconfirmedBalance: 0 }; var options = { queryMempool: true }; var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); as.createOutputsStream = sinon.stub().returns(streamStub); var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(new Buffer(txid, 'hex'), 2); as.mempoolSpentIndex[spentIndexSyncKey] = true; as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { if (err) { return done(err); } result.appearanceIds[txid].should.equal(10); result.balance.should.equal(1000); result.totalReceived.should.equal(1000); result.unconfirmedBalance.should.equal(-1000); done(); }); streamStub.emit('data', { txid: txid, height: 10, outputIndex: 2, satoshis: 1000 }); streamStub.push(null); }); it('handle stream error', function(done) { var streamStub = new stream.Readable(); streamStub._read = function() { /* do nothing */ }; var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = { appearanceIds: {}, unconfirmedAppearanceIds: {}, balance: 0, totalReceived: 0, unconfirmedBalance: 0 }; var options = {}; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); as.createOutputsStream = sinon.stub().returns(streamStub); as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { should.exist(err); err.message.should.equal('test'); done(); }); streamStub.emit('error', new Error('test')); streamStub.push(null); }); }); describe('#_setAndSortTxidsFromAppearanceIds', function() { it('will sort correctly', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var result = { appearanceIds: { '22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be': 200, '1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93': 100, '206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505': 101 }, unconfirmedAppearanceIds: { 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681': 1452898347406, 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693': 1452898331964, 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345': 1452897902377, 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a': 1452897971363, 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9': 1452897923107 } }; as._setAndSortTxidsFromAppearanceIds(result, function(err, result) { if (err) { return done(err); } should.exist(result.txids); result.txids[0].should.equal('1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93'); result.txids[1].should.equal('206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505'); result.txids[2].should.equal('22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be'); result.unconfirmedTxids[0].should.equal('f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345'); result.unconfirmedTxids[1].should.equal('f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9'); result.unconfirmedTxids[2].should.equal('edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a'); result.unconfirmedTxids[3].should.equal('ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693'); result.unconfirmedTxids[4].should.equal('ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681'); done(); }); }); }); describe.only('#_updateAddressIndex', function() { var as; beforeEach(function(){ var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); }); it('should add using 2 keys', function() { _.values(as.mempoolAddressIndex).should.deep.equal([]); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index2', true); as._updateAddressIndex('index2', true); as.mempoolAddressIndex.should.deep.equal({ "index1": 4, "index2": 2 }); }); it('should add/remove using 2 keys', function() { _.values(as.mempoolAddressIndex).should.deep.equal([]); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', true); as._updateAddressIndex('index1', false); as._updateAddressIndex('index2', true); as._updateAddressIndex('index2', true); as._updateAddressIndex('index2', false); as._updateAddressIndex('index2', false); as._updateAddressIndex('index2', false); as.mempoolAddressIndex.should.deep.equal({ "index1": 3 }); }); }); describe('#_getAddressMempoolSummary', function() { it('skip if options not enabled', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var resultBase = { unconfirmedAppearanceIds: {}, unconfirmedBalance: 0 }; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var options = {}; as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { if (err) { return done(err); } Object.keys(result.unconfirmedAppearanceIds).length.should.equal(0); result.unconfirmedBalance.should.equal(0); done(); }); }); it('include all txids and balance from inputs and outputs', function(done) { var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var resultBase = { unconfirmedAppearanceIds: {}, unconfirmedBalance: 0 }; var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var options = { queryMempool: true }; var mempoolInputs = [ { address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', hashType: 'scripthash', txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', inputIndex: 0, timestamp: 1452874536321, height: -1, confirmations: 0 } ]; var mempoolOutputs = [ { address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', hashType: 'scripthash', txid: '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d', outputIndex: 0, height: -1, timestamp: 1452874521466, satoshis: 131368318, script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', confirmations: 0 }, { address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', hashType: 'scripthash', txid: '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', outputIndex: 0, height: -1, timestamp: 1452874521466, satoshis: 131368318, script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', confirmations: 0 } ]; var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( new Buffer(mempoolOutputs[1].txid, 'hex'), 0 ); as.mempoolSpentIndex[spentIndexSyncKey] = true; var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; var addressIndex = encoding.encodeMempoolAddressIndexKey(address.hashBuffer, hashTypeBuffer); as.mempoolAddressIndex[addressIndex] = 1; as._getInputsMempool = sinon.stub().callsArgWith(3, null, mempoolInputs); as._getOutputsMempool = sinon.stub().callsArgWith(3, null, mempoolOutputs); as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { if (err) { return done(err); } var txid1 = '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8'; var txid2 = '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d'; var txid3 = '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247'; result.unconfirmedAppearanceIds[txid1].should.equal(1452874536321); result.unconfirmedAppearanceIds[txid2].should.equal(1452874521466); result.unconfirmedAppearanceIds[txid3].should.equal(1452874521466); result.unconfirmedBalance.should.equal(131368318); done(); }); }); }); describe('#_transformAddressSummaryFromResult', function() { var result = { totalReceived: 1000000, balance: 500000, txids: [ '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92' ], appearanceIds: { 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92': 100000, '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8': 200000 }, unconfirmedAppearanceIds: { '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d': 1452874536321, '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247': 1452874521466 }, unconfirmedTxids: [ '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d' ], unconfirmedBalance: 500000 }; var testnode = { services: { bitcoind: { on: sinon.stub() } }, datadir: 'testdir' }; it('will transform result into summary', function() { var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = {}; var summary = as._transformAddressSummaryFromResult(result, options); summary.totalReceived.should.equal(1000000); summary.totalSpent.should.equal(500000); summary.balance.should.equal(500000); summary.appearances.should.equal(2); summary.unconfirmedAppearances.should.equal(2); summary.unconfirmedBalance.should.equal(500000); summary.txids.length.should.equal(4); }); it('will omit txlist', function() { var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = { noTxList: true }; var summary = as._transformAddressSummaryFromResult(result, options); should.not.exist(summary.txids); }); it('will include full appearance ids', function() { var as = new AddressService({ mempoolMemoryIndex: true, node: testnode }); var options = { fullTxList: true }; var summary = as._transformAddressSummaryFromResult(result, options); should.exist(summary.appearanceIds); should.exist(summary.unconfirmedAppearanceIds); }); }); });