import {ZeroEx} from '0x.js'; import BigNumber from 'bignumber.js'; 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, ScreenWidths, Styles, Token, TokenByAddress, TokenStateByAddress, 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 TokenBalancesProps { blockchain: Blockchain; blockchainErr: BlockchainErrs; blockchainIsLoaded: boolean; dispatcher: Dispatcher; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; tokenStateByAddress: TokenStateByAddress; userAddress: string; userEtherBalance: BigNumber; networkId: number; } interface TokenBalancesState { errorType: BalanceErrs; isBalanceSpinnerVisible: boolean; isDharmaDialogVisible: boolean; isZRXSpinnerVisible: boolean; currentZrxBalance?: BigNumber; isTokenPickerOpen: boolean; isAddingToken: boolean; } export class TokenBalances extends React.Component { public constructor(props: TokenBalancesProps) { super(props); this.state = { errorType: undefined, isBalanceSpinnerVisible: false, isZRXSpinnerVisible: false, isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), isTokenPickerOpen: false, isAddingToken: false, }; } 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, }); } const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL); const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance; if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) { if (this.state.isZRXSpinnerVisible) { const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance); const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, 18); this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`); } this.setState({ isZRXSpinnerVisible: false, currentZrxBalance: undefined, }); } } public componentDidMount() { window.scrollTo(0, 0); } public render() { const errorDialogActions = [ , ]; const dharmaDialogActions = [ , ]; const isTestNetwork = this.props.networkId === constants.TESTNET_NETWORK_ID; const dharmaButtonColumnStyle = { paddingLeft: 3, display: isTestNetwork ? '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,
\ you can request a loan from the Dharma Loan
\ network. Your loan should be funded in 5
\ minutes or less.'; const allowanceExplanation = '0x smart contracts require access to your
\ token balances in order to execute trades.
\ Toggling sets an allowance for the
\ smart contract so you can start trading that token.'; return (

{isTestNetwork ? 'Test ether' : 'Ether'}

{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 by clicking the "convert" button below.' }
Currency Balance { isTestNetwork && {isSmallScreen ? 'Faucet' : 'Request from faucet'} } { isTestNetwork && {isSmallScreen ? 'Loan' : 'Request Dharma loan'} } {this.props.userEtherBalance.toFixed(PRECISION)} ETH {this.state.isBalanceSpinnerVisible && } { isTestNetwork && } { isTestNetwork && }

{isTestNetwork ? 'Test tokens' : 'Tokens'}

{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.' }
Token Balance
Allowance
Action {this.props.screenWidth !== ScreenWidths.Sm && Send }
{this.renderTokenTableRows()}
{this.renderErrorDialogBody()} {this.renderDharmaLoanFrame()}
); } 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 allTokens = _.values(this.props.tokenByAddress); const trackedTokens = _.filter(allTokens, t => t.isTracked); 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.props.tokenStateByAddress[token.address]; const tokenLink = utils.getEtherScanLinkIfExists(token.address, this.props.networkId, EtherscanLinkSuffixes.Address); const isMintable = _.includes(configs.symbolsOfMintableTokens, token.symbol) && this.props.networkId !== constants.MAINNET_NETWORK_ID; return ( {_.isUndefined(tokenLink) ? this.renderTokenName(token) : {this.renderTokenName(token)} } {this.renderAmount(tokenState.balance, token.decimals)} {token.symbol} {this.state.isZRXSpinnerVisible && token.symbol === ZRX_TOKEN_SYMBOL && } {isMintable && Minting...} labelComplete="Minted!" onClickAsyncFn={this.onMintTestTokensAsync.bind(this, token)} /> } {token.symbol === ZRX_TOKEN_SYMBOL && this.props.networkId === constants.TESTNET_NETWORK_ID && } {this.props.screenWidth !== ScreenWidths.Sm && } ); } private onAssetTokenPicked(tokenAddress: string) { if (_.isEmpty(tokenAddress)) { this.setState({ isTokenPickerOpen: false, }); return; } const token = this.props.tokenByAddress[tokenAddress]; const isDefaultTrackedToken = _.includes(configs.defaultTrackedTokenSymbols, token.symbol); if (!this.state.isAddingToken && !isDefaultTrackedToken) { if (token.isRegistered) { // Remove the token from tracked tokens const newToken = _.assign({}, token, { isTracked: false, }); this.props.dispatcher.updateTokenByAddress([newToken]); } else { this.props.dispatcher.removeTokenToTokenByAddress(token); } this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress); 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 (
{token.name}
{token.address}
); } private renderErrorDialogBody() { switch (this.state.errorType) { case BalanceErrs.incorrectNetworkForFaucet: return (
Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} {' '}testnet (networkId {constants.TESTNET_NETWORK_ID}). Please make sure you are {' '}connected to the {constants.TESTNET_NAME} testnet and try requesting ether again.
); case BalanceErrs.faucetRequestFailed: return (
An unexpected error occurred while trying to request test Ether from our faucet. {' '}Please refresh the page and try again.
); case BalanceErrs.faucetQueueIsFull: return (
Our test Ether faucet queue is full. Please try requesting test Ether again later.
); case BalanceErrs.mintingFailed: return (
Minting your test tokens failed unexpectedly. Please refresh the page and try again.
); case BalanceErrs.allowanceSettingFailed: return (
An unexpected error occurred while trying to set your test token allowance. {' '}Please refresh the page and try again.
); case undefined: return null; // No error to show default: throw utils.spawnSwitchErr('errorType', this.state.errorType); } } private renderDharmaLoanFrame() { if (utils.isUserOnMobile()) { return (

We apologize -- Dharma loan requests are not available on mobile yet. Please try again through your desktop browser.

); } else { return ( ); } } private onErrorOccurred(errorType: BalanceErrs) { this.setState({ errorType, }); } private async onMintTestTokensAsync(token: Token): Promise { try { await this.props.blockchain.mintTestTokensAsync(token); 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 (_.includes(errMsg, 'User denied transaction')) { return false; } utils.consoleLog(`Unexpected error encountered: ${err}`); utils.consoleLog(err.stack); await errorReporter.reportAsync(err); this.setState({ errorType: BalanceErrs.mintingFailed, }); return false; } } private async faucetRequestAsync(isEtherRequest: boolean): Promise { 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.TESTNET_NETWORK_ID) { this.setState({ errorType: BalanceErrs.incorrectNetworkForFaucet, }); return false; } await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY); const segment = isEtherRequest ? 'ether' : 'zrx'; const response = await fetch(`${constants.ETHER_FAUCET_ENDPOINT}/${segment}/${this.props.userAddress}`); const responseBody = await response.text(); if (response.status !== constants.SUCCESS_STATUS) { utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`); await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`)); const errorType = response.status === constants.UNAVAILABLE_STATUS ? BalanceErrs.faucetQueueIsFull : BalanceErrs.faucetRequestFailed; this.setState({ errorType, }); return false; } if (isEtherRequest) { this.setState({ isBalanceSpinnerVisible: true, }); } else { const tokens = _.values(this.props.tokenByAddress); const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL); const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address]; this.setState({ isZRXSpinnerVisible: true, currentZrxBalance: zrxTokenState.balance, }); // tslint:disable-next-line:no-floating-promises this.props.blockchain.pollTokenBalanceAsync(zrxToken); } 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, }); } } // tslint:disable:max-file-line-count