Merge pull request #33 from chester1000/trader-cleanup

refactor(plugins): DO NOT MERGE server-side support for new plugins API
This commit is contained in:
Josh Harvey 2014-08-21 16:13:40 -04:00
commit ac5e2d63c9
16 changed files with 996 additions and 688 deletions

View file

@ -5,7 +5,7 @@ var https = require('https');
var express = require('express'); var express = require('express');
var LamassuConfig = require('lamassu-config'); var LamassuConfig = require('lamassu-config');
var routes = require('./routes'); var routes = require('./routes');
var Trader = require('./trader'); var plugins = require('./plugins');
var PostgresqlInterface = require('./postgresql_interface'); var PostgresqlInterface = require('./postgresql_interface');
var logger = require('./logger'); var logger = require('./logger');
@ -14,7 +14,6 @@ module.exports = function (options) {
var connectionString; var connectionString;
var server; var server;
var config; var config;
var trader;
var db; var db;
connectionString = options.postgres || connectionString = options.postgres ||
@ -22,7 +21,8 @@ module.exports = function (options) {
config = new LamassuConfig(connectionString); config = new LamassuConfig(connectionString);
db = new PostgresqlInterface(connectionString); db = new PostgresqlInterface(connectionString);
trader = new Trader(db); plugins.init(db);
config.load(function (err, config) { config.load(function (err, config) {
if (err) { if (err) {
@ -30,8 +30,8 @@ module.exports = function (options) {
throw err; throw err;
} }
trader.configure(config); plugins.configure(config);
trader.startPolling(); plugins.startPolling();
}); });
config.on('configUpdate', function () { config.on('configUpdate', function () {
@ -40,7 +40,7 @@ module.exports = function (options) {
return logger.error('Error while reloading config'); return logger.error('Error while reloading config');
} }
trader.configure(config); plugins.configure(config);
logger.info('Config reloaded'); logger.info('Config reloaded');
}); });
}); });
@ -89,7 +89,7 @@ module.exports = function (options) {
routes.init({ routes.init({
app: app, app: app,
lamassuConfig: config, lamassuConfig: config,
trader: trader, plugins: plugins,
authMiddleware: authMiddleware, authMiddleware: authMiddleware,
mock: options.mock mock: options.mock
}); });

401
lib/plugins.js Normal file
View file

@ -0,0 +1,401 @@
'use strict';
var async = require('async');
var logger = require('./logger');
var SATOSHI_FACTOR = 1e8;
var SESSION_TIMEOUT = 60 * 60 * 1000;
var POLLING_RATE = 60 * 1000; // poll each minute
var db = null;
var tickerPlugin = null;
var traderPlugin = null;
var walletPlugin = null;
var idVerifierPlugin = null;
var cachedConfig = null;
var deviceCurrency = 'USD'; // Can 'USD' be set as default?
var lastBalances = null;
var lastRates = {};
var balanceInterval = null;
var rateInterval = null;
var tradeInterval = null;
var tradesQueue = [];
var sessions = {};
// that's basically a constructor
exports.init = function init(databaseHandle) {
if (!databaseHandle) {
throw new Error('`db` is required');
}
db = databaseHandle;
}
function loadPlugin(name, config) {
// plugins definitions
var moduleMethods = {
ticker: [ 'ticker' ],
trader: [ 'balance', 'purchase', 'sell' ],
wallet: [ 'balance', 'sendBitcoins' ],
idVerifier: [ 'verifyUser', 'verifyTransaction' ]
};
var plugin = null;
// each used plugin MUST be installed
try {
plugin = require('lamassu-' + name);
} catch(_) {
throw new Error(name + ' module is not installed. Try running \'npm install --save lamassu-' + name + '\' first');
}
// each plugin MUST implement those
if (typeof plugin.SUPPORTED_MODULES !== 'undefined') {
if(plugin.SUPPORTED_MODULES === 'string')
plugin.SUPPORTED_MODULES = [plugin.SUPPORTED_MODULES];
}
if(!(plugin.SUPPORTED_MODULES instanceof Array))
throw new Error('\'' + name + '\' fails to implement *required* \'SUPPORTED_MODULES\' constant');
plugin.SUPPORTED_MODULES.forEach(function(moduleName) {
moduleMethods[moduleName].forEach(function(methodName) {
if (typeof plugin[methodName] !== 'function') {
throw new Error('\'' + name + '\' declares \'' + moduleName + '\', but fails to implement \'' + methodName + '\' method');
}
});
});
// each plugin SHOULD implement those
if (typeof plugin.NAME === 'undefined')
logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'NAME\' field'));
if (typeof plugin.config !== 'function') {
logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'config\' method'));
plugin.config = function() {};
} else if (config !== null)
plugin.config(config); // only when plugin supports it, and config is passed
return plugin;
};
exports.configure = function configure(config) {
if (config.exchanges.settings.lowBalanceMargin < 1) {
throw new Error('`settings.lowBalanceMargin` has to be >= 1');
}
deviceCurrency = config.exchanges.settings.currency;
var plugins = config.exchanges.plugins
// [required] configure (or load) ticker
var tickerName = plugins.current.ticker;
var tickerConfig = plugins.settings[tickerName] || {};
tickerConfig.currency = deviceCurrency;
if (tickerPlugin) tickerPlugin.config(tickerConfig);
else tickerPlugin = loadPlugin(tickerName, tickerConfig);
// [required] configure (or load) wallet
var walletName = plugins.current.transfer;
var walletConfig = plugins.settings[walletName] || {};
if (walletPlugin) walletPlugin.config(walletConfig);
else walletPlugin = loadPlugin(walletName, walletConfig);
// [optional] configure (or load) trader
var traderName = plugins.current.trade;
if (traderName) { // traderPlugin may be disabled
var traderConfig = plugins.settings[traderName] || {};
if (traderPlugin) traderPlugin.config(traderConfig);
else traderPlugin = loadPlugin(traderName, traderConfig);
}
// [optional] ID Verifier
var verifierName = plugins.current.idVerifier;
if (verifierName) { // idVerifierPlugin may be disabled
var verifierConfig = plugins.settings[verifierName] || {};
if (idVerifierPlugin) idVerifierPlugin.config(verifierConfig);
else loadPlugin(verifierName, verifierConfig);
}
cachedConfig = config;
pollBalance();
pollRate();
};
// This is where we record starting trade balance at the beginning
// of the user session
exports.trade = function trade(rawTrade, deviceFingerprint) {
var sessionInfo = sessions[deviceFingerprint];
if (!sessionInfo) {
sessions[deviceFingerprint] = {
timestamp: Date.now(),
reaper: setTimeout(function() {
delete sessions[deviceFingerprint];
}, SESSION_TIMEOUT)
};
}
tradesQueue.push({
currency: rawTrade.currency,
satoshis: rawTrade.satoshis
});
};
exports.fiatBalance = function fiatBalance(deviceFingerprint) {
var rawRate = getDeviceRate().rates.ask;
var commision = cachedConfig.exchanges.settings.commision;
if (!rawRate || !lastBalances) return null;
// The rate is actually our commission times real rate.
var rate = commission * rawRate;
// `lowBalanceMargin` is our safety net. It's a number > 1, and we divide
// all our balances by it to provide a safety margin.
var lowBalanceMargin = cachedConfig.exchanges.settings.lowBalanceMargin;
// `balance.transferBalance` is the balance of our transfer account (the one
// we use to send Bitcoins to clients) in satoshis.
var transferBalance = balance.transferBalance;
// Since `transferBalance` is in satoshis, we need to turn it into
// bitcoins and then fiat to learn how much fiat currency we can exchange.
//
// Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
// [ $ ] = [ B * $/B ]
// [ $ ] = [ $ ]
var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;
return fiatTransferBalance;
};
exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, callback) {
db.summonTransaction(deviceFingerprint, tx, function(err, txInfo) {
if (err) return callback(err);
if (!txInfo) {
clearSession(deviceFingerprint);
return walletPlugin.sendBitcoins(
tx.toAddress,
tx.satoshis,
cachedConfig.exchanges.settings.transactionFee,
function(err, txHash) {
if (err) {
var status = err.name === 'InsufficientFunds' ?
'insufficientFunds' :
'failed';
db.reportTransactionError(tx, err.message, status);
return callback(err);
}
db.completeTransaction(tx, txHash);
pollBalance();
callback(null, txHash);
}
);
}
// Out of bitcoins: special case
var txErr = null;
if (txInfo.err) {
txErr = new Error(txInfo.err);
if (txInfo.status === 'insufficientFunds') {
txErr.name = 'InsufficientFunds';
}
}
// transaction exists, but txHash might be null,
// in which case ATM should continue polling
pollBalance();
callback(txErr, txInfo.txHash);
});
};
/*
* Polling livecycle
*/
exports.startPolling = function startPolling() {
executeTrades();
if (!balanceInterval) {
balanceInterval = setInterval(pollBalance, POLLING_RATE);
}
if (!rateInterval) {
rateInterval = setInterval(pollRate, POLLING_RATE);
}
// Always start trading, even if we don't have a trade exchange configured,
// since configuration can always change in `Trader#configure`.
// `Trader#executeTrades` returns early if we don't have a trade exchange
// configured at the moment.
if (!tradeInterval) {
tradeInterval = setInterval(
executeTrades,
cachedConfig.exchanges.settings.tradeInterval
);
}
};
function stopPolling() {
clearInterval(balanceInterval);
clearInterval(rateInterval);
// clearInterval(tradeInterval); // TODO: should this get cleared too?
};
function pollBalance(callback) {
logger.debug('collecting balance');
var jobs = {
transferBalance: walletPlugin.balance
};
// only add if trader is enabled
// if (traderPlugin) {
// // NOTE: we would need to handle when traderCurrency!=deviceCurrency
// jobs.tradeBalance = traderPlugin.balance;
// }
async.parallel(jobs, function(err, balance) {
if (err) {
logger.error(err);
return callback && callback(err);
}
logger.debug('Balance update:', balance);
balance.timestamp = Date.now();
lastBalances = balance;
return callback && callback(null, lastBalances);
});
};
function pollRate(callback) {
logger.debug('polling for rates');
tickerPlugin.ticker(deviceCurrency, function(err, resRates) {
if (err) {
logger.error(err);
return callback && callback(err);
}
logger.debug('got rates: %j', resRates);
resRates.timestamp = new Date();
lastRates = resRates;
return callback && callback(null, lastRates);
});
};
/*
* Getters | Helpers
*/
function getLastRate(currency) {
if (!lastRates) return null;
var tmpCurrency = currency || deviceCurrency;
if (!lastRates[tmpCurrency]) return null;
return lastRates[tmpCurrency];
};
function getDeviceRate() {
return getLastRate(deviceCurrency);
};
function getBalance() {
if (!lastBalances) return null;
return lastBalances.transferBalance;
};
function clearSession(deviceFingerprint) {
var session = sessions[deviceFingerprint];
if (session) {
clearTimeout(session.reaper);
delete sessions[deviceFingerprint];
}
};
/*
* Trader functions
*/
function purchase(trade, callback) {
traderPlugin.purchase(trade.satoshis, null, function(err, _) {
if (err) return callback(err);
pollBalance();
callback && callback();
});
};
function consolidateTrades() {
// NOTE: value in satoshis stays the same no matter the currency
var consolidatedTrade = {
currency: deviceCurrency,
satoshis: tradesQueue.reduce(function(prev, current) {
return prev + current.satoshis;
}, 0)
};
logger.debug('consolidated: ', JSON.stringify(consolidatedTrade));
return consolidatedTrade;
};
function executeTrades() {
if (!traderPlugin) return;
logger.debug('checking for trades');
var trade = consolidateTrades();
if (trade.satoshis === 0) {
logger.debug('rejecting 0 trade');
return;
}
logger.trade.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR);
purchase(trade, function(err) {
if (err) logger.error(err);
});
};
/*
* ID Verifier functions
*/
exports.verifyUser = function verifyUser(data, callback) {
idVerifierPlugin.verifyUser(data, callback);
};
exports.verifyTransaction = function verifyTransaction(data, callback) {
idVerifier.verifyTransaction(data, callback);
};

