diff --git a/lib/plugins.js b/lib/plugins.js index 55859c4f..db417713 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -7,9 +7,6 @@ var SATOSHI_FACTOR = 1e8; var POLLING_RATE = 60 * 1000; // poll each minute var REAP_RATE = 5 * 1000; var PENDING_TIMEOUT = 70 * 1000; -var RECOMMENDED_FEE = 10000; -var TX_0CONF_WAIT_TIME = 20 * 1000; // wait 20 seconds -var MIN_CONFIDENCE = 0.7; var db = null; @@ -215,16 +212,29 @@ function executeTx(deviceFingerprint, tx, cb) { }); } +function reapIncomingTx(deviceFingerprint, tx) { + executeTx(deviceFingerprint, tx, function(err) { + if (err) logger.error(err); + }); +} + +function reapOutgoingTx(deviceFingerprint, tx) { + infoPlugin.checkAddress(tx.toAddress, function(err, status, satoshisReceived) { + if (status === 'notSeen') return; + db.addOutgoingTx(deviceFingerprint, tx, status, satoshisReceived); + }); +} + function reapTx(row) { var deviceFingerprint = row.device_fingerprint; var tx = { txId: row.txid, toAddress: row.to_address, - currencyCode: row.currency_code + currencyCode: row.currency_code, + incoming: row.incoming }; - executeTx(deviceFingerprint, tx, function(err) { - if (err) logger.error(err); - }); + if (row.incoming) reapIncomingTx(deviceFingerprint, tx); + else reapOutgoingTx(deviceFingerprint, tx); } function reapTxs() { @@ -253,6 +263,8 @@ exports.trade = function trade(deviceFingerprint, rawTrade, cb) { var tx = { txId: rawTrade.txId, + fiat: 0, + satoshis: 0, toAddress: rawTrade.toAddress, currencyCode: rawTrade.currency }; @@ -297,82 +309,6 @@ function _setDispenseStatus(deviceFingerprint, tx, status, deposit) { dispenseStatuses[deviceFingerprint] = statusObject; } -function _checkTx(deviceFingerprint, tx, txInfo) { - // accept if tx is already confirmed - if (txInfo.confirmations > 0) { - _setDispenseStatus(deviceFingerprint, tx, 'authorizedDeposit', txInfo.amount); - return true; - } - - // NOTE: we can put some heuristics here - - // consider authorization raported by the 'info' plugin - if (txInfo.authorized === true && txInfo.confidence >= MIN_CONFIDENCE) { - _setDispenseStatus(deviceFingerprint, tx, 'authorizedDeposit', txInfo.amount); - return true; - } - - // SHOULD TAKE MUCH MORE FACTORS INTO ACCOUNT HERE - // accept txs with recommended fee and with at least 20s of propagation time - if (txInfo.fees >= RECOMMENDED_FEE && txInfo.tsReceived + TX_0CONF_WAIT_TIME < Date.now()) { - _setDispenseStatus(deviceFingerprint, tx, 'authorizedDeposit', txInfo.amount); - return true; - } - - return false; -} - -// this is invoked only when tx is fresh enough AND is for a right amount -function _monitorTx(deviceFingerprint, tx) { - infoPlugin.getTx(tx.txHash, tx.toAddress, function(err, txInfo) { - if (err) { - logger.error(err); - return setTimeout(_monitorTx, 300, deviceFingerprint, tx); - } - - if (_checkTx(deviceFingerprint, tx, txInfo)) - return; - - setTimeout(_monitorTx, 300, deviceFingerprint, tx); - }); -} - -function _monitorAddress(deviceFingerprint, tx) { - if (!tx) throw new Error('No tx'); - infoPlugin.getAddressLastTx(tx.toAddress, function(err, txInfo) { - if (err) { - logger.error(err); - return setTimeout(_monitorAddress, 300, deviceFingerprint, tx); - } - - // no tx occured at all or deposit address was reused; some previous tx was returned - if (!txInfo || txInfo.tsReceived < tx.created) { - return setTimeout(_monitorAddress, 300, deviceFingerprint, tx); - } - - // when sent TX is not enough - if (txInfo.amount < tx.satoshis) - return _setDispenseStatus(deviceFingerprint, tx, 'insufficientDeposit', txInfo.amount); - - // store txHash for later reference - tx.txHash = txInfo.txHash; - - // warn about dangerous TX - if (txInfo.fees < RECOMMENDED_FEE) - logger.warn('TXs w/o fee can take forever to confirm!'); - - // make sure tx isn't already in an acceptable state - if (_checkTx(deviceFingerprint, tx, txInfo)) - return; - - // update tx status and save first txHash - _setDispenseStatus(deviceFingerprint, tx, 'fullDeposit', txInfo.amount); - - // start monitoring TX (instead of an address) - setTimeout(_monitorTx, 300, deviceFingerprint, tx); - }); -} - exports.cashOut = function cashOut(deviceFingerprint, tx, cb) { var tmpInfo = { label: 'TX ' + Date.now(), @@ -383,19 +319,10 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) { return cb(new Error(err)); tx.toAddress = address; - tx.tx_type = 'sell'; + tx.incoming = false; - db.insertTx(deviceFingerprint, tx, function(err) { - if (err) - return cb(new Error(err)); - - _setDispenseStatus(deviceFingerprint, tx, 'noDeposit'); - - // start watching address for incoming txs - _monitorAddress(deviceFingerprint, tx); - - // return address to the machine - return cb(null, address); + db.addPendingTx(deviceFingerprint, tx, function(err) { + cb(err, address); }); }); }; diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index d0be0c88..9efa0582 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -110,16 +110,16 @@ function silentQuery(client, queryStr, values, cb) { } // OPTIMIZE: No need to query bills if tx.fiat and tx.satoshis are set -function billsAndTxs(client, txid, currencyCode, deviceFingerprint, cb) { +function billsAndTxs(client, sessionId, currencyCode, deviceFingerprint, cb) { var billsQuery = 'SELECT COALESCE(SUM(denomination), 0) as fiat, ' + 'COALESCE(SUM(satoshis), 0) AS satoshis ' + 'FROM bills ' + 'WHERE transaction_id=$1 AND currency_code=$2 AND device_fingerprint=$3'; - var billsValues = [txid, currencyCode, deviceFingerprint]; + var billsValues = [sessionId, currencyCode, deviceFingerprint]; var txQuery = 'SELECT COALESCE(SUM(fiat), 0) AS fiat, ' + 'COALESCE(SUM(satoshis), 0) AS satoshis ' + 'FROM transactions ' + - 'WHERE txid=$1 AND currency_code=$2 AND device_fingerprint=$3'; + 'WHERE session_id=$1 AND currency_code=$2 AND device_fingerprint=$3'; var txValues = billsValues; // They happen to be the same async.parallel([ @@ -157,7 +157,7 @@ exports.pendingTxs = function pendingTxs(timeoutMS, cb) { connect(function(err, client, done) { var sql = 'SELECT * FROM transactions ' + 'WHERE status=$1 AND ' + - 'EXTRACT(EPOCH FROM now() - created) > $2 ' + + '(NOT incoming OR EXTRACT(EPOCH FROM now() - created > $2) ' + 'ORDER BY created ASC'; var timeoutS = timeoutMS / 1000; var values = ['pending', timeoutS]; @@ -169,7 +169,7 @@ exports.pendingTxs = function pendingTxs(timeoutMS, cb) { }; function removePendingTx(client, tx, cb) { - silentQuery(client, 'DELETE FROM transactions WHERE txid=$1 AND status=$2', + silentQuery(client, 'DELETE FROM transactions WHERE session_id=$1 AND status=$2', [tx.txId, 'pending'], cb); } @@ -187,9 +187,9 @@ function maybeInsertTx(client, deviceFingerprint, tx, totals, cb) { function insertTx(client, deviceFingerprint, tx, satoshis, fiat, status, cb) { var fields = [ - 'txid', + 'session_id', 'status', - 'tx_type', + 'incoming', 'device_fingerprint', 'to_address', 'satoshis', @@ -200,7 +200,7 @@ function insertTx(client, deviceFingerprint, tx, satoshis, fiat, status, cb) { var values = [ tx.txId, status, - tx.tx_type || 'buy', + tx.incoming === false ? false : true, deviceFingerprint, tx.toAddress, satoshis, @@ -211,16 +211,19 @@ function insertTx(client, deviceFingerprint, tx, satoshis, fiat, status, cb) { query(client, getInsertQuery('transactions', fields, true), values, cb); } -exports.addPendingTx = function addPendingTx(deviceFingerprint, tx) { +exports.addPendingTx = function addPendingTx(deviceFingerprint, tx, cb) { connect(function(err, client, done) { - if (err) return; - insertTx(client, deviceFingerprint, tx, 0, 0, 'pending', + if (err) return cb(err); + insertTx(client, deviceFingerprint, tx, tx.satoshis, tx.fiat, 'pending', function(err) { done(); // If pending tx already exists, do nothing - if (err && PG_ERRORS[err.code] !== 'uniqueViolation') + if (err && PG_ERRORS[err.code] !== 'uniqueViolation') { logger.error(err); + return cb(err); + } + cb(); }); }); }; diff --git a/migrations/004-transactions-reload.js b/migrations/004-transactions-reload.js index 119e47f1..1c166f9d 100644 --- a/migrations/004-transactions-reload.js +++ b/migrations/004-transactions-reload.js @@ -12,6 +12,7 @@ exports.up = function(next){ 'ALTER TABLE transactions ADD PRIMARY KEY (id)', 'CREATE INDEX ON transactions (session_id)', 'ALTER TABLE transactions ADD CONSTRAINT transactions_session_status UNIQUE (session_id,status)', + 'ALTER TABLE transactions ADD COLUMN incoming boolean', 'CREATE TABLE digital_transactions ( ' + 'id serial PRIMARY KEY, ' +