From 450da4ecdcd7089c12ed64a020c0771729a18ac7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 7 Jan 2016 12:29:47 -0300 Subject: [PATCH] add fiat rate service --- lib/expressapp.js | 21 ++++- lib/fiatrateproviders/bitpay.js | 17 ++++ lib/fiatrateproviders/bitstamp.js | 11 +++ lib/fiatrateproviders/index.js | 6 ++ lib/fiatrateservice.js | 132 ++++++++++++++++++++++++++++ lib/storage.js | 34 +++++++ test/integration/fiatrateservice.js | 52 +++++++++++ 7 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 lib/fiatrateproviders/bitpay.js create mode 100644 lib/fiatrateproviders/bitstamp.js create mode 100644 lib/fiatrateproviders/index.js create mode 100644 lib/fiatrateservice.js create mode 100644 test/integration/fiatrateservice.js diff --git a/lib/expressapp.js b/lib/expressapp.js index c78b1d2..599230a 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -5,7 +5,6 @@ var async = require('async'); var log = require('npmlog'); var express = require('express'); -var querystring = require('querystring'); var bodyParser = require('body-parser') var WalletService = require('./server'); @@ -501,6 +500,26 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); + router.get('/v1/fiatrates/:code/', function(req, res) { + var opts = { + code: req.params['code'], + source: req.query.source, + ts: req.query.ts, + } + // if (_.isString(ts) && ts.indexOf(',') !== -1) { + // ts = ts.split(','); + // } + server.getFiatRate(opts, function(err, rates) { + if (err) returnError({ + code: 500, + message: err, + }); + res.json(rates); + res.end(); + }); + }); + + this.app.use(opts.basePath || '/bws/api', router); WalletService.initialize(opts, cb); diff --git a/lib/fiatrateproviders/bitpay.js b/lib/fiatrateproviders/bitpay.js new file mode 100644 index 0000000..ba58f01 --- /dev/null +++ b/lib/fiatrateproviders/bitpay.js @@ -0,0 +1,17 @@ +var _ = require('lodash'); + +var provider = { + 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; diff --git a/lib/fiatrateproviders/bitstamp.js b/lib/fiatrateproviders/bitstamp.js new file mode 100644 index 0000000..3c34ed5 --- /dev/null +++ b/lib/fiatrateproviders/bitstamp.js @@ -0,0 +1,11 @@ +var provider = { + url: 'https://www.bitstamp.net/api/ticker/', + parseFn: function(raw) { + return [{ + code: 'USD', + value: parseFloat(raw.last) + }]; + } +}; + +module.exports = provider; diff --git a/lib/fiatrateproviders/index.js b/lib/fiatrateproviders/index.js new file mode 100644 index 0000000..d361b21 --- /dev/null +++ b/lib/fiatrateproviders/index.js @@ -0,0 +1,6 @@ +var Providers = { + BitPay: require('./bitpay'), + Bitstamp: require('./bitstamp'), +} + +module.exports = Providers; diff --git a/lib/fiatrateservice.js b/lib/fiatrateservice.js new file mode 100644 index 0000000..dc57514 --- /dev/null +++ b/lib/fiatrateservice.js @@ -0,0 +1,132 @@ +'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 Utils = require('./common/utils'); +var Storage = require('./storage'); + +var Model = require('./model'); + + +var DEFAULT_PROVIDER = 'BitPay'; +var FETCH_INTERVAL = 15; // In minutes + +function FiatRateService() {}; + +FiatRateService.prototype.start = function(opts, cb) { + var self = this; + + opts = opts || {}; + + if (_.isArray(opts.providers)) { + self.providers = opts.providers; + } else { + self.providers = require('./fiatrateproviders'); + } + self.request = opts.request || request; + self.defaultProvider = opts.defaultProvider || DEFAULT_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); + } + + var interval = opts.fetchInterval || 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(_.values(self.providers), function(provider, next) { + self._retrieve(provider, function(err, res) { + if (err) { + log.warn(err); + return next(); + } + self.storage.storeFiatRate(provider.name, res, function(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) { + log.warn('Error fetching data for ' + provider.name, err); + return cb(err); + } + + log.debug('Data for ' + provider.name + ' fetched successfully'); + + if (!provider.parseFn) { + return cb('No parse function for provider ' + provider.name); + } + var rates = provider.parseFn(body); + + return cb(null, rates); + }); +}; + + +FiatRateService.prototype.getRate = function(code, opts, cb) { + var self = this; + + $.shouldBeFunction(cb); + + opts = opts || {}; + + var providerName = opts.providerName || DEFAULT_PROVIDER; + var ts = opts.ts || Date.now(); + + async.map([].concat(ts), function(ts, cb) { + self.storage.fetchFiatRate(providerName, code, ts, function(err, rate) { + if (err) return cb(err); + return cb(null, { + ts: +ts, + rate: rate + }); + }); + }, function(err, res) { + if (err) return cb(err); + if (!_.isArray(ts)) res = res[0]; + return cb(null, res); + }); +}; + + +module.exports = FiatRateService; diff --git a/lib/storage.js b/lib/storage.js index d159ece..dc2d00a 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -21,6 +21,7 @@ var collections = { PREFERENCES: 'preferences', EMAIL_QUEUE: 'email_queue', CACHE: 'cache', + FIAT_RATES: 'fiat_rates', }; var Storage = function(opts) { @@ -572,6 +573,39 @@ Storage.prototype.fetchActiveAddresses = function(walletId, cb) { }); }; +Storage.prototype.storeFiatRate = function(providerName, rates, cb) { + var self = this; + + var now = Date.now(); + async.each(rates, function(rate, next) { + self.db.collection(collections.FIAT_RATES).insert({ + provider: providerName, + ts: now, + code: rate.code, + value: rate.value, + }, { + w: 1 + }, next); + }, cb); +}; + +Storage.prototype.fetchFiatRate = function(providerName, code, ts, cb) { + var self = this; + + self.db.collection(collections.FIAT_RATES).find({ + provider: providerName, + code: code, + ts: { + $lte: ts + }, + }).sort({ + ts: -1 + }).limit(1).toArray(function(err, result) { + if (err || _.isEmpty(result)) return cb(err); + return cb(null, result[0]); + }); +}; + Storage.prototype._dump = function(cb, fn) { fn = fn || console.log; cb = cb || function() {}; diff --git a/test/integration/fiatrateservice.js b/test/integration/fiatrateservice.js new file mode 100644 index 0000000..460da7c --- /dev/null +++ b/test/integration/fiatrateservice.js @@ -0,0 +1,52 @@ +'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.start({ + storage: helpers.getStorage(), + request: request, + }, done); + }); + }); + describe.only('#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('USD', {}, function(err, res) { + should.not.exist(err); + res.rate.value.should.equal(123.45); + done(); + }); + }); + }); + }); +});