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 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;
@ -16,8 +20,7 @@ var tickerPlugin = null;
var traderPlugin = null;
var walletPlugin = null;
var idVerifierPlugin = null;
var blockchainUtil = null;
var infoPlugin = null;
var currentlyUsedPlugins = {};
@ -53,8 +56,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: [ '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);
@ -168,9 +175,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;
@ -297,52 +305,108 @@ 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) {
blockchainUtil.addressReceived(address, confs, function(err, _received) {
if (err) logger.error(err);
if (_received > 0) received = _received;
setTimeout(_cb, interval);
// sets given status both "locally" (dispenseStatuses) and saves to db
function _setDispenseStatus(deviceFingerprint, tx, status, deposit) {
tx.status = status;
// 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 _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] = {
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.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(),
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);
_setDispenseStatus(deviceFingerprint, tx, 'noDeposit');
// start watching address for incoming txs
_monitorAddress(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
});
});
};
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) {

View file

@ -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);

View file

@ -110,10 +110,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);
});
}

View file

@ -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",

View file

@ -27,7 +27,8 @@
"trade": "mockTrader",
"wallet": "mockWallet",
"transfer": "mockWallet",
"idVerifier": "mockVerifier"
"idVerifier": "mockVerifier",
"info": "mockInfo"
},
"settings": {
"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';
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 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]);