diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 357e081b1..a9b86ca8c 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -250,53 +250,6 @@ ConfigManager.prototype.getTOSHash = function () { return data.TOSHash } -ConfigManager.prototype.setCurrentFiat = function (currency) { - var data = this.getData() - data.fiatCurrency = currency - this.setData(data) -} - -ConfigManager.prototype.getCurrentFiat = function () { - var data = this.getData() - return data.fiatCurrency || 'USD' -} - -ConfigManager.prototype.updateConversionRate = function () { - var data = this.getData() - return fetch(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`) - .then(response => response.json()) - .then((parsedResponse) => { - this.setConversionPrice(parsedResponse.ticker.price) - this.setConversionDate(parsedResponse.timestamp) - }).catch((err) => { - console.warn('MetaMask - Failed to query currency conversion.') - this.setConversionPrice(0) - this.setConversionDate('N/A') - }) -} - -ConfigManager.prototype.setConversionPrice = function (price) { - var data = this.getData() - data.conversionRate = Number(price) - this.setData(data) -} - -ConfigManager.prototype.setConversionDate = function (datestring) { - var data = this.getData() - data.conversionDate = datestring - this.setData(data) -} - -ConfigManager.prototype.getConversionRate = function () { - var data = this.getData() - return (data.conversionRate) || 0 -} - -ConfigManager.prototype.getConversionDate = function () { - var data = this.getData() - return (data.conversionDate) || 'N/A' -} - ConfigManager.prototype.getShapeShiftTxList = function () { var data = this.getData() var shapeShiftTxList = data.shapeShiftTxList ? data.shapeShiftTxList : [] diff --git a/app/scripts/lib/controllers/currency.js b/app/scripts/lib/controllers/currency.js new file mode 100644 index 000000000..c4904f8ac --- /dev/null +++ b/app/scripts/lib/controllers/currency.js @@ -0,0 +1,70 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +// every ten minutes +const POLLING_INTERVAL = 600000 + +class CurrencyController { + + constructor (opts = {}) { + const initState = extend({ + currentCurrency: 'USD', + conversionRate: 0, + conversionDate: 'N/A', + }, opts.initState) + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + getCurrentCurrency () { + return this.store.getState().currentCurrency + } + + setCurrentCurrency (currentCurrency) { + this.store.updateState({ currentCurrency }) + } + + getConversionRate () { + return this.store.getState().conversionRate + } + + setConversionRate (conversionRate) { + this.store.updateState({ conversionRate }) + } + + getConversionDate () { + return this.store.getState().conversionDate + } + + setConversionDate (conversionDate) { + this.store.updateState({ conversionDate }) + } + + updateConversionRate () { + const currentCurrency = this.getCurrentCurrency() + return fetch(`https://www.cryptonator.com/api/ticker/eth-${currentCurrency}`) + .then(response => response.json()) + .then((parsedResponse) => { + this.setConversionRate(Number(parsedResponse.ticker.price)) + this.setConversionDate(Number(parsedResponse.timestamp)) + }).catch((err) => { + console.warn('MetaMask - Failed to query currency conversion.') + this.setConversionRate(0) + this.setConversionDate('N/A') + }) + } + + scheduleConversionInterval () { + if (this.conversionInterval) { + clearInterval(this.conversionInterval) + } + this.conversionInterval = setInterval(() => { + this.updateConversionRate() + }, POLLING_INTERVAL) + } +} + +module.exports = CurrencyController diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index e4cbca456..ac395440d 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -97,9 +97,6 @@ IdentityStore.prototype.getState = function () { isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(), selectedAddress: configManager.getSelectedAccount(), shapeShiftTxList: configManager.getShapeShiftTxList(), - currentFiat: configManager.getCurrentFiat(), - conversionRate: configManager.getConversionRate(), - conversionDate: configManager.getConversionDate(), gasMultiplier: configManager.getGasMultiplier(), })) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 785bb7b33..2f1623c7b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -12,6 +12,7 @@ const MetaMaskProvider = require('web3-provider-engine/zero.js') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const KeyringController = require('./keyring-controller') const PreferencesController = require('./lib/controllers/preferences') +const CurrencyController = require('./lib/controllers/currency') const NoticeController = require('./notice-controller') const MessageManager = require('./lib/message-manager') const TxManager = require('./transaction-manager') @@ -41,13 +42,19 @@ module.exports = class MetamaskController extends EventEmitter { this.configManager = new ConfigManager({ store: this.store, }) - this.configManager.updateConversionRate() // preferences controller this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, }) + // currency controller + this.currencyController = new CurrencyController({ + initState: initState.CurrencyController, + }) + this.currencyController.updateConversionRate() + this.currencyController.scheduleConversionInterval() + // rpc provider this.provider = this.initializeProvider(opts) this.provider.on('block', this.logBlock.bind(this)) @@ -97,8 +104,6 @@ module.exports = class MetamaskController extends EventEmitter { this.checkTOSChange() - this.scheduleConversionInterval() - // TEMPORARY UNTIL FULL DEPRECATION: this.idStoreMigrator = new IdStoreMigrator({ configManager: this.configManager, @@ -114,11 +119,15 @@ module.exports = class MetamaskController extends EventEmitter { this.txManager.store.subscribe((state) => { this.store.updateState({ TransactionManager: state }) }) + this.currencyController.store.subscribe((state) => { + this.store.updateState({ CurrencyController: state }) + }) // manual mem state subscriptions this.ethStore.subscribe(this.sendUpdate.bind(this)) this.networkStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) + this.currencyController.store.subscribe(this.sendUpdate.bind(this)) this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -189,15 +198,13 @@ module.exports = class MetamaskController extends EventEmitter { this.messageManager.memStore.getState(), this.keyringController.memStore.getState(), this.preferencesController.store.getState(), + this.currencyController.store.getState(), this.noticeController.memStore.getState(), // config manager this.configManager.getConfig(), { shapeShiftTxList: this.configManager.getShapeShiftTxList(), lostAccounts: this.configManager.getLostAccounts(), - currentFiat: this.configManager.getCurrentFiat(), - conversionRate: this.configManager.getConversionRate(), - conversionDate: this.configManager.getConversionDate(), isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(), seedWords: this.configManager.getSeedWords(), } @@ -223,7 +230,7 @@ module.exports = class MetamaskController extends EventEmitter { useEtherscanProvider: this.useEtherscanProvider.bind(this), agreeToDisclaimer: this.agreeToDisclaimer.bind(this), resetDisclaimer: this.resetDisclaimer.bind(this), - setCurrentFiat: this.setCurrentFiat.bind(this), + setCurrentCurrency: this.setCurrentCurrency.bind(this), setTOSHash: this.setTOSHash.bind(this), checkTOSChange: this.checkTOSChange.bind(this), setGasMultiplier: this.setGasMultiplier.bind(this), @@ -550,31 +557,21 @@ module.exports = class MetamaskController extends EventEmitter { this.verifyNetwork() } - setCurrentFiat (fiat, cb) { + setCurrentCurrency (currencyCode, cb) { try { - this.configManager.setCurrentFiat(fiat) - this.configManager.updateConversionRate() - this.scheduleConversionInterval() + this.currencyController.setCurrentCurrency(currencyCode) + this.currencyController.updateConversionRate() const data = { - conversionRate: this.configManager.getConversionRate(), - currentFiat: this.configManager.getCurrentFiat(), - conversionDate: this.configManager.getConversionDate(), + conversionRate: this.currencyController.getConversionRate(), + currentFiat: this.currencyController.getCurrentCurrency(), + conversionDate: this.currencyController.getConversionDate(), } - cb(data) + cb(null, data) } catch (err) { - cb(null, err) + cb(err) } } - scheduleConversionInterval () { - if (this.conversionInterval) { - clearInterval(this.conversionInterval) - } - this.conversionInterval = setInterval(() => { - this.configManager.updateConversionRate() - }, 300000) - } - buyEth (address, amount) { if (!amount) amount = '5' diff --git a/app/scripts/migrations/009.js b/app/scripts/migrations/009.js new file mode 100644 index 000000000..61b8b7fa2 --- /dev/null +++ b/app/scripts/migrations/009.js @@ -0,0 +1,40 @@ +const version = 9 + +/* + +This migration breaks out the CurrencyController substate + +*/ + +const merge = require('deep-merge') + +module.exports = { + version, + + migrate: function (versionedData) { + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = merge(state, { + CurrencyController: { + currentCurrency: state.currentFiat || 'USD', + conversionRate: state.conversionRate, + conversionDate: state.conversionDate, + }, + }) + delete newState.currentFiat + delete newState.conversionRate + delete newState.conversionDate + + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index a25d3b044..22bf008ba 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -19,4 +19,5 @@ module.exports = [ require('./006'), require('./007'), require('./008'), + require('./009'), ] diff --git a/package.json b/package.json index 3285bab5f..ed0be3d4b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", + "deep-merge": "^1.0.0", "denodeify": "^1.2.1", "disc": "^1.3.2", "dnode": "^1.2.2", diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index acc73ebb4..c6f60192f 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -14,81 +14,6 @@ describe('config-manager', function() { configManager = configManagerGen() }) - describe('currency conversions', function() { - - describe('#setCurrentFiat', function() { - it('should return USD as default', function() { - assert.equal(configManager.getCurrentFiat(), 'USD') - }) - - it('should be able to set to other currency', function() { - assert.equal(configManager.getCurrentFiat(), 'USD') - configManager.setCurrentFiat('JPY') - var result = configManager.getCurrentFiat() - assert.equal(result, 'JPY') - }) - }) - - describe('#getConversionRate', function() { - it('should return undefined if non-existent', function() { - var result = configManager.getConversionRate() - assert.ok(!result) - }) - }) - - describe('#updateConversionRate', function() { - it('should retrieve an update for ETH to USD and set it in memory', function(done) { - this.timeout(15000) - var usdMock = nock('https://www.cryptonator.com') - .get('/api/ticker/eth-USD') - .reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') - - assert.equal(configManager.getConversionRate(), 0) - var promise = new Promise( - function (resolve, reject) { - configManager.setCurrentFiat('USD') - configManager.updateConversionRate().then(function() { - resolve() - }) - }) - - promise.then(function() { - var result = configManager.getConversionRate() - assert.equal(typeof result, 'number') - done() - }).catch(function(err) { - console.log(err) - }) - - }) - - it('should work for JPY as well.', function() { - this.timeout(15000) - assert.equal(configManager.getConversionRate(), 0) - - var jpyMock = nock('https://www.cryptonator.com') - .get('/api/ticker/eth-JPY') - .reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') - - - var promise = new Promise( - function (resolve, reject) { - configManager.setCurrentFiat('JPY') - configManager.updateConversionRate().then(function() { - resolve() - }) - }) - - promise.then(function() { - var result = configManager.getConversionRate() - assert.equal(typeof result, 'number') - }).catch(function(err) { - console.log(err) - }) - }) - }) - }) - describe('confirmation', function() { describe('#getConfirmedDisclaimer', function() { diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js new file mode 100644 index 000000000..c57b522c7 --- /dev/null +++ b/test/unit/currency-controller-test.js @@ -0,0 +1,87 @@ +// polyfill fetch +global.fetch = global.fetch || require('isomorphic-fetch') + +const assert = require('assert') +const extend = require('xtend') +const rp = require('request-promise') +const nock = require('nock') +const CurrencyController = require('../../app/scripts/lib/controllers/currency') + +describe('config-manager', function() { + var currencyController + + beforeEach(function() { + currencyController = new CurrencyController() + }) + + describe('currency conversions', function() { + + describe('#setCurrentCurrency', function() { + it('should return USD as default', function() { + assert.equal(currencyController.getCurrentCurrency(), 'USD') + }) + + it('should be able to set to other currency', function() { + assert.equal(currencyController.getCurrentCurrency(), 'USD') + currencyController.setCurrentCurrency('JPY') + var result = currencyController.getCurrentCurrency() + assert.equal(result, 'JPY') + }) + }) + + describe('#getConversionRate', function() { + it('should return undefined if non-existent', function() { + var result = currencyController.getConversionRate() + assert.ok(!result) + }) + }) + + describe('#updateConversionRate', function() { + it('should retrieve an update for ETH to USD and set it in memory', function(done) { + this.timeout(15000) + var usdMock = nock('https://www.cryptonator.com') + .get('/api/ticker/eth-USD') + .reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') + + assert.equal(currencyController.getConversionRate(), 0) + currencyController.setCurrentCurrency('USD') + currencyController.updateConversionRate() + .then(function() { + var result = currencyController.getConversionRate() + console.log('currencyController.getConversionRate:', result) + assert.equal(typeof result, 'number') + done() + }).catch(function(err) { + done(err) + }) + + }) + + it('should work for JPY as well.', function() { + this.timeout(15000) + assert.equal(currencyController.getConversionRate(), 0) + + var jpyMock = nock('https://www.cryptonator.com') + .get('/api/ticker/eth-JPY') + .reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') + + + var promise = new Promise( + function (resolve, reject) { + currencyController.setCurrentCurrency('JPY') + currencyController.updateConversionRate().then(function() { + resolve() + }) + }) + + promise.then(function() { + var result = currencyController.getConversionRate() + assert.equal(typeof result, 'number') + }).catch(function(err) { + done(err) + }) + }) + }) + }) + +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 228adcf18..c153a55a6 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -325,12 +325,16 @@ function showInfoPage () { } } -function setCurrentFiat (fiat) { +function setCurrentFiat (currencyCode) { return (dispatch) => { dispatch(this.showLoadingIndication()) if (global.METAMASK_DEBUG) console.log(`background.setCurrentFiat`) - background.setCurrentFiat(fiat, (data, err) => { + background.setCurrentCurrency(currencyCode, (err, data) => { dispatch(this.hideLoadingIndication()) + if (err) { + console.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } dispatch({ type: this.SET_CURRENT_FIAT, value: {