refactor(plugins): DO NOT MERGE server-side support for new plugins API added
This commit is contained in:
parent
bf6b5c0958
commit
308cd76656
4 changed files with 391 additions and 402 deletions
14
lib/app.js
14
lib/app.js
|
|
@ -5,7 +5,7 @@ var https = require('https');
|
|||
var express = require('express');
|
||||
var LamassuConfig = require('lamassu-config');
|
||||
var routes = require('./routes');
|
||||
var Trader = require('./trader');
|
||||
var tmpName = require('./tmpName');
|
||||
var PostgresqlInterface = require('./postgresql_interface');
|
||||
var logger = require('./logger');
|
||||
|
||||
|
|
@ -14,7 +14,6 @@ module.exports = function (options) {
|
|||
var connectionString;
|
||||
var server;
|
||||
var config;
|
||||
var trader;
|
||||
var db;
|
||||
|
||||
connectionString = options.postgres ||
|
||||
|
|
@ -22,7 +21,8 @@ module.exports = function (options) {
|
|||
|
||||
config = new LamassuConfig(connectionString);
|
||||
db = new PostgresqlInterface(connectionString);
|
||||
trader = new Trader(db);
|
||||
tmpName.init(db);
|
||||
|
||||
|
||||
config.load(function (err, config) {
|
||||
if (err) {
|
||||
|
|
@ -30,8 +30,8 @@ module.exports = function (options) {
|
|||
throw err;
|
||||
}
|
||||
|
||||
trader.configure(config);
|
||||
trader.startPolling();
|
||||
tmpName.configure(config);
|
||||
tmpName.startPolling();
|
||||
});
|
||||
|
||||
config.on('configUpdate', function () {
|
||||
|
|
@ -40,7 +40,7 @@ module.exports = function (options) {
|
|||
return logger.error('Error while reloading config');
|
||||
}
|
||||
|
||||
trader.configure(config);
|
||||
tmpName.configure(config);
|
||||
logger.info('Config reloaded');
|
||||
});
|
||||
});
|
||||
|
|
@ -89,7 +89,7 @@ module.exports = function (options) {
|
|||
routes.init({
|
||||
app: app,
|
||||
lamassuConfig: config,
|
||||
trader: trader,
|
||||
tmpName: tmpName,
|
||||
authMiddleware: authMiddleware,
|
||||
mock: options.mock
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
var _trader;
|
||||
var _lamassuConfig;
|
||||
var _idVerifier = null;
|
||||
var _trader = null;
|
||||
var _mock = false;
|
||||
var logger = require('./logger');
|
||||
|
||||
var idVerifier = null;
|
||||
var mock = false;
|
||||
|
||||
var tmpName;
|
||||
var config;
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
getFingerprint: getFingerprint
|
||||
|
|
@ -18,8 +19,8 @@ var STALE_TICKER = 180000;
|
|||
var STALE_BALANCE = 180000;
|
||||
|
||||
function poll(req, res) {
|
||||
var rateRec = _trader.rate();
|
||||
var balanceRec = _trader.balance;
|
||||
var rateRec = tmpName.getDeviceRate();
|
||||
var balanceRec = tmpName.getBalance();
|
||||
var fingerprint = getFingerprint(req);
|
||||
|
||||
logger.debug('poll request from: %s', fingerprint);
|
||||
|
|
@ -39,9 +40,9 @@ function poll(req, res) {
|
|||
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'});
|
||||
var fiatBalance = _trader.fiatBalance(fingerprint);
|
||||
var fiatBalance = tmpName.fiatBalance(fingerprint);
|
||||
if (fiatBalance === null) return res.json({err: 'No balance available'});
|
||||
|
||||
var idVerificationLimit = _trader.config.exchanges.settings.
|
||||
|
|
@ -51,18 +52,17 @@ function poll(req, res) {
|
|||
|
||||
res.json({
|
||||
err: null,
|
||||
rate: rate * _trader.config.exchanges.settings.commission,
|
||||
rate: rate * config.exchanges.settings.commission,
|
||||
fiat: fiatBalance,
|
||||
locale: _trader.config.brain.locale,
|
||||
txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10),
|
||||
locale: config.brain.locale,
|
||||
txLimit: parseInt(config.exchanges.settings.compliance.maximum.limit, 10),
|
||||
idVerificationLimit: idVerificationLimit
|
||||
});
|
||||
}
|
||||
|
||||
function trade(req, res) {
|
||||
var fingerprint = getFingerprint(req);
|
||||
_trader.trade(req.body, fingerprint);
|
||||
|
||||
tmpName.trade(req.body, fingerprint);
|
||||
res.json({err: null});
|
||||
}
|
||||
|
||||
|
|
@ -73,9 +73,9 @@ function deviceEvent(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) {
|
||||
idVerifier.verifyUser(req.body, function (err, idResult) {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
return res.json({err: 'Verification failed'});
|
||||
|
|
@ -87,7 +87,7 @@ function verifyUser(req, res) {
|
|||
function verifyTransaction(req, res) {
|
||||
if (_mock) return res.json({success: true});
|
||||
|
||||
_idVerifier.verifyTransaction(req.body, function (err, idResult) {
|
||||
idVerifier.verifyTransaction(req.body, function (err, idResult) {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
return res.json({err: 'Verification failed'});
|
||||
|
|
@ -98,7 +98,7 @@ function verifyTransaction(req, res) {
|
|||
|
||||
function send(req, res) {
|
||||
var fingerprint = getFingerprint(req);
|
||||
_trader.sendBitcoins(fingerprint, req.body, function(err, txHash) {
|
||||
tmpName.sendBitcoins(fingerprint, req.body, function(err, txHash) {
|
||||
res.json({
|
||||
err: err && err.message,
|
||||
txHash: txHash,
|
||||
|
|
@ -111,7 +111,7 @@ function pair(req, res) {
|
|||
var token = req.body.token;
|
||||
var name = req.body.name;
|
||||
|
||||
_lamassuConfig.pair(
|
||||
config.pair(
|
||||
token,
|
||||
getFingerprint(req),
|
||||
name,
|
||||
|
|
@ -125,17 +125,17 @@ function pair(req, res) {
|
|||
);
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
_lamassuConfig = config.lamassuConfig;
|
||||
_trader = config.trader;
|
||||
_mock = config.mock;
|
||||
function init(localConfig) {
|
||||
config = localConfig.lamassuConfig;
|
||||
tmpName = localConfig.tmpName;
|
||||
mock = localConfig.mock;
|
||||
|
||||
var authMiddleware = config.authMiddleware;
|
||||
var app = config.app;
|
||||
_lamassuConfig.readExchangesConfig(function (err, res) {
|
||||
var authMiddleware = localConfig.authMiddleware;
|
||||
var app = localConfig.app;
|
||||
config.readExchangesConfig(function (err, res) {
|
||||
var idVerifyConfig = res.exchanges.plugins.settings.identitymind;
|
||||
_idVerifier = require('lamassu-identitymind');
|
||||
_idVerifier.init(idVerifyConfig);
|
||||
idVerifier = require('lamassu-identitymind');
|
||||
idVerifier.init(idVerifyConfig);
|
||||
});
|
||||
|
||||
app.get('/poll', authMiddleware, poll);
|
||||
|
|
|
|||
356
lib/tmpName.js
Normal file
356
lib/tmpName.js
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
'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 cachedConfig = null;
|
||||
var deviceCurrency = 'USD'; // Can 'USD' it 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) {
|
||||
var moduleMethods = {
|
||||
ticker: [ 'ticker' ],
|
||||
trader: [ 'balance', 'purchase', 'sell' ],
|
||||
wallet: [ 'balance', 'sendBitcoins' ]
|
||||
};
|
||||
|
||||
var plugin = null;
|
||||
|
||||
try {
|
||||
plugin = require('lamassu-' + name);
|
||||
|
||||
} catch(_) {
|
||||
throw new Error(name + ' module is not installed. Try running \'npm install --save lamassu-' + name + '\' first');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (config !== null) {
|
||||
plugin.config(config);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
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
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue