diff --git a/lib/routes.js b/lib/routes.js index 4621fd29..0ac54dbe 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -19,19 +19,21 @@ Error.prototype.toJSON = function () { var poll = function(req, res) { var rateRec = _trader.rate(); - var satoshiBalanceRec = _trader.balance; + var balanceRec = _trader.balance; + var fingerprint = req.connection.getPeerCertificate().fingerprint; - // `rateRec` and `satoshiBalanceRec` are both objects, so there's no danger + // `rateRec` and `balanceRec` are both objects, so there's no danger // of misinterpreting rate or balance === 0 as 'Server initializing'. - if (!rateRec || !satoshiBalanceRec) { + if (!rateRec || !balanceRec) { return res.json({err: 'Server initializing'}); } - if (Date.now() - rateRec.timestamp > STALE_TICKER) { + var now = Date.now(); + if (now - rateRec.timestamp > STALE_TICKER) { return res.json({err: 'Stale ticker'}); } - if (Date.now() - rateRec.timestamp > STALE_BALANCE) { + if (now - balanceRec.timestamp > STALE_BALANCE) { return res.json({err: 'Stale balance'}); } @@ -40,16 +42,16 @@ var poll = function(req, res) { res.json({ err: null, rate: rate * _trader.config.exchanges.settings.commission, - fiat: _trader.fiatBalance(0, 0), + fiat: _trader.fiatBalance(fingerprint), locale: _trader.config.brain.locale, txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10) }); }; var trade = function (req, res) { - _trader.trade(req.body.fiat, req.body.satoshis, req.body.currency, function(err) { - res.json({err: err}); - }); + var fingerprint = req.connection.getPeerCertificate().fingerprint; + _trader.trade(req.body, fingerprint); + res.json({err: null}); }; var send = function(req, res) { diff --git a/lib/trader.js b/lib/trader.js index 4be3b5fd..0dc269af 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -6,6 +6,9 @@ var winston = require('winston'); var SATOSHI_FACTOR = Math.pow(10, 8); +// TODO: Define this somewhere more global +var SESSION_TIMEOUT = 60 * 60 * 1000; // an hour + var Trader = module.exports = function (db) { if (!db) { throw new Error('`db` is required'); @@ -18,6 +21,7 @@ var Trader = module.exports = function (db) { }); this._tradeQueue = []; + this._sessionInfo = {}; }; Trader.prototype._findExchange = function (name) { @@ -79,12 +83,13 @@ Trader.prototype._consolidateTrades = function () { return tradeRec; }; -Trader.prototype._purchase = function (trade) { +Trader.prototype._purchase = function (trade, cb) { var self = this; var rate = self.rate(trade.currency); self.tradeExchange.purchase(trade.satoshis, rate.rate, function (err) { - // TODO: don't ignore purchase errors + if (err) return cb(err); self.pollBalance(); + cb(); }); }; @@ -114,65 +119,60 @@ Trader.prototype.configure = function (config) { this.pollRate(); }; -/** - * 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(this.config.exchanges.settings.currency).rate; +// IMPORTANT: This function returns the estimated minimum available balance +// in fiat as of the start of the current user session on the device. User +// session starts when a user presses then Start button and ends when we +// send the bitcoins. +Trader.prototype.fiatBalance = function (deviceFingerprint) { + var rawRate = this.rate(this.config.exchanges.settings.currency).rate; var balance = this.balance; var commission = this.config.exchanges.settings.commission; - if (!rate || !balance) { + if (!rawRate || !balance) { return 0; } // The rate is actually our commission times real rate. - rate = commission * rate; + var rate = commission * rawRate; // `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.exchanges.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; + // we use to send Bitcoins to clients). + var transferBalance = balance.transferBalance; - // Since `adjustedTransferBalance` is in Satoshis, we need to turn it into - // Bitcoins and then fiat to learn how much fiat currency we can exchange. + // Since `transferBalance` 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; + var fiatTransferBalance = ((transferBalance / 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 + this._tradeQueueFiatBalance(rate); - - // So we subtract `adjustedFiat` from `tradeBalance` and again, apply - // `lowBalanceMargin`. - var fiatTradeBalance = (tradeBalance - adjustedFiat) / lowBalanceMargin; + // We're reporting balance as of the start of the user session. + var sessionInfo = this._sessionInfo[deviceFingerprint]; + var sessionBalance = sessionInfo ? sessionInfo.tradeBalance : tradeBalance; + var fiatTradeBalance = sessionBalance / lowBalanceMargin; // And we return the smallest number. return Math.min(fiatTransferBalance, fiatTradeBalance); }; +Trader.prototype._clearSession = function (deviceFingerprint) { + var sessionInfo = this._sessionInfo[deviceFingerprint]; + if (sessionInfo) { + clearTimeout(sessionInfo.reaper); + delete this._sessionInfo[deviceFingerprint]; + } +}; + Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { var self = this; @@ -182,6 +182,7 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { } if (isNew) { + this._clearSession(deviceFingerprint); return self.transferExchange.sendBitcoins( tx.toAddress, tx.satoshis, @@ -205,9 +206,21 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { }); }; -Trader.prototype.trade = function (fiat, satoshis, currency, callback) { - this._tradeQueue.push({fiat: fiat, satoshis: satoshis, currency: currency}); - callback(null); +Trader.prototype.trade = function (rec, deviceFingerprint) { + // This is where we record starting trade balance at the beginning + // of the user session + var sessionInfo = this._sessionInfo[deviceFingerprint]; + var self = this; + if (!sessionInfo) { + this._sessionInfo[deviceFingerprint] = { + tradeBalance: this.balance.tradeBalance, + timestamp: Date.now(), + reaper: setTimeout(function () { + delete self._sessionInfo[deviceFingerprint]; + }, SESSION_TIMEOUT) + }; + } + this._tradeQueue.push({fiat: rec.fiat, satoshis: rec.satoshis, currency: rec.currency}); }; Trader.prototype.executeTrades = function () { @@ -231,7 +244,10 @@ Trader.prototype.executeTrades = function () { } this.logger.info('making a trade: %d', trade.satoshis / Math.pow(10, 8)); - this._purchase(trade); + var self = this; + this._purchase(trade, function (err) { + if (err) self.logger.error(err); + }); }; Trader.prototype.startPolling = function () {