From f0360a78a05e65492b6fa78408e301db105592b5 Mon Sep 17 00:00:00 2001 From: Chris Kleeschulte Date: Fri, 25 Aug 2017 10:35:36 -0400 Subject: [PATCH] Fixed a problem with address balances. --- lib/services/address/index.js | 41 ++-- lib/services/header/index.js | 3 + test/regtest/address.js | 373 ++++++++++++++++++++++++++++++++++ 3 files changed, 395 insertions(+), 22 deletions(-) create mode 100644 test/regtest/address.js diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 271272a5..b19f73c5 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -22,6 +22,9 @@ var AddressService = function(options) { if (this._network === 'livenet') { this._network = 'main'; } + if (this._network === 'regtest') { + this._network = 'testnet'; + } }; inherits(AddressService, BaseService); @@ -95,7 +98,7 @@ AddressService.prototype.getAddressSummary = function(address, options, callback var criteria = { gte: start, - lt: end + lte: end }; // txid stream @@ -110,8 +113,9 @@ AddressService.prototype.getAddressSummary = function(address, options, callback txStream.on('end', function() { result.balance = Unit.fromSatoshis(result.balanceSat).toBTC(); + assert(result.balance >= 0, 'Balance can\'t be less than zero.'); result.totalReceived = Unit.fromSatoshis(result.totalReceivedSat).toBTC(); - result.totalSent = result.totalSentSat; + result.totalSent = Unit.fromSatoshis(result.totalSentSat).toBTC(); result.unconfirmedBalance = result.unconfirmedBalanceSat; callback(null, result); }); @@ -121,6 +125,7 @@ AddressService.prototype.getAddressSummary = function(address, options, callback txStream._transform = function(chunk, enc, callback) { + // TODO: what if the address in tx more than once? such as an address sending from itself to itself var key = self._encoding.decodeAddressIndexKey(chunk); self._tx.getTransaction(key.txid, options, function(err, tx) { @@ -137,32 +142,22 @@ AddressService.prototype.getAddressSummary = function(address, options, callback return; } - var confirmations = self._header.getBestHeight() - key.height; + var confirmations = self._header.getBestHeight() - key.height + 1; result.transactions.push(tx.txid()); result.txApperances++; // is this an input? if (key.input) { - return self._transaction.getInputValues(key.txid, null, function(err, tx) { + result.balanceSat -= tx.__inputValues[key.index]; + result.totalSentSat += tx.__inputValues[key.index]; - if(err) { - log.error(err); - txStream.emit('error', err); - return; - } + if (confirmations < 1) { + result.unconfirmedBalanceSat -= tx.__inputValues[key.index]; + result.unconfirmedTxApperances++; + } - result.balanceSat -= tx.__inputValues[key.index]; - result.totalSentSat += tx.__inputValues[key.index]; - - if (confirmations < 1) { - result.unconfirmedBalanceSat -= tx.__inputValues[key.index]; - result.unconfirmedTxApperances++; - } - - callback(); - - }); + return callback(); } @@ -470,6 +465,7 @@ AddressService.prototype.onBlock = function(block, callback) { } operations = _.flatten(operations); + callback(null, operations); }; @@ -512,6 +508,7 @@ AddressService.prototype._processInput = function(tx, input, opts) { }; AddressService.prototype._processOutput = function(tx, output, index, opts) { + var address = output.getAddress(); if(!address) { @@ -524,7 +521,7 @@ AddressService.prototype._processOutput = function(tx, output, index, opts) { var timestamp = this._timestamp.getTimestampSync(opts.block.rhash()); assert(timestamp, 'Must have a timestamp in order to process output.'); - var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, opts.outputIndex, 0, timestamp); + var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, index, 0, timestamp); var utxoKey = this._encoding.encodeUtxoIndexKey(address, txid, index); var utxoValue = this._encoding.encodeUtxoIndexValue( @@ -568,7 +565,7 @@ AddressService.prototype._processTransaction = function(tx, opts) { inputOperations = _.flatten(_.compact(inputOperations)); - outputOperations.concat(inputOperations); + outputOperations = outputOperations.concat(inputOperations); return outputOperations; }; diff --git a/lib/services/header/index.js b/lib/services/header/index.js index 385f377a..b759f5e2 100644 --- a/lib/services/header/index.js +++ b/lib/services/header/index.js @@ -235,6 +235,7 @@ HeaderService.prototype.getPublishEvents = function() { }; +// TODO: if blocks come in too rapidly, there can be an erroreous reorg situation HeaderService.prototype._onBlock = function(block) { var self = this; @@ -535,11 +536,13 @@ HeaderService.prototype._handleReorg = function(block, header, callback) { assert(self._lastHeader, 'Expected our reorg block to have a header entry, but it did not.'); headers.set(hash, header); // appends to the end self.emit('reorg', hash, headers, block); + return callback(); } assert(hash, 'To reorg, we need a hash to reorg to.'); self.emit('reorg', hash, headers); callback(); + }); }; diff --git a/test/regtest/address.js b/test/regtest/address.js new file mode 100644 index 00000000..2c86363d --- /dev/null +++ b/test/regtest/address.js @@ -0,0 +1,373 @@ +'use strict'; + +var expect = require('chai').expect; +var spawn = require('child_process').spawn; +var path = require('path'); +var rimraf = require('rimraf'); +var mkdirp = require('mkdirp'); +var fs = require('fs'); +var async = require('async'); +var RPC = require('bitcoind-rpc'); +var http = require('http'); + +var rpc1Address; +var rpc2Address; + +var rpcConfig = { + protocol: 'http', + user: 'local', + pass: 'localtest', + host: '127.0.0.1', + port: 58332, + rejectUnauthorized: false +}; + +var rpc1 = new RPC(rpcConfig); +rpcConfig.port++; +var rpc2 = new RPC(rpcConfig); +var debug = true; +var bitcoreDataDir = '/tmp/bitcore'; +var bitcoinDataDirs = ['/tmp/bitcoin1', '/tmp/bitcoin2']; + +var bitcoin = { + args: { + datadir: null, + listen: 1, + regtest: 1, + server: 1, + rpcuser: 'local', + rpcpassword: 'localtest', + //printtoconsole: 1 + rpcport: 58332, + }, + datadir: null, + exec: 'bitcoind', //if this isn't on your PATH, then provide the absolute path, e.g. /usr/local/bin/bitcoind + processes: [] +}; + +var bitcore = { + configFile: { + file: bitcoreDataDir + '/bitcore-node.json', + conf: { + network: 'regtest', + port: 53001, + datadir: bitcoreDataDir, + services: [ + 'p2p', + 'db', + 'header', + 'block', + 'address', + 'transaction', + 'mempool', + 'web', + 'insight-api', + 'fee', + 'timestamp' + ], + servicesConfig: { + 'p2p': { + 'peers': [ + { 'ip': { 'v4': '127.0.0.1' }, port: 18444 } + ] + }, + 'insight-api': { + 'routePrefix': 'api' + } + } + } + }, + httpOpts: { + protocol: 'http:', + hostname: 'localhost', + port: 53001, + }, + opts: { cwd: bitcoreDataDir }, + datadir: bitcoreDataDir, + exec: path.resolve(__dirname, '../../bin/bitcore-node'), + args: ['start'], + process: null +}; + +var startBitcoind = function(count, callback) { + + var listenCount = 0; + console.log('starting ' + count + ' bitcoind\'s'); + async.timesSeries(count, function(n, next) { + + var datadir = bitcoinDataDirs.shift(); + + bitcoin.datadir = datadir; + bitcoin.args.datadir = datadir; + + if (listenCount++ > 0) { + bitcoin.args.listen = 0; + bitcoin.args.rpcport++; + bitcoin.args.connect = '127.0.0.1'; + } + + rimraf(datadir, function(err) { + + if(err) { + return next(err); + } + + mkdirp(datadir, function(err) { + + if(err) { + return next(err); + } + + var args = bitcoin.args; + var argList = Object.keys(args).map(function(key) { + return '-' + key + '=' + args[key]; + }); + + var bitcoinProcess = spawn(bitcoin.exec, argList, bitcoin.opts); + bitcoin.processes.push(bitcoinProcess); + + bitcoinProcess.stdout.on('data', function(data) { + + if (debug) { + process.stdout.write(data.toString()); + } + + }); + + bitcoinProcess.stderr.on('data', function(data) { + + if (debug) { + process.stderr.write(data.toString()); + } + + }); + + next(); + + }); + + }); + }, function(err) { + + if (err) { + return callback(err); + } + + var pids = bitcoin.processes.map(function(process) { + return process.pid; + }); + + console.log(count + ' bitcoind\'s started at pid(s): ' + pids); + callback(); + }); +}; + + +var shutdownBitcoind = function(callback) { + bitcoin.processes.forEach(function(process) { + process.kill(); + }); + callback(); +}; + +var shutdownBitcore = function(callback) { + if (bitcore.process) { + bitcore.process.kill(); + } + callback(); +}; + +var txid; +var buildInitialChain = function(callback) { + async.waterfall([ + function(next) { + console.log('checking to see if bitcoind\'s are connected to each other.'); + rpc1.getinfo(function(err, res) { + if (err || res.result.connections !== 1) { + next(err || new Error('bitcoind\'s not connected to each other.')); + } + next(); + }); + }, + function(next) { + console.log('generating 101 blocks'); + rpc1.generate(101, next); + }, + function(res, next) { + console.log('getting new address from rpc2'); + rpc2.getNewAddress(function(err, res) { + if (err) { + return next(err); + } + rpc2Address = res.result; + console.log(rpc2Address); + next(null, rpc2Address); + }); + }, + function(addr, next) { + rpc1.sendToAddress(rpc2Address, 25, next); + }, + function(res, next) { + console.log('TXID: ' + res.result); + console.log('generating 6 blocks'); + rpc1.generate(7, next); + }, + function(res, next) { + rpc2.getBalance(function(err, res) { + console.log(res); + next(); + }); + }, + function(next) { + console.log('getting new address from rpc1'); + rpc1.getNewAddress(function(err, res) { + if (err) { + return next(err); + } + rpc1Address = res.result; + next(null, rpc1Address); + }); + }, + function(addr, next) { + rpc2.sendToAddress(rpc1Address, 20, next); + }, + function(res, next) { + txid = res.result; + console.log('sending from rpc2Address TXID: ', res); + console.log('generating 6 blocks'); + rpc2.generate(6, next); + } + ], function(err) { + + if (err) { + return callback(err); + } + rpc1.getInfo(function(err, res) { + console.log(res); + callback(); + }); + }); + +}; + +var startBitcore = function(callback) { + + rimraf(bitcoreDataDir, function(err) { + + if(err) { + return callback(err); + } + + mkdirp(bitcoreDataDir, function(err) { + + if(err) { + return callback(err); + } + + fs.writeFileSync(bitcore.configFile.file, JSON.stringify(bitcore.configFile.conf)); + + var args = bitcore.args; + bitcore.process = spawn(bitcore.exec, args, bitcore.opts); + + bitcore.process.stdout.on('data', function(data) { + + if (debug) { + process.stdout.write(data.toString()); + } + + }); + bitcore.process.stderr.on('data', function(data) { + + if (debug) { + process.stderr.write(data.toString()); + } + + }); + + callback(); + }); + + }); + + +}; + +describe('Address', function() { + + this.timeout(60000); + + before(function(done) { + + async.series([ + function(next) { + startBitcoind(2, next); + }, + function(next) { + setTimeout(function() { + buildInitialChain(next); + }, 8000); + }, + function(next) { + setTimeout(function() { + startBitcore(next); + }, 6000); + } + ], function(err) { + if (err) { + return done(err); + } + setTimeout(done, 2000); + }); + + }); + + after(function(done) { + shutdownBitcoind(function() { + shutdownBitcore(done); + }); + }); + + it('should get address info correctly: /addr/:addr', function(done) { + + + var request = http.request('http://localhost:53001/api/addr/' + rpc2Address, function(res) { + + var error; + if (res.statusCode !== 200 && res.statusCode !== 201) { + if (error) { + return; + } + return done('Error from bitcore-node webserver: ' + res.statusCode); + } + + var resError; + var resData = ''; + + res.on('error', function(e) { + resError = e; + }); + + res.on('data', function(data) { + resData += data; + }); + + res.on('end', function() { + if (error) { + return; + } + var data = JSON.parse(resData); + console.log(data); + expect(data.balance).to.equal(0); + expect(data.totalSent).to.equal(25); + done(); + }); + + }); + + request.write(''); + request.end(); + }); + +}); + +