diff options
Diffstat (limited to 'packages/website')
-rw-r--r-- | packages/website/public/images/ether.png | bin | 2312 -> 4235 bytes | |||
-rw-r--r-- | packages/website/public/images/token_icons/ether_erc20.png | bin | 69772 -> 7584 bytes | |||
-rw-r--r-- | packages/website/public/images/wrapped_eth_gray.png | bin | 0 -> 7649 bytes | |||
-rw-r--r-- | packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx | 130 | ||||
-rw-r--r-- | packages/website/ts/components/eth_weth_conversion_button.tsx | 37 | ||||
-rw-r--r-- | packages/website/ts/components/eth_wrappers.tsx | 360 | ||||
-rw-r--r-- | packages/website/ts/components/inputs/token_amount_input.tsx | 7 | ||||
-rw-r--r-- | packages/website/ts/components/portal.tsx | 18 | ||||
-rw-r--r-- | packages/website/ts/components/portal_menu.tsx | 8 | ||||
-rw-r--r-- | packages/website/ts/components/token_balances.tsx | 24 | ||||
-rw-r--r-- | packages/website/ts/index.tsx | 2 | ||||
-rw-r--r-- | packages/website/ts/local_storage/tracked_token_storage.ts | 11 | ||||
-rw-r--r-- | packages/website/ts/types.ts | 13 | ||||
-rw-r--r-- | packages/website/ts/utils/configs.ts | 25 |
14 files changed, 547 insertions, 88 deletions
diff --git a/packages/website/public/images/ether.png b/packages/website/public/images/ether.png Binary files differindex 6a40a976d..c92f45681 100644 --- a/packages/website/public/images/ether.png +++ b/packages/website/public/images/ether.png diff --git a/packages/website/public/images/token_icons/ether_erc20.png b/packages/website/public/images/token_icons/ether_erc20.png Binary files differindex f4154db7b..bc8beae8b 100644 --- a/packages/website/public/images/token_icons/ether_erc20.png +++ b/packages/website/public/images/token_icons/ether_erc20.png diff --git a/packages/website/public/images/wrapped_eth_gray.png b/packages/website/public/images/wrapped_eth_gray.png Binary files differnew file mode 100644 index 000000000..397b31b1c --- /dev/null +++ b/packages/website/public/images/wrapped_eth_gray.png diff --git a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx index aba5b9faf..c8bdced9b 100644 --- a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx +++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx @@ -1,14 +1,15 @@ import BigNumber from 'bignumber.js'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; -import RadioButton from 'material-ui/RadioButton'; -import RadioButtonGroup from 'material-ui/RadioButton/RadioButtonGroup'; import * as React from 'react'; import {EthAmountInput} from 'ts/components/inputs/eth_amount_input'; import {TokenAmountInput} from 'ts/components/inputs/token_amount_input'; import {Side, Token, TokenState} from 'ts/types'; +const DARK_BLUE = '#4D5481'; + interface EthWethConversionDialogProps { + direction: Side; onComplete: (direction: Side, value: BigNumber) => void; onCancelled: () => void; isOpen: boolean; @@ -19,7 +20,6 @@ interface EthWethConversionDialogProps { interface EthWethConversionDialogState { value?: BigNumber; - direction: Side; shouldShowIncompleteErrs: boolean; hasErrors: boolean; } @@ -29,7 +29,6 @@ export class EthWethConversionDialog extends constructor() { super(); this.state = { - direction: Side.deposit, shouldShowIncompleteErrs: false, hasErrors: true, }; @@ -48,11 +47,13 @@ export class EthWethConversionDialog extends onTouchTap={this.onConvertClick.bind(this)} />, ]; + const title = this.props.direction === Side.deposit ? 'Wrap ETH' : 'Unwrap WETH'; return ( <Dialog - title="I want to convert" + title={title} titleStyle={{fontWeight: 100}} actions={convertDialogActions} + contentStyle={{width: 448}} open={this.props.isOpen} > {this.renderConversionDialogBody()} @@ -60,56 +61,81 @@ export class EthWethConversionDialog extends ); } private renderConversionDialogBody() { + const explanation = this.props.direction === Side.deposit ? + 'Convert your Ether into a tokenized, tradable form.' : + 'Convert your Wrapped Ether back into it\'s native form.'; + const isWrappedVersion = this.props.direction === Side.receive; return ( - <div className="mx-auto" style={{maxWidth: 300}}> - <RadioButtonGroup - className="pb1" - defaultSelected={this.state.direction} - name="conversionDirection" - onChange={this.onConversionDirectionChange.bind(this)} - > - <RadioButton - className="pb1" - value={Side.deposit} - label="Ether -> Ether Tokens" - /> - <RadioButton - value={Side.receive} - label="Ether Tokens -> Ether" - /> - </RadioButtonGroup> - {this.state.direction === Side.receive ? - <TokenAmountInput - label="Amount to convert" - token={this.props.token} - tokenState={this.props.tokenState} - shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} - shouldCheckBalance={true} - shouldCheckAllowance={false} - onChange={this.onValueChange.bind(this)} - amount={this.state.value} - onVisitBalancesPageClick={this.props.onCancelled} - /> : - <EthAmountInput - label="Amount to convert" - balance={this.props.etherBalance} - amount={this.state.value} - onChange={this.onValueChange.bind(this)} - shouldCheckBalance={true} - shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} - onVisitBalancesPageClick={this.props.onCancelled} - /> - } + <div> + <div className="pb2"> + {explanation} + </div> + <div className="mx-auto" style={{maxWidth: 312}}> + <div className="flex"> + {this.renderCurrency(isWrappedVersion)} + <div style={{paddingTop: 68}}> + <i + style={{fontSize: 28, color: DARK_BLUE}} + className="zmdi zmdi-arrow-right" + /> + </div> + {this.renderCurrency(!isWrappedVersion)} + </div> + <div + className="pt2 mx-auto" + style={{width: 245}} + > + {this.props.direction === Side.receive ? + <TokenAmountInput + token={this.props.token} + tokenState={this.props.tokenState} + shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} + shouldCheckBalance={true} + shouldCheckAllowance={false} + onChange={this.onValueChange.bind(this)} + amount={this.state.value} + onVisitBalancesPageClick={this.props.onCancelled} + /> : + <EthAmountInput + balance={this.props.etherBalance} + amount={this.state.value} + onChange={this.onValueChange.bind(this)} + shouldCheckBalance={true} + shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} + onVisitBalancesPageClick={this.props.onCancelled} + /> + } + <div + className="pt1" + style={{fontSize: 12}} + > + 1 ETH = 1 WETH + </div> + </div> + </div> </div> ); } - private onConversionDirectionChange(e: any, direction: Side) { - this.setState({ - value: undefined, - shouldShowIncompleteErrs: false, - direction, - hasErrors: true, - }); + private renderCurrency(isWrappedVersion: boolean) { + const name = isWrappedVersion ? 'Wrapped Ether' : 'Ether'; + const iconUrl = isWrappedVersion ? '/images/token_icons/ether_erc20.png' : '/images/ether.png'; + const symbol = isWrappedVersion ? 'WETH' : 'ETH'; + return ( + <div className="mx-auto pt2"> + <div + className="center" + style={{color: DARK_BLUE}} + > + {name} + </div> + <div className="center py2"> + <img src={iconUrl} style={{width: 60}} /> + </div> + <div className="center" style={{fontSize: 12}}> + ({symbol}) + </div> + </div> + ); } private onValueChange(isValid: boolean, amount?: BigNumber) { this.setState({ @@ -127,7 +153,7 @@ export class EthWethConversionDialog extends this.setState({ value: undefined, }); - this.props.onComplete(this.state.direction, value); + this.props.onComplete(this.props.direction, value); } } private onCancel() { diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx index a83b1543f..b017de27b 100644 --- a/packages/website/ts/components/eth_weth_conversion_button.tsx +++ b/packages/website/ts/components/eth_weth_conversion_button.tsx @@ -12,12 +12,15 @@ import {errorReporter} from 'ts/utils/error_reporter'; import {utils} from 'ts/utils/utils'; interface EthWethConversionButtonProps { + direction: Side; ethToken: Token; ethTokenState: TokenState; dispatcher: Dispatcher; blockchain: Blockchain; userEtherBalance: BigNumber; - onError: () => void; + isOutdatedWrappedEther: boolean; + onConversionSuccessful?: () => void; + isDisabled?: boolean; } interface EthWethConversionButtonState { @@ -27,6 +30,10 @@ interface EthWethConversionButtonState { export class EthWethConversionButton extends React.Component<EthWethConversionButtonProps, EthWethConversionButtonState> { + public static defaultProps: Partial<EthWethConversionButtonProps> = { + isDisabled: false, + onConversionSuccessful: _.noop, + }; public constructor(props: EthWethConversionButtonProps) { super(props); this.state = { @@ -36,16 +43,26 @@ export class EthWethConversionButton extends } public render() { const labelStyle = this.state.isEthConversionHappening ? {fontSize: 10} : {}; + let callToActionLabel; + let inProgressLabel; + if (this.props.direction === Side.deposit) { + callToActionLabel = 'Wrap'; + inProgressLabel = 'Wrapping...'; + } else { + callToActionLabel = 'Unwrap'; + inProgressLabel = 'Unwrapping...'; + } return ( <div> <RaisedButton style={{width: '100%'}} labelStyle={labelStyle} - disabled={this.state.isEthConversionHappening} - label={this.state.isEthConversionHappening ? 'Converting...' : 'Convert'} + disabled={this.props.isDisabled || this.state.isEthConversionHappening} + label={this.state.isEthConversionHappening ? inProgressLabel : callToActionLabel} onClick={this.toggleConversionDialog.bind(this)} /> <EthWethConversionDialog + direction={this.props.direction} isOpen={this.state.isEthConversionDialogVisible} onComplete={this.onConversionAmountSelectedAsync.bind(this)} onCancelled={this.toggleConversionDialog.bind(this)} @@ -73,15 +90,18 @@ export class EthWethConversionButton extends if (direction === Side.deposit) { await this.props.blockchain.convertEthToWrappedEthTokensAsync(value); const ethAmount = ZeroEx.toUnitAmount(value, constants.ETH_DECIMAL_PLACES); - this.props.dispatcher.showFlashMessage(`Successfully converted ${ethAmount.toString()} ETH to WETH`); + this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`); balance = balance.plus(value); } else { await this.props.blockchain.convertWrappedEthTokensToEthAsync(value); const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals); - this.props.dispatcher.showFlashMessage(`Successfully converted ${tokenAmount.toString()} WETH to ETH`); + this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`); balance = balance.minus(value); } - this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + if (!this.props.isOutdatedWrappedEther) { + this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + } + this.props.onConversionSuccessful(); } catch (err) { const errMsg = '' + err; if (_.includes(errMsg, BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES)) { @@ -90,7 +110,10 @@ export class EthWethConversionButton extends utils.consoleLog(`Unexpected error encountered: ${err}`); utils.consoleLog(err.stack); await errorReporter.reportAsync(err); - this.props.onError(); + const errorMsg = direction === Side.deposit ? + 'Failed to wrap your ETH. Please try again.' : + 'Failed to unwrap your WETH. Please try again.'; + this.props.dispatcher.showFlashMessage(errorMsg); } } this.setState({ diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx new file mode 100644 index 000000000..ccbed4188 --- /dev/null +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -0,0 +1,360 @@ +import {ZeroEx} from '0x.js'; +import BigNumber from 'bignumber.js'; +import * as _ from 'lodash'; +import Divider from 'material-ui/Divider'; +import Paper from 'material-ui/Paper'; +import RaisedButton from 'material-ui/RaisedButton'; +import {colors} from 'material-ui/styles'; +import { + Table, + TableBody, + TableHeader, + TableHeaderColumn, + TableRow, + TableRowColumn, +} from 'material-ui/Table'; +import * as moment from 'moment'; +import * as React from 'react'; +import {Blockchain} from 'ts/blockchain'; +import {EthWethConversionButton} from 'ts/components/eth_weth_conversion_button'; +import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button'; +import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import { + OutdatedWrappedEtherByNetworkId, + Side, + Token, + TokenByAddress, + TokenState, + TokenStateByAddress, +} from 'ts/types'; +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 PRECISION = 5; +const DATE_FORMAT = 'D/M/YY'; +const LIGHT_GRAY = '#A5A5A5'; +const ICON_DIMENSION = 40; +const ETHER_ICON_PATH = '/images/ether.png'; +const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png'; +const ETHER_TOKEN_SYMBOL = 'WETH'; + +interface OutdatedWETHAddressToIsStateLoaded { + [address: string]: boolean; +} +interface OutdatedWETHStateByAddress { + [address: string]: TokenState; +} + +interface EthWrappersProps { + networkId: number; + blockchain: Blockchain; + dispatcher: Dispatcher; + tokenByAddress: TokenByAddress; + tokenStateByAddress: TokenStateByAddress; + userAddress: string; + userEtherBalance: BigNumber; +} + +interface EthWrappersState { + outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded; + outdatedWETHStateByAddress: OutdatedWETHStateByAddress; +} + +export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> { + constructor(props: EthWrappersProps) { + super(props); + const outdatedWETHAddresses = this.getOutdatedWETHAddresses(); + const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; + const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; + _.each(outdatedWETHAddresses, outdatedWETHAddress => { + outdatedWETHAddressToIsStateLoaded[outdatedWETHAddress] = false; + outdatedWETHStateByAddress[outdatedWETHAddress] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + }; + }); + this.state = { + outdatedWETHAddressToIsStateLoaded, + outdatedWETHStateByAddress, + }; + } + public componentDidMount() { + window.scrollTo(0, 0); + // tslint:disable-next-line:no-floating-promises + this.fetchOutdatedWETHStateAsync(); + } + public render() { + const tokens = _.values(this.props.tokenByAddress); + const etherToken = _.find(tokens, {symbol: 'WETH'}); + const etherTokenState = this.props.tokenStateByAddress[etherToken.address]; + const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, 18); + const isBidirectional = true; + return ( + <div className="clearfix lg-px4 md-px4 sm-px2" style={{minHeight: 600}}> + <div className="relative"> + <h3>ETH Wrapper</h3> + <div className="absolute" style={{top: 0, right: 0}}> + <a + target="_blank" + href="https://weth.io/" + style={{color: LIGHT_GRAY}} + > + <div className="flex"> + <div>About Wrapped ETH</div> + <div className="pl1"> + <i className="zmdi zmdi-open-in-new" /> + </div> + </div> + </a> + </div> + </div> + <Divider /> + <div> + <div className="py2"> + Wrap ETH into an ERC20-compliant Ether token. 1 ETH = 1 WETH. + </div> + <div> + <Table + selectable={false} + style={{backgroundColor: colors.grey50}} + > + <TableHeader displaySelectAll={false} adjustForCheckbox={false}> + <TableRow> + <TableHeaderColumn>ETH Token</TableHeaderColumn> + <TableHeaderColumn>Balance</TableHeaderColumn> + <TableHeaderColumn className="center"> + {this.renderActionColumnTitle(isBidirectional)} + </TableHeaderColumn> + </TableRow> + </TableHeader> + <TableBody displayRowCheckbox={false}> + <TableRow key="ETH"> + <TableRowColumn className="py1"> + <div className="flex"> + <img + style={{width: ICON_DIMENSION, height: ICON_DIMENSION}} + src={ETHER_ICON_PATH} + /> + <div className="mt2 ml2 sm-hide xs-hide"> + Ether + </div> + </div> + </TableRowColumn> + <TableRowColumn> + {this.props.userEtherBalance.toFixed(PRECISION)} ETH + </TableRowColumn> + <TableRowColumn> + <EthWethConversionButton + isOutdatedWrappedEther={false} + direction={Side.deposit} + ethToken={etherToken} + ethTokenState={etherTokenState} + dispatcher={this.props.dispatcher} + blockchain={this.props.blockchain} + userEtherBalance={this.props.userEtherBalance} + /> + </TableRowColumn> + </TableRow> + <TableRow key="WETH"> + <TableRowColumn className="py1"> + <div className="flex"> + <img + style={{width: ICON_DIMENSION, height: ICON_DIMENSION}} + src={constants.iconUrlBySymbol.WETH} + /> + <div className="mt2 ml2 sm-hide xs-hide"> + Wrapped Ether + </div> + </div> + </TableRowColumn> + <TableRowColumn> + {wethBalance.toFixed(PRECISION)} WETH + </TableRowColumn> + <TableRowColumn> + <EthWethConversionButton + isOutdatedWrappedEther={false} + direction={Side.receive} + ethToken={etherToken} + ethTokenState={etherTokenState} + dispatcher={this.props.dispatcher} + blockchain={this.props.blockchain} + userEtherBalance={this.props.userEtherBalance} + /> + </TableRowColumn> + </TableRow> + </TableBody> + </Table> + </div> + </div> + <div> + <h4>Outdated WETH</h4> + <Divider /> + <div className="pt2" style={{lineHeight: 1.5}}> + The{' '} + <a + href="https://blog.0xproject.com/canonical-weth-a9aa7d0279dd" + target="_blank" + > + canonical WETH + </a> contract is updated when necessary. + Unwrap outdated WETH in order to
retrieve your ETH and move it + to the updated WETH token. + </div> + <div> + <Table + selectable={false} + style={{backgroundColor: colors.grey50}} + > + <TableHeader displaySelectAll={false} adjustForCheckbox={false}> + <TableRow> + <TableHeaderColumn>WETH Version</TableHeaderColumn> + <TableHeaderColumn>Balance</TableHeaderColumn> + <TableHeaderColumn className="center"> + {this.renderActionColumnTitle(!isBidirectional)} + </TableHeaderColumn> + </TableRow> + </TableHeader> + <TableBody displayRowCheckbox={false}> + {this.renderOutdatedWeths(etherToken, etherTokenState)} + </TableBody> + </Table> + </div> + </div> + </div> + ); + } + private renderActionColumnTitle(isBidirectional: boolean) { + let iconClass = 'zmdi-long-arrow-right'; + let leftSymbol = 'WETH'; + let rightSymbol = 'ETH'; + if (isBidirectional) { + iconClass = 'zmdi-swap'; + leftSymbol = 'ETH'; + rightSymbol = 'WETH'; + } + return ( + <div className="flex mx-auto" style={{width: 85}}> + <div style={{paddingTop: 3}}>{leftSymbol}</div> + <div className="px1"> + <i + style={{fontSize: 18}} + className={`zmdi ${iconClass}`} + /> + </div> + <div style={{paddingTop: 3}}>{rightSymbol}</div> + </div> + ); + } + private renderOutdatedWeths(etherToken: Token, etherTokenState: TokenState) { + const rows = _.map(configs.outdatedWrappedEthers, + (outdatedWETHByNetworkId: OutdatedWrappedEtherByNetworkId) => { + const outdatedWETH = outdatedWETHByNetworkId[this.props.networkId]; + const timestampMsRange = outdatedWETH.timestampMsRange; + let dateRange: string; + if (!_.isUndefined(timestampMsRange)) { + const startMoment = moment(timestampMsRange.startTimestampMs); + const endMoment = moment(timestampMsRange.endTimestampMs); + dateRange = `${startMoment.format(DATE_FORMAT)}-${endMoment.format(DATE_FORMAT)}`; + } else { + dateRange = '-'; + } + const outdatedEtherToken = { + ...etherToken, + address: outdatedWETH.address, + }; + const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETH.address]; + const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETH.address]; + const balanceInEthIfExists = isStateLoaded ? + ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, 18).toFixed(PRECISION) : + undefined; + const onConversionSuccessful = this.onOutdatedConversionSuccessfulAsync.bind(this, outdatedWETH.address); + return ( + <TableRow key={`weth-${outdatedWETH.address}`}> + <TableRowColumn className="py1"> + <div className="flex"> + <img + style={{width: ICON_DIMENSION, height: ICON_DIMENSION}} + src={OUTDATED_WETH_ICON_PATH} + /> + <div className="mt2 ml2 sm-hide xs-hide"> + {dateRange} + </div> + </div> + </TableRowColumn> + <TableRowColumn> + {isStateLoaded ? + `${balanceInEthIfExists} WETH` : + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + } + </TableRowColumn> + <TableRowColumn> + <EthWethConversionButton + isDisabled={!isStateLoaded} + isOutdatedWrappedEther={true} + direction={Side.receive} + ethToken={outdatedEtherToken} + ethTokenState={outdatedEtherTokenState} + dispatcher={this.props.dispatcher} + blockchain={this.props.blockchain} + userEtherBalance={this.props.userEtherBalance} + onConversionSuccessful={onConversionSuccessful} + /> + </TableRowColumn> + </TableRow> + ); + }); + return rows; + } + private async onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) { + this.setState({ + outdatedWETHAddressToIsStateLoaded: { + ...this.state.outdatedWETHAddressToIsStateLoaded, + [outdatedWETHAddress]: false, + }, + }); + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, outdatedWETHAddress, + ); + this.setState({ + outdatedWETHAddressToIsStateLoaded: { + ...this.state.outdatedWETHAddressToIsStateLoaded, + [outdatedWETHAddress]: true, + }, + outdatedWETHStateByAddress: { + ...this.state.outdatedWETHStateByAddress, + [outdatedWETHAddress]: { + balance, + allowance, + }, + }, + }); + } + private async fetchOutdatedWETHStateAsync() { + const outdatedWETHAddresses = this.getOutdatedWETHAddresses(); + const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; + const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; + for (const address of outdatedWETHAddresses) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, address, + ); + outdatedWETHStateByAddress[address] = { + balance, + allowance, + }; + outdatedWETHAddressToIsStateLoaded[address] = true; + } + this.setState({ + outdatedWETHStateByAddress, + outdatedWETHAddressToIsStateLoaded, + }); + } + private getOutdatedWETHAddresses(): string[] { + const outdatedWETHAddresses = _.map(configs.outdatedWrappedEthers, outdatedWrappedEther => { + return outdatedWrappedEther[this.props.networkId].address; + }); + return outdatedWETHAddresses; + } +} // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx index f39341a4f..1ae9bc85e 100644 --- a/packages/website/ts/components/inputs/token_amount_input.tsx +++ b/packages/website/ts/components/inputs/token_amount_input.tsx @@ -8,9 +8,9 @@ import {BalanceBoundedInput} from 'ts/components/inputs/balance_bounded_input'; import {InputErrMsg, Token, TokenState, ValidatedBigNumberCallback, WebsitePaths} from 'ts/types'; interface TokenAmountInputProps { - label: string; token: Token; tokenState: TokenState; + label?: string; amount?: BigNumber; shouldShowIncompleteErrs: boolean; shouldCheckBalance: boolean; @@ -26,8 +26,9 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok const amount = this.props.amount ? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) : undefined; + const hasLabel = !_.isUndefined(this.props.label); return ( - <div className="flex overflow-hidden" style={{height: 84}}> + <div className="flex overflow-hidden" style={{height: hasLabel ? 84 : 62}}> <BalanceBoundedInput label={this.props.label} amount={amount} @@ -38,7 +39,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs} onVisitBalancesPageClick={this.props.onVisitBalancesPageClick} /> - <div style={{paddingTop: 39}}> + <div style={{paddingTop: hasLabel ? 39 : 14}}> {this.props.token.symbol} </div> </div> diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index 90549e0e3..74b1bdec6 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/portal.tsx @@ -8,6 +8,7 @@ import {Route, Switch} from 'react-router-dom'; import {Blockchain} from 'ts/blockchain'; import {BlockchainErrDialog} from 'ts/components/dialogs/blockchain_err_dialog'; import {PortalDisclaimerDialog} from 'ts/components/dialogs/portal_disclaimer_dialog'; +import {EthWrappers} from 'ts/components/eth_wrappers'; import {FillOrder} from 'ts/components/fill_order'; import {Footer} from 'ts/components/footer'; import {PortalMenu} from 'ts/components/portal_menu'; @@ -179,6 +180,10 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { {this.props.blockchainIsLoaded ? <Switch> <Route + path={`${WebsitePaths.Portal}/weth`} + render={this.renderEthWrapper.bind(this)} + /> + <Route path={`${WebsitePaths.Portal}/fill`} render={this.renderFillOrder.bind(this)} /> @@ -223,6 +228,19 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { </div> ); } + private renderEthWrapper() { + return ( + <EthWrappers + networkId={this.props.networkId} + blockchain={this.blockchain} + dispatcher={this.props.dispatcher} + tokenByAddress={this.props.tokenByAddress} + tokenStateByAddress={this.props.tokenStateByAddress} + userAddress={this.props.userAddress} + userEtherBalance={this.props.userEtherBalance} + /> + ); + } private renderTradeHistory() { return ( <TradeHistory diff --git a/packages/website/ts/components/portal_menu.tsx b/packages/website/ts/components/portal_menu.tsx index 7b9a31a15..822c4d0d7 100644 --- a/packages/website/ts/components/portal_menu.tsx +++ b/packages/website/ts/components/portal_menu.tsx @@ -49,6 +49,14 @@ export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState > {this.renderMenuItemWithIcon('Trade history', 'zmdi-format-list-bulleted')} </MenuItem> + <MenuItem + style={this.props.menuItemStyle} + className="py2" + to={`${WebsitePaths.Portal}/weth`} + onClick={this.props.onClick.bind(this)} + > + {this.renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')} + </MenuItem> </div> ); } diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index b68081c37..ab232326a 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -22,7 +22,6 @@ import * as React from 'react'; import ReactTooltip = require('react-tooltip'); import firstBy = require('thenby'); import {Blockchain} from 'ts/blockchain'; -import {EthWethConversionButton} from 'ts/components/eth_weth_conversion_button'; import {AssetPicker} from 'ts/components/generate_order/asset_picker'; import {AllowanceToggle} from 'ts/components/inputs/allowance_toggle'; import {SendButton} from 'ts/components/send_button'; @@ -424,16 +423,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala onClickAsyncFn={this.onMintTestTokensAsync.bind(this, token)} /> } - {token.symbol === ETHER_TOKEN_SYMBOL && - <EthWethConversionButton - blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} - ethToken={this.getWrappedEthToken()} - ethTokenState={tokenState} - userEtherBalance={this.props.userEtherBalance} - onError={this.onEthWethConversionFailed.bind(this)} - /> - } {token.symbol === ZRX_TOKEN_SYMBOL && this.props.networkId === constants.TESTNET_NETWORK_ID && <LifeCycleRaisedButton labelReady="Request" @@ -487,11 +476,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala isTokenPickerOpen: false, }); } - private onEthWethConversionFailed() { - this.setState({ - errorType: BalanceErrs.wethConversionFailed, - }); - } private onSendFailed() { this.setState({ errorType: BalanceErrs.sendFailed, @@ -550,14 +534,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala </div> ); - case BalanceErrs.wethConversionFailed: - return ( - <div> - Converting between Ether and Ether Tokens failed unexpectedly. - Please refresh the page and try again. - </div> - ); - case BalanceErrs.allowanceSettingFailed: return ( <div> diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index cbefac8bd..71565fa4d 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -10,6 +10,7 @@ import {BrowserRouter as Router, Redirect, Route, Switch} from 'react-router-dom import * as injectTapEventPlugin from 'react-tap-event-plugin'; import {createStore, Store as ReduxStore} from 'redux'; import {createLazyComponent} from 'ts/lazy_component'; +import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage'; import {About} from 'ts/pages/about/about'; import {FAQ} from 'ts/pages/faq/faq'; @@ -29,6 +30,7 @@ BigNumber.config({ // Check if we've introduced an update that requires us to clear the tradeHistory local storage entries tradeHistoryStorage.clearIfRequired(); +trackedTokenStorage.clearIfRequired(); const CUSTOM_GREY = 'rgb(39, 39, 39)'; const CUSTOM_GREEN = 'rgb(102, 222, 117)'; diff --git a/packages/website/ts/local_storage/tracked_token_storage.ts b/packages/website/ts/local_storage/tracked_token_storage.ts index 051a78ae1..086b06af5 100644 --- a/packages/website/ts/local_storage/tracked_token_storage.ts +++ b/packages/website/ts/local_storage/tracked_token_storage.ts @@ -1,10 +1,21 @@ import * as _ from 'lodash'; import {localStorage} from 'ts/local_storage/local_storage'; import {Token, TrackedTokensByNetworkId} from 'ts/types'; +import {configs} from 'ts/utils/configs'; const TRACKED_TOKENS_KEY = 'trackedTokens'; +const TRACKED_TOKENS_CLEAR_KEY = 'lastClearTrackedTokensDate'; export const trackedTokenStorage = { + // Clear trackedTokens localStorage if we've updated the config variable in an update + // that introduced a backward incompatible change requiring the tracked tokens to be re-set + clearIfRequired() { + const lastClearFillDate = localStorage.getItemIfExists(TRACKED_TOKENS_CLEAR_KEY); + if (lastClearFillDate !== configs.lastLocalStorageTrackedTokenClearanceDate) { + localStorage.removeItem(TRACKED_TOKENS_KEY); + } + localStorage.setItem(TRACKED_TOKENS_CLEAR_KEY, configs.lastLocalStorageTrackedTokenClearanceDate); + }, addTrackedTokenToUser(userAddress: string, networkId: number, token: Token) { const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress(); let trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress]; diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index d225e7784..2005cf265 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -135,7 +135,6 @@ export enum BalanceErrs { faucetRequestFailed, faucetQueueIsFull, mintingFailed, - wethConversionFailed, sendFailed, allowanceSettingFailed, } @@ -683,4 +682,16 @@ export interface DocsInfoConfig { menuSubsectionToVersionWhenIntroduced?: {[sectionName: string]: string}; } +export interface TimestampMsRange { + startTimestampMs: number; + endTimestampMs: number; +} + +export interface OutdatedWrappedEtherByNetworkId { + [networkId: number]: { + address: string; + timestampMsRange: TimestampMsRange; + }; +} + // tslint:disable:max-file-line-count diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index 63fcd27b6..adc51374f 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -1,5 +1,9 @@ import * as _ from 'lodash'; -import {Environments} from 'ts/types'; +import { + Environments, + OutdatedWrappedEtherByNetworkId, + TimestampMsRange, +} from 'ts/types'; const BASE_URL = window.location.origin; const isDevelopment = _.includes(BASE_URL, 'https://0xproject.dev:3572') || @@ -14,5 +18,24 @@ export const configs = { // WARNING: ZRX & WETH MUST always be default trackedTokens defaultTrackedTokenSymbols: ['WETH', 'ZRX'], lastLocalStorageFillClearanceDate: '2017-11-22', + lastLocalStorageTrackedTokenClearanceDate: '2017-12-13', isMainnetEnabled: true, + outdatedWrappedEthers: [ + { + 42: { + address: '0x05d090b51c40b020eab3bfcb6a2dff130df22e9c', + timestampMsRange: { + startTimestampMs: 1501614680000, + endTimestampMs: 1513106129000, + }, + }, + 1: { + address: '0x2956356cd2a2bf3202f771f50d3d14a367b48070', + timestampMsRange: { + startTimestampMs: 1513123415000, + endTimestampMs: 1513106129000, + }, + }, + }, + ] as OutdatedWrappedEtherByNetworkId[], }; |