From 2b243cb0a2c8330de6dc4688a197d44c7a10b869 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 15 Jul 2015 22:42:05 -0300 Subject: [PATCH] dynamic fees --- lib/blockchainexplorers/insight.js | 17 ++++++ lib/server.js | 88 +++++++++++++++++++++++++++--- test/blockchainexplorer.js | 1 + test/integration/server.js | 60 ++++++++++++++++++++ 4 files changed, 159 insertions(+), 7 deletions(-) diff --git a/lib/blockchainexplorers/insight.js b/lib/blockchainexplorers/insight.js index c392b21..9e66230 100644 --- a/lib/blockchainexplorers/insight.js +++ b/lib/blockchainexplorers/insight.js @@ -105,6 +105,23 @@ Insight.prototype.getAddressActivity = function(addresses, cb) { }); }; +Insight.prototype.estimateFee = function(nbBlocks, cb) { + var url = this.url + '/api/utils/estimatefee'; + if (nbBlocks) { + url += '?nbBlocks=' + nbBlocks; + } + + var args = { + method: 'GET', + url: url, + }; + + request(args, function(err, res, body) { + if (err || res.statusCode !== 200) return cb(err || res); + return cb(null, body.feePerKB); + }); +}; + Insight.prototype.initSocket = function() { var socket = io.connect(this.url, { 'reconnection': true, diff --git a/lib/server.js b/lib/server.js index d23f85e..82170da 100644 --- a/lib/server.js +++ b/lib/server.js @@ -605,13 +605,14 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { }; -WalletService.prototype._getBlockchainExplorer = function(provider, network) { +WalletService.prototype._getBlockchainExplorer = function(network) { if (!this.blockchainExplorer) { var opts = {}; if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) { opts = this.blockchainExplorerOpts[network]; } - opts.provider = provider; + // TODO: provider should be configurable + opts.provider = 'insight'; opts.network = network; this.blockchainExplorer = new BlockchainExplorer(opts); } @@ -636,7 +637,7 @@ WalletService.prototype._getUtxos = function(cb) { var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance var networkName = Bitcore.Address(addressStrs[0]).toObject().network; - var bc = self._getBlockchainExplorer('insight', networkName); + var bc = self._getBlockchainExplorer(networkName); bc.getUnspentUtxos(addressStrs, function(err, inutxos) { if (err) { log.error('Could not fetch unspent outputs', err); @@ -765,6 +766,79 @@ WalletService.prototype.getBalance = function(opts, cb) { }); }; +WalletService.prototype._sampleFeeLevels = function(network, points, cb) { + var self = this; + + // TODO: cache blockexplorer data + async.map(points, function(p, next) { + var bc = self._getBlockchainExplorer(network); + bc.estimateFee(p, function(err, result) { + if (err) { + log.error('Error estimating fee', err); + return next(err); + } + return next(null, [p, result.feePerKB * 1e8]); + }); + }, function(err, results) { + if (err) return cb(err); + return cb(null, _.zipObject(results)); + }); +}; + +/** + * Returns fee levels for the current state of the network. + * @param {Object} opts + * @param {string} [opts.network = 'livenet'] - The Bitcoin network to estimate fee levels from. + * @returns {Object} feeLevels - A list of fee levels & associated amount per kB in satoshi. + */ +WalletService.prototype.getFeeLevels = function(opts, cb) { + var self = this; + + opts = opts || {}; + + var network = opts.network || 'livenet'; + if (network != 'livenet' && network != 'testnet') + return cb(new ClientError('Invalid network')); + + var levels = [{ + name: 'emergency', + nbBlocks: 2, + modifier: 1.5, + defaultValue: 50000 + }, { + name: 'priority', + nbBlocks: 2, + modifier: 1, + defaultValue: 20000 + }, { + name: 'normal', + nbBlocks: 4, + modifier: 1, + defaultValue: 10000 + }, { + name: 'economy', + nbBlocks: 4, + modifier: 0.5, + defaultValue: 5000 + }, ]; + + var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks')); + self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) { + var values = _.zipObject(_.map(levels, function(level) { + var feePerKB; + if (err) { + feePerKB = level.defaultValue; + } else { + var sample = feeSamples[level.nbBlocks]; + feePerKB = (sample <= 0) ? level.defaultValue : sample * level.modifier; + } + return [level.name, feePerKB] + })); + + return cb(null, values); + }); +}; + WalletService.prototype._selectTxInputs = function(txp, cb) { var self = this; @@ -1102,7 +1176,7 @@ WalletService.prototype._broadcastTx = function(txp, cb) { } catch (ex) { return cb(ex); } - var bc = this._getBlockchainExplorer('insight', txp.getNetworkName()); + var bc = this._getBlockchainExplorer(txp.getNetworkName()); bc.broadcast(raw, function(err, txid) { if (err) { log.error('Could not broadcast transaction', err); @@ -1114,7 +1188,7 @@ WalletService.prototype._broadcastTx = function(txp, cb) { WalletService.prototype._checkTxInBlockchain = function(txp, cb) { var tx = txp.getBitcoreTx(); - var bc = this._getBlockchainExplorer('insight', txp.getNetworkName()); + var bc = this._getBlockchainExplorer(txp.getNetworkName()); bc.getTransaction(tx.id, function(err, tx) { if (err) { log.error('Could not get transaction info', err); @@ -1502,7 +1576,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { var addressStrs = _.pluck(addresses, 'address'); var networkName = Bitcore.Address(addressStrs[0]).toObject().network; - var bc = self._getBlockchainExplorer('insight', networkName); + var bc = self._getBlockchainExplorer(networkName); async.parallel([ function(next) { @@ -1556,7 +1630,7 @@ WalletService.prototype.scan = function(opts, cb) { }; function checkActivity(addresses, networkName, cb) { - var bc = self._getBlockchainExplorer('insight', networkName); + var bc = self._getBlockchainExplorer(networkName); bc.getAddressActivity(addresses, cb); }; diff --git a/test/blockchainexplorer.js b/test/blockchainexplorer.js index 097dfa0..0b5a803 100644 --- a/test/blockchainexplorer.js +++ b/test/blockchainexplorer.js @@ -19,6 +19,7 @@ describe('Blockchain explorer', function() { exp.should.respondTo('getTransactions'); exp.should.respondTo('getAddressActivity'); exp.should.respondTo('getUnspentUtxos'); + exp.should.respondTo('estimateFee'); exp.should.respondTo('initSocket'); var exp = new BlockchainExplorer({ provider: 'insight', diff --git a/test/integration/server.js b/test/integration/server.js index 5c10e66..491a426 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -198,6 +198,14 @@ helpers.stubHistory = function(txs) { }; }; +helpers.stubFeeLevels = function(levels) { + blockchainExplorer.estimateFee = function(nbBlocks, cb) { + return cb(null, { + feePerKB: levels[nbBlocks] / 1e8 || -1 + }); + }; +}; + helpers.stubAddressActivity = function(activeAddresses) { blockchainExplorer.getAddressActivity = function(addresses, cb) { return cb(null, _.intersection(activeAddresses, addresses).length > 0); @@ -1443,6 +1451,58 @@ describe('Wallet service', function() { }); }); + describe('#getFeeLevels', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should get current fee levels', function(done) { + helpers.stubFeeLevels({ + 2: 40000, + 4: 20000, + 6: 18000, + }); + server.getFeeLevels({}, function(err, fees) { + should.not.exist(err); + fees.emergency.should.equal(60000); + fees.priority.should.equal(40000); + fees.normal.should.equal(20000); + fees.economy.should.equal(10000); + done(); + }); + }); + it('should get default fees if network cannot be accessed', function(done) { + blockchainExplorer.estimateFee = sinon.stub().yields('dummy error'); + server.getFeeLevels({}, function(err, fees) { + should.not.exist(err); + fees.emergency.should.equal(50000); + fees.priority.should.equal(20000); + fees.normal.should.equal(10000); + fees.economy.should.equal(5000); + done(); + }); + }); + it('should get default fees if network cannot estimate (returns -1)', function(done) { + helpers.stubFeeLevels({ + 2: -1, + 4: 18000, + }); + server.getFeeLevels({}, function(err, fees) { + should.not.exist(err); + fees.emergency.should.equal(50000); + fees.priority.should.equal(20000); + fees.normal.should.equal(18000); + fees.economy.should.equal(9000); + done(); + }); + }); + }); + describe('Wallet not complete tests', function() { it('should fail to create address when wallet is not complete', function(done) { var server = new WalletService();