Matias Alejo Garcia
9 years ago
13 changed files with 591 additions and 2 deletions
@ -0,0 +1,16 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
'use strict'; |
|||
|
|||
var config = require('../config'); |
|||
var FiatRateService = require('../lib/fiatrateservice'); |
|||
|
|||
var service = new FiatRateService(); |
|||
service.init(config, function(err) { |
|||
if (err) throw err; |
|||
service.startCron(config, function(err) { |
|||
if (err) throw err; |
|||
|
|||
console.log('Fiat rate service started'); |
|||
}); |
|||
}); |
@ -0,0 +1,18 @@ |
|||
var _ = require('lodash'); |
|||
|
|||
var provider = { |
|||
name: 'BitPay', |
|||
url: 'https://bitpay.com/api/rates/', |
|||
parseFn: function(raw) { |
|||
var rates = _.compact(_.map(raw, function(d) { |
|||
if (!d.code || !d.rate) return null; |
|||
return { |
|||
code: d.code, |
|||
value: d.rate, |
|||
}; |
|||
})); |
|||
return rates; |
|||
}, |
|||
}; |
|||
|
|||
module.exports = provider; |
@ -0,0 +1,12 @@ |
|||
var provider = { |
|||
name: 'Bitstamp', |
|||
url: 'https://www.bitstamp.net/api/ticker/', |
|||
parseFn: function(raw) { |
|||
return [{ |
|||
code: 'USD', |
|||
value: parseFloat(raw.last) |
|||
}]; |
|||
} |
|||
}; |
|||
|
|||
module.exports = provider; |
@ -0,0 +1,6 @@ |
|||
var Providers = { |
|||
BitPay: require('./bitpay'), |
|||
Bitstamp: require('./bitstamp'), |
|||
} |
|||
|
|||
module.exports = Providers; |
@ -0,0 +1,138 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var $ = require('preconditions').singleton(); |
|||
var async = require('async'); |
|||
var log = require('npmlog'); |
|||
log.debug = log.verbose; |
|||
var request = require('request'); |
|||
|
|||
var Common = require('./common'); |
|||
var Defaults = Common.Defaults; |
|||
|
|||
var Storage = require('./storage'); |
|||
var Model = require('./model'); |
|||
|
|||
function FiatRateService() {}; |
|||
|
|||
FiatRateService.prototype.init = function(opts, cb) { |
|||
var self = this; |
|||
|
|||
opts = opts || {}; |
|||
|
|||
self.request = opts.request || request; |
|||
self.defaultProvider = opts.defaultProvider || Defaults.FIAT_RATE_PROVIDER; |
|||
|
|||
async.parallel([ |
|||
|
|||
function(done) { |
|||
if (opts.storage) { |
|||
self.storage = opts.storage; |
|||
done(); |
|||
} else { |
|||
self.storage = new Storage(); |
|||
self.storage.connect(opts.storageOpts, done); |
|||
} |
|||
}, |
|||
], function(err) { |
|||
if (err) { |
|||
log.error(err); |
|||
} |
|||
return cb(err); |
|||
}); |
|||
}; |
|||
|
|||
FiatRateService.prototype.startCron = function(opts, cb) { |
|||
var self = this; |
|||
|
|||
opts = opts || {}; |
|||
|
|||
self.providers = _.values(require('./fiatrateproviders')); |
|||
|
|||
var interval = opts.fetchInterval || Defaults.FIAT_RATE_FETCH_INTERVAL; |
|||
if (interval) { |
|||
self._fetch(); |
|||
setInterval(function() { |
|||
self._fetch(); |
|||
}, interval * 60 * 1000); |
|||
} |
|||
|
|||
return cb(); |
|||
}; |
|||
|
|||
FiatRateService.prototype._fetch = function(cb) { |
|||
var self = this; |
|||
|
|||
cb = cb || function() {}; |
|||
|
|||
async.each(self.providers, function(provider, next) { |
|||
self._retrieve(provider, function(err, res) { |
|||
if (err) { |
|||
log.warn('Error retrieving data for ' + provider.name, err); |
|||
return next(); |
|||
} |
|||
self.storage.storeFiatRate(provider.name, res, function(err) { |
|||
if (err) { |
|||
log.warn('Error storing data for ' + provider.name, err); |
|||
} |
|||
return next(); |
|||
}); |
|||
}); |
|||
}, cb); |
|||
}; |
|||
|
|||
FiatRateService.prototype._retrieve = function(provider, cb) { |
|||
var self = this; |
|||
|
|||
log.debug('Fetching data for ' + provider.name); |
|||
self.request.get({ |
|||
url: provider.url, |
|||
json: true, |
|||
}, function(err, res, body) { |
|||
if (err || !body) { |
|||
return cb(err); |
|||
} |
|||
|
|||
log.debug('Data for ' + provider.name + ' fetched successfully'); |
|||
|
|||
if (!provider.parseFn) { |
|||
return cb(new Error('No parse function for provider ' + provider.name)); |
|||
} |
|||
var rates = provider.parseFn(body); |
|||
|
|||
return cb(null, rates); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
FiatRateService.prototype.getRate = function(opts, cb) { |
|||
var self = this; |
|||
|
|||
$.shouldBeFunction(cb); |
|||
|
|||
opts = opts || {}; |
|||
|
|||
var now = Date.now(); |
|||
var provider = opts.provider || self.defaultProvider; |
|||
var ts = (_.isNumber(opts.ts) || _.isArray(opts.ts)) ? opts.ts : now; |
|||
|
|||
async.map([].concat(ts), function(ts, cb) { |
|||
self.storage.fetchFiatRate(provider, opts.code, ts, function(err, rate) { |
|||
if (err) return cb(err); |
|||
if (rate && (now - rate.ts) > Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME * 60 * 1000) rate = null; |
|||
|
|||
return cb(null, { |
|||
ts: +ts, |
|||
rate: rate ? rate.value : undefined, |
|||
fetchedOn: rate ? rate.ts : undefined, |
|||
}); |
|||
}); |
|||
}, function(err, res) { |
|||
if (err) return cb(err); |
|||
if (!_.isArray(ts)) res = res[0]; |
|||
return cb(null, res); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
module.exports = FiatRateService; |
@ -0,0 +1,296 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var async = require('async'); |
|||
|
|||
var chai = require('chai'); |
|||
var sinon = require('sinon'); |
|||
var should = chai.should(); |
|||
var log = require('npmlog'); |
|||
log.debug = log.verbose; |
|||
log.level = 'info'; |
|||
|
|||
var helpers = require('./helpers'); |
|||
|
|||
var FiatRateService = require('../../lib/fiatrateservice'); |
|||
|
|||
describe('Fiat rate service', function() { |
|||
var service, request; |
|||
|
|||
before(function(done) { |
|||
helpers.before(done); |
|||
}); |
|||
after(function(done) { |
|||
helpers.after(done); |
|||
}); |
|||
beforeEach(function(done) { |
|||
helpers.beforeEach(function() { |
|||
service = new FiatRateService(); |
|||
request = sinon.stub(); |
|||
request.get = sinon.stub(); |
|||
service.init({ |
|||
storage: helpers.getStorage(), |
|||
request: request, |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
service.startCron({}, done); |
|||
}); |
|||
}); |
|||
}); |
|||
describe('#getRate', function() { |
|||
it('should get current rate', function(done) { |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 123.45, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.rate.should.equal(123.45); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
it('should get current rate for different currency', function(done) { |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 123.45, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'EUR', |
|||
value: 345.67, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'EUR' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.rate.should.equal(345.67); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should get current rate for different provider', function(done) { |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 100.00, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.storage.storeFiatRate('Bitstamp', [{ |
|||
code: 'USD', |
|||
value: 200.00, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.rate.should.equal(100.00, 'Should use default provider'); |
|||
service.getRate({ |
|||
code: 'USD', |
|||
provider: 'Bitstamp', |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.rate.should.equal(200.00); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should get rate for specific ts', function(done) { |
|||
var clock = sinon.useFakeTimers(0, 'Date'); |
|||
clock.tick(20); |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 123.45, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
clock.tick(100); |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 345.67, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD', |
|||
ts: 50, |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.ts.should.equal(50); |
|||
res.rate.should.equal(123.45); |
|||
res.fetchedOn.should.equal(20); |
|||
clock.restore(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should get rates for a series of ts', function(done) { |
|||
var clock = sinon.useFakeTimers(0, 'Date'); |
|||
async.each([1.00, 2.00, 3.00, 4.00], function(value, next) { |
|||
clock.tick(100); |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: value, |
|||
}, { |
|||
code: 'EUR', |
|||
value: value, |
|||
}], next); |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD', |
|||
ts: [50, 100, 199, 500], |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.length.should.equal(4); |
|||
|
|||
res[0].ts.should.equal(50); |
|||
should.not.exist(res[0].rate); |
|||
should.not.exist(res[0].fetchedOn); |
|||
|
|||
res[1].ts.should.equal(100); |
|||
res[1].rate.should.equal(1.00); |
|||
res[1].fetchedOn.should.equal(100); |
|||
|
|||
res[2].ts.should.equal(199); |
|||
res[2].rate.should.equal(1.00); |
|||
res[2].fetchedOn.should.equal(100); |
|||
|
|||
res[3].ts.should.equal(500); |
|||
res[3].rate.should.equal(4.00); |
|||
res[3].fetchedOn.should.equal(400); |
|||
|
|||
clock.restore(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should not get rate older than 2hs', function(done) { |
|||
var clock = sinon.useFakeTimers(0, 'Date'); |
|||
service.storage.storeFiatRate('BitPay', [{ |
|||
code: 'USD', |
|||
value: 123.45, |
|||
}], function(err) { |
|||
should.not.exist(err); |
|||
clock.tick((120 * 60 - 1) * 1000); // Almost 2 hours
|
|||
service.getRate({ |
|||
code: 'USD', |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.rate.should.equal(123.45); |
|||
res.fetchedOn.should.equal(0); |
|||
clock.restore(); |
|||
clock.tick(2 * 1000); // 2 seconds later...
|
|||
service.getRate({ |
|||
code: 'USD', |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
should.not.exist(res.rate); |
|||
clock.restore(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('#fetch', function() { |
|||
it('should fetch rates from all providers', function(done) { |
|||
var clock = sinon.useFakeTimers(100, 'Date'); |
|||
var bitpay = [{ |
|||
code: 'USD', |
|||
rate: 123.45, |
|||
}, { |
|||
code: 'EUR', |
|||
rate: 234.56, |
|||
}]; |
|||
var bitstamp = { |
|||
last: 120.00, |
|||
}; |
|||
request.get.withArgs({ |
|||
url: 'https://bitpay.com/api/rates/', |
|||
json: true |
|||
}).yields(null, null, bitpay); |
|||
request.get.withArgs({ |
|||
url: 'https://www.bitstamp.net/api/ticker/', |
|||
json: true |
|||
}).yields(null, null, bitstamp); |
|||
|
|||
service._fetch(function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.fetchedOn.should.equal(100); |
|||
res.rate.should.equal(123.45); |
|||
service.getRate({ |
|||
code: 'USD', |
|||
provider: 'Bitstamp', |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.fetchedOn.should.equal(100); |
|||
res.rate.should.equal(120.00); |
|||
service.getRate({ |
|||
code: 'EUR' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.fetchedOn.should.equal(100); |
|||
res.rate.should.equal(234.56); |
|||
clock.restore(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should not stop when failing to fetch provider', function(done) { |
|||
var clock = sinon.useFakeTimers(100, 'Date'); |
|||
var bitstamp = { |
|||
last: 120.00, |
|||
}; |
|||
request.get.withArgs({ |
|||
url: 'https://bitpay.com/api/rates/', |
|||
json: true |
|||
}).yields('dummy error', null, null); |
|||
request.get.withArgs({ |
|||
url: 'https://www.bitstamp.net/api/ticker/', |
|||
json: true |
|||
}).yields(null, null, bitstamp); |
|||
|
|||
service._fetch(function(err) { |
|||
should.not.exist(err); |
|||
service.getRate({ |
|||
code: 'USD' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.ts.should.equal(100); |
|||
should.not.exist(res.rate) |
|||
should.not.exist(res.fetchedOn) |
|||
service.getRate({ |
|||
code: 'USD', |
|||
provider: 'Bitstamp' |
|||
}, function(err, res) { |
|||
should.not.exist(err); |
|||
res.fetchedOn.should.equal(100); |
|||
res.rate.should.equal(120.00); |
|||
clock.restore(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
Loading…
Reference in new issue