diff --git a/lib/trader.js b/lib/trader.js index e7caa635..ee181f71 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -17,6 +17,7 @@ var Trader = module.exports = function (db) { this.rates = {}; this._tradeQueue = []; this._sessionInfo = {}; + this.rateInfo = null; }; Trader.prototype._findExchange = function (name) { @@ -258,20 +259,51 @@ Trader.prototype.stopPolling = function () { clearInterval(this.rateInterval); }; +// Trade exchange could be in a different currency than Bitcoin Machine. +// For instance, trade exchange could be Bitstamp, denominated in USD, +// while Bitcoin Machine is set to ILS. +// +// We need this function to convert the trade exchange balance into the +// Bitcoin Machine denomination, in the example case: ILS. +// +// The best way to do that with available data is to take the ratio between +// the exchange rates for the Bitcoin Machine and the trade exchange. +Trader.prototype._tradeForexMultiplier = function _tradeForexMultiplier() { + var deviceCurrency = this.config.exchanges.settings.currency; + var tradeCurrency = this.tradeExchange.currency(); + var deviceRate = this._deviceRate(); + var tradeRate = this._tradeRate(); + + var forexMultiplier = deviceRate && tradeRate ? + deviceRate / tradeRate : + null; + + return deviceCurrency === tradeCurrency ? + 1 : + forexMultiplier; +}; + +Trader.prototype._tradeBalanceFunc = function _tradeBalanceFunc(callback) { + if (!this.tradeExchange) return callback(null, null); + var forexMultiplier = this._tradeForexMultiplier(); + if (!forexMultiplier) return callback(new Error('Can\'t compute balance, no tickers yet.')); + this.tradeExchange.balance(function (err, localBalance) { + if (err) return callback(err); + callback(null, localBalance * forexMultiplier); + }); +}; + Trader.prototype.pollBalance = function (callback) { var self = this; logger.debug('collecting balance'); - async.parallel({ - transferBalance: self.transferExchange.balance.bind(self.transferExchange), - tradeBalance: function (next) { - if (!self.tradeExchange) { - return next(null, null); - } + var transferBalanceFunc = this.transferExchange.balance.bind(this.transferExchange); + var tradeBalanceFunc = this._tradeBalanceFunc.bind(this); - self.tradeExchange.balance(next); - } + async.parallel({ + transferBalance: transferBalanceFunc, + tradeBalance: tradeBalanceFunc }, function (err, balance) { if (err) { return callback && callback(err); @@ -281,7 +313,7 @@ Trader.prototype.pollBalance = function (callback) { logger.debug('Balance update:', balance); self.balance = balance; - return callback && callback(null, balance); + return callback && callback(); }); }; @@ -289,7 +321,14 @@ Trader.prototype.pollRate = function (callback) { var self = this; logger.debug('polling for rates...'); - self.tickerExchange.ticker(function(err, resRates) { + var deviceCurrency = this.config.exchanges.settings.currency; + var currencies = [deviceCurrency]; + if (this.tradeExchange) { + var tradeCurrency = this.tradeExchange.currency(); + if (tradeCurrency !== deviceCurrency) currencies.push(tradeCurrency); + } + + self.tickerExchange.ticker(currencies, function(err, resRates) { if (err) { logger.error(err); return callback && callback(err); @@ -297,13 +336,25 @@ Trader.prototype.pollRate = function (callback) { logger.debug('got rates: %j', resRates); self.rateInfo = {rates: resRates, timestamp: new Date()}; + callback && callback(); }); }; -// This is the rate in local currency quote to the user +Trader.prototype._deviceRate = function _deviceRate() { + if (!this.rateInfo) return null; + return this.rateInfo.rates[this.config.exchanges.settings.currency].rate; +}; + +Trader.prototype._tradeRate = function _tradeRate() { + if (!this.tradeExchange || !this.rateInfo) return null; + return this.rateInfo.rates[this.tradeExchange.currency()].rate; +}; + +// This is the rate in local currency to quote to the user Trader.prototype.rate = function () { + if (!this.rateInfo) return null; return { - rate: this.rateInfo.rates[this.config.exchanges.settings.currency], + rate: this._deviceRate(), timestamp: this.rateInfo.timestamp }; }; diff --git a/test/unit/traderFiatBalanceTest.js b/test/unit/traderFiatBalanceTest.js index b913e6e3..12536f26 100644 --- a/test/unit/traderFiatBalanceTest.js +++ b/test/unit/traderFiatBalanceTest.js @@ -1,17 +1,17 @@ +/*global describe, it */ 'use strict'; var assert = require('chai').assert; var Trader = require('../../lib/trader.js'); -var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; -var psqlInterface = new PostgresqlInterface(db); var RATE = 101; var CURRENCY = 'USD'; -var SATOSHI_FACTOR = Math.pow(10, 8); +var SATOSHI_FACTOR = 1e8; var LOW_BALANCE_MARGIN = 1.2; var COMMISSION = 1.1; +var FINGERPRINT = '00:7A:5A:B3:02:F1:44:46:E2:EA:24:D3:A8:29:DE:22:BA:1B:F9:50'; var settings = { currency: CURRENCY, @@ -41,8 +41,67 @@ describe('trader/fiatBalance', function() { tradeBalance: null }; trader.rates[CURRENCY] = { rate: RATE }; - - var balance = trader.fiatBalance(1 * SATOSHI_FACTOR, 100); - assert.equal(balance, (202 / LOW_BALANCE_MARGIN) * COMMISSION); + trader.rateInfo = {rates: {USD: {rate: RATE}}}; + var fiatBalance = trader.fiatBalance(FINGERPRINT); + assert.equal(fiatBalance, (3 * RATE * COMMISSION / LOW_BALANCE_MARGIN)); }); + + it('should calculate balance correctly with transfer and trade exchange', function() { + var trader = new Trader(db); + trader.configure({ + exchanges: { + plugins: { + current: { + transfer: 'blockchain', + ticker: 'bitpay', + trade: 'bitstamp' + }, + settings: { blockchain: {}, bitpay: {}, bitstamp: {} } + }, + settings: settings + } + }); + + // We have 3 bitcoins in transfer, worth 3 * RATE * COMMISSION = 333.3 + // We have 150 USD in trade + trader.balance = { + transferBalance: 3 * SATOSHI_FACTOR, + tradeBalance: 150 + }; + trader.rates[CURRENCY] = { rate: RATE }; + trader.rateInfo = {rates: {USD: {rate: RATE}}}; + var fiatBalance = trader.fiatBalance(FINGERPRINT); + assert.equal(fiatBalance, 150 / LOW_BALANCE_MARGIN); + }); + + it('should calculate balance correctly with transfer and ' + + 'trade exchange with different currencies', function() { + var trader = new Trader(db); + trader.configure({ + exchanges: { + plugins: { + current: { + transfer: 'blockchain', + ticker: 'bitpay', + trade: 'bitstamp' + }, + settings: { blockchain: {}, bitpay: {}, bitstamp: {} } + }, + settings: settings + } + }); + + // We have 6 bitcoins in transfer, worth 6 * RATE * COMMISSION = 666.6 + // We have 150 USD in trade, 1 USD = 4 ILS => 600 ILS in trade + trader.balance = { + transferBalance: 6 * SATOSHI_FACTOR, + tradeBalance: 600 + }; + trader.rates = {USD: {rate: RATE}, ILS: {rate: RATE * 4} }; + trader.rateInfo = {rates: {USD: {rate: RATE}}}; + var fiatBalance = trader.fiatBalance(FINGERPRINT); + assert.equal(fiatBalance, 600 / LOW_BALANCE_MARGIN); + }); + }); + diff --git a/test/unit/traderTickerTest.js b/test/unit/traderTickerTest.js index dca1cf1a..5256a5bf 100644 --- a/test/unit/traderTickerTest.js +++ b/test/unit/traderTickerTest.js @@ -1,8 +1,7 @@ +/*global describe, it */ 'use strict'; var assert = require('chai').assert; -var hock = require('hock'); -var uuid = require('node-uuid').v4; var Trader = require('../../lib/trader.js'); var PostgresqlInterface = require('../../lib/postgresql_interface.js'); @@ -26,7 +25,7 @@ describe('trader/send', function () { } }; - trader.pollBalance(function (err, balance) { + trader.pollBalance(function (err) { assert.notOk(err); assert.equal(trader.balance.transferBalance, 100); assert.ok(trader.balance.timestamp); @@ -36,17 +35,15 @@ describe('trader/send', function () { it('should call `ticker` on the ticker exchange', function (done) { trader.tickerExchange = { - ticker: function (currency, callback) { - assert.equal(currency, CURRENCY); - callback(null, 100); + ticker: function (currencies, callback) { + assert.equal(currencies[0], CURRENCY); + callback(null, {USD: {rate: 100}}); } }; - trader.pollRate(function (err, rate) { - var rate; - + trader.pollRate(function (err) { assert.notOk(err); - rate = trader.rate(CURRENCY); + var rate = trader.rate(CURRENCY); assert.equal(rate.rate, 100); assert.ok(rate.timestamp); done();