Merge pull request #7 from lamassu/atm-protocol-merge
ATM protocol merge
This commit is contained in:
commit
eae1749e88
19 changed files with 1218 additions and 39 deletions
27
bin/lamassu-server
Executable file
27
bin/lamassu-server
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env node
|
||||
var fs = require('fs');
|
||||
var createServer = require('../lib/app.js');
|
||||
var argv = require('optimist').argv;
|
||||
|
||||
var options = {
|
||||
postgres: process.env.DATABASE_URL
|
||||
};
|
||||
|
||||
var port = process.env.PORT || 3000;
|
||||
|
||||
if (!argv.http) {
|
||||
if (!argv.key || !argv.cert) {
|
||||
console.error('--key and --cert are required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
options.https = {
|
||||
key: fs.readFileSync(argv.key),
|
||||
cert: fs.readFileSync(argv.cert)
|
||||
};
|
||||
}
|
||||
|
||||
var server = createServer(options);
|
||||
server.listen(port, function () {
|
||||
console.log('lamassu-server listening on port ' + port + ' ' + (argv.http ? '(http)' : '(https)'));
|
||||
});
|
||||
91
lib/app.js
Executable file → Normal file
91
lib/app.js
Executable file → Normal file
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
/*jshint globalstrict: true, white: false, unused:false */
|
||||
/*globals require, exports, console, module, process */
|
||||
/*
|
||||
|
|
@ -21,35 +20,73 @@ var http = require('http');
|
|||
var https = require('https');
|
||||
var path = require('path');
|
||||
var express = require('express');
|
||||
var argv = require('optimist').argv;
|
||||
var app = express();
|
||||
var fs = require('fs');
|
||||
var LamassuConfig = require('lamassu-config');
|
||||
var atm = require('lamassu-atm-protocol');
|
||||
var routes = require('./routes');
|
||||
var Trader = require('./trader');
|
||||
var PostgresqlInterface = require('./postgresql_interface');
|
||||
|
||||
var conString, dbConfig, config;
|
||||
module.exports = function (options) {
|
||||
var app = express();
|
||||
var connectionString;
|
||||
var server;
|
||||
var config;
|
||||
var trader;
|
||||
var db;
|
||||
|
||||
conString = process.env.DATABASE_URL || 'postgres://lamassu:lamassu@localhost/lamassu';
|
||||
connectionString = options.postgres ||
|
||||
'postgres://lamassu:lamassu@localhost/lamassu';
|
||||
|
||||
config = new LamassuConfig(conString);
|
||||
config = new LamassuConfig(connectionString);
|
||||
db = new PostgresqlInterface(connectionString);
|
||||
trader = new Trader(db);
|
||||
|
||||
var port = process.env.PORT || 3000;
|
||||
app.use(express.logger());
|
||||
app.use(express.favicon());
|
||||
app.use(express.bodyParser());
|
||||
app.use(express.methodOverride());
|
||||
config.load(function (err, config) {
|
||||
if (err) {
|
||||
console.error('Loading config failed');
|
||||
throw err;
|
||||
}
|
||||
|
||||
config.load(function(err, conf) {
|
||||
if (err) { console.log(err); process.exit(1); }
|
||||
trader.configure(config);
|
||||
trader.startPolling();
|
||||
});
|
||||
|
||||
var authMiddleware = function (req, res, next) { return next(); };
|
||||
config.on('configUpdate', function () {
|
||||
config.load(function (err, config) {
|
||||
if (err) {
|
||||
return console.error('Error while reloading config');
|
||||
}
|
||||
|
||||
if (argv.http) {
|
||||
http.createServer(app).listen(port, function () {
|
||||
console.log('Express server listening on port ' + port + ' (http)');
|
||||
trader.configure(config);
|
||||
console.log('Config reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
app.use(express.logger());
|
||||
app.use(express.bodyParser());
|
||||
|
||||
if (!options.https) {
|
||||
server = http.createServer(app);
|
||||
}
|
||||
else {
|
||||
var serverOptions = {
|
||||
key: options.https.key,
|
||||
cert: options.https.cert,
|
||||
requestCert: true,
|
||||
secureProtocol: 'TLSv1_method',
|
||||
ciphers: 'AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH',
|
||||
honorCipherOrder: true
|
||||
};
|
||||
|
||||
server = https.createServer(serverOptions, app);
|
||||
}
|
||||
|
||||
var authMiddleware = function (req, res, next) {
|
||||
req.device = {};
|
||||
return next();
|
||||
};
|
||||
|
||||
if (options.https) {
|
||||
authMiddleware = function(req, res, next) {
|
||||
var fingerprint = req.connection.getPeerCertificate().fingerprint;
|
||||
var e = new Error('Unauthorized');
|
||||
|
|
@ -62,21 +99,9 @@ config.load(function(err, conf) {
|
|||
next();
|
||||
});
|
||||
};
|
||||
|
||||
var options = {
|
||||
key: fs.readFileSync(argv.key),
|
||||
cert: fs.readFileSync(argv.cert),
|
||||
requestCert: true,
|
||||
secureProtocol: 'TLSv1_method',
|
||||
ciphers: 'AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH',
|
||||
honorCipherOrder: true
|
||||
};
|
||||
|
||||
https.createServer(options, app).listen(port, function () {
|
||||
console.log('Express server listening on port ' + port + ' (https)');
|
||||
});
|
||||
}
|
||||
|
||||
atm.init(app, conf, config, authMiddleware);
|
||||
routes.init(app, trader, authMiddleware);
|
||||
|
||||
});
|
||||
return server;
|
||||
};
|
||||
|
|
|
|||
69
lib/postgresql_interface.js
Normal file
69
lib/postgresql_interface.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
'use strict';
|
||||
|
||||
var pg = require('pg');
|
||||
var PG_ERRORS = {
|
||||
23505: 'uniqueViolation'
|
||||
};
|
||||
|
||||
var PostgresqlInterface = function (conString) {
|
||||
if (!conString) {
|
||||
throw new Error('Postgres connection string is required');
|
||||
}
|
||||
|
||||
this.client = new pg.Client(conString);
|
||||
|
||||
// TODO better logging
|
||||
this.client.on('error', function (err) { console.log(err); });
|
||||
|
||||
this.client.connect();
|
||||
};
|
||||
PostgresqlInterface.factory = function factory(conString) { return new PostgresqlInterface(conString); };
|
||||
module.exports = PostgresqlInterface;
|
||||
|
||||
PostgresqlInterface.prototype.summonTransaction =
|
||||
function summonTransaction(deviceFingerprint, tx, cb) {
|
||||
// First do an INSERT
|
||||
// If it worked, go ahead with transaction
|
||||
// If duplicate, fetch status and return
|
||||
var self = this;
|
||||
this.client.query('INSERT INTO transactions (id, status, "deviceFingerprint", ' +
|
||||
'"toAddress", satoshis, "currencyCode", fiat) ' +
|
||||
'VALUES ($1, $2, $3, $4, $5, $6, $7)', [tx.txId, 'pending', deviceFingerprint,
|
||||
tx.toAddress, tx.satoshis, tx.currencyCode, tx.fiat],
|
||||
function (err) {
|
||||
if (err && PG_ERRORS[err.code] === 'uniqueViolation')
|
||||
return self._fetchTransaction(tx.txId, cb);
|
||||
if (err) return cb(err);
|
||||
cb(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
PostgresqlInterface.prototype.reportTransactionError =
|
||||
function reportTransactionError(tx, err) {
|
||||
this.client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3',
|
||||
['failed', err.message, tx.txId]);
|
||||
};
|
||||
|
||||
PostgresqlInterface.prototype.completeTransaction =
|
||||
function completeTransaction(tx, txHash) {
|
||||
if (txHash)
|
||||
this.client.query('UPDATE transactions SET "txHash"=$1, status=$2, completed=now() WHERE id=$3',
|
||||
[txHash, 'completed', tx.txId]);
|
||||
else
|
||||
this.client.query('UPDATE transactions SET status=$1, error=$2 WHERE id=$3',
|
||||
['failed', 'No txHash received', tx.txId]);
|
||||
};
|
||||
|
||||
PostgresqlInterface.prototype._fetchTransaction =
|
||||
function _fetchTransaction(txId, cb) {
|
||||
this.client.query('SELECT status, "txHash" FROM transactions WHERE id=$1',
|
||||
[txId], function (err, results) {
|
||||
if (err) return cb(err);
|
||||
|
||||
// This should never happen, since we already checked for existence
|
||||
if (results.rows.length === 0) return cb(new Error('Couldn\'t find transaction.'));
|
||||
|
||||
var result = results.rows[0];
|
||||
cb(null, false, result.txHash);
|
||||
});
|
||||
};
|
||||
100
lib/protocol/api/trade.js
Normal file
100
lib/protocol/api/trade.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
'use strict';
|
||||
|
||||
require('date-utils');
|
||||
var winston = require('winston');
|
||||
var _ = require('underscore');
|
||||
var logger = new (winston.Logger)({transports:[new (winston.transports.Console)()]});
|
||||
|
||||
var _tradeExchange;
|
||||
var _ticker;
|
||||
var _tradeQueue = [];
|
||||
var _api;
|
||||
var _config;
|
||||
|
||||
var SATOSHI_FACTOR = Math.pow(10, 8);
|
||||
|
||||
var _consolidateTrades = function() {
|
||||
var queue = _tradeQueue;
|
||||
var tradeRec = {
|
||||
fiat: 0,
|
||||
satoshis: 0,
|
||||
currency: 'USD'
|
||||
};
|
||||
|
||||
while (true) {
|
||||
var lastRec = queue.shift();
|
||||
if (!lastRec) {
|
||||
break;
|
||||
}
|
||||
tradeRec.fiat += lastRec.fiat;
|
||||
tradeRec.satoshis += lastRec.satoshis;
|
||||
tradeRec.currency = lastRec.currency;
|
||||
}
|
||||
return tradeRec;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TODO: add error reporting
|
||||
*/
|
||||
var _purchase = function(trade) {
|
||||
_ticker.rate(trade.currency, function(err, rate) {
|
||||
_tradeExchange.purchase(trade.satoshis, rate, function(err) {
|
||||
_api.triggerBalance();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.init = function(config, api, tradeExchange, ticker) {
|
||||
_config = config;
|
||||
_api = api;
|
||||
_tradeExchange = tradeExchange;
|
||||
_ticker = ticker;
|
||||
|
||||
var interval = setInterval(function() {
|
||||
exports.executeTrades();
|
||||
}, _config.settings.tradeInterval);
|
||||
interval.unref();
|
||||
};
|
||||
|
||||
exports.trade = function(fiat, satoshis, currency, cb) {
|
||||
_tradeQueue.push({fiat: fiat, satoshis: satoshis, currency: currency});
|
||||
cb(null);
|
||||
};
|
||||
|
||||
exports.queueFiatBalance = function(exchangeRate) {
|
||||
var satoshis = _.reduce(_tradeQueue, function(memo, rec) {
|
||||
return memo + rec.satoshis;
|
||||
}, 0);
|
||||
return (satoshis / SATOSHI_FACTOR) * exchangeRate;
|
||||
};
|
||||
|
||||
exports.executeTrades = function() {
|
||||
if (!_tradeExchange) return;
|
||||
|
||||
logger.info('checking for trades');
|
||||
|
||||
if (!_config.plugins.current.trade) {
|
||||
logger.info('NO ENGINE');
|
||||
return;
|
||||
}
|
||||
|
||||
var trade = _consolidateTrades();
|
||||
logger.info('consolidated: ' + JSON.stringify(trade));
|
||||
|
||||
if (trade.fiat === 0) {
|
||||
logger.info('reject fiat 0');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trade.fiat < _config.settings.minimumTradeFiat) {
|
||||
// throw it back in the water
|
||||
logger.info('reject fiat too small');
|
||||
_tradeQueue.unshift(trade);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('making a trade: %d', trade.satoshis / Math.pow(10,8));
|
||||
_purchase(trade);
|
||||
};
|
||||
45
lib/protocol/exchanges/custom_ticker.js
Normal file
45
lib/protocol/exchanges/custom_ticker.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
'use strict';
|
||||
|
||||
// TODO: refactor this with bitpay_ticker.js
|
||||
|
||||
var https = require('https');
|
||||
var _ = require('underscore');
|
||||
|
||||
var CustomTicker = function(config) {
|
||||
this.config = config;
|
||||
};
|
||||
|
||||
CustomTicker.factory = function factory(config) {
|
||||
return new CustomTicker(config);
|
||||
};
|
||||
|
||||
CustomTicker.prototype.ticker = function ticker(currency, cb) {
|
||||
var self = this;
|
||||
https.get(this.config.uri, function(res) {
|
||||
var buf = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', function(chunk) {
|
||||
buf += chunk;
|
||||
})
|
||||
.on('end', function() {
|
||||
var json = null;
|
||||
try {
|
||||
json = JSON.parse(buf);
|
||||
} catch(e) {
|
||||
cb(new Error('Couldn\'t parse JSON response'));
|
||||
return;
|
||||
}
|
||||
var rec = _.findWhere(json, {code: currency});
|
||||
|
||||
if (!rec) {
|
||||
cb(new Error('Currency not listed: ' + currency));
|
||||
return;
|
||||
}
|
||||
cb(null, rec.rate);
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
cb(e);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CustomTicker;
|
||||
95
lib/routes.js
Normal file
95
lib/routes.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
'use strict';
|
||||
|
||||
var _trader;
|
||||
var _lamassuConfig;
|
||||
|
||||
// Make sure these are higher than polling interval
|
||||
// or there will be a lot of errors
|
||||
var STALE_TICKER = 180000;
|
||||
var STALE_BALANCE = 180000;
|
||||
|
||||
Error.prototype.toJSON = function () {
|
||||
var self = this;
|
||||
var ret = {};
|
||||
Object.getOwnPropertyNames(self).forEach(function (key) {
|
||||
ret[key] = self[key];
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
var poll = function(req, res) {
|
||||
if (req.device.unpair) {
|
||||
return res.json({
|
||||
unpair: true
|
||||
});
|
||||
}
|
||||
|
||||
var rateRec = _trader.rate(req.params.currency);
|
||||
var satoshiBalanceRec = _trader.balance;
|
||||
|
||||
// `rateRec` and `satoshiBalanceRec` are both objects, so there's no danger
|
||||
// of misinterpreting rate or balance === 0 as 'Server initializing'.
|
||||
if (!rateRec || !satoshiBalanceRec) {
|
||||
return res.json({err: 'Server initializing'});
|
||||
}
|
||||
|
||||
if (Date.now() - rateRec.timestamp > STALE_TICKER) {
|
||||
return res.json({err: 'Stale ticker'});
|
||||
}
|
||||
|
||||
if (Date.now() - rateRec.timestamp > STALE_BALANCE) {
|
||||
return res.json({err: 'Stale balance'});
|
||||
}
|
||||
|
||||
var rate = rateRec.rate;
|
||||
|
||||
res.json({
|
||||
err: null,
|
||||
rate: rate * _trader.config.exchanges.settings.commission,
|
||||
fiat: _trader.fiatBalance(0, 0),
|
||||
currency: req.params.currency,
|
||||
txLimit: parseInt(_trader.config.exchanges.settings.compliance.maximum.limit, 10)
|
||||
});
|
||||
};
|
||||
|
||||
var trade = function (req, res) {
|
||||
_trader.trade(req.body.fiat, req.body.satoshis, req.body.currency, function(err) {
|
||||
res.json({err: err});
|
||||
});
|
||||
};
|
||||
|
||||
var send = function(req, res) {
|
||||
var fingerprint = req.connection.getPeerCertificate().fingerprint;
|
||||
_trader.sendBitcoins(fingerprint, req.body, function(err, txHash) {
|
||||
res.json({err: err, txHash: txHash});
|
||||
});
|
||||
};
|
||||
|
||||
var pair = function(req, res) {
|
||||
var token = req.body.token;
|
||||
var name = req.body.name;
|
||||
|
||||
_lamassuConfig.pair(
|
||||
token,
|
||||
req.connection.getPeerCertificate().fingerprint,
|
||||
name,
|
||||
function(err) {
|
||||
if (err) {
|
||||
return res.json(500, { err: err.message });
|
||||
}
|
||||
|
||||
res.json(200);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
exports.init = function(app, trader, authMiddleware) {
|
||||
_trader = trader;
|
||||
|
||||
app.get('/poll/:currency', authMiddleware, poll);
|
||||
app.post('/send', authMiddleware, send);
|
||||
app.post('/trade', authMiddleware, trade);
|
||||
app.post('/pair', pair);
|
||||
|
||||
return app;
|
||||
};
|
||||
245
lib/trader.js
Normal file
245
lib/trader.js
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
|
||||
var SATOSHI_FACTOR = Math.pow(10, 8);
|
||||
|
||||
var Trader = module.exports = function (db) {
|
||||
if (!db) {
|
||||
throw new Error('`db` is required');
|
||||
}
|
||||
|
||||
this.db = db;
|
||||
this.rates = {};
|
||||
this.logger = new (winston.Logger)({
|
||||
transports: [new (winston.transports.Console)()]
|
||||
});
|
||||
|
||||
this._tradeQueue = [];
|
||||
};
|
||||
|
||||
Trader.prototype._findExchange = function (name) {
|
||||
var exchange;
|
||||
|
||||
try {
|
||||
exchange = require('lamassu-' + name);
|
||||
} catch (err) {
|
||||
if (!err.message.match(/Cannot find module/)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
exchange = require(path.join(path.dirname(__dirname), 'exchanges', name));
|
||||
}
|
||||
|
||||
return exchange;
|
||||
};
|
||||
|
||||
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.configure = function (config) {
|
||||
if (config.exchanges.settings.lowBalanceMargin < 1) {
|
||||
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 tradeExchangeCode = config.exchanges.plugins.current.trade;
|
||||
if (tradeExchangeCode) {
|
||||
var tradeExchangeConfig = config.exchanges.plugins.settings[tradeExchangeCode];
|
||||
this.tradeExchange = this._findTrader(tradeExchangeCode).factory(tradeExchangeConfig);
|
||||
}
|
||||
|
||||
var transferExchangeCode = config.exchanges.plugins.current.transfer;
|
||||
var transferExchangeConfig = config.exchanges.plugins.settings[transferExchangeCode];
|
||||
this.transferExchange = this._findWallet(transferExchangeCode).factory(transferExchangeConfig);
|
||||
|
||||
this.config = config;
|
||||
};
|
||||
|
||||
/**
|
||||
* return fiat balance
|
||||
*
|
||||
* in input to this function, balance has the following parameters...
|
||||
*
|
||||
* balance.transferBalance - in satoshis
|
||||
* balance.tradeBalance - in USD
|
||||
*
|
||||
* Have added conversion here, but this really needs to be thought through, lamassu-bitstamp should perhaps
|
||||
* return balance in satoshis
|
||||
*/
|
||||
Trader.prototype.fiatBalance = function (transferSatoshis, tradeFiat) {
|
||||
var rate = this.rate(this.config.exchanges.settings.currency).rate;
|
||||
var balance = this.balance;
|
||||
var commission = this.config.exchanges.settings.commission;
|
||||
|
||||
if (!rate || !balance) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The rate is actually our commission times real rate.
|
||||
rate = commission * rate;
|
||||
|
||||
// `lowBalanceMargin` is our safety net. It's a number > 1, and we divide
|
||||
// all our balances by it to provide a safety margin.
|
||||
var lowBalanceMargin = this.config.exchanges.settings.lowBalanceMargin;
|
||||
|
||||
// `balance.transferBalance` is the balance of our transfer account (the one
|
||||
// we use to send Bitcoins to clients). `transferSatoshis` is the number
|
||||
// of satoshis we're expected to send for this transaction. By subtracting
|
||||
// them, we get `adjustedTransferBalance`, amount of satoshis we'll have
|
||||
// after the transaction.
|
||||
var adjustedTransferBalance = balance.transferBalance - transferSatoshis;
|
||||
|
||||
// Since `adjustedTransferBalance` is in Satoshis, we need to turn it into
|
||||
// Bitcoins and then fiat to learn how much fiat currency we can exchange.
|
||||
//
|
||||
// Unit validity proof: [ $ ] = [ (B * 10^8) / 10^8 * $/B ]
|
||||
// [ $ ] = [ B * $/B ]
|
||||
// [ $ ] = [ $ ]
|
||||
var fiatTransferBalance = ((adjustedTransferBalance / SATOSHI_FACTOR) * rate) / lowBalanceMargin;
|
||||
|
||||
return fiatTransferBalance;
|
||||
};
|
||||
|
||||
Trader.prototype.sendBitcoins = function (deviceFingerprint, tx, cb) {
|
||||
var self = this;
|
||||
|
||||
self.db.summonTransaction(deviceFingerprint, tx, function (err, isNew, txHash) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
return self.transferExchange.sendBitcoins(
|
||||
tx.toAddress,
|
||||
tx.satoshis,
|
||||
self.config.exchanges.settings.transactionFee,
|
||||
function(err, txHash) {
|
||||
if (err) {
|
||||
self.db.reportTransactionError(tx, err);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
cb(null, txHash);
|
||||
self.db.completeTransaction(tx, txHash);
|
||||
self.pollRate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// transaction exists, but txHash might be null,
|
||||
// in which case ATM should continue polling
|
||||
cb(null, txHash);
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.trade = function (fiat, satoshis, currency, callback) {
|
||||
this._tradeQueue.push({fiat: fiat, satoshis: satoshis, currency: currency});
|
||||
callback(null);
|
||||
};
|
||||
|
||||
Trader.prototype.tradeQueueFiatBalance = function (exchangeRate) {
|
||||
var satoshis = this._tradeQueue.reduce(function (memo, rec) {
|
||||
return memo + rec.satoshis;
|
||||
}, 0);
|
||||
return (satoshis / SATOSHI_FACTOR) * exchangeRate;
|
||||
};
|
||||
|
||||
Trader.prototype._consolidateTrades = function () {
|
||||
var queue = this._tradeQueue;
|
||||
|
||||
var tradeRec = {
|
||||
fiat: 0,
|
||||
satoshis: 0,
|
||||
currency: 'USD'
|
||||
};
|
||||
|
||||
while (true) {
|
||||
var lastRec = queue.shift();
|
||||
if (!lastRec) {
|
||||
break;
|
||||
}
|
||||
tradeRec.fiat += lastRec.fiat;
|
||||
tradeRec.satoshis += lastRec.satoshis;
|
||||
tradeRec.currency = lastRec.currency;
|
||||
}
|
||||
return tradeRec;
|
||||
};
|
||||
|
||||
Trader.prototype.startPolling = function () {
|
||||
this.pollBalance();
|
||||
this.pollRate();
|
||||
|
||||
this.balanceInterval = setInterval(this.pollBalance.bind(this), 60 * 1000);
|
||||
this.rateInterval = setInterval(this.pollRate.bind(this), 60 * 1000);
|
||||
};
|
||||
|
||||
Trader.prototype.stopPolling = function () {
|
||||
clearInterval(this.balanceInterval);
|
||||
clearInterval(this.rateInterval);
|
||||
};
|
||||
|
||||
Trader.prototype.pollBalance = function (callback) {
|
||||
var self = this;
|
||||
|
||||
self.logger.info('collecting balance');
|
||||
|
||||
async.parallel({
|
||||
transferBalance: self.transferExchange.balance.bind(self.transferExchange),
|
||||
tradeBalance: function (next) {
|
||||
if (!self.tradeExchange) {
|
||||
return next(null, null);
|
||||
}
|
||||
|
||||
self.tradeExchange.balance(next);
|
||||
}
|
||||
}, function (err, balance) {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
balance.timestamp = Date.now();
|
||||
self.logger.info('Balance update:', balance);
|
||||
self.balance = balance;
|
||||
|
||||
return callback && callback(null, balance);
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.pollRate = function (callback) {
|
||||
var self = this;
|
||||
|
||||
var currency = self.config.exchanges.settings.currency;
|
||||
self.logger.info('polling for rate...');
|
||||
self.tickerExchange.ticker(currency, function(err, rate) {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
self.logger.info('Rate update:', rate);
|
||||
self.rates[currency] = {rate: rate, timestamp: new Date()};
|
||||
return callback && callback(null, self.rates[currency]);
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.rate = function (currency) {
|
||||
return this.rates[currency];
|
||||
};
|
||||
25
package.json
25
package.json
|
|
@ -15,23 +15,36 @@
|
|||
"express": "~3.4.7",
|
||||
"optimist": "~0.6.0",
|
||||
"lamassu-config": "~0.2.0",
|
||||
"lamassu-atm-protocol": "~0.2.0"
|
||||
"lodash": "~2.4.1",
|
||||
"async": "~0.2.9",
|
||||
"deepmerge": "~0.2.7",
|
||||
"underscore": "~1.5.2",
|
||||
"error-create": "0.0.0",
|
||||
"date-utils": "~1.2.15",
|
||||
"bitstamp": "~0.1.3",
|
||||
"winston": "~0.7.2",
|
||||
"pg": "~2.11.1",
|
||||
"lamassu-bitpay": "~0.0.1",
|
||||
"lamassu-bitstamp": "~0.0.1",
|
||||
"lamassu-mtgox": "~0.0.1",
|
||||
"lamassu-blockchain": "0.0.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lamassu/lamassu-server.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-contrib-jshint": "~0.6.0",
|
||||
"load-grunt-tasks": "~0.1.0",
|
||||
"chai": "~1.8.1",
|
||||
"matchdep": "~0.3.0",
|
||||
"mocha": "~1.13.0",
|
||||
"grunt-mocha-test": "~0.7.0",
|
||||
"grunt-mocha-cov": "0.0.7"
|
||||
"hock": "git+https://github.com/mmalecki/hock.git#no-http-server",
|
||||
"jsonquest": "^0.2.2",
|
||||
"node-uuid": "^1.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"lamassu-server": "./lib/app.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --recursive test"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
125
test/api/sendTest.js
Normal file
125
test/api/sendTest.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
119
test/api/tickerTest.js
Normal file
119
test/api/tickerTest.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
70
test/api/tradeTest.js
Normal file
70
test/api/tradeTest.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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()
|
||||
});
|
||||
});
|
||||
14
test/helpers/create-https-server.js
Normal file
14
test/helpers/create-https-server.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var https = require('https');
|
||||
var fixtures = path.join(__dirname, '..', 'fixtures');
|
||||
|
||||
module.exports = function(handler, callback) {
|
||||
var server = https.createServer({
|
||||
key: fs.readFileSync(path.join(fixtures, 'privatekey.pem')),
|
||||
cert: fs.readFileSync(path.join(fixtures, 'certificate.pem'))
|
||||
}, handler);
|
||||
server.listen(0, function() {
|
||||
callback(null, server);
|
||||
});
|
||||
};
|
||||
54
test/unit/traderApiTest.js
Normal file
54
test/unit/traderApiTest.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var Trader = require('../../lib/trader.js');
|
||||
var PostgresqlInterface = require('../../lib/postgresql_interface.js');
|
||||
|
||||
var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
|
||||
var psqlInterface = new PostgresqlInterface(db);
|
||||
|
||||
describe('trader/api', function () {
|
||||
it('should throw when trying to create a trader with no DB', function () {
|
||||
assert.throws(function () {
|
||||
new Trader();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when trying to configure a trader with `lowBalanceMargin` < 1', function () {
|
||||
var trader = new Trader(psqlInterface);
|
||||
assert.throws(function () {
|
||||
trader.configure({
|
||||
exchanges: {
|
||||
settings: {
|
||||
lowBalanceMargin: 0.8
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find and instantiate ticker and trade exchanges', function () {
|
||||
var trader = new Trader(psqlInterface);
|
||||
trader.configure({
|
||||
exchanges: {
|
||||
plugins: {
|
||||
current: {
|
||||
ticker: 'bitpay',
|
||||
transfer: 'blockchain'
|
||||
},
|
||||
settings: {
|
||||
bitpay: {},
|
||||
blockchain: {}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
currency: 'USD',
|
||||
lowBalanceMargin: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(trader.tickerExchange);
|
||||
assert.ok(trader.transferExchange);
|
||||
});
|
||||
});
|
||||
48
test/unit/traderFiatBalanceTest.js
Normal file
48
test/unit/traderFiatBalanceTest.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var Trader = require('../../lib/trader.js');
|
||||
var PostgresqlInterface = require('../../lib/postgresql_interface.js');
|
||||
|
||||
var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
|
||||
var psqlInterface = new PostgresqlInterface(db);
|
||||
|
||||
var RATE = 101;
|
||||
var CURRENCY = 'USD';
|
||||
var SATOSHI_FACTOR = Math.pow(10, 8);
|
||||
var LOW_BALANCE_MARGIN = 1.2;
|
||||
var COMMISSION = 1.1;
|
||||
|
||||
var settings = {
|
||||
currency: CURRENCY,
|
||||
lowBalanceMargin: LOW_BALANCE_MARGIN,
|
||||
commission: COMMISSION
|
||||
};
|
||||
|
||||
describe('trader/fiatBalance', function() {
|
||||
it('should calculate balance correctly with transfer exchange only', function() {
|
||||
var trader = new Trader(db);
|
||||
trader.configure({
|
||||
exchanges: {
|
||||
plugins: {
|
||||
current: {
|
||||
transfer: 'blockchain',
|
||||
ticker: 'bitpay'
|
||||
},
|
||||
settings: { blockchain: {}, bitpay: {} }
|
||||
},
|
||||
settings: settings
|
||||
}
|
||||
});
|
||||
|
||||
// We have 3 bitcoins, want to trade 1 bitcoin for 100 fiat
|
||||
trader.balance = {
|
||||
transferBalance: 3 * SATOSHI_FACTOR,
|
||||
tradeBalance: null
|
||||
};
|
||||
trader.rates[CURRENCY] = { rate: RATE };
|
||||
|
||||
var balance = trader.fiatBalance(1 * SATOSHI_FACTOR, 100);
|
||||
assert.equal(balance, (202 / LOW_BALANCE_MARGIN) * COMMISSION);
|
||||
});
|
||||
});
|
||||
75
test/unit/traderSendTest.js
Normal file
75
test/unit/traderSendTest.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var hock = require('hock');
|
||||
var uuid = require('node-uuid').v4;
|
||||
var Trader = require('../../lib/trader.js');
|
||||
var PostgresqlInterface = require('../../lib/postgresql_interface.js');
|
||||
|
||||
var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
|
||||
var psqlInterface = new PostgresqlInterface(db);
|
||||
|
||||
var TRANSACTION_FEE = 1;
|
||||
var FINGERPRINT = 'CB:3D:78:49:03:39:BA:47:0A:33:29:3E:31:25:F7:C6:4F:74:71:D7';
|
||||
var TXID = '216dabdb692670bae940deb71e59486038a575f637903d3c9af601ddd48057fc';
|
||||
var ADDRESS = '1LhkU2R8nJaU8Zj6jB8VjWrMpvVKGqCZ64';
|
||||
var SATOSHIS = 1337;
|
||||
var CURRENCY = 'USD';
|
||||
|
||||
var OUR_TXID = uuid();
|
||||
|
||||
describe('trader/send', function () {
|
||||
var trader = new Trader(psqlInterface);
|
||||
trader.config = {
|
||||
exchanges: {
|
||||
settings: {
|
||||
transactionFee: TRANSACTION_FEE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
trader.pollRate = function () {};
|
||||
|
||||
it('should call `sendBitcoins` on the transfer exchange', function (done) {
|
||||
trader.transferExchange = {
|
||||
sendBitcoins: function (address, satoshis, transactionFee, callback) {
|
||||
assert.equal(ADDRESS, address);
|
||||
assert.equal(SATOSHIS, satoshis);
|
||||
assert.equal(transactionFee, TRANSACTION_FEE);
|
||||
callback(null, TXID);
|
||||
}
|
||||
};
|
||||
|
||||
trader.sendBitcoins(FINGERPRINT, {
|
||||
fiat: 100,
|
||||
txId: OUR_TXID,
|
||||
currencyCode: CURRENCY,
|
||||
toAddress: ADDRESS,
|
||||
satoshis: SATOSHIS
|
||||
}, function (err, txId) {
|
||||
assert.notOk(err);
|
||||
assert.equal(txId, TXID);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call `sendBitcoins` on the transfer exchange with same send', function (done) {
|
||||
trader.transferExchange = {
|
||||
sendBitcoins: function () {
|
||||
throw new Error('This should not have been called');
|
||||
}
|
||||
};
|
||||
|
||||
trader.sendBitcoins(FINGERPRINT, {
|
||||
fiat: 100,
|
||||
txId: OUR_TXID,
|
||||
currencyCode: CURRENCY,
|
||||
toAddress: ADDRESS,
|
||||
satoshis: SATOSHIS
|
||||
}, function (err, txId) {
|
||||
assert.notOk(err);
|
||||
assert.equal(txId, TXID);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
55
test/unit/traderTickerTest.js
Normal file
55
test/unit/traderTickerTest.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var hock = require('hock');
|
||||
var uuid = require('node-uuid').v4;
|
||||
var Trader = require('../../lib/trader.js');
|
||||
var PostgresqlInterface = require('../../lib/postgresql_interface.js');
|
||||
|
||||
var db = 'psql://lamassu:lamassu@localhost/lamassu-test';
|
||||
var psqlInterface = new PostgresqlInterface(db);
|
||||
|
||||
var CURRENCY = 'USD';
|
||||
|
||||
describe('trader/send', function () {
|
||||
var trader = new Trader(psqlInterface);
|
||||
trader.config = {
|
||||
exchanges: {
|
||||
settings: { currency: CURRENCY }
|
||||
}
|
||||
};
|
||||
|
||||
it('should call `balance` on the transfer exchange', function (done) {
|
||||
trader.transferExchange = {
|
||||
balance: function (callback) {
|
||||
callback(null, 100);
|
||||
}
|
||||
};
|
||||
|
||||
trader.pollBalance(function (err, balance) {
|
||||
assert.notOk(err);
|
||||
assert.equal(trader.balance.transferBalance, 100);
|
||||
assert.ok(trader.balance.timestamp);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `ticker` on the ticker exchange', function (done) {
|
||||
trader.tickerExchange = {
|
||||
ticker: function (currency, callback) {
|
||||
assert.equal(currency, CURRENCY);
|
||||
callback(null, 100);
|
||||
}
|
||||
};
|
||||
|
||||
trader.pollRate(function (err, rate) {
|
||||
var rate;
|
||||
|
||||
assert.notOk(err);
|
||||
rate = trader.rate(CURRENCY);
|
||||
assert.equal(rate.rate, 100);
|
||||
assert.ok(rate.timestamp);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue