From 1510533ec7daaddf755fa64daa81ca10d38ccf3f Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Sat, 18 Nov 2017 13:15:02 -0700 Subject: [PATCH] Equivalent values for all tokens (ETH + ERC20s) (#420) * Fetch all token rates at once. Add option for displaying all token values. * Ensure spinner always shows before equivalent values are ready. * Fix up test. --- common/actions/rates/actionCreators.ts | 4 +- common/actions/rates/actionPayloads.ts | 45 +++++-- common/actions/rates/actionTypes.ts | 2 +- .../BalanceSidebar/EquivalentValues.tsx | 116 ++++++++++++++---- common/components/BalanceSidebar/index.tsx | 5 +- common/reducers/rates.ts | 5 +- spec/reducers/rates.spec.ts | 7 +- 7 files changed, 135 insertions(+), 49 deletions(-) diff --git a/common/actions/rates/actionCreators.ts b/common/actions/rates/actionCreators.ts index 2fc5255b..ef5705c1 100644 --- a/common/actions/rates/actionCreators.ts +++ b/common/actions/rates/actionCreators.ts @@ -3,10 +3,10 @@ import { TypeKeys } from './constants'; import { fetchRates, CCResponse } from './actionPayloads'; export type TFetchCCRates = typeof fetchCCRates; -export function fetchCCRates(symbol: string): interfaces.FetchCCRates { +export function fetchCCRates(symbols: string[] = []): interfaces.FetchCCRates { return { type: TypeKeys.RATES_FETCH_CC, - payload: fetchRates(symbol) + payload: fetchRates(symbols) }; } diff --git a/common/actions/rates/actionPayloads.ts b/common/actions/rates/actionPayloads.ts index aa4f9e50..9256049a 100644 --- a/common/actions/rates/actionPayloads.ts +++ b/common/actions/rates/actionPayloads.ts @@ -1,31 +1,52 @@ import { handleJSONResponse } from 'api/utils'; export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH']; -const rateSymbolsArg = rateSymbols.join(','); // TODO - internationalize const ERROR_MESSAGE = 'Could not fetch rate data.'; const CCApi = 'https://min-api.cryptocompare.com'; -const CCRates = (symbol: string) => { - return `${CCApi}/data/price?fsym=${symbol}&tsyms=${rateSymbolsArg}`; +const CCRates = (symbols: string[]) => { + const tsyms = rateSymbols.concat(symbols).join(','); + return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`; }; export interface CCResponse { - symbol: string; - rates: { - BTC: number; + [symbol: string]: { + USD: number; EUR: number; GBP: number; + BTC: number; CHF: number; REP: number; ETH: number; }; } -export const fetchRates = (symbol: string): Promise => - fetch(CCRates(symbol)) +export const fetchRates = (symbols: string[] = []): Promise => + fetch(CCRates(symbols)) .then(response => handleJSONResponse(response, ERROR_MESSAGE)) - .then(rates => ({ - symbol, - rates - })); + .then(rates => { + // All currencies are in ETH right now. We'll do token -> eth -> value to + // do it all in one request + // to their respective rates via ETH. + return symbols.reduce( + (eqRates, sym) => { + eqRates[sym] = rateSymbols.reduce((symRates, rateSym) => { + symRates[rateSym] = 1 / rates[sym] * rates[rateSym]; + return symRates; + }, {}); + return eqRates; + }, + { + ETH: { + USD: rates.USD, + EUR: rates.EUR, + GBP: rates.GBP, + BTC: rates.BTC, + CHF: rates.CHF, + REP: rates.REP, + ETH: 1 + } + } + ); + }); diff --git a/common/actions/rates/actionTypes.ts b/common/actions/rates/actionTypes.ts index 53810686..0609c318 100644 --- a/common/actions/rates/actionTypes.ts +++ b/common/actions/rates/actionTypes.ts @@ -18,6 +18,6 @@ export interface FetchCCRatesFailed { /*** Union Type ***/ export type RatesAction = - | FetchCCRatesSucceeded | FetchCCRates + | FetchCCRatesSucceeded | FetchCCRatesFailed; diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 8555141c..7fda3f81 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import BN from 'bn.js'; import translate from 'translations'; import { State } from 'reducers/rates'; import { rateSymbols, TFetchCCRates } from 'actions/rates'; @@ -8,6 +9,8 @@ import Spinner from 'components/ui/Spinner'; import UnitDisplay from 'components/ui/UnitDisplay'; import './EquivalentValues.scss'; +const ALL_OPTION = 'All'; + interface Props { balance?: Balance; tokenBalances?: TokenBalance[]; @@ -22,17 +25,18 @@ interface CmpState { export default class EquivalentValues extends React.Component { public state = { - currency: 'ETH' + currency: ALL_OPTION }; - private balanceLookup = {}; + private balanceLookup: { [key: string]: Balance['wei'] | undefined } = {}; + private requestedCurrencies: string[] = []; public constructor(props) { super(props); this.makeBalanceLookup(props); - } - public componentDidMount() { - this.props.fetchCCRates(this.state.currency); + if (props.balance && props.tokenBalances) { + this.fetchRates(props); + } } public componentWillReceiveProps(nextProps) { @@ -42,18 +46,27 @@ export default class EquivalentValues extends React.Component { nextProps.tokenBalances !== tokenBalances ) { this.makeBalanceLookup(nextProps); + this.fetchRates(nextProps); } } public render() { - const { tokenBalances, rates, ratesError } = this.props; + const { balance, tokenBalances, rates, ratesError } = this.props; const { currency } = this.state; - const balance = this.balanceLookup[currency]; - let values; - if (balance && rates && rates[currency]) { - values = rateSymbols.map(key => { - if (!rates[currency][key] || key === currency) { + // There are a bunch of reasons why the incorrect balances might be rendered + // while we have incomplete data that's being fetched. + const isFetching = + !balance || + balance.isPending || + !tokenBalances || + Object.keys(rates).length === 0; + + let valuesEl; + if (!isFetching && (rates[currency] || currency === ALL_OPTION)) { + const values = this.getEquivalentValues(currency); + valuesEl = rateSymbols.map(key => { + if (!values[key] || key === currency) { return null; } @@ -63,23 +76,19 @@ export default class EquivalentValues extends React.Component { {key}: {' '} - {balance.isPending ? ( - - ) : ( - - )} + ); }); } else if (ratesError) { - values =
{ratesError}
; + valuesEl =
{ratesError}
; } else { - values = ( + valuesEl = (
@@ -95,6 +104,7 @@ export default class EquivalentValues extends React.Component { onChange={this.changeCurrency} value={currency} > + {tokenBalances && tokenBalances.map(tk => { @@ -111,7 +121,7 @@ export default class EquivalentValues extends React.Component { -
    {values}
+
    {valuesEl}
); } @@ -119,9 +129,6 @@ export default class EquivalentValues extends React.Component { private changeCurrency = (ev: React.FormEvent) => { const currency = ev.currentTarget.value; this.setState({ currency }); - if (!this.props.rates || !this.props.rates[currency]) { - this.props.fetchCCRates(currency); - } }; private makeBalanceLookup(props: Props) { @@ -136,4 +143,61 @@ export default class EquivalentValues extends React.Component { { ETH: props.balance && props.balance.wei } ); } + + private fetchRates(props: Props) { + // Duck out if we haven't gotten balances yet + if (!props.balance || !props.tokenBalances) { + return; + } + + // First determine which currencies we're asking for + const currencies = props.tokenBalances + .filter(tk => !tk.balance.isZero()) + .map(tk => tk.symbol) + .sort(); + + // If it's the same currencies as we have, skip it + if (currencies.join() === this.requestedCurrencies.join()) { + return; + } + + // Fire off the request and save the currencies requested + this.props.fetchCCRates(currencies); + this.requestedCurrencies = currencies; + } + + private getEquivalentValues( + currency: string + ): { + [key: string]: BN | undefined; + } { + // Recursively call on all currencies + if (currency === ALL_OPTION) { + return ['ETH'].concat(this.requestedCurrencies).reduce( + (prev, curr) => { + const currValues = this.getEquivalentValues(curr); + rateSymbols.forEach( + sym => (prev[sym] = prev[sym].add(currValues[sym] || new BN(0))) + ); + return prev; + }, + rateSymbols.reduce((prev, sym) => { + prev[sym] = new BN(0); + return prev; + }, {}) + ); + } + + // Calculate rates for a single currency + const { rates } = this.props; + const balance = this.balanceLookup[currency]; + if (!balance || !rates[currency]) { + return {}; + } + + return rateSymbols.reduce((prev, sym) => { + prev[sym] = balance ? balance.muln(rates[currency][sym]) : null; + return prev; + }, {}); + } } diff --git a/common/components/BalanceSidebar/index.tsx b/common/components/BalanceSidebar/index.tsx index c11e5fc9..7fab773d 100644 --- a/common/components/BalanceSidebar/index.tsx +++ b/common/components/BalanceSidebar/index.tsx @@ -21,7 +21,6 @@ import AccountInfo from './AccountInfo'; import EquivalentValues from './EquivalentValues'; import Promos from './Promos'; import TokenBalances from './TokenBalances'; -import { State } from 'reducers/rates'; import OfflineToggle from './OfflineToggle'; interface Props { @@ -29,8 +28,8 @@ interface Props { balance: Balance; network: NetworkConfig; tokenBalances: TokenBalance[]; - rates: State['rates']; - ratesError: State['ratesError']; + rates: AppState['rates']['rates']; + ratesError: AppState['rates']['ratesError']; showNotification: TShowNotification; addCustomToken: TAddCustomToken; removeCustomToken: TRemoveCustomToken; diff --git a/common/reducers/rates.ts b/common/reducers/rates.ts index c45b26d8..8c4c3bbc 100644 --- a/common/reducers/rates.ts +++ b/common/reducers/rates.ts @@ -8,7 +8,8 @@ export interface State { } export const INITIAL_STATE: State = { - rates: {} + rates: {}, + ratesError: null }; function fetchCCRatesSucceeded( @@ -19,7 +20,7 @@ function fetchCCRatesSucceeded( ...state, rates: { ...state.rates, - [action.payload.symbol]: action.payload.rates + ...action.payload } }; } diff --git a/spec/reducers/rates.spec.ts b/spec/reducers/rates.spec.ts index 902891f7..54fab934 100644 --- a/spec/reducers/rates.spec.ts +++ b/spec/reducers/rates.spec.ts @@ -4,8 +4,8 @@ import * as ratesActions from 'actions/rates'; describe('rates reducer', () => { it('should handle RATES_FETCH_CC_SUCCEEDED', () => { const fakeCCResp: ratesActions.CCResponse = { - symbol: 'USD', - rates: { + ETH: { + USD: 0, BTC: 1, EUR: 2, GBP: 3, @@ -14,13 +14,14 @@ describe('rates reducer', () => { ETH: 6 } }; + expect( rates(undefined, ratesActions.fetchCCRatesSucceeded(fakeCCResp)) ).toEqual({ ...INITIAL_STATE, rates: { ...INITIAL_STATE.rates, - [fakeCCResp.symbol]: fakeCCResp.rates + ...fakeCCResp } }); });