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 cachedConfig = null;
var deviceCurrency = 'USD';
@ -33,7 +32,6 @@ var reapTxInterval = null;
var tradesQueue = [];
var dispenseStatuses = {};
// that's basically a constructor
exports.init = function init(databaseHandle) {
if (!databaseHandle) {
@ -43,16 +41,15 @@ exports.init = function init(databaseHandle) {
db = databaseHandle;
};
function loadPlugin(name, config) {
// plugins definitions
var moduleMethods = {
ticker: [ 'ticker' ],
trader: [ 'balance', 'purchase', 'sell' ],
wallet: [ 'balance', 'sendBitcoins', 'newAddress' ],
idVerifier: [ 'verifyUser', 'verifyTransaction' ],
info: [ 'getAddressLastTx', 'getTx' ]
ticker: ['ticker'],
trader: ['balance', 'purchase', 'sell'],
wallet: ['balance', 'sendBitcoins', 'newAddress'],
idVerifier: ['verifyUser', 'verifyTransaction'],
info: ['getAddressLastTx', 'getTx']
};
var plugin = null;
@ -60,67 +57,72 @@ function loadPlugin(name, config) {
// 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');
} 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')
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');
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');
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'));
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'));
logger.warn(new Error('\'' + name +
'\' fails to implement *recommended* \'config\' method'));
plugin.config = function() {};
} else if (config !== null)
} else if (config !== null) {
plugin.config(config); // only when plugin supports it, and config is passed
}
return plugin;
}
function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCallback) {
function loadOrConfigPlugin(pluginHandle, pluginType, currency,
onChangeCallback) {
var currentName = cachedConfig.exchanges.plugins.current[pluginType];
var pluginChanged = currentlyUsedPlugins[pluginType] !== currentName;
if (!currentName) pluginHandle = null;
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 (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig);
else {
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;
}
exports.configure = function configure(config) {
if (config.exchanges.settings.lowBalanceMargin < 1) {
throw new Error('\'settings.lowBalanceMargin\' has to be >= 1');
@ -217,9 +219,10 @@ function reapOutgoingTx(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;
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);
};
// sets given status both "locally" (dispenseStatuses) and saves to db
function _setDispenseStatus(deviceFingerprint, tx, status, deposit) {
tx.status = status;
@ -313,14 +315,10 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) {
account: 'deposit'
};
walletPlugin.newAddress(tmpInfo, function(err, address) {
if (err)
return cb(new Error(err));
if (err) return cb(err);
tx.toAddress = address;
tx.incoming = true;
db.addPendingTx(deviceFingerprint, tx, function(err) {
cb(err, address);
db.addInitialIncoming(deviceFingerprint, tx, address, function(_err) {
cb(_err, address);
});
});
};
@ -333,7 +331,6 @@ exports.dispenseStatus = function dispenseStatus(deviceFingerprint) {
return dispenseStatuses[deviceFingerprint];
};
exports.fiatBalance = function fiatBalance() {
var rawRate = exports.getDeviceRate().rates.ask;
var commission = cachedConfig.exchanges.settings.commission;
@ -357,12 +354,12 @@ exports.fiatBalance = function fiatBalance() {
// Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
// [ $ ] = [ B * $/B ]
// [ $ ] = [ $ ]
var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;
var fiatTransferBalance = ((transferBalance / SATOSHI_FACTOR) * rate) /
lowBalanceMargin;
return fiatTransferBalance;
};
/*
* Polling livecycle
*/
@ -401,7 +398,6 @@ function stopTrader() {
}
}
function pollBalance(cb) {
logger.debug('collecting balance');
@ -446,7 +442,6 @@ function pollRate(cb) {
});
}
/*
* Getters | Helpers
*/
@ -515,7 +510,6 @@ function executeTrades() {
});
}
/*
* ID Verifier functions
*/

View file

@ -35,7 +35,6 @@ function getInsertQuery(tableName, fields, hasId) {
return query;
}
exports.init = function init(_conString) {
conString = _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) {
if (err) return;
client.query('INSERT INTO device_events (device_fingerprint, event_type, note, device_time)' +
'VALUES ($1, $2, $3, $4)',
[deviceFingerprint, event.eventType, event.note, event.deviceTime],
done);
var sql = 'INSERT INTO device_events (device_fingerprint, event_type, ' +
'note, device_time) VALUES ($1, $2, $3, $4)';
var values = [deviceFingerprint, event.eventType, event.note,
event.deviceTime];
client.query(sql, values, done);
});
};
@ -144,7 +145,8 @@ function computeSendAmount(tx, totals) {
};
if (result.fiat < 0 || result.satoshis < 0) {
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.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';
silentQuery(client, sql, [tx.txId], cb);
silentQuery(client, sql, [sessionId], 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);
}
exports.addPendingTx = function addPendingTx(deviceFingerprint, sessionId,
incoming, cb) {
function addPendingTx(deviceFingerprint, sessionId,
incoming, currencyCode, toAddress, cb) {
connect(function(err, client, done) {
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);
query(client, sql, [sessionId, incoming], function(_err) {
var values = [sessionId, incoming, currencyCode, toAddress];
query(client, sql, values, function(_err) {
done();
// If pending tx already exists, do nothing
@ -241,7 +244,7 @@ exports.addPendingTx = function addPendingTx(deviceFingerprint, sessionId,
cb(_err);
});
});
};
}
// Calling function should only send bitcoins if result.satoshisToSend > 0
exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
@ -249,9 +252,10 @@ exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
if (err) return cb(err);
async.waterfall([
async.apply(silentQuery, client, 'BEGIN', null),
async.apply(insertOutgoingCompleteTx, client, deviceFingerprint, tx)
async.apply(removePendingTx, client, tx),
async.apply(billsAndTxs, client, tx.txId, tx.currencyCode, deviceFingerprint),
async.apply(insertOutgoingCompleteTx, client, deviceFingerprint, tx),
async.apply(removePendingTx, client, tx.sessionId),
async.apply(billsAndTxs, client, tx.txId, tx.currencyCode,
deviceFingerprint),
async.apply(insertOutgoingTx, client, deviceFingerprint, tx),
], function(err, satoshisToSend) {
if (err) {
@ -266,32 +270,56 @@ exports.addOutgoingTx = function addOutgoingTx(deviceFingerprint, tx, cb) {
});
};
function removeIncomingPendingTx(client, tx, status, cb) {
if (status !== 'published') return removePendingTx(client, tx, cb);
cb();
}
exports.addIncomingTx = function addIncomingTx(deviceFingerprint, tx, status,
exports.addIncomingTx = function addIncomingTx(deviceFingerprint, tx, source,
satoshisReceived, cb) {
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);
async.waterfall([
async.apply(silentQuery, client, 'BEGIN', null),
async.apply(removeOutgoingPendingTx, client, tx, status),
async.apply(insertTx, client, tx, satoshisReceived, 0, 'deposit')
], function(err, result) {
async.apply(maybeRemovePending, client, tx.sessionId, source),
async.apply(insertTx, client, tx, satoshisReceived, 0, 'deposit', source)
], function(err) {
if (err) {
rollback(client, done);
return cb(err);
}
silentQuery(client, 'COMMIT', null, function() {
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 =
function decrementCartridges(fingerprint, cartridge1, cartridge2, cb) {

View file

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