import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import { normalize as normalizeAddress } from 'eth-sig-util'; import ethUtil from 'ethereumjs-util'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; const fetchWithTimeout = getFetchWithTimeout(30000); // 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 fetchWithTimeout( `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); } }