Merge pull request #1730 from isocolsky/history

Refactor rate service as model
This commit is contained in:
Matias Alejo Garcia 2014-11-07 11:59:13 -03:00
commit 692852617f
13 changed files with 361 additions and 122 deletions

View file

@ -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');

View file

@ -59,7 +59,7 @@ angular.module('copayApp.controllers').controller('HistoryController',
return;
}
_.each(res, function (r) {
_.each(res, function(r) {
r.ts = r.minedTs || r.sentTs;
if (r.action === 'sent' && r.peerActions) {
r.actionList = controllerUtils.getActionList(r.peerActions);
@ -82,14 +82,6 @@ angular.module('copayApp.controllers').controller('HistoryController',
var w = $rootScope.wallet;
return w.getNetworkName().substring(0, 4);
};
$scope.amountAlternative = function(amount, txIndex, cb) {
var w = $rootScope.wallet;
rateService.whenAvailable(function() {
var valueSat = amount * w.settings.unitToSatoshi;
$scope.alternativeCurrency[txIndex] = rateService.toFiat(valueSat, w.settings.alternativeIsoCode);
return cb ? cb() : null;
});
};
// Autoload transactions
$scope.getTransactions();

View file

@ -25,7 +25,6 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.rateService = rateService;
rateService.whenAvailable(function() {
$scope.isRateAvailable = true;
$scope.$digest();

View file

@ -86,7 +86,9 @@ Identity.create = function(opts, cb) {
opts = _.extend({}, opts);
var iden = new Identity(opts);
iden.store(_.extend(opts, {failIfExists: true}), function(err) {
iden.store(_.extend(opts, {
failIfExists: true
}), function(err) {
if (err) return cb(err);
return cb(null, iden);
});

139
js/models/RateService.js Normal file
View file

@ -0,0 +1,139 @@
'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;
opts = opts || {};
self.request = opts.request || request;
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();
};
var _instance;
RateService.singleton = function(opts) {
if (!_instance) {
_instance = new RateService(opts);
}
return _instance;
};
RateService.prototype._fetchCurrencies = function() {
var self = this;
log.info('Fetching exchange rates');
var backoffSeconds = 5;
var updateFrequencySeconds = 3600;
var rateServiceUrl = 'https://bitpay.com/api/rates';
self.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;

View file

@ -2866,19 +2866,23 @@ Wallet.prototype.getTransactionHistory = function(cb) {
type: 'out'
});
var proposal = _.findWhere(proposals, {
sentTxid: tx.txid
});
tx.comment = proposal ? proposal.comment : undefined;
tx.labelTo = firstOut ? firstOut.label : undefined;
tx.addressTo = firstOut ? firstOut.address : undefined;
tx.amountSat = Math.abs(amount);
tx.amount = tx.amountSat * satToUnit;
tx.sentTs = proposal ? proposal.sentTs : undefined;
tx.minedTs = !_.isNaN(tx.time) ? tx.time * 1000 : undefined;
tx.merchant = proposal ? proposal.merchant : undefined;
tx.peerActions = proposal ? proposal.peerActions : undefined;
tx.finallyRejected = proposal ? proposal.finallyRejected : undefined;
var proposal = _.findWhere(proposals, {
sentTxid: tx.txid
});
if (proposal) {
tx.comment = proposal.comment;
tx.sentTs = proposal.sentTs;
tx.merchant = proposal.merchant;
tx.peerActions = proposal.peerActions;
tx.finallyRejected = proposal.finallyRejected;
}
};
if (addresses.length > 0) {

View file

@ -324,6 +324,17 @@ angular.module('copayApp.services')
};
root.updateTxs = function(opts) {
function computeAlternativeAmount(w, tx, cb) {
rateService.whenAvailable(function() {
_.each(tx.outs, function(out) {
var valueSat = out.value * w.settings.unitToSatoshi;
out.alternativeAmount = rateService.toFiat(valueSat, w.settings.alternativeIsoCode);
out.alternativeIsoCode = w.settings.alternativeIsoCode;
});
if (cb) return cb();
});
};
var w = opts.wallet || $rootScope.wallet;
if (!w) return;
opts = opts || $rootScope.txsOpts || {};
@ -364,6 +375,9 @@ angular.module('copayApp.services')
i.fee = i.builder.feeSat * satToUnit;
i.missingSignatures = tx.countInputMissingSignatures(0);
i.actionList = getActionList(i.peerActions);
if (i.isPending) {
computeAlternativeAmount(w, i);
}
txs.push(i);
}
});

View file

@ -1,85 +1,7 @@
'use strict';
var MINS_IN_HOUR = 60;
var MILLIS_IN_SECOND = 1000;
var RateService = function(request) {
this.isAvailable = false;
this.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable or use service.whenAvailable';
this.SAT_TO_BTC = 1 / 1e8;
this.BTC_TO_SAT = 1e8;
var rateServiceConfig = config.rate;
var updateFrequencySeconds = rateServiceConfig.updateFrequencySeconds || 60 * MINS_IN_HOUR;
var rateServiceUrl = rateServiceConfig.url || 'https://bitpay.com/api/rates';
this.queued = [];
this.alternatives = [];
var self = this;
var backoffSeconds = 5;
var retrieve = function() {
request.get({
url: rateServiceUrl,
json: true
}, function(err, response, listOfCurrencies) {
if (err) {
backoffSeconds *= 1.5;
setTimeout(retrieve, backoffSeconds * MILLIS_IN_SECOND);
return;
}
var rates = {};
listOfCurrencies.forEach(function(element) {
rates[element.code] = element.rate;
self.alternatives.push({
name: element.name,
isoCode: element.code,
rate: element.rate
});
});
self.isAvailable = true;
self.rates = rates;
self.queued.forEach(function(callback) {
setTimeout(callback, 1);
});
setTimeout(retrieve, updateFrequencySeconds * MILLIS_IN_SECOND);
});
};
retrieve();
};
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.rates[code];
};
RateService.prototype.fromFiat = function(amount, code) {
if (!this.isAvailable) {
throw new Error(this.UNAVAILABLE_ERROR);
}
return amount / this.rates[code] * this.BTC_TO_SAT;
};
RateService.prototype.listAlternatives = function() {
if (!this.isAvailable) {
throw new Error(this.UNAVAILABLE_ERROR);
}
var alts = [];
this.alternatives.forEach(function(element) {
alts.push({
name: element.name,
isoCode: element.isoCode
});
angular.module('copayApp.services').factory('rateService', function(request) {
return copay.RateService.singleton({
request: request
});
return alts;
};
angular.module('copayApp.services').service('rateService', RateService);
});

View file

@ -83,7 +83,6 @@ describe('PayPro (in Wallet) model', function() {
c.network.getHexNonces = sinon.stub();
c.network.send = sinon.stub();
return new Wallet(c);
}

179
test/RateService.js Normal file
View file

@ -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);
});
});
});

View file

@ -56,8 +56,8 @@ var addCopayers = function(w) {
}
};
describe('Wallet model', function() {
describe('Wallet model', function() {
it('should fail to create an instance', function() {
(function() {
new Wallet(walletConfig)
@ -102,8 +102,6 @@ describe('Wallet model', function() {
c.network.peerFromCopayer = sinon.stub().returns('xxxx');
c.network.send = sinon.stub();
c.addressBook = {
'2NFR2kzH9NUdp8vsXTB4wWQtTtzhpKxsyoJ': {
label: 'John',

View file

@ -26,11 +26,11 @@ describe("Unit: Controllers", function() {
var server;
beforeEach(module('copayApp'));
beforeEach(module('copayApp.controllers'));
beforeEach(module(function($provide) {
$provide.value('request', {
'get': function(_, cb) {
cb(null, null, [{
beforeEach(module('copayApp.controllers'));
beforeEach(module(function($provide) {
$provide.value('request', {
'get': function(_, cb) {
cb(null, null, [{
name: 'USD Dollars',
code: 'USD',
rate: 2
@ -71,7 +71,7 @@ describe("Unit: Controllers", function() {
w.createTx = sinon.stub().yields(null);
w.sendTx = sinon.stub().yields(null);
w.requiresMultipleSignatures = sinon.stub().returns(true);
w.getTxProposals = sinon.stub().returns([1,2,3]);
w.getTxProposals = sinon.stub().returns([1, 2, 3]);
$rootScope.wallet = w;
@ -172,17 +172,6 @@ describe("Unit: Controllers", function() {
scope.getTransactions();
expect(scope.blockchain_txs).to.be.empty;
});
it('should call amountAlternative and return a value', function() {
var cb = sinon.spy();
var s1 = sinon.stub(scope, 'amountAlternative');
s1.onCall(0).returns(1000);
s1.onCall(1).returns(2000);
expect(s1(100, 0, cb)).equal(1000);
expect(s1(200, 1, cb)).equal(2000);
sinon.assert.callCount(scope.amountAlternative, 2);
s1.restore();
});
});
describe('Send Controller', function() {

View file

@ -10,6 +10,7 @@
<div class="large-3 medium-3 small-4 columns">
<div class="size-12">
<span>{{out.value |noFractionNumber}} {{$root.wallet.settings.unitName}}</span>
<span>{{out.alternativeAmount|noFractionNumber}} {{out.alternativeIsoCode}}</span>
</div>
</div>
<div class="large-1 medium-1 small-1 columns fi-arrow-right"></div>