From 476f6395e5e391931aac96a857375cf284e24a74 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 5 Nov 2014 12:57:21 -0300 Subject: [PATCH] rate service + tests --- copay.js | 1 + js/models/RateService.js | 125 +++++++++++++++++++++++++++ test/RateService.js | 179 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 js/models/RateService.js create mode 100644 test/RateService.js diff --git a/copay.js b/copay.js index f3d332743..bdb5c759b 100644 --- a/copay.js +++ b/copay.js @@ -12,6 +12,7 @@ module.exports.logger = require('./js/log'); // components var Async = module.exports.Async = require('./js/models/Async'); var Insight = module.exports.Insight = require('./js/models/Insight'); +var RateService = module.exports.RateService = require('./js/models/RateService'); module.exports.Identity = require('./js/models/Identity'); module.exports.Wallet = require('./js/models/Wallet'); diff --git a/js/models/RateService.js b/js/models/RateService.js new file mode 100644 index 000000000..267631419 --- /dev/null +++ b/js/models/RateService.js @@ -0,0 +1,125 @@ +'use strict'; + +var util = require('util'); +var _ = require('lodash'); +var log = require('../log'); +var preconditions = require('preconditions').singleton(); +var request = require('request'); + +/* + This class lets interfaces with BitPay's exchange rate API. +*/ + +var RateService = function(opts) { + var self = this; + + self.SAT_TO_BTC = 1 / 1e8; + self.BTC_TO_SAT = 1e8; + self.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable() or use service.whenAvailable()'; + self.UNSUPPORTED_CURRENCY_ERROR = 'Currency not supported'; + + self._isAvailable = false; + self._rates = {}; + self._alternatives = {}; + self.queued = []; + + self._fetchCurrencies(); +} + +RateService.prototype._fetchCurrencies = function() { + var self = this; + + var backoffSeconds = 5; + var updateFrequencySeconds = 3600; + var rateServiceUrl = 'https://bitpay.com/api/rates'; + + request.get({ + url: rateServiceUrl, + json: true + }, function(err, res, body) { + if (err || !body) { + backoffSeconds *= 1.5; + setTimeout(retrieve, backoffSeconds * 1000); + return; + } + _.each(body, function(currency) { + self.rates[currency.code] = currency.rate; + self.alternatives.push({ + name: currency.name, + isoCode: currency.code, + rate: currency.rate + }); + }); + self._isAvailable = true; + _.each(self.queued, function(callback) { + setTimeout(callback, 1); + }); + setTimeout(function() { + self._fetchCurrencies() + }, updateFrequencySeconds * 1000); + }); +}; + +RateService.prototype._getRate = function(code) { + return this._rates[code]; +}; + +RateService.prototype._getHistoricRate = function(code, date, cb) { + // TODO (isocolsky): implement with a remote call + return cb(new Error('Not implemented')); +}; + +RateService.prototype._getAlternatives = function() { + return this._alternatives; +}; + +RateService.prototype.isAvailable = function() { + return this._isAvailable; +}; + +RateService.prototype.whenAvailable = function(callback) { + if (!this.isAvailable()) { + setTimeout(callback, 1); + } else { + this.queued.push(callback); + } +}; + +RateService.prototype.toFiat = function(satoshis, code) { + if (!this.isAvailable()) { + throw new Error(this.UNAVAILABLE_ERROR); + } + return satoshis * this.SAT_TO_BTC * this._getRate(code); +}; + +RateService.prototype.toFiatHistoric = function(satoshis, code, date, cb) { + var self = this; + + self._getHistoricRate(code, date, function(err, rate) { + if (err) return cb(err); + return cb(null, satoshis * self.SAT_TO_BTC * rate); + }); +}; + +RateService.prototype.fromFiat = function(amount, code) { + if (!this.isAvailable()) { + throw new Error(this.UNAVAILABLE_ERROR); + } + return amount / this._getRate(code) * this.BTC_TO_SAT; +}; + +RateService.prototype.listAlternatives = function() { + if (!this.isAvailable()) { + throw new Error(this.UNAVAILABLE_ERROR); + } + + return _.map(this._getAlternatives(), function(item) { + return { + name: item.name, + isoCode: item.isoCode + } + }); +}; + + +module.exports = RateService; diff --git a/test/RateService.js b/test/RateService.js new file mode 100644 index 000000000..3ebb9a5dc --- /dev/null +++ b/test/RateService.js @@ -0,0 +1,179 @@ +'use strict'; + +var RateService = copay.RateService; + +describe('RateService model', function() { + before(function() { + sinon.stub(RateService.prototype, '_fetchCurrencies').returns(); + }); + after(function() {}); + + it('should create an instance', function() { + var rs = new RateService(); + should.exist(rs); + }); + + describe('#toFiat', function() { + it('should throw error when unavailable', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(false); + (function() { + rs.toFiat(10000, 'USD'); + }).should.throw; + }); + it('should return current valuation', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(true); + var getRateStub = sinon.stub(rs, '_getRate') + getRateStub.withArgs('USD').returns(300.00); + getRateStub.withArgs('EUR').returns(250.00); + var params = [{ + satoshis: 0, + code: 'USD', + expected: '0.00' + }, { + satoshis: 1e8, + code: 'USD', + expected: '300.00' + }, { + satoshis: 10000, + code: 'USD', + expected: '0.03' + }, { + satoshis: 20000, + code: 'EUR', + expected: '0.05' + }, ]; + + _.each(params, function(p) { + rs.toFiat(p.satoshis, p.code).toFixed(2).should.equal(p.expected); + }); + }); + }); + + describe('#toFiatHistoric', function() { + it('should return historic valuation', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(true); + var today = Date.now(); + var yesterday = today - 24 * 3600; + var getHistoricalRateStub = sinon.stub(rs, '_getHistoricRate'); + getHistoricalRateStub.withArgs('USD', today).yields(null, 300.00); + getHistoricalRateStub.withArgs('USD', yesterday).yields(null, 250.00); + getHistoricalRateStub.withArgs('EUR', today).yields(null, 250.00); + getHistoricalRateStub.withArgs('EUR', yesterday).yields(null, 200.00); + var params = [{ + satoshis: 0, + code: 'USD', + date: today, + expected: '0.00' + }, { + satoshis: 1e8, + code: 'USD', + date: today, + expected: '300.00' + }, { + satoshis: 10000, + code: 'USD', + date: today, + expected: '0.03' + }, { + satoshis: 0, + code: 'USD', + date: today, + expected: '0.00' + }, { + satoshis: 1e8, + code: 'USD', + date: today, + expected: '300.00' + }, { + satoshis: 10000, + code: 'USD', + date: today, + expected: '0.03' + }, { + satoshis: 20000, + code: 'EUR', + date: today, + expected: '0.05' + }, { + satoshis: 20000, + code: 'EUR', + date: yesterday, + expected: '0.04' + }, ]; + + _.each(params, function(p) { + rs.toFiatHistoric(p.satoshis, p.code, p.date, function(err, rate) { + rate.toFixed(2).should.equal(p.expected); + }); + }); + }); + }); + + describe('#fromFiat', function() { + it('should throw error when unavailable', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(false); + (function() { + rs.fromFiat(300, 'USD'); + }).should.throw; + }); + it('should return current valuation', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(true); + var getRateStub = sinon.stub(rs, '_getRate') + getRateStub.withArgs('USD').returns(300.00); + getRateStub.withArgs('EUR').returns(250.00); + var params = [{ + amount: 0, + code: 'USD', + expected: 0 + }, { + amount: 300.00, + code: 'USD', + expected: 1e8 + }, { + amount: 600.00, + code: 'USD', + expected: 2e8 + }, { + amount: 250.00, + code: 'EUR', + expected: 1e8 + }, ]; + + _.each(params, function(p) { + rs.fromFiat(p.amount, p.code).should.equal(p.expected); + }); + }); + }); + describe('#listAlternatives', function() { + it('should throw error when unavailable', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(false); + (function() { + rs.listAlternatives(); + }).should.throw; + }); + + it('should return list of available currencies', function() { + var rs = new RateService(); + rs.isAvailable = sinon.stub().returns(true); + sinon.stub(rs, '_getAlternatives').returns([{ + name: 'United States Dollar', + isoCode: 'USD', + rate: 300.00, + }, { + name: 'European Union Euro', + isoCode: 'EUR', + rate: 250.00, + }, ]) + + var list = rs.listAlternatives(); + list.should.exist; + list.length.should.equal(2); + }); + }); +});