diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js new file mode 100644 index 000000000..85a8ca24e --- /dev/null +++ b/app/scripts/controllers/token-rates.js @@ -0,0 +1,76 @@ +const ObservableStore = require('obs-store') + +// By default, poll every 3 minutes +const DEFAULT_INTERVAL = 180 * 1000 + +/** + * A controller that polls for token exchange + * rates based on a user's current token list + */ +class TokenRatesController { + /** + * Creates a TokenRatesController + * + * @param {Object} [config] - Options to configure controller + */ + constructor ({ interval = DEFAULT_INTERVAL, preferences } = {}) { + this.store = new ObservableStore() + this.preferences = preferences + this.interval = interval + } + + /** + * Updates exchange rates for all tokens + */ + async updateExchangeRates () { + const contractExchangeRates = {} + for (const i in this._tokens) { + const address = this._tokens[i].address + contractExchangeRates[address] = await this.fetchExchangeRate(address) + } + this.store.putState({ contractExchangeRates }) + } + + /** + * Fetches a token exchange rate by address + * + * @param {String} address - Token contract address + */ + async fetchExchangeRate (address) { + try { + const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`) + const json = await response.json() + return json && json.length ? json[0].averagePrice : 0 + } catch (error) { } + } + + /** + * @type {Number} - Interval used to poll for exchange rates + */ + set interval (interval) { + this._handle && clearInterval(this._handle) + if (!interval) { return } + this._handle = setInterval(() => { this.updateExchangeRates() }, interval) + } + + /** + * @type {Object} - Preferences controller instance + */ + set preferences (preferences) { + this._preferences && this._preferences.unsubscribe() + if (!preferences) { return } + this._preferences = preferences + this.tokens = preferences.getState().tokens + preferences.subscribe(({ tokens = [] }) => { this.tokens = tokens }) + } + + /** + * @type {Array} - Array of token objects with contract addresses + */ + set tokens (tokens) { + this._tokens = tokens + this.updateExchangeRates() + } +} + +module.exports = TokenRatesController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fa7890c50..750a97b86 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -34,6 +34,7 @@ const PersonalMessageManager = require('./lib/personal-message-manager') const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') +const TokenRatesController = require('./controllers/token-rates') const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') @@ -104,6 +105,11 @@ module.exports = class MetamaskController extends EventEmitter { this.provider = this.initializeProvider() this.blockTracker = this.provider._blockTracker + // token exchange rate tracker + this.tokenRatesController = new TokenRatesController({ + preferences: this.preferencesController.store, + }) + this.recentBlocksController = new RecentBlocksController({ blockTracker: this.blockTracker, provider: this.provider, @@ -201,6 +207,7 @@ module.exports = class MetamaskController extends EventEmitter { AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, BalancesController: this.balancesController.store, + TokenRatesController: this.tokenRatesController.store, MessageManager: this.messageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore, TypesMessageManager: this.typedMessageManager.memStore, diff --git a/test/unit/token-rates-controller.js b/test/unit/token-rates-controller.js new file mode 100644 index 000000000..55bfe7823 --- /dev/null +++ b/test/unit/token-rates-controller.js @@ -0,0 +1,28 @@ +const assert = require('assert') +const sinon = require('sinon') +const TokenRatesController = require('../../app/scripts/controllers/token-rates') +const ObservableStore = require('obs-store') + +describe('TokenRatesController', () => { + it('should listen for preferences store updates', () => { + const preferences = new ObservableStore({ tokens: [] }) + const controller = new TokenRatesController({ preferences }) + preferences.putState({ tokens: ['foo'] }) + assert.deepEqual(controller._tokens, ['foo']) + }) + + it('should poll on correct interval', async () => { + const stub = sinon.stub(global, 'setInterval') + new TokenRatesController({ interval: 1337 }) // eslint-disable-line no-new + assert.strictEqual(stub.getCall(0).args[1], 1337) + stub.restore() + }) + + it('should fetch each token rate based on address', async () => { + const controller = new TokenRatesController() + controller.fetchExchangeRate = address => address + controller.tokens = [{ address: 'foo' }, { address: 'bar' }] + await controller.updateExchangeRates() + assert.deepEqual(controller.store.getState().contractExchangeRates, { foo: 'foo', bar: 'bar' }) + }) +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 6453a2bc2..46f34e149 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -220,10 +220,6 @@ var actions = { coinBaseSubview: coinBaseSubview, SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', shapeShiftSubview: shapeShiftSubview, - UPDATE_CONTRACT_EXCHANGE_RATES: 'UPDATE_CONTRACT_EXCHANGE_RATES', - UPDATE_CONTRACT_EXCHANGE_RATE: 'UPDATE_CONTRACT_EXCHANGE_RATE', - updateContractExchangeRates, - updateContractExchangeRate, PAIR_UPDATE: 'PAIR_UPDATE', pairUpdate: pairUpdate, coinShiftRquest: coinShiftRquest, @@ -1082,12 +1078,9 @@ function unlockMetamask (account) { } function updateMetamaskState (newState) { - return async dispatch => { - await dispatch({ - type: actions.UPDATE_METAMASK_STATE, - value: newState, - }) - dispatch(updateContractExchangeRates()) + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, } } @@ -1300,12 +1293,9 @@ function addTokens (tokens) { } function updateTokens (newTokens) { - return async dispatch => { - await dispatch({ - type: actions.UPDATE_TOKENS, - newTokens, - }) - dispatch(updateContractExchangeRates()) + return { + type: actions.UPDATE_TOKENS, + newTokens, } } @@ -1759,42 +1749,6 @@ function shapeShiftRequest (query, options, cb) { } } -async function fetchContractRate (address) { - try { - const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`) - const json = await response.json() - const rate = json && json.length ? json[0].averagePrice : 0 - return { address, rate } - } catch (error) { } -} - -function updateContractExchangeRates () { - return async (dispatch, getState) => { - const { metamask: { tokens = [] } } = getState() - const newExchangeRates = {} - - for (const i in tokens) { - const address = tokens[i].address - newExchangeRates[address] = (await fetchContractRate(address)).rate - } - - dispatch({ - type: actions.UPDATE_CONTRACT_EXCHANGE_RATES, - payload: { newExchangeRates }, - }) - } -} - -function updateContractExchangeRate (address) { - return async dispatch => { - const { address, rate } = await fetchContractRate(address) - dispatch({ - type: actions.UPDATE_CONTRACT_EXCHANGE_RATE, - payload: { address, rate }, - }) - } -} - function setFeatureFlag (feature, activated, notificationType) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 75f16b382..adfc91240 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -66,7 +66,6 @@ function mapDispatchToProps (dispatch) { showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })), estimateGas: params => dispatch(actions.estimateGas(params)), getGasPrice: () => dispatch(actions.getGasPrice()), - updateContractExchangeRate: address => dispatch(actions.updateContractExchangeRate(address)), signTokenTx: (tokenAddress, toAddress, amount, txData) => ( dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index d6b6de3b0..1ed049be2 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -177,23 +177,6 @@ function reduceMetamask (state, action) { conversionDate: action.value.conversionDate, }) - case actions.UPDATE_CONTRACT_EXCHANGE_RATES: - const { payload: { newExchangeRates } } = action - return { - ...metamaskState, - contractExchangeRates: newExchangeRates, - } - - case actions.UPDATE_CONTRACT_EXCHANGE_RATE: - const { payload: { address, rate } } = action - return { - ...metamaskState, - contractExchangeRates: { - ...metamaskState.contractExchangeRates, - [address]: rate, - }, - } - case actions.UPDATE_TOKENS: return extend(metamaskState, { tokens: action.newTokens, diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 12cf55f62..30d3d3152 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -88,17 +88,6 @@ SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) { } SendTransactionScreen.prototype.componentWillMount = function () { - const { - updateContractExchangeRate, - selectedToken = {}, - } = this.props - - const { address } = selectedToken || {} - - if (address) { - updateContractExchangeRate(address) - } - this.updateGas() }