diff options
-rw-r--r-- | packages/website/ts/components/wallet/wallet.tsx | 73 | ||||
-rw-r--r-- | packages/website/ts/utils/backend_client.ts | 46 | ||||
-rw-r--r-- | packages/website/ts/utils/configs.ts | 1 | ||||
-rw-r--r-- | packages/website/ts/utils/fetch_utils.ts | 33 |
4 files changed, 105 insertions, 48 deletions
diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index d1ae38550..2097c0eab 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -37,7 +37,9 @@ import { TokenStateByAddress, } from 'ts/types'; import { backendClient } from 'ts/utils/backend_client'; +import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; +import { fetchUtils } from 'ts/utils/fetch_utils'; import { utils } from 'ts/utils/utils'; import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; @@ -128,9 +130,13 @@ const FOOTER_ITEM_KEY = 'FOOTER'; const DISCONNECTED_ITEM_KEY = 'DISCONNECTED'; const ETHER_ITEM_KEY = 'ETHER'; const USD_DECIMAL_PLACES = 2; +const CRYPTO_COMPARE_MULTI_ENDPOINT = '/pricemulti'; +// Crypto compare recommends requesting no more than once every 10s: https://www.cryptocompare.com/api/?javascript#requests +const CRYPTO_COMPARE_UPDATE_INTERVAL_MS = 10 * 1000; export class Wallet extends React.Component<WalletProps, WalletState> { private _isUnmounted: boolean; + private _cryptoCompareLastFetchTimestampMs?: number; constructor(props: WalletProps) { super(props); this._isUnmounted = false; @@ -461,16 +467,27 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple; } - const pricesByAddress = await this._getPricesByAddressAsync(tokenAddresses); + // if we are allowed to fetch prices do so, if not, keep the old price state + const canFetchPrices = this._canGetPrice(); + let priceByAddress: ItemByAddress<BigNumber> = {}; + if (canFetchPrices) { + priceByAddress = await this._getPricesByAddressAsync(tokenAddresses); + } else { + const cachedPricesByAddress = _.mapValues( + this.state.trackedTokenStateByAddress, + tokenState => tokenState.price, + ); + priceByAddress = cachedPricesByAddress; + } const trackedTokenStateByAddress = _.reduce( tokenAddresses, (acc, address) => { const [balance, allowance] = balanceAndAllowanceTupleByAddress[address]; - const price = pricesByAddress[address]; + const priceIfExists = _.get(priceByAddress, address); acc[address] = { balance, allowance, - price, + price: priceIfExists, isLoaded: true, }; return acc; @@ -491,16 +508,56 @@ export class Wallet extends React.Component<WalletProps, WalletState> { if (_.isEmpty(tokenAddresses)) { return {}; } + // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists + // create a mapping from existing symbols -> address + const tokenAddressBySymbol = _.fromPairs( + _.compact( + _.map(tokenAddresses, address => { + const tokenIfExists = _.get(this.props.tokenByAddress, address); + if (!_.isUndefined(tokenIfExists)) { + const symbol = tokenIfExists.symbol; + // the crypto compare api doesn't understand 'WETH' so we need to replace it with 'ETH' + const key = symbol === ETHER_TOKEN_SYMBOL ? ETHER_SYMBOL : symbol; + return [key, address]; + } else { + return undefined; + } + }), + ), + ); + const joinedTokenSymbols = _.keys(tokenAddressBySymbol).join(','); + const quoteCurrency = 'USD'; + const queryParams = { + fsyms: joinedTokenSymbols, + tsyms: quoteCurrency, + }; try { - const websiteBackendPriceInfos = await backendClient.getPriceInfosAsync(tokenAddresses); - const addresses = _.map(websiteBackendPriceInfos, info => info.address); - const prices = _.map(websiteBackendPriceInfos, info => new BigNumber(info.price)); - const pricesByAddress = _.zipObject(addresses, prices); - return pricesByAddress; + this._cryptoCompareLastFetchTimestampMs = Date.now(); + const priceInfoBySymbol = await fetchUtils.requestAsync( + configs.CRYPTO_COMPARE_BASE_URL, + CRYPTO_COMPARE_MULTI_ENDPOINT, + queryParams, + ); + const priceInfoByAddress = _.mapKeys(priceInfoBySymbol, (value, symbol) => + _.get(tokenAddressBySymbol, symbol), + ); + const result = _.mapValues(priceInfoByAddress, priceInfo => { + const price = _.get(priceInfo, quoteCurrency); + const priceBigNumber = new BigNumber(price); + return priceBigNumber; + }); + return result; } catch (err) { return {}; } } + private _canGetPrice() { + const currentTimeStamp = Date.now(); + const result = + _.isUndefined(this._cryptoCompareLastFetchTimestampMs) || + this._cryptoCompareLastFetchTimestampMs + CRYPTO_COMPARE_UPDATE_INTERVAL_MS < currentTimeStamp; + return result; + } private _openWrappedEtherActionRow(wrappedEtherDirection: Side) { this.setState({ wrappedEtherDirection, diff --git a/packages/website/ts/utils/backend_client.ts b/packages/website/ts/utils/backend_client.ts index fdbb3e03a..ab0fb4f32 100644 --- a/packages/website/ts/utils/backend_client.ts +++ b/packages/website/ts/utils/backend_client.ts @@ -1,16 +1,8 @@ -import { BigNumber, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; -import * as queryString from 'query-string'; -import { - ArticlesBySection, - ItemByAddress, - WebsiteBackendGasInfo, - WebsiteBackendPriceInfo, - WebsiteBackendRelayerInfo, -} from 'ts/types'; +import { ArticlesBySection, WebsiteBackendGasInfo, WebsiteBackendPriceInfo, WebsiteBackendRelayerInfo } from 'ts/types'; import { configs } from 'ts/utils/configs'; -import { errorReporter } from 'ts/utils/error_reporter'; +import { fetchUtils } from 'ts/utils/fetch_utils'; const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station'; const PRICES_ENDPOINT = '/prices'; @@ -19,7 +11,7 @@ const WIKI_ENDPOINT = '/wiki'; export const backendClient = { async getGasInfoAsync(): Promise<WebsiteBackendGasInfo> { - const result = await requestAsync(ETH_GAS_STATION_ENDPOINT); + const result = await fetchUtils.requestAsync(configs.BACKEND_BASE_URL, ETH_GAS_STATION_ENDPOINT); return result; }, async getPriceInfosAsync(tokenAddresses: string[]): Promise<WebsiteBackendPriceInfo[]> { @@ -30,41 +22,15 @@ export const backendClient = { const queryParams = { tokens: joinedTokenAddresses, }; - const result = await requestAsync(PRICES_ENDPOINT, queryParams); + const result = await fetchUtils.requestAsync(configs.BACKEND_BASE_URL, PRICES_ENDPOINT, queryParams); return result; }, async getRelayerInfosAsync(): Promise<WebsiteBackendRelayerInfo[]> { - const result = await requestAsync(RELAYERS_ENDPOINT); + const result = await fetchUtils.requestAsync(configs.BACKEND_BASE_URL, RELAYERS_ENDPOINT); return result; }, async getWikiArticlesBySectionAsync(): Promise<ArticlesBySection> { - const result = await requestAsync(WIKI_ENDPOINT); + const result = await fetchUtils.requestAsync(configs.BACKEND_BASE_URL, WIKI_ENDPOINT); return result; }, }; - -async function requestAsync(endpoint: string, queryParams?: object): Promise<any> { - const query = queryStringFromQueryParams(queryParams); - const url = `${configs.BACKEND_BASE_URL}${endpoint}${query}`; - const response = await fetch(url); - if (response.status !== 200) { - const errorText = `Error requesting url: ${url}, ${response.status}: ${response.statusText}`; - logUtils.log(errorText); - const error = Error(errorText); - // tslint:disable-next-line:no-floating-promises - errorReporter.reportAsync(error); - throw error; - } - const result = await response.json(); - return result; -} - -function queryStringFromQueryParams(queryParams?: object): string { - // if params are undefined or empty, return an empty string - if (_.isUndefined(queryParams) || _.isEmpty(queryParams)) { - return ''; - } - // stringify the formatted object - const stringifiedParams = queryString.stringify(queryParams); - return `?${stringifiedParams}`; -} diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index a54fc56a8..edfd04291 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -13,6 +13,7 @@ export const configs = { BACKEND_BASE_URL: 'https://website-api.0xproject.com', BASE_URL, BITLY_ACCESS_TOKEN: 'ffc4c1a31e5143848fb7c523b39f91b9b213d208', + CRYPTO_COMPARE_BASE_URL: 'https://min-api.cryptocompare.com/data', DEFAULT_DERIVATION_PATH: `44'/60'/0'`, // WARNING: ZRX & WETH MUST always be default trackedTokens DEFAULT_TRACKED_TOKEN_SYMBOLS: ['WETH', 'ZRX'], diff --git a/packages/website/ts/utils/fetch_utils.ts b/packages/website/ts/utils/fetch_utils.ts new file mode 100644 index 000000000..d2e902db5 --- /dev/null +++ b/packages/website/ts/utils/fetch_utils.ts @@ -0,0 +1,33 @@ +import { logUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as queryString from 'query-string'; + +import { errorReporter } from 'ts/utils/error_reporter'; + +export const fetchUtils = { + async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> { + const query = queryStringFromQueryParams(queryParams); + const url = `${baseUrl}${path}${query}`; + const response = await fetch(url); + if (response.status !== 200) { + const errorText = `Error requesting url: ${url}, ${response.status}: ${response.statusText}`; + logUtils.log(errorText); + const error = Error(errorText); + // tslint:disable-next-line:no-floating-promises + errorReporter.reportAsync(error); + throw error; + } + const result = await response.json(); + return result; + }, +}; + +function queryStringFromQueryParams(queryParams?: object): string { + // if params are undefined or empty, return an empty string + if (_.isUndefined(queryParams) || _.isEmpty(queryParams)) { + return ''; + } + // stringify the formatted object + const stringifiedParams = queryString.stringify(queryParams); + return `?${stringifiedParams}`; +} |