Merge pull request #52 from chester1000/canary
feat(dualWay): WIP; new plugin type and initial sell structure chunks added
This commit is contained in:
commit
8009cd04ac
8 changed files with 158 additions and 85 deletions
191
lib/plugins.js
191
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;
|
||||
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,7 +221,6 @@ 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++;
|
||||
|
|
@ -231,7 +232,7 @@ exports.changeTxStatus = function changeTxStatus(txId, newStatus, data, cb) {
|
|||
query += ', is_completed=$' + n++;
|
||||
values.push(data.is_completed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
query += ' WHERE id=$' + n++;
|
||||
values.push(txId);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
10
test/mocks/info.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
SUPPORTED_MODULES: ['info'],
|
||||
NAME: 'Mock Info',
|
||||
|
||||
config: function config() {},
|
||||
getAddressLastTx: function verifyUser() {},
|
||||
getTx: function verifyTransaction() {}
|
||||
};
|
||||
|
|
@ -21,6 +21,9 @@ module.exports = {
|
|||
e.name = 'InsufficientFunds';
|
||||
cb(e);
|
||||
}
|
||||
},
|
||||
newAddress: function(info, cb) {
|
||||
cb(null, ADDR);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue