aboutsummaryrefslogblamecommitdiffstats
path: root/packages/website/ts/components/token_balances.tsx
blob: d91dbd093933bfafee1c9f98ed77e8d2444b3959 (plain) (tree)
1
2
3
4
5
6
7
8
9
                               
                                             
                                                
                            


                                                
                                                                    
                                                    

                                                                 
                                                                                                               
                               
                                               
                                   








                                                                                 
        
                
                       

                          
             



                   

                    




                                                        


















                                             







                               






                                   
                           


                                
                                       






                                     

                               
                                                    


                                                                                            
                                  

                                                   
                                  
                                                                                                                  






                                                                        
                                                                          

          
                                 
                                                                                    
                                                        

                                                                     


                                   









                                                                                                              













                                                                                              













                                                                                     
                                                            
                                                                     







                                    
                                

                              
                                                                        



                                     
                                    

                              
                                                                         

               







                                                               
                                                                                       

                                         
                                                                

                                 
                                                           

                                                                                             

                                                                                                    
                                                                         
                                                                                   

                                                                 


                                                                                      

                                                            
                                                                                  
                                                                          


                                                                                       
                                                                 

                                         
                                  


                                                                                                       
                                                                                                  
                      
                                                                 



                                                                                    
                                                                                                  
                                               
                                                                              

                                                                                      
                              
                                                    
                                                                                   
                                                                                    
                                                                                                                  
                                                    
                              




                                                            
                                                                                                                       


                                                                                    
                                                                        


                                                                                        
                                  
                                             
                                                                                                  
                                               
                                                                           



                                                                 
                                                                                                  

                                                 
                              
                                                    


                                                                                
                                                                 
                                                                                          

                                                 
                              


                                   
                                                                       
                                                
                                                                                           

                                                               
                                                                                                                  



                                                               
                                                                                                                     





                                               
                                  

                                                                                                          
                      
                                                                                                          

                                                                                    

                                                                                                     
                                               
                                                                             
                                                                                                             
                                                

                                                                                                                       

                                   
                                                                                                    


                                 
                                                    

                                                               
                                                                                
                 
                                                   


                                               


                                                                                   



                                                           
                                                   







                                                         
                                                                       





                                                                                                                    
                                     
                                                                                                    

                      
                                                                         

                                                                                   
                                                       
                                                                       


                                                                    


                                                
                                                                          


                         
                                                                                         
                                                                                





                                                         






                                                                                                                     
                





                                                                                               
                                                          
                            
                      
                                 
                                                                            












                                                                                                   


                                    
                                                        



                                                          
                                                                          
                                                            

                                                                                                       

                                 

                                                                                                      

                                              
                                                                                           
                                                   
                                                                                          
                          

                                                         
                                                                                






                                                                                           
                                 
                                                                
                                   



                                                         

                                   

                                                                


                                                              
                                                                   

                                                                                                           

                                     
                  


                       
                                                       






                                                              
                                                                                                      


                                                                  

                                  
                                     
                  



                                                                         







                                                                                                               
                             



                                              
                                                                


                                                                 
                                            



                                                                     
                                                                                              





                                                                           
                                      



                                                       

                                                                                                                        
                                                               





                                                 

                                                                                                                       



                                               
                                                                                                                     

                                           
                                                                                                                       
 


                                                    

                                                                                                                      









                                                                              
                                      

                                     


                                                                                                                       











                                                        
                                                      



                       
                                                                          

                                                                   
                                                              



                                                                                                                 
                                    
                                                                                      


                                                                                  
                                                       



                                                                     


                                                     
                                                 


                         
                                                                                  






                                                                                    
                                                                             








                                                                 


                                                                                                                     


                                                                                              



                                                                


                           
                                                                                                               







                                              

                                          
               
                                                            
                                                


                    
                                                   



                                 
                                     



                                                                     
                                  




                                    
                                     




                                    



























                                                                                                      




                                           



























                                                                                                  
                                       
import { ZeroEx } from '0x.js';
import { BigNumber } from '@0xproject/utils';
import DharmaLoanFrame from 'dharma-loan-frame';
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
import Divider from 'material-ui/Divider';
import FlatButton from 'material-ui/FlatButton';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import RaisedButton from 'material-ui/RaisedButton';
import ContentAdd from 'material-ui/svg-icons/content/add';
import ContentRemove from 'material-ui/svg-icons/content/remove';
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table';
import * as React from 'react';
import ReactTooltip = require('react-tooltip');
import firstBy = require('thenby');
import { Blockchain } from 'ts/blockchain';
import { AssetPicker } from 'ts/components/generate_order/asset_picker';
import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle';
import { SendButton } from 'ts/components/send_button';
import { HelpTooltip } from 'ts/components/ui/help_tooltip';
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
import { TokenIcon } from 'ts/components/ui/token_icon';
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import {
    BalanceErrs,
    BlockchainCallErrs,
    BlockchainErrs,
    EtherscanLinkSuffixes,
    Networks,
    ScreenWidths,
    Styles,
    Token,
    TokenByAddress,
    TokenVisibility,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';

const ETHER_ICON_PATH = '/images/ether.png';
const ETHER_TOKEN_SYMBOL = 'WETH';
const ZRX_TOKEN_SYMBOL = 'ZRX';

const PRECISION = 5;
const ICON_DIMENSION = 40;
const ARTIFICIAL_FAUCET_REQUEST_DELAY = 1000;
const TOKEN_TABLE_ROW_HEIGHT = 60;
const MAX_TOKEN_TABLE_HEIGHT = 420;
const TOKEN_COL_SPAN_LG = 2;
const TOKEN_COL_SPAN_SM = 1;

const styles: Styles = {
    bgColor: {
        backgroundColor: colors.grey50,
    },
};

interface TokenStateByAddress {
    [address: string]: {
        balance: BigNumber;
        allowance: BigNumber;
        isLoaded: boolean;
    };
}

interface TokenBalancesProps {
    blockchain: Blockchain;
    blockchainErr: BlockchainErrs;
    blockchainIsLoaded: boolean;
    dispatcher: Dispatcher;
    screenWidth: ScreenWidths;
    tokenByAddress: TokenByAddress;
    trackedTokens: Token[];
    userAddress: string;
    userEtherBalance: BigNumber;
    networkId: number;
    lastForceTokenStateRefetch: number;
}

interface TokenBalancesState {
    errorType: BalanceErrs;
    isBalanceSpinnerVisible: boolean;
    isDharmaDialogVisible: boolean;
    isZRXSpinnerVisible: boolean;
    isTokenPickerOpen: boolean;
    isAddingToken: boolean;
    trackedTokenStateByAddress: TokenStateByAddress;
}

export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
    private _isUnmounted: boolean;
    public constructor(props: TokenBalancesProps) {
        super(props);
        this._isUnmounted = false;
        const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
        this.state = {
            errorType: undefined,
            isBalanceSpinnerVisible: false,
            isZRXSpinnerVisible: false,
            isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(),
            isTokenPickerOpen: false,
            isAddingToken: false,
            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: TokenBalancesProps) {
        if (nextProps.userEtherBalance !== this.props.userEtherBalance) {
            if (this.state.isBalanceSpinnerVisible) {
                const receivedAmount = nextProps.userEtherBalance.minus(this.props.userEtherBalance);
                this.props.dispatcher.showFlashMessage(`Received ${receivedAmount.toString(10)} Kovan Ether`);
            }
            this.setState({
                isBalanceSpinnerVisible: false,
            });
        }

        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 componentDidMount() {
        window.scrollTo(0, 0);
    }
    public render() {
        const errorDialogActions = [
            <FlatButton
                key="errorOkBtn"
                label="Ok"
                primary={true}
                onTouchTap={this._onErrorDialogToggle.bind(this, false)}
            />,
        ];
        const dharmaDialogActions = [
            <FlatButton
                key="dharmaCloseBtn"
                label="Close"
                primary={true}
                onTouchTap={this._onDharmaDialogToggle.bind(this, false)}
            />,
        ];
        const isTestNetwork = _.includes(
            [
                constants.NETWORK_ID_BY_NAME[Networks.Kovan],
                constants.NETWORK_ID_BY_NAME[Networks.Rinkeby],
                constants.NETWORK_ID_BY_NAME[Networks.Ropsten],
            ],
            this.props.networkId,
        );
        const isKovanTestNetwork = this.props.networkId === constants.NETWORK_ID_KOVAN;
        const dharmaButtonColumnStyle = {
            paddingLeft: 3,
            display: isKovanTestNetwork ? 'table-cell' : 'none',
        };
        const stubColumnStyle = {
            display: isTestNetwork ? 'none' : 'table-cell',
        };
        const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT;
        const tokenTableHeight =
            allTokenRowHeight < MAX_TOKEN_TABLE_HEIGHT ? allTokenRowHeight : MAX_TOKEN_TABLE_HEIGHT;
        const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
        const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
        const dharmaLoanExplanation =
            'If you need access to larger amounts of ether,<br> \
                                     you can request a loan from the Dharma Loan<br> \
                                     network.  Your loan should be funded in 5<br>  \
                                     minutes or less.';
        const allowanceExplanation =
            '0x smart contracts require access to your<br> \
                                  token balances in order to execute trades.<br> \
                                  Toggling sets an allowance for the<br> \
                                  smart contract so you can start trading that token.';
        return (
            <div className="lg-px4 md-px4 sm-px1 pb2">
                <h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3>
                <Divider />
                <div className="pt2 pb2">
                    {isTestNetwork
                        ? 'In order to try out the 0x Portal Dapp, request some test ether to pay for \
                        gas costs. It might take a bit of time for the test ether to show up.'
                        : 'Ether must be converted to Ether Tokens in order to be tradable via 0x. \
                         You can convert between Ether and Ether Tokens from the "Wrap ETH" tab.'}
                </div>
                <Table selectable={false} style={styles.bgColor}>
                    <TableHeader displaySelectAll={false} adjustForCheckbox={false}>
                        <TableRow>
                            <TableHeaderColumn>Currency</TableHeaderColumn>
                            <TableHeaderColumn>Balance</TableHeaderColumn>
                            <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
                            {isTestNetwork && (
                                <TableHeaderColumn style={{ paddingLeft: 3 }}>
                                    {isSmallScreen ? 'Faucet' : 'Request from faucet'}
                                </TableHeaderColumn>
                            )}
                            {isKovanTestNetwork && (
                                <TableHeaderColumn style={dharmaButtonColumnStyle}>
                                    {isSmallScreen ? 'Loan' : 'Request Dharma loan'}
                                    <HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} />
                                </TableHeaderColumn>
                            )}
                        </TableRow>
                    </TableHeader>
                    <TableBody displayRowCheckbox={false}>
                        <TableRow key="ETH">
                            <TableRowColumn className="py1">
                                <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />
                            </TableRowColumn>
                            <TableRowColumn>
                                {this.props.userEtherBalance.toFixed(PRECISION)} ETH
                                {this.state.isBalanceSpinnerVisible && (
                                    <span className="pl1">
                                        <i className="zmdi zmdi-spinner zmdi-hc-spin" />
                                    </span>
                                )}
                            </TableRowColumn>
                            <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
                            {isTestNetwork && (
                                <TableRowColumn style={{ paddingLeft: 3 }}>
                                    <LifeCycleRaisedButton
                                        labelReady="Request"
                                        labelLoading="Sending..."
                                        labelComplete="Sent!"
                                        onClickAsyncFn={this._faucetRequestAsync.bind(this, true)}
                                    />
                                </TableRowColumn>
                            )}
                            {isKovanTestNetwork && (
                                <TableRowColumn style={dharmaButtonColumnStyle}>
                                    <RaisedButton
                                        label="Request"
                                        style={{ width: '100%' }}
                                        onTouchTap={this._onDharmaDialogToggle.bind(this)}
                                    />
                                </TableRowColumn>
                            )}
                        </TableRow>
                    </TableBody>
                </Table>
                <div className="clearfix" style={{ paddingBottom: 1 }}>
                    <div className="col col-10">
                        <h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
                    </div>
                    <div className="col col-1 pt3 align-right">
                        <FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}>
                            <ContentAdd />
                        </FloatingActionButton>
                    </div>
                    <div className="col col-1 pt3 align-right">
                        <FloatingActionButton mini={true} zDepth={0} onClick={this._onRemoveTokenClicked.bind(this)}>
                            <ContentRemove />
                        </FloatingActionButton>
                    </div>
                </div>
                <Divider />
                <div className="pt2 pb2">
                    {isTestNetwork
                        ? "Mint some test tokens you'd like to use to generate or fill an order using 0x."
                        : "Set trading permissions for a token you'd like to start trading."}
                </div>
                <Table selectable={false} bodyStyle={{ height: tokenTableHeight }} style={styles.bgColor}>
                    <TableHeader displaySelectAll={false} adjustForCheckbox={false}>
                        <TableRow>
                            <TableHeaderColumn colSpan={tokenColSpan}>Token</TableHeaderColumn>
                            <TableHeaderColumn style={{ paddingLeft: 3 }}>Balance</TableHeaderColumn>
                            <TableHeaderColumn>
                                <div className="inline-block">Allowance</div>
                                <HelpTooltip style={{ paddingLeft: 4 }} explanation={allowanceExplanation} />
                            </TableHeaderColumn>
                            <TableHeaderColumn>Action</TableHeaderColumn>
                            {this.props.screenWidth !== ScreenWidths.Sm && <TableHeaderColumn>Send</TableHeaderColumn>}
                        </TableRow>
                    </TableHeader>
                    <TableBody displayRowCheckbox={false}>{this._renderTokenTableRows()}</TableBody>
                </Table>
                <Dialog
                    title="Oh oh"
                    titleStyle={{ fontWeight: 100 }}
                    actions={errorDialogActions}
                    open={!_.isUndefined(this.state.errorType)}
                    onRequestClose={this._onErrorDialogToggle.bind(this, false)}
                >
                    {this._renderErrorDialogBody()}
                </Dialog>
                <Dialog
                    title="Request Dharma Loan"
                    titleStyle={{ fontWeight: 100, backgroundColor: colors.white }}
                    bodyStyle={{ backgroundColor: colors.dharmaDarkGrey }}
                    actionsContainerStyle={{ backgroundColor: colors.white }}
                    autoScrollBodyContent={true}
                    actions={dharmaDialogActions}
                    open={this.state.isDharmaDialogVisible}
                >
                    {this._renderDharmaLoanFrame()}
                </Dialog>
                <AssetPicker
                    userAddress={this.props.userAddress}
                    networkId={this.props.networkId}
                    blockchain={this.props.blockchain}
                    dispatcher={this.props.dispatcher}
                    isOpen={this.state.isTokenPickerOpen}
                    currentTokenAddress={''}
                    onTokenChosen={this._onAssetTokenPicked.bind(this)}
                    tokenByAddress={this.props.tokenByAddress}
                    tokenVisibility={this.state.isAddingToken ? TokenVisibility.UNTRACKED : TokenVisibility.TRACKED}
                />
            </div>
        );
    }
    private _renderTokenTableRows() {
        if (!this.props.blockchainIsLoaded || this.props.blockchainErr !== BlockchainErrs.NoError) {
            return '';
        }
        const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
        const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
        const actionPaddingX = isSmallScreen ? 2 : 24;
        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'),
        );
        const tableRows = _.map(
            trackedTokensStartingWithEtherToken,
            this._renderTokenRow.bind(this, tokenColSpan, actionPaddingX),
        );
        return tableRows;
    }
    private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) {
        const tokenState = this.state.trackedTokenStateByAddress[token.address];
        const tokenLink = utils.getEtherScanLinkIfExists(
            token.address,
            this.props.networkId,
            EtherscanLinkSuffixes.Address,
        );
        const isMintable =
            (_.includes(configs.SYMBOLS_OF_MINTABLE_KOVAN_TOKENS, token.symbol) &&
                this.props.networkId === constants.NETWORK_ID_BY_NAME[Networks.Kovan]) ||
            (_.includes(configs.SYMBOLS_OF_MINTABLE_RINKEBY_ROPSTEN_TOKENS, token.symbol) &&
                _.includes(
                    [constants.NETWORK_ID_BY_NAME[Networks.Rinkeby], constants.NETWORK_ID_BY_NAME[Networks.Ropsten]],
                    this.props.networkId,
                ));
        return (
            <TableRow key={token.address} style={{ height: TOKEN_TABLE_ROW_HEIGHT }}>
                <TableRowColumn colSpan={tokenColSpan}>
                    {_.isUndefined(tokenLink) ? (
                        this._renderTokenName(token)
                    ) : (
                        <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}>
                            {this._renderTokenName(token)}
                        </a>
                    )}
                </TableRowColumn>
                <TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}>
                    {tokenState.isLoaded ? (
                        <span>
                            {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol}
                            {this.state.isZRXSpinnerVisible &&
                                token.symbol === ZRX_TOKEN_SYMBOL && (
                                    <span className="pl1">
                                        <i className="zmdi zmdi-spinner zmdi-hc-spin" />
                                    </span>
                                )}
                        </span>
                    ) : (
                        <i className="zmdi zmdi-spinner zmdi-hc-spin" />
                    )}
                </TableRowColumn>
                <TableRowColumn>
                    <AllowanceToggle
                        networkId={this.props.networkId}
                        blockchain={this.props.blockchain}
                        dispatcher={this.props.dispatcher}
                        token={token}
                        tokenState={tokenState}
                        onErrorOccurred={this._onErrorOccurred.bind(this)}
                        userAddress={this.props.userAddress}
                        isDisabled={!tokenState.isLoaded}
                        refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
                    />
                </TableRowColumn>
                <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
                    {isMintable && (
                        <LifeCycleRaisedButton
                            labelReady="Mint"
                            labelLoading={<span style={{ fontSize: 12 }}>Minting...</span>}
                            labelComplete="Minted!"
                            onClickAsyncFn={this._onMintTestTokensAsync.bind(this, token)}
                        />
                    )}
                    {token.symbol === ZRX_TOKEN_SYMBOL &&
                        this.props.networkId === constants.NETWORK_ID_KOVAN && (
                            <LifeCycleRaisedButton
                                labelReady="Request"
                                labelLoading="Sending..."
                                labelComplete="Sent!"
                                onClickAsyncFn={this._faucetRequestAsync.bind(this, false)}
                            />
                        )}
                </TableRowColumn>
                {this.props.screenWidth !== ScreenWidths.Sm && (
                    <TableRowColumn
                        style={{
                            paddingLeft: actionPaddingX,
                            paddingRight: actionPaddingX,
                        }}
                    >
                        <SendButton
                            userAddress={this.props.userAddress}
                            networkId={this.props.networkId}
                            blockchain={this.props.blockchain}
                            dispatcher={this.props.dispatcher}
                            token={token}
                            onError={this._onSendFailed.bind(this)}
                            lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
                            refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
                        />
                    </TableRowColumn>
                )}
            </TableRow>
        );
    }
    private _onAssetTokenPicked(tokenAddress: string) {
        if (_.isEmpty(tokenAddress)) {
            this.setState({
                isTokenPickerOpen: false,
            });
            return;
        }
        const token = this.props.tokenByAddress[tokenAddress];
        const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol);
        if (!this.state.isAddingToken && !isDefaultTrackedToken) {
            if (token.isRegistered) {
                // Remove the token from tracked tokens
                const newToken = {
                    ...token,
                    isTracked: false,
                };
                this.props.dispatcher.updateTokenByAddress([newToken]);
            } else {
                this.props.dispatcher.removeTokenToTokenByAddress(token);
            }
            trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
        } else if (isDefaultTrackedToken) {
            this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`);
        }
        this.setState({
            isTokenPickerOpen: false,
        });
    }
    private _onSendFailed() {
        this.setState({
            errorType: BalanceErrs.sendFailed,
        });
    }
    private _renderAmount(amount: BigNumber, decimals: number) {
        const unitAmount = ZeroEx.toUnitAmount(amount, decimals);
        return unitAmount.toNumber().toFixed(PRECISION);
    }
    private _renderTokenName(token: Token) {
        const tooltipId = `tooltip-${token.address}`;
        return (
            <div className="flex">
                <TokenIcon token={token} diameter={ICON_DIMENSION} />
                <div data-tip={true} data-for={tooltipId} className="mt2 ml2 sm-hide xs-hide">
                    {token.name}
                </div>
                <ReactTooltip id={tooltipId}>{token.address}</ReactTooltip>
            </div>
        );
    }
    private _renderErrorDialogBody() {
        switch (this.state.errorType) {
            case BalanceErrs.incorrectNetworkForFaucet:
                return (
                    <div>
                        Our faucet can only send test Ether to addresses on the {Networks.Kovan} testnet (networkId{' '}
                        {constants.NETWORK_ID_KOVAN}). Please make sure you are connected to the {Networks.Kovan}{' '}
                        testnet and try requesting ether again.
                    </div>
                );

            case BalanceErrs.faucetRequestFailed:
                return (
                    <div>
                        An unexpected error occurred while trying to request test Ether from our faucet. Please refresh
                        the page and try again.
                    </div>
                );

            case BalanceErrs.faucetQueueIsFull:
                return <div>Our test Ether faucet queue is full. Please try requesting test Ether again later.</div>;

            case BalanceErrs.mintingFailed:
                return <div>Minting your test tokens failed unexpectedly. Please refresh the page and try again.</div>;

            case BalanceErrs.allowanceSettingFailed:
                return (
                    <div>
                        An unexpected error occurred while trying to set your test token allowance. Please refresh the
                        page and try again.
                    </div>
                );

            case undefined:
                return null; // No error to show

            default:
                throw utils.spawnSwitchErr('errorType', this.state.errorType);
        }
    }
    private _renderDharmaLoanFrame() {
        if (utils.isUserOnMobile()) {
            return (
                <h4 style={{ textAlign: 'center' }}>
                    We apologize -- Dharma loan requests are not available on mobile yet. Please try again through your
                    desktop browser.
                </h4>
            );
        } else {
            return (
                <DharmaLoanFrame
                    partner="0x"
                    env={utils.getCurrentEnvironment()}
                    screenWidth={this.props.screenWidth}
                />
            );
        }
    }
    private _onErrorOccurred(errorType: BalanceErrs) {
        this.setState({
            errorType,
        });
    }
    private async _onMintTestTokensAsync(token: Token): Promise<boolean> {
        try {
            await this.props.blockchain.mintTestTokensAsync(token);
            await this._refetchTokenStateAsync(token.address);
            const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals);
            this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`);
            return true;
        } catch (err) {
            const errMsg = `${err}`;
            if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
                this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
                return false;
            }
            if (utils.didUserDenyWeb3Request(errMsg)) {
                return false;
            }
            utils.consoleLog(`Unexpected error encountered: ${err}`);
            utils.consoleLog(err.stack);
            this.setState({
                errorType: BalanceErrs.mintingFailed,
            });
            await errorReporter.reportAsync(err);
            return false;
        }
    }
    private async _faucetRequestAsync(isEtherRequest: boolean): Promise<boolean> {
        if (this.props.userAddress === '') {
            this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
            return false;
        }

        // If on another network other then the testnet our faucet serves test ether
        // from, we must show user an error message
        if (this.props.blockchain.networkId !== constants.NETWORK_ID_KOVAN) {
            this.setState({
                errorType: BalanceErrs.incorrectNetworkForFaucet,
            });
            return false;
        }

        await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY);

        const segment = isEtherRequest ? 'ether' : 'zrx';
        const response = await fetch(
            `${constants.URL_TESTNET_FAUCET}/${segment}/${this.props.userAddress}?networkId=${this.props.networkId}`,
        );
        const responseBody = await response.text();
        if (response.status !== constants.SUCCESS_STATUS) {
            utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`);
            const errorType =
                response.status === constants.UNAVAILABLE_STATUS
                    ? BalanceErrs.faucetQueueIsFull
                    : BalanceErrs.faucetRequestFailed;
            this.setState({
                errorType,
            });
            await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`));
            return false;
        }

        if (isEtherRequest) {
            this.setState({
                isBalanceSpinnerVisible: true,
            });
        } else {
            this.setState({
                isZRXSpinnerVisible: true,
            });
            // tslint:disable-next-line:no-floating-promises
            this._startPollingZrxBalanceAsync();
        }
        return true;
    }
    private _onErrorDialogToggle(isOpen: boolean) {
        this.setState({
            errorType: undefined,
        });
    }
    private _onDharmaDialogToggle() {
        this.setState({
            isDharmaDialogVisible: !this.state.isDharmaDialogVisible,
        });
    }
    private _onAddTokenClicked() {
        this.setState({
            isTokenPickerOpen: true,
            isAddingToken: true,
        });
    }
    private _onRemoveTokenClicked() {
        this.setState({
            isTokenPickerOpen: true,
            isAddingToken: false,
        });
    }
    private async _startPollingZrxBalanceAsync() {
        const tokens = _.values(this.props.tokenByAddress);
        const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL);

        // tslint:disable-next-line:no-floating-promises
        const balance = await this.props.blockchain.pollTokenBalanceAsync(zrxToken);
        const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
        trackedTokenStateByAddress[zrxToken.address] = {
            ...trackedTokenStateByAddress[zrxToken.address],
            balance,
        };
        this.setState({
            isZRXSpinnerVisible: false,
        });
    }
    private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) {
        const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
        for (const tokenAddress of tokenAddresses) {
            const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
                this.props.userAddress,
                tokenAddress,
            );
            trackedTokenStateByAddress[tokenAddress] = {
                balance,
                allowance,
                isLoaded: true,
            };
        }
        if (!this._isUnmounted) {
            this.setState({
                trackedTokenStateByAddress,
            });
        }
    }
    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 _refetchTokenStateAsync(tokenAddress: string) {
        const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
            this.props.userAddress,
            tokenAddress,
        );
        this.setState({
            trackedTokenStateByAddress: {
                ...this.state.trackedTokenStateByAddress,
                [tokenAddress]: {
                    balance,
                    allowance,
                    isLoaded: true,
                },
            },
        });
    }
} // tslint:disable:max-file-line-count