diff --git a/lib/protocol/api/balance.js b/lib/protocol/api/balance.js deleted file mode 100644 index 9df3c274..00000000 --- a/lib/protocol/api/balance.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var _transferExchange; -var _tradeExchange; -var _api; -var _config; -var _balance = null; -var _balanceTriggers = []; - -var winston = require('winston'); -var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); - -var async = require('async'); - -exports.init = function(config, api, transferExchange, tradeExchange) { - _api = api; - _config = config; - - _transferExchange = transferExchange; - _tradeExchange = tradeExchange; - - _balanceTriggers = [function (cb) { _transferExchange.balance(cb); }]; - - if (tradeExchange) - _balanceTriggers.push(function(cb) { _tradeExchange.balance(cb); }); - - _pollBalance(); - setInterval(_pollBalance, 60 * 1000); -}; - -exports.balance = function balance() { - return _balance; -}; - -exports.triggerBalance = _pollBalance; - -function _pollBalance() { - logger.info('collecting balance'); - async.parallel(_balanceTriggers, function(err, results) { - if (err) return; - - _balance = { - transferBalance: results[0], - tradeBalance: results.length === 2 ? results[1] : null, - timestamp: Date.now() - }; - logger.info('Balance update:', _balance); - }); -} diff --git a/lib/trader.js b/lib/trader.js new file mode 100644 index 00000000..b28ecf32 --- /dev/null +++ b/lib/trader.js @@ -0,0 +1,179 @@ +'use strict'; + +var path = require('path'); + +var SATOSHI_FACTOR = Math.pow(10, 8); + +var Trader = module.exports = function (db) { + if (!db) { + throw new Error('`db` is required'); + } + + this.db = db; + this.logger = new (winston.Logger)({ + transports: [new (winston.transports.Console)()] + }); +}; + +Trader.prototype._findExchange = function (name) { + var exchange; + + try { + exchange = require('lamassu-' + name); + } catch (err) { + if (!err.message.match(/Cannot find module/)) throw err; + exchange = require(path.join(path.dirname(__dirname), 'exchanges', name)); + } + + return exchange; +}; + +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.configure = function (config) { + if (config.settings.lowBalanceMargin < 1) { + throw new Error('`settings.lowBalanceMargin` has to be >= 1'); + } + + var tickerExchangeCode = config.plugins.current.ticker; + var tickerExchangeConfig = config.plugins.settings[tickerExchangeCode] || {}; + tickerExchangeConfig.currency = config.settings.currency; + this.tickerExchange = exports.findTicker(tickerExchangeCode).factory(tickerExchangeConfig); + + var tradeExchangeCode = config.plugins.current.trade; + if (tradeExchangeCode) { + var tradeExchangeConfig = config.plugins.settings[tradeExchangeCode]; + this.tradeExchange = this._findTrader(tradeExchangeCode).factory(tradeExchangeConfig); + } + + var transferExchangeCode = config.plugins.current.transfer; + var transferExchangeConfig = config.plugins.settings[transferExchangeCode]; + this.transferExchange = this._findWallet(transferExchangeCode).factory(transferExchangeConfig); + + this.config = config; +}; + +/** + * return fiat balance + * + * in input to this function, balance has the following parameters... + * + * balance.transferBalance - in satoshis + * balance.tradeBalance - in USD + * + * Have added conversion here, but this really needs to be thought through, lamassu-bitstamp should perhaps + * return balance in satoshis + */ +Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) { + var rate = this.rate; + var balance = this.balance; + var commission = this.config.exchanges.settings.commission; + + if (!rate || !balance) return 0; + + // The rate is actually our commission times real rate. + rate = commission * rate; + + // `lowBalanceMargin` is our safety net. It's a number > 1, and we divide + // all our balances by it to provide a safety margin. + var lowBalanceMargin = this.config.settings.lowBalanceMargin; + + // `balance.transferBalance` is the balance of our transfer account (the one + // we use to send Bitcoins to clients). `transferSatoshis` is the number + // of satoshis we're expected to send for this transaction. By subtracting + // them, we get `adjustedTransferBalance`, amount of satoshis we'll have + // after the transaction. + var adjustedTransferBalance = balance.transferBalance - transferSatoshis; + + // Since `adjustedTransferBalance` is in Satoshis, we need to turn it into + // Bitcoins and then fiat to learn how much fiat currency we can exchange. + // + // Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ] + // [ $ ] = [ B * $/B ] + // [ $ ] = [ $ ] + var fiatTransferBalance = ((adjustedTransferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin; + + // If this server is also configured to trade received fiat for Bitcoins, + // we also need to calculate if we have enough funds on our trade exchange. + if (balance.tradeBalance === null) return fiatTransferBalance; + var tradeBalance = balance.tradeBalance; + + // We need to secure `tradeFiat` (amount of fiat in this transaction) and + // enough fiat to cover our trading queue (trades aren't executed immediately). + var adjustedFiat = tradeFiat + exports.trade.queueFiatBalance(rate); + + // So we subtract `adjustedFiat` from `tradeBalance` and again, apply + // `lowBalanceMargin`. + var fiatTradeBalance = (tradeBalance - adjustedFiat) / lowBalanceMargin; + + // And we return the smallest number. + return Math.min(fiatTransferBalance, fiatTradeBalance); +}; + +Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { + var self = this; + + self.db.summonTransaction(deviceFingerprint, tx, function (err, isNew, txHash) { + if (err) return cb(err); + + if (isNew) { + return self.transferExchange.sendBitcoins( + tx.toAddress, + tx.satoshis, + self.config.settings.transactionFee, + function(err, txHash) { + if (err) { + self.db.reportTransactionError(tx, err); + return cb(err); + } + + cb(null, txHash); + self.db.completeTransaction(tx, txHash); + self.triggerBalance(); + } + ); + } + + // transaction exists, but txHash might be null, + // in which case ATM should continue polling + cb(null, txHash); + }); +}; + +Trader.prototype.startPolling = function () { + setInterval(this.pollBalance.bind(this), 60 * 1000); +}; + +Trader.prototype.pollBalance = function () { + var self = this; + + self.logger.info('collecting balance'); + + async.parallel({ + transferBalance: self.transferExchange.balance.bind(self.transferExchange), + tradeBalance: function (next) { + if (!self.tradeExchange) { + return next(null, null); + } + + self.tradeExchange.balance(next); + } + }, function (err, balance) { + balance.timestamp = Date.now(); + self.logger.info('Balance update:', balance); + self.balance = balance; + }); +};