Merge branch 'master' into compliance
This commit is contained in:
commit
656de852c0
4 changed files with 60 additions and 382 deletions
128
lib/trader.js
128
lib/trader.js
|
|
@ -8,6 +8,32 @@ var SATOSHI_FACTOR = 1e8;
|
|||
// TODO: Define this somewhere more global
|
||||
var SESSION_TIMEOUT = 60 * 60 * 1000; // an hour
|
||||
|
||||
|
||||
function findExchange(name) {
|
||||
try {
|
||||
return require('lamassu-' + name);
|
||||
|
||||
} catch(_) {
|
||||
throw new Error(name + ' module is not installed. Try running `npm install --save lamassu-' + name + '` first');
|
||||
}
|
||||
};
|
||||
|
||||
function findTicker (name) {
|
||||
var exchange = findExchange(name);
|
||||
return exchange.ticker || exchange;
|
||||
};
|
||||
|
||||
function findTrader (name) {
|
||||
var exchange = findExchange(name);
|
||||
return exchange.trader || exchange;
|
||||
};
|
||||
|
||||
function findWallet (name) {
|
||||
var exchange = findExchange(name);
|
||||
return exchange.wallet || exchange;
|
||||
};
|
||||
|
||||
|
||||
var Trader = module.exports = function (db) {
|
||||
if (!db) {
|
||||
throw new Error('`db` is required');
|
||||
|
|
@ -20,49 +46,18 @@ var Trader = module.exports = function (db) {
|
|||
this.rateInfo = null;
|
||||
};
|
||||
|
||||
Trader.prototype._findExchange = function (name) {
|
||||
try {
|
||||
return require('lamassu-' + name);
|
||||
|
||||
} catch(_) {
|
||||
throw new Error(name + ' module is not installed. Try running `npm install --save lamassu-' + name + '` first');
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype._findTicker = function (name) {
|
||||
var exchange = Trader.prototype._findExchange(name);
|
||||
return exchange.ticker || exchange;
|
||||
};
|
||||
|
||||
Trader.prototype._findTrader = function (name) {
|
||||
var exchange = Trader.prototype._findExchange(name);
|
||||
return exchange.trader || exchange;
|
||||
};
|
||||
|
||||
Trader.prototype._findWallet = function (name) {
|
||||
var exchange = Trader.prototype._findExchange(name);
|
||||
return exchange.wallet || exchange;
|
||||
};
|
||||
|
||||
Trader.prototype._consolidateTrades = function () {
|
||||
var queue = this._tradeQueue;
|
||||
|
||||
var tradeRec = {
|
||||
fiat: 0,
|
||||
satoshis: 0,
|
||||
currency: this.config.exchanges.settings.currency
|
||||
// NOTE: value in satoshis stays the same no matter the currency
|
||||
var consolidatedTrade = {
|
||||
currency: this.config.exchanges.settings.currency,
|
||||
satoshis: queue.reduce(function (prev, current) {
|
||||
return prev + current.satoshis;
|
||||
}, 0)
|
||||
};
|
||||
|
||||
while (true) {
|
||||
var lastRec = queue.shift();
|
||||
if (!lastRec) {
|
||||
break;
|
||||
}
|
||||
tradeRec.fiat += lastRec.fiat;
|
||||
tradeRec.satoshis += lastRec.satoshis;
|
||||
tradeRec.currency = lastRec.currency;
|
||||
}
|
||||
return tradeRec;
|
||||
return consolidatedTrade;
|
||||
};
|
||||
|
||||
Trader.prototype._purchase = function (trade, cb) {
|
||||
|
|
@ -81,20 +76,25 @@ Trader.prototype.configure = function (config) {
|
|||
throw new Error('`settings.lowBalanceMargin` has to be >= 1');
|
||||
}
|
||||
|
||||
var tickerExchangeCode = config.exchanges.plugins.current.ticker;
|
||||
var tickerExchangeConfig = config.exchanges.plugins.settings[tickerExchangeCode] || {};
|
||||
tickerExchangeConfig.currency = config.exchanges.settings.currency;
|
||||
this.tickerExchange = this._findTicker(tickerExchangeCode).factory(tickerExchangeConfig);
|
||||
var plugins = config.exchanges.plugins
|
||||
|
||||
var tradeExchangeCode = config.exchanges.plugins.current.trade;
|
||||
if (tradeExchangeCode) {
|
||||
var tradeExchangeConfig = config.exchanges.plugins.settings[tradeExchangeCode];
|
||||
this.tradeExchange = this._findTrader(tradeExchangeCode).factory(tradeExchangeConfig);
|
||||
// source of current BTC price (init and configure)
|
||||
var tickerName = plugins.current.ticker;
|
||||
var tickerConfig = plugins.settings[tickerName] || {};
|
||||
tickerConfig.currency = config.exchanges.settings.currency;
|
||||
this.tickerExchange = findTicker(tickerName).factory(tickerConfig);
|
||||
|
||||
// Exchange used for trading (init and configure)
|
||||
var traderName = plugins.current.trade;
|
||||
if (traderName) {
|
||||
var tradeConfig = plugins.settings[traderName];
|
||||
this.tradeExchange = findTrader(traderName).factory(tradeConfig);
|
||||
}
|
||||
|
||||
var transferExchangeCode = config.exchanges.plugins.current.transfer;
|
||||
var transferExchangeConfig = config.exchanges.plugins.settings[transferExchangeCode];
|
||||
this.transferExchange = this._findWallet(transferExchangeCode).factory(transferExchangeConfig);
|
||||
// Wallet (init and configure)
|
||||
var walletName = plugins.current.transfer;
|
||||
var walletConfig = plugins.settings[walletName];
|
||||
this.transferExchange = findWallet(walletName).factory(walletConfig);
|
||||
|
||||
this.config = config;
|
||||
|
||||
|
|
@ -160,9 +160,7 @@ Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) {
|
|||
var self = this;
|
||||
|
||||
self.db.summonTransaction(deviceFingerprint, tx, function (err, txRec) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (err) return cb(err);
|
||||
|
||||
if (!txRec) {
|
||||
self._clearSession(deviceFingerprint);
|
||||
|
|
@ -224,7 +222,10 @@ Trader.prototype.trade = function (rec, deviceFingerprint) {
|
|||
}, SESSION_TIMEOUT)
|
||||
};
|
||||
}
|
||||
this._tradeQueue.push({fiat: rec.fiat, satoshis: rec.satoshis, currency: rec.currency});
|
||||
this._tradeQueue.push({
|
||||
satoshis: rec.satoshis,
|
||||
currency: rec.currency
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.executeTrades = function () {
|
||||
|
|
@ -235,19 +236,12 @@ Trader.prototype.executeTrades = function () {
|
|||
var trade = this._consolidateTrades();
|
||||
logger.debug('consolidated: ', JSON.stringify(trade));
|
||||
|
||||
if (trade.fiat === 0) {
|
||||
if (trade.satoshis === 0) {
|
||||
logger.debug('rejecting 0 trade');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trade.fiat < this.config.exchanges.settings.minimumTradeFiat) {
|
||||
// throw it back in the water
|
||||
logger.debug('reject fiat too small');
|
||||
this._tradeQueue.unshift(trade);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('making a trade: %d', trade.satoshis / Math.pow(10, 8));
|
||||
logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR);
|
||||
this._purchase(trade, function (err) {
|
||||
if (err) logger.error(err);
|
||||
});
|
||||
|
|
@ -286,16 +280,14 @@ Trader.prototype.stopPolling = function () {
|
|||
Trader.prototype._tradeForexMultiplier = function _tradeForexMultiplier() {
|
||||
var deviceCurrency = this.config.exchanges.settings.currency;
|
||||
var tradeCurrency = this.tradeExchange.currency();
|
||||
if (deviceCurrency === tradeCurrency)
|
||||
return 1;
|
||||
|
||||
var deviceRate = this._deviceRate();
|
||||
var tradeRate = this._tradeRate();
|
||||
|
||||
var forexMultiplier = deviceRate && tradeRate ?
|
||||
return deviceRate && tradeRate ?
|
||||
deviceRate / tradeRate :
|
||||
null;
|
||||
|
||||
return deviceCurrency === tradeCurrency ?
|
||||
1 :
|
||||
forexMultiplier;
|
||||
};
|
||||
|
||||
Trader.prototype._tradeBalanceFunc = function _tradeBalanceFunc(callback) {
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var hock = require('hock');
|
||||
var createServer = require('../helpers/create-https-server.js');
|
||||
var assert = require('chai').assert;
|
||||
|
||||
var LamassuConfig = require('lamassu-config');
|
||||
var con = 'psql://lamassu:lamassu@localhost/lamassu';
|
||||
var config = new LamassuConfig(con);
|
||||
|
||||
var fnTable = {};
|
||||
|
||||
var app = {
|
||||
get: function(route, fn) {
|
||||
fnTable[route] = fn;
|
||||
},
|
||||
post: function(route, fn) {
|
||||
fnTable[route] = fn;
|
||||
}
|
||||
};
|
||||
|
||||
var cfg;
|
||||
var port;
|
||||
|
||||
var blockchainMock = hock.createHock();
|
||||
|
||||
// blockchain info
|
||||
var guid = '3acf1633-db4d-44a9-9013-b13e85405404';
|
||||
var pwd = 'baz';
|
||||
var bitAddr = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64';
|
||||
|
||||
|
||||
describe('send test', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
|
||||
async.parallel({
|
||||
blockchain: async.apply(createServer, blockchainMock.handler),
|
||||
config: function(cb) {
|
||||
config.load(cb);
|
||||
}
|
||||
}, function(err, results) {
|
||||
assert.isNull(err);
|
||||
|
||||
cfg = results.config;
|
||||
port = results.blockchain.address().port;
|
||||
|
||||
cfg.exchanges.plugins.current.transfer = 'blockchain';
|
||||
cfg.exchanges.plugins.settings.blockchain = {
|
||||
host: 'localhost',
|
||||
port: results.blockchain.address().port,
|
||||
rejectUnauthorized: false,
|
||||
password: pwd,
|
||||
fromAddress: bitAddr,
|
||||
guid: guid
|
||||
};
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send to blockchain', function(done) {
|
||||
this.timeout(1000000);
|
||||
|
||||
var amount= 100000000;
|
||||
|
||||
var address_reponse = {
|
||||
'hash160':'660d4ef3a743e3e696ad990364e555c271ad504b',
|
||||
'address': bitAddr,
|
||||
'n_tx': 1,
|
||||
'n_unredeemed': 1,
|
||||
'total_received': 0,
|
||||
'total_sent': 0,
|
||||
'final_balance': 0,
|
||||
'txs': []
|
||||
};
|
||||
|
||||
var payment_response = {
|
||||
'message': 'Sent 0.1 BTC to 1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64',
|
||||
'tx_hash': 'f322d01ad784e5deeb25464a5781c3b20971c1863679ca506e702e3e33c18e9c',
|
||||
'notice': 'Some funds are pending confirmation and cannot be spent yet (Value 0.001 BTC)'
|
||||
};
|
||||
|
||||
blockchainMock
|
||||
.get('/address/1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64?format=json&limit=10&password=baz')
|
||||
.reply(200, address_reponse)
|
||||
.post('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/payment?to=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&amount=100000000&from=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&password=baz')
|
||||
.reply(200, payment_response);
|
||||
|
||||
|
||||
var api = require('../../lib/protocol/atm-api');
|
||||
api.init(app, cfg);
|
||||
|
||||
var params = {
|
||||
body: {
|
||||
address: bitAddr,
|
||||
satoshis: amount
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
fnTable['/send'](params, {json: function(result) {
|
||||
assert.isNull(result.err);
|
||||
assert.equal(payment_response.tx_hash, result.results);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var hock = require('hock');
|
||||
var async = require('async');
|
||||
var createServer = require('../helpers/create-https-server.js');
|
||||
var assert = require('chai').assert;
|
||||
|
||||
var LamassuConfig = require('lamassu-config');
|
||||
var con = 'psql://lamassu:lamassu@localhost/lamassu';
|
||||
var config = new LamassuConfig(con);
|
||||
|
||||
var cfg;
|
||||
|
||||
var blockchainMock = hock.createHock();
|
||||
var bitpayMock = hock.createHock();
|
||||
|
||||
var jsonquest = require('jsonquest');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var testPort = 4000;
|
||||
|
||||
|
||||
|
||||
describe('ticker test', function(){
|
||||
|
||||
beforeEach(function(done) {
|
||||
|
||||
app.listen(testPort);
|
||||
|
||||
async.parallel({
|
||||
blockchain: async.apply(createServer, blockchainMock.handler),
|
||||
bitpay: async.apply(createServer, bitpayMock.handler),
|
||||
config: config.load.bind(config)
|
||||
}, function(err, results) {
|
||||
assert.isNull(err);
|
||||
|
||||
cfg = results.config;
|
||||
|
||||
cfg.exchanges.settings.commission = 1;
|
||||
|
||||
cfg.exchanges.plugins.current.ticker = 'bitpay';
|
||||
cfg.exchanges.plugins.current.trade = null;
|
||||
cfg.exchanges.plugins.settings.bitpay = {
|
||||
host: 'localhost',
|
||||
port: results.bitpay.address().port,
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
|
||||
cfg.exchanges.plugins.current.transfer = 'blockchain';
|
||||
cfg.exchanges.plugins.settings.blockchain = {
|
||||
host: 'localhost',
|
||||
port: results.blockchain.address().port,
|
||||
rejectUnauthorized: false,
|
||||
password: 'baz',
|
||||
fromAddress: '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64',
|
||||
guid: '3acf1633-db4d-44a9-9013-b13e85405404'
|
||||
};
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should read ticker data from bitpay', function(done) {
|
||||
this.timeout(1000000);
|
||||
|
||||
bitpayMock
|
||||
.get('/api/rates')
|
||||
.reply(200, [
|
||||
{ code: 'EUR', rate: 1337 },
|
||||
{ code: 'USD', rate: 100 }
|
||||
]);
|
||||
|
||||
blockchainMock
|
||||
.get('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/address_balance?address=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&confirmations=0&password=baz')
|
||||
.reply(200, { balance: 100000000, total_received: 100000000 })
|
||||
.get('/merchant/3acf1633-db4d-44a9-9013-b13e85405404/address_balance?address=1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64&confirmations=1&password=baz')
|
||||
.reply(200, { balance: 100000000, total_received: 100000000 });
|
||||
// That's 1 BTC.
|
||||
|
||||
var api = require('../../lib/protocol/atm-api');
|
||||
api.init(app, cfg);
|
||||
|
||||
// let ticker rate fetch finish...
|
||||
setTimeout(function() {
|
||||
jsonquest({
|
||||
host: 'localhost',
|
||||
port: testPort,
|
||||
path: '/poll/USD',//:currency
|
||||
method: 'GET',
|
||||
protocol: 'http'
|
||||
}, function (err, res, body) {
|
||||
assert.isNull(err);
|
||||
assert.equal(res.statusCode, 200);
|
||||
|
||||
assert.isNull(body.err);
|
||||
assert.equal(Number(body.rate) > 0, true);
|
||||
console.log(100 / cfg.exchanges.settings.lowBalanceMargin, body.fiat);
|
||||
assert.equal(body.fiat, 100 / cfg.exchanges.settings.lowBalanceMargin);
|
||||
|
||||
done();
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var hock = require('hock');
|
||||
|
||||
var LamassuConfig = require('lamassu-config');
|
||||
var con = 'psql://lamassu:lamassu@localhost/lamassu';
|
||||
var config = new LamassuConfig(con);
|
||||
|
||||
var fnTable = {};
|
||||
var app = { get: function(route, fn) {
|
||||
fnTable[route] = fn;
|
||||
},
|
||||
post: function(route, fn) {
|
||||
fnTable[route] = fn;
|
||||
}
|
||||
};
|
||||
var cfg;
|
||||
|
||||
var bitstampMock = hock.createHock();
|
||||
|
||||
/**
|
||||
* the tests
|
||||
*/
|
||||
describe('trade test', function(){
|
||||
|
||||
beforeEach(function(done) {
|
||||
config.load(function(err, result) {
|
||||
assert.isNull(err);
|
||||
cfg = result;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('should execute a trade against bitstamp', function(done) {
|
||||
this.timeout(1000000);
|
||||
|
||||
cfg.exchanges.plugins.trade = 'bitstamp';
|
||||
var api = require('../../lib/protocol/atm-api');
|
||||
api.init(app, cfg);
|
||||
|
||||
// schedule two trades this should result in a single consolidated trade hitting the trading system
|
||||
fnTable['/trade']({body: {fiat: 100, satoshis: 10, currency: 'USD'}}, {json: function(result) {
|
||||
console.log(result);
|
||||
}});
|
||||
|
||||
fnTable['/trade']({body: {fiat: 100, satoshis: 10, currency: 'USD'}}, {json: function(result) {
|
||||
console.log(result);
|
||||
}});
|
||||
|
||||
setTimeout(function() { done(); }, 1000000);
|
||||
// check results and execute done()
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue