This commit is contained in:
Josh Harvey 2014-11-20 23:01:36 -05:00
parent 0cec3670a9
commit e12238c4fe
4 changed files with 163 additions and 73 deletions

66
.jscsrc Normal file
View file

@ -0,0 +1,66 @@
{
"requireCurlyBraces": [
"for",
"while",
"do",
"try",
"catch"
],
"requireOperatorBeforeLineBreak": true,
"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties",
"maximumLineLength": {
"value": 80,
"allowComments": true,
"allowRegex": true
},
"validateIndentation": 2,
"validateQuoteMarks": "'",
"disallowMultipleLineStrings": true,
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"disallowSpaceAfterPrefixUnaryOperators": true,
"disallowMultipleVarDecl": true,
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"return",
"try",
"catch"
],
"requireSpaceBeforeBinaryOperators": [
"=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=",
"&=", "|=", "^=", "+=",
"+", "-", "*", "/", "%", "<<", ">>", ">>>", "&",
"|", "^", "&&", "||", "===", "==", ">=",
"<=", "<", ">", "!=", "!=="
],
"requireSpaceAfterBinaryOperators": true,
"requireSpacesInConditionalExpression": true,
"requireSpaceBeforeBlockStatements": true,
"requireLineFeedAtFileEnd": true,
"requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"disallowSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideObjectBrackets": "all",
"disallowSpacesInsideArrayBrackets": "all",
"disallowSpacesInsideParentheses": true,
"validateJSDoc": {
"checkParamNames": true,
"requireParamTypes": true
},
"disallowMultipleLineBreaks": true,
"disallowNewlineBeforeBlockStatements": true
}

View file

