import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import { normalize as normalizeAddress } from 'eth-sig-util' import ethUtil from 'ethereumjs-util' // 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 */ export default class TokenRatesController { /** * Creates a TokenRatesController * * @param {Object} [config] - Options to configure controller */ constructor({ currency, preferences } = {}) { this.store = new ObservableStore() this.currency = currency this.preferences = preferences } /** * Updates exchange rates for all tokens */ async updateExchangeRates() { const contractExchangeRates = {} const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth' const pairs = this._tokens.map((token) => token.address).join(',') const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}` if (this._tokens.length > 0) { try { const response = await window.fetch( `https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`, ) const prices = await response.json() this._tokens.forEach((token) => { const price = prices[token.address.toLowerCase()] || prices[ethUtil.toChecksumAddress(token.address)] contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0 }) } catch (error) { log.warn( `MetaMask - TokenRatesController exchange rate fetch failed.`, error, ) } } this.store.putState({ contractExchangeRates }) } /* eslint-disable accessor-pairs */ /** * @type {Object} */ 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} */ set tokens(tokens) { this._tokens = tokens this.updateExchangeRates() } /* eslint-enable accessor-pairs */ start(interval = DEFAULT_INTERVAL) { this._handle && clearInterval(this._handle) if (!interval) { return } this._handle = setInterval(() => { this.updateExchangeRates() }, interval) this.updateExchangeRates() } stop() { this._handle && clearInterval(this._handle) } }