From f376e96ab2136fb6118bcc41492d224fb7e46a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Thu, 10 Apr 2014 12:09:29 +0200 Subject: [PATCH 01/45] Start the refactor --- lib/app.js | 2 +- lib/protocol/api/api.js | 145 ++++++++++++++++++++++++ lib/protocol/api/balance.js | 49 ++++++++ lib/protocol/api/send.js | 37 ++++++ lib/protocol/api/ticker.js | 33 ++++++ lib/protocol/api/trade.js | 100 ++++++++++++++++ lib/protocol/atm-api.js | 116 +++++++++++++++++++ lib/protocol/db/postgresql_interface.js | 69 +++++++++++ lib/protocol/exchanges/custom_ticker.js | 45 ++++++++ package.json | 14 ++- 10 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 lib/protocol/api/api.js create mode 100644 lib/protocol/api/balance.js create mode 100644 lib/protocol/api/send.js create mode 100644 lib/protocol/api/ticker.js create mode 100644 lib/protocol/api/trade.js create mode 100644 lib/protocol/atm-api.js create mode 100644 lib/protocol/db/postgresql_interface.js create mode 100644 lib/protocol/exchanges/custom_ticker.js diff --git a/lib/app.js b/lib/app.js index 36dbdecb..8aff211e 100755 --- a/lib/app.js +++ b/lib/app.js @@ -25,7 +25,7 @@ var argv = require('optimist').argv; var app = express(); var fs = require('fs'); var LamassuConfig = require('lamassu-config'); -var atm = require('lamassu-atm-protocol'); +var atm = require('./protocol/atm-api.js'); var conString, dbConfig, config; diff --git a/lib/protocol/api/api.js b/lib/protocol/api/api.js new file mode 100644 index 00000000..2ad7bfa6 --- /dev/null +++ b/lib/protocol/api/api.js @@ -0,0 +1,145 @@ +'use strict'; + +require('date-utils'); + +//var async = require('async'); +var winston = require('winston'); +var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); +var path = require('path'); + +var _transferExchange; +var _tickerExchange; +var _tradeExchange; +var _rates = {}; +var _config; +var _commission; +var _config; +var SATOSHI_FACTOR = Math.pow(10, 8); + +exports.ticker = require('./ticker'); +exports.trade = require('./trade'); +exports.send = require('./send'); +exports.balance = require('./balance'); +exports._tradeExchange = null; +exports._transferExchange = null; + +exports.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; +}; + +exports.findTicker = function (name) { + var exchange = exports.findExchange(name); + return exchange.ticker || exchange; +}; + +exports.findTrader = function (name) { + var exchange = exports.findExchange(name); + return exchange.trader || exchange; +}; + +exports.findWallet = function (name) { + var exchange = exports.findExchange(name); + return exchange.wallet || exchange; +}; + +exports.triggerBalance = function triggerBalance() { + this.balance.triggerBalance(); +}; + +exports.init = function(config) { + _config = 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; + _tickerExchange = exports.findTicker(tickerExchangeCode).factory(tickerExchangeConfig); + + var tradeExchangeCode = config.plugins.current.trade; + if (tradeExchangeCode) { + var tradeExchangeConfig = config.plugins.settings[tradeExchangeCode]; + _tradeExchange = exports.findTrader(tradeExchangeCode).factory(tradeExchangeConfig); + } + + var transferExchangeCode = config.plugins.current.transfer; + var transferExchangeConfig = config.plugins.settings[transferExchangeCode]; + _commission = config.settings.commission; + _transferExchange = exports.findWallet(transferExchangeCode).factory(transferExchangeConfig); + + var doRequestTradeExchange = _tradeExchange && tradeExchangeCode !== transferExchangeCode; + + exports._tradeExchange = _tradeExchange; + exports._transferExchange = _transferExchange; + exports.ticker.init(config, exports, _tickerExchange); + exports.trade.init(config, exports, _tradeExchange, exports.ticker); + exports.send.init(config, exports, _transferExchange, exports.ticker); + exports.balance.init(config, exports, _transferExchange, + doRequestTradeExchange ? _tradeExchange : null); +}; + +/** + * 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 + */ +exports.fiatBalance = function(rate, balance, transferSatoshis, tradeFiat, callback) { + 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 = _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); +}; + + diff --git a/lib/protocol/api/balance.js b/lib/protocol/api/balance.js new file mode 100644 index 00000000..9df3c274 --- /dev/null +++ b/lib/protocol/api/balance.js @@ -0,0 +1,49 @@ +'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/protocol/api/send.js b/lib/protocol/api/send.js new file mode 100644 index 00000000..6f3b9672 --- /dev/null +++ b/lib/protocol/api/send.js @@ -0,0 +1,37 @@ +'use strict'; + +var _transferExchange; +var _api; +var _config; +var _conString = process.env.DATABASE_URL || 'postgres://lamassu:lamassu@localhost/lamassu'; +var _db = require('../db/postgresql_interface').factory(_conString); + +exports.init = function(config, api, transferExchange) { + _api = api; + _config = config; + _transferExchange = transferExchange; +}; + +exports.setDomain = function(domain) { + _transferExchange.setDomain(domain); +}; + +exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, cb) { + _db.summonTransaction(deviceFingerprint, tx, function (err, isNew, txHash) { + if (err) return cb(err); + if (isNew) return _transferExchange.sendBitcoins(tx.toAddress, tx.satoshis, + _config.settings.transactionFee, function(err, txHash) { + if (err) { + _db.reportTransactionError(tx, err); + return cb(err); + } + cb(null, txHash); + _db.completeTransaction(tx, txHash); + _api.triggerBalance(); + }); + + // transaction exists, but txHash might be null, + // in which case ATM should continue polling + cb(null, txHash); + }); +}; diff --git a/lib/protocol/api/ticker.js b/lib/protocol/api/ticker.js new file mode 100644 index 00000000..635c6d97 --- /dev/null +++ b/lib/protocol/api/ticker.js @@ -0,0 +1,33 @@ +'use strict'; + +require('date-utils'); +var winston = require('winston'); +var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); + +var _tickerExchange; +var _api; +var _rates = {}; + +var _pollRate = function(currency) { + logger.info('polling for rate...'); + _tickerExchange.ticker(currency, function(err, rate) { + if (err) return; + logger.info('Rate update:', rate); + _rates[currency] = {rate: rate, timestamp: new Date()}; + }); +}; + +exports.init = function(config, api, tickerExchange) { + _api = api; + _tickerExchange = tickerExchange; + + _pollRate(config.settings.currency); + setInterval(function () { + _pollRate(config.settings.currency); + }, 60 * 1000); +}; + +exports.rate = function(currency) { + if (!_rates[currency]) return null; + return _rates[currency]; +}; diff --git a/lib/protocol/api/trade.js b/lib/protocol/api/trade.js new file mode 100644 index 00000000..afd8b784 --- /dev/null +++ b/lib/protocol/api/trade.js @@ -0,0 +1,100 @@ +'use strict'; + +require('date-utils'); +var winston = require('winston'); +var _ = require('underscore'); +var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); + +var _tradeExchange; +var _ticker; +var _tradeQueue = []; +var _api; +var _config; + +var SATOSHI_FACTOR = Math.pow(10, 8); + +var _consolidateTrades = function() { + var queue = _tradeQueue; + var tradeRec = { + fiat: 0, + satoshis: 0, + currency: 'USD' + }; + + while (true) { + var lastRec = queue.shift(); + if (!lastRec) { + break; + } + tradeRec.fiat += lastRec.fiat; + tradeRec.satoshis += lastRec.satoshis; + tradeRec.currency = lastRec.currency; + } + return tradeRec; +}; + + + +/** + * TODO: add error reporting + */ +var _purchase = function(trade) { + _ticker.rate(trade.currency, function(err, rate) { + _tradeExchange.purchase(trade.satoshis, rate, function(err) { + _api.triggerBalance(); + }); + }); +}; + +exports.init = function(config, api, tradeExchange, ticker) { + _config = config; + _api = api; + _tradeExchange = tradeExchange; + _ticker = ticker; + + var interval = setInterval(function() { + exports.executeTrades(); + }, _config.settings.tradeInterval); + interval.unref(); +}; + +exports.trade = function(fiat, satoshis, currency, cb) { + _tradeQueue.push({fiat: fiat, satoshis: satoshis, currency: currency}); + cb(null); +}; + +exports.queueFiatBalance = function(exchangeRate) { + var satoshis = _.reduce(_tradeQueue, function(memo, rec) { + return memo + rec.satoshis; + }, 0); + return (satoshis / SATOSHI_FACTOR) * exchangeRate; +}; + +exports.executeTrades = function() { + if (!_tradeExchange) return; + + logger.info('checking for trades'); + + if (!_config.plugins.current.trade) { + logger.info('NO ENGINE'); + return; + } + + var trade = _consolidateTrades(); + logger.info('consolidated: ' + JSON.stringify(trade)); + + if (trade.fiat === 0) { + logger.info('reject fiat 0'); + return; + } + + if (trade.fiat < _config.settings.minimumTradeFiat) { + // throw it back in the water + logger.info('reject fiat too small'); + _tradeQueue.unshift(trade); + return; + } + + logger.info('making a trade: %d', trade.satoshis / Math.pow(10,8)); + _purchase(trade); +}; diff --git a/lib/protocol/atm-api.js b/lib/protocol/atm-api.js new file mode 100644 index 00000000..e170806f --- /dev/null +++ b/lib/protocol/atm-api.js @@ -0,0 +1,116 @@ +'use strict'; + +var api = exports.api = require('./api/api'); +var _config; +var _lamassuConfig; +var _commission; + +// Make sure these are higher than polling interval +// or there will be a lot of errors +var STALE_TICKER = 180000; +var STALE_BALANCE = 180000; + +Error.prototype.toJSON = function () { + var self = this; + var ret = {}; + Object.getOwnPropertyNames(self).forEach(function (key) { + ret[key] = self[key]; + }); + return ret; +}; + +var poll = function(req, res) { + if (req.device.unpair) { + return res.json({ + unpair: true + }); + } + + var rateRec = api.ticker.rate(req.params.currency); + var satoshiBalanceRec = api.balance.balance(); + + if (rateRec === null || satoshiBalanceRec === null) + return res.json({err: 'Server initializing'}); + if (Date.now() - rateRec.timestamp > STALE_TICKER) + return res.json({err: 'Stale ticker'}); + if (Date.now() - rateRec.timestamp > STALE_BALANCE) + return res.json({err: 'Stale balance'}); + + var rate = rateRec.rate; + + res.json({ + err: null, + rate: rate * _commission, + fiat: api.fiatBalance(rate, satoshiBalanceRec, 0, 0), + currency: req.params.currency, + txLimit: parseInt(_config.exchanges.settings.compliance.maximum.limit, 10) + }); +}; + +// TODO need to add in a UID for this trade +var trade = function(req, res) { + api.trade.trade(req.body.fiat, req.body.satoshis, req.body.currency, function(err) { + res.json({err: err}); + }); +}; + +var send = function(req, res) { + var fingerprint = req.connection.getPeerCertificate().fingerprint; + api.send.sendBitcoins(fingerprint, req.body, function(err, txHash) { + res.json({err: err, txHash: txHash}); + }); +}; + +var configurations = function(req, res) { + res.json({ + err: _config.exchanges && _config.exchanges.settings ? null : new Error('Settings Not Found!'), + results: _config.exchanges.settings + }); +}; + +var pair = function(req, res) { + var token = req.body.token; + var name = req.body.name; + + _lamassuConfig.pair( + token, + req.connection.getPeerCertificate().fingerprint, + name, + function(err) { + if (err) res.json(500, { err: err.message }); + else res.json(200); + } + ); +}; + +exports.init = function(app, config, lamassuConfig, authMiddleware) { + _config = config; + _lamassuConfig = lamassuConfig; + + api.init(_config.exchanges); + + _commission = _config.exchanges.settings.commission; + + exports._tradeExchange = api._tradeExchange; + exports._transferExchange = api._transferExchange; + + app.get('/poll/:currency', authMiddleware, poll); + app.get('/config', authMiddleware, configurations); + app.post('/trade', authMiddleware, trade); + app.post('/send', authMiddleware, send); + app.post('/pair', pair); + + lamassuConfig.on('configUpdate', function () { + _lamassuConfig.load(function(err, config) { + if (err) { + return console.error('Error while reloading config'); + } + + _config = config; + api.init(_config.exchanges); + console.log('Config reloaded'); + }); + }); + + return app; +}; diff --git a/lib/protocol/db/postgresql_interface.js b/lib/protocol/db/postgresql_interface.js new file mode 100644 index 00000000..10e1b253 --- /dev/null +++ b/lib/protocol/db/postgresql_interface.js @@ -0,0 +1,69 @@ +'use strict'; + +var pg = require('pg'); +var PG_ERRORS = { + 23505: 'uniqueViolation' +}; + +var PostgresqlInterface = function (conString) { + if (!conString) { + throw new Error('Postgres connection string is required'); + } + + this.client = new pg.Client(conString); + + // TODO better logging + this.client.on('error', function (err) { console.log(err); }); + + this.client.connect(); +}; +PostgresqlInterface.factory = function factory(conString) { return new PostgresqlInterface(conString); }; +module.exports = PostgresqlInterface; + +PostgresqlInterface.prototype.summonTransaction = + function summonTransaction(deviceFingerprint, tx, cb) { + // First do an INSERT + // If it worked, go ahead with transaction + // If duplicate, fetch status and return + var self = this; + this.client.query('INSERT INTO transactions (id, status, deviceFingerprint, ' + + 'toAddress, satoshis, currencyCode, fiat) ' + + 'VALUES ($1, $2, $3, $4, $5, $6, $7)', [tx.txId, 'pending', deviceFingerprint, + tx.toAddress, tx.satoshis, tx.currencyCode, tx.fiat], + function (err) { + if (err && PG_ERRORS[err.code] === 'uniqueViolation') + return self._fetchTransaction(tx.txId, cb); + if (err) return cb(err); + cb(null, true); + }); +}; + +PostgresqlInterface.prototype.reportTransactionError = + function reportTransactionError(tx, err) { + this.client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3', + ['failed', err.message, tx.txId]); +}; + +PostgresqlInterface.prototype.completeTransaction = + function completeTransaction(tx, txHash) { + if (txHash) + this.client.query('UPDATE transactions SET txHash=$1, status=$2, completed=now() WHERE id=$3', + [txHash, 'completed', tx.txId]); + else + this.client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3', + ['failed', 'No txHash received', tx.txId]); +}; + +PostgresqlInterface.prototype._fetchTransaction = + function _fetchTransaction(txId, cb) { + this.client.query('SELECT status, txHash FROM transaction WHERE id=$1', + [txId], function (err, rows) { + if (err) return cb(err); + + // This should never happen, since we already checked for existence + if (rows === 0) return cb(new Error('Couldn\'t find transaction.')); + + var result = rows[0]; + cb(null, false, result.txHash); + }); +}; diff --git a/lib/protocol/exchanges/custom_ticker.js b/lib/protocol/exchanges/custom_ticker.js new file mode 100644 index 00000000..e8a61228 --- /dev/null +++ b/lib/protocol/exchanges/custom_ticker.js @@ -0,0 +1,45 @@ +'use strict'; + +// TODO: refactor this with bitpay_ticker.js + +var https = require('https'); +var _ = require('underscore'); + +var CustomTicker = function(config) { + this.config = config; +}; + +CustomTicker.factory = function factory(config) { + return new CustomTicker(config); +}; + +CustomTicker.prototype.ticker = function ticker(currency, cb) { + var self = this; + https.get(this.config.uri, function(res) { + var buf = ''; + res.setEncoding('utf8'); + res.on('data', function(chunk) { + buf += chunk; + }) + .on('end', function() { + var json = null; + try { + json = JSON.parse(buf); + } catch(e) { + cb(new Error('Couldn\'t parse JSON response')); + return; + } + var rec = _.findWhere(json, {code: currency}); + + if (!rec) { + cb(new Error('Currency not listed: ' + currency)); + return; + } + cb(null, rec.rate); + }); + }).on('error', function(e) { + cb(e); + }); +}; + +module.exports = CustomTicker; diff --git a/package.json b/package.json index 3a8b947e..c8719922 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,19 @@ "express": "~3.4.7", "optimist": "~0.6.0", "lamassu-config": "~0.2.0", - "lamassu-atm-protocol": "~0.2.0" + "lodash": "~2.4.1", + "async": "~0.2.9", + "deepmerge": "~0.2.7", + "underscore": "~1.5.2", + "error-create": "0.0.0", + "date-utils": "~1.2.15", + "bitstamp": "~0.1.3", + "winston": "~0.7.2", + "pg": "~2.11.1", + "lamassu-bitpay": "~0.0.1", + "lamassu-bitstamp": "~0.0.1", + "lamassu-mtgox": "~0.0.1", + "lamassu-blockchain": "0.0.4" }, "repository": { "type": "git", From 016f1351bdc1941d95867ccc605e702e84c7cc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 01:14:28 +0200 Subject: [PATCH 02/45] Move tests --- test/api/apiTest.js | 16 ++++ test/api/configTest.js | 71 ++++++++++++++++ test/api/sendTest.js | 125 ++++++++++++++++++++++++++++ test/api/tickerTest.js | 119 ++++++++++++++++++++++++++ test/api/tradeTest.js | 70 ++++++++++++++++ test/fiatBalanceTest.js | 57 +++++++++++++ test/fixtures/certificate.pem | 17 ++++ test/fixtures/certrequest.csr | 13 +++ test/fixtures/privatekey.pem | 15 ++++ test/helpers/create-https-server.js | 14 ++++ 10 files changed, 517 insertions(+) create mode 100644 test/api/apiTest.js create mode 100644 test/api/configTest.js create mode 100644 test/api/sendTest.js create mode 100644 test/api/tickerTest.js create mode 100644 test/api/tradeTest.js create mode 100644 test/fiatBalanceTest.js create mode 100644 test/fixtures/certificate.pem create mode 100644 test/fixtures/certrequest.csr create mode 100644 test/fixtures/privatekey.pem create mode 100644 test/helpers/create-https-server.js diff --git a/test/api/apiTest.js b/test/api/apiTest.js new file mode 100644 index 00000000..f5117c44 --- /dev/null +++ b/test/api/apiTest.js @@ -0,0 +1,16 @@ +/* + * 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'; + diff --git a/test/api/configTest.js b/test/api/configTest.js new file mode 100644 index 00000000..6c08d49b --- /dev/null +++ b/test/api/configTest.js @@ -0,0 +1,71 @@ +/* + * 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 LamassuConfig = require('lamassu-config'); +var con = 'psql://lamassu:lamassu@localhost/lamassu'; +var config = new LamassuConfig(con); + +var jsonquest = require('jsonquest'); +var express = require('express'); +var app = express(); +var testPort = 4000; + +var cfg; + +describe('configurations test', function(){ + + beforeEach(function(done) { + + app.listen(testPort); + config.load(function(err, results) { + assert.isNull(err); + assert.ok(results.ok); + + cfg = results.config; + + done(); + }); + }); + + + it('should get configurations from remote server', function(done) { + this.timeout(1000000); + + var api = require('../../lib/atm-api'); + api.init(app, cfg); + + // make the request + setTimeout(function() { + jsonquest({ + host: 'localhost', + port: testPort, + path: '/config', + method: 'GET', + protocol: 'http' + }, function (err, res, body) { + assert.isNull(err); + assert.equal(res.statusCode, 200); + + assert.isNull(body.err); + assert.ok(body.results); + + done(); + }); + }, 2000); + }); +}); \ No newline at end of file diff --git a/test/api/sendTest.js b/test/api/sendTest.js new file mode 100644 index 00000000..97e44044 --- /dev/null +++ b/test/api/sendTest.js @@ -0,0 +1,125 @@ +/* + * 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.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/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); + }); +}); \ No newline at end of file diff --git a/test/api/tickerTest.js b/test/api/tickerTest.js new file mode 100644 index 00000000..f4bbf14b --- /dev/null +++ b/test/api/tickerTest.js @@ -0,0 +1,119 @@ +/* + * 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.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/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 new file mode 100644 index 00000000..387d4d3a --- /dev/null +++ b/test/api/tradeTest.js @@ -0,0 +1,70 @@ +/* + * 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.config; + done(); + }); + }); + + + + it('should execute a trade against bitstamp', function(done) { + this.timeout(1000000); + + cfg.exchanges.plugins.trade = 'bitstamp'; + var api = require('../../lib/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() + }); +}); diff --git a/test/fiatBalanceTest.js b/test/fiatBalanceTest.js new file mode 100644 index 00000000..6f436d40 --- /dev/null +++ b/test/fiatBalanceTest.js @@ -0,0 +1,57 @@ +'use strict'; + +var assert = require('chai').assert; + +var LamassuConfig = require('lamassu-config'); +var con = 'psql://lamassu:lamassu@localhost/lamassu'; +var config = new LamassuConfig(con); + +var api = require('../lib/protocol/api/api.js'); +var RATE = 100; +var SATOSHI_FACTOR = Math.pow(10, 8); +var cfg; + +describe('fiatBalance test', function() { + before(function(done) { + config.load(function(err, result) { + assert.isNull(err); + cfg = result.exchanges; + api.init(cfg); + done(); + }); + }); + + after(function(done) { + config.end(); + done(); + }); + + it('should calculate balance correctly with transfer exchange only', function() { + // We have 2 bitcoins, want to trade 1 bitcoin for 100 fiat + var balance = api.fiatBalance(RATE / cfg.settings.commission, { + transferBalance: 2 * SATOSHI_FACTOR, + tradeBalance: null + }, 1 * SATOSHI_FACTOR, 100); + assert.equal(balance, 100 / cfg.settings.lowBalanceMargin); + }); + + it('should calculate balance correctly with both exchanges (trade > transfer)', function() { + // We have 2 bitcoins for transfer, 2000 fiat for trade, want to trade 1 + // bitcoin for 100 fiat + var balance = api.fiatBalance(RATE / cfg.settings.commission, { + transferBalance: 2 * SATOSHI_FACTOR, + tradeBalance: 2000 + }, 1 * SATOSHI_FACTOR, 100); + assert.equal(balance, 100 / cfg.settings.lowBalanceMargin); + }); + + it('should calculate balance correctly with both exchanges (transfer > trade)', function() { + // We have 2 bitcoins for transfer, 150 fiat for trade, want to trade 1 + // bitcoin for 100 fiat + var balance = api.fiatBalance(RATE / cfg.settings.commission, { + transferBalance: 2 * SATOSHI_FACTOR, + tradeBalance: 150 + }, 1 * SATOSHI_FACTOR, 100); + assert.equal(balance, 50 / cfg.settings.lowBalanceMargin); + }); +}); diff --git a/test/fixtures/certificate.pem b/test/fixtures/certificate.pem new file mode 100644 index 00000000..542541e1 --- /dev/null +++ b/test/fixtures/certificate.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICozCCAgwCCQCSX2bhri8GETANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMC +SUUxEjAQBgNVBAgTCVdhdGVyZm9yZDESMBAGA1UEBxMJV2F0ZXJmb3JkMREwDwYD +VQQKEwhuZWFyRm9ybTEMMAoGA1UECxMDZGV2MRQwEgYDVQQDEwtQZXRlciBFbGdl +cjEnMCUGCSqGSIb3DQEJARYYcGV0ZXIuZWxnZXJAbmVhcmZvcm0uY29tMB4XDTE0 +MDEyMDExMjc1NloXDTE0MDIxOTExMjc1NlowgZUxCzAJBgNVBAYTAklFMRIwEAYD +VQQIEwlXYXRlcmZvcmQxEjAQBgNVBAcTCVdhdGVyZm9yZDERMA8GA1UEChMIbmVh +ckZvcm0xDDAKBgNVBAsTA2RldjEUMBIGA1UEAxMLUGV0ZXIgRWxnZXIxJzAlBgkq +hkiG9w0BCQEWGHBldGVyLmVsZ2VyQG5lYXJmb3JtLmNvbTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAtZBpc6ZpF3rVSOq0D2zD2PMgR4hBzka0tD7coEDRWgjH +CFCtwtB97yuV3zq6V0zcApogXIIM6NQm6vcSAna9hqEiQCJV18GEAylC7Z/AW7HP +L63kYTMAXhdoztLFrkczUbtHwCB6wyUehszSzWaozpS9+ESpf/bPxMJjfhuqOvcC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQCF3oZsXXC4QIm5Say8AVPYlhyb0liUSmr8 +owvESnPFy2PYFHMwzLCE4wnVsXcRq4gK0rXiiuBQsNEw/v93RfLWV4DLAlf9DoB2 +sO3LA/LSj4ptjZ4Dki5NKfTK9b6QJoQkc/u68hEEOe/WYZPIxdaeki4aVCcnrv1v +zJ9YTluP7w== +-----END CERTIFICATE----- diff --git a/test/fixtures/certrequest.csr b/test/fixtures/certrequest.csr new file mode 100644 index 00000000..9b36b045 --- /dev/null +++ b/test/fixtures/certrequest.csr @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICBDCCAW0CAQAwgZUxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwlXYXRlcmZvcmQx +EjAQBgNVBAcTCVdhdGVyZm9yZDERMA8GA1UEChMIbmVhckZvcm0xDDAKBgNVBAsT +A2RldjEUMBIGA1UEAxMLUGV0ZXIgRWxnZXIxJzAlBgkqhkiG9w0BCQEWGHBldGVy +LmVsZ2VyQG5lYXJmb3JtLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +tZBpc6ZpF3rVSOq0D2zD2PMgR4hBzka0tD7coEDRWgjHCFCtwtB97yuV3zq6V0zc +ApogXIIM6NQm6vcSAna9hqEiQCJV18GEAylC7Z/AW7HPL63kYTMAXhdoztLFrkcz +UbtHwCB6wyUehszSzWaozpS9+ESpf/bPxMJjfhuqOvcCAwEAAaAuMBMGCSqGSIb3 +DQEJBzEGEwRhc2RmMBcGCSqGSIb3DQEJAjEKEwhuZWFyRm9ybTANBgkqhkiG9w0B +AQUFAAOBgQC0+vxHKcdpeiUYYXJjUVHUC9xSR19l+8F7FNtehXyGNoxmpMwmEVkM +J1TUokG/HgBoh9vy8TxleHldDdA+9jjWfaPHk8LaRIkNguJ9IMvv3yctjcCD39lJ +Yb1mQWOYaDOsgsEqiN/U2K6yUneYGGKIndA/PrEmd1aBMOTQ7R9Tvg== +-----END CERTIFICATE REQUEST----- diff --git a/test/fixtures/privatekey.pem b/test/fixtures/privatekey.pem new file mode 100644 index 00000000..a3e93ede --- /dev/null +++ b/test/fixtures/privatekey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC1kGlzpmkXetVI6rQPbMPY8yBHiEHORrS0PtygQNFaCMcIUK3C +0H3vK5XfOrpXTNwCmiBcggzo1Cbq9xICdr2GoSJAIlXXwYQDKULtn8Bbsc8vreRh +MwBeF2jO0sWuRzNRu0fAIHrDJR6GzNLNZqjOlL34RKl/9s/EwmN+G6o69wIDAQAB +AoGAS35aen3tIJgwavs8F1XQMTsWZuffBNgddvzUW8XXxqnzqB9zwJdUZcgnrwQK +SxZoPKS9Y/mnSIA+FmDZGjEpKEeLrFePYz9UHpSW3j3vmLwWNTyAFl/DjqyrCIpE ++qB309t9NYEdd7LTeJyfIfideyEDAZUaQ2VsVDQDypYeoUECQQDrNstTpkv24MLA +4rFiXSSiC5IT86QuXbmXeHNTdChhqFS8C5LVKqt4Prpq4QvwQksiHJkipp5zvy2V +zfvfVTNnAkEAxZvnnYR5XyIPMVRQtlV0iskbvnQovJ4l3B7UfHeP9DZ9uhAR4MUo +ttJGjDjUMo78w381KEAqePpKn+RhF70R8QJADqwEUtt0sZmjjFSXrAMTXehK3GO+ +QgYmpYQl7Xa5bh4J6xDtv85Bk+aVykTvcbUw6pfOFZM/Hwk11rpak7vE0QJAEFGD +mvppm3WQk55G3AfKi/t3kw68nnbg4YCaQ30MIjqtv0O8djdR2Wcb9FBtFY/BR9Ol +bCGAYGUq7HFLo041wQJBAL2x2OvwnYYtXXOBY27tox3B1hDye7jRI5Q/IvcRB7YE +00q8L0XVaV4lmyd7tfFDQq/bfDUgTuwvkSPnQcpJDDw= +-----END RSA PRIVATE KEY----- diff --git a/test/helpers/create-https-server.js b/test/helpers/create-https-server.js new file mode 100644 index 00000000..a7c9cf9f --- /dev/null +++ b/test/helpers/create-https-server.js @@ -0,0 +1,14 @@ +var fs = require('fs'); +var path = require('path'); +var https = require('https'); +var fixtures = path.join(__dirname, '..', 'fixtures'); + +module.exports = function(handler, callback) { + var server = https.createServer({ + key: fs.readFileSync(path.join(fixtures, 'privatekey.pem')), + cert: fs.readFileSync(path.join(fixtures, 'certificate.pem')) + }, handler); + server.listen(0, function() { + callback(null, server); + }); +}; From 5e99e40a3826ff8b43037fbc471011b403f104e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 01:22:24 +0200 Subject: [PATCH 03/45] Add a test script --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index c8719922..f92fa723 100644 --- a/package.json +++ b/package.json @@ -45,5 +45,8 @@ }, "bin": { "lamassu-server": "./lib/app.js" + }, + "scripts": { + "test": "mocha --recursive test" } } From b980b77815a82c1a4d8b2dcfcd4947a9bfe66fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 02:27:00 +0200 Subject: [PATCH 04/45] Add required dev dependencies --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index f92fa723..c8aec3f2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "matchdep": "~0.3.0", "mocha": "~1.13.0", "grunt-mocha-test": "~0.7.0", + "hock": "git+https://github.com/mmalecki/hock.git#no-http-server", + "jsonquest": "^0.2.2", "grunt-mocha-cov": "0.0.7" }, "bin": { From 204932f4f9670b05ca4aaf13da270f7a01b300ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 02:27:56 +0200 Subject: [PATCH 05/45] Remove unused devDeps --- package.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/package.json b/package.json index c8aec3f2..454547dd 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,11 @@ "url": "https://github.com/lamassu/lamassu-server.git" }, "devDependencies": { - "grunt": "~0.4.1", - "grunt-contrib-jshint": "~0.6.0", - "load-grunt-tasks": "~0.1.0", "chai": "~1.8.1", "matchdep": "~0.3.0", "mocha": "~1.13.0", - "grunt-mocha-test": "~0.7.0", "hock": "git+https://github.com/mmalecki/hock.git#no-http-server", - "jsonquest": "^0.2.2", - "grunt-mocha-cov": "0.0.7" + "jsonquest": "^0.2.2" }, "bin": { "lamassu-server": "./lib/app.js" From bd1963fb6af24e53bfa42f9f546e169403f6d012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 03:05:27 +0200 Subject: [PATCH 06/45] Remove `/config` endpoint We seem to have went with stashing relevant configuration onto `/poll/:currency` result. --- lib/protocol/atm-api.js | 8 ----- test/api/configTest.js | 71 ----------------------------------------- 2 files changed, 79 deletions(-) delete mode 100644 test/api/configTest.js diff --git a/lib/protocol/atm-api.js b/lib/protocol/atm-api.js index e170806f..f465fc98 100644 --- a/lib/protocol/atm-api.js +++ b/lib/protocol/atm-api.js @@ -61,13 +61,6 @@ var send = function(req, res) { }); }; -var configurations = function(req, res) { - res.json({ - err: _config.exchanges && _config.exchanges.settings ? null : new Error('Settings Not Found!'), - results: _config.exchanges.settings - }); -}; - var pair = function(req, res) { var token = req.body.token; var name = req.body.name; @@ -95,7 +88,6 @@ exports.init = function(app, config, lamassuConfig, authMiddleware) { exports._transferExchange = api._transferExchange; app.get('/poll/:currency', authMiddleware, poll); - app.get('/config', authMiddleware, configurations); app.post('/trade', authMiddleware, trade); app.post('/send', authMiddleware, send); app.post('/pair', pair); diff --git a/test/api/configTest.js b/test/api/configTest.js deleted file mode 100644 index 6c08d49b..00000000 --- a/test/api/configTest.js +++ /dev/null @@ -1,71 +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 LamassuConfig = require('lamassu-config'); -var con = 'psql://lamassu:lamassu@localhost/lamassu'; -var config = new LamassuConfig(con); - -var jsonquest = require('jsonquest'); -var express = require('express'); -var app = express(); -var testPort = 4000; - -var cfg; - -describe('configurations test', function(){ - - beforeEach(function(done) { - - app.listen(testPort); - config.load(function(err, results) { - assert.isNull(err); - assert.ok(results.ok); - - cfg = results.config; - - done(); - }); - }); - - - it('should get configurations from remote server', function(done) { - this.timeout(1000000); - - var api = require('../../lib/atm-api'); - api.init(app, cfg); - - // make the request - setTimeout(function() { - jsonquest({ - host: 'localhost', - port: testPort, - path: '/config', - method: 'GET', - protocol: 'http' - }, function (err, res, body) { - assert.isNull(err); - assert.equal(res.statusCode, 200); - - assert.isNull(body.err); - assert.ok(body.results); - - done(); - }); - }, 2000); - }); -}); \ No newline at end of file From 3932e17b2e8d860c6a66569841c775637b5f73a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 03:32:37 +0200 Subject: [PATCH 07/45] Move `atm-api.js` to `routes.js` --- lib/app.js | 4 ++-- lib/{protocol/atm-api.js => routes.js} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/{protocol/atm-api.js => routes.js} (98%) diff --git a/lib/app.js b/lib/app.js index 8aff211e..0be5541f 100755 --- a/lib/app.js +++ b/lib/app.js @@ -25,7 +25,7 @@ var argv = require('optimist').argv; var app = express(); var fs = require('fs'); var LamassuConfig = require('lamassu-config'); -var atm = require('./protocol/atm-api.js'); +var routes = require('./routes'); var conString, dbConfig, config; @@ -77,6 +77,6 @@ config.load(function(err, conf) { }); } - atm.init(app, conf, config, authMiddleware); + routes.init(app, conf, config, authMiddleware); }); diff --git a/lib/protocol/atm-api.js b/lib/routes.js similarity index 98% rename from lib/protocol/atm-api.js rename to lib/routes.js index f465fc98..e2a646d6 100644 --- a/lib/protocol/atm-api.js +++ b/lib/routes.js @@ -1,6 +1,6 @@ 'use strict'; -var api = exports.api = require('./api/api'); +var api = exports.api = require('./protocol/api/api'); var _config; var _lamassuConfig; var _commission; From 02fcc426385beacf104851764b8fc521265ed56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 04:30:22 +0200 Subject: [PATCH 08/45] Make `lib/app.js` a factory for `lamassu-server` Extract all command line parsing to `bin/lamassu-server`. This will effectively make it easier to write tests for `lamassu-server`. --- bin/lamassu-server | 26 +++++++++++++++ lib/app.js | 82 ++++++++++++++++++++++++---------------------- 2 files changed, 68 insertions(+), 40 deletions(-) create mode 100755 bin/lamassu-server diff --git a/bin/lamassu-server b/bin/lamassu-server new file mode 100755 index 00000000..3ad549ba --- /dev/null +++ b/bin/lamassu-server @@ -0,0 +1,26 @@ +#!/usr/bin/env node +var createServer = require('../lib/app.js'); +var argv = require('optimist').argv; + +var options = { + postgres: process.env.DATABASE_URL +}; + +var port = process.env.PORT || 3000; + +if (!argv.http) { + if (!argv.key || !argv.cert) { + console.error('--key and --cert are required'); + process.exit(1); + } + + options.https = { + key: fs.readFileSync(argv.key), + cert: fs.readFileSync(argv.cert) + }; +} + +var server = createServer(options); +server.listen(port, function () { + console.log('lamassu-server listening on port ' + port + ' ' + argv.http ? '(http)' : '(https)'); +}); diff --git a/lib/app.js b/lib/app.js index 0be5541f..ad6dc169 100755 --- a/lib/app.js +++ b/lib/app.js @@ -21,62 +21,64 @@ var http = require('http'); var https = require('https'); var path = require('path'); var express = require('express'); -var argv = require('optimist').argv; -var app = express(); var fs = require('fs'); var LamassuConfig = require('lamassu-config'); var routes = require('./routes'); -var conString, dbConfig, config; +module.exports = function (options) { + var connectionString = options.postgres; + var app = express(); + var server; + var config; -conString = process.env.DATABASE_URL || 'postgres://lamassu:lamassu@localhost/lamassu'; + connectionString = connectionString || + 'postgres://lamassu:lamassu@localhost/lamassu'; -config = new LamassuConfig(conString); + config = new LamassuConfig(connectionString); -var port = process.env.PORT || 3000; -app.use(express.logger()); -app.use(express.favicon()); -app.use(express.bodyParser()); -app.use(express.methodOverride()); + app.use(express.logger()); + app.use(express.favicon()); + app.use(express.bodyParser()); + app.use(express.methodOverride()); -config.load(function(err, conf) { - if (err) { console.log(err); process.exit(1); } - - var authMiddleware = function (req, res, next) { return next(); }; - - if (argv.http) { - http.createServer(app).listen(port, function () { - console.log('Express server listening on port ' + port + ' (http)'); - }); + if (!options.https) { + server = http.createServer(app); } else { - authMiddleware = function(req, res, next) { - var fingerprint = req.connection.getPeerCertificate().fingerprint; - var e = new Error('Unauthorized'); - e.status = 401; - - config.isAuthorized(fingerprint, function (err, device) { - if (err) { return next(e); } - if (!device) { return next(e); } - req.device = device; - next(); - }); - }; - - var options = { - key: fs.readFileSync(argv.key), - cert: fs.readFileSync(argv.cert), + var serverOptions = { + key: options.https.key, + cert: options.https.cert, requestCert: true, secureProtocol: 'TLSv1_method', ciphers: 'AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH', honorCipherOrder: true }; - https.createServer(options, app).listen(port, function () { - console.log('Express server listening on port ' + port + ' (https)'); - }); + server = https.createServer(options, app); } - routes.init(app, conf, config, authMiddleware); + config.load(function(err, conf) { + if (err) { console.log(err); process.exit(1); } -}); + var authMiddleware = function (req, res, next) { return next(); }; + + if (options.https) { + authMiddleware = function(req, res, next) { + var fingerprint = req.connection.getPeerCertificate().fingerprint; + var e = new Error('Unauthorized'); + e.status = 401; + + config.isAuthorized(fingerprint, function (err, device) { + if (err) { return next(e); } + if (!device) { return next(e); } + req.device = device; + next(); + }); + }; + } + + routes.init(app, conf, config, authMiddleware); + }); + + return server; +}; From 73f1f31fd99273da4ec950b61a4c5c12936abcd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 04:48:07 +0200 Subject: [PATCH 09/45] Fix listen logging --- bin/lamassu-server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/lamassu-server b/bin/lamassu-server index 3ad549ba..12f6aebe 100755 --- a/bin/lamassu-server +++ b/bin/lamassu-server @@ -22,5 +22,5 @@ if (!argv.http) { var server = createServer(options); server.listen(port, function () { - console.log('lamassu-server listening on port ' + port + ' ' + argv.http ? '(http)' : '(https)'); + console.log('lamassu-server listening on port ' + port + ' ' + (argv.http ? '(http)' : '(https)')); }); From c245c528f05006b527a84a57f2b0a850a8fec271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 04:52:49 +0200 Subject: [PATCH 10/45] Fix location and lamassu-config API in tests --- test/api/sendTest.js | 6 +++--- test/api/tickerTest.js | 4 ++-- test/api/tradeTest.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/api/sendTest.js b/test/api/sendTest.js index 97e44044..182d7eb2 100644 --- a/test/api/sendTest.js +++ b/test/api/sendTest.js @@ -57,7 +57,7 @@ describe('send test', function() { }, function(err, results) { assert.isNull(err); - cfg = results.config.config; + cfg = results.config; port = results.blockchain.address().port; cfg.exchanges.plugins.current.transfer = 'blockchain'; @@ -103,7 +103,7 @@ describe('send test', function() { .reply(200, payment_response); - var api = require('../../lib/atm-api'); + var api = require('../../lib/protocol/atm-api'); api.init(app, cfg); var params = { @@ -122,4 +122,4 @@ describe('send test', function() { }); }, 2000); }); -}); \ No newline at end of file +}); diff --git a/test/api/tickerTest.js b/test/api/tickerTest.js index f4bbf14b..93265480 100644 --- a/test/api/tickerTest.js +++ b/test/api/tickerTest.js @@ -48,7 +48,7 @@ describe('ticker test', function(){ }, function(err, results) { assert.isNull(err); - cfg = results.config.config; + cfg = results.config; cfg.exchanges.settings.commission = 1; @@ -92,7 +92,7 @@ describe('ticker test', function(){ .reply(200, { balance: 100000000, total_received: 100000000 }); // That's 1 BTC. - var api = require('../../lib/atm-api'); + var api = require('../../lib/protocol/atm-api'); api.init(app, cfg); // let ticker rate fetch finish... diff --git a/test/api/tradeTest.js b/test/api/tradeTest.js index 387d4d3a..c307be53 100644 --- a/test/api/tradeTest.js +++ b/test/api/tradeTest.js @@ -41,7 +41,7 @@ describe('trade test', function(){ beforeEach(function(done) { config.load(function(err, result) { assert.isNull(err); - cfg = result.config; + cfg = result; done(); }); }); @@ -52,7 +52,7 @@ describe('trade test', function(){ this.timeout(1000000); cfg.exchanges.plugins.trade = 'bitstamp'; - var api = require('../../lib/atm-api'); + 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 From 818f48398c7fd305153b10ca9fa21f12f011d613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Fri, 11 Apr 2014 05:08:38 +0200 Subject: [PATCH 11/45] Fix https --- bin/lamassu-server | 1 + lib/app.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/lamassu-server b/bin/lamassu-server index 12f6aebe..86bc13d0 100755 --- a/bin/lamassu-server +++ b/bin/lamassu-server @@ -1,4 +1,5 @@ #!/usr/bin/env node +var fs = require('fs'); var createServer = require('../lib/app.js'); var argv = require('optimist').argv; diff --git a/lib/app.js b/lib/app.js index ad6dc169..24a8fff5 100755 --- a/lib/app.js +++ b/lib/app.js @@ -54,7 +54,7 @@ module.exports = function (options) { honorCipherOrder: true }; - server = https.createServer(options, app); + server = https.createServer(serverOptions, app); } config.load(function(err, conf) { From b2d643cf1de4a4a5f05ecfd34fe70880e202e750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Sat, 12 Apr 2014 13:55:03 +0200 Subject: [PATCH 12/45] Do not re-assign --- lib/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/app.js b/lib/app.js index 24a8fff5..6dcb972f 100755 --- a/lib/app.js +++ b/lib/app.js @@ -26,12 +26,12 @@ var LamassuConfig = require('lamassu-config'); var routes = require('./routes'); module.exports = function (options) { - var connectionString = options.postgres; var app = express(); + var connectionString; var server; var config; - connectionString = connectionString || + connectionString = options.postgres || 'postgres://lamassu:lamassu@localhost/lamassu'; config = new LamassuConfig(connectionString); From d6a0f17e80ea0ec843e5db68db220b3733ce05de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 13:06:48 +0200 Subject: [PATCH 13/45] Get started with `Trade` class The reason for isolating the trader class is testability. It's hard to properly test code with as much global state as files in `lib/api` had. With a `Trader` class we can easily instantiate multiple objects, with different configs, without the need for seeding the database with test configs, effectively enabling us to do unit tests as well as integration tests. Besides that, this will allow easier reconfiguration, thanks to `Trader#configure` method. --- lib/protocol/api/balance.js | 49 ---------- lib/trader.js | 179 ++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 49 deletions(-) delete mode 100644 lib/protocol/api/balance.js create mode 100644 lib/trader.js 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; + }); +}; From 05fef31fce63c3137c2ce89810221f6b56126b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 14:37:28 +0200 Subject: [PATCH 14/45] -x lib/app.js --- lib/app.js | 1 - 1 file changed, 1 deletion(-) mode change 100755 => 100644 lib/app.js diff --git a/lib/app.js b/lib/app.js old mode 100755 new mode 100644 index 6dcb972f..547169c4 --- a/lib/app.js +++ b/lib/app.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node /*jshint globalstrict: true, white: false, unused:false */ /*globals require, exports, console, module, process */ /* From 1f3c59dbc07268bbaa1204bd99f6f5c4d470f26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 15:16:53 +0200 Subject: [PATCH 15/45] More refactoring on `Trader` --- lib/app.js | 64 +++++++++++----- lib/protocol/api/api.js | 145 ------------------------------------- lib/protocol/api/send.js | 37 ---------- lib/protocol/api/ticker.js | 33 --------- lib/routes.js | 39 ++-------- lib/trader.js | 47 +++++++++--- 6 files changed, 88 insertions(+), 277 deletions(-) delete mode 100644 lib/protocol/api/api.js delete mode 100644 lib/protocol/api/send.js delete mode 100644 lib/protocol/api/ticker.js diff --git a/lib/app.js b/lib/app.js index 547169c4..b7a2bbf4 100644 --- a/lib/app.js +++ b/lib/app.js @@ -23,17 +23,44 @@ var express = require('express'); var fs = require('fs'); var LamassuConfig = require('lamassu-config'); var routes = require('./routes'); +var Trader = require('./trader'); +var PostgresqlInterface = require('./protocol/db/postgresql_interface'); module.exports = function (options) { var app = express(); var connectionString; var server; var config; + var trader; + var db; connectionString = options.postgres || 'postgres://lamassu:lamassu@localhost/lamassu'; config = new LamassuConfig(connectionString); + db = new PostgresqlInterface(connectionString); + trader = new Trader(db); + + config.load(function (err, config) { + if (err) { + console.error('Loading config failed'); + throw err; + } + + trader.configure(config); + trader.startPolling(); + }); + + config.on('configUpdate', function () { + config.load(function (err, config) { + if (err) { + return console.error('Error while reloading config'); + } + + trader.configure(config); + console.log('Config reloaded'); + }); + }); app.use(express.logger()); app.use(express.favicon()); @@ -56,28 +83,27 @@ module.exports = function (options) { server = https.createServer(serverOptions, app); } - config.load(function(err, conf) { - if (err) { console.log(err); process.exit(1); } + var authMiddleware = function (req, res, next) { + req.device = {}; + return next(); + }; - var authMiddleware = function (req, res, next) { return next(); }; + if (options.https) { + authMiddleware = function(req, res, next) { + var fingerprint = req.connection.getPeerCertificate().fingerprint; + var e = new Error('Unauthorized'); + e.status = 401; - if (options.https) { - authMiddleware = function(req, res, next) { - var fingerprint = req.connection.getPeerCertificate().fingerprint; - var e = new Error('Unauthorized'); - e.status = 401; + config.isAuthorized(fingerprint, function (err, device) { + if (err) { return next(e); } + if (!device) { return next(e); } + req.device = device; + next(); + }); + }; + } - config.isAuthorized(fingerprint, function (err, device) { - if (err) { return next(e); } - if (!device) { return next(e); } - req.device = device; - next(); - }); - }; - } - - routes.init(app, conf, config, authMiddleware); - }); + routes.init(app, trader, authMiddleware); return server; }; diff --git a/lib/protocol/api/api.js b/lib/protocol/api/api.js deleted file mode 100644 index 2ad7bfa6..00000000 --- a/lib/protocol/api/api.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict'; - -require('date-utils'); - -//var async = require('async'); -var winston = require('winston'); -var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); -var path = require('path'); - -var _transferExchange; -var _tickerExchange; -var _tradeExchange; -var _rates = {}; -var _config; -var _commission; -var _config; -var SATOSHI_FACTOR = Math.pow(10, 8); - -exports.ticker = require('./ticker'); -exports.trade = require('./trade'); -exports.send = require('./send'); -exports.balance = require('./balance'); -exports._tradeExchange = null; -exports._transferExchange = null; - -exports.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; -}; - -exports.findTicker = function (name) { - var exchange = exports.findExchange(name); - return exchange.ticker || exchange; -}; - -exports.findTrader = function (name) { - var exchange = exports.findExchange(name); - return exchange.trader || exchange; -}; - -exports.findWallet = function (name) { - var exchange = exports.findExchange(name); - return exchange.wallet || exchange; -}; - -exports.triggerBalance = function triggerBalance() { - this.balance.triggerBalance(); -}; - -exports.init = function(config) { - _config = 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; - _tickerExchange = exports.findTicker(tickerExchangeCode).factory(tickerExchangeConfig); - - var tradeExchangeCode = config.plugins.current.trade; - if (tradeExchangeCode) { - var tradeExchangeConfig = config.plugins.settings[tradeExchangeCode]; - _tradeExchange = exports.findTrader(tradeExchangeCode).factory(tradeExchangeConfig); - } - - var transferExchangeCode = config.plugins.current.transfer; - var transferExchangeConfig = config.plugins.settings[transferExchangeCode]; - _commission = config.settings.commission; - _transferExchange = exports.findWallet(transferExchangeCode).factory(transferExchangeConfig); - - var doRequestTradeExchange = _tradeExchange && tradeExchangeCode !== transferExchangeCode; - - exports._tradeExchange = _tradeExchange; - exports._transferExchange = _transferExchange; - exports.ticker.init(config, exports, _tickerExchange); - exports.trade.init(config, exports, _tradeExchange, exports.ticker); - exports.send.init(config, exports, _transferExchange, exports.ticker); - exports.balance.init(config, exports, _transferExchange, - doRequestTradeExchange ? _tradeExchange : null); -}; - -/** - * 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 - */ -exports.fiatBalance = function(rate, balance, transferSatoshis, tradeFiat, callback) { - 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 = _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); -}; - - diff --git a/lib/protocol/api/send.js b/lib/protocol/api/send.js deleted file mode 100644 index 6f3b9672..00000000 --- a/lib/protocol/api/send.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var _transferExchange; -var _api; -var _config; -var _conString = process.env.DATABASE_URL || 'postgres://lamassu:lamassu@localhost/lamassu'; -var _db = require('../db/postgresql_interface').factory(_conString); - -exports.init = function(config, api, transferExchange) { - _api = api; - _config = config; - _transferExchange = transferExchange; -}; - -exports.setDomain = function(domain) { - _transferExchange.setDomain(domain); -}; - -exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, cb) { - _db.summonTransaction(deviceFingerprint, tx, function (err, isNew, txHash) { - if (err) return cb(err); - if (isNew) return _transferExchange.sendBitcoins(tx.toAddress, tx.satoshis, - _config.settings.transactionFee, function(err, txHash) { - if (err) { - _db.reportTransactionError(tx, err); - return cb(err); - } - cb(null, txHash); - _db.completeTransaction(tx, txHash); - _api.triggerBalance(); - }); - - // transaction exists, but txHash might be null, - // in which case ATM should continue polling - cb(null, txHash); - }); -}; diff --git a/lib/protocol/api/ticker.js b/lib/protocol/api/ticker.js deleted file mode 100644 index 635c6d97..00000000 --- a/lib/protocol/api/ticker.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -require('date-utils'); -var winston = require('winston'); -var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]}); - -var _tickerExchange; -var _api; -var _rates = {}; - -var _pollRate = function(currency) { - logger.info('polling for rate...'); - _tickerExchange.ticker(currency, function(err, rate) { - if (err) return; - logger.info('Rate update:', rate); - _rates[currency] = {rate: rate, timestamp: new Date()}; - }); -}; - -exports.init = function(config, api, tickerExchange) { - _api = api; - _tickerExchange = tickerExchange; - - _pollRate(config.settings.currency); - setInterval(function () { - _pollRate(config.settings.currency); - }, 60 * 1000); -}; - -exports.rate = function(currency) { - if (!_rates[currency]) return null; - return _rates[currency]; -}; diff --git a/lib/routes.js b/lib/routes.js index e2a646d6..7ba01df8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,9 +1,6 @@ 'use strict'; -var api = exports.api = require('./protocol/api/api'); -var _config; -var _lamassuConfig; -var _commission; +var _trader; // Make sure these are higher than polling interval // or there will be a lot of errors @@ -26,8 +23,8 @@ var poll = function(req, res) { }); } - var rateRec = api.ticker.rate(req.params.currency); - var satoshiBalanceRec = api.balance.balance(); + var rateRec = _trader.rate(req.params.currency); + var satoshiBalanceRec = _trader.balance; if (rateRec === null || satoshiBalanceRec === null) return res.json({err: 'Server initializing'}); @@ -40,10 +37,10 @@ var poll = function(req, res) { res.json({ err: null, - rate: rate * _commission, - fiat: api.fiatBalance(rate, satoshiBalanceRec, 0, 0), + rate: rate * _trader.config.exchanges.settings.commission, + fiat: api.fiatBalance(0, 0), currency: req.params.currency, - txLimit: parseInt(_config.exchanges.settings.compliance.maximum.limit, 10) + txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10) }); }; @@ -76,33 +73,13 @@ var pair = function(req, res) { ); }; -exports.init = function(app, config, lamassuConfig, authMiddleware) { - _config = config; - _lamassuConfig = lamassuConfig; - - api.init(_config.exchanges); - - _commission = _config.exchanges.settings.commission; - - exports._tradeExchange = api._tradeExchange; - exports._transferExchange = api._transferExchange; +exports.init = function(app, trader, authMiddleware) { + _trader = trader; app.get('/poll/:currency', authMiddleware, poll); app.post('/trade', authMiddleware, trade); app.post('/send', authMiddleware, send); app.post('/pair', pair); - lamassuConfig.on('configUpdate', function () { - _lamassuConfig.load(function(err, config) { - if (err) { - return console.error('Error while reloading config'); - } - - _config = config; - api.init(_config.exchanges); - console.log('Config reloaded'); - }); - }); - return app; }; diff --git a/lib/trader.js b/lib/trader.js index b28ecf32..2175dd1c 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -1,6 +1,8 @@ 'use strict'; var path = require('path'); +var async = require('async'); +var winston = require('winston'); var SATOSHI_FACTOR = Math.pow(10, 8); @@ -10,6 +12,7 @@ var Trader = module.exports = function (db) { } this.db = db; + this.rates = {}; this.logger = new (winston.Logger)({ transports: [new (winston.transports.Console)()] }); @@ -29,38 +32,38 @@ Trader.prototype._findExchange = function (name) { }; Trader.prototype._findTicker = function (name) { - var exchange = Trader.prototype.findExchange(name); + var exchange = Trader.prototype._findExchange(name); return exchange.ticker || exchange; }; Trader.prototype._findTrader = function (name) { - var exchange = Trader.prototype.findExchange(name); + var exchange = Trader.prototype._findExchange(name); return exchange.trader || exchange; }; Trader.prototype._findWallet = function (name) { - var exchange = Trader.prototype.findExchange(name); + var exchange = Trader.prototype._findExchange(name); return exchange.wallet || exchange; }; Trader.prototype.configure = function (config) { - if (config.settings.lowBalanceMargin < 1) { + if (config.exchanges.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 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 tradeExchangeCode = config.plugins.current.trade; + var tradeExchangeCode = config.exchanges.plugins.current.trade; if (tradeExchangeCode) { - var tradeExchangeConfig = config.plugins.settings[tradeExchangeCode]; + var tradeExchangeConfig = config.exchanges.plugins.settings[tradeExchangeCode]; this.tradeExchange = this._findTrader(tradeExchangeCode).factory(tradeExchangeConfig); } - var transferExchangeCode = config.plugins.current.transfer; - var transferExchangeConfig = config.plugins.settings[transferExchangeCode]; + var transferExchangeCode = config.exchanges.plugins.current.transfer; + var transferExchangeConfig = config.exchanges.plugins.settings[transferExchangeCode]; this.transferExchange = this._findWallet(transferExchangeCode).factory(transferExchangeConfig); this.config = config; @@ -154,7 +157,11 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { }; Trader.prototype.startPolling = function () { + this.pollBalance(); + this.pollRate(); + setInterval(this.pollBalance.bind(this), 60 * 1000); + setInterval(this.pollRate.bind(this), 60 * 1000); }; Trader.prototype.pollBalance = function () { @@ -177,3 +184,19 @@ Trader.prototype.pollBalance = function () { self.balance = balance; }); }; + +Trader.prototype.pollRate = function () { + var self = this; + + var currency = self.config.exchanges.settings.currency; + self.logger.info('polling for rate...'); + self.tickerExchange.ticker(currency, function(err, rate) { + if (err) return; + self.logger.info('Rate update:', rate); + self.rates[currency] = {rate: rate, timestamp: new Date()}; + }); +}; + +Trader.prototype.rate = function (currency) { + return this.rates[currency]; +}; From 83a69f1c56d84a9be93b16156bbc88028d960e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 15:36:50 +0200 Subject: [PATCH 16/45] Fix `fiatBalance` call --- lib/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes.js b/lib/routes.js index 7ba01df8..c0364695 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -38,7 +38,7 @@ var poll = function(req, res) { res.json({ err: null, rate: rate * _trader.config.exchanges.settings.commission, - fiat: api.fiatBalance(0, 0), + fiat: _trader.fiatBalance(0, 0), currency: req.params.currency, txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10) }); From dd80eecdfb21786b2a0a694b41db1850ea9d78d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 15:41:06 +0200 Subject: [PATCH 17/45] Fix `fiatBalance` --- lib/trader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index 2175dd1c..81de3832 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -81,7 +81,7 @@ Trader.prototype.configure = function (config) { * return balance in satoshis */ Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) { - var rate = this.rate; + var rate = this.rate(this.config.exchanges.settings.currency).rate; var balance = this.balance; var commission = this.config.exchanges.settings.commission; @@ -92,7 +92,7 @@ Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) { // `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; + 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 From b4199a3e1f23fee509aa0f4d3fcd83db3de80195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Mon, 14 Apr 2014 15:43:59 +0200 Subject: [PATCH 18/45] Fix server initialization message --- lib/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes.js b/lib/routes.js index c0364695..e09b91e8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -26,7 +26,7 @@ var poll = function(req, res) { var rateRec = _trader.rate(req.params.currency); var satoshiBalanceRec = _trader.balance; - if (rateRec === null || satoshiBalanceRec === null) + if (!rateRec || !satoshiBalanceRec) return res.json({err: 'Server initializing'}); if (Date.now() - rateRec.timestamp > STALE_TICKER) return res.json({err: 'Stale ticker'}); From 06667620514ef981ab9538ddd187ef8e8925321e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 10:47:32 +0200 Subject: [PATCH 19/45] Add a comment about `rateRec` and its friend --- lib/routes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/routes.js b/lib/routes.js index e09b91e8..6b11a17e 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -26,6 +26,8 @@ var poll = function(req, res) { var rateRec = _trader.rate(req.params.currency); var satoshiBalanceRec = _trader.balance; + // `rateRec` and `satoshiBalanceRec` are both objects, so there's no danger + // of misinterpreting rate or balance === 0 as 'Server initializing'. if (!rateRec || !satoshiBalanceRec) return res.json({err: 'Server initializing'}); if (Date.now() - rateRec.timestamp > STALE_TICKER) From 8ccf5bbf9e12f237b54edf7c537d469ddd02baad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 11:06:09 +0200 Subject: [PATCH 20/45] Add a test for Trader API --- test/unit/traderApiTest.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/unit/traderApiTest.js diff --git a/test/unit/traderApiTest.js b/test/unit/traderApiTest.js new file mode 100644 index 00000000..b1e0eeca --- /dev/null +++ b/test/unit/traderApiTest.js @@ -0,0 +1,29 @@ +'use strict'; + +var assert = require('chai').assert; +var Trader = require('../../lib/trader.js'); +var PostgresqlInterface = require('../../lib/protocol/db/postgresql_interface.js'); + +var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; +var psqlInterface = new PostgresqlInterface(db); + +describe('trader/api', function () { + it('should throw when trying to create a trader with no DB', function () { + assert.throws(function () { + new Trader(); + }); + }); + + it('should throw when trying to configure a trader with `lowBalanceMargin` < 1', function () { + var trader = new Trader(psqlInterface); + assert.throws(function () { + trader.configure({ + exchanges: { + settings: { + lowBalanceMargin: 0.8 + } + } + }); + }); + }); +}); From 06242fe78a266054de9b74d0f08d50bf429d7e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 11:17:50 +0200 Subject: [PATCH 21/45] Add tests for exchange creation --- test/unit/traderApiTest.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/unit/traderApiTest.js b/test/unit/traderApiTest.js index b1e0eeca..002c6e7e 100644 --- a/test/unit/traderApiTest.js +++ b/test/unit/traderApiTest.js @@ -26,4 +26,29 @@ describe('trader/api', function () { }); }); }); + + it('should find and instantiate ticker and trade exchanges', function () { + var trader = new Trader(psqlInterface); + trader.configure({ + exchanges: { + plugins: { + current: { + ticker: 'bitpay', + transfer: 'blockchain' + }, + settings: { + bitpay: {}, + blockchain: {} + } + }, + settings: { + currency: 'USD', + lowBalanceMargin: 2 + } + } + }); + + assert.ok(trader.tickerExchange); + assert.ok(trader.transferExchange); + }); }); From 206b33ec38f90fc2d1edd726bfd26a320c65ce08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 12:08:35 +0200 Subject: [PATCH 22/45] Make fiat balance test use new Trader API Leave out the trading part for now since nobody is using this feature in production. --- test/fiatBalanceTest.js | 57 ------------------------------ test/unit/traderFiatBalanceTest.js | 48 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 57 deletions(-) delete mode 100644 test/fiatBalanceTest.js create mode 100644 test/unit/traderFiatBalanceTest.js diff --git a/test/fiatBalanceTest.js b/test/fiatBalanceTest.js deleted file mode 100644 index 6f436d40..00000000 --- a/test/fiatBalanceTest.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -var assert = require('chai').assert; - -var LamassuConfig = require('lamassu-config'); -var con = 'psql://lamassu:lamassu@localhost/lamassu'; -var config = new LamassuConfig(con); - -var api = require('../lib/protocol/api/api.js'); -var RATE = 100; -var SATOSHI_FACTOR = Math.pow(10, 8); -var cfg; - -describe('fiatBalance test', function() { - before(function(done) { - config.load(function(err, result) { - assert.isNull(err); - cfg = result.exchanges; - api.init(cfg); - done(); - }); - }); - - after(function(done) { - config.end(); - done(); - }); - - it('should calculate balance correctly with transfer exchange only', function() { - // We have 2 bitcoins, want to trade 1 bitcoin for 100 fiat - var balance = api.fiatBalance(RATE / cfg.settings.commission, { - transferBalance: 2 * SATOSHI_FACTOR, - tradeBalance: null - }, 1 * SATOSHI_FACTOR, 100); - assert.equal(balance, 100 / cfg.settings.lowBalanceMargin); - }); - - it('should calculate balance correctly with both exchanges (trade > transfer)', function() { - // We have 2 bitcoins for transfer, 2000 fiat for trade, want to trade 1 - // bitcoin for 100 fiat - var balance = api.fiatBalance(RATE / cfg.settings.commission, { - transferBalance: 2 * SATOSHI_FACTOR, - tradeBalance: 2000 - }, 1 * SATOSHI_FACTOR, 100); - assert.equal(balance, 100 / cfg.settings.lowBalanceMargin); - }); - - it('should calculate balance correctly with both exchanges (transfer > trade)', function() { - // We have 2 bitcoins for transfer, 150 fiat for trade, want to trade 1 - // bitcoin for 100 fiat - var balance = api.fiatBalance(RATE / cfg.settings.commission, { - transferBalance: 2 * SATOSHI_FACTOR, - tradeBalance: 150 - }, 1 * SATOSHI_FACTOR, 100); - assert.equal(balance, 50 / cfg.settings.lowBalanceMargin); - }); -}); diff --git a/test/unit/traderFiatBalanceTest.js b/test/unit/traderFiatBalanceTest.js new file mode 100644 index 00000000..38752305 --- /dev/null +++ b/test/unit/traderFiatBalanceTest.js @@ -0,0 +1,48 @@ +'use strict'; + +var assert = require('chai').assert; +var Trader = require('../../lib/trader.js'); +var PostgresqlInterface = require('../../lib/protocol/db/postgresql_interface.js'); + +var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; +var psqlInterface = new PostgresqlInterface(db); + +var RATE = 100; +var CURRENCY = 'USD'; +var SATOSHI_FACTOR = Math.pow(10, 8); +var LOW_BALANCE_MARGIN = 1.2; +var COMMISSION = 1.1; + +var settings = { + currency: CURRENCY, + lowBalanceMargin: LOW_BALANCE_MARGIN, + commission: COMMISSION +}; + +describe('trader/fiatBalance', function() { + it('should calculate balance correctly with transfer exchange only', function() { + var trader = new Trader(db); + trader.configure({ + exchanges: { + plugins: { + current: { + transfer: 'blockchain', + ticker: 'bitpay' + }, + settings: { blockchain: {}, bitpay: {} } + }, + settings: settings + } + }); + + // We have 2 bitcoins, want to trade 1 bitcoin for 100 fiat + trader.balance = { + transferBalance: 2 * SATOSHI_FACTOR, + tradeBalance: null + }; + trader.rates[CURRENCY] = { rate: RATE }; + + var balance = trader.fiatBalance(1 * SATOSHI_FACTOR, 100); + assert.equal(balance, (100 / LOW_BALANCE_MARGIN) * COMMISSION); + }); +}); From 26ae44b2a796513849f002127e8eb5dd8ba355d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 14:32:43 +0200 Subject: [PATCH 23/45] Remove balance calculation for trading Trading feature is not used in production, remove it for now. --- lib/trader.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index 81de3832..733d9d70 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -109,21 +109,7 @@ Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) { // [ $ ] = [ $ ] 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); + return fiatTransferBalance; }; Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { From 100362934125be7cbfdca74cdff3232a2ce92315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 15:03:36 +0200 Subject: [PATCH 24/45] `testkeys` were moved to `test/fixtures` --- testkeys/certificate.pem | 17 ----------------- testkeys/certrequest.csr | 13 ------------- testkeys/privatekey.pem | 15 --------------- 3 files changed, 45 deletions(-) delete mode 100644 testkeys/certificate.pem delete mode 100644 testkeys/certrequest.csr delete mode 100644 testkeys/privatekey.pem diff --git a/testkeys/certificate.pem b/testkeys/certificate.pem deleted file mode 100644 index 542541e1..00000000 --- a/testkeys/certificate.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICozCCAgwCCQCSX2bhri8GETANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMC -SUUxEjAQBgNVBAgTCVdhdGVyZm9yZDESMBAGA1UEBxMJV2F0ZXJmb3JkMREwDwYD -VQQKEwhuZWFyRm9ybTEMMAoGA1UECxMDZGV2MRQwEgYDVQQDEwtQZXRlciBFbGdl -cjEnMCUGCSqGSIb3DQEJARYYcGV0ZXIuZWxnZXJAbmVhcmZvcm0uY29tMB4XDTE0 -MDEyMDExMjc1NloXDTE0MDIxOTExMjc1NlowgZUxCzAJBgNVBAYTAklFMRIwEAYD -VQQIEwlXYXRlcmZvcmQxEjAQBgNVBAcTCVdhdGVyZm9yZDERMA8GA1UEChMIbmVh -ckZvcm0xDDAKBgNVBAsTA2RldjEUMBIGA1UEAxMLUGV0ZXIgRWxnZXIxJzAlBgkq -hkiG9w0BCQEWGHBldGVyLmVsZ2VyQG5lYXJmb3JtLmNvbTCBnzANBgkqhkiG9w0B -AQEFAAOBjQAwgYkCgYEAtZBpc6ZpF3rVSOq0D2zD2PMgR4hBzka0tD7coEDRWgjH -CFCtwtB97yuV3zq6V0zcApogXIIM6NQm6vcSAna9hqEiQCJV18GEAylC7Z/AW7HP -L63kYTMAXhdoztLFrkczUbtHwCB6wyUehszSzWaozpS9+ESpf/bPxMJjfhuqOvcC -AwEAATANBgkqhkiG9w0BAQUFAAOBgQCF3oZsXXC4QIm5Say8AVPYlhyb0liUSmr8 -owvESnPFy2PYFHMwzLCE4wnVsXcRq4gK0rXiiuBQsNEw/v93RfLWV4DLAlf9DoB2 -sO3LA/LSj4ptjZ4Dki5NKfTK9b6QJoQkc/u68hEEOe/WYZPIxdaeki4aVCcnrv1v -zJ9YTluP7w== ------END CERTIFICATE----- diff --git a/testkeys/certrequest.csr b/testkeys/certrequest.csr deleted file mode 100644 index 9b36b045..00000000 --- a/testkeys/certrequest.csr +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICBDCCAW0CAQAwgZUxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwlXYXRlcmZvcmQx -EjAQBgNVBAcTCVdhdGVyZm9yZDERMA8GA1UEChMIbmVhckZvcm0xDDAKBgNVBAsT -A2RldjEUMBIGA1UEAxMLUGV0ZXIgRWxnZXIxJzAlBgkqhkiG9w0BCQEWGHBldGVy -LmVsZ2VyQG5lYXJmb3JtLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA -tZBpc6ZpF3rVSOq0D2zD2PMgR4hBzka0tD7coEDRWgjHCFCtwtB97yuV3zq6V0zc -ApogXIIM6NQm6vcSAna9hqEiQCJV18GEAylC7Z/AW7HPL63kYTMAXhdoztLFrkcz -UbtHwCB6wyUehszSzWaozpS9+ESpf/bPxMJjfhuqOvcCAwEAAaAuMBMGCSqGSIb3 -DQEJBzEGEwRhc2RmMBcGCSqGSIb3DQEJAjEKEwhuZWFyRm9ybTANBgkqhkiG9w0B -AQUFAAOBgQC0+vxHKcdpeiUYYXJjUVHUC9xSR19l+8F7FNtehXyGNoxmpMwmEVkM -J1TUokG/HgBoh9vy8TxleHldDdA+9jjWfaPHk8LaRIkNguJ9IMvv3yctjcCD39lJ -Yb1mQWOYaDOsgsEqiN/U2K6yUneYGGKIndA/PrEmd1aBMOTQ7R9Tvg== ------END CERTIFICATE REQUEST----- diff --git a/testkeys/privatekey.pem b/testkeys/privatekey.pem deleted file mode 100644 index a3e93ede..00000000 --- a/testkeys/privatekey.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC1kGlzpmkXetVI6rQPbMPY8yBHiEHORrS0PtygQNFaCMcIUK3C -0H3vK5XfOrpXTNwCmiBcggzo1Cbq9xICdr2GoSJAIlXXwYQDKULtn8Bbsc8vreRh -MwBeF2jO0sWuRzNRu0fAIHrDJR6GzNLNZqjOlL34RKl/9s/EwmN+G6o69wIDAQAB -AoGAS35aen3tIJgwavs8F1XQMTsWZuffBNgddvzUW8XXxqnzqB9zwJdUZcgnrwQK -SxZoPKS9Y/mnSIA+FmDZGjEpKEeLrFePYz9UHpSW3j3vmLwWNTyAFl/DjqyrCIpE -+qB309t9NYEdd7LTeJyfIfideyEDAZUaQ2VsVDQDypYeoUECQQDrNstTpkv24MLA -4rFiXSSiC5IT86QuXbmXeHNTdChhqFS8C5LVKqt4Prpq4QvwQksiHJkipp5zvy2V -zfvfVTNnAkEAxZvnnYR5XyIPMVRQtlV0iskbvnQovJ4l3B7UfHeP9DZ9uhAR4MUo -ttJGjDjUMo78w381KEAqePpKn+RhF70R8QJADqwEUtt0sZmjjFSXrAMTXehK3GO+ -QgYmpYQl7Xa5bh4J6xDtv85Bk+aVykTvcbUw6pfOFZM/Hwk11rpak7vE0QJAEFGD -mvppm3WQk55G3AfKi/t3kw68nnbg4YCaQ30MIjqtv0O8djdR2Wcb9FBtFY/BR9Ol -bCGAYGUq7HFLo041wQJBAL2x2OvwnYYtXXOBY27tox3B1hDye7jRI5Q/IvcRB7YE -00q8L0XVaV4lmyd7tfFDQq/bfDUgTuwvkSPnQcpJDDw= ------END RSA PRIVATE KEY----- From 36081c41097bc78ba21be2027e729433a8fe9bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 15:07:46 +0200 Subject: [PATCH 25/45] Use new `Trader` API to send bitcoins --- lib/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes.js b/lib/routes.js index 6b11a17e..318ccae0 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -55,7 +55,7 @@ var trade = function(req, res) { var send = function(req, res) { var fingerprint = req.connection.getPeerCertificate().fingerprint; - api.send.sendBitcoins(fingerprint, req.body, function(err, txHash) { + _trader.sendBitcoins(fingerprint, req.body, function(err, txHash) { res.json({err: err, txHash: txHash}); }); }; From 230b31cdbe7742f3470e57398e654bb19a905f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 16:26:09 +0200 Subject: [PATCH 26/45] Fix `sendBitcoins` --- lib/trader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index 733d9d70..2657773f 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -122,7 +122,7 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { return self.transferExchange.sendBitcoins( tx.toAddress, tx.satoshis, - self.config.settings.transactionFee, + self.config.exchanges.settings.transactionFee, function(err, txHash) { if (err) { self.db.reportTransactionError(tx, err); @@ -131,7 +131,7 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) { cb(null, txHash); self.db.completeTransaction(tx, txHash); - self.triggerBalance(); + self.pollRate(); } ); } From 74b5c72b5d94803d95a5320d924fd35af8b57544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 16:26:20 +0200 Subject: [PATCH 27/45] Add a basic test for sending bitcoins --- test/unit/traderSendTest.js | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/unit/traderSendTest.js diff --git a/test/unit/traderSendTest.js b/test/unit/traderSendTest.js new file mode 100644 index 00000000..d8d26146 --- /dev/null +++ b/test/unit/traderSendTest.js @@ -0,0 +1,55 @@ +'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/protocol/db/postgresql_interface.js'); + +var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; +var psqlInterface = new PostgresqlInterface(db); + +var TRANSACTION_FEE = 1; +var FINGERPRINT = 'CB:3D:78:49:03:39:BA:47:0A:33:29:3E:31:25:F7:C6:4F:74:71:D7'; +var TXID = '216dabdb692670bae940deb71e59486038a575f637903d3c9af601ddd48057fc'; +var CURRENCY = 'USD'; + +describe('trader/send', function () { + var trader = new Trader(psqlInterface); + trader.config = { + exchanges: { + settings: { + transactionFee: TRANSACTION_FEE + } + } + }; + + trader.tickerExchange = { ticker: function () { } }; + + it('should call `sendBitcoins` on the transfer exchange', function (done) { + var address = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; + var txId = uuid(); + var satoshis = 1337; + + trader.transferExchange = { + sendBitcoins: function (address_, satoshis_, transactionFee, callback) { + assert.equal(address, address_); + assert.equal(satoshis, satoshis_); + assert.equal(transactionFee, TRANSACTION_FEE); + callback(null, TXID); + } + }; + + trader.sendBitcoins(FINGERPRINT, { + fiat: 100, + txId: txId, + currencyCode: CURRENCY, + toAddress: address, + satoshis: satoshis + }, function (err, txId) { + assert.notOk(err); + assert.equal(txId, TXID); + done(); + }); + }); +}); From 34fb7e7dafd6ba01d661afcf8f87ae4926e2b90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 19:24:21 +0200 Subject: [PATCH 28/45] Add node-uuid to dev deps --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 454547dd..5f5213b0 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "matchdep": "~0.3.0", "mocha": "~1.13.0", "hock": "git+https://github.com/mmalecki/hock.git#no-http-server", - "jsonquest": "^0.2.2" + "jsonquest": "^0.2.2", + "node-uuid": "^1.4.1" }, "bin": { "lamassu-server": "./lib/app.js" From 1900d74287adf24f067cd6a636790e05ca56fa72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Tue, 15 Apr 2014 19:24:36 +0200 Subject: [PATCH 29/45] Mock `pollRate` instead --- test/unit/traderSendTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/traderSendTest.js b/test/unit/traderSendTest.js index d8d26146..e1e0ac59 100644 --- a/test/unit/traderSendTest.js +++ b/test/unit/traderSendTest.js @@ -24,7 +24,7 @@ describe('trader/send', function () { } }; - trader.tickerExchange = { ticker: function () { } }; + trader.pollRate = function () {}; it('should call `sendBitcoins` on the transfer exchange', function (done) { var address = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; From 5e4369da27bfec9c74eea8067a2a0a0a5547a260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 09:19:39 +0200 Subject: [PATCH 30/45] Remove empty file --- test/api/apiTest.js | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 test/api/apiTest.js diff --git a/test/api/apiTest.js b/test/api/apiTest.js deleted file mode 100644 index f5117c44..00000000 --- a/test/api/apiTest.js +++ /dev/null @@ -1,16 +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'; - From 926e10da012241ff2bfe88378660f42ccd4045fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 09:36:04 +0200 Subject: [PATCH 31/45] Add `stopPolling` method --- lib/trader.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index 2657773f..43db0309 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -146,8 +146,13 @@ Trader.prototype.startPolling = function () { this.pollBalance(); this.pollRate(); - setInterval(this.pollBalance.bind(this), 60 * 1000); - setInterval(this.pollRate.bind(this), 60 * 1000); + this.balanceInterval = setInterval(this.pollBalance.bind(this), 60 * 1000); + this.rateInterval = setInterval(this.pollRate.bind(this), 60 * 1000); +}; + +Trader.prototype.stopPolling = function () { + clearInterval(this.balanceInterval); + clearInterval(this.rateInterval); }; Trader.prototype.pollBalance = function () { From 5b53d59cbaf2daa5720b29e2df04b4a92a28bdd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 10:01:31 +0200 Subject: [PATCH 32/45] Add optional callback to `pollBalance` --- lib/trader.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/trader.js b/lib/trader.js index 43db0309..d7698e93 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -155,7 +155,7 @@ Trader.prototype.stopPolling = function () { clearInterval(this.rateInterval); }; -Trader.prototype.pollBalance = function () { +Trader.prototype.pollBalance = function (callback) { var self = this; self.logger.info('collecting balance'); @@ -170,9 +170,15 @@ Trader.prototype.pollBalance = function () { self.tradeExchange.balance(next); } }, function (err, balance) { + if (err) { + return callback && callback(err); + } + balance.timestamp = Date.now(); self.logger.info('Balance update:', balance); self.balance = balance; + + callback && callback(null, balance); }); }; From 8276d87e2a86769c9f3da4cfc744617cbcb670b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 10:12:38 +0200 Subject: [PATCH 33/45] Add optional callback to `pollRate` --- lib/trader.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index d7698e93..d7c958c7 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -182,15 +182,19 @@ Trader.prototype.pollBalance = function (callback) { }); }; -Trader.prototype.pollRate = function () { +Trader.prototype.pollRate = function (callback) { var self = this; var currency = self.config.exchanges.settings.currency; self.logger.info('polling for rate...'); self.tickerExchange.ticker(currency, function(err, rate) { - if (err) return; + if (err) { + return callback && callback(err); + } + self.logger.info('Rate update:', rate); self.rates[currency] = {rate: rate, timestamp: new Date()}; + callback && callback(null, self.rates[currency]); }); }; From a6f899f06258ca0b01eb33d965b10cfcde74a92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 10:12:52 +0200 Subject: [PATCH 34/45] Add a unit test for ticker --- test/unit/traderTickerTest.js | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/unit/traderTickerTest.js diff --git a/test/unit/traderTickerTest.js b/test/unit/traderTickerTest.js new file mode 100644 index 00000000..62845a82 --- /dev/null +++ b/test/unit/traderTickerTest.js @@ -0,0 +1,55 @@ +'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/protocol/db/postgresql_interface.js'); + +var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; +var psqlInterface = new PostgresqlInterface(db); + +var CURRENCY = 'USD'; + +describe('trader/send', function () { + var trader = new Trader(psqlInterface); + trader.config = { + exchanges: { + settings: { currency: CURRENCY } + } + }; + + it('should call `balance` on the transfer exchange', function (done) { + trader.transferExchange = { + balance: function (callback) { + callback(null, 100); + } + }; + + trader.pollBalance(function (err, balance) { + assert.notOk(err); + assert.equal(trader.balance.transferBalance, 100); + assert.ok(trader.balance.timestamp); + done(); + }); + }); + + it('should call `ticker` on the ticker exchange', function (done) { + trader.tickerExchange = { + ticker: function (currency, callback) { + assert.equal(currency, CURRENCY); + callback(null, 100); + } + }; + + trader.pollRate(function (err, rate) { + var rate; + + assert.notOk(err); + rate = trader.rate(CURRENCY); + assert.equal(rate.rate, 100); + assert.ok(rate.timestamp); + done(); + }); + }); +}); From d9682e668f5b76fa65bb8e37de2b1f0385ec9345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 10:59:15 +0200 Subject: [PATCH 35/45] Remove `favicon` and `methodOverride` middlewares `methodOverride` is supposed to provide compatibility for older "browsers" which aren't able to send XHR requests with proper HTTP methods. Since the only thing talking to `lamassu-server` is the ATM, we don't need this middleware. `favicon` middleware is exactly what it sounds like. --- lib/app.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/app.js b/lib/app.js index b7a2bbf4..73619483 100644 --- a/lib/app.js +++ b/lib/app.js @@ -63,9 +63,7 @@ module.exports = function (options) { }); app.use(express.logger()); - app.use(express.favicon()); app.use(express.bodyParser()); - app.use(express.methodOverride()); if (!options.https) { server = http.createServer(app); From f5f27edaf07de677dc65c7aa9dfc83ebaaadb7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 11:15:03 +0200 Subject: [PATCH 36/45] Move `postgresql_interface.js to lib --- lib/app.js | 2 +- lib/{protocol/db => }/postgresql_interface.js | 0 test/unit/traderApiTest.js | 2 +- test/unit/traderFiatBalanceTest.js | 2 +- test/unit/traderSendTest.js | 2 +- test/unit/traderTickerTest.js | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename lib/{protocol/db => }/postgresql_interface.js (100%) diff --git a/lib/app.js b/lib/app.js index 73619483..b57a9c40 100644 --- a/lib/app.js +++ b/lib/app.js @@ -24,7 +24,7 @@ var fs = require('fs'); var LamassuConfig = require('lamassu-config'); var routes = require('./routes'); var Trader = require('./trader'); -var PostgresqlInterface = require('./protocol/db/postgresql_interface'); +var PostgresqlInterface = require('./postgresql_interface'); module.exports = function (options) { var app = express(); diff --git a/lib/protocol/db/postgresql_interface.js b/lib/postgresql_interface.js similarity index 100% rename from lib/protocol/db/postgresql_interface.js rename to lib/postgresql_interface.js diff --git a/test/unit/traderApiTest.js b/test/unit/traderApiTest.js index 002c6e7e..ae8ed901 100644 --- a/test/unit/traderApiTest.js +++ b/test/unit/traderApiTest.js @@ -2,7 +2,7 @@ var assert = require('chai').assert; var Trader = require('../../lib/trader.js'); -var PostgresqlInterface = require('../../lib/protocol/db/postgresql_interface.js'); +var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; var psqlInterface = new PostgresqlInterface(db); diff --git a/test/unit/traderFiatBalanceTest.js b/test/unit/traderFiatBalanceTest.js index 38752305..10b71257 100644 --- a/test/unit/traderFiatBalanceTest.js +++ b/test/unit/traderFiatBalanceTest.js @@ -2,7 +2,7 @@ var assert = require('chai').assert; var Trader = require('../../lib/trader.js'); -var PostgresqlInterface = require('../../lib/protocol/db/postgresql_interface.js'); +var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; var psqlInterface = new PostgresqlInterface(db); diff --git a/test/unit/traderSendTest.js b/test/unit/traderSendTest.js index e1e0ac59..c9765352 100644 --- a/test/unit/traderSendTest.js +++ b/test/unit/traderSendTest.js @@ -4,7 +4,7 @@ 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/protocol/db/postgresql_interface.js'); +var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; var psqlInterface = new PostgresqlInterface(db); diff --git a/test/unit/traderTickerTest.js b/test/unit/traderTickerTest.js index 62845a82..dca1cf1a 100644 --- a/test/unit/traderTickerTest.js +++ b/test/unit/traderTickerTest.js @@ -4,7 +4,7 @@ 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/protocol/db/postgresql_interface.js'); +var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; var psqlInterface = new PostgresqlInterface(db); From 770cc8392c924c9866dce05eca91c5b861e1c5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 14:19:36 +0200 Subject: [PATCH 37/45] Change the numbers around a bit --- test/unit/traderFiatBalanceTest.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/traderFiatBalanceTest.js b/test/unit/traderFiatBalanceTest.js index 10b71257..b913e6e3 100644 --- a/test/unit/traderFiatBalanceTest.js +++ b/test/unit/traderFiatBalanceTest.js @@ -7,7 +7,7 @@ var PostgresqlInterface = require('../../lib/postgresql_interface.js'); var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; var psqlInterface = new PostgresqlInterface(db); -var RATE = 100; +var RATE = 101; var CURRENCY = 'USD'; var SATOSHI_FACTOR = Math.pow(10, 8); var LOW_BALANCE_MARGIN = 1.2; @@ -35,14 +35,14 @@ describe('trader/fiatBalance', function() { } }); - // We have 2 bitcoins, want to trade 1 bitcoin for 100 fiat + // We have 3 bitcoins, want to trade 1 bitcoin for 100 fiat trader.balance = { - transferBalance: 2 * SATOSHI_FACTOR, + transferBalance: 3 * SATOSHI_FACTOR, tradeBalance: null }; trader.rates[CURRENCY] = { rate: RATE }; var balance = trader.fiatBalance(1 * SATOSHI_FACTOR, 100); - assert.equal(balance, (100 / LOW_BALANCE_MARGIN) * COMMISSION); + assert.equal(balance, (202 / LOW_BALANCE_MARGIN) * COMMISSION); }); }); From 73d3d4b56918db941830281ba10b1b48e9b092ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 14:46:18 +0200 Subject: [PATCH 38/45] Add missing variable --- lib/routes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/routes.js b/lib/routes.js index 318ccae0..0463aaae 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,6 +1,7 @@ 'use strict'; var _trader; +var _lamassuConfig; // Make sure these are higher than polling interval // or there will be a lot of errors From 971f3f2f5471f7bd042fa80ec11ef5f537807229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 14:49:16 +0200 Subject: [PATCH 39/45] JSHint, remove trading --- lib/routes.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/routes.js b/lib/routes.js index 0463aaae..4611d98d 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -29,12 +29,17 @@ var poll = function(req, res) { // `rateRec` and `satoshiBalanceRec` are both objects, so there's no danger // of misinterpreting rate or balance === 0 as 'Server initializing'. - if (!rateRec || !satoshiBalanceRec) + if (!rateRec || !satoshiBalanceRec) { return res.json({err: 'Server initializing'}); - if (Date.now() - rateRec.timestamp > STALE_TICKER) + } + + if (Date.now() - rateRec.timestamp > STALE_TICKER) { return res.json({err: 'Stale ticker'}); - if (Date.now() - rateRec.timestamp > STALE_BALANCE) + } + + if (Date.now() - rateRec.timestamp > STALE_BALANCE) { return res.json({err: 'Stale balance'}); + } var rate = rateRec.rate; @@ -47,13 +52,6 @@ var poll = function(req, res) { }); }; -// TODO need to add in a UID for this trade -var trade = function(req, res) { - api.trade.trade(req.body.fiat, req.body.satoshis, req.body.currency, function(err) { - res.json({err: err}); - }); -}; - var send = function(req, res) { var fingerprint = req.connection.getPeerCertificate().fingerprint; _trader.sendBitcoins(fingerprint, req.body, function(err, txHash) { @@ -70,8 +68,11 @@ var pair = function(req, res) { req.connection.getPeerCertificate().fingerprint, name, function(err) { - if (err) res.json(500, { err: err.message }); - else res.json(200); + if (err) { + return res.json(500, { err: err.message }); + } + + res.json(200); } ); }; @@ -80,7 +81,6 @@ exports.init = function(app, trader, authMiddleware) { _trader = trader; app.get('/poll/:currency', authMiddleware, poll); - app.post('/trade', authMiddleware, trade); app.post('/send', authMiddleware, send); app.post('/pair', pair); From 8cb9783bde5fe5d609b6d39200729d816b88fa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 14:58:44 +0200 Subject: [PATCH 40/45] JSHint trader --- lib/trader.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/trader.js b/lib/trader.js index d7c958c7..7bc7ccdf 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -24,7 +24,10 @@ Trader.prototype._findExchange = function (name) { try { exchange = require('lamassu-' + name); } catch (err) { - if (!err.message.match(/Cannot find module/)) throw err; + if (!err.message.match(/Cannot find module/)) { + throw err; + } + exchange = require(path.join(path.dirname(__dirname), 'exchanges', name)); } @@ -85,7 +88,9 @@ Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) { var balance = this.balance; var commission = this.config.exchanges.settings.commission; - if (!rate || !balance) return 0; + if (!rate || !balance) { + return 0; + } // The rate is actually our commission times real rate. rate = commission * rate; @@ -116,7 +121,9 @@ 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 (err) { + return cb(err); + } if (isNew) { return self.transferExchange.sendBitcoins( @@ -178,7 +185,7 @@ Trader.prototype.pollBalance = function (callback) { self.logger.info('Balance update:', balance); self.balance = balance; - callback && callback(null, balance); + return callback && callback(null, balance); }); }; @@ -194,7 +201,7 @@ Trader.prototype.pollRate = function (callback) { self.logger.info('Rate update:', rate); self.rates[currency] = {rate: rate, timestamp: new Date()}; - callback && callback(null, self.rates[currency]); + return callback && callback(null, self.rates[currency]); }); }; From b79bdf8789c9b5dee1986f07f733aba839d249e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 17:09:39 +0200 Subject: [PATCH 41/45] Start adding trading back to Trader --- lib/trader.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/trader.js b/lib/trader.js index 7bc7ccdf..5584b44b 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -16,6 +16,8 @@ var Trader = module.exports = function (db) { this.logger = new (winston.Logger)({ transports: [new (winston.transports.Console)()] }); + + this._tradeQueue = []; }; Trader.prototype._findExchange = function (name) { @@ -149,6 +151,18 @@ 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.tradeQueueFiatBalance = function (exchangeRate) { + var satoshis = this._tradeQueue.reduce(function (memo, rec) { + return memo + rec.satoshis; + }, 0); + return (satoshis / SATOSHI_FACTOR) * exchangeRate; +}; + Trader.prototype.startPolling = function () { this.pollBalance(); this.pollRate(); From 27f0119022b165a4e617cc4c03c090543ad07870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 17:09:54 +0200 Subject: [PATCH 42/45] Restore `/trade` route --- lib/routes.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/routes.js b/lib/routes.js index 4611d98d..2d3e1b56 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -52,6 +52,12 @@ var poll = function(req, res) { }); }; +var trade = function (req, res) { + _trader.trade(req.body.fiat, req.body.satoshis, req.body.currency, function(err) { + res.json({err: err}); + }); +}; + var send = function(req, res) { var fingerprint = req.connection.getPeerCertificate().fingerprint; _trader.sendBitcoins(fingerprint, req.body, function(err, txHash) { @@ -82,6 +88,7 @@ exports.init = function(app, trader, authMiddleware) { app.get('/poll/:currency', authMiddleware, poll); app.post('/send', authMiddleware, send); + app.post('/trade', authMiddleware, trade); app.post('/pair', pair); return app; From a6687964ba54f3d4686baa82e2075f55e101f76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Thu, 17 Apr 2014 13:02:36 +0200 Subject: [PATCH 43/45] Add `Trader#_consolidateTrades` --- lib/trader.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/trader.js b/lib/trader.js index 5584b44b..a07b99be 100644 --- a/lib/trader.js +++ b/lib/trader.js @@ -163,6 +163,27 @@ Trader.prototype.tradeQueueFiatBalance = function (exchangeRate) { return (satoshis / SATOSHI_FACTOR) * exchangeRate; }; +Trader.prototype._consolidateTrades = function () { + var queue = this._tradeQueue; + + var tradeRec = { + fiat: 0, + satoshis: 0, + currency: 'USD' + }; + + while (true) { + var lastRec = queue.shift(); + if (!lastRec) { + break; + } + tradeRec.fiat += lastRec.fiat; + tradeRec.satoshis += lastRec.satoshis; + tradeRec.currency = lastRec.currency; + } + return tradeRec; +}; + Trader.prototype.startPolling = function () { this.pollBalance(); this.pollRate(); From f90ded6144a5b1558d18c394c602ca6c5dd4e644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 15:07:10 +0200 Subject: [PATCH 44/45] Fix table name --- lib/postgresql_interface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index 10e1b253..b3bec237 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -56,7 +56,7 @@ PostgresqlInterface.prototype.completeTransaction = PostgresqlInterface.prototype._fetchTransaction = function _fetchTransaction(txId, cb) { - this.client.query('SELECT status, txHash FROM transaction WHERE id=$1', + this.client.query('SELECT status, txHash FROM transactions WHERE id=$1', [txId], function (err, rows) { if (err) return cb(err); From aa81cfe33b693d4a80cdd5fce08f31f011183afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Ma=C5=82ecki?= Date: Wed, 16 Apr 2014 16:45:32 +0200 Subject: [PATCH 45/45] Make idempotent send work, together with a test --- lib/postgresql_interface.js | 14 ++++++------- test/unit/traderSendTest.js | 40 +++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index b3bec237..b04cd217 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -26,8 +26,8 @@ PostgresqlInterface.prototype.summonTransaction = // If it worked, go ahead with transaction // If duplicate, fetch status and return var self = this; - this.client.query('INSERT INTO transactions (id, status, deviceFingerprint, ' + - 'toAddress, satoshis, currencyCode, fiat) ' + + this.client.query('INSERT INTO transactions (id, status, "deviceFingerprint", ' + + '"toAddress", satoshis, "currencyCode", fiat) ' + 'VALUES ($1, $2, $3, $4, $5, $6, $7)', [tx.txId, 'pending', deviceFingerprint, tx.toAddress, tx.satoshis, tx.currencyCode, tx.fiat], function (err) { @@ -47,7 +47,7 @@ PostgresqlInterface.prototype.reportTransactionError = PostgresqlInterface.prototype.completeTransaction = function completeTransaction(tx, txHash) { if (txHash) - this.client.query('UPDATE transactions SET txHash=$1, status=$2, completed=now() WHERE id=$3', + this.client.query('UPDATE transactions SET "txHash"=$1, status=$2, completed=now() WHERE id=$3', [txHash, 'completed', tx.txId]); else this.client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3', @@ -56,14 +56,14 @@ PostgresqlInterface.prototype.completeTransaction = PostgresqlInterface.prototype._fetchTransaction = function _fetchTransaction(txId, cb) { - this.client.query('SELECT status, txHash FROM transactions WHERE id=$1', - [txId], function (err, rows) { + this.client.query('SELECT status, "txHash" FROM transactions WHERE id=$1', + [txId], function (err, results) { if (err) return cb(err); // This should never happen, since we already checked for existence - if (rows === 0) return cb(new Error('Couldn\'t find transaction.')); + if (results.rows.length === 0) return cb(new Error('Couldn\'t find transaction.')); - var result = rows[0]; + var result = results.rows[0]; cb(null, false, result.txHash); }); }; diff --git a/test/unit/traderSendTest.js b/test/unit/traderSendTest.js index c9765352..fcac1dde 100644 --- a/test/unit/traderSendTest.js +++ b/test/unit/traderSendTest.js @@ -12,8 +12,12 @@ var psqlInterface = new PostgresqlInterface(db); var TRANSACTION_FEE = 1; var FINGERPRINT = 'CB:3D:78:49:03:39:BA:47:0A:33:29:3E:31:25:F7:C6:4F:74:71:D7'; var TXID = '216dabdb692670bae940deb71e59486038a575f637903d3c9af601ddd48057fc'; +var ADDRESS = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; +var SATOSHIS = 1337; var CURRENCY = 'USD'; +var OUR_TXID = uuid(); + describe('trader/send', function () { var trader = new Trader(psqlInterface); trader.config = { @@ -27,14 +31,10 @@ describe('trader/send', function () { trader.pollRate = function () {}; it('should call `sendBitcoins` on the transfer exchange', function (done) { - var address = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; - var txId = uuid(); - var satoshis = 1337; - trader.transferExchange = { - sendBitcoins: function (address_, satoshis_, transactionFee, callback) { - assert.equal(address, address_); - assert.equal(satoshis, satoshis_); + sendBitcoins: function (address, satoshis, transactionFee, callback) { + assert.equal(ADDRESS, address); + assert.equal(SATOSHIS, satoshis); assert.equal(transactionFee, TRANSACTION_FEE); callback(null, TXID); } @@ -42,10 +42,30 @@ describe('trader/send', function () { trader.sendBitcoins(FINGERPRINT, { fiat: 100, - txId: txId, + txId: OUR_TXID, currencyCode: CURRENCY, - toAddress: address, - satoshis: satoshis + toAddress: ADDRESS, + satoshis: SATOSHIS + }, function (err, txId) { + assert.notOk(err); + assert.equal(txId, TXID); + done(); + }); + }); + + it('should not call `sendBitcoins` on the transfer exchange with same send', function (done) { + trader.transferExchange = { + sendBitcoins: function () { + throw new Error('This should not have been called'); + } + }; + + trader.sendBitcoins(FINGERPRINT, { + fiat: 100, + txId: OUR_TXID, + currencyCode: CURRENCY, + toAddress: ADDRESS, + satoshis: SATOSHIS }, function (err, txId) { assert.notOk(err); assert.equal(txId, TXID);