@ -18,7 +18,6 @@ var infoPlugin = null;
var currentlyUsedPlugins = {}; var currentlyUsedPlugins = {};
var cachedConfig = null; var cachedConfig = null;
var deviceCurrency = 'USD'; var deviceCurrency = 'USD';
@ -33,7 +32,6 @@ var reapTxInterval = null;
var tradesQueue = []; var tradesQueue = [];
var dispenseStatuses = {}; var dispenseStatuses = {};
// that's basically a constructor // that's basically a constructor
exports.init = function init(databaseHandle) { exports.init = function init(databaseHandle) {
if (!databaseHandle) { if (!databaseHandle) {
@ -43,16 +41,15 @@ exports.init = function init(databaseHandle) {
db = databaseHandle; db = databaseHandle;
}; };
function loadPlugin(name, config) { function loadPlugin(name, config) {
// plugins definitions // plugins definitions
var moduleMethods = { var moduleMethods = {
ticker: [ 'ticker' ], ticker: ['ticker'],
trader: [ 'balance', 'purchase', 'sell' ], trader: ['balance', 'purchase', 'sell'],
wallet: [ 'balance', 'sendBitcoins', 'newAddress' ], wallet: ['balance', 'sendBitcoins', 'newAddress'],
idVerifier: [ 'verifyUser', 'verifyTransaction' ], idVerifier: ['verifyUser', 'verifyTransaction'],
info: [ 'getAddressLastTx', 'getTx' ] info: ['getAddressLastTx', 'getTx']
}; };
var plugin = null; var plugin = null;
@ -60,67 +57,72 @@ function loadPlugin(name, config) {
// each used plugin MUST be installed // each used plugin MUST be installed
try { try {
plugin = require('lamassu-' + name); plugin = require('lamassu-' + name);
} catch (_) {
} catch(_) { throw new Error(name + ' module is not installed. ' +
throw new Error(name + ' module is not installed. Try running \'npm install --save lamassu-' + name + '\' first'); 'Try running \'npm install --save lamassu-' + name + '\' first');
} }
// each plugin MUST implement those // each plugin MUST implement those
if (typeof plugin.SUPPORTED_MODULES !== 'undefined') { if (typeof plugin.SUPPORTED_MODULES !== 'undefined') {
if(plugin.SUPPORTED_MODULES === 'string') if (plugin.SUPPORTED_MODULES === 'string')
plugin.SUPPORTED_MODULES = [plugin.SUPPORTED_MODULES]; plugin.SUPPORTED_MODULES = [plugin.SUPPORTED_MODULES];
} }
if(!(plugin.SUPPORTED_MODULES instanceof Array)) if (!(plugin.SUPPORTED_MODULES instanceof Array))
throw new Error('\'' + name + '\' fails to implement *required* \'SUPPORTED_MODULES\' constant'); throw new Error('\'' + name + '\' fails to implement *required* ' +
'\'SUPPORTED_MODULES\' constant');
plugin.SUPPORTED_MODULES.forEach(function(moduleName) { plugin.SUPPORTED_MODULES.forEach(function(moduleName) {
moduleMethods[moduleName].forEach(function(methodName) { moduleMethods[moduleName].forEach(function(methodName) {
if (typeof plugin[methodName] !== 'function') { if (typeof plugin[methodName] !== 'function') {
throw new Error('\'' + name + '\' declares \'' + moduleName + '\', but fails to implement \'' + methodName + '\' method'); throw new Error('\'' + name + '\' declares \'' + moduleName +
'\', but fails to implement \'' + methodName + '\' method');
} }
}); });
}); });
// each plugin SHOULD implement those // each plugin SHOULD implement those
if (typeof plugin.NAME === 'undefined') if (typeof plugin.NAME === 'undefined')
logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'NAME\' field')); logger.warn(new Error('\'' + name +
'\' fails to implement *recommended* \'NAME\' field'));
if (typeof plugin.config !== 'function') { if (typeof plugin.config !== 'function') {
logger.warn(new Error('\'' + name + '\' fails to implement *recommended* \'config\' method')); logger.warn(new Error('\'' + name +
'\' fails to implement *recommended* \'config\' method'));
plugin.config = function() {}; plugin.config = function() {};
} else if (config !== null) } else if (config !== null) {
plugin.config(config); // only when plugin supports it, and config is passed plugin.config(config); // only when plugin supports it, and config is passed
}
return plugin; return plugin;
} }
function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCallback) { function loadOrConfigPlugin(pluginHandle, pluginType, currency,
onChangeCallback) {
var currentName = cachedConfig.exchanges.plugins.current[pluginType]; var currentName = cachedConfig.exchanges.plugins.current[pluginType];
var pluginChanged = currentlyUsedPlugins[pluginType] !== currentName; var pluginChanged = currentlyUsedPlugins[pluginType] !== currentName;
if (!currentName) pluginHandle = null; if (!currentName) pluginHandle = null;
else { // some plugins may be disabled else { // some plugins may be disabled
var pluginConfig = cachedConfig.exchanges.plugins.settings[currentName] || {}; var pluginConfig = cachedConfig.exchanges.plugins.settings[currentName] ||
{};
if (currency) pluginConfig.currency = currency; if (currency) pluginConfig.currency = currency;
if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig); if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig);
else { else {
pluginHandle = loadPlugin(currentName, pluginConfig); pluginHandle = loadPlugin(currentName, pluginConfig);
logger.debug('plugin(%s) loaded: %s', pluginType, pluginHandle.NAME || currentName); logger.debug('plugin(%s) loaded: %s', pluginType, pluginHandle.NAME ||
currentName);
} }
} }
if (typeof onChangeCallback === 'function') onChangeCallback(pluginHandle, currency); if (typeof onChangeCallback === 'function')
onChangeCallback(pluginHandle, currency);
return pluginHandle; return pluginHandle;
} }
exports.configure = function configure(config) { exports.configure = function configure(config) {
if (config.exchanges.settings.lowBalanceMargin < 1) { if (config.exchanges.settings.lowBalanceMargin < 1) {
throw new Error('\'settings.lowBalanceMargin\' has to be >= 1'); throw new Error('\'settings.lowBalanceMargin\' has to be >= 1');
@ -217,9 +219,10 @@ function reapOutgoingTx(deviceFingerprint, tx) {
} }
function reapIncomingTx(deviceFingerprint, tx) { function reapIncomingTx(deviceFingerprint, tx) {
infoPlugin.checkAddress(tx.toAddress, function(err, status, satoshisReceived) { infoPlugin.checkAddress(tx.toAddress, function(err, status,
satoshisReceived) {
if (status === 'notSeen') return; if (status === 'notSeen') return;
db.addIngoingTx(deviceFingerprint, tx, status, satoshisReceived); db.addIncomingTx(deviceFingerprint, tx, status, satoshisReceived);
}); });
} }
@ -277,7 +280,6 @@ exports.sendBitcoins = function sendBitcoins(deviceFingerprint, rawTx, cb) {
executeTx(deviceFingerprint, rawTx, cb); executeTx(deviceFingerprint, rawTx, cb);
}; };
// sets given status both "locally" (dispenseStatuses) and saves to db // sets given status both "locally" (dispenseStatuses) and saves to db
function _setDispenseStatus(deviceFingerprint, tx, status, deposit) { function _setDispenseStatus(deviceFingerprint, tx, status, deposit) {
tx.status = status; tx.status = status;
@ -313,14 +315,10 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) {
account: 'deposit' account: 'deposit'
}; };
walletPlugin.newAddress(tmpInfo, function(err, address) { walletPlugin.newAddress(tmpInfo, function(err, address) {
if (err) if (err) return cb(err);
return cb(new Error(err));
tx.toAddress = address; db.addInitialIncoming(deviceFingerprint, tx, address, function(_err) {
tx.incoming = true; cb(_err, address);
db.addPendingTx(deviceFingerprint, tx, function(err) {
cb(err, address);
}); });
}); });
}; };
@ -333,7 +331,6 @@ exports.dispenseStatus = function dispenseStatus(deviceFingerprint) {
return dispenseStatuses[deviceFingerprint]; return dispenseStatuses[deviceFingerprint];
}; };
exports.fiatBalance = function fiatBalance() { exports.fiatBalance = function fiatBalance() {
var rawRate = exports.getDeviceRate().rates.ask; var rawRate = exports.getDeviceRate().rates.ask;
var commission = cachedConfig.exchanges.settings.commission; var commission = cachedConfig.exchanges.settings.commission;
@ -357,12 +354,12 @@ exports.fiatBalance = function fiatBalance() {
// Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ] // Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
// [ $ ] = [ B * $/B ] // [ $ ] = [ B * $/B ]
// [ $ ] = [ $ ] // [ $ ] = [ $ ]
var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin; var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) /
lowBalanceMargin;
return fiatTransferBalance; return fiatTransferBalance;
}; };
/* /*
* Polling livecycle * Polling livecycle
*/ */
@ -401,7 +398,6 @@ function stopTrader() {
} }
} }
function pollBalance(cb) { function pollBalance(cb) {
logger.debug('collecting balance'); logger.debug('collecting balance');
@ -446,7 +442,6 @@ function pollRate(cb) {
}); });
} }
/* /*
* Getters | Helpers * Getters | Helpers
*/ */
@ -515,7 +510,6 @@ function executeTrades() {
}); });
} }
/* /*
* ID Verifier functions * ID Verifier functions
*/ */

