diff --git a/lib/trader.js b/lib/trader.js index 3b27abea..0bed4a6e 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -8,6 +8,32 @@ var SATOSHI_FACTOR = 1e8; // TODO: Define this somewhere more global var SESSION_TIMEOUT = 60 * 60 * 1000; // an hour + +function findExchange(name) { + try { + return require('lamassu-' + name); + + } catch(_) { + throw new Error(name + ' module is not installed. Try running `npm install --save lamassu-' + name + '` first'); + } +}; + +function findTicker (name) { + var exchange = findExchange(name); + return exchange.ticker || exchange; +}; + +function findTrader (name) { + var exchange = findExchange(name); + return exchange.trader || exchange; +}; + +function findWallet (name) { + var exchange = findExchange(name); + return exchange.wallet || exchange; +}; + + var Trader = module.exports = function (db) { if (!db) { throw new Error('`db` is required'); @@ -20,49 +46,18 @@ var Trader = module.exports = function (db) { this.rateInfo = null; }; -Trader.prototype._findExchange = function (name) { - try { - return require('lamassu-' + name); - - } catch(_) { - throw new Error(name + ' module is not installed. Try running `npm install --save lamassu-' + name + '` first'); - } -}; - -Trader.prototype._findTicker = function (name) { - var exchange = Trader.prototype._findExchange(name); - return exchange.ticker || exchange; -}; - -Trader.prototype._findTrader = function (name) { - var exchange = Trader.prototype._findExchange(name); - return exchange.trader || exchange; -}; - -Trader.prototype._findWallet = function (name) { - var exchange = Trader.prototype._findExchange(name); - return exchange.wallet || exchange; -}; - Trader.prototype._consolidateTrades = function () { var queue = this._tradeQueue; - var tradeRec = { - fiat: 0, - satoshis: 0, - currency: this.config.exchanges.settings.currency + // NOTE: value in satoshis stays the same no matter the currency + var consolidatedTrade = { + currency: this.config.exchanges.settings.currency, + satoshis: queue.reduce(function (prev, current) { + return prev + current.satoshis; + }, 0) }; - while (true) { - var lastRec = queue.shift(); - if (!lastRec) { - break; - } - tradeRec.fiat += lastRec.fiat; - tradeRec.satoshis += lastRec.satoshis; - tradeRec.currency = lastRec.currency; - } - return tradeRec; + return consolidatedTrade; }; Trader.prototype._purchase = function (trade, cb) { @@ -81,20 +76,25 @@ Trader.prototype.configure = function (config) { throw new Error('`settings.lowBalanceMargin` has to be >= 1'); } - var tickerExchangeCode = config.exchanges.plugins.current.ticker; - var tickerExchangeConfig = config.exchanges.plugins.settings[tickerExchangeCode] || {}; - tickerExchangeConfig.currency = config.exchanges.settings.currency; - this.tickerExchange = this._findTicker(tickerExchangeCode).factory(tickerExchangeConfig); + var plugins = config.exchanges.plugins - var tradeExchangeCode = config.exchanges.plugins.current.trade; - if (tradeExchangeCode) { - var tradeExchangeConfig = config.exchanges.plugins.settings[tradeExchangeCode]; - this.tradeExchange = this._findTrader(tradeExchangeCode).factory(tradeExchangeConfig); + // source of current BTC price (init and configure) + var tickerName = plugins.current.ticker; + var tickerConfig = plugins.settings[tickerName] || {}; + tickerConfig.currency = config.exchanges.settings.currency; + this.tickerExchange = findTicker(tickerName).factory(tickerConfig); + + // Exchange used for trading (init and configure) + var traderName = plugins.current.trade; + if (traderName) { + var tradeConfig = plugins.settings[traderName]; + this.tradeExchange = findTrader(traderName).factory(tradeConfig); } - var transferExchangeCode = config.exchanges.plugins.current.transfer; - var transferExchangeConfig = config.exchanges.plugins.settings[transferExchangeCode]; - this.transferExchange = this._findWallet(transferExchangeCode).factory(transferExchangeConfig); + // Wallet (init and configure) + var walletName = plugins.current.transfer; + var walletConfig = plugins.settings[walletName]; + this.transferExchange = findWallet(walletName).factory(walletConfig); this.config = config; @@ -160,9 +160,7 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { var self = this; self.db.summonTransaction(deviceFingerprint, tx, function (err, txRec) { - if (err) { - return cb(err); - } + if (err) return cb(err); if (!txRec) { self._clearSession(deviceFingerprint); @@ -224,7 +222,10 @@ Trader.prototype.trade = function (rec, deviceFingerprint) { }, SESSION_TIMEOUT) }; } - this._tradeQueue.push({fiat: rec.fiat, satoshis: rec.satoshis, currency: rec.currency}); + this._tradeQueue.push({ + satoshis: rec.satoshis, + currency: rec.currency + }); }; Trader.prototype.executeTrades = function () { @@ -235,19 +236,12 @@ Trader.prototype.executeTrades = function () { var trade = this._consolidateTrades(); logger.debug('consolidated: ', JSON.stringify(trade)); - if (trade.fiat === 0) { + if (trade.satoshis === 0) { logger.debug('rejecting 0 trade'); return; } - if (trade.fiat < this.config.exchanges.settings.minimumTradeFiat) { - // throw it back in the water - logger.debug('reject fiat too small'); - this._tradeQueue.unshift(trade); - return; - } - - logger.debug('making a trade: %d', trade.satoshis / Math.pow(10, 8)); + logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR); this._purchase(trade, function (err) { if (err) logger.error(err); }); @@ -286,16 +280,14 @@ Trader.prototype.stopPolling = function () { Trader.prototype._tradeForexMultiplier = function _tradeForexMultiplier() { var deviceCurrency = this.config.exchanges.settings.currency; var tradeCurrency = this.tradeExchange.currency(); + if (deviceCurrency === tradeCurrency) + return 1; + var deviceRate = this._deviceRate(); var tradeRate = this._tradeRate(); - - var forexMultiplier = deviceRate && tradeRate ? + return deviceRate && tradeRate ? deviceRate / tradeRate : null; - - return deviceCurrency === tradeCurrency ? - 1 : - forexMultiplier; }; Trader.prototype._tradeBalanceFunc = function _tradeBalanceFunc(callback) { diff --git a/test/api/sendTest.js b/test/api/sendTest.js deleted file mode 100644 index 622e3d15..00000000 --- a/test/api/sendTest.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -'use strict'; - -var async = require('async'); -var hock = require('hock'); -var createServer = require('../helpers/create-https-server.js'); -var assert = require('chai').assert; - -var LamassuConfig = require('lamassu-config'); -var con = 'psql://lamassu:lamassu@localhost/lamassu'; -var config = new LamassuConfig(con); - -var fnTable = {}; - -var app = { - get: function(route, fn) { - fnTable[route] = fn; - }, - post: function(route, fn) { - fnTable[route] = fn; - } -}; - -var cfg; -var port; - -var blockchainMock = hock.createHock(); - -// blockchain info -var guid = '3acf1633-db4d-44a9-9013-b13e85405404'; -var pwd = 'baz'; -var bitAddr = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; - - -describe('send test', function() { - - beforeEach(function(done) { - - async.parallel({ - blockchain: async.apply(createServer, blockchainMock.handler), - config: function(cb) { - config.load(cb); - } - }, function(err, results) { - assert.isNull(err); - - cfg = results.config; - port = results.blockchain.address().port; - - cfg.exchanges.plugins.current.transfer = 'blockchain'; - cfg.exchanges.plugins.settings.blockchain = { - host: 'localhost', - port: results.blockchain.address().port, - rejectUnauthorized: false, - password: pwd, - fromAddress: bitAddr, - guid: guid - }; - - done(); - }); - }); - - it('should send to blockchain', function(done) { - this.timeout(1000000); - - var amount= 100000000; - - var address_reponse = { - 'hash160':'660d4ef3a743e3e696ad990364e555c271ad504b', - 'address': bitAddr, - 'n_tx': 1, - 'n_unredeemed': 1, - 'total_received': 0, - 'total_sent': 0, - 'final_balance': 0, - 'txs': [] - }; - - var payment_response = { - 'message': 'Sent 0.1 BTC to 1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64', - 'tx_hash': 'f322d01ad784e5deeb25464a5781c3b20971c1863679ca506e702e3e33c18e9c', - 'notice': 'Some funds are pending confirmation and cannot be spent yet (Value 0.001 BTC)' - }; - - blockchainMock - .get('/address/1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64?format=json&limit=10&password=baz') - .reply(200, address_reponse) - .post('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/payment?to=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&amount=100000000&from=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&password=baz') - .reply(200, payment_response); - - - var api = require('../../lib/protocol/atm-api'); - api.init(app, cfg); - - var params = { - body: { - address: bitAddr, - satoshis: amount - } - }; - - setTimeout(function() { - fnTable['/send'](params, {json: function(result) { - assert.isNull(result.err); - assert.equal(payment_response.tx_hash, result.results); - done(); - } - }); - }, 2000); - }); -}); diff --git a/test/api/tickerTest.js b/test/api/tickerTest.js deleted file mode 100644 index 93265480..00000000 --- a/test/api/tickerTest.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -'use strict'; - -var hock = require('hock'); -var async = require('async'); -var createServer = require('../helpers/create-https-server.js'); -var assert = require('chai').assert; - -var LamassuConfig = require('lamassu-config'); -var con = 'psql://lamassu:lamassu@localhost/lamassu'; -var config = new LamassuConfig(con); - -var cfg; - -var blockchainMock = hock.createHock(); -var bitpayMock = hock.createHock(); - -var jsonquest = require('jsonquest'); -var express = require('express'); -var app = express(); -var testPort = 4000; - - - -describe('ticker test', function(){ - - beforeEach(function(done) { - - app.listen(testPort); - - async.parallel({ - blockchain: async.apply(createServer, blockchainMock.handler), - bitpay: async.apply(createServer, bitpayMock.handler), - config: config.load.bind(config) - }, function(err, results) { - assert.isNull(err); - - cfg = results.config; - - cfg.exchanges.settings.commission = 1; - - cfg.exchanges.plugins.current.ticker = 'bitpay'; - cfg.exchanges.plugins.current.trade = null; - cfg.exchanges.plugins.settings.bitpay = { - host: 'localhost', - port: results.bitpay.address().port, - rejectUnauthorized: false - }; - - cfg.exchanges.plugins.current.transfer = 'blockchain'; - cfg.exchanges.plugins.settings.blockchain = { - host: 'localhost', - port: results.blockchain.address().port, - rejectUnauthorized: false, - password: 'baz', - fromAddress: '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64', - guid: '3acf1633-db4d-44a9-9013-b13e85405404' - }; - - done(); - }); - }); - - - it('should read ticker data from bitpay', function(done) { - this.timeout(1000000); - - bitpayMock - .get('/api/rates') - .reply(200, [ - { code: 'EUR', rate: 1337 }, - { code: 'USD', rate: 100 } - ]); - - blockchainMock - .get('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/address_balance?address=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&confirmations=0&password=baz') - .reply(200, { balance: 100000000, total_received: 100000000 }) - .get('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/address_balance?address=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&confirmations=1&password=baz') - .reply(200, { balance: 100000000, total_received: 100000000 }); - // That's 1 BTC. - - var api = require('../../lib/protocol/atm-api'); - api.init(app, cfg); - - // let ticker rate fetch finish... - setTimeout(function() { - jsonquest({ - host: 'localhost', - port: testPort, - path: '/poll/USD',//:currency - method: 'GET', - protocol: 'http' - }, function (err, res, body) { - assert.isNull(err); - assert.equal(res.statusCode, 200); - - assert.isNull(body.err); - assert.equal(Number(body.rate) > 0, true); - console.log(100 / cfg.exchanges.settings.lowBalanceMargin, body.fiat); - assert.equal(body.fiat, 100 / cfg.exchanges.settings.lowBalanceMargin); - - done(); - }); - }, 2000); - }); -}); diff --git a/test/api/tradeTest.js b/test/api/tradeTest.js deleted file mode 100644 index c307be53..00000000 --- a/test/api/tradeTest.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -'use strict'; - -var assert = require('chai').assert; -var hock = require('hock'); - -var LamassuConfig = require('lamassu-config'); -var con = 'psql://lamassu:lamassu@localhost/lamassu'; -var config = new LamassuConfig(con); - -var fnTable = {}; -var app = { get: function(route, fn) { - fnTable[route] = fn; - }, - post: function(route, fn) { - fnTable[route] = fn; - } - }; -var cfg; - -var bitstampMock = hock.createHock(); - -/** - * the tests - */ -describe('trade test', function(){ - - beforeEach(function(done) { - config.load(function(err, result) { - assert.isNull(err); - cfg = result; - done(); - }); - }); - - - - it('should execute a trade against bitstamp', function(done) { - this.timeout(1000000); - - cfg.exchanges.plugins.trade = 'bitstamp'; - var api = require('../../lib/protocol/atm-api'); - api.init(app, cfg); - - // schedule two trades this should result in a single consolidated trade hitting the trading system - fnTable['/trade']({body: {fiat: 100, satoshis: 10, currency: 'USD'}}, {json: function(result) { - console.log(result); - }}); - - fnTable['/trade']({body: {fiat: 100, satoshis: 10, currency: 'USD'}}, {json: function(result) { - console.log(result); - }}); - - setTimeout(function() { done(); }, 1000000); - // check results and execute done() - }); -});