diff --git a/lib/plugins.js b/lib/plugins.js index ccdb2292..35c7da33 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -182,8 +182,11 @@ exports.trade = function trade(rawTrade, deviceFingerprint) { sessions[deviceFingerprint] = { timestamp: Date.now(), reaper: setTimeout(function() { - db.getTransaction(rawTrade.txId, function(err, tx) { - _sendBitcoins(tx, function() { }); + // NOTE: at this point we either have bills ONLY *or a partial tx* + // TODO: deal with #1 from ^ + db.getPendingTransactions(rawTrade.txId, function(err, txs) { + // NOTE: returns ARRAY of txs + _sendBitcoins(txs[0], function() { }); delete sessions[deviceFingerprint]; }); }, SESSION_TIMEOUT) @@ -193,21 +196,6 @@ exports.trade = function trade(rawTrade, deviceFingerprint) { // record (vel log) inserted bill db.recordBill(deviceFingerprint, rawTrade); - // cache partial Transaction (if supported by machine) - if (rawTrade.partialTx) { - var tx = { - txId: rawTrade.txId, - status: 'partial', - toAddress: rawTrade.toAddress, - currencyCode: rawTrade.currency, - satoshis: rawTrade.partialTx.satoshis, - fiat: rawTrade.partialTx.fiat - }; - db.summonTransaction(deviceFingerprint, tx, function() { - logger.debug('partial tx: %j', tx); - }); - } - // add bill to trader queue (if trader is enabled) if (traderPlugin) { tradesQueue.push({ @@ -258,11 +246,14 @@ function _sendBitcoins(tx, callback) { 'insufficientFunds' : 'failed'; - db.reportTransactionError(tx, err.message, status); + // report insufficient funds error + db.changeTxStatus(tx.txId, status, {error: err.message}); return callback(err); } - db.completeTransaction(tx, txHash); + if (txHash) db.changeTxStatus(tx.txId, 'completed', {hash: txHash}); + else db.changeTxStatus(tx.txId, 'failed', {error: 'No txHash received'}); + pollBalance(); callback(null, txHash); } @@ -295,6 +286,31 @@ exports.sendBitcoins = function sendBitcoins(deviceFingerprint, tx, callback) { }); }; +// NOTE: temporarily here +exports.sendBitcoinians = function(deviceFingerprint, tx, callback) { + db.summonTransaction(deviceFingerprint, tx, function(err, txInfo) { + if (err) return callback(err); + + if (txInfo) { + if (txInfo.status === 'insufficientFunds') { + + } + + if (txInfo.status === 'executing') { + + } + + // TODO: check `part` and what has already been sent + } + + // no error & no tx record exists + // TODO: should it be confirmed with bills? + + + + }); +}; + /* * Polling livecycle diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index 9b85a739..70dbcb8c 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -9,6 +9,21 @@ var PG_ERRORS = { var client = null; + +function getInsertQuery(tableName, fields) { + + // outputs string like: '$1, $2, $3...' with proper No of items + var placeholders = fields.map(function(_, i) { + return '$' + (i + 1); + }).join(', '); + + return 'INSERT INTO ' + tableName + + ' (' + fields.join(', ') + ')' + + ' VALUES' + + ' (' + placeholders + ')'; +} + + exports.init = function init(conString) { if (client !== null) return; @@ -23,12 +38,36 @@ exports.init = function init(conString) { }; +// logs inputted bill and overall tx status (if available) exports.recordBill = function recordBill(deviceFingerprint, rec, cb) { - client.query('INSERT INTO bills (device_fingerprint, denomination, currency_code, ' + - 'satoshis, to_address, transaction_id, device_time) ' + - 'VALUES ($1, $2, $3, $4, $5, $6, $7)', - [deviceFingerprint, rec.fiat, rec.currency, rec.satoshis, rec.toAddress, rec.txId, rec.deviceTime], - cb); + var fields = [ + 'device_fingerprint', + 'currency_code', + 'to_address', + 'transaction_id', + 'device_time', + + 'satoshis', + 'denomination' + ]; + + var values = [ + deviceFingerprint, + rec.currency, + rec.toAddress, + res.txId, + rec.deviceTime, + + rec.satoshis, + rec.fiat + ]; + + if (rec.partialTx) { + values.push(rec.partialTx.satoshis, rec.partialTx.fiat); + fields.push('total_satoshis', 'total_fiat'); + } + + client.query(getInsertQuery('bills', fields), values, cb); }; exports.recordDeviceEvent = function recordDeviceEvent(deviceFingerprint, event, cb) { @@ -38,51 +77,54 @@ exports.recordDeviceEvent = function recordDeviceEvent(deviceFingerprint, event, cb); }; -// each received "partial transaction" contains sum of all previous bills -// (vel. no need to do any server-side summing) -function updatePartialTransaction(values, cb) { - var values2 = [ - values[4], - values[6], - values[0], - 'partial' - ]; - client.query('UPDATE transactions SET ' + - 'satoshis=$1, ' + - 'fiat=$2 ' + - 'WHERE id=$3 AND status=$4', - values2, - cb); -} -exports.getTransaction = function getTransaction(txId, cb) { - client.query('SELECT * FROM transactions WHERE id=$1', - [txId], - function(err, results) { - if (err) return cb(err); +function _getTransactions(txId, onlyPending, cb) { + var query = 'SELECT * FROM transactions WHERE id=$1'; + var values = [txId]; - cb(null, results.rows.length >= 0 && results.rows[0]); - }); -}; -exports.fetchTransaction = function fetchTransaction(txId, cb) { - exports.getTransaction(txId, function(err, tx) { + if (onlyPending) { + query += ' AND status=$2 AND tx_hash IS NULL'; + values.push('pending'); + } + + client.query(query, values, function(err, results) { if (err) return cb(err); - if (!tx) - return cb(new Error('Couldn\'t find transaction.')); + if (results.rows.length === 0) + return cb(new Error('Couldn\'t find transaction')); - cb(null, { - txHash: tx.tx_hash, - err: tx.error, - status: tx.status - }); + cb(null, results.rows); }); } + +// returns complete [txs] +exports.getTransactions = function getTransactions(txId, cb) { + _getTransactions(txId, false, cb); +}; + +// TODO: this should probably return bills, associated with failed/inexistent tx +exports.getPendingTransactions = function getPendingTransactions(txId, cb) { + // should return + // is_completed === false + // amount from bils is less than sum of all parts with the same txId + // latest bill with txId not present in transactions + // + _getTransactions(txId, true, cb); +}; + exports.summonTransaction = function summonTransaction(deviceFingerprint, tx, cb) { - var status = tx.status || 'pending'; + var fields = [ + 'id', + 'status', + 'device_fingerprint', + 'to_address', + 'satoshis', + 'currency_code', + 'fiat' + ]; var values = [ tx.txId, - status, + tx.status || 'pending', deviceFingerprint, tx.toAddress, tx.satoshis, @@ -90,23 +132,19 @@ exports.summonTransaction = function summonTransaction(deviceFingerprint, tx, cb tx.fiat ]; + if (tx.part && tx.part > 1) { + fields.push('part'); + values.push(tx.part); + } + // First attampt an INSERT // If it worked, go ahead with transaction - // If duplicate and partial update with new bills - // If duplicate, but not partial fetch status and return - - client.query('INSERT INTO transactions ' + - '(id, status, device_fingerprint, to_address, satoshis, currency_code, fiat) ' + - 'VALUES ($1, $2, $3, $4, $5, $6, $7)', + client.query(getInsertQuery('transactions', fields), values, function(err) { if (err) { - if (PG_ERRORS[err.code] === 'uniqueViolation') { - if (status === 'partial') - return updatePartialTransaction(values, cb); - - return exports.fetchTransaction(tx.txId, cb); - } + if (PG_ERRORS[err.code] === 'uniqueViolation') + return exports.getTransactions(tx.txId, cb); return cb(err); } @@ -115,17 +153,29 @@ exports.summonTransaction = function summonTransaction(deviceFingerprint, tx, cb }); }; +// `@more` can contain `part`, `hash`, or `error` +exports.changeTxStatus = function changeTxStatus(txId, newStatus, more, cb) { + cb = typeof cb === 'function' ? cb : function() {}; -exports.reportTransactionError = function reportTransactionError(tx, errString, status) { - client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3', - [status, errString, tx.txId]); -}; + var query = 'UPDATE transactions SET status=$1'; + var values = [ + newStatus + ]; -exports.completeTransaction = function completeTransaction(tx, txHash) { - if (txHash) - client.query('UPDATE transactions SET tx_hash=$1, status=$2, completed=now() WHERE id=$3', - [txHash, 'completed', tx.txId]); - else - client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3', - ['failed', 'No txHash received', tx.txId]); + var n = 2; + + if (newStatus === 'error') { + query += ', error=$' + n++; + values.push(more.error); + } + + if (newStatus === 'completed') { + query += ', tx_hash=$' + n++; + values.push(more.hash); + } + + query += ' WHERE id=$' + n; + values.push(txId); + + client.query(query, values, cb); };