View file

@ -35,7 +35,6 @@ function getInsertQuery(tableName, fields, hasId) {
return query; return query;
} }
exports.init = function init(_conString) { exports.init = function init(_conString) {
conString = _conString; conString = _conString;
if (!conString) { if (!conString) {
@ -86,13 +85,15 @@ exports.recordBill = function recordBill(deviceFingerprint, rec, cb) {
}); });
}; };
exports.recordDeviceEvent = function recordDeviceEvent(deviceFingerprint, event) { exports.recordDeviceEvent = function recordDeviceEvent(deviceFingerprint,
event) {
connect(function(err, client, done) { connect(function(err, client, done) {
if (err) return; if (err) return;
client.query('INSERT INTO device_events (device_fingerprint, event_type, note, device_time)' + var sql = 'INSERT INTO device_events (device_fingerprint, event_type, ' +
'VALUES ($1, $2, $3, $4)', 'note, device_time) VALUES ($1, $2, $3, $4)';
[deviceFingerprint, event.eventType, event.note, event.deviceTime], var values = [deviceFingerprint, event.eventType, event.note,
done); event.deviceTime];
client.query(sql, values, done);
}); });
}; };
@ -144,7 +145,8 @@ function computeSendAmount(tx, totals) {
}; };
if (result.fiat < 0 || result.satoshis < 0) { if (result.fiat < 0 || result.satoshis < 0) {
logger.warn({tx: tx, totals: totals, result: result}, logger.warn({tx: tx, totals: totals, result: result},
'computeSendAmount result < 0, this shouldn\'t happen. Maybe timeout arrived after machineSend.'); 'computeSendAmount result < 0, this shouldn\'t happen. ' +
'Maybe timeout arrived after machineSend.');
result.fiat = 0; result.fiat = 0;
result.satoshis = 0; result.satoshis = 0;
} }
@ -165,9 +167,9 @@ exports.pendingTxs = function pendingTxs(timeoutMS, cb) {
}); });
}; };
function removePendingTx(client, tx, cb) { function removePendingTx(client, sessionId, cb) {
var sql = 'DELETE FROM pending_transactions WHERE session_id=$1'; var sql = 'DELETE FROM pending_transactions WHERE session_id=$1';
silentQuery(client, sql, [tx.txId], cb); silentQuery(client, sql, [sessionId], cb);
} }
function insertOutgoingTx(client, deviceFingerprint, tx, totals, cb) { function insertOutgoingTx(client, deviceFingerprint, tx, totals, cb) {
@ -225,13 +227,14 @@ function insertTx(client, deviceFingerprint, tx, satoshis, fiat, stage,
query(client, getInsertQuery('transactions', fields, true), values, cb); query(client, getInsertQuery('transactions', fields, true), values, cb);
} }
exports.addPendingTx = function addPendingTx(deviceFingerprint, sessionId, function addPendingTx(deviceFingerprint, sessionId,
incoming, cb) { incoming, currencyCode, toAddress, cb) {
connect(function(err, client, done) { connect(function(err, client, done) {
if (err) return cb(err); if (err) return cb(err);
var fields = ['session_id', 'incoming']; var fields = ['session_id', 'incoming', 'currency_code', 'to_address'];
var sql = getInsertQuery('pending_transactions', fields); var sql = getInsertQuery('pending_transactions', fields);
query(client, sql, [sessionId, incoming], function(_err) { var values = [sessionId, incoming, currencyCode, toAddress];
query(client, sql, values, function(_err) {
done(); done();
// If pending tx already exists, do nothing // If pending tx already exists, do nothing
@ -241,7 +244,7 @@ exports.addPendingTx = function addPendingTx(deviceFingerprint, sessionId,
cb(_err); cb(_err);
}); });
}); });
}; }
// Calling function should only send bitcoins if result.satoshisToSend > 0 // Calling function should only send bitcoins if result.satoshisToSend > 0
exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) { exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
@ -249,9 +252,10 @@ exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
if (err) return cb(err); if (err) return cb(err);
async.waterfall([ async.waterfall([
async.apply(silentQuery, client, 'BEGIN', null), async.apply(silentQuery, client, 'BEGIN', null),
async.apply(insertOutgoingCompleteTx, client, deviceFingerprint, tx) async.apply(insertOutgoingCompleteTx, client, deviceFingerprint, tx),
async.apply(removePendingTx, client, tx), async.apply(removePendingTx, client, tx.sessionId),
async.apply(billsAndTxs, client, tx.txId, tx.currencyCode, deviceFingerprint), async.apply(billsAndTxs, client, tx.txId, tx.currencyCode,
deviceFingerprint),
async.apply(insertOutgoingTx, client, deviceFingerprint, tx), async.apply(insertOutgoingTx, client, deviceFingerprint, tx),
], function(err, satoshisToSend) { ], function(err, satoshisToSend) {
if (err) { if (err) {
@ -266,32 +270,56 @@ exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
}); });
}; };
function removeIncomingPendingTx(client, tx, status, cb) { exports.addIncomingTx = function addIncomingTx(deviceFingerprint, tx, source,
if (status !== 'published') return removePendingTx(client, tx, cb);
cb();
}
exports.addIncomingTx = function addIncomingTx(deviceFingerprint, tx, status,
satoshisReceived, cb) { satoshisReceived, cb) {
connect(function(err, client, done) { connect(function(err, client, done) {
function maybeRemovePending(client, sessionId, source, cb) {
if (source === 'published') return cb();
removePendingTx(client, sessionId, cb);
}
if (err) return cb(err); if (err) return cb(err);
async.waterfall([ async.waterfall([
async.apply(silentQuery, client, 'BEGIN', null), async.apply(silentQuery, client, 'BEGIN', null),
async.apply(removeOutgoingPendingTx, client, tx, status), async.apply(maybeRemovePending, client, tx.sessionId, source),
async.apply(insertTx, client, tx, satoshisReceived, 0, 'deposit') async.apply(insertTx, client, tx, satoshisReceived, 0, 'deposit', source)
], function(err, result) { ], function(err) {
if (err) { if (err) {
rollback(client, done); rollback(client, done);
return cb(err); return cb(err);
} }
silentQuery(client, 'COMMIT', null, function() { silentQuery(client, 'COMMIT', null, function() {
done(); done();
cb(null, result); cb();
}); });
}); });
}); });
}; };
exports.addInitialIncoming = function addInitialIncoming(deviceFingerprint, tx,
address, cb) {
connect(function(err, client, done) {
if (err) return cb(err);
async.waterfall([
async.apply(silentQuery, client, 'BEGIN', null),
async.apply(addPendingTx, client, deviceFingerprint, tx.sessionId,
tx.incoming, tx.currencyCode, tx.toAddress),
async.apply(insertTx, client, tx, tx.satoshis, tx.fiat,
'initial_request', 'pending')
], function(err) {
if (err) {
rollback(client, done);
return cb(err);
}
silentQuery(client, 'COMMIT', null, function() {
done();
cb();
});
});
});
};
/* /*
exports.decrementCartridges = exports.decrementCartridges =
function decrementCartridges(fingerprint, cartridge1, cartridge2, cb) { function decrementCartridges(fingerprint, cartridge1, cartridge2, cb) {

View file

@ -8,8 +8,8 @@ exports.up = function(next){
var stages = ['initial_request', 'partial_request', 'final_request', var stages = ['initial_request', 'partial_request', 'final_request',
'partial_send', 'deposit', 'dispense_request', 'dispense']. 'partial_send', 'deposit', 'dispense_request', 'dispense'].
map(singleQuotify).join(','); map(singleQuotify).join(',');
var sources = ['timeout', 'machine', 'published', 'authorized', 'rejected']. var sources = ['timeout', 'machine', 'pending', 'published',
map(singleQuotify).join(','); 'authorized', 'rejected'].map(singleQuotify).join(',');
var sqls = [ var sqls = [
'CREATE TYPE transaction_stage AS ENUM (' + stages + ')', 'CREATE TYPE transaction_stage AS ENUM (' + stages + ')',
'CREATE TYPE transaction_source AS ENUM (' + sources + ')', 'CREATE TYPE transaction_source AS ENUM (' + sources + ')',
@ -37,8 +37,10 @@ exports.up = function(next){
'CREATE TABLE pending_transactions ( ' + 'CREATE TABLE pending_transactions ( ' +
'id serial PRIMARY KEY, ' + 'id serial PRIMARY KEY, ' +
'session_id uuid UNIQUE, ' + 'session_id uuid UNIQUE NOT NULL, ' +
'incoming boolean, ' + 'incoming boolean NOT NULL, ' +
'currency_code text NOT NULL, ' +
'to_address text NOT NULL, ' +
'created timestamp NOT NULL DEFAULT now() ' + 'created timestamp NOT NULL DEFAULT now() ' +
')', ')',