View file

@ -1,12 +1,12 @@
'use strict'; 'use strict';
var _trader;
var _lamassuConfig;
var _idVerifier = null;
var _trader = null;
var _mock = false;
var logger = require('./logger'); var logger = require('./logger');
var mock = false;
var plugins;
var config;
module.exports = { module.exports = {
init: init, init: init,
getFingerprint: getFingerprint getFingerprint: getFingerprint
@ -18,8 +18,8 @@ var STALE_TICKER = 180000;
var STALE_BALANCE = 180000; var STALE_BALANCE = 180000;
function poll(req, res) { function poll(req, res) {
var rateRec = _trader.rate(); var rateRec = plugins.getDeviceRate();
var balanceRec = _trader.balance; var balanceRec = plugins.getBalance();
var fingerprint = getFingerprint(req); var fingerprint = getFingerprint(req);
logger.debug('poll request from: %s', fingerprint); logger.debug('poll request from: %s', fingerprint);
@ -39,9 +39,9 @@ function poll(req, res) {
return res.json({err: 'Stale balance'}); return res.json({err: 'Stale balance'});
} }
var rate = rateRec.rate; var rate = rateRec.rates.ask;
if (rate === null) return res.json({err: 'No rate available'}); if (rate === null) return res.json({err: 'No rate available'});
var fiatBalance = _trader.fiatBalance(fingerprint); var fiatBalance = plugins.fiatBalance(fingerprint);
if (fiatBalance === null) return res.json({err: 'No balance available'}); if (fiatBalance === null) return res.json({err: 'No balance available'});
var idVerificationLimit = _trader.config.exchanges.settings. var idVerificationLimit = _trader.config.exchanges.settings.
@ -51,18 +51,17 @@ function poll(req, res) {
res.json({ res.json({
err: null, err: null,
rate: rate * _trader.config.exchanges.settings.commission, rate: rate * config.exchanges.settings.commission,
fiat: fiatBalance, fiat: fiatBalance,
locale: _trader.config.brain.locale, locale: config.brain.locale,
txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10), txLimit: parseInt(config.exchanges.settings.compliance.maximum.limit, 10),
idVerificationLimit: idVerificationLimit idVerificationLimit: idVerificationLimit
}); });
} }
function trade(req, res) { function trade(req, res) {
var fingerprint = getFingerprint(req); var fingerprint = getFingerprint(req);
_trader.trade(req.body, fingerprint); plugins.trade(req.body, fingerprint);
res.json({err: null}); res.json({err: null});
} }
@ -72,21 +71,35 @@ function deviceEvent(req, res) {
res.json({err: null}); res.json({err: null});
} }
function idVerify(req, res) { function verifyUser(req, res) {
if (_mock) return res.json({success: true}); if (mock) return res.json({success: true});
_idVerifier.verify(req.body, function (err, idResult) { plugins.verifyUser(req.body, function (err, idResult) {
if (err) { if (err) {
logger.error(err); logger.error(err);
return res.json({err: 'Verification failed'}); return res.json({err: 'Verification failed'});
} }
res.json(idResult);
});
}
function verifyTransaction(req, res) {
if (mock) return res.json({success: true});
plugins.verifyTransaction(req.body, function (err, idResult) {
if (err) {
logger.error(err);
return res.json({err: 'Verification failed'});
}
res.json(idResult); res.json(idResult);
}); });
} }
function send(req, res) { function send(req, res) {
var fingerprint = getFingerprint(req); var fingerprint = getFingerprint(req);
_trader.sendBitcoins(fingerprint, req.body, function(err, txHash) { plugins.sendBitcoins(fingerprint, req.body, function(err, txHash) {
res.json({ res.json({
err: err && err.message, err: err && err.message,
txHash: txHash, txHash: txHash,
@ -99,7 +112,7 @@ function pair(req, res) {
var token = req.body.token; var token = req.body.token;
var name = req.body.name; var name = req.body.name;
_lamassuConfig.pair( config.pair(
token, token,
getFingerprint(req), getFingerprint(req),
name, name,
@ -113,23 +126,20 @@ function pair(req, res) {
); );
} }
function init(config) { function init(localConfig) {
_lamassuConfig = config.lamassuConfig; config = localConfig.lamassuConfig;
_trader = config.trader; plugins = localConfig.plugins;
_mock = config.mock; mock = localConfig.mock;
var authMiddleware = config.authMiddleware; var authMiddleware = localConfig.authMiddleware;
var app = config.app; var app = localConfig.app;
_lamassuConfig.readExchangesConfig(function (err, res) {
var idVerifyConfig = res.exchanges.plugins.settings.identitymind;
_idVerifier = require('lamassu-identitymind').factory(idVerifyConfig);
});
app.get('/poll', authMiddleware, poll); app.get('/poll', authMiddleware, poll);
app.post('/send', authMiddleware, send); app.post('/send', authMiddleware, send);
app.post('/trade', authMiddleware, trade); app.post('/trade', authMiddleware, trade);
app.post('/event', authMiddleware, deviceEvent); app.post('/event', authMiddleware, deviceEvent);
app.post('/verify_id', authMiddleware, idVerify); app.post('/verify_user', authMiddleware, verifyUser);
app.post('/verify_transaction', authMiddleware, verifyTransaction);
app.post('/pair', pair); app.post('/pair', pair);
return app; return app;

View file

@ -1,367 +0,0 @@
'use strict';
var async = require('async');
var logger = require('./logger');
var SATOSHI_FACTOR = 1e8;
// TODO: Define this somewhere more global
var SESSION_TIMEOUT = 60 * 60 * 1000; // an hour
function findExchange(name) {
try {
return require('lamassu-' + name);
} catch(_) {
throw new Error(name + ' module is not installed. Try running `npm install --save lamassu-' + name + '` first');
}
};
function findTicker (name) {
var exchange = findExchange(name);
return exchange.ticker || exchange;
};
function findTrader (name) {
var exchange = findExchange(name);
return exchange.trader || exchange;
};
function findWallet (name) {
var exchange = findExchange(name);
return exchange.wallet || exchange;
};
var Trader = module.exports = function (db) {
if (!db) {
throw new Error('`db` is required');
}
this.db = db;
this.rates = {};
this._tradeQueue = [];
this._sessionInfo = {};
this.rateInfo = null;
};
Trader.prototype._consolidateTrades = function () {
var queue = this._tradeQueue;
// NOTE: value in satoshis stays the same no matter the currency
var consolidatedTrade = {
currency: this.config.exchanges.settings.currency,
satoshis: queue.reduce(function (prev, current) {
return prev + current.satoshis;
}, 0)
};
return consolidatedTrade;
};
Trader.prototype._purchase = function (trade, cb) {
var self = this;
var tradeCurrency = this.tradeExchange.currency();
var rate = this.rate(tradeCurrency).rate;
this.tradeExchange.purchase(trade.satoshis, rate, function (err) {
if (err) return cb(err);
self.pollBalance();
cb();
});
};
Trader.prototype.configure = function (config) {
if (config.exchanges.settings.lowBalanceMargin < 1) {
throw new Error('`settings.lowBalanceMargin` has to be >= 1');
}
var plugins = config.exchanges.plugins
// source of current BTC price (init and configure)
var tickerName = plugins.current.ticker;
var tickerConfig = plugins.settings[tickerName] || {};
tickerConfig.currency = config.exchanges.settings.currency;
this.tickerExchange = findTicker(tickerName).factory(tickerConfig);
// Exchange used for trading (init and configure)
var traderName = plugins.current.trade;
if (traderName) {
var tradeConfig = plugins.settings[traderName];
this.tradeExchange = findTrader(traderName).factory(tradeConfig);
}
// Wallet (init and configure)
var walletName = plugins.current.transfer;
var walletConfig = plugins.settings[walletName];
this.transferExchange = findWallet(walletName).factory(walletConfig);
this.config = config;
this.pollBalance();
this.pollRate();
};
// IMPORTANT: This function returns the estimated minimum available balance
// in fiat as of the start of the current user session on the device. User
// session starts when a user presses then Start button and ends when we
// send the bitcoins.
Trader.prototype.fiatBalance = function (deviceFingerprint) {
var rawRate = this.rate(this.config.exchanges.settings.currency).rate;
var balance = this.balance;
var commission = this.config.exchanges.settings.commission;
if (!rawRate || !balance) {
return null;
}
// The rate is actually our commission times real rate.
var rate = commission * rawRate;
// `lowBalanceMargin` is our safety net. It's a number > 1, and we divide
// all our balances by it to provide a safety margin.
var lowBalanceMargin = this.config.exchanges.settings.lowBalanceMargin;
// `balance.transferBalance` is the balance of our transfer account (the one
// we use to send Bitcoins to clients) in satoshis.
var transferBalance = balance.transferBalance;
// Since `transferBalance` is in satoshis, we need to turn it into
// bitcoins and then fiat to learn how much fiat currency we can exchange.
//
// Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
// [ $ ] = [ B * $/B ]
// [ $ ] = [ $ ]
var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;
// If this server is also configured to trade received fiat for Bitcoins,
// we also need to calculate if we have enough funds on our trade exchange.
if (balance.tradeBalance === null) return fiatTransferBalance;
var tradeBalance = balance.tradeBalance;
// We're reporting balance as of the start of the user session.
var sessionInfo = this._sessionInfo[deviceFingerprint];
var sessionBalance = sessionInfo ? sessionInfo.tradeBalance : tradeBalance;
var fiatTradeBalance = sessionBalance / lowBalanceMargin;
// And we return the smallest number.
return Math.min(fiatTransferBalance, fiatTradeBalance);
};
Trader.prototype._clearSession = function (deviceFingerprint) {
var sessionInfo = this._sessionInfo[deviceFingerprint];
if (sessionInfo) {
clearTimeout(sessionInfo.reaper);
delete this._sessionInfo[deviceFingerprint];
}
};
Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) {
var self = this;
self.db.summonTransaction(deviceFingerprint, tx, function (err, txRec) {
if (err) return cb(err);
if (!txRec) {
self._clearSession(deviceFingerprint);
return self.transferExchange.sendBitcoins(
tx.toAddress,
tx.satoshis,
self.config.exchanges.settings.transactionFee,
function(err, txHash) {
if (err) {
var status = err.name === 'InsufficientFunds' ?
'insufficientFunds' :
'failed';
self.db.reportTransactionError(tx, err.message, status);
return cb(err);
}
self.db.completeTransaction(tx, txHash);
self.pollBalance();
cb(null, txHash);
}
);
}
// Out of bitcoins: special case
var txErr = null;
if (txRec.err) {
txErr = new Error(txRec.err);
if (txRec.status === 'insufficientFunds') txErr.name = 'InsufficientFunds';
}
// transaction exists, but txHash might be null,
// in which case ATM should continue polling
self.pollBalance();
cb(txErr, txRec.txHash);
});
};
Trader.prototype.deviceEvent = function deviceEvent(rec, deviceFingerprint) {
this.db.recordDeviceEvent(deviceFingerprint, rec, function (err) {
if (err) logger.error(err);
});
};
Trader.prototype.trade = function (rec, deviceFingerprint) {
this.db.recordBill(deviceFingerprint, rec, function (err) {
if (err) logger.error(err);
});
// This is where we record starting trade balance at the beginning
// of the user session
var sessionInfo = this._sessionInfo[deviceFingerprint];
var self = this;
if (!sessionInfo) {
this._sessionInfo[deviceFingerprint] = {
tradeBalance: this.balance.tradeBalance,
timestamp: Date.now(),
reaper: setTimeout(function () {
delete self._sessionInfo[deviceFingerprint];
}, SESSION_TIMEOUT)
};
}
this._tradeQueue.push({
satoshis: rec.satoshis,
currency: rec.currency
});
};
Trader.prototype.executeTrades = function () {
if (!this.tradeExchange) return;
logger.debug('checking for trades');
var trade = this._consolidateTrades();
logger.debug('consolidated: ', JSON.stringify(trade));
if (trade.satoshis === 0) {
logger.debug('rejecting 0 trade');
return;
}
logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR);
this._purchase(trade, function (err) {
if (err) logger.error(err);
});
};
Trader.prototype.startPolling = function () {
this.executeTrades();
this.balanceInterval = setInterval(this.pollBalance.bind(this), 60 * 1000);
this.rateInterval = setInterval(this.pollRate.bind(this), 60 * 1000);
// Always start trading, even if we don't have a trade exchange configured,
// since configuration can always change in `Trader#configure`.
// `Trader#executeTrades` returns early if we don't have a trade exchange
// configured at the moment.
this.tradeInterval = setInterval(
this.executeTrades.bind(this),
this.config.exchanges.settings.tradeInterval
);
};
Trader.prototype.stopPolling = function () {
clearInterval(this.balanceInterval);
clearInterval(this.rateInterval);
};
// Trade exchange could be in a different currency than Bitcoin Machine.
// For instance, trade exchange could be Bitstamp, denominated in USD,
// while Bitcoin Machine is set to ILS.
//
// We need this function to convert the trade exchange balance into the
// Bitcoin Machine denomination, in the example case: ILS.
//
// The best way to do that with available data is to take the ratio between
// the exchange rates for the Bitcoin Machine and the trade exchange.
Trader.prototype._tradeForexMultiplier = function _tradeForexMultiplier() {
var deviceCurrency = this.config.exchanges.settings.currency;
var tradeCurrency = this.tradeExchange.currency();
if (deviceCurrency === tradeCurrency)
return 1;
var deviceRate = this._deviceRate();
var tradeRate = this._tradeRate();
return deviceRate && tradeRate ?
deviceRate / tradeRate :
null;
};
Trader.prototype._tradeBalanceFunc = function _tradeBalanceFunc(callback) {
if (!this.tradeExchange) return callback(null, null);
var forexMultiplier = this._tradeForexMultiplier();
if (!forexMultiplier) return callback(new Error('Can\'t compute balance, no tickers yet.'));
this.tradeExchange.balance(function (err, localBalance) {
if (err) return callback(err);
callback(null, localBalance * forexMultiplier);
});
};
Trader.prototype.pollBalance = function (callback) {
var self = this;
logger.debug('collecting balance');
var transferBalanceFunc = this.transferExchange.balance.bind(this.transferExchange);
var tradeBalanceFunc = this._tradeBalanceFunc.bind(this);
async.parallel({
transferBalance: transferBalanceFunc,
tradeBalance: tradeBalanceFunc
}, function (err, balance) {
if (err) {
return callback && callback(err);
}
balance.timestamp = Date.now();
logger.debug('Balance update:', balance);
self.balance = balance;
return callback && callback();
});
};
Trader.prototype.pollRate = function (callback) {
var self = this;
logger.debug('polling for rates...');
var deviceCurrency = this.config.exchanges.settings.currency;
var currencies = [deviceCurrency];
if (this.tradeExchange) {
var tradeCurrency = this.tradeExchange.currency();
if (tradeCurrency !== deviceCurrency) currencies.push(tradeCurrency);
}
self.tickerExchange.ticker(currencies, function(err, resRates) {
if (err) {
logger.error(err);
return callback && callback(err);
}
logger.debug('got rates: %j', resRates);
self.rateInfo = {rates: resRates, timestamp: new Date()};
if (callback) callback();
});
};
Trader.prototype._deviceRate = function _deviceRate() {
if (!this.rateInfo) return null;
return this.rateInfo.rates[this.config.exchanges.settings.currency].rate;
};
Trader.prototype._tradeRate = function _tradeRate() {
if (!this.tradeExchange || !this.rateInfo) return null;
return this.rateInfo.rates[this.tradeExchange.currency()].rate;
};
// This is the rate in local currency to quote to the user
Trader.prototype.rate = function () {
if (!this.rateInfo) return null;
return {
rate: this._deviceRate(),
timestamp: this.rateInfo.timestamp
};
};

View file

@ -9,35 +9,34 @@
"node": "0.10.x" "node": "0.10.x"
}, },
"dependencies": { "dependencies": {
"express": "~3.4.7",
"async": "~0.2.9", "async": "~0.2.9",
"pg": "~2.11.1",
"lamassu-config": "~0.4.0",
"lamassu-bitpay": "~0.3.0",
"lamassu-bitstamp": "~0.2.0",
"lamassu-blockchain": "~0.1.0",
"bunyan": "~0.22.3", "bunyan": "~0.22.3",
"express": "~3.4.7",
"lamassu-bitcoinaverage": "~1.0.0",
"lamassu-bitcoind": "~1.0.0",
"lamassu-bitpay": "~1.0.0",
"lamassu-bitstamp": "~1.0.0",
"lamassu-blockchain": "~1.0.0",
"lamassu-coindesk": "~1.0.0",
"lamassu-config": "~0.4.0",
"lamassu-identitymind": "^1.0.1",
"minimist": "0.0.8", "minimist": "0.0.8",
"lamassu-bitcoind": "~0.1.0", "pg": "~2.11.1"
"lamassu-coindesk": "~0.2.0",
"lamassu-bitcoinaverage": "~0.2.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/lamassu/lamassu-server.git" "url": "https://github.com/lamassu/lamassu-server.git"
}, },
"devDependencies": {
"chai": "~1.8.1",
"matchdep": "~0.3.0",
"mocha": "~1.13.0",
"hock": "git+https://github.com/mmalecki/hock.git#no-http-server",
"jsonquest": "^0.2.2",
"node-uuid": "^1.4.1"
},
"bin": { "bin": {
"lamassu-server": "./bin/lamassu-server" "lamassu-server": "./bin/lamassu-server"
}, },
"scripts": { "scripts": {
"test": "mocha --recursive test" "test": "mocha --recursive test"
},
"devDependencies": {
"chai": "^1.9.1",
"lodash": "^2.4.1",
"mocha": "^1.21.4",
"mockery": "^1.4.0"
} }
} }

View file

@ -1,14 +1,14 @@
var fs = require('fs'); // var fs = require('fs');
var path = require('path'); // var path = require('path');
var https = require('https'); // var https = require('https');
var fixtures = path.join(__dirname, '..', 'fixtures'); // var fixtures = path.join(__dirname, '..', 'fixtures');
module.exports = function(handler, callback) { // module.exports = function(handler, callback) {
var server = https.createServer({ // var server = https.createServer({
key: fs.readFileSync(path.join(fixtures, 'privatekey.pem')), // key: fs.readFileSync(path.join(fixtures, 'privatekey.pem')),
cert: fs.readFileSync(path.join(fixtures, 'certificate.pem')) // cert: fs.readFileSync(path.join(fixtures, 'certificate.pem'))
}, handler); // }, handler);
server.listen(0, function() { // server.listen(0, function() {
callback(null, server); // callback(null, server);
}); // });
}; // };

136
test/index.js Normal file
View file

@ -0,0 +1,136 @@
'use strict'
var _ = require('lodash');
var should = require('chai').should();
var mockery = require('mockery');
var config = require('./mocks/config');
var CONFIG = _.cloneDeep(config);
function requireFreshConfig() {
return _.cloneDeep(CONFIG);
};
var currents = config.exchanges.plugins.current;
var walletMock = require('./mocks/wallet');
var tickerMock = require('./mocks/ticker');
var traderMock = require('./mocks/trader');
var verifierMock = require('./mocks/verifier');
mockery.registerMock('lamassu-mockWallet', walletMock);
mockery.registerMock('lamassu-mockTicker', tickerMock);
mockery.registerMock('lamassu-mockTrader', traderMock);
mockery.registerMock('lamassu-mockVerifier', verifierMock);
describe('Plugins', function() {
var plugins = null;
before(function() {
mockery.enable({
useCleanCache: true,
warnOnReplace: false,
warnOnUnregistered: false
});
plugins = require('../lib/plugins');
});
afterEach(function() {
config = requireFreshConfig();
});
it('should properly load', function() {
should.exist(plugins);
});
it('should throw when db is not provided', function() {
plugins.init.should.throw(/db.*required/);
});
it('should throw when invalid balance margin', function() {
config.exchanges.settings.lowBalanceMargin = .99;
function configurer() {
plugins.configure(config);
};
configurer.should.throw(/lowBalanceMargin/);
});
it('should throw when module is not installed', function() {
config.exchanges.plugins.current.ticker = 'inexistent-plugin';
function configurer() {
plugins.configure(config);
};
configurer.should.throw(/module.*not installed/);
});
it('should throw when used plugin has no SUPPORTED_MODULES', function() {
var tmp = tickerMock.SUPPORTED_MODULES;
delete tickerMock.SUPPORTED_MODULES;
function configurer() {
plugins.configure(config);
};
configurer.should.throw(/required.*SUPPORTED_MODULES/);
tickerMock.SUPPORTED_MODULES = tmp;
});
it('should throw when used plugin has required method missing', function() {
var tmp = tickerMock.ticker;
delete tickerMock.ticker;
function configurer() {
plugins.configure(config);
};
configurer.should.throw(/fails.*implement.*method/);
tickerMock.ticker = tmp;
});
describe('should configure all enabled plugins', function() {
var confList = {};
before(function() {
function configTest(name) {
return function config(config) {
should.exist(config);
config.should.be.an.Object;
confList[name] = config;
};
};
walletMock.config = configTest('wallet');
tickerMock.config = configTest('ticker');
traderMock.config = configTest('trader');
verifierMock.config = configTest('verifier');
plugins.configure(config);
});
['wallet', 'ticker', 'trader', 'verifier'].forEach(function(name) {
it('should configure ' + name, function() {
confList.should.have.property(name);
should.exist(confList[name]);
confList[name].should.be.an.Object;
});
});
});
this.timeout(9000);
describe('Ticker', function() {
it('should have .ticker() called at least once', function() {
tickerMock.tickerCalls.should.be.at.least(1);
});
});
});

75
test/mocks/config.json Normal file
View file

@ -0,0 +1,75 @@
{
"exchanges": {
"settings": {
"compliance": {
"maximum": {
"limit": null
}
},
"commission": 1,
"fastPoll": 5000,
"fastPollLimit": 10,
"tickerInterval": 5000,
"balanceInterval": 5000,
"tradeInterval": 5000,
"retryInterval": 5000,
"retries": 3,
"lowBalanceMargin": 1.05,
"transactionFee": 10000,
"tickerDelta": 0,
"minimumTradeFiat": 0,
"currency": "PLN",
"networkTimeout": 20000
},
"plugins": {
"current": {
"ticker": "mockTicker",
"trade": "mockTrader",
"wallet": "mockWallet",
"transfer": "mockWallet",
"idVerifier": "mockVerifier"
},
"settings": {
"bitpay": { },
"bitstamp": {
"currency": "USD",
"key": "",
"secret": "",
"clientId": ""
},
"blockchain": {
"retryInterval": 10000,
"retryTimeout": 60000,
"guid": "",
"password": "",
"fromAddress": ""
}
}
}
},
"brain": {
"qrTimeout": 60000,
"goodbyeTimeout": 2000,
"billTimeout": 60000,
"completedTimeout": 60000,
"networkTimeout": 20000,
"triggerRetry": 5000,
"idleTime": 600000,
"checkIdleTime": 60000,
"maxProcessSize": 104857600,
"freeMemRatio": 0.15,
"unit": {
"ssn": "xx-1234-45",
"owner": "Lamassu, Inc. \/ Trofa \/ Portugal"
},
"locale": {
"currency": "PLN",
"localeInfo": {
"primaryLocale": "pl-PL",
"primaryLocales": [
"pl-PL"
]
}
}
}
}

23
test/mocks/ticker.js Normal file
View file

@ -0,0 +1,23 @@
'use strict';
module.exports = {
SUPPORTED_MODULES: [ 'ticker' ],
NAME: 'Mock Ticker',
tickerCalls: 0,
config: function() {},
ticker: function(currency, callback) {
this.tickerCalls++;
var out = {};
out[currency] = {
currency: currency,
rates: {
ask: 1001.0,
bid: 999.0
}
};
callback(null, out);
}
};

11
test/mocks/trader.js Normal file
View file

@ -0,0 +1,11 @@
'use strict';
module.exports = {
SUPPORTED_MODULES: ['trader'],
NAME: 'Mock Trader',
config: function() {},
balance: function() {},
purchase: function() {},
sell: function() {}
};

10
test/mocks/verifier.js Normal file
View file

@ -0,0 +1,10 @@
'use strict';
module.exports = {
SUPPORTED_MODULES: ['idVerifier'],
NAME: 'Mock Verifier',
config: function() {},
verifyUser: function() {},
verifyTransaction: function() {}
};

10
test/mocks/wallet.js Normal file
View file

@ -0,0 +1,10 @@
'use strict';
module.exports = {
SUPPORTED_MODULES: ['wallet'],
NAME: 'Mock Wallet',
config: function() {},
balance: function() {},
sendBitcoins: function() {}
};

View file

@ -1,54 +1,54 @@
'use strict'; // 'use strict';
var assert = require('chai').assert; // var assert = require('chai').assert;
var Trader = require('../../lib/trader.js'); // var Trader = require('../../lib/trader.js');
var PostgresqlInterface = require('../../lib/postgresql_interface.js'); // var PostgresqlInterface = require('../../lib/postgresql_interface.js');
var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; // var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
var psqlInterface = new PostgresqlInterface(db); // var psqlInterface = new PostgresqlInterface(db);
describe('trader/api', function () { // describe('trader/api', function () {
it('should throw when trying to create a trader with no DB', function () { // it('should throw when trying to create a trader with no DB', function () {
assert.throws(function () { // assert.throws(function () {
new Trader(); // new Trader();
}); // });
}); // });
it('should throw when trying to configure a trader with `lowBalanceMargin` < 1', function () { // it('should throw when trying to configure a trader with `lowBalanceMargin` < 1', function () {
var trader = new Trader(psqlInterface); // var trader = new Trader(psqlInterface);
assert.throws(function () { // assert.throws(function () {
trader.configure({ // trader.configure({
exchanges: { // exchanges: {
settings: { // settings: {
lowBalanceMargin: 0.8 // lowBalanceMargin: 0.8
} // }
} // }
}); // });
}); // });
}); // });
it('should find and instantiate ticker and trade exchanges', function () { // it('should find and instantiate ticker and trade exchanges', function () {
var trader = new Trader(psqlInterface); // var trader = new Trader(psqlInterface);
trader.configure({ // trader.configure({
exchanges: { // exchanges: {
plugins: { // plugins: {
current: { // current: {
ticker: 'bitpay', // ticker: 'bitpay',
transfer: 'blockchain' // transfer: 'blockchain'
}, // },
settings: { // settings: {
bitpay: {}, // bitpay: {},
blockchain: {} // blockchain: {}
} // }
}, // },
settings: { // settings: {
currency: 'USD', // currency: 'USD',
lowBalanceMargin: 2 // lowBalanceMargin: 2
} // }
} // }
}); // });
assert.ok(trader.tickerExchange); // assert.ok(trader.tickerExchange);
assert.ok(trader.transferExchange); // assert.ok(trader.transferExchange);
}); // });
}); // });

View file

@ -1,107 +1,107 @@
/*global describe, it */ // /*global describe, it */
'use strict'; // 'use strict';
var assert = require('chai').assert; // var assert = require('chai').assert;
var Trader = require('../../lib/trader.js'); // var Trader = require('../../lib/trader.js');
var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; // var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
var RATE = 101; // var RATE = 101;
var CURRENCY = 'USD'; // var CURRENCY = 'USD';
var SATOSHI_FACTOR = 1e8; // var SATOSHI_FACTOR = 1e8;
var LOW_BALANCE_MARGIN = 1.2; // var LOW_BALANCE_MARGIN = 1.2;
var COMMISSION = 1.1; // var COMMISSION = 1.1;
var FINGERPRINT = '00:7A:5A:B3:02:F1:44:46:E2:EA:24:D3:A8:29:DE:22:BA:1B:F9:50'; // var FINGERPRINT = '00:7A:5A:B3:02:F1:44:46:E2:EA:24:D3:A8:29:DE:22:BA:1B:F9:50';
var settings = { // var settings = {
currency: CURRENCY, // currency: CURRENCY,
lowBalanceMargin: LOW_BALANCE_MARGIN, // lowBalanceMargin: LOW_BALANCE_MARGIN,
commission: COMMISSION // commission: COMMISSION
}; // };
describe('trader/fiatBalance', function() { // describe('trader/fiatBalance', function() {
it('should calculate balance correctly with transfer exchange only', function() { // it('should calculate balance correctly with transfer exchange only', function() {
var trader = new Trader(db); // var trader = new Trader(db);
trader.configure({ // trader.configure({
exchanges: { // exchanges: {
plugins: { // plugins: {
current: { // current: {
transfer: 'blockchain', // transfer: 'blockchain',
ticker: 'bitpay' // ticker: 'bitpay'
}, // },
settings: { blockchain: {}, bitpay: {} } // settings: { blockchain: {}, bitpay: {} }
}, // },
settings: settings // settings: settings
} // }
}); // });
// We have 3 bitcoins, want to trade 1 bitcoin for 100 fiat // // We have 3 bitcoins, want to trade 1 bitcoin for 100 fiat
trader.balance = { // trader.balance = {
transferBalance: 3 * SATOSHI_FACTOR, // transferBalance: 3 * SATOSHI_FACTOR,
tradeBalance: null // tradeBalance: null
}; // };
trader.rates[CURRENCY] = { rate: RATE }; // trader.rates[CURRENCY] = { rate: RATE };
trader.rateInfo = {rates: {USD: {rate: RATE}}}; // trader.rateInfo = {rates: {USD: {rate: RATE}}};
var fiatBalance = trader.fiatBalance(FINGERPRINT); // var fiatBalance = trader.fiatBalance(FINGERPRINT);
assert.equal(fiatBalance, (3 * RATE * COMMISSION / LOW_BALANCE_MARGIN)); // assert.equal(fiatBalance, (3 * RATE * COMMISSION / LOW_BALANCE_MARGIN));
}); // });
it('should calculate balance correctly with transfer and trade exchange', function() { // it('should calculate balance correctly with transfer and trade exchange', function() {
var trader = new Trader(db); // var trader = new Trader(db);
trader.configure({ // trader.configure({
exchanges: { // exchanges: {
plugins: { // plugins: {
current: { // current: {
transfer: 'blockchain', // transfer: 'blockchain',
ticker: 'bitpay', // ticker: 'bitpay',
trade: 'bitstamp' // trade: 'bitstamp'
}, // },
settings: { blockchain: {}, bitpay: {}, bitstamp: {} } // settings: { blockchain: {}, bitpay: {}, bitstamp: {} }
}, // },
settings: settings // settings: settings
} // }
}); // });
// We have 3 bitcoins in transfer, worth 3 * RATE * COMMISSION = 333.3 // // We have 3 bitcoins in transfer, worth 3 * RATE * COMMISSION = 333.3
// We have 150 USD in trade // // We have 150 USD in trade
trader.balance = { // trader.balance = {
transferBalance: 3 * SATOSHI_FACTOR, // transferBalance: 3 * SATOSHI_FACTOR,
tradeBalance: 150 // tradeBalance: 150
}; // };
trader.rates[CURRENCY] = { rate: RATE }; // trader.rates[CURRENCY] = { rate: RATE };
trader.rateInfo = {rates: {USD: {rate: RATE}}}; // trader.rateInfo = {rates: {USD: {rate: RATE}}};
var fiatBalance = trader.fiatBalance(FINGERPRINT); // var fiatBalance = trader.fiatBalance(FINGERPRINT);
assert.equal(fiatBalance, 150 / LOW_BALANCE_MARGIN); // assert.equal(fiatBalance, 150 / LOW_BALANCE_MARGIN);
}); // });
it('should calculate balance correctly with transfer and ' + // it('should calculate balance correctly with transfer and ' +
'trade exchange with different currencies', function() { // 'trade exchange with different currencies', function() {
var trader = new Trader(db); // var trader = new Trader(db);
trader.configure({ // trader.configure({
exchanges: { // exchanges: {
plugins: { // plugins: {
current: { // current: {
transfer: 'blockchain', // transfer: 'blockchain',
ticker: 'bitpay', // ticker: 'bitpay',
trade: 'bitstamp' // trade: 'bitstamp'
}, // },
settings: { blockchain: {}, bitpay: {}, bitstamp: {} } // settings: { blockchain: {}, bitpay: {}, bitstamp: {} }
}, // },
settings: settings // settings: settings
} // }
}); // });
// We have 6 bitcoins in transfer, worth 6 * RATE * COMMISSION = 666.6 // // We have 6 bitcoins in transfer, worth 6 * RATE * COMMISSION = 666.6
// We have 150 USD in trade, 1 USD = 4 ILS => 600 ILS in trade // // We have 150 USD in trade, 1 USD = 4 ILS => 600 ILS in trade
trader.balance = { // trader.balance = {
transferBalance: 6 * SATOSHI_FACTOR, // transferBalance: 6 * SATOSHI_FACTOR,
tradeBalance: 600 // tradeBalance: 600
}; // };
trader.rates = {USD: {rate: RATE}, ILS: {rate: RATE * 4} }; // trader.rates = {USD: {rate: RATE}, ILS: {rate: RATE * 4} };
trader.rateInfo = {rates: {USD: {rate: RATE}}}; // trader.rateInfo = {rates: {USD: {rate: RATE}}};
var fiatBalance = trader.fiatBalance(FINGERPRINT); // var fiatBalance = trader.fiatBalance(FINGERPRINT);
assert.equal(fiatBalance, 600 / LOW_BALANCE_MARGIN); // assert.equal(fiatBalance, 600 / LOW_BALANCE_MARGIN);
}); // });
}); // });

View file

@ -1,77 +1,77 @@
'use strict'; // 'use strict';
var assert = require('chai').assert; // var assert = require('chai').assert;
var hock = require('hock'); // var hock = require('hock');
var uuid = require('node-uuid').v4; // var uuid = require('node-uuid').v4;
var Trader = require('../../lib/trader.js'); // var Trader = require('../../lib/trader.js');
var PostgresqlInterface = require('../../lib/postgresql_interface.js'); // var PostgresqlInterface = require('../../lib/postgresql_interface.js');
var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; // var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
var psqlInterface = new PostgresqlInterface(db); // var psqlInterface = new PostgresqlInterface(db);
var TRANSACTION_FEE = 1; // 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 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 TXID = '216dabdb692670bae940deb71e59486038a575f637903d3c9af601ddd48057fc';
var ADDRESS = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64'; // var ADDRESS = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64';
var SATOSHIS = 1337; // var SATOSHIS = 1337;
var CURRENCY = 'USD'; // var CURRENCY = 'USD';
var OUR_TXID = uuid(); // var OUR_TXID = uuid();
describe('trader/send', function () { // describe('trader/send', function () {
var trader = new Trader(psqlInterface); // var trader = new Trader(psqlInterface);
trader.config = { // trader.config = {
exchanges: { // exchanges: {
settings: { // settings: {
transactionFee: TRANSACTION_FEE // transactionFee: TRANSACTION_FEE
} // }
} // }
}; // };
trader.pollRate = function () {}; // trader.pollRate = function () {};
it('should call `sendBitcoins` on the transfer exchange', function (done) { // it('should call `sendBitcoins` on the transfer exchange', function (done) {
trader.transferExchange = { // trader.transferExchange = {
sendBitcoins: function (address, satoshis, transactionFee, callback) { // sendBitcoins: function (address, satoshis, transactionFee, callback) {
assert.equal(ADDRESS, address); // assert.equal(ADDRESS, address);
assert.equal(SATOSHIS, satoshis); // assert.equal(SATOSHIS, satoshis);
assert.equal(transactionFee, TRANSACTION_FEE); // assert.equal(transactionFee, TRANSACTION_FEE);
callback(null, TXID); // callback(null, TXID);
}, // },
balance: function () {} // balance: function () {}
}; // };
trader.sendBitcoins(FINGERPRINT, { // trader.sendBitcoins(FINGERPRINT, {
fiat: 100, // fiat: 100,
txId: OUR_TXID, // txId: OUR_TXID,
currencyCode: CURRENCY, // currencyCode: CURRENCY,
toAddress: ADDRESS, // toAddress: ADDRESS,
satoshis: SATOSHIS // satoshis: SATOSHIS
}, function (err, txId) { // }, function (err, txId) {
assert.notOk(err); // assert.notOk(err);
assert.equal(txId, TXID); // assert.equal(txId, TXID);
done(); // done();
}); // });
}); // });
it('should not call `sendBitcoins` on the transfer exchange with same send', function (done) { // it('should not call `sendBitcoins` on the transfer exchange with same send', function (done) {
trader.transferExchange = { // trader.transferExchange = {
sendBitcoins: function () { // sendBitcoins: function () {
throw new Error('This should not have been called'); // throw new Error('This should not have been called');
}, // },
balance: function () {} // balance: function () {}
}; // };
trader.sendBitcoins(FINGERPRINT, { // trader.sendBitcoins(FINGERPRINT, {
fiat: 100, // fiat: 100,
txId: OUR_TXID, // txId: OUR_TXID,
currencyCode: CURRENCY, // currencyCode: CURRENCY,
toAddress: ADDRESS, // toAddress: ADDRESS,
satoshis: SATOSHIS // satoshis: SATOSHIS
}, function (err, txId) { // }, function (err, txId) {
assert.notOk(err); // assert.notOk(err);
assert.equal(txId, TXID); // assert.equal(txId, TXID);
done(); // done();
}); // });
}); // });
}); // });

View file

@ -1,52 +1,52 @@
/*global describe, it */ // /*global describe, it */
'use strict'; // 'use strict';
var assert = require('chai').assert; // var assert = require('chai').assert;
var Trader = require('../../lib/trader.js'); // var Trader = require('../../lib/trader.js');
var PostgresqlInterface = require('../../lib/postgresql_interface.js'); // var PostgresqlInterface = require('../../lib/postgresql_interface.js');
var db = 'psql://lamassu:lamassu@localhost/lamassu-test'; // var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
var psqlInterface = new PostgresqlInterface(db); // var psqlInterface = new PostgresqlInterface(db);
var CURRENCY = 'USD'; // var CURRENCY = 'USD';
describe('trader/send', function () { // describe('trader/send', function () {
var trader = new Trader(psqlInterface); // var trader = new Trader(psqlInterface);
trader.config = { // trader.config = {
exchanges: { // exchanges: {
settings: { currency: CURRENCY } // settings: { currency: CURRENCY }
} // }
}; // };
it('should call `balance` on the transfer exchange', function (done) { // it('should call `balance` on the transfer exchange', function (done) {
trader.transferExchange = { // trader.transferExchange = {
balance: function (callback) { // balance: function (callback) {
callback(null, 100); // callback(null, 100);
} // }
}; // };
trader.pollBalance(function (err) { // trader.pollBalance(function (err) {
assert.notOk(err); // assert.notOk(err);
assert.equal(trader.balance.transferBalance, 100); // assert.equal(trader.balance.transferBalance, 100);
assert.ok(trader.balance.timestamp); // assert.ok(trader.balance.timestamp);
done(); // done();
}); // });
}); // });
it('should call `ticker` on the ticker exchange', function (done) { // it('should call `ticker` on the ticker exchange', function (done) {
trader.tickerExchange = { // trader.tickerExchange = {
ticker: function (currencies, callback) { // ticker: function (currencies, callback) {
assert.equal(currencies[0], CURRENCY); // assert.equal(currencies[0], CURRENCY);
callback(null, {USD: {rate: 100}}); // callback(null, {USD: {rate: 100}});
} // }
}; // };
trader.pollRate(function (err) { // trader.pollRate(function (err) {
assert.notOk(err); // assert.notOk(err);
var rate = trader.rate(CURRENCY); // var rate = trader.rate(CURRENCY);
assert.equal(rate.rate, 100); // assert.equal(rate.rate, 100);
assert.ok(rate.timestamp); // assert.ok(rate.timestamp);
done(); // done();
}); // });
}); // });
}); // });