Merge pull request #52 from chester1000/canary

feat(dualWay): WIP; new plugin type and initial sell structure chunks added
This commit is contained in:
Josh Harvey 2014-11-12 15:06:38 -05:00
commit 8009cd04ac
8 changed files with 158 additions and 85 deletions

View file

@ -9,6 +9,10 @@ var SATOSHI_FACTOR = 1e8;
var SESSION_TIMEOUT = 60 * 60 * 1000; var SESSION_TIMEOUT = 60 * 60 * 1000;
var POLLING_RATE = 60 * 1000; // poll each minute 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; var db = null;
@ -16,8 +20,7 @@ var tickerPlugin = null;
var traderPlugin = null; var traderPlugin = null;
var walletPlugin = null; var walletPlugin = null;
var idVerifierPlugin = null; var idVerifierPlugin = null;
var infoPlugin = null;
var blockchainUtil = null;
var currentlyUsedPlugins = {}; var currentlyUsedPlugins = {};
@ -53,8 +56,9 @@ function loadPlugin(name, config) {
var moduleMethods = { var moduleMethods = {
ticker: [ 'ticker' ], ticker: [ 'ticker' ],
trader: [ 'balance', 'purchase', 'sell' ], trader: [ 'balance', 'purchase', 'sell' ],
wallet: [ 'balance', 'sendBitcoins' ], wallet: [ 'balance', 'sendBitcoins', 'newAddress' ],
idVerifier: [ 'verifyUser', 'verifyTransaction' ] idVerifier: [ 'verifyUser', 'verifyTransaction' ],
info: [ 'getAddressLastTx', 'getTx' ]
}; };
var plugin = null; var plugin = null;
@ -111,7 +115,10 @@ function loadOrConfigPlugin(pluginHandle, pluginType, currency, onChangeCallback
if (currency) pluginConfig.currency = currency; if (currency) pluginConfig.currency = currency;
if (pluginHandle && !pluginChanged) pluginHandle.config(pluginConfig); 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); if (typeof onChangeCallback === 'function') onChangeCallback(pluginHandle, currency);
@ -168,9 +175,10 @@ exports.configure = function configure(config) {
'idVerifier' 'idVerifier'
); );
// NOTE: temp solution infoPlugin = loadOrConfigPlugin(
if (blockchainUtil === null) infoPlugin,
blockchainUtil = require('./blockchain_util'); 'info'
);
}; };
exports.getCachedConfig = function getCachedConfig() { exports.getCachedConfig = function getCachedConfig() {
return cachedConfig; return cachedConfig;
@ -297,52 +305,108 @@ exports.sendBitcoins = function sendBitcoins(deviceFingerprint, rawTx, cb) {
executeTx(deviceFingerprint, rawTx.txId, true, 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) { // sets given status both "locally" (dispenseStatuses) and saves to db
blockchainUtil.addressReceived(address, confs, function(err, _received) { function _setDispenseStatus(deviceFingerprint, tx, status, deposit) {
if (err) logger.error(err); 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() { var fiat = 0;
return received > 0 || Date.now() - t0 > timeOut; if (status === 'authorizedDeposit' || status === 'confirmedDeposit')
} fiat = tx.fiat;
function handler() { var statusObject = null;
if (received === 0) if (status !== 'dispensedDeposit')
return cb(new Error('Timeout while monitoring address')); statusObject = {
cb(null, received);
}
async.doUntil(checkAddress, test, handler);
}
function _waitDeposit(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] = {
status: status, status: status,
txId: tx.txId, txId: tx.txId,
deposit: received, deposit: deposit || 0,
dispenseFiat: dispenseFiat, dispenseFiat: fiat,
expectedDeposit: tx.satoshis 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.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) {
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]);
}); });
} }
@ -351,37 +415,30 @@ exports.cashOut = function cashOut(deviceFingerprint, tx, cb) {
label: 'TX ' + Date.now(), label: 'TX ' + Date.now(),
account: 'deposit' account: 'deposit'
}; };
walletPlugin.newAddress('deposit', function(err, address) { walletPlugin.newAddress(tmpInfo, function(err, address) {
if (err) return cb(new Error(err)); if (err)
return cb(new Error(err));
tx.toAddress = address; 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) { db.insertTx(deviceFingerprint, tx, function(err) {
if (err) return cb(new Error(err)); if (err)
return cb(new Error(err));
_waitDeposit(deviceFingerprint, tx); _setDispenseStatus(deviceFingerprint, tx, 'noDeposit');
// start watching address for incoming txs
_monitorAddress(deviceFingerprint, tx);
// return address to the machine
return cb(null, address); return cb(null, address);
// NOTE: logic here will depend on a way we want to handle those txs
}); });
}); });
}; };
exports.depositAck = function depositAck(deviceFingerprint, tx, cb) { exports.depositAck = function depositAck(deviceFingerprint, tx) {
/* TODO _setDispenseStatus(deviceFingerprint, tx, 'dispensedDeposit');
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.dispenseStatus = function dispenseStatus(deviceFingerprint) { exports.dispenseStatus = function dispenseStatus(deviceFingerprint) {

View file

@ -156,6 +156,7 @@ exports.insertTx = function insertTx(deviceFingerprint, tx, cb) {
var fields = [ var fields = [
'id', 'id',
'status', 'status',
'tx_type',
'device_fingerprint', 'device_fingerprint',
'to_address', 'to_address',
'satoshis', 'satoshis',
@ -166,6 +167,7 @@ exports.insertTx = function insertTx(deviceFingerprint, tx, cb) {
var values = [ var values = [
tx.txId, tx.txId,
tx.status || 'pending', tx.status || 'pending',
tx.tx_type || 'buy',
deviceFingerprint, deviceFingerprint,
tx.toAddress, tx.toAddress,
tx.satoshis, tx.satoshis,
@ -219,20 +221,19 @@ exports.changeTxStatus = function changeTxStatus(txId, newStatus, data, cb) {
values.push(data.error); values.push(data.error);
} }
if (newStatus === 'completed') { // set tx_hash (if available)
// set tx_hash (if available) if (typeof data.hash !== 'undefined') {
if (typeof data.hash !== 'undefined') { query += ', tx_hash=$' + n++;
query += ', tx_hash=$' + n++; values.push(data.hash);
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);
}
} }
// 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++; query += ' WHERE id=$' + n++;
values.push(txId); values.push(txId);

View file

@ -110,10 +110,7 @@ function cashOut(req, res) {
function depositAck(req, res) { function depositAck(req, res) {
plugins.depositAck(getFingerprint(req), req.body, function(err) { plugins.depositAck(getFingerprint(req), req.body, function(err) {
res.json({ res.json(200);
err: err && err.message,
errType: err && err.name
});
}); });
} }

View file

@ -17,6 +17,7 @@
"lamassu-bitpay": "~1.0.0", "lamassu-bitpay": "~1.0.0",
"lamassu-bitstamp": "~1.0.0", "lamassu-bitstamp": "~1.0.0",
"lamassu-blockchain": "~1.0.0", "lamassu-blockchain": "~1.0.0",
"lamassu-chain": "chester1000/lamassu-chain",
"lamassu-coindesk": "~1.0.0", "lamassu-coindesk": "~1.0.0",
"lamassu-config": "~0.4.0", "lamassu-config": "~0.4.0",
"lamassu-identitymind": "^1.0.1", "lamassu-identitymind": "^1.0.1",

View file

@ -27,7 +27,8 @@
"trade": "mockTrader", "trade": "mockTrader",
"wallet": "mockWallet", "wallet": "mockWallet",
"transfer": "mockWallet", "transfer": "mockWallet",
"idVerifier": "mockVerifier" "idVerifier": "mockVerifier",
"info": "mockInfo"
}, },
"settings": { "settings": {
"bitpay": { }, "bitpay": { },

10
test/mocks/info.js Normal file
View file

@ -0,0 +1,10 @@
'use strict';
module.exports = {
SUPPORTED_MODULES: ['info'],
NAME: 'Mock Info',
config: function config() {},
getAddressLastTx: function verifyUser() {},
getTx: function verifyTransaction() {}
};

View file

@ -21,6 +21,9 @@ module.exports = {
e.name = 'InsufficientFunds'; e.name = 'InsufficientFunds';
cb(e); cb(e);
} }
},
newAddress: function(info, cb) {
cb(null, ADDR);
} }
}; };

View file

@ -18,11 +18,13 @@ var walletMock = require('./mocks/wallet');
var tickerMock = require('./mocks/ticker'); var tickerMock = require('./mocks/ticker');
var traderMock = require('./mocks/trader'); var traderMock = require('./mocks/trader');
var verifierMock = require('./mocks/verifier'); var verifierMock = require('./mocks/verifier');
var infoMock = require('./mocks/info');
mockery.registerMock('lamassu-mockWallet', walletMock); mockery.registerMock('lamassu-mockWallet', walletMock);
mockery.registerMock('lamassu-mockTicker', tickerMock); mockery.registerMock('lamassu-mockTicker', tickerMock);
mockery.registerMock('lamassu-mockTrader', traderMock); mockery.registerMock('lamassu-mockTrader', traderMock);
mockery.registerMock('lamassu-mockVerifier', verifierMock); mockery.registerMock('lamassu-mockVerifier', verifierMock);
mockery.registerMock('lamassu-mockInfo', infoMock);
describe('Plugins', function() { describe('Plugins', function() {
@ -110,11 +112,12 @@ describe('Plugins', function() {
tickerMock.config = configTest('ticker'); tickerMock.config = configTest('ticker');
traderMock.config = configTest('trader'); traderMock.config = configTest('trader');
verifierMock.config = configTest('verifier'); verifierMock.config = configTest('verifier');
infoMock.config = configTest('info');
plugins.configure(config); plugins.configure(config);
}); });
['wallet', 'ticker', 'trader', 'verifier'].forEach(function(name) { ['wallet', 'ticker', 'trader', 'verifier', 'info'].forEach(function(name) {
it('should configure ' + name, function() { it('should configure ' + name, function() {
confList.should.have.property(name); confList.should.have.property(name);
should.exist(confList[name]); should.exist(confList[name]);