diff --git a/lib/plugins.js b/lib/plugins.js index ebc349fd..2ebe3dc0 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -9,6 +9,10 @@ var SATOSHI_FACTOR = 1e8; var SESSION_TIMEOUT = 60 * 60 * 1000; var POLLING_RATE = 60 * 1000; // poll each minute +var RECOMMENDED_FEE = 10000; +var TX_0CONF_WAIT_TIME = 20 * 1000; // wait 20 seconds +var MIN_CONFIDENCE = 0.7; + var db = null; @@ -54,7 +58,7 @@ function loadPlugin(name, config) { trader: [ 'balance', 'purchase', 'sell' ], wallet: [ 'balance', 'sendBitcoins', 'newAddress' ], idVerifier: [ 'verifyUser', 'verifyTransaction' ], - info: [ 'getLastTx', 'getTxStatus' ] + info: [ 'getAddressLastTx', 'getTx' ] }; var plugin = null; @@ -111,7 +115,10 @@ function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCallback if (currency) pluginConfig.currency = currency; if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig); - else pluginHandle = loadPlugin(currentName, pluginConfig); + else { + pluginHandle = loadPlugin(currentName, pluginConfig); + logger.debug('plugin(%s) loaded: %s', pluginType, pluginHandle.NAME || currentName); + } } if (typeof onChangeCallback === 'function') onChangeCallback(pluginHandle, currency); @@ -298,112 +305,111 @@ exports.sendBitcoins = function sendBitcoins(deviceFingerprint, rawTx, cb) { executeTx(deviceFingerprint, rawTx.txId, true, cb); }; -function _monitorAddress(address, cb) { - var confs = 0; - var received = 0; - var t0 = Date.now(); - var timeOut = 90000; // TODO make config - var interval = 300; // TODO make config - function checkAddress(_cb) { - infoPlugin.getLastTx(address, function(err, tx) { - if (err) logger.error(err); +// sets given status both "locally" (dispenseStatuses) and saves to db +function _setDispenseStatus(deviceFingerprint, tx, status, deposit) { + tx.status = status; - if (_received > 0) received = _received; - setTimeout(_cb, interval); + // No need to set default state again + if (status !== 'noDeposit') + // save to db ASAP + db.changeTxStatus(tx.txId, status, { + hash: tx.txHash }); - } - function test() { - return received > 0 || Date.now() - t0 > timeOut; - } + var fiat = 0; + if (status === 'authorizedDeposit' || status === 'confirmedDeposit') + fiat = tx.fiat; - function handler() { - if (received === 0) - return cb(new Error('Timeout while monitoring address')); - - cb(null, received); - } - - async.doUntil(checkAddress, test, handler); -} - -function _awaitDeposit(deviceFingerprint, tx) { - _monitorAddress(tx.toAddress, function(err, received) { - var status = 'fullDeposit'; - - if (err) status = 'timeout'; - else if (received < tx.satoshis) status = 'insufficientDeposit'; - - var dispenseFiat = received >= tx.satoshis ? tx.fiat : 0; - dispenseStatuses[deviceFingerprint] = { + var statusObject = null; + if (status !== 'dispensedDeposit') + statusObject = { status: status, txId: tx.txId, - deposit: received, - dispenseFiat: dispenseFiat, + deposit: deposit || 0, + dispenseFiat: fiat, expectedDeposit: tx.satoshis }; - // TODO db.dispenseReady(tx); - }); + // keep local copy + dispenseStatuses[deviceFingerprint] = statusObject; } +function _checkTx(deviceFingerprint, tx, txInfo) { + // accept if tx is already confirmed + if (txInfo.confirmations > 0) { + _setDispenseStatus(deviceFingerprint, tx, 'confirmedDeposit', 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.getTxStatus(tx.txHash, function(err, txStatus) { - if (err) + infoPlugin.getTx(tx.txHash, tx.toAddress, function(err, txInfo) { + if (err) { + logger.error(err); return setTimeout(_monitorTx, 300, [deviceFingerprint, tx]); - - if (!txStatus || txStatus === 'fullDeposit') - return setTimeout(_monitorTx, 300, [deviceFingerprint, tx]); - - if (txStatus.status === 'confirmedDeposit') - return db.changeTxStatus(tx.txId, 'confirmedDeposit'); - - if (txStatus.status === 'authorizedDeposit') { - logger.info('Proceeding with confidence level:' + txStatus.confidence); - db.changeTxStatus(tx.txId, 'confirmedDeposit'); } + + if (_checkTx(deviceFingerprint, tx, txInfo)) + return; + + setTimeout(_monitorTx, 300, [deviceFingerprint, tx]); }); } function _monitorAddress(deviceFingerprint, tx) { - infoPlugin.getLastTx(tx.toAddress, function(err, txInfo) { + 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) + if (!txInfo || txInfo.tsReceived < tx.created) { return setTimeout(_monitorAddress, 300, [deviceFingerprint, tx]); - - // enough was sent - if (txInfo.amount >= tx.satoshis) { - - tx.txHash = txInfo.txHash; - - // tx is already confirmed - if (txInfo.confirmations > 0) - return db.changeTxStatus(tx.txId, 'confirmedDeposit', { - hash: tx.txHash - }); - - // warn about dangerous TX - if (txInfo.fees === 0) - logger.warn('TXs w/o fee can take forever to confirm!'); - - // update tx status and save txHash - db.changeTxStatus(tx.txId, 'fullDeposit', { - hash: tx.txHash - }); - - // start monitoring TX - _monitorTx(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(), @@ -420,8 +426,10 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) { if (err) return cb(new Error(err)); + _setDispenseStatus(deviceFingerprint, tx, 'noDeposit'); + // start watching address for incoming txs - _awaitDeposit(deviceFingerprint, tx); + _monitorAddress(deviceFingerprint, tx); // return address to the machine return cb(null, address); @@ -429,21 +437,8 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) { }); }; -exports.depositAck = function depositAck(deviceFingerprint, tx, cb) { -/* TODO - var status = dispenseStatuses[deviceFingerprint]; - - if (status === 'dispense') { - db.dispensing(tx, function (err) { - if (err) return cb(new Error(err)); - dispenseStatuses[deviceFingerprint] = null; - return cb(); - }); - } -*/ - - dispenseStatuses[deviceFingerprint] = null; - cb(); +exports.depositAck = function depositAck(deviceFingerprint, tx) { + _setDispenseStatus(deviceFingerprint, tx, 'dispensedDeposit'); }; exports.dispenseStatus = function dispenseStatus(deviceFingerprint) { diff --git a/lib/routes.js b/lib/routes.js index 01f61599..17c56d94 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -101,10 +101,7 @@ function cashOut(req, res) { function depositAck(req, res) { plugins.depositAck(getFingerprint(req), req.body, function(err) { - res.json({ - err: err && err.message, - errType: err && err.name - }); + res.json(200); }); }