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 );