From 0c0fb74dcd9b297369c5d76af0e1c0383a279cf6 Mon Sep 17 00:00:00 2001 From: Damian Mee Date: Sun, 9 Nov 2014 04:33:01 +0100 Subject: [PATCH 1/3] feat(dualWay): WIP; new plugin type and initial sell structure chunks added --- lib/plugins.js | 94 ++++++++++++++++++++++++++++++------- lib/postgresql_interface.js | 25 +++++----- package.json | 1 + 3 files changed, 92 insertions(+), 28 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index 603b4283..ebc349fd 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -16,8 +16,7 @@ var tickerPlugin = null; var traderPlugin = null; var walletPlugin = null; var idVerifierPlugin = null; - -var blockchainUtil = null; +var infoPlugin = null; var currentlyUsedPlugins = {}; @@ -53,8 +52,9 @@ function loadPlugin(name, config) { var moduleMethods = { ticker: [ 'ticker' ], trader: [ 'balance', 'purchase', 'sell' ], - wallet: [ 'balance', 'sendBitcoins' ], - idVerifier: [ 'verifyUser', 'verifyTransaction' ] + wallet: [ 'balance', 'sendBitcoins', 'newAddress' ], + idVerifier: [ 'verifyUser', 'verifyTransaction' ], + info: [ 'getLastTx', 'getTxStatus' ] }; var plugin = null; @@ -168,9 +168,10 @@ exports.configure = function configure(config) { 'idVerifier' ); - // NOTE: temp solution - if (blockchainUtil === null) - blockchainUtil = require('./blockchain_util'); + infoPlugin = loadOrConfigPlugin( + infoPlugin, + 'info' + ); }; exports.getCachedConfig = function getCachedConfig() { return cachedConfig; @@ -305,8 +306,9 @@ function _monitorAddress(address, cb) { var interval = 300; // TODO make config function checkAddress(_cb) { - blockchainUtil.addressReceived(address, confs, function(err, _received) { + infoPlugin.getLastTx(address, function(err, tx) { if (err) logger.error(err); + if (_received > 0) received = _received; setTimeout(_cb, interval); }); @@ -326,7 +328,7 @@ function _monitorAddress(address, cb) { async.doUntil(checkAddress, test, handler); } -function _waitDeposit(deviceFingerprint, tx) { +function _awaitDeposit(deviceFingerprint, tx) { _monitorAddress(tx.toAddress, function(err, received) { var status = 'fullDeposit'; @@ -346,24 +348,84 @@ function _waitDeposit(deviceFingerprint, tx) { }); } +function _monitorTx(deviceFingerprint, tx) { + infoPlugin.getTxStatus(tx.txHash, function(err, txStatus) { + if (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'); + } + }); +} + +function _monitorAddress(deviceFingerprint, tx) { + infoPlugin.getLastTx(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]); + + // 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); + } + }); +} + + exports.cashOut = function cashOut(deviceFingerprint, tx, cb) { var tmpInfo = { label: 'TX ' + Date.now(), account: 'deposit' }; - walletPlugin.newAddress('deposit', function(err, address) { - if (err) return cb(new Error(err)); + walletPlugin.newAddress(tmpInfo, function(err, address) { + if (err) + return cb(new Error(err)); tx.toAddress = address; - // WARN: final db structure will determine if we can use this method + tx.tx_type = 'sell'; + db.insertTx(deviceFingerprint, tx, function(err) { - if (err) return cb(new Error(err)); + if (err) + return cb(new Error(err)); - _waitDeposit(deviceFingerprint, tx); + // start watching address for incoming txs + _awaitDeposit(deviceFingerprint, tx); + + // return address to the machine return cb(null, address); - // NOTE: logic here will depend on a way we want to handle those txs }); - }); }; diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index cd16e206..f9867060 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -156,6 +156,7 @@ exports.insertTx = function insertTx(deviceFingerprint, tx, cb) { var fields = [ 'id', 'status', + 'tx_type', 'device_fingerprint', 'to_address', 'satoshis', @@ -166,6 +167,7 @@ exports.insertTx = function insertTx(deviceFingerprint, tx, cb) { var values = [ tx.txId, tx.status || 'pending', + tx.tx_type || 'buy', deviceFingerprint, tx.toAddress, tx.satoshis, @@ -219,20 +221,19 @@ exports.changeTxStatus = function changeTxStatus(txId, newStatus, data, cb) { values.push(data.error); } - if (newStatus === 'completed') { - // set tx_hash (if available) - if (typeof data.hash !== 'undefined') { - query += ', tx_hash=$' + n++; - values.push(data.hash); - } - - // indicates if tx was finished by a `/send` call (and not timeout) - if (typeof data.is_completed !== 'undefined') { - query += ', is_completed=$' + n++; - values.push(data.is_completed); - } + // set tx_hash (if available) + if (typeof data.hash !== 'undefined') { + query += ', tx_hash=$' + n++; + values.push(data.hash); } + // indicates if tx was finished by a `/send` call (and not timeout) + if (typeof data.is_completed !== 'undefined') { + query += ', is_completed=$' + n++; + values.push(data.is_completed); + } + + query += ' WHERE id=$' + n++; values.push(txId); diff --git a/package.json b/package.json index 6d391987..92063595 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "lamassu-bitpay": "~1.0.0", "lamassu-bitstamp": "~1.0.0", "lamassu-blockchain": "~1.0.0", + "lamassu-chain": "chester1000/lamassu-chain", "lamassu-coindesk": "~1.0.0", "lamassu-config": "~0.4.0", "lamassu-identitymind": "^1.0.1", From 4ebc3db302bdea330c8e0f40782d594c768a816e Mon Sep 17 00:00:00 2001 From: Damian Mee Date: Tue, 11 Nov 2014 05:46:12 +0100 Subject: [PATCH 2/3] feat(santo-tirso): 2nd direction w/now plugin type working --- lib/plugins.js | 183 ++++++++++++++++++++++++------------------------- lib/routes.js | 5 +- 2 files changed, 90 insertions(+), 98 deletions(-) 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); }); } From 75807cd0fa6ee085cc7d0b21dbd82871f9b61db5 Mon Sep 17 00:00:00 2001 From: Damian Mee Date: Tue, 11 Nov 2014 06:01:02 +0100 Subject: [PATCH 3/3] teste(info): fixed --- test/mocks/config.json | 3 ++- test/mocks/info.js | 10 ++++++++++ test/mocks/wallet.js | 3 +++ test/plugins.js | 5 ++++- 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 test/mocks/info.js diff --git a/test/mocks/config.json b/test/mocks/config.json index a2ab8664..d5270c1c 100644 --- a/test/mocks/config.json +++ b/test/mocks/config.json @@ -27,7 +27,8 @@ "trade": "mockTrader", "wallet": "mockWallet", "transfer": "mockWallet", - "idVerifier": "mockVerifier" + "idVerifier": "mockVerifier", + "info": "mockInfo" }, "settings": { "bitpay": { }, diff --git a/test/mocks/info.js b/test/mocks/info.js new file mode 100644 index 00000000..9aeb14b4 --- /dev/null +++ b/test/mocks/info.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + SUPPORTED_MODULES: ['info'], + NAME: 'Mock Info', + + config: function config() {}, + getAddressLastTx: function verifyUser() {}, + getTx: function verifyTransaction() {} +}; diff --git a/test/mocks/wallet.js b/test/mocks/wallet.js index 289a7090..28882ca5 100644 --- a/test/mocks/wallet.js +++ b/test/mocks/wallet.js @@ -21,6 +21,9 @@ module.exports = { e.name = 'InsufficientFunds'; cb(e); } + }, + newAddress: function(info, cb) { + cb(null, ADDR); } }; diff --git a/test/plugins.js b/test/plugins.js index c5793564..00616ef5 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -18,11 +18,13 @@ var walletMock = require('./mocks/wallet'); var tickerMock = require('./mocks/ticker'); var traderMock = require('./mocks/trader'); var verifierMock = require('./mocks/verifier'); +var infoMock = require('./mocks/info'); mockery.registerMock('lamassu-mockWallet', walletMock); mockery.registerMock('lamassu-mockTicker', tickerMock); mockery.registerMock('lamassu-mockTrader', traderMock); mockery.registerMock('lamassu-mockVerifier', verifierMock); +mockery.registerMock('lamassu-mockInfo', infoMock); describe('Plugins', function() { @@ -110,11 +112,12 @@ describe('Plugins', function() { tickerMock.config = configTest('ticker'); traderMock.config = configTest('trader'); verifierMock.config = configTest('verifier'); + infoMock.config = configTest('info'); plugins.configure(config); }); - ['wallet', 'ticker', 'trader', 'verifier'].forEach(function(name) { + ['wallet', 'ticker', 'trader', 'verifier', 'info'].forEach(function(name) { it('should configure ' + name, function() { confList.should.have.property(name); should.exist(confList[name]);