/ common / components / BalanceSidebar / EquivalentValues.tsx
EquivalentValues.tsx
  1  import React from 'react';
  2  import { connect } from 'react-redux';
  3  import Select from 'react-select';
  4  import BN from 'bn.js';
  5  import { chain, flatMap } from 'lodash';
  6  
  7  import translate from 'translations';
  8  import { rateSymbols } from 'api/rates';
  9  import { NetworkConfig } from 'types/network';
 10  import { Balance } from 'libs/wallet';
 11  import { AppState } from 'features/reducers';
 12  import * as selectors from 'features/selectors';
 13  import { getOffline, getNetworkConfig } from 'features/config';
 14  import { ratesActions } from 'features/rates';
 15  import { walletTypes } from 'features/wallet';
 16  import { UnitDisplay, Spinner } from 'components/ui';
 17  import btcIco from 'assets/images/bitcoin.png';
 18  import ethIco from 'assets/images/ether.png';
 19  import repIco from 'assets/images/augur.png';
 20  import './EquivalentValues.scss';
 21  
 22  interface AllValue {
 23    symbol: string;
 24    balance: Balance['wei'];
 25  }
 26  
 27  interface DefaultOption {
 28    label: string;
 29    value: AllValue[];
 30  }
 31  
 32  interface Option {
 33    label: string;
 34    value: Balance['wei'] | AllValue[];
 35  }
 36  
 37  interface State {
 38    equivalentValues: Option;
 39    options: Option[];
 40  }
 41  
 42  interface StateProps {
 43    balance: Balance;
 44    network: NetworkConfig;
 45  
 46    tokenBalances: walletTypes.TokenBalance[];
 47    rates: AppState['rates']['rates'];
 48    ratesError: AppState['rates']['ratesError'];
 49    isOffline: AppState['config']['meta']['offline'];
 50  }
 51  
 52  interface DispatchProps {
 53    fetchCCRates: ratesActions.TFetchCCRatesRequested;
 54  }
 55  
 56  interface FiatSymbols {
 57    [key: string]: string;
 58  }
 59  
 60  interface Rates {
 61    [rate: string]: number;
 62  }
 63  
 64  type Props = StateProps & DispatchProps;
 65  
 66  class EquivalentValues extends React.Component<Props, State> {
 67    private requestedCurrencies: string[] | null = null;
 68  
 69    public constructor(props: Props) {
 70      super(props);
 71      const { balance, tokenBalances, network } = this.props;
 72      this.state = {
 73        equivalentValues: this.defaultOption(balance, tokenBalances, network),
 74        options: []
 75      };
 76  
 77      if (props.balance && props.tokenBalances) {
 78        this.fetchRates(props);
 79      }
 80    }
 81  
 82    public defaultOption(
 83      balance: Balance,
 84      tokenBalances: walletTypes.TokenBalance[],
 85      network: StateProps['network']
 86    ): DefaultOption {
 87      return {
 88        label: 'All',
 89        value: [{ symbol: network.unit, balance: balance.wei }, ...tokenBalances]
 90      };
 91    }
 92  
 93    public UNSAFE_componentWillReceiveProps(nextProps: Props) {
 94      const { balance, tokenBalances, isOffline, network } = this.props;
 95      if (
 96        nextProps.balance !== balance ||
 97        nextProps.tokenBalances !== tokenBalances ||
 98        nextProps.isOffline !== isOffline ||
 99        nextProps.network.unit !== network.unit
100      ) {
101        const defaultOption = this.defaultOption(
102          nextProps.balance,
103          nextProps.tokenBalances,
104          nextProps.network
105        );
106        const options: Option[] = [
107          defaultOption,
108          { label: nextProps.network.unit, value: nextProps.balance.wei },
109          ...Object.values(nextProps.tokenBalances).map(token => {
110            return { label: token.symbol, value: token.balance };
111          })
112        ];
113        const equivalentValues =
114          options.find(opt => opt.label === this.state.equivalentValues.label) || defaultOption;
115        this.setState({
116          equivalentValues,
117          options
118        });
119        this.fetchRates(nextProps);
120      }
121    }
122  
123    public selectOption = (equivalentValues: Option) => {
124      this.setState({ equivalentValues });
125    };
126  
127    public render(): JSX.Element {
128      const { balance, isOffline, tokenBalances, rates, network, ratesError } = this.props;
129      const { equivalentValues, options } = this.state;
130      const isFetching =
131        !balance || balance.isPending || !tokenBalances || Object.keys(rates).length === 0;
132      const pairRates = this.generateValues(equivalentValues.label, equivalentValues.value);
133      const fiatSymbols: FiatSymbols = {
134        USD: '$',
135        EUR: '€',
136        GBP: '£',
137        CHF: ' '
138      };
139      const coinAndTokenSymbols: any = {
140        BTC: btcIco,
141        ETH: ethIco,
142        REP: repIco
143      };
144      interface ValueProps {
145        className: string;
146        rate: string;
147        value: BN | null;
148        symbol?: string;
149        icon?: string;
150        key?: number | string;
151      }
152  
153      const Value = (props: ValueProps) => (
154        <div className={`EquivalentValues-values-currency ${props.className}`}>
155          <img src={props.icon} />
156          {!!props.symbol && (
157            <span className="EquivalentValues-values-currency-fiat-symbol">{props.symbol}</span>
158          )}
159          <span className="EquivalentValues-values-currency-label">{props.rate}</span>{' '}
160          <span className="EquivalentValues-values-currency-value">
161            <UnitDisplay
162              unit={'ether'}
163              value={props.value}
164              displayShortBalance={rateSymbols.isFiat(props.rate) ? 2 : 3}
165              checkOffline={true}
166            />
167          </span>
168        </div>
169      );
170  
171      return (
172        <div className="EquivalentValues">
173          <div className="EquivalentValues-header">
174            <h5 className="EquivalentValues-title">{translate('SIDEBAR_EQUIV')}</h5>
175            <Select
176              name="equivalentValues"
177              // TODO: Update type
178              value={equivalentValues as any}
179              options={options as any}
180              onChange={this.selectOption as any}
181              clearable={false}
182              searchable={false}
183            />
184          </div>
185  
186          {isOffline ? (
187            <div className="EquivalentValues-offline well well-sm">
188              {translate('EQUIV_VALS_OFFLINE')}
189            </div>
190          ) : network.isTestnet ? (
191            <div className="text-center">
192              <h5 style={{ color: 'red' }}>{translate('EQUIV_VALS_TESTNET')}</h5>
193            </div>
194          ) : ratesError ? (
195            <h5>{ratesError}</h5>
196          ) : isFetching ? (
197            <div className="EquivalentValues-spinner">
198              <Spinner size="x3" />
199            </div>
200          ) : (
201            <div className="EquivalentValues-values">
202              {pairRates.length ? (
203                <React.Fragment>
204                  {pairRates.map(
205                    (equiv, i) =>
206                      (rateSymbols.symbols.fiat as string[]).includes(equiv.rate) && (
207                        <Value
208                          className="EquivalentValues-values-currency-fiat"
209                          rate={equiv.rate}
210                          value={equiv.value}
211                          symbol={fiatSymbols[equiv.rate]}
212                          key={i}
213                        />
214                      )
215                  )}
216                  <div className="EquivalentValues-values-spacer" />
217                  {pairRates.map(
218                    (equiv, i) =>
219                      (rateSymbols.symbols.coinAndToken as string[]).includes(equiv.rate) && (
220                        <Value
221                          className="EquivalentValues-values-currency-coin-and-token"
222                          rate={equiv.rate}
223                          value={equiv.value}
224                          icon={coinAndTokenSymbols[equiv.rate]}
225                          key={i}
226                        />
227                      )
228                  )}
229                </React.Fragment>
230              ) : (
231                <p>{translate('EQUIV_VALS_UNSUPPORTED_UNIT')}</p>
232              )}
233            </div>
234          )}
235        </div>
236      );
237    }
238  
239    // return the sum of all equivalent values (unit * rate * balance) grouped by rate (USD, EUR, ETH, etc...)
240    private handleAllValues = (balance: AllValue[]) => {
241      const { rates } = this.props;
242      const allRates = Object.values(balance).map(
243        value => !!rates[value.symbol] && rates[value.symbol]
244      );
245      const allEquivalentValues = allRates.map((rateType: any, i) => {
246        return {
247          symbol: Object.keys(rates)[i],
248          equivalentValues: [
249            ...Object.keys(rateType).map(rate => {
250              const balanceIndex: AllValue = balance[i];
251              const value =
252                balanceIndex && !!balanceIndex.balance
253                  ? balanceIndex.balance.muln(rateType[rate])
254                  : null;
255              return { rate, value };
256            })
257          ]
258        };
259      });
260      // flatten all equivalent values for each unit (ETH, ETC, OMG, etc...) into an array
261      const collection = flatMap([
262        ...Object.values(allEquivalentValues).map(v => v.equivalentValues)
263      ]);
264      // group equivalent values by rate (USD, EUR, etc...)
265      const groupedCollection = chain(collection)
266        .groupBy('rate')
267        .mapValues(v => Object.values(v).map(s => s.value))
268        .value();
269      // finally, add all the equivalent values together and return an array of objects with the sum of equivalent values for each rate
270      return Object.values(groupedCollection).map((v, i) => {
271        return {
272          rate: Object.keys(groupedCollection)[i],
273          value: v.reduce((acc, curr) => acc && curr && acc.add(curr))
274        };
275      });
276    };
277  
278    // return equivalent value (unit * rate * balance)
279    private handleValues(unit: string, balance: Balance['wei']) {
280      const { rates } = this.props;
281      const ratesObj: Rates = { ...rates[unit] };
282      return Object.keys(ratesObj).map(key => {
283        const value = balance!.muln(ratesObj[key]);
284        return { rate: key, value };
285      });
286    }
287  
288    private generateValues = (
289      unit: string,
290      balance: Balance['wei'] | AllValue[]
291    ): { rate: string; value: Balance['wei'] }[] => {
292      if (unit === 'All') {
293        return this.handleAllValues(balance as AllValue[]);
294      } else {
295        return this.handleValues(unit, balance as Balance['wei']);
296      }
297    };
298  
299    private fetchRates(props: Props) {
300      const { balance, tokenBalances, isOffline } = props;
301      // Duck out if we haven't gotten balances yet, or we're not going to
302      if (!balance || !tokenBalances || isOffline) {
303        return;
304      }
305  
306      // First determine which currencies we're asking for
307      const currencies = tokenBalances
308        .filter(tk => !tk.balance.isZero())
309        .map(tk => tk.symbol)
310        .sort()
311        .concat([props.network.unit]);
312  
313      // If it's the same currencies as we have, skip it
314      if (this.requestedCurrencies && currencies.join() === this.requestedCurrencies.join()) {
315        return;
316      }
317  
318      // Fire off the request and save the currencies requested
319      props.fetchCCRates(currencies);
320      this.requestedCurrencies = currencies;
321    }
322  }
323  function mapStateToProps(state: AppState): StateProps {
324    return {
325      balance: state.wallet.balance,
326      tokenBalances: selectors.getShownTokenBalances(state, true),
327      network: getNetworkConfig(state),
328      rates: state.rates.rates,
329      ratesError: state.rates.ratesError,
330      isOffline: getOffline(state)
331    };
332  }
333  
334  export default connect(mapStateToProps, { fetchCCRates: ratesActions.fetchCCRatesRequested })(
335    EquivalentValues
336  );