chore(all) merge in from canary
This commit is contained in:
commit
dca000abc3
16 changed files with 983 additions and 689 deletions
14
lib/app.js
14
lib/app.js
|
|
@ -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
401
lib/plugins.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
@ -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});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,32 +72,34 @@ function deviceEvent(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyUser(req, res) {
|
function verifyUser(req, res) {
|
||||||
if (_mock) return res.json({success: true});
|
if (mock) return res.json({success: true});
|
||||||
|
|
||||||
_idVerifier.verifyUser(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);
|
res.json(idResult);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyTransaction(req, res) {
|
function verifyTransaction(req, res) {
|
||||||
if (_mock) return res.json({success: true});
|
if (mock) return res.json({success: true});
|
||||||
|
|
||||||
_idVerifier.verifyTransaction(req.body, function (err, idResult) {
|
plugins.verifyTransaction(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);
|
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,
|
||||||
|
|
@ -111,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,
|
||||||
|
|
@ -125,18 +126,13 @@ 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');
|
|
||||||
_idVerifier.config(idVerifyConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/poll', authMiddleware, poll);
|
app.get('/poll', authMiddleware, poll);
|
||||||
app.post('/send', authMiddleware, send);
|
app.post('/send', authMiddleware, send);
|
||||||
|
|
|
||||||
367
lib/trader.js
367
lib/trader.js
|
|
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
33
package.json
33
package.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
136
test/index.js
Normal 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
75
test/mocks/config.json
Normal 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
23
test/mocks/ticker.js
Normal 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
11
test/mocks/trader.js
Normal 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
10
test/mocks/verifier.js
Normal 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
10
test/mocks/wallet.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SUPPORTED_MODULES: ['wallet'],
|
||||||
|
NAME: 'Mock Wallet',
|
||||||
|
|
||||||
|
config: function() {},
|
||||||
|
balance: function() {},
|
||||||
|
sendBitcoins: function() {}
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
// });
|
||||||
|
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue