import { ZeroEx } from '0x.js'; import { colors, constants as sharedConstants, EtherscanLinkSuffixes, Styles, utils as sharedUtils, } from '@0xproject/react-shared'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import FlatButton from 'material-ui/FlatButton'; import { List, ListItem } from 'material-ui/List'; import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward'; import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward'; import * as React from 'react'; import ReactTooltip = require('react-tooltip'); import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle'; import { Identicon } from 'ts/components/ui/identicon'; import { TokenIcon } from 'ts/components/ui/token_icon'; import { Dispatcher } from 'ts/redux/dispatcher'; import { BalanceErrs, BlockchainErrs, Token, TokenByAddress, TokenState, TokenStateByAddress } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; export interface WalletProps { userAddress?: string; networkId?: number; blockchain?: Blockchain; blockchainIsLoaded: boolean; blockchainErr: BlockchainErrs; dispatcher: Dispatcher; tokenByAddress: TokenByAddress; trackedTokens: Token[]; userEtherBalanceInWei: BigNumber; lastForceTokenStateRefetch: number; } interface WalletState { trackedTokenStateByAddress: TokenStateByAddress; } enum WrappedEtherAction { Wrap, Unwrap, } interface AllowanceToggleConfig { token: Token; tokenState: TokenState; } interface AccessoryItemConfig { wrappedEtherAction?: WrappedEtherAction; allowanceToggleConfig?: AllowanceToggleConfig; } const styles: Styles = { wallet: { width: 346, backgroundColor: colors.white, borderBottomRightRadius: 10, borderBottomLeftRadius: 10, borderTopRightRadius: 10, borderTopLeftRadius: 10, boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`, overflow: 'hidden', }, list: { padding: 0, }, tokenItemInnerDiv: { paddingLeft: 60, }, headerItemInnerDiv: { paddingLeft: 65, }, footerItemInnerDiv: { paddingLeft: 24, }, borderedItem: { borderBottomColor: colors.walletBorder, borderBottomStyle: 'solid', borderWidth: 1, }, tokenItem: { backgroundColor: colors.walletDefaultItemBackground, paddingTop: 8, paddingBottom: 8, }, headerItem: { paddingTop: 8, paddingBottom: 8, }, wrappedEtherButtonLabel: { fontSize: 12, }, amountLabel: { fontWeight: 'bold', color: colors.black, }, }; const ETHER_ICON_PATH = '/images/ether.png'; const ETHER_TOKEN_SYMBOL = 'WETH'; const ZRX_TOKEN_SYMBOL = 'ZRX'; const ETHER_SYMBOL = 'ETH'; const ICON_DIMENSION = 24; const TOKEN_AMOUNT_DISPLAY_PRECISION = 3; export class Wallet extends React.Component { private _isUnmounted: boolean; constructor(props: WalletProps) { super(props); this._isUnmounted = false; const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens); this.state = { trackedTokenStateByAddress: initialTrackedTokenStateByAddress, }; } public componentWillMount() { const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); // tslint:disable-next-line:no-floating-promises this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); } public componentWillUnmount() { this._isUnmounted = true; } public componentWillReceiveProps(nextProps: WalletProps) { if ( nextProps.userAddress !== this.props.userAddress || nextProps.networkId !== this.props.networkId || nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch ) { const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); // tslint:disable-next-line:no-floating-promises this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); } if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) { const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens); const newTokenAddresses = _.map(newTokens, token => token.address); // Add placeholder entry for this token to the state, since fetching the // balance/allowance is asynchronous const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; for (const tokenAddress of newTokenAddresses) { trackedTokenStateByAddress[tokenAddress] = { balance: new BigNumber(0), allowance: new BigNumber(0), isLoaded: false, }; } this.setState({ trackedTokenStateByAddress, }); // Fetch the actual balance/allowance. // tslint:disable-next-line:no-floating-promises this._fetchBalancesAndAllowancesAsync(newTokenAddresses); } } public render() { const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return
{isReadyToRender ? this._renderRows() :
}
; } private _renderRows() { return ( {_.concat( this._renderHeaderRows(), this._renderEthRows(), this._renderTokenRows(), this._renderFooterRows(), )} ); } private _renderHeaderRows() { const userAddress = this.props.userAddress; const primaryText = utils.getAddressBeginAndEnd(userAddress); return ( } style={{ ...styles.headerItem, ...styles.borderedItem }} innerDivStyle={styles.headerItemInnerDiv} /> ); } private _renderFooterRows() { const primaryText = '+ other tokens'; return ( ); } private _renderEthRows() { const primaryText = this._renderAmount( this.props.userEtherBalanceInWei, constants.DECIMAL_PLACES_ETH, ETHER_SYMBOL, ); const accessoryItemConfig = { wrappedEtherAction: WrappedEtherAction.Wrap, }; return ( } rightAvatar={this._renderAccessoryItems(accessoryItemConfig)} style={{ ...styles.tokenItem, ...styles.borderedItem }} innerDivStyle={styles.tokenItemInnerDiv} /> ); } private _renderTokenRows() { const trackedTokens = this.props.trackedTokens; const trackedTokensStartingWithEtherToken = trackedTokens.sort( firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) .thenBy('address'), ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } private _renderTokenRow(token: Token) { const tokenState = this.state.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, this.props.networkId, EtherscanLinkSuffixes.Address, ); const amount = this._renderAmount(tokenState.balance, token.decimals, token.symbol); const wrappedEtherAction = token.symbol === ETHER_TOKEN_SYMBOL ? WrappedEtherAction.Unwrap : undefined; const accessoryItemConfig: AccessoryItemConfig = { wrappedEtherAction, allowanceToggleConfig: { token, tokenState, }, }; return ( ); } private _renderAccessoryItems(config: AccessoryItemConfig) { const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherAction); const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig); return (
{shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherAction)}
{shouldShowToggle && this._renderAllowanceToggle(config.allowanceToggleConfig)}
); } private _renderAllowanceToggle(config: AllowanceToggleConfig) { return ( ); } private _renderAmount(amount: BigNumber, decimals: number, symbol: string) { const unitAmount = ZeroEx.toUnitAmount(amount, decimals); const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION); const result = `${formattedAmount} ${symbol}`; return
{result}
; } private _renderTokenIcon(token: Token, tokenLink?: string) { const tooltipId = `tooltip-${token.address}`; const tokenIcon = ; if (_.isUndefined(tokenLink)) { return tokenIcon; } else { return ( {tokenIcon} ); } } private _renderWrappedEtherButton(action: WrappedEtherAction) { let buttonLabel; let buttonIcon; switch (action) { case WrappedEtherAction.Wrap: buttonLabel = 'wrap'; buttonIcon = ; break; case WrappedEtherAction.Unwrap: buttonLabel = 'unwrap'; buttonIcon = ; break; default: throw utils.spawnSwitchErr('wrappedEtherAction', action); } return ( ); } private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) { const trackedTokenStateByAddress: TokenStateByAddress = {}; _.each(trackedTokens, token => { trackedTokenStateByAddress[token.address] = { balance: new BigNumber(0), allowance: new BigNumber(0), isLoaded: false, }; }); return trackedTokenStateByAddress; } private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) { const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; for (const tokenAddress of tokenAddresses) { const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( userAddressIfExists, tokenAddress, ); trackedTokenStateByAddress[tokenAddress] = { balance, allowance, isLoaded: true, }; } if (!this._isUnmounted) { this.setState({ trackedTokenStateByAddress, }); } } private async _refetchTokenStateAsync(tokenAddress: string) { const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( userAddressIfExists, tokenAddress, ); this.setState({ trackedTokenStateByAddress: { ...this.state.trackedTokenStateByAddress, [tokenAddress]: { balance, allowance, isLoaded: true, }, }, }); } }