Merge pull request #1730 from isocolsky/history
Refactor rate service as model
This commit is contained in:
commit
692852617f
13 changed files with 361 additions and 122 deletions
1
copay.js
1
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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
$scope.rateService = rateService;
|
||||
|
||||
|
||||
|
||||
rateService.whenAvailable(function() {
|
||||
$scope.isRateAvailable = true;
|
||||
$scope.$digest();
|
||||
|
|
|
|||
|
|
@ -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
139
js/models/RateService.js
Normal 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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
179
test/RateService